HelmでKubernetesのマニフェストファイルを管理する

Helmを使って公開されているChartをインストール方法はたくさん紹介されていますが、独自のChartを作成する方法が簡潔に紹介されている記事が無かったので紹介します。

Helm自体の紹介やインストール方法などは省略します。

環境

Chartの初期化

$ helm create example

で必要となるファイル一式が作成されます。
中身は以下のようになっています。

$ tree example
example
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

3 directories, 9 files

このようなファイルが作成されますが、使わないファイルが多いので以下のファイル以外を削除します。

  • example/Chart.yaml
  • example/templates/deployment.yaml
  • example/templates/_helpers.tpl
  • example/values.yaml

テンプレートの内容

example/templates 配下がテンプレートが配置されるディレクトリになります。 deployment.yaml の中を見てみます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "example.fullname" . }}
  labels:
    {{- include "example.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "example.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "example.selectorLabels" . | nindent 8 }}
    spec:
    {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      serviceAccountName: {{ include "example.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
    {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
    {{- end }}
    {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
    {{- end }}

これに変数が代入されてマニフェストファイルが生成されます。

マニフェストファイルの生成

helm template コマンドでマニフェストファイルを生成します。

$ helm template ./example --name-template example --namespace default
---
# Source: example/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example_template
  labels:
    helm.sh/chart: example-0.1.0
    app.kubernetes.io/name: example
    app.kubernetes.io/instance: example_template
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: example
      app.kubernetes.io/instance: example_template
  template:
    metadata:
      labels:
        app.kubernetes.io/name: example
        app.kubernetes.io/instance: example_template
    spec:
      serviceAccountName: example_template
      securityContext:
        {}
      containers:
        - name: example
          securityContext:
            {}
          image: "nginx:1.16.0"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {}

テンプレートの変数

テンプレートに代入された変数がどこに定義されたものか紹介します。

  • .Values
    • values.yamlで宣言されているものを.つなぎで指定し読み込みます
  • include
    • _helpers.tpldefine "変数名"と宣言されているものが読み込まれます
  • .Chart
    • Chart.yaml`に書かれた値を読み込んでいます
  • .Release
    • 生成時に引数として渡されたものが読み込まれます

その他の変数や関数はなんとなく雰囲気で理解できそうですが、以下の公式ドキュメントに書かれています。

helm.sh

helm.sh

helm.sh

インストール

生成したマニフェストファイルをパイプでkubectlに渡せばOKです。

$ helm template ./example --name-template example --namespace default | kubectl apply -f -

また、helmが無い環境でもインストールするためにyamlファイルとして保存してしまうのも1つのやり方だと思います。

$ helm template ./example --name-template example --namespace default > bundle.yaml

fastlaneのpilotでThis request is forbidden for security reasons - The API key in use does not allow this requestとエラーになったときの対処法

環境

fastlane 2.131.0

エラー内容

Spaceship::UnexpectedResponse: [!] This request is forbidden for security reasons - The API key in use does not allow this request
  /Users/travis/build/xxx/xxx/vendor/bundle/ruby/2.4.0/gems/fastlane-2.131.0/spaceship/lib/spaceship/connect_api/client.rb:134:in `handle_response'
  /Users/travis/build/xxx/xxx/vendor/bundle/ruby/2.4.0/gems/fastlane-2.131.0/spaceship/lib/spaceship/connect_api/client.rb:105:in `patch'
  /Users/travis/build/xxx/xxx/vendor/bundle/ruby/2.4.0/gems/fastlane-2.131.0/spaceship/lib/spaceship/connect_api/testflight/testflight.rb:78:in `patch_beta_app_review_detail'

対応策

TestFlightへアップロードするアカウントの権限をapp manager以上の権限にすることで解決します。
developerだとこのエラーになります。

備考

2FAの対応でアップロード用のアカウントを作ったときにこの問題に遭遇することが多いようです。

以下のissueでdeveloper権限でこの問題を回避する方法が望まれていますが、まだ回避策は無いようです。

github.com

gRPCサーバーのデバッグとServer Reflection

gRPCはRPCの1つでProtocol Buffersでシリアライズ化することで高速な通信を実現させています。そのため、通常のREST-likeなWEB APIデバッグでつかうcurlやPostmanのようなGUIツールでデバッグすることができません。

そこで今回はgRPCで開発するときに役に立つデバッグの方法を紹介します。

CLIクライアント

一番基本となるデバッグ方法は自由にリクエストを送ることだと思います。
以下のようにいくつかCLIからgRPCのリクエストを送ることができるツールがあります。

公式が提供しているツールを使うのもいいですが、curl-likeなほうが使い慣れた人が多いと思うのでgrpcurlがオススメです。
このようにしてリクエストを送ることができます。

$ grpcurl -plaintext -import-path ./api -proto route_guide.proto -d '{"latitude": 10, "longitude": 20}' localhost:10000 RouteGuide.GetFeature

Server Reflection

gRPCのはデバッグ用の機能としてServer Reflection Protocolというものがあります。

通常は上記の例のように、リクエストを送ってレスポンスを表示するには関連する.protoファイルを指定する必要があります。
しかし、Server Reflection Protocolを使えばサーバーから必要な情報を取得することができ、.protoファイルが無くても実行することができます。

この機能を使うにはサーバーにReflectionの設定をする必要がありますが、数行に追加で済みます。

import (
    "google.golang.org/grpc"
+   "google.golang.org/grpc/reflection"
)

    grpcServer := grpc.NewServer()
+   reflection.Register(grpcServer)
    ...
    _ = grpcServer.Serve(lis)

これでServer Reflectionの対応は完了です。

また、Server Reflectionに対応することで以下のようにサーバーに実装されているサービスの一覧や詳細も.protoファイルなしで確認できるようになります。
とくにサービスの一覧の表示は途中から開発に参加したプロジェクトなどではとても役に立つと思います。

$ grpcurl -plaintext localhost:10000 list
RouteGuide
grpc.reflection.v1alpha.ServerReflection
$ grpcurl -plaintext localhost:10000 list RouteGuide
RouteGuide.GetFeature
$ grpcurl -plaintext localhost:10000 describe RouteGuide.GetFeature
RouteGuide.GetFeature is a method:
rpc GetFeature ( .Point ) returns ( .Feature );

環境変数

サーバー側のデバッグ情報として環境変数を設定することで追加のデバッグ情報を出力することができます。
これは各言語の実装により指定する項目が変わるので注意する必要があります。

Go

Goでは以下のように環境変数を設定すれば追加のログが出力されます。

$ export GRPC_GO_LOG_VERBOSITY_LEVEL=99
$ export GRPC_GO_LOG_SEVERITY_LEVEL=info

ドキュメントにも少しだけ記述があります。

godoc.org

また、Goの場合はGo自体に実装されたhttp2のデバッグ機能を使うのもオススメです。
こちらはフレームの内容なども出力されるため、とても役に立つと思います。

$ export GODEBUG=http2debug=2
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: wrote SETTINGS len=0
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: read SETTINGS len=0
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: wrote SETTINGS flags=ACK len=0
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: read SETTINGS flags=ACK len=0
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: read HEADERS flags=END_HEADERS stream=1 len=68
2019/09/15 21:41:14 http2: decoded hpack field header field ":method" = "POST"
2019/09/15 21:41:14 http2: decoded hpack field header field ":scheme" = "http"
2019/09/15 21:41:14 http2: decoded hpack field header field ":path" = "/RouteGuide/GetFeature"
2019/09/15 21:41:14 http2: decoded hpack field header field ":authority" = "localhost:10000"
2019/09/15 21:41:14 http2: decoded hpack field header field "content-type" = "application/grpc"
2019/09/15 21:41:14 http2: decoded hpack field header field "user-agent" = "grpc-go/1.21.0"
2019/09/15 21:41:14 http2: decoded hpack field header field "te" = "trailers"
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: read DATA flags=END_STREAM stream=1 len=9 data="\x00\x00\x00\x00\x04\b\n\x10\x14"
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: wrote WINDOW_UPDATE len=4 (conn) incr=9
2019/09/15 21:41:14 http2: Framer 0xc0003001c0: wrote PING len=8 ping="\x02\x04\x10\x10\t\x0e\a\a"

Cベースの実装

Cをベースとした言語(C++, Python, Ruby, Objective-C, PHP, C#)の実装では以下のような環境変数を設定することで追加のログが出力できます。

$ export GRPC_VERBOSITY=DEBUG
$ export GRPC_TRACE=all

Cベースの場合は他にも設定できる項目があり、以下のドキュメントに書かれています。

github.com

また、これらの環境変数はgRPCを内部で使用しているSDKなどでも有効です。
特にGoogleSDKではgRPCがたくさん使われているため、それらのデバッグでも有効活用できると思います。

Digdagのbackfillを無効化する

Digdagでスケジュール実行の設定をしているワークフローを一時停止したあとに停止を解除した場合は、停止していた期間に実行されるはずだったセッションを実行するBackfillという機能があります。
GUIでPAUSEした後にRESUMEしたときや、CLIでのenable/disableを実行したときが該当します。

この機能はデフォルトで有効になっているため、冪等性が保たれていないジョブなどを一時停止した場合はBackfillを無効にする必要があります。

Backfillを無効にする方法として、digdag rescheduleを実行する方法とマニフェストファイルでskip_delayed_byを設定する方法があります。

rescheduleコマンドを使う方法

rescheduleコマンドでスケジュールを更新して現時刻よりも前にスケジューリングされたセッションをスキップするように設定します。

まずはBackfillを無効にしたいワークフローのIDを取得します。
CLIからは以下のコマンドで取得できます。

$ digdag schedules

取得したIDで以下のコマンドを実行すればBackfillを無効にできます。

$ digdag reschedule <schedule id> --skip-to "$(date '+%Y-%m-%d %H:%M:%S %z')"

このやり方で注意しなければいけないポイントとして、Backfillが無効になるのは今回の一時停止で実行されなかったセッションだけであるところです。
後日また一時停止したときにもBackfillを無効にする場合は再度rescheduleコマンドを実行する必要があります。

冪等性が無い場合など、毎回Backfillを無効にしたい場合は2つめのやり方で設定できます。

skip_delayed_byを設定する方法

マニフェストファイルのscheduleskip_delayed_byを設定することで、指定された期間よりも以前のセッションをスキップするように設定できます。
例としては以下のようになります。

schedule:
  skip_delayed_by: 3m

+setup:
  ...

この例の場合は一時停止を解除した時刻の3mよりも前のセッションを無効にします。
つまり、この設定を1sとかにしておけばBackfillを完全に無効にできます。

ワークフローの内容によりBackfillが実行されると不都合がある場合は、この設定を入れておくことをオススメします。

Go ModulesなプロジェクトのDockerビルドを高速化する

Go Modulesに対応させたプロジェクトをDocker内でビルドして実行するとします。
単純にDockerfileを書いてしまうとソースコードに変更が入るたびにModulesのダウンロードが走ってしまい、とても時間がかかってしまいます。
そこでDockerのイメージレイヤのキャッシュ機能を使って高速化する方法を紹介します。

Go Modulesのキャッシュ

キャッシュが効かずに毎回Modulesのダウンロードが行われる原因としては、Docker内でGoのビルドをするためにソースコードをコピーしている COPY コマンド部分が、前回の状態と異なるためキャッシュを使うことができないところにあります。
そのため、ソースコードをコピーする前にModulesのダウンロードを行えば、ソースコードの変更を行っても前回のキャッシュが使え、大幅な短縮になります。

以下が一例です。

FROM golang:1.12.4 as build

ENV GO111MODULE=on
WORKDIR /go/src/path/to/proj

COPY go.mod .
COPY go.sum .

RUN set -x \
  && go mod download

COPY . .

RUN set -x \
  && go build -o /go/bin/app

FROM ubuntu:18.04

COPY --from=build /go/bin/app /go/bin/app

CMD ["/go/bin/app"]

ソースコード全体をコピーする前にModules関連だけをコピーして、 go mod download を実行してダウンロードしています。
これでModulesに変更が入らない限り COPY . . から実行されるため、大幅な短縮になります。

KubernetesのPodでDockerコンテナを実行する

KubernetesのPodでDockerコンテナを実行する方法を紹介します。

Dockerコンテナ内でコンテナを起動する方法のことをDinD (Docker inside Docker)などと言われていますが、同じことをKubernetesのPodで実行するには少し工夫する必要があります。
アプローチとして2種類のやり方があり、双方にメリットとデメリットがあるため状況に合わせて適切なものを選択する必要があります。

アプローチ

1つめのやり方はPodが動いているホストマシンのDockerデーモンを共有する方法で、DooD (Docker outside of Docker)とよばれるものをPodに適応させたパターンです。
2つめのやり方はPod内でDockerデーモンを動かす、DinD (Docker inside Docker)と呼ばれるものを適応させたパターンです。

それぞれの方法のやり方とメリット・デメリットについて紹介します。

DooDパターン

DooDパターンをk8sに適応させるのは簡単で、ホストの /var/run を共有してDockerデーモンにアクセスできるようにするだけです。

k8sマニフェストファイルで表すとこのようになります。

apiVersion: v1
kind: Pod
metadata:
  name: dood
spec:
  containers:
    - name: docker
      image: docker:19.03
      command: ["docker", "run", "nginx:latest"]
      volumeMounts:
        - mountPath: /var/run
          name: docker-sock
  volumes:
    - name: docker-sock
      hostPath:
        path: /var/run

DooDパターンの場合、コンテナ内で作成されたコンテナはホストが管理するその他のコンテナと同列の扱いになることに注意する必要があります。
つまり、コンテナを作成したPodと同列の扱いのコンテナとなります。
しかしながらPod内で作成したコンテナはk8sの管理下にはありません。
また、もし他にもコンテナを作成するPodが存在した場合は、コンテナ名などのリソースが競合する可能性があるので注意しなければいけません。
偶然発生する可能性は低いですが、k8sのリソースと競合する可能性もあります。

コンテナのポートをマッピングする場合も注意が必要で、デーモンが動いているのはホストマシンになるためポートマッピングされるのもホストになります。
そのためコンテナを作成したPodのIPで作成したコンテナにはアクセスできません。ホストのIPでアクセスする必要があります。

また、k8sの管理から外れるため、Podに指定したリソース制限が適用されません。
さらに -d としてコンテナを起動した場合はPodの削除時に作成したコンテナが削除されません。

このように様々な落とし穴があるため、k8sのPodの場合はDooDパターンはオススメしません。次に解説するDinDパターンを使うべきです。

メリット

  • 特別な権限を与える必要がない
  • コンテナを動かしたいイメージにDockerコマンドをインストールするだけで動かすことができる

デメリット

  • Podの中ではなくホストマシンの中にコンテナが作成される
  • k8sの管理下からはずれたところにコンテナが作成される
  • ホストでリソースの競合が発生する可能性がある

DinDパターン

DinDパターンはDooDパターンと比べると少しだけ複雑です。
dockerコマンドを実行するコンテナのサイドカーとしてDockerデーモンを動かし、dockerコマンドがサイドカーに対してアクセスするように設定します。

以下がその設定をしたマニフェストファイルです。

apiVersion: v1
kind: Pod
metadata:
  name: dind
spec:
  containers:
    - name: docker
      image: docker:18.09
      command: ["docker", "run", "nginx:latest"]
      env:
        - name: DOCKER_HOST
          value: tcp://localhost:2375
    - name: dind-daemon
      image: docker:18.09-dind
      resources:
        requests:
          cpu: 20m
          memory: 512Mi
      securityContext:
        privileged: true

Dockerデーモンを動かすコンテナは公式のイメージがあるのでそれを使います。この公式イメージは2375ポートでDockerのREST APIを公開しています。
そこに対してDocker CLIがアクセスするように環境変数 DOCKER_HOST を設定します。

DinDパターンでは作成したコンテナはDockerデーモンが動くサイドカーコンテナの中に作成されるため、Podに設定したCPUやメモリなどのリソースの制限を受けた状態でコンテナを作成できます。
また、このPodを削除したときに作成したコンテナも同時に削除されます。
ポートマッピングを使用したときもDockerデーモンが動いているネットワーク、つまりはPodのネットワークに対して行われるため、ホストのリソースを汚すことなくPodのIPでアクセスできます。
同様にコンテナ名などのリソース競合もPod内でしかおこりません。

ただし一つだけ意識しなければならないことがあり、Dockerデーモンが動くコンテナは privileged 権限で実行しなければいけません。ここが障害にならない限りDinDパターンを選択することをオススメします。

メリット

  • Pod内にコンテナを作成できる
  • ホストに影響を与えることなくPod内で完結する
  • Podの設定(リソースやネットワーク)を引き継ぐことができる

デメリット

  • サイドカーコンテナが1つ増える
  • privilegedでコンテナを動かす必要がある

まとめ

privilegedで動かしても問題ない環境であればDinDパターンがオススメです。
DooDを使う場合はリソース競合など、ホスト側でコンテナが動いていることを忘れずに使う必要があります。

QtでMacアプリを作ってみる

QtでMacのアプリをビルドし、.appの形式として書き出します。

環境

事前準備

使用するXcodeのCommand Line Toolsを設定します。

$ sudo xcode-select -s /Applications/Xcode10.2.1.app

Command Line Toolsが正しく設定されているか確認します。

$ xcode-select -p
/Applications/Xcode10.2.1.app/Contents/Developer

qtのインストール

Qtの公式サイトからオープンソース版をダウンロードします。

www.qt.io

ダウンロードしたら流れにそってインストールします。
ここでQt本体にチェックをつけないとDeveloper Toolsしかインストールされないのでチェックを入れます。

f:id:akaimo3:20190604211035p:plain

ダウンロードが終わったらインストール完了です。

プロジェクトの作成

インストールが完了するとQt Creatorが起動するので、New ProjectをクリックしてQt Widgets Applicationを選択します。
適当なプロジェクト名を入力すれば作成されます。

f:id:akaimo3:20190604211821p:plain

Qt Creatorでビルドを実行するとこのような画面が作られます。

f:id:akaimo3:20190604212046p:plain

アプリ(.app)化する

Qt Creatorからはアプリを実行できたので次は.appとして書き出します。

書き出すにはqmakeというツールを使うため、qmakeにパスを通します。
.bashrc.zshrcなどに以下を追加します。

export PATH="$HOME/Qt/5.12.3/clang_64/bin:$PATH"

プロジェクトのあるディレクトリに移動して以下のコマンドを実行すれば、プロジェクトのあるディレクトリに.appが作られます。

$ qmake -project
$ qmake
$ make

この環境ではmakeを実行するときにエラーになりました。
.proファイルに以下を追加するとmakeが動くようになります。

QT += widgets

参考

Getting Started on the Commandline/ja - Qt Wiki

c++ - Static QT Compilation problems - Stack Overflow