Railsアプリ開発のためのDocker/Kubernetes入門6 Helm編

Table of Contents

  1. 1. 前提
  2. 2. 準備
  3. 3. Helmとは
  4. 4. Helmをk8sにインストールする方法
  5. 5. Helmによるパッケージ管理のチュートリアル
    1. 5.1. helm update repo: リポジトリ情報の更新
    2. 5.2. helm search: Chartの検索
    3. 5.3. helm inspect: Chartの詳細情報の表示
    4. 5.4. helm install: Chartのインストール(デプロイ)
    5. 5.5. helm list: クラスタ上のChartインスタンス(Release)の一覧を表示
    6. 5.6. helm upgrade: Chartの更新のデプロイ
    7. 5.7. helm delete: ReleaseとAPIオブジェクトの削除
  6. 6. NamespaceとContextについて
    1. 6.1. kubectl config get-contexts: Contextの一覧の表示
    2. 6.2. kubectl config current-context: 現在有効になっているContext名の表示
    3. 6.3. kubectl config view: Contextの詳細の表示
    4. 6.4. kubectl config set-context: Contextの作成または更新
    5. 6.5. kubectl config use-context: 有効なContextを変更
    6. 6.6. kubectl config delete-context: Contextの削除
    7. 6.7. kubectxとkubens
    8. 6.8. Namespaceの削除について
  7. 7. Chartの作り方
    1. 7.1. helm upgrade –install: 作ったChartのデプロイ
    2. 7.2. helm template: テンプレートのレンダリング
    3. 7.3. helm create: 新しいChartの作成
    4. 7.4. テンプレート
      1. 7.4.1. パラメータ化
      2. 7.4.2. オブジェクトの名前
      3. 7.4.3. ラベル
      4. 7.4.4. アノテーション
      5. 7.4.5. 関数とパイプライン
      6. 7.4.6. フロー制御
    5. 7.5. values.yaml
    6. 7.6. values.yamlに含まれる秘密情報の暗号化
      1. 7.6.1. GCP KMSの準備
      2. 7.6.2. yaml_vault
      3. 7.6.3. sops
    7. 7.7. Subchart
    8. 7.8. まとめ
  8. 8. 自作のChartによるワークフロー
    1. 8.1. helm upgrade: Chartの更新を反映
    2. 8.2. helm history: Releaseの履歴を表示
    3. 8.3. helm rollback: Releaseを前のリビジョンに戻す
    4. 8.4. helm get: Releaseの詳細を表示
    5. 8.5. helm diff: Chartの差分を表示
  9. 9. おわりに

RailsアプリケーションをKubernetes(以後、k8s)で運用できるようにするための手順を書きます。 この記事はシリーズ連載記事の第六回です。

はじめにHelmについて簡単に紹介した後、 前回の Kubernetes応用編 で作成したマニフェストをもとにHelmチャートの作り方を説明します。

サンプルコードは全て下記のリポジトリにあります。

https://github.com/kwhrtsk/rails-k8s-demoapp

前提

  • macOSでの作業を前提としています。
  • 使用したツールのバージョンなどは 初回 の記事を参照してください。
  • ツールのインストール手順は 第三回 の記事を参照してください。

準備

まずサンプルアプリのコードをチェックアウトしてminikubeを起動してください。 前回とほぼ同じですが、minikubeのメモリを3GBに増やしています。 これは2GBだと後述するSentryの起動に失敗するためです。 すでに2GBで作っている場合はminikubeインスタンスを作り直すか、 Sentryを使ったhelmコマンドのチュートリアルを飛ばしてください。 Railsのサンプルアプリは2GBでも動作します。

また、今回は初めからIngressアドオンを有効にしています。 minikubeインスタンスを削除した場合は忘れずにもう一度有効にしてください。

# サンプルアプリのコードをチェックアウト
$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git
$ cd rails-k8s-demoapp

# minikubeを起動
$ minikube start --cpus=3 --memory=3072 --vm-driver=hyperkit --disk-size=12g

# Ingressアドオンをインストール
$ minikube addons enable ingress

# kubernetesのダッシュボードをオープン
$ minikube dashboard

Helmとは

Helm - The Kubernetes Package Manager

アプリケーションに必要なマニフェストファイル一式をパッケージ化して管理するためのツールです。 これを使うと必要なコンテナをまとめてk8sにデプロイすることができます。 このパッケージはChartと呼ばれます。

HelmではマニフェストファイルをGoのtext/template形式でテンプレート化することができ、 イメージのタグやレプリカの数など、マニフェストの一部をパラメータ化することができます。 これらのパラメータは通常デフォルト値を持ち、Chartパッケージをk8sにデプロイ(インストール)する際に上書きできます。

Helmでは公式リポジトリで配布されているChartをk8sクラスタにデプロイすることもできるし、 自分で開発しているプライベートなアプリのChartを作成して、Helmでk8sにデプロイすることもできます。

Helmをk8sにインストールする方法

Helmはクライアント・サーバモデルのアプリケーションです。 k8sクラスタ側にTillerと呼ばれるサーバモジュールをインストールします。 クライアントはhelmコマンドです。

minikubeの場合は下記のコマンドでTillerをインストールできます。

# インストールが完了するまでブロック
$ helm init --wait

Tillerがインストールされているかどうかは下記のコマンドで確認できます。

$ helm version
Client: &version.Version{SemVer:"v2.9.1", GitCommit:"20adb27c7c5868466912eebdf6664e7390ebe710", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.9.1", GitCommit:"20adb27c7c5868466912eebdf6664e7390ebe710", GitTreeState:"clean"}

アンインストールは下記のコマンドです。

$ helm reset

Helmによるパッケージ管理のチュートリアル

この章ではSentry というWebアプリを題材に、 Chartを使ってアプリケーションをminikubeにデプロイするデモをします。

Sentryはアプリケーションのエラー情報を収集してSlackなどに通知するためのサービスです。 SentryのWebサービスはPythonで実装されていますが、Ruby用のクライアントもgemとして配布されており、Railsにも簡単に組み込むことができます。

SaaSを利用することもできるのですが、 公式のDockerイメージが配布されており、 Dockerを使って自前で運用することもできます。

Sentryサービスの内部構成はPythonのWebアプリ、バックグラウンドジョブのためのワーカー、PostgreSQLサーバ、Redisサーバとそれなりに複雑です。 ただ、Helm Chartを使えば比較的簡単にセットアップすることが可能です。

この章ではhelmコマンドのチュートリアルとしてminikube上にSentryをセットアップする手順を示します。

helm update repo: リポジトリ情報の更新

公式のChartリポジトリはここです。

https://github.com/kubernetes/charts

このリポジトリはstableとincubatorの二つに別れています。

helmコマンドはローカルにこのリポジトリの情報をキャッシュしています。 下記のコマンドでこのキャッシュを更新することができます。

$ helm repo update
...Skip local chart repository
...Successfully got an update from the "stable" chart repository
...Successfully got an update from the "jenkins-x" chart repository
Update Complete. ⎈ Happy Helming!⎈

helm search: Chartの検索

helm searchコマンドでstableのChartを検索することができます。

$ helm search sentry
NAME            CHART VERSION   APP VERSION     DESCRIPTION
stable/sentry   0.1.14          8.17            Sentry is a cross-platform crash reporting and ...

少しややこしいですが、CHART VERSIONはマニフェストやパラメータ定義などこのChart自体のバージョンを示し、 APP VERSIONはSentryのバージョンを示しています。

パラメータなしで実行すると全てのChartを表示します。

helm inspect: Chartの詳細情報の表示

Chartのメタ情報やパラメータのデフォルト値、READMEなどを表示するコマンドがあります。

$ helm inspect stable/sentry

後述するhelm install-f--setオプションで指定できるパラメータの一覧などを確認することができます。

helm install: Chartのインストール(デプロイ)

下記のコマンドを実行すると、stable/sentryというChartをminikubeにデプロイします。

$ helm install stable/sentry \
  --name sentry \
  --namespace sentry \
  --wait \
  --timeout 600 \
  --version 0.1.14 \
  --set service.type=NodePort \
  --set ingress.enabled=true \
  --set ingress.hostname=$(minikube ip).nip.io \
  --set image.tag=8.22 \
  --set email.host="" \
  --set sentrySecret=sentry \
  --set postgresql.postgresPassword=postgres \
  --set redis.redisPassword=redis

色々オプションを指定していますが、実際のところはhelm install stable/sentryだけでも動作します。 今回は比較的よく使いそうなオプションをまとめて紹介します。

はじめに、特に重要な--nameオプションについて説明します。 k8sにデプロイされたChartのインスタンスはReleaseと呼ばれ、一意な名前を持ちます (Helmでは同じChartのReleaseを二つ以上デプロイすることができます)。 この名前を--nameオプションで指定することができます。この例ではsentryという名前をつけていますが、 本来はstagingproductionなど環境を示すような名前をつける方が良いかもしれません。

名前を指定しない場合はhappy-pandaのように辞書から単語を二つ適当に拾ってきてくっつけたみたいな名前が自動的に与えられます。 helm upgradehelm deleteなど、特定のReleaseを対象とするコマンドはこの名前で対象を指定するので、 できるだけわかりやすい名前を自分でつけるのがおすすめです。

その他のオプションの意味は次の通りです。

  • --namespaceオプションでは、このChartのAPIオブジェクトを作成するNamespacesentryにしています。Namespaceについては後述します。
  • --waitオプションをつけると、各オブジェクトの状態が準備完了になるまで待機してからコマンドを終了します。
  • --timeoutオプションは--waitのタイムアウト値(秒)です。筆者の環境ではstable/sentryの初期化に5分強かかるので、10分に拡張しています。
  • --versionオプションはインストールするChartのバージョンです。指定しないと最新版がインストールされます。
  • --setオプションはChartに定義されているパラメータを入力しています。この例では次のパラメータの値をデフォルトから変更しています。
    • service.type: Serviceオブジェクトのタイプです。デフォルトはLoadBalancerなのですが、minikubeが対応していないためNodePortに変更しています。 --waitオプションをつけている場合、LoadBalancerのままだとServiceオブジェクトの状態が準備完了にならず、コマンドがタイムアウトして失敗します。
    • ingress.enabled: デフォルトではIngressを使わない設定になっていますが、この例では有効にしています。
    • ingress.hostname: minikube ipとワイルドカードDNSを組み合わせたURLにしています。
    • image.tag: sentryイメージのタグです。Sentry自体のバージョンを意味します。デフォルトは8.17ですが、新しいバージョンが出ているので8.22に変更しています。
    • email.host: 招待メールやレポートなどメールを送信するためのSMTPサーバを指定します。空文字を指定するとメール送信機能が無効化されます。
    • sentrySecret: Sentry内部での暗号処理の鍵です。省略するとランダムな文字列が自動的に割り当てられます。*2
    • postgres.postgresPassword: PostgreSQLサーバのパスワードです。省略するとランダムな文字列が自動的に割り当てられます。*2
    • redis.redisPassword: Redisサーバのパスワードです。省略するとランダムな文字列が自動的に割り当てられます。*2

*2: これらのパラメータを省略してランダム文字列にした場合、 email.hostのような別のパラメータを更新した場合にもランダム文字列が再生成されてDBに接続できなくなるなどの問題が起きるため、 継続的に運用する予定なら明示的に設定しておくのが無難です。

--setでは見ての通り複数のパラメータを指定できます。パラメータの一覧を書いたYAMLファイルを-fオプションに渡すことでも同様のことができます。

# values-custom.yaml
service:
  type: NodePort

ingress:
  enabled: true

image:
  tag: 8.22

email:
  host: ""

sentrySecret: sentry

postgresql:
  postgresPassword: postgres

redis:
  redisPassword: redis

上記のようなYAMLファイルを用意して、-fオプションに渡します。 この例では、ingress.hostnameのみminikube ipコマンドの出力を使いたいので--setで指定しています。

$ helm install stable/sentry \
  --name sentry \
  --namespace sentry \
  --wait \
  --timeout 600 \
  --version 0.1.14 \
  --f values-custom.yaml \
  --set ingress.hostname=$(minikube ip).nip.io

このChartのソースコードは下記にあります。他にどのようなパラメータがあるかなどはここを参照してください。

kubernetes/charts | stable/sentry

結構時間がかかるので、別の端末にログを表示して進捗を確認できるようにしておくと良いでしょう。

$ stern --namespace sentry ".*"

完了すると下記のようなメッセージが表示されます。

NAME:   sentry
LAST DEPLOYED: Tue May 22 00:07:12 2018
NAMESPACE: sentry
STATUS: DEPLOYED

RESOURCES:
==> v1/Secret
NAME               TYPE    DATA  AGE
sentry-postgresql  Opaque  1     5m
sentry-redis       Opaque  1     5m
sentry-sentry      Opaque  3     5m

==> v1/PersistentVolumeClaim
NAME               STATUS  VOLUME                                    CAPACITY  ACCESS MODES  STORAGECLASS  AGE
sentry-postgresql  Bound   pvc-a3af7917-5d08-11e8-9576-9a07dd1ebfb4  8Gi       RWO           standard      5m
sentry-redis       Bound   pvc-a3b0850d-5d08-11e8-9576-9a07dd1ebfb4  8Gi       RWO           standard      5m
sentry-sentry      Bound   pvc-a3b159e9-5d08-11e8-9576-9a07dd1ebfb4  10Gi      RWO           standard      5m

==> v1/Service
NAME               TYPE       CLUSTER-IP      EXTERNAL-IP  PORT(S)         AGE
sentry-postgresql  ClusterIP  10.106.179.194  <none>       5432/TCP        5m
sentry-redis       ClusterIP  10.104.155.56   <none>       6379/TCP        5m
sentry-sentry      NodePort   10.101.218.50   <none>       9000:31416/TCP  5m

==> v1beta1/Deployment
NAME                  DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
sentry-postgresql     1        1        1           1          5m
sentry-redis          1        1        1           1          5m
sentry-sentry-cron    1        1        1           1          5m
sentry-sentry-web     1        1        1           1          5m
sentry-sentry-worker  2        2        2           2          5m

==> v1beta1/Ingress
NAME           HOSTS                 ADDRESS        PORTS  AGE
sentry-sentry  192.168.64.29.nip.io  192.168.64.29  80     5m

==> v1/Pod(related)
NAME                                   READY  STATUS   RESTARTS  AGE
sentry-postgresql-5795779885-hh88k     1/1    Running  0         5m
sentry-redis-6f8d889d4d-r5wbh          1/1    Running  0         5m
sentry-sentry-cron-8578df4d69-2kzd9    1/1    Running  0         5m
sentry-sentry-web-6b857b74c8-96fsq     1/1    Running  1         5m
sentry-sentry-worker-66cf9bb6db-2qzkh  1/1    Running  0         5m
sentry-sentry-worker-66cf9bb6db-x9plg  1/1    Running  0         5m


NOTES:
1. Get the application URL by running these commands:
  export NODE_PORT=$(kubectl get --namespace sentry -o jsonpath="{.spec.ports[0].nodePort}" services sentry-sentry)
  export NODE_IP=$(kubectl get nodes --namespace sentry -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT/auth/login/sentry

2. Log in with

  USER: [email protected]
  Get login password with
    kubectl get secret --namespace sentry sentry-sentry -o jsonpath="{.data.user-password}" | base64 --decode

作成されたAPIオブジェクトのレポートと、Chart側で設定された初期メッセージがNOTES以下に表示されています。

1. Get the application URL...にはログインURLの取得方法が書いてありますが、これはNodePortを使う場合のものです。 今回はパラメータでIngressを有効にしているので、ログインURLは open http://$(minikube ip).nip.io/ でOKです。 2. Log in with手順に従って初期ユーザのパスワードを取得し、Sentryの管理画面にログインできることを確認してください。 ユーザ名は [email protected] で、パスワードは上記のkubectl get secretコマンドを実行して表示されるテキストです。 最後に%が化けて表示されるようですが、その前までのテキストを入力してください。

公式のChartはだいたいインストール後にこのようなヘルプメッセージが表示されます。 このメッセージもGoのテンプレート構文で記述されています。

kubernetes/charts | stable/sentry/templates/NOTES.txt

helm list: クラスタ上のChartインスタンス(Release)の一覧を表示

k8sクラスタにインストール済みのReleaseの一覧は下記のコマンドで確認できます。

$ helm list
NAME    REVISION        UPDATED                         STATUS          CHART           NAMESPACE
sentry  1               Tue May 22 00:07:12 2018        DEPLOYED        sentry-0.1.14   sentry

helm upgrade: Chartの更新のデプロイ

Release名を指定して、

$ helm upgrade sentry

helm delete: ReleaseとAPIオブジェクトの削除

作成したReleaseによって作られたAPIオブジェクトを削除する場合は、下記のコマンドを実行します。

$ helm delete --purge sentry

パラメータにはReleaseの名前を指定します。 --purgeオプションをつけるとRelease自体も削除します。 このオプションをつけない場合、APIオブジェクトだけが削除され、Releaseは削除されずにHelmの管理DBに残る点に注意してください。 purgeするまでその名前のReleaseを新しく作ることはできません。

なお、stable/sentryの場合は少し厄介で、READMEにも書かれている通りhelm deleteしただけではJobのオブジェクトが残ります。

$ kubectl get all --namespace sentry
NAME                           READY     STATUS      RESTARTS   AGE
pod/sentry-db-init-9mnqq       0/1       Completed   0          22h
pod/sentry-db-init-ckgzj       0/1       Error       0          22h
pod/sentry-user-create-8dgbj   0/1       Completed   0          22h

NAME                           DESIRED   SUCCESSFUL   AGE
job.batch/sentry-db-init       1         1            22h
job.batch/sentry-user-create   1         1            22h

これらのオブジェクトは下記のように個別に削除する必要があります。

$ kubectl delete job/sentry-db-init job/sentry-user-create

sentry-db-initsentry部分はReleaseの名前です。別の名前でhelm installするとここも変わるので注意してください。 後述するようにネームスペースごと削除してしまう方が簡単です。

$ kubectl delete ns sentry

NamespaceとContextについて

k8sにはNamespaceというオブジェクトがあり、DeploymentServiceなど主要なオブジェクトは基本的にいずれかのNamespaceに属しています。 前回の記事まで全くNamespaceを指定してこなかったのですが、ずっとdefaultという組み込みのNamespaceが操作の対象になっていました。

# defaultネームスペースのPodのリストを表示
$ kubectl get pods

# sentryネームスペースのPodのリストを表示
$ kubectl -n sentry get pods

また、これまでのサンプルではServiceのエンドポイントにアクセスする際にはそのサービスの名前をDNS名として指定しました。 同一のネームスペース内のオブジェクトから接続する場合はそれで良いのですが、 異なるネームスペースのオブジェクトから接続する場合には${サービス名}.${ネームスペース名}のようにドメインを追加する必要があります。

ところで、default以外のネームスペースを主に使う場合に、kubectlのパラメータにその都度-nオプションを指定するのは面倒です。

kubectlコマンドにはContextという概念があり、コマンド側で接続先のk8sクラスタやデフォルトのネームスペースを記憶しています。

  • 接続先のクラスタやデフォルトのネームスペースなどの情報をContextという単位で保存
  • Contextは複数保存でき、有効になっているのはそのうちの一つ(CURRENT)
  • 有効なContextを切り替えることで接続先クラスタやデフォルトのネームスペースを変更可能

これらの設定は ~/.kube/config に記録されています。

kubectl config get-contexts: Contextの一覧の表示

minikubeしか使っていない場合は下記のようになっていると思います。

$ kubectl config get-contexts
CURRENT   NAME       CLUSTER    AUTHINFO   NAMESPACE
*         minikube   minikube   minikube

kubectl config current-context: 現在有効になっているContext名の表示

$ kubectl config current-context
minikube

kubectl config view: Contextの詳細の表示

~/.kube/configの中身を表示しているだけです。

$ kubectl config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority: /Users/kawahara_taisuke/.minikube/ca.crt
    server: https://192.168.64.29:8443
  name: minikube
contexts:
- context:
    cluster: minikube
    user: minikube
  name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube
  user:
    client-certificate: /Users/kawahara_taisuke/.minikube/client.crt
    client-key: /Users/kawahara_taisuke/.minikube/client.key

kubectl config set-context: Contextの作成または更新

現在のContextのネームスペースをsentryに変更するにはこうします。

$ kubectl config set-context $(kubectl config current-context) --namespace=sentry
Context "minikube" modified.

# 変わっていることを確認
$ kubectl config get-contexts
CURRENT   NAME       CLUSTER    AUTHINFO   NAMESPACE
*         minikube   minikube   minikube   sentry

無指定(default)に戻す場合はこうします。

$ kubectl config unset contexts.minikube.namespace
Property "contexts.minikube.namespace" unset.

# 戻っていることを確認
$ kubectl config get-contexts
CURRENT   NAME       CLUSTER    AUTHINFO   NAMESPACE
*         minikube   minikube   minikube

存在しない名前を指定すると新しいContextを作ります。 次の例はクラスタがminikubeでネームスペースがsentryであるContextの例です。

$ kubectl config set-context minikube-sentry --cluster=minikube --user=minikube --namespace=sentry

$ kubectl config get-contexts
CURRENT   NAME              CLUSTER    AUTHINFO   NAMESPACE
*         minikube          minikube   minikube
          minikube-sentry   minikube   minikube   sentry

kubectl config use-context: 有効なContextを変更

$ kubectl config use-context minikube-sentry

% kubectl config get-contexts
CURRENT   NAME              CLUSTER    AUTHINFO   NAMESPACE
          minikube          minikube   minikube
*         minikube-sentry   minikube   minikube   sentry

kubectl config delete-context: Contextの削除

指定したコンテキストを削除できます。

$ kubectl config delete-context minikube-sentry

kubectxとkubens

Contextとネームスペースの切り替えは見ての通り煩雑です。 kubectxとkubensというツールを使えばもっと簡単に切り替えの操作を行うことができます。

macOSの場合はHomebrewでインストール可能です。

$ brew install kubectx

インストール時に--with-short-nameオプションをつけると、コマンド名をkctxknsに変更できます。 kubectlとの前方一致部分が減るのでシェルでの補完が少し楽になります。

kctx(kubectx)Contextの一覧表示と切り替え、削除、リネームが可能です。

$ kctx                        # パラメータ無しだとContext一覧を表示(有効なものは黄色で表示)
$ kctx minikube-sentry        # 指定したContextに切り替え(Tabで補完が効く)
$ kctx -                      # 一つ前のContextに戻る
$ kctx -d minikube-sentry     # Contextを削除
$ kctx sentry=minikube-sentry # minikube-sentry を sentry にリネーム
$ kctx sentry=.               # 現在のContextを sentry にリネーム

kns(kubens)は現在のContextに対して、ネームスペースの一覧表示と切り替えが可能です。

$ kns         # ネームスペースの一覧表示(現在のネームスペースを黄色で表示)
$ kns sentry  # 指定したネームスペースをデフォルトに変更(Tabで補完が効く)
$ kns -       # 一つ前のネームスペースに変更

なお、kubectx(kctx)ではContextの作成や削除はできませんのでkubectlコマンドを使う必要があります。 ただ、知っての通りminikubeの場合はminikube startの際に自動的にContextが作成されますし、 GCPのGKEの場合も指定したGKEクラスタに接続するためのContextを作成する機能がgcloudコマンドに組み込まれています。

gcloud container clusters get-credentials | Cloud SDK | Google Cloud

minikubeやGKEを使っている限りではContextを作成したり編集したりする機会はそれほど多くないと思いますので、 kctxknsの使い方を覚えておけばほとんどの場合十分です。

Namespaceの削除について

helm install--namespaceオプションを指定すると、当該ネームスペースがない場合は自動的に作成されます。 このネームスペースはhelm deleteしても残ります。

ネームスペースを削除する場合にはkubectl deleteを使います。

$ kubectl delete namespace sentry
# または
$ kubectl delete ns sentry

ネームスペース内のオブジェクトが全て削除される点に注意してください。

永続ボリュームも確認無しで削除されるため、事故によるデータロストに十分注意する必要があります。 PVオブジェクトが削除された場合に、データの実体(GCPのPersistentDiskやAWSのEBSなど)が削除されず残るようにするためには、 StorageClassreclaimPolicyRetainに変更しておく必要があります。

Chartの作り方

前節では既存のパッケージを使う方法を見てきました。 今節では自分でパッケージを作り、k8sクラスタにデプロイする方法を見ていきます。

具体的には、第2回で取り上げたサンプルアプリのHelm Chartを作成します。 構成はStep4のマニフェストファイルと同等にします。

最終的なコードは下記に置いてあります。

k8s/chart/

なお、このドキュメントでは作ったHelm Chartをアプリのパッケージとして公開するというより、 プライベートなアプリのk8sマニフェストをHelmで管理できるようにChart化する、 という使い方を想定しています。

このような使い方においては、作成したChartをリポジトリサーバに登録する必要はなく、 helmコマンドでローカルFS上のChartディレクトリを直接指定することでChartのデプロイを行うことができます。

helm upgrade –install: 作ったChartのデプロイ

サンプルコードのChartをデプロイしてみましょう。

もしもまだであれば、準備の節を参照して、サンプルコードのチェックアウトとminikubeの起動を済ませて置いてください。

次に、サンプルアプリのイメージをminikube上でビルドしておいてください。

$ cd rails-k8s-demoapp
$ (eval $(minikube docker-env) && $ docker build . -t demoapp:0.0.1)

また、Step4と同じくIngressのTLS証明書用のSecretオブジェクトはkubectlコマンドで作成します。 証明書と鍵の内容をChartのパラメータ化することも不可能ではないのですが煩雑なので、 公式リポジトリのChartもSecretオブジェクトの名前だけをパラメータ化しているものが多いようです。 今回はそれにならいます。

ちょっと手を抜いてMakefileを利用して作成します。手順はStep4の時と同じなので詳細は前回の記事を参照してください。

cd k8s/chart
$ make kubectl-create-secret-tls

Tiller(Helmのサーバモジュール)のインストールがまだであれば下記を実行します。

$ helm init --wait

次に、helm upgradeコマンドでChartをminikubeにデプロイします。

$ helm upgrade staging . --install --wait \
  --set ingress.host=demoapp-puma.$(minikube ip).nip.io

stagingReleaseの名前です。 --installオプションをつけると、Releaseが存在しない場合のみhelm installのような動作をするようになります。 初回と2回目以降でコマンドを分けずに済むので便利です。

--setでは、ingress.hostというパラメータを指定しています。

最後にブラウザでWebアプリを開きます。

$ open https://demoapp-puma.$(minikube ip).nip.io/

Makefileに同様のタスクを定義しているので、下記のコマンドでも同じことができます。

$ make helm-init
$ make minikube-docker-build
$ make kubectl-create-secret-tls
$ make helm-upgrade
$ make open

# または単に
$ make

Releaseと証明書の削除は次のコマンドです。

make clean

helm template: テンプレートのレンダリング

Chartに含まれるテンプレートが最終的にどのようなYAMLデータになるかを事前に確認するには、 helm templateコマンドを使います。

$ cd rails-k8s-demoapp/k8s/chart
$ helm template . --name staging

上記のコマンドは、すべてのテンプレートにパラメータを埋め込んだ結果を表示します。

  • .はChartディレクトリのパスです。stable/sentryのようにリポジトリのChartを指定することもできます。
  • --nameオプションはReleaseの名前です。この他に、--set-fでパラメータを指定することもできます。 レンダリングの際はそれらの値を埋め込んだ結果が表示されます。

また、特定のテンプレートのみレンダリングしたい場合は-xオプションを指定します。

$ helm template . --name staging -x templates/puma-deploy.yaml

-xオプションの値は、カレントディレクトリからの相対パスではなく、 Chartのルートディレクトリからの相対パスを指定する必要がある点に注意してください。

helm create: 新しいChartの作成

demoappという名前のChartを作るには下記のようにします。

$ cd rails-k8s-demoapp/k8s
$ helm create demoapp

次のような構成のディレクトリが作られます。

demoapp/
 Chart.yaml
 values.yaml
 charts/
 templates/
    NOTES.txt
    _helpers.tpl
    deployment.yaml
    ingress.yaml
    service.yaml

作成後、demoappディレクトリはリネームしても問題ありません。

  • Chart.yamlには、Chartの名前やバージョンなどの情報が記述されます。
  • values.yamlには、Chartのパラメータのデフォルト値が記述されます。
  • charts/には、このChartが依存している別のChartを保存します。今回は扱いません。
  • templates/には、次のファイルを保存します。
    • NOTES.txtは、Chartのインストール後に表示するメッセージです。
    • _helpers.tplには、テンプレート全般で再利用するための部分テンプレートを書きます。
    • 残りの*.yamlがマニフェストのテンプレートです。

helm createで作成した直後のChartは、DeploymentServiceを一つずつ作成して、 Ingress経由でそれを外部に公開するというk8sの基本構成が記述されています。 なので、それらを一旦削除します。

rm demoapp/values.yaml demoapp/templates/*.yaml

次に、Step4のマニフェスト一式をdemoapp/templates/以下にコピーします。

cp manifests-step4/*.yaml demoapp/templates/

さて、これでおおよそ動くChartができました。Ingress以外はこのままでも動作します *1demoapp/templates/に置くのはテンプレートなので、テンプレートとしての構文を一切使わないYAMLのままでもとりあえず動くのです。 マニフェストファイルが存在するプロジェクトをHelm Chart化する場合には、 このように一旦templates/ディレクトリに置いてから各マニフェストファイルをテンプレート構文で書き換えていくことになります。

そうやって作成したChartが下記のパスにあります。

k8s/chart/

次節以降では、このChartの中のテンプレートとStep4のマニフェストを比較しつつ、Chartの書き方を解説します。

*1: Ingressについては、ホスト名を示すパラメータが$(minikube ip)というコマンドで取得するまでわからないため、 下記のように元々sed${MINIKUBE_IP}を書き換えてからkubectl apply -f -に流し込む前提のマニフェストでした。 Chart化する際には、テンプレートの機能を使ってパラメータとしてこのホスト名を指定できるようにします。

# k8s/manifests-step4/puma-ing.yaml
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: demoapp-puma
  # 省略
spec:
  tls:
  - hosts:
    - demoapp-puma.${MINIKUBE_IP}.nip.io
    secretName: demoapp-puma-tls
  rules:
  - host: demoapp-puma.${MINIKUBE_IP}.nip.io
    # 省略
export MINIKUBE_IP=$(minikube ip)
cat *.yaml | sed s/\${MINIKUBE_IP}/$(MINIKUBE_IP)/ | kubectl apply --record -f -

詳細な説明は 前回の記事を参照してください。

テンプレート

Helm Chartのテンプレート構文は、ほぼGo言語のtext/templateです。 ただし、下記のように拡張されています。

はじめに、Step4時点でのpumaDeploymentを例に、Chartのテンプレート化の勘所とテンプレート記法の基本を説明します。

元になるマニフェストは下記です。

# k8s/manifests-step4/puma-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demoapp-puma
  labels:
    app: demoapp
    component: puma
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demoapp
      component: puma
  template:
    metadata:
      labels:
        app: demoapp
        component: puma
    spec:
      restartPolicy: Always
      containers:
        - name: puma
          image: demoapp:0.0.1
          imagePullPolicy: IfNotPresent
          command:
            - ./bin/start-puma
          livenessProbe:
            httpGet:
              path: /health_check/full
              port: 3000
            initialDelaySeconds: 30
          readinessProbe:
            httpGet:
              path: /health_check/full
              port: 3000
            initialDelaySeconds: 30
          envFrom:
            - configMapRef:
                name: demoapp-rails-env
            - secretRef:
                name: demoapp-rails-env

これをテンプレート化したものが下記です。

# k8s/chart/templates/puma-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ template "demoapp.puma.name" . }}
  labels:
    app: {{ template "demoapp.name" . }}
    chart: {{ template "demoapp.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
    component: puma
spec:
  replicas: {{ .Values.puma.replicas }}
  selector:
    matchLabels:
      app: {{ template "demoapp.name" . }}
      release: {{ .Release.Name }}
      component: puma
  template:
    metadata:
      labels:
        app: {{ template "demoapp.name" . }}
        release: {{ .Release.Name }}
        component: puma
      annotations:
        checksum/rails-env-cm: {{ include (print $.Template.BasePath "/rails-env-cm.yaml") . | sha256sum }}
        checksum/rails-env-secret: {{ include (print $.Template.BasePath "/rails-env-secret.yaml") . | sha256sum }}
    spec:
      restartPolicy: Always
      containers:
        - name: puma
          image: {{ .Values.rails.image.repository }}:{{ .Values.rails.image.tag }}
          imagePullPolicy: IfNotPresent
          command:
            - ./bin/start-puma
          livenessProbe:
            httpGet:
              path: /health_check/full
              port: 3000
            initialDelaySeconds: 30
          readinessProbe:
            httpGet:
              path: /health_check/full
              port: 3000
            initialDelaySeconds: 30
          envFrom:
            - configMapRef:
                name: {{ template "demoapp.rails-env.name" . }}
            - secretRef:
                name: {{ template "demoapp.rails-env.name" . }}

このようにテンプレートでは{{ }}の中にコード片を埋め込んでいきます。

いっぱい差分があるように見えますが、書き換えられているものは次の四つです。

  • レプリカの数やイメージの名前など、パラメータ化されたもの
  • オブジェクトの名前
  • ラベル
  • アノテーション

次節以降で、一つずつ詳細を見ていきます。

パラメータ化

Chart版のマニフェストでは、レプリカの数とイメージの名前がパラメータ化されています。 Step4のマニフェストとChartのテンプレートから関連する部分だけを抜き出して比較します。

# before: k8s/manifests-step4/puma-deploy.yaml
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: puma
          image: demoapp:0.0.1

# after: k8s/chart/templates/puma-deploy.yaml
spec:
  replicas: {{ .Values.puma.replicas }}
  template:
    spec:
      containers:
        - name: puma
          image: {{ .Values.rails.image.repository }}:{{ .Values.rails.image.tag }}

Valuesという組み込みオブジェクトの値を参照しています。 これはChartの利用者側で任意に変更可能なパラメータを管理するオブジェクトです。 パラメータのデフォルト値はChartディレクトリのvalues.yamlファイルに記述します。 この値はhelm installまたはhelm upgradeの際に--setオプションや-fオプションで上書きすることができます。 (詳細は前半のSentryのデモを参照してください)

サンプルコードのvalues.yamlから今回関連のある部分だけを以下に抜き出しています。

puma:
  replicas: 2

rails:
  image:
    repository: demoapp
    tag: 0.0.1

このYAMLデータの中身を.Values.puma.replicasのように.区切りで指定することができます。

オブジェクトの名前

次に、オブジェクトの名前に関する部分だけを抜き出して比較します。

# before: k8s/manifests-step4/puma-deploy.yaml
metadata:
  name: demoapp-puma
spec:
  template:
    spec:
      containers:
        - name: puma
          envFrom:
            - configMapRef:
                name: demoapp-rails-env
            - secretRef:
                name: demoapp-rails-env

# after: k8s/chart/templates/puma-deploy.yaml
metadata:
  name: {{ template "demoapp.puma.name" . }}
spec:
  template:
    spec:
      containers:
        - name: puma
          envFrom:
            - configMapRef:
                name: {{ template "demoapp.rails-env.name" . }}
            - secretRef:
                name: {{ template "demoapp.rails-env.name" . }}

templateというのは、定義済みの部分テンプレートを呼び出すActionです。 Helmの場合、templates/_helpers.tpldefineにより定義した値を埋め込むことになります。 この例では、demoapp.puma.nameという定義を参照しています。

以下、templates/_helpers.tplから関連のある部分だけ抜き出します。

{{/* k8s/chart/templates/_helpers.tpl */}}

{{- define "demoapp.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{- define "demoapp.puma.name" -}}
{{- include "demoapp.fullname" . -}}-puma
{{- end -}}

{{- define "demoapp.rails-env.name" -}}
{{- include "demoapp.fullname" . -}}-rails-env
{{- end -}}

demoapp.fullnamedemoapp.puma.name, demoapp.rials-env.nameの三つのdefineがあります。 前者はhelm createした時から存在するもので、残り二つは筆者が追加したものです。

結構ボリュームがあるのですが、ほとんどの状況では次のようなシンプルな結果になります。


  • demoapp.fullnameは、Chart.yamlに書かれたnameフィールドの値にReleaseの名前をプレフィクスとしてつけた文字列を返します。 サンプルの場合は、Releaseの名前がstagingならstaging-demoappになります。
  • demoapp.puma.nameは、Releasestagingの場合はstaging-demoapp-pumaになります。 includeは、define定義の中でさらに別の部分テンプレートを参照する時に使います。
  • demoapp.rails-env.nameは、Releasestagingの場合はstaging-demoapp-rails-envになります。

つまりChart化した後のオブジェクト名には、元々のマニフェストでの名前であるdemoapp-pumaに プレフィクスとしてReleaseの名前をつけていることになります。 Helm Chartでは、このようにオブジェクトの名前にReleaseの名前をプレフィクスとしてつけるのが慣例です。

なお、defineを追加せずdemoapp.fullnameだけを使って下記のように書くことも可能です。

metadata:
  name: {{ template "demoapp.fullname" . }}-puma
spec:
  template:
    spec:
      containers:
        - name: puma
          envFrom:
            - configMapRef:
                name: {{ template "demoapp.fullname" . }}-rails-env
            - secretRef:
                name: {{ template "demoapp.fullname" . }}-rails-env

公式のHelmチャートにはこのように書かれているものも多いですが、 typoに気づきにくいという問題があるので、このサンプルでは前記のようにdefineして共通化しています。

ところで、template関数の呼び出しの際には名前を示す一つ目のパラメータの後ろに.がありますが、 これはカレントスコープを渡していることを示しています。 テンプレート定義の内部で.Values.Releaseなどの組み込みオブジェクトを参照する場合には、 呼び出し時にスコープを渡す必要がある点に注意してください。 これはincludeも同様です。

SETTING THE SCOPE OF A TEMPLATE

ラベル

次に、ラベルに関連するところだけを抜き出して比較します。

# before: k8s/manifests-step4/puma-deploy.yaml
metadata:
  labels:
    app: demoapp
    component: puma
spec:
  selector:
    matchLabels:
      app: demoapp
      component: puma

# after: k8s/chart/templates/puma-deploy.yaml
metadata:
  labels:
    app: {{ template "demoapp.name" . }}
    chart: {{ template "demoapp.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
    component: puma
spec:
  selector:
    matchLabels:
      app: {{ template "demoapp.name" . }}
      release: {{ .Release.Name }}
      component: puma

元々appcomponentだったところに、chartreleaseheritageが追加されています。 これらのラベルは、Helmの公式ドキュメントでBest Practiceのページに標準的なラベルとして定義されているものです。

STANDARD LABELS - Chart Best Practices

chartはChartの名前、releaseReleaseの名前を示すラベルです。 heritageは常にTillerという文字列になります。これはこのオブジェクトがHelm(Tiller)によって管理されているということを示します。

releaseのみ、.spec.selector.matchLabelsにも追加されている点に注意してください。 これは同一のChartが同じNamespace内に複数のReleaseとしてデプロイされた場合に、適切に分離されるために必要です。 chartheritageは動作に直接影響しませんが、慣例として推奨されているラベルです。

アノテーション

最後にアノテーションについて説明します。

下記のように、マニフェストにはなかった.spec.template.metadata.annotationsエントリがテンプレートには追加されています。

# after: k8s/chart/templates/puma-deploy.yaml
spec:
  template:
    metadata:
      annotations:
        checksum/rails-env-cm: {{ include (print $.Template.BasePath "/rails-env-cm.yaml") . | sha256sum }}
        checksum/rails-env-secret: {{ include (print $.Template.BasePath "/rails-env-secret.yaml") . | sha256sum }}

アノテーションは、オブジェクトの識別以外に使われるメタ情報です(識別に使うのはラベル)。 ラベルと同様にKey-Value形式の構造で、通常はビルドやリリースに関する情報やモニタリングのための情報などを記録します。

ところで、ConfigMapまたはSecretから環境変数などの設定情報を参照しているDeploymentには、運用上少し面倒な制限があります。 それは、ConfigMapSecretの内容を更新したとしても、それを参照するDeploymentはローリングアップデートされるわけではないということです。

一方、Deploymentはこのアノテーションが更新された場合もローリングリスタートが行われます。 そこでそれを利用して、ConfigMapまたはSecretの更新後にkubectl patchコマンドなどを用いてアノテーションを更新することで 強制的にローリングリスタートをかけるというテクニックが知られています。

kubectl patch deploy demoapp-puma -p \
  "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"config-updated-at\":\"`date +'%s'`\"}}}}}"

Helmでテンプレート使う場合には、先に挙げた例のようにConfigMapおよびSecretのマニフェストのダイジェストをアノテーションに含めることで、 これらの設定オブジェクトの内容が変わったときに自動的にローリングリスタートがかかるようにできます。 このダイジェスト計算はレンダリング後のマニフェストに対して行われるため、 例えばrails-env-cm.yamlに全く変更が無い場合でも、values.yamlを編集するなどしてrails-env-cm.yaml内部で参照しているValuesの値が変わればDeploymentはリスタートされます。

Automatically Roll Deployments When ConfigMaps or Secrets change

同様のアノテーションをsidekiq-deploy.yamlにも追加しています。 なお、mysql-deploy.yamlでもConfigMapSecretを参照していますが、 こちらは初回起動時に作成するDBやユーザなどの情報が主で運用中に変更することを想定していないため、アノテーションは追加していません。

関数とパイプライン

関数とパイプラインを使いこなすことでテンプレートはより便利になります。

Secretオブジェクトの定義では、データの値を次に示すようにBASE64エンコードして記述する必要がありました。

# k8s/manifests-step4/rails-env-secret.yaml
---
apiVersion: v1
kind: Secret
metadata:
  name: demoapp-rails-env
data:
  SECRET_KEY_BASE: MTIz    # echo -n "123" | base64
  MYSQL_PASSWORD: c2VjcmV0 # echo -n "secret" | base64

いちいちエンコードしてからファイルに記述するのは面倒です。

Helmのテンプレートでは先に述べたようにSprigの関数が組み込まれているため、 b64encという関数を使って次のようにSecretを定義することができます。

# k8s/chart/templates/rails-env-secret.yaml
---
apiVersion: v1
kind: Secret
metadata:
  name: {{ template "demoapp.rails-env.name" . }}
data:
  SECRET_KEY_BASE: {{ .Values.env.secret.SECRET_KEY_BASE | b64enc }}
  MYSQL_PASSWORD: {{ .Values.env.secret.MYSQL_PASSWORD | b64enc }}
# k8s/chart/values.yaml

env:
  secret:
    MYSQL_PASSWORD: secret
    SECRET_KEY_BASE: "123"

{{ }}の中身はパイプラインであり、 | 演算子を使って別の関数のパラメータとすることができます。 関数が二つ以上のパラメータをとる場合、|の後ろの関数には最後のパラメータとして|の前の結果が渡されます。

比較的よく使われる関数にdefaultquoteがあります。これを使った例を一つ見てみましょう。

{{ .Values.rails.testValue | default "abc" | b64enc | quote }}

defaultは、二つ目のパラメータが空の場合は一つ目の値を、空でない場合は二つ目の値を返す関数です。 quoteは、パラメータの前後に"をつけた文字列を返す関数です。 上記の例では、.Values.rails.testValueをBASE64エンコードして"で囲むという処理です。 .Values.rails.testValueが空だった場合はabcという文字列を代わりにエンコードしてクォートします。

フロー制御

今回のサンプルでは特に使っていないのですが、テンプレートではif/elseによる分岐やrangeによるループを使用できます。

Flow Control

stable/sentryingress.yamlが参考になると思います。

https://github.com/kubernetes/charts/blob/master/stable/sentry/templates/ingress.yaml

values.yaml

ここではvalues.yamlについて説明します。サンプルコードのvalues.yamlは次のような内容です。

mysql:
  image:
    repository: mysql
    tag: 5.7.21
  storage:
    size: 8Gi
    className: standard

redis:
  image:
    repository: redis
    tag: 4.0.9
  storage:
    size: 8Gi
    className: standard

puma:
  replicas: 2

sidekiq:
  replicas: 1
  timeout: 60

rails:
  image:
    repository: demoapp
    tag: 0.0.1
    setupDbTag: 0.0.1

ingress:
  tlsSecretName: demoapp-puma-tls
  # required from --set parameter
  # helm upgrade release-name . --set ingress.host=demoapp-puma.${MINIKUBE_IP}.nip.io
  # host: demoapp-puma.${MINIKUBE_IP}.nip.io

env:
  configmap:
    MYSQL_USER: demoapp
    MYSQL_DATABASE: demoapp_production
    RAILS_SERVE_STATIC_FILES: "true"
    RAILS_LOG_TO_STDOUT: "true"
  secret:
    MYSQL_PASSWORD: secret
    MYSQL_ROOT_PASSWORD: topsecret
    SECRET_KEY_BASE: "123"

注意が必要なのは下記の点です。

  • Jobオブジェクトで参照するイメージのタグはDeploymentと分ける必要があります。 具体的には.rails.image.tag.rails.image.setupDbTagです。Jobのイメージは変更不可なので、分けておかないとアプリを更新できなくなります。
  • 秘匿情報は集約しておく方が管理が楽です。 この例では.env.secretの下にまとめています。こうしておくとsopsyaml_vaultでファイルの一部だけ暗号化するのが簡単になります。 これについては次節で詳細を説明します。

参考:

values.yamlに含まれる秘密情報の暗号化

先の例では、DBのパスワードやRailsのSECRET_KEY_BASEなどの秘密情報がvalues.yamlに平文で書き込まれていました。 プライベートなアプリの場合でも、こういった秘密情報は平文のままでリポジトリにコミットするべきではありません。

ここでは、YAMLファイルを暗号化する方法を二つ紹介します。

GCP KMSの準備

紹介する方法は、いずれもGCP KMSまたはAWS KMSで暗号鍵を管理できます。 今回はGCP KMSを使う方法を紹介します。

Cloud SDKをインストールした後、適当なプロジェクトを作成しください。 その後、下記のコマンドを実行して鍵を作成してください。

# ログイン
$ gcloud auth login

# キーリングの作成
$ gcloud kms keyrings create demoapp --location global

# キーの作成
$ gcloud kms keys create values-key --location global --keyring demoapp --purpose encryption

# 作成したキーの確認
$ gcloud kms keys list --location global --keyring demoapp
NAME                                                                                PURPOSE          LABELS  PRIMARY_ID  PRIMARY_STATE
projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key  ENCRYPT_DECRYPT          1           ENABLED

この例では、プロジェクトIDはrails-k8s-demoappです。各自の環境に合わせて読み替えてください。

途中、下記のようなメッセージが出た場合は、ブラウザでリンクを開いてKMSを有効化してください。

ERROR: (gcloud.kms.keyrings.create) FAILED_PRECONDITION: Google Cloud KMS API has not been used in this project before, or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudkms.googleapis.com/overview?project=123456789012 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

使い終わった鍵は忘れずに破棄しておいてください。使っていなくても費用が発生します。

https://cloud.google.com/kms/pricing?hl=ja

また、下記のページを参照してサービスアカウントキーを作成してください。

https://cloud.google.com/docs/authentication/getting-started

役割は、Cloud KMS - クラウドKMS暗号鍵の暗号化/復号化を選択します。 JSON形式でダウンロードした鍵を kms-key.jsonという名前で保存してください。

yaml_vault

yaml_vaultはruby製の暗号化ツールです。 AWS KMSまたはGCP KMSで暗号鍵を管理することができます。

gemでインストールできます。GCP KMSを使う場合は、google-api-clientも必要です。

$ gem install yaml_vault google-api-client

Gemfileに追加しても良いですが、そのままだとDockerイメージのサイズが50MBほど増えるため、少し工夫が必要です。

下記のコマンドを実行すると、values.yamlを暗号化してencrypted_values.yamlに出力します。 その際、暗号化するキーを--keyオプションで限定している点に注目してください。このオプションを指定しない場合、YAMLの全ての値が暗号化されます。

$ yaml_vault encrypt values.yaml -o encrypted_values.yaml --cryptor=gcp-kms \
  --gcp-kms-resource-id=projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key \
  --gcp-credential-file=kms-key.json \
  --key '$.env.secret'
encrypted values.yaml -> encrypted_values.yaml

暗号化されたファイルは次のような内容になります。

# encrypted_values.yaml
mysql:
  image:
    repository: mysql
    tag: 5.7.21
  storage:
    size: 8Gi
    className: standard
redis:
  image:
    repository: redis
    tag: 4.0.9
  storage:
    size: 8Gi
    className: standard
puma:
  replicas: 2
sidekiq:
  replicas: 1
  timeout: 60
rails:
  image:
    repository: demoapp
    tag: 0.0.1
    setupDbTag: 0.0.1
ingress:
  tlsSecretName: demoapp-puma-tls
env:
  configmap:
    MYSQL_USER: demoapp
    MYSQL_DATABASE: demoapp_production
    RAILS_SERVE_STATIC_FILES: "true"
    RAILS_LOG_TO_STDOUT: "true"
  secret:
    MYSQL_PASSWORD: CiQA/TkaHXJd4HsdRnMTJ9tgso7tYdCbIJxuWbGgj5UJfpkbI+gSOADQcvdxFdICOfR8oiQa1GOM9UX0koe0AC9TM5C+fpV1nRuaajJGAF0IfFRf9KtKKHyz/Qp4ad3E
    MYSQL_ROOT_PASSWORD: CiQA/TkaHbi65E4QSHRKwpxRQcD43rMwo4rr9aUgAj8ezPjgtGcSOwDQcvdxaMRAJ+yE87zeGDIBCbeZVzyZMXAg83Odxj2y9fbL4NELzViD5bqx8XoxWe0Y7nhWYJomNpmC
    SECRET_KEY_BASE: "CiQA/TkaHfsk/7i7IrlJVhheCPifd0bCvho9CJgRbiR10lnqc1oSMgDQcvdxijXncXs0gYuMfk9/IdeSiL8fApLEDd6zISmgnmZh3d7KxNioeliHZcWApaai"

復号するときは次のようにします。

$ yaml_vault decrypt encrypted_values.yaml -o values.yaml --cryptor=gcp-kms \
  --gcp-kms-resource-id=projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key \
  --gcp-credential-file=kms-key.json \
  --key '$.env.secret'

yaml_vaultを使って運用する場合には、values.yamlはコミットせず、暗号文のencrypted_values.yamlをコミットして運用します。

なお、暗号化を行う際には、元の平文に変更がなかったとしても毎回暗号文の部分は値が変わってしまう点に注意が必要です。

sops

sopsはmozilla製の暗号化ツールです。

使用するためには、まずGCPの鍵を環境変数で指定する必要があります。

$ export GOOGLE_APPLICATION_CREDENTIALS=kms-key.json

暗号化は下記のように行います。

$ sops --encrypt --gcp-kms projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key values.yaml > encrypted_values.yaml

ファイル全体が下記のように暗号化されます。

mysql:
    image:
        repository: ENC[AES256_GCM,data:eD0o4pw=,iv:3DbUfobOeqAL0kowEKVqbMjtEC88J9/nMysGdMf4nqk=,tag:wEwatUbf9v1/LVPLYkunPA==,type:str]
        tag: ENC[AES256_GCM,data:PgWECj+7,iv:UlUPjWbxO+lW6bAm/2F5JTfAbbWa3/wCjyOIyGmEG7I=,tag:vPmfEGQkKKxE7nsJsZfYZA==,type:str]
    storage:
        size: ENC[AES256_GCM,data:Zn9a,iv:OaFk2rRTiNB3doRZBo5SvL/7j5G1PLKA0H7mBXpa/Ew=,tag:04A9OIJgYlXWOwk35kPHqg==,type:str]
        className: ENC[AES256_GCM,data:k4J42uu3fiE=,iv:YrXlk57NgadQg9VQaTbTjo8E5nT5XWZXz6UvNXiSO6E=,tag:hCOuXuQBY3rSxzsY4sx+0A==,type:str]
redis:
    image:
        repository: ENC[AES256_GCM,data:wjqwX64=,iv:hqxguyQImpZFgGz83CyQhfi/tGZbIefTHbpXMiMuL3w=,tag:Y5kdJOxdH4R2HMrYeFv5xA==,type:str]
        tag: ENC[AES256_GCM,data:tCM+YHQ=,iv:owoFufImNd81KqP5XVvEdA+3ZHpZMYp1Q56pHm7lW/o=,tag:VuiJh+zPegQvjfMMySRWTA==,type:str]
    storage:
        size: ENC[AES256_GCM,data:g5n2,iv:AVbPlOkfrBFq9UzuILE6fgt6Xww6311KbyFhXHnpiAk=,tag:S+xMDWCQ3XlT7PCQlB4kiw==,type:str]
        className: ENC[AES256_GCM,data:yrqW0RSihnc=,iv:O9wpDqHd3zYNdoS8VJI1y2giLnSByaGAvGrDMrF8ba8=,tag:hmE475vdibBfUc8R21AMzg==,type:str]
puma:
    replicas: ENC[AES256_GCM,data:oA==,iv:RkUoYXEivf5vzDrWBD7kWaGtyr1k4lApn1T0qWA67K0=,tag:Z4fPAiGEtfuxxaqQM2nXQw==,type:int]
sidekiq:
    replicas: ENC[AES256_GCM,data:QQ==,iv:NWZPcXVz7tcuXIRftmacJ2SegYfDgh/iwS82ovlYeNA=,tag:J2WSY3vjOkJcNvIJrLFy7w==,type:int]
    timeout: ENC[AES256_GCM,data:G5s=,iv:t/866qY7jTnGSRUQAhgXWEw9lcdtHll8fYIQsyARo4A=,tag:yZ2cIFXdJO2xis4B9mzWSA==,type:int]
rails:
    image:
        repository: ENC[AES256_GCM,data:x4dh5BigFg==,iv:C7jm5zl6rr7PeLwgX2i73Yo1yKWp+lv3eA0zCJipuk4=,tag:2FhxjAmnnIYVTsN6WkFoBQ==,type:str]
        tag: ENC[AES256_GCM,data:WaWw06E=,iv:S8Tx8rQPfzfk00of3HiNJVJYM3qeNQKNxxyHYyvTAFM=,tag:74upYPpg3fMoEya9YFTVog==,type:str]
        setupDbTag: ENC[AES256_GCM,data:zFo1gJ0=,iv:+P0F1hH5TagxLLe7yqCMAfoLtEsu+s5lLV7WKDJFp7s=,tag:T/Bix+hd1DZBIhA634mVrQ==,type:str]
ingress:
    tlsSecretName: ENC[AES256_GCM,data:sJcZXlktHsdfsYm6BbkGQg==,iv:J+bmgm2qC0wIMSDzXec9xsujOl3kjfFVUv+qmk3FWhs=,tag:qyecdT2dDvsPI+rAGe9+oA==,type:str]
    #ENC[AES256_GCM,data:H0TCFT5qkWAfK6E+j9sSjudYmCVAKckB7CQxgwbH,iv:kl625mCSUA/NNV35CU+xez9GOFggKnDm5nXYJ9waVMU=,tag:AKdSsCJNcfdC+I3nvXhStw==,type:comment]
    #ENC[AES256_GCM,data:cGNTwdT+KF0CoUCmNVwLxyvUqNozAZvz0xDWzoEB/qElfEMVRSqdPXMjMjsjEy1Apm0WOBfp5HFgZUFW16Q+vucAhgeFAn0mM7WhADNjKZGZUw==,iv:sMn+N9nHPbWB0Raps0559jxHKDMeo98CfnUIjIvTFjU=,tag:bR8mdcFEf2ZaJ2IdKHp9xg==,type:comment]
    #ENC[AES256_GCM,data:cl97jwOSlNZmfBAK+GjtzUZgqyABB9bXUN0J+xeFQpc5I3V3Ez2Olrc=,iv:jcrqo86rp+SVSB+lKLee/YdEz+8IfnULzYA+A0vj+SA=,tag:tqSfgPe//SlU0GJSog0PcQ==,type:comment]
env:
    configmap:
        MYSQL_USER: ENC[AES256_GCM,data:ckkEsrePZg==,iv:ssnCPFBqnihIe5c9T79EJlA2vGWsihOI53RL1SwdklA=,tag:ewyXNB6S6Kd25FIcSGtF4w==,type:str]
        MYSQL_DATABASE: ENC[AES256_GCM,data:LmgD+B0MF2jaHXiLAbL57UdT,iv:0eivT7Dkkso4zcvbekb0Li5CBGEn4TcaAFzcC7QtvTs=,tag:/k2xCsM+BKkPF540QZNRqA==,type:str]
        RAILS_SERVE_STATIC_FILES: ENC[AES256_GCM,data:B7YMSg==,iv:XM6zzCChuIPUUYmOog/XvbaC88j+AZtNcfps2/e/2h0=,tag:AK9aHDYW2WaBTITLQuBA6Q==,type:str]
        RAILS_LOG_TO_STDOUT: ENC[AES256_GCM,data:HV9B/Q==,iv:mSKjUGQSmCXKcCutSmjnenb3lVzVHWEKrTa+N+nBexY=,tag:4bYviLxwkVCZgjjNZ3cvFQ==,type:str]
    secret:
        MYSQL_PASSWORD: ENC[AES256_GCM,data:aRQsAyGK,iv:tjhn1oDqbp83vNartgIL4S8SgveNLUL91gjUzSCBuNo=,tag:1PtcSk8gt0XBuboZzE93pQ==,type:str]
        MYSQL_ROOT_PASSWORD: ENC[AES256_GCM,data:QgUKdkPZ3URM,iv:iQl52u3fruHtoBh6uLCJ0ArzvLHbw7XbNVQcKuBbaVs=,tag:u/V3k0D15Ne0TQLfRcTF/g==,type:str]
        SECRET_KEY_BASE: ENC[AES256_GCM,data:0Bsu,iv:1MfHBuEa2fF84H+IJXHeufBWH+dJWGEE4zJqts3EJrw=,tag:VAgJgAS+xlgAKnPeff1eHA==,type:str]
sops:
    kms: []
    gcp_kms:
    -   resource_id: projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key
        created_at: '2018-05-28T14:55:44Z'
        enc: CiQA/TkaHVv9JRDpzqLYXfGD8QDW92/4xSZhMcuao5VlStYzBAwSSQDQcvdxZjXWoG1h8nEbj9o1TNWvdSlz5rxY/l9WEd/WieUiORJLVxI+MCsZ764vUksSk9jrmPxnLqlrfKy/Klz/6yKGi3pyx/g=
    lastmodified: '2018-05-28T14:55:45Z'
    mac: ENC[AES256_GCM,data:zoHHamGXApQLsCJSxYuuVkYFo8xAROv+ier9wZYEpn2jDu9vwDZObwEb52Ewx4UTEJphq4KWi/DKodcUZHfyhWIEjqpwwUxqlIXlWmq1AO5XERa/XS0egi6gK3VnKYHsxbRSKF/hY/DOS6bO5A6Fnh0lBfENUZ8qStt5gKtwPOE=,iv:FK4V/TOHJ6cY3mPk4rlrgrSWm8s+HFazY+62d8Cck58=,tag:HkgWUGc4O4rcHZ1jDY+viQ==,type:str]
    pgp: []
    unencrypted_suffix: _unencrypted
    version: 3.0.5

このファイルを編集するときは、下記のように直接sopsコマンドで指定します。

$ sops encrypted_values.yaml

するとファイルの内容が復号され、環境変数EDITORで指定したエディタ上で編集可能になります。 保存してエディタを閉じると再び暗号化された状態で保存されます。

$ sops --decrypt --gcp-kms projects/rails-k8s-demoapp/locations/global/keyRings/demoapp/cryptoKeys/values-key encrypted_values.yaml > values.yaml

sopsもyaml_vaultのようにファイルの一部分だけを暗号化することもできますが、 「YAMLデータのキーにサフィックスをつけて指定した部分だけを平文にする」という仕組みであり、 values.yamlの構造に直接影響を与える方法なので使いにくいです。

Encrypting only part of a file

sopsを使う場合、values.yamlには秘密情報を以外の値だけを書くようにし、 secret-values.yamlに秘密情報を全て取り出して、常に-fオプションでsecret-values.yamlも指定するという方法が良いと思います。

Subchart

Helm Chartでは、他のChartをSubchartとして使用することができます。

Subcharts and Global Values

例えばstable/sentryパッケージはstable/postgresstable/redisをSubchartとして使用しており、 PostgreSQLとRedisのセットアップはこれらのChartに依存しています。

今回、rails-k8s-demoappのChartではあえてstable/mysqlstable/redisを使わずに自前でテンプレートを用意しました。 この理由は公式のChartと言えどまだ枯れているとは言えないと判断したためです。

例えばstable/redisではパスワードを示すパラメータの名前が途中で変わっており、 stable/sentryの現時点での最新版(0.1.14)は古いバージョンのstable/redis(0.10.1)に依存しています。 そのため最新のstable/redisのドキュメントを見て設定してしまうと動きません。 (バージョンを指定してドキュメントを見られるようなサービスもありません)

また、当然Valuesで変更できる部分以外の動作は変更できなくなるため、Chart側で十分な柔軟性が確保されていない場合には、 自分のアプリケーションの要件を満たせないあるいは途中で満たせなくなる可能性があります。

このあたりはChefの(特に初期の)コミュニティクックブックに状況が似ていると思います。

公式のChartとして公開することを目指すのであればできるだけ他のChartを利用するべきだと思いますが、 そうでない場合には無理に依存を増やす必要はありません。 公式のChartは汎用性を高めるために多機能なものが多く、全貌を把握すること自体がコストになるケースもあります。 簡単なものなら自作した方がトータルでは楽なことも多いです。

また、Subchartまで完全に自分でコントロール可能な場合はChartの分割を検討しても良いと思いますが、 多くのChartに依存されたChartは更新が難しくなるので、そこはトレードオフになります。

まとめ

この節では、プライベートなアプリのHelm Chartの作り方に関して、 k8sのプレーンなマニフェストをどのようにテンプレート化するかについて、puma-deploy.yamlを例に説明しました。

その他のオブジェクトのマニフェストについてもだいたい同じような手順でテンプレート化できるため、 それらについての詳細な説明は割愛します。コードを見てみてください。

k8s/chart/

自作のChartによるワークフロー

以下についてはすでに説明しました。

この節では、アプリケーションを運用する上でさらに必要になるであろういくつかのオペレーションを説明します。

helm upgrade: Chartの更新を反映

コマンド自体は初回デプロイと同じhelm upgradeを使いますが、 アプリケーションを更新する場合には次のいずれかの方法でDeploymentのイメージを変更する必要があります。

  1. --setオプションでrails.image.tagを変更する。

この場合、次のように新しいタグを指定して実行します。

$ helm upgrade staging . --install --wait \
  --set ingress.host=demoapp-puma.$(minikube ip).nip.io \
  --set rails.image.tag=0.0.2
  1. values.yamlファイルの.rails.image.tagChart.yamlのバージョン情報を書き換えてhelm upgradeを実行する。

差分は典型的には下記のようになります。

diff --git a/k8s/chart/Chart.yaml b/k8s/chart/Chart.yaml
index 43907c6..a6283b0 100644
--- a/k8s/chart/Chart.yaml
+++ b/k8s/chart/Chart.yaml
@@ -1,5 +1,5 @@
 apiVersion: v1
-appVersion: "0.0.1"
+appVersion: "0.0.2"
 description: A Helm chart for Kubernetes
 name: demoapp
-version: 0.0.1
+version: 0.0.2
diff --git a/k8s/chart/values.yaml b/k8s/chart/values.yaml
index 3f6eea1..96610f8 100644
--- a/k8s/chart/values.yaml
+++ b/k8s/chart/values.yaml
@@ -24,7 +24,7 @@ sidekiq:
 rails:
   image:
     repository: demoapp
-    tag: 0.0.1
+    tag: 0.0.2
     setupDbTag: 0.0.1

 ingress:

ファイルを書き換えたら、初回デプロイと全く同じコマンドを実行します。

$ helm upgrade staging . --install --wait \
  --set ingress.host=demoapp-puma.$(minikube ip).nip.io

簡単なのは1の方法ですが、現在デプロイされているイメージの情報がgit側に残らないというデメリットがあります。

長期的には2の方法がおすすめです。Chart.yamlversionも更新しておくと、 後に紹介するhelm historyとgitのログでインフラ構成の履歴を追うのが楽になります。

helm upgrade

helm history: Releaseの履歴を表示

指定したReleaseの更新履歴を表示します。

$ helm history staging
REVISION        UPDATED                         STATUS          CHART           DESCRIPTION
1               Mon May 28 00:01:37 2018        SUPERSEDED      demoapp-0.0.1   Install complete
2               Mon May 28 00:07:00 2018        DEPLOYED        demoapp-0.0.2   Upgrade complete

REVISIONは、そのReleaseの現在のバージョンを示す数値で、1から始まりhelm upgradeするたびに1ずつ増えていきます。

helm history

helm rollback: Releaseを前のリビジョンに戻す

Releaseの状態を指定したリビジョンに戻します。

$ helm rollback staging 1
Rollback was a success! Happy Helming!

$ helm history staging
REVISION        UPDATED                         STATUS          CHART           DESCRIPTION
1               Mon May 28 00:01:37 2018        SUPERSEDED      demoapp-0.0.1   Install complete
2               Mon May 28 00:07:00 2018        SUPERSEDED      demoapp-0.0.2   Upgrade complete
3               Mon May 28 00:10:31 2018        DEPLOYED        demoapp-0.0.1   Rollback to 1

helm rollback

helm get: Releaseの詳細を表示

指定したReleaseに関する下記の情報を表示することができます。

  • Chartの名称やデプロイ日時などの基本情報
  • values.yaml--set-fオプションなどの値をマージした最終的なValuesオブジェクト
  • 全テンプレートのレンダリング済みマニフェスト
$ helm get staging
REVISION: 1
RELEASED: Mon May 28 00:01:37 2018
CHART: demoapp-0.0.1
USER-SUPPLIED VALUES:
ingress:
  host: demoapp-puma.192.168.64.29.nip.io

COMPUTED VALUES:
env:
  configmap:
    MYSQL_DATABASE: demoapp_production
    MYSQL_USER: demoapp
    RAILS_LOG_TO_STDOUT: "true"
    RAILS_SERVE_STATIC_FILES: "true"
(省略)

HOOKS:
MANIFEST:

---
# Source: demoapp/templates/mysql-env-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: staging-demoapp-mysql-env
data:
  MYSQL_PASSWORD: c2VjcmV0
  MYSQL_ROOT_PASSWORD: dG9wc2VjcmV0
(省略)

helm get

helm diff: Chartの差分を表示

helmコマンドはプラグイン機構により拡張することが可能です。

利用可能なプラグインは下記のページに掲載されています。

HELM PLUGINS

ここではプラグインの一つHelm Diffを紹介します。

helm pluginコマンドでインストールできます。

$ helm plugin install https://github.com/databus23/helm-diff

helm diffコマンドを使うと、helm upgradeにより発生する差分をdiff形式で表示することができます。

% helm diff upgrade staging . --set ingress.host=demoapp-puma.$(minikube ip).nip.io --set rails.image.tag=0.0.2
staging-demoapp-sidekiq, Deployment (apps/v1) has changed:
  # Source: demoapp/templates/sidekiq-deploy.yaml
  apiVersion: apps/v1
  kind: Deployment
(省略)
-           image: demoapp:0.0.1
+           image: demoapp:0.0.2
(省略)
staging-demoapp-puma, Deployment (apps/v1) has changed:
  # Source: demoapp/templates/puma-deploy.yaml
  apiVersion: apps/v1
  kind: Deployment
(省略)
-           image: demoapp:0.0.1
+           image: demoapp:0.0.2
(省略)

また、helm rollbackで発生する差分や、過去のリビジョン同士の差分を表示することもできます。 詳細はHelm Diffのドキュメントを参照してください。

$ helm diff -h

おわりに

Railsアプリ開発のためのDocker/Kubernetes入門を主題として、 Docker/Kubernetesの基本と、小さなRailsアプリケーションをHelmでk8sクラスタにデプロイする方法について書きました。

Docker/Kubernetesを本番サービスへ投入するのであれば、さらに下記について学ぶと良いと思います。

クラウドを使うならGKEが簡単でおすすめですが、GKEでHelmを使おうとするとRBACが最初のハードルになると思います。 次回はその辺りについて書きたいと思います。

広告