Table of Contents
- 1. 前提
- 2. 準備
- 3. Helmとは
- 4. Helmをk8sにインストールする方法
- 5. Helmによるパッケージ管理のチュートリアル
- 6. NamespaceとContextについて
- 6.1. kubectl config get-contexts: Contextの一覧の表示
- 6.2. kubectl config current-context: 現在有効になっているContext名の表示
- 6.3. kubectl config view: Contextの詳細の表示
- 6.4. kubectl config set-context: Contextの作成または更新
- 6.5. kubectl config use-context: 有効なContextを変更
- 6.6. kubectl config delete-context: Contextの削除
- 6.7. kubectxとkubens
- 6.8. Namespaceの削除について
- 7. Chartの作り方
- 8. 自作のChartによるワークフロー
- 9. おわりに
RailsアプリケーションをKubernetes(以後、k8s)で運用できるようにするための手順を書きます。 この記事はシリーズ連載記事の第六回です。
- 第一回 Docker編
- 第二回 Docker Compose/Dockerfile編
- 第三回 Kubernetes入門編
- 第四回 Kubernetes基礎編
- 第五回 Kubernetes応用編
- 第六回 Helm編
はじめにHelmについて簡単に紹介した後、 前回の Kubernetes応用編 で作成したマニフェストをもとにHelmチャートの作り方を説明します。
サンプルコードは全て下記のリポジトリにあります。
https://github.com/kwhrtsk/rails-k8s-demoapp
前提
準備
まずサンプルアプリのコードをチェックアウトして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
という名前をつけていますが、
本来はstaging
やproduction
など環境を示すような名前をつける方が良いかもしれません。
名前を指定しない場合はhappy-panda
のように辞書から単語を二つ適当に拾ってきてくっつけたみたいな名前が自動的に与えられます。
helm upgrade
やhelm delete
など、特定のRelease
を対象とするコマンドはこの名前で対象を指定するので、
できるだけわかりやすい名前を自分でつけるのがおすすめです。
その他のオプションの意味は次の通りです。
--namespace
オプションでは、このChartのAPIオブジェクトを作成するNamespace
をsentry
にしています。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内部での暗号処理の鍵です。省略するとランダムな文字列が自動的に割り当てられます。*2postgres.postgresPassword
: PostgreSQLサーバのパスワードです。省略するとランダムな文字列が自動的に割り当てられます。*2redis.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-init
のsentry
部分はRelease
の名前です。別の名前でhelm install
するとここも変わるので注意してください。
後述するようにネームスペースごと削除してしまう方が簡単です。
$ kubectl delete ns sentry
NamespaceとContextについて
k8sにはNamespace
というオブジェクトがあり、Deployment
やService
など主要なオブジェクトは基本的にいずれかの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
オプションをつけると、コマンド名をkctx
とkns
に変更できます。
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
を作成したり編集したりする機会はそれほど多くないと思いますので、
kctx
とkns
の使い方を覚えておけばほとんどの場合十分です。
Namespaceの削除について
helm install
で--namespace
オプションを指定すると、当該ネームスペースがない場合は自動的に作成されます。
このネームスペースはhelm delete
しても残ります。
ネームスペースを削除する場合にはkubectl delete
を使います。
$ kubectl delete namespace sentry
# または
$ kubectl delete ns sentry
ネームスペース内のオブジェクトが全て削除される点に注意してください。
永続ボリュームも確認無しで削除されるため、事故によるデータロストに十分注意する必要があります。
PV
オブジェクトが削除された場合に、データの実体(GCPのPersistentDiskやAWSのEBSなど)が削除されず残るようにするためには、
StorageClass
のreclaimPolicyをRetain
に変更しておく必要があります。
Chartの作り方
前節では既存のパッケージを使う方法を見てきました。 今節では自分でパッケージを作り、k8sクラスタにデプロイする方法を見ていきます。
具体的には、第2回で取り上げたサンプルアプリのHelm Chartを作成します。 構成はStep4のマニフェストファイルと同等にします。
最終的なコードは下記に置いてあります。
なお、このドキュメントでは作った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
staging
はRelease
の名前です。
--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は、Deployment
とService
を一つずつ作成して、
Ingress
経由でそれを外部に公開するというk8sの基本構成が記述されています。
なので、それらを一旦削除します。
rm demoapp/values.yaml demoapp/templates/*.yaml
次に、Step4のマニフェスト一式をdemoapp/templates/
以下にコピーします。
cp manifests-step4/*.yaml demoapp/templates/
さて、これでおおよそ動くChartができました。Ingress
以外はこのままでも動作します *1。
demoapp/templates/
に置くのはテンプレートなので、テンプレートとしての構文を一切使わないYAMLのままでもとりあえず動くのです。
マニフェストファイルが存在するプロジェクトをHelm Chart化する場合には、
このように一旦templates/
ディレクトリに置いてから各マニフェストファイルをテンプレート構文で書き換えていくことになります。
そうやって作成した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です。 ただし、下記のように拡張されています。
- Sprigのtemplate functionが50個ほど使えるようになっています。
- ValuesやChart, Releaseなどの組み込みオブジェクトが追加されています。
はじめに、Step4時点でのpuma
のDeployment
を例に、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.tpl
でdefine
により定義した値を埋め込むことになります。
この例では、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.fullname
、demoapp.puma.name
, demoapp.rials-env.name
の三つのdefine
があります。
前者はhelm create
した時から存在するもので、残り二つは筆者が追加したものです。
結構ボリュームがあるのですが、ほとんどの状況では次のようなシンプルな結果になります。
demoapp.fullname
は、Chart.yamlに書かれたname
フィールドの値にRelease
の名前をプレフィクスとしてつけた文字列を返します。 サンプルの場合は、Release
の名前がstaging
ならstaging-demoapp
になります。demoapp.puma.name
は、Release
がstaging
の場合はstaging-demoapp-puma
になります。include
は、define
定義の中でさらに別の部分テンプレートを参照する時に使います。demoapp.rails-env.name
は、Release
がstaging
の場合は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
元々app
とcomponent
だったところに、chart
、release
、heritage
が追加されています。
これらのラベルは、Helmの公式ドキュメントでBest Practiceのページに標準的なラベルとして定義されているものです。
STANDARD LABELS - Chart Best Practices
chart
はChartの名前、release
はRelease
の名前を示すラベルです。
heritage
は常にTiller
という文字列になります。これはこのオブジェクトがHelm(Tiller)によって管理されているということを示します。
release
のみ、.spec.selector.matchLabels
にも追加されている点に注意してください。
これは同一のChartが同じNamespace
内に複数のRelease
としてデプロイされた場合に、適切に分離されるために必要です。
chart
とheritage
は動作に直接影響しませんが、慣例として推奨されているラベルです。
アノテーション
最後にアノテーションについて説明します。
下記のように、マニフェストにはなかった.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
には、運用上少し面倒な制限があります。
それは、ConfigMap
やSecret
の内容を更新したとしても、それを参照する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
でもConfigMap
とSecret
を参照していますが、
こちらは初回起動時に作成する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"
{{ }}
の中身はパイプラインであり、
|
演算子を使って別の関数のパラメータとすることができます。
関数が二つ以上のパラメータをとる場合、|
の後ろの関数には最後のパラメータとして|
の前の結果が渡されます。
比較的よく使われる関数にdefault
とquote
があります。これを使った例を一つ見てみましょう。
{{ .Values.rails.testValue | default "abc" | b64enc | quote }}
default
は、二つ目のパラメータが空の場合は一つ目の値を、空でない場合は二つ目の値を返す関数です。
quote
は、パラメータの前後に"
をつけた文字列を返す関数です。
上記の例では、.Values.rails.testValue
をBASE64エンコードして"
で囲むという処理です。
.Values.rails.testValue
が空だった場合はabc
という文字列を代わりにエンコードしてクォートします。
フロー制御
今回のサンプルでは特に使っていないのですが、テンプレートではif/else
による分岐やrange
によるループを使用できます。
stable/sentry
のingress.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
の下にまとめています。こうしておくとsopsやyaml_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として使用することができます。
例えばstable/sentry
パッケージはstable/postgres
とstable/redis
をSubchartとして使用しており、
PostgreSQLとRedisのセットアップはこれらのChartに依存しています。
今回、rails-k8s-demoapp
のChartではあえてstable/mysql
とstable/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
を例に説明しました。
その他のオブジェクトのマニフェストについてもだいたい同じような手順でテンプレート化できるため、 それらについての詳細な説明は割愛します。コードを見てみてください。
自作のChartによるワークフロー
以下についてはすでに説明しました。
この節では、アプリケーションを運用する上でさらに必要になるであろういくつかのオペレーションを説明します。
helm upgrade: Chartの更新を反映
コマンド自体は初回デプロイと同じhelm upgrade
を使いますが、
アプリケーションを更新する場合には次のいずれかの方法でDeployment
のイメージを変更する必要があります。
--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
values.yaml
ファイルの.rails.image.tag
とChart.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.yaml
のversion
も更新しておくと、
後に紹介するhelm history
とgitのログでインフラ構成の履歴を追うのが楽になります。
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 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 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 diff: Chartの差分を表示
helm
コマンドはプラグイン機構により拡張することが可能です。
利用可能なプラグインは下記のページに掲載されています。
ここではプラグインの一つ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を本番サービスへ投入するのであれば、さらに下記について学ぶと良いと思います。
- GCPやAWSのようなパブリッククラウドでk8sを使う方法(GKEやkops)
- ロールベースのアクセス制御(Using RBAC Authorization)
- k8sクラスタ自体やその上で稼働するアプリケーションのリソース監視(Prometheusとの統合など)
- Dockerイメージのさらなるスリム化の手法
Pod
をどのノードに配置するかを制御する方法(Assigning Pods to Nodes、Taints and Tolerations)Pod
に割り当てるCPUやメモリなどの計算資源を制御する方法(Managing Compute Resources for Containers)- 負荷に応じて動的に
Pod
の数を増減させる方法(Horizontal Pod Autoscaler)
クラウドを使うならGKEが簡単でおすすめですが、GKEでHelmを使おうとするとRBACが最初のハードルになると思います。 次回はその辺りについて書きたいと思います。