Table of Contents
- 1. 前提
- 2. 準備
- 3. 概要
- 4. Railsアプリのイメージをminikube上で使えるようにする方法
- 5. Step1: Deployment, Service, ConfigMap, Secret
- 5.1. mysql-deploy.yaml: mysql の Deployment
- 5.2. mysql-svc.yaml: mysql の Service
- 5.3. mysql-env-cm.yaml: mysqlの環境変数用ConfigMap
- 5.4. mysql-env-secret.yaml: mysqlの環境変数用Secret
- 5.5. redis-deploy.yaml: redis の Deployment
- 5.6. redis-svc.yaml: redis の Service
- 5.7. puma-deploy.yaml: puma の Deployment
- 5.8. puma-svc.yaml: puma の Service
- 5.9. rails-env-cm.yaml: pumaとsidekiqに共通の環境変数用ConfigMap
- 5.10. rails-env-secret.yaml: pumaとsidekiqに共通の環境変数用Secret
- 5.11. sidekiq-deploy.yaml: sidekiq の Deployment
- 5.12. まとめ
- 6. 参考情報
RailsアプリケーションをKubernetes(以後、k8s)で運用できるようにするための手順を書きます。 この記事はシリーズ連載記事の第四回です。
- 第一回 Docker編
- 第二回 Docker Compose/Dockerfile編
- 第三回 Kubernetes入門編
- 第四回 Kubernetes基礎編
- 第五回 Kubernetes応用編
- 第六回 Helm編
今回は以下のサンプルアプリケーションをminikubeにデプロイするためのマニフェストや手順について説明します。
https://github.com/kwhrtsk/rails-k8s-demoapp
前提
準備
まずサンプルアプリのコードをチェックアウトしてminikubeを起動してください。 (前回と同じなのですでにやっている人は読み飛ばしてください)
# サンプルアプリのコードをチェックアウト
$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git
$ cd rails-k8s-demoapp
# minikubeを起動
$ minikube start --cpus=3 --memory=2048 --vm-driver=hyperkit --disk-size=12g
# kubernetesのダッシュボードをオープン
$ minikube dashboard
概要
前回は下記について説明しました。
- APIオブジェクトを定義するマニフェストファイルをYAML形式で記述する方法
- マニフェストファイルと
kubectl
コマンドを使ってAPIオブジェクトを管理する方法
今回は実際にRailsアプリをMySQLやRedisなどのミドルウェアも含めて丸ごとk8sクラスタ上にデプロイするための手順を説明します。 一度に多種のAPIオブジェクトを使うと全体を理解するのが難しくなるので、 4つのステップに分けて進めます。
Step1では、第2回Docker Compose/Dockerfile編
のdocker-compose-preview.yml
に相当する構成をできるだけ少ないAPIオブジェクトで簡潔に記述します。
この時点では簡潔さと引き換えにいくつかの制約がありますが、それらをStep2からStep4で解消していきます。
今回のKubernetes基礎編ではStep1を説明し、 次回のKubernetes応用編でStep2以降を説明します。
Railsアプリのイメージをminikube上で使えるようにする方法
マニフェストの説明に入る前に、ビルドしたRailsアプリのイメージをminikube上にデプロイできるようにする方法を説明します。
いくつかの方法があります。
- Docker Hubのようなパブリックレジストリに登録する。
- AWSのECRやGCPのContainer Registry のようなプライベートレジストリサービスに登録する。
- プライベートレジストリサービスを自前で用意してそこに登録する。
- minikube VM上のDockerプロセス上に接続して直接イメージをビルドする。
今回は一番簡単な4の方法を使います。
準備 の節を参照して、サンプルコードをチェックアウトしminikubeを起動したら、下記のコマンドを実行してください。
$ (eval $(minikube docker-env) && docker build . -t demoapp:0.0.1)
minikube docker-env
コマンドは、minikube VM上のDockerプロセスに接続できるようにするための環境変数を出力します。
上記のようにすることでminikube VM上のDockerプロセス上で直接イメージをビルドします。
これにより、マニフェストではイメージ名をdemoapp:0.0.1
と指定すればこのイメージを使えるようになります。
この方法を使う場合、タグにはlatest
以外の値を指定してください。
k8sの仕様で、タグがlatest
になっているイメージを指定するとコンテナの起動前に必ずレジストリからイメージをpullしようとするため、
4の方法だと必ず失敗するようになってしまいます。
これは4の方法を使う場合の制約ではあるのですが、
インフラの構成管理という観点ではそもそもイメージのタグにlatest
を使うとデプロイされるバージョンがタイミング依存になるため、
4以外の方法でもlatest
タグの使用は避けたほうが良いと思います。
Step1: Deployment, Service, ConfigMap, Secret
Step1では、第2回Docker Compose/Dockerfile編の
Composeファイルdocker-compose-preview.yml
に相当する構成を(いくつかの制約と引き換えに)できるだけ簡潔に記述します。
# docker-compose-preview.yml
version: "3"
services:
puma:
image: demoapp
build:
context: .
env_file:
- .dockerenv/rails
ports:
- 3000:3000
depends_on:
- mysql
- redis
command: ./bin/setup-db-and-start-puma
sidekiq:
image: demoapp
env_file:
- .dockerenv/rails
depends_on:
- puma
command: ./bin/start-sidekiq
mysql:
image: mysql:5.7.21
env_file:
- .dockerenv/mysql
redis:
image: redis:4.0.9
command: redis-server --appendonly yes
このComposeファイルの要点は下記の通りです。
- puma, sidekiq, mysql, redisの4つのサービス(コンテナ)を起動
- puma, sidekiqコンテナに設定する環境変数は
.dockerenv/rails
にまとめて記述 - mysqlコンテナに設定する環境変数は
.dockerenv/mysql
にまとめて記述 - pumaコンテナには3000番ポートで接続可能
- データベーススキーマの初期化(
rails db:setup
)はpumaコンテナの起動時に実行- 詳細は前回の記事の
./bin/setup-db-and-start-puma
の解説を参照
- 詳細は前回の記事の
これと同等の構成をk8sで実現するために、Deployment
、Service
、ConfigMap
、Secret
の4種類のAPIオブジェクトを使います。
Deployment
は、Pod
(≒コンテナ)の起動管理を行うオブジェクトです。
Service
は、Pod
へアクセスするためのI/Fを提供します。
この二つは前回のKubernetes入門編で解説したので詳細はそちらを参照してください。
Deployment
とService
の組み合わせで、Docker Composeにおけるservice
に相当する機能になります。
今回の例だと、mysql
, redis
, puma
のサービスには外部のコンテナまたはクラスタの外部(ブラウザ)からアクセスする必要があるため、
Deployment
とService
を一組ずつ定義します。
sidekiq
は外から参照する必要がないため、Deployment
のみ定義します。
ConfigMap
とSecret
は、Deployment
に設定する環境変数の管理に使います。
今回の使い方ではComposeファイルにおける.dockerenv/mysql
と.dockerenv/rails
に相当します。
使用するマニフェストファイルは以下の11個で、全てk8s/manifests-step1/に置いてあります。 一つのYAMLファイルに一つのAPIオブジェクトの定義を書いています。
- mysql-deploy.yaml
- mysql-env-cm.yaml
- mysql-env-secret.yaml
- mysql-svc.yaml
- redis-deploy.yaml
- redis-svc.yaml
- puma-deploy.yaml
- puma-svc.yaml
- rails-env-cm.yaml
- rails-env-secret.yaml
- sidekiq-deploy.yaml
これから順に内容を説明していきますが、まずはデプロイを実行してみましょう。
準備 の節を参照して、サンプルコードをチェックアウトしminikubeを起動したら、下記のコマンドを実行してください。 デプロイが済んだ後、ブラウザでサンプルアプリを開くことができます。
# Railsアプリのイメージをビルド
$ (eval $(minikube docker-env) && docker build . -t demoapp:0.0.1)
$ cd k8s/manifests-step1
# APIオブジェクトを作成
$ cat *.yaml | kubectl apply -f -
# pumaとsidekiqの起動が完了するまで待機
$ kubectl rollout status deploy demoapp-puma
$ kubectl rollout status deploy demoapp-sidekiq
# puma serviceのエンドポイントをブラウザでオープン
$ minikube service demoapp-puma
上記の手順と等価なタスクをMakefileに定義してあるので、代わりにmake
コマンドで実行することもできます。
$ cd k8s/manifests-step1
$ make kubectl-apply
$ make kubectl-rollout-status
$ make minikube-service
# またはパラメータなしの make で全て順に実行
APIオブジェクトを削除するには次のようにします。
$ cd k8s/manifests-step1
$ cat *.yaml | kubectl delete -f -
# または make clean
Makefileは下記のような内容です。GNU Makeで動作を確認しています。
# k8s/manifests-step1/Makefile
SHELL = /bin/bash
all:
$(MAKE) minikube-docker-build
$(MAKE) kubectl-apply
$(MAKE) kubectl-rollout-status
$(MAKE) minikube-service
clean: kubectl-delete
minikube-docker-build:
eval $$(minikube docker-env) && docker build ../../ -t demoapp:0.0.1
kubectl-apply:
cat *.yaml | kubectl apply -f -
kubectl-rollout-status:
kubectl rollout status deploy demoapp-puma
kubectl rollout status deploy demoapp-sidekiq
minikube-service:
minikube service demoapp-puma
kubectl-delete:
cat *.yaml | kubectl delete -f -
スクリプト化するまでもないような短いコマンドであっても、 定型的な処理はこのように形として残しておくのがおすすめです。 こういった小さな積み重ねがチーム内に暗黙知が生まれることを防ぎます。
次に、各マニフェストファイルの内容を説明します。
mysql-deploy.yaml: mysql の Deployment
はじめにMySQLに関連するマニフェストの内容を説明します。
まずは Deployment
から。
# k8s/manifests-step1/mysql-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp-mysql
labels:
app: demoapp
component: mysql
spec:
replicas: 1
selector:
matchLabels:
app: demoapp
component: mysql
template:
metadata:
labels:
app: demoapp
component: mysql
spec:
restartPolicy: Always
containers:
- name: mysql
image: mysql:5.7.21
livenessProbe:
tcpSocket:
port: 3306
readinessProbe:
tcpSocket:
port: 3306
envFrom:
- configMapRef:
name: demoapp-mysql-env
- secretRef:
name: demoapp-mysql-env
最初なので全般事項についてもここで説明します。
.metadata.name
にはdemoapp-mysql
のようにdemoapp-
というプレフィクスをつけます。 例えば、Service
オブジェクトの名前をredis
にしてしまうと、それ以降に起動する全てのコンテナにk8sによってREDIS_PORT
のような環境変数が設定されるのですが、 sidekiqなど一部のgemはこういった環境変数によって動作が変わってしまう場合があります。 名前のプレフィクスはこのような環境変数とgemなどで一般的に使用される環境変数が競合するのを避けるのが目的です。 自動設定される環境変数の詳細についてはドキュメントを参照してください。- 各種ラベルには、共通で
app
ラベルにdemoapp
を、コンポーネントごとにcomponent
ラベルを設定します。 コンポーネントはpuma
,sidekiq
,mysql
,redis
などです。 - 同一のコンポーネントに属するAPIオブジェクトには、同一の名前とラベルを設定します。
例えば、
mysql
用のDeployment
、Service
には.metadata.name
に共通のdemoapp-mysql
という名前をつけます。 また、.metadata.labels
にはapp: demoapp
,component: mysql
の二つを設定します。 - マニフェストのファイル名には、コンポーネントとAPIオブジェクトの種類の略称を使います。
例えば
mysql
のDeployment
であればmysql-deploy.yaml
となります。 略称については 前回のkubectl get
の節を参照してください。
次に、mysql
のための固有の設定について説明します。
livenessProbe
とreadinessProbe
は、コンテナの死活監視のための設定項目です。
この例では3306番ポートにTCP接続できるかどうかで判定を行なっています。
また、上記では使用していませんが、それぞれinitialDelaySeconds
(最初に検査を行うまでの待機秒数)やperiodSeconds
(検査の間隔: デフォルト10秒)
などの項目を設定することができます(詳細)。
また、コマンドを実行したりHTTPリクエストを投げることもできます。
livenessProbe
で死亡判定されると、ReplicaSet
はrestartPolicy
に基づいてそのPod
の再起動か再作成を試みます。
条件が厳しすぎるとプロセスの起動処理中に強制的に再起動されていつまで経っても起動しなくなります。
その場合はinitialDelaySeconds
などの値を緩めるなどして調整する必要があります。
readinessProbe
はService
オブジェクトがそのPod
に接続リクエストを転送するかどうかの判定に使います。
こちらは起動直後のPod
にリクエストを転送するのを防ぐことなどを目的に使用します。
envFrom
は環境変数を設定するための項目です。
この例では ConfigMap
と Secret
という別のAPIオブジェクトを参照して環境変数を設定しています。
ConfigMap
とSecret
については後述します。
env
を使うと直接Deployment
の定義の内部に環境変数を定義することもできます。詳細は下記を参照してください。
Define Environment Variables for a Container | Kubernetes
mysql-svc.yaml: mysql の Service
続いてmysql-service.yaml
の内容を説明します。
# k8s/manifests-step1/mysql-svc.yaml
---
apiVersion: v1
kind: Service
metadata:
name: demoapp-mysql
labels:
app: demoapp
component: mysql
spec:
ports:
- protocol: TCP
port: 3306
selector:
app: demoapp
component: mysql
.spec.selector
にapp
とcomponent
という二つのラベルを指定しています。
これはmysql-deploy.yaml
の.spec.template.metadata.labels
と一致させる必要があります。
前回のKubernetes入門編ではapp
というラベル一つで
Deployment
とService
を接続していましたが、
今回はDeployment
がmysql
の他にredis
やpuma
など複数存在するため、
app: demoapp
というラベルだけだとmysql
のService
がredis
やpuma
のPod
を参照して混線することになります。
これを避けるためにapp
とcomponent
という二つのラベルをセレクタに指定しています。
mysql-env-cm.yaml: mysqlの環境変数用ConfigMap
次にmysql-env-cm.yaml
を説明します。
# k8s/manifests-step1/mysql-env-cm.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: demoapp-mysql-env
data:
MYSQL_USER: demoapp
MYSQL_DATABASE: demoapp_production
ConfigMap
は環境変数や設定ファイルなどアプリケーションのパラメータを管理するためのAPIオブジェクトです。
使用形態には大まかに3つの方法があります。
envFrom
で丸ごと環境変数に設定する。env
でキーを指定して個別に環境変数に設定する。- キーを指定して値をファイルとして
Pod
上のファイルにマウントする。
mysql-deploy.yaml
では1の方法を使っています。関係ある部分だけ抜き出すと下記の通りです。
# k8s/manifests-step1/mysql-deploy.yaml
---
spec:
template:
spec:
containers:
- name: mysql
image: mysql:5.7.21
envFrom:
- configMapRef:
name: demoapp-mysql-env # ConfigMapの `.metadata.name` を指定
このように参照すると、ConfigMapの.data
の各エントリが全て環境変数として設定されます。
2の方法で同じことをしようとすると下記のようになります。
# k8s/manifests-step1/mysql-deploy.yaml
---
spec:
template:
spec:
containers:
- name: mysql
image: mysql:5.7.21
env:
- name: MYSQL_USER # 環境変数名を指定
valueFrom:
configMapKeyRef:
name: demoapp-mysql-env # ConfigMapの`.metadata.name`を指定
key: MYSQL_USER # ConfigMapの`.data`エントリのキーを指定
- name: MYSQL_DATABASE
valueFrom:
configMapKeyRef:
name: demoapp-mysql-env
key: DATABASE
だいぶ冗長になりました。envFrom
は比較的最近追加された機能なので以前はこの方法で書いていましたが大変でした。
ConfigMap
はできるだけ用途に応じた単位で分割して、envFrom
を使えるようにした方が良いと思います。
3の方法は、例えば/etc/mysql/my.cnf
のような設定ファイルをConfigMap
で管理する方法です。
.data
エントリに定義した値をファイルとしてPod
上の任意のパスにマウントすることができます。
この方法についてはこのドキュメントでは扱いませんので、詳細は下記の資料を参照してください。
Configure a Pod to Use a ConfigMap | Kubernetes
ConfigMap
とSecret
については、APIオブジェクトの名前(.metadata.name
)を
${プレフィクス}-${コンポーネント}
の形式ではなく、さらに-env
を付け加えてdemoapp-mysql-env
のような形にしています。
これは将来的に環境変数に加えて3の方法で設定ファイルを追加する場合に名前が競合するのを防ぐ意図があります。
(設定ファイル用のConfigMap
を追加する場合はdemoapp-mysql-conf
のような名前にするのが良いと思います)
mysql-env-secret.yaml: mysqlの環境変数用Secret
最後にmysql-env-secret.yaml
を説明します。
# k8s/manifests-step1/mysql-env-secret.yaml
---
apiVersion: v1
kind: Secret
metadata:
name: demoapp-mysql-env
data:
MYSQL_PASSWORD: c2VjcmV0 # echo -n "secret" | base64
MYSQL_ROOT_PASSWORD: dG9wc2VjcmV0 # echo -n "topsecret" | base64
Secret
はConfigMap
同様キー・バリュー形式で設定値を管理できるAPIオブジェクトですが、
パスワードやクレデンシャルなど機密性の高い情報を管理することを前提としています。
Secret
は、ConfigMap
と比較すると表面的には下記の点が異なります。
.data
エントリに値を書き込むときはBASE64エンコードして書く必要がある。- Dashboard上ではデフォルトで値が表示されないようになっている。(クリックで表示させることはできる)
- GCPのConsole上だと表示できない
また、Secret
の中身はPod
のtmpfs上に展開されディスクには書き込まれないなど、
内部的にはConfigMap
よりも安全性に配慮された運用が行われます。
セキュリティ上の制限については下記のドキュメントを参照してください。
Encrypting Secret Data at Rest | Kubernetes
Deployment
からの参照方法はほとんどConfigMap
と同じなので説明を割愛します。
configMapRef
の代わりにsecretRef
を使う必要がある点にだけ注意してください。
なお、Secret
オブジェクトのマニフェストファイルにはBASE64エンコードしただけの値を書いているので平文で機密情報を書いているのと同じです。
通常はこのままgitリポジトリにコミットすることはしません。
実際の運用の際には sopsやyaml_vault とGCP/AWSのKMSで暗号化するのがおすすめですが、それについてはHelm編で紹介したいと思います。
redis-deploy.yaml: redis の Deployment
次にRedisに関連するマニフェストの内容を説明します。
まずは Deployment
から。
# k8s/manifests-step1/redis-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp-redis
labels:
app: demoapp
component: redis
spec:
replicas: 1
selector:
matchLabels:
app: demoapp
component: redis
template:
metadata:
labels:
app: demoapp
component: redis
spec:
restartPolicy: Always
containers:
- name: redis
image: redis:4.0.9
livenessProbe:
tcpSocket:
port: 6379
readinessProbe:
tcpSocket:
port: 6379
command:
- redis-server
- --appendonly
- "yes"
構造としては mysql-deploy.yaml
とほとんど同じです。
command
に関しては、docker-compose-preview.yaml
では下記のように文字列のエントリとして定義していましたが、
k8sのマニフェストでは上記の通り配列として定義する必要がある点に注意してください。
services:
redis:
image: redis:4.0.9
command: redis-server --appendonly yes
redis-svc.yaml: redis の Service
続いて Service
の内容を確認します。
# k8s/manifests-step1/redis-svc.yaml
---
apiVersion: v1
kind: Service
metadata:
name: demoapp-redis
labels:
app: demoapp
component: redis
spec:
ports:
- protocol: TCP
port: 6379
selector:
app: demoapp
component: redis
component
とポート番号が違うだけでmysql
のものとほとんど同じです。
特筆すべき点はありません。
puma-deploy.yaml: puma の Deployment
いよいよRailsアプリ本体のためのマニフェストの説明に移ります。
まずはpuma
用のDeployment
から説明します。
# k8s/manifests-step1/puma-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp-puma
labels:
app: demoapp
component: puma
spec:
replicas: 1
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/setup-db-and-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
image
にはdocker buildコマンドで指定したイメージ名を指定します。imagePullPolicy
はIfNotPresent
を指定します。前述の通りこのイメージはレジストリに登録していないためです。command
にはdocker-compose-preview.yml
と同じく./bin/setup-db-and-start-puma
を指定します。 これはpumaの起動前にrails db:setup
を試みるスクリプトです。詳細は第2回の記事で説明しました。livenessProbe
とreadinessProbe
ではそれぞれ/health_check/full
というパスを指定しています。 このサンプルアプリではhealth_checkというgemで死活監視用のエンドポイントを実装しています。 このパスを叩くだけでMySQLやRedisとの接続検証を行うことができるようになります。envFrom
では、demoapp-rails-env
という名前のConfigMap
とSecret
を参照しています。 これらのオブジェクトはdocker-compose-preview.yml
におけるenv_file
に相当する環境変数の設定を保持しています。
puma-svc.yaml: puma の Service
次にpuma用のService
の定義を確認します。
# k8s/manifests-step1/puma-svc.yaml
---
apiVersion: v1
kind: Service
metadata:
name: demoapp-puma
labels:
app: demoapp
component: puma
spec:
type: NodePort
ports:
- protocol: TCP
port: 3000
selector:
app: demoapp
component: puma
.spec.type
にNodePort
を指定しています。
mysql
やredis
と異なり、puma
の接続相手は他のコンテナではなくクラスタの外にあるブラウザです。
そのため、クラスタ内での通信のためのエンドポイントを生成するClusterIP
の代わりにNodePort
を指定しています。
NodePort
を指定すると、k8sクラスタを構成する全てのノードに、このサービスへ接続するためのエンドポイントが作成されます。
.ports[*].nodePort
で公開するポートを指定することもできますが、使用中のポートを指定するとエラーになります。
無指定の場合は、30000-32767
のレンジから自動的に割り当てられます。
いずれの場合も、ポート番号は全てのノードで同じになります。
minikubeの場合、このサービスへ接続するためのURLはminikube service list
で確認することができます。
% minikube service list
|-------------|----------------------|----------------------------|
| NAMESPACE | NAME | URL |
|-------------|----------------------|----------------------------|
| default | demoapp-mysql | No node port |
| default | demoapp-puma | http://192.168.64.25:32320 |
| default | demoapp-redis | No node port |
| default | kubernetes | No node port |
| kube-system | default-http-backend | http://192.168.64.25:30001 |
| kube-system | kube-dns | No node port |
| kube-system | kubernetes-dashboard | http://192.168.64.25:30000 |
|-------------|----------------------|----------------------------|
また、minikube service demoapp-puma
のようにService
の名前をパラメータとして渡せば、そのURLをブラウザでオープンすることができます。
典型的には、http://$(minikube ip):32320
のようにminikube ip
にランダムなポート番号を与えた形式のURLでアクセスできるようになります。
さて、前述のようにNodePort
を指定するとk8sクラスタを構成する全てのノードにエンドポイントが用意されます。
minikubeのようにシングルノード構成の検証用クラスタではこれで十分ですが、
マルチノード構成の本番環境では不十分です。
少なくとも負荷分散や可用性担保のためには前段にロードバランサが必要になるし、
k8sの外側でロードバランサの管理をするのは面倒です。
外向けのエンドポイントにロードバランサを組み合わせる場合、
通常はLoadBalancer
タイプのService
かIngress
というオブジェクトを使います。
次回Kubernetes応用編のStep4
ではIngress
を使ってこの問題に対処する方法を示します。
rails-env-cm.yaml: pumaとsidekiqに共通の環境変数用ConfigMap
下記のような内容です。MySQLやRedisのホスト名として、それぞれmysql-svc.yaml
とredis-svc.yaml
で定義した
Service
オブジェクトの名前を指定しています。
# k8s/manifests-step1/rails-env-cm.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: demoapp-rails-env
data:
RAILS_SERVE_STATIC_FILES: "true"
RAILS_LOG_TO_STDOUT: "true"
SIDEKIQ_TIMEOUT: "60"
MYSQL_HOST: demoapp-mysql
MYSQL_USER: demoapp
MYSQL_DATABASE: demoapp_production
REDIS_HOST: demoapp-redis
REDIS_URL: redis://demoapp-redis:6379/1
rails-env-secret.yaml: pumaとsidekiqに共通の環境変数用Secret
特筆すべき点は特にありません。
mysql-env-secret.yaml
の説明の際にも述べたとおり、環境変数の値はこの時点では暗号化されていないため、
実際の運用においては sopsやyaml_vault で暗号化するなどしてからgitリポジトリに含める必要があります。
# k8s/manifests-step1/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
sidekiq-deploy.yaml: sidekiq の Deployment
最後にsidekiq用のDeployment
の定義を見ます。
# k8s/manifests-step1/sidekiq-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp-sidekiq
labels:
app: demoapp
component: sidekiq
spec:
replicas: 1
selector:
matchLabels:
app: demoapp
component: sidekiq
template:
metadata:
labels:
app: demoapp
component: sidekiq
spec:
restartPolicy: Always
terminationGracePeriodSeconds: 65
containers:
- name: sidekiq
image: demoapp:0.0.1
imagePullPolicy: IfNotPresent
command:
- ./bin/start-sidekiq
livenessProbe:
exec:
command:
- ./bin/health-check-sidekiq
initialDelaySeconds: 30
envFrom:
- configMapRef:
name: demoapp-rails-env
- secretRef:
name: demoapp-rails-env
内容としてはほぼpuma-deploy.yaml
と同じですが、下記の点に注目してください。
terminationGracePeriodSeconds
はPod
にTERMシグナルを送ってからプロセスの終了を待つ時間です。 この時間を超えるとプロセスはKILLシグナルで強制的に停止されます。デフォルト値は30秒です(参考)。./bin/start-sidekiq
では-t $SIDEKIQ_TIMEOUT
オプションをつけてsidekiq
を起動しており、$SIDEKIQ_TIMEOUT
にはrails-env-cm.yaml
で60秒を指定しているので、terminationGracePeriodSeconds
には65秒を設定しています。 ローリングアップデートやコンテナ終了時の動作の詳細についてはStep2で説明します。livenessProbe
はpuma
と異なりexec
タイプで独自実装のコマンドを呼び出します。内容については後述します。Service
と接続しないためreadinessProbe
は定義しません。envFrom
は完全にpuma
と同じです。厳密にいうと環境変数SECRET_KEY_BASE
は無くても動作するのですが、 設定を簡素化するためにpuma
と共通化しています。
sidekiqワーカープロセスの死活監視
sidekiq
のワーカープロセスには、puma
のようなヘルスチェックのためのインタフェースがありません。
sidekiq
のオプションでPIDをファイルに書き出すようにすればプロセスの存在確認くらいはできるのですが、
そもそもプロセスが停止しているようなケースではコンテナ自体が終了してしまうためk8s側で死亡判定できます。
問題は何らかの原因でsidekiq
プロセスが生きたまま応答しなくなっているようなケースをいかに検出するかです。
sidekiq
のワーカープロセスは定期的に自身のホスト名やプロセスIDなどの情報をRedisに書き込んでいます。
この処理はheartbeatと呼ばれています。
(詳細はこの素敵な絵文字メソッドを参照)
heartbeatは5秒間隔で実行されますが、60秒間更新が無いと自動的にRedisから削除されるようになっています。
heartbeatによってRedisに書き込まれたワーカーのリストはsidekiq
のWeb UI上でも確認することができます。
そしてこのワーカーのリストは、SidekiqのAPIでも取得することができます。
https://github.com/mperham/sidekiq/wiki/API#processes
k8sではPod
のホスト名はPod
の名前と同じになるのでユニークであることが保証されています。
livenessProbe
で実行しているスクリプト./bin/health-check-sidekiq
では、
このAPIを利用してワーカーのリストに自身のホスト名が含まれているかどうかを確認しています。
#!/bin/bash -e
cd $(dirname $0)/..
./bin/rake sidekiq:status
# lib/tasks/sidekiq.rake
namespace :sidekiq do
desc "Health check for sidekiq worker process."
task status: [:environment] do
require "socket"
hostname = Socket.gethostname
if Sidekiq::ProcessSet.new.any? { |ps| ps["hostname"] == hostname }
exit 0
else
exit 1
end
end
end
死活監視が意図したように動作するかどうかは、sidekiq
プロセスにSIGSTOP
シグナルを送ってサスペンドすれば確認できます。
SIGKILL
やSIGTERM
などでは即座にコンテナが終了するため、livenessProbe
を待たずPod
が再起動される点に注意してください。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
demoapp-mysql-57d56b47cd-ztc5k 1/1 Running 0 2m
demoapp-puma-749c456c87-6vhrl 1/1 Running 0 2m
demoapp-redis-58f795f487-v2x4j 1/1 Running 0 2m
demoapp-sidekiq-669cd7cb6c-bfhpf 1/1 Running 0 2m
$ kubectl exec demoapp-sidekiq-669cd7cb6c-bfhpf -- pgrep -l sidekiq
31 sidekiq 5.1.3 app [0 of 25 busy]
$ kubectl exec demoapp-sidekiq-669cd7cb6c-bfhpf -- pkill -STOP sidekiq
この後、60秒ほど待つとSidekiqのWeb UI上でワーカープロセスが消えるのを確認できます。
さらにその後、だいたい10秒に一度の頻度でヘルスチェックが失敗したというログがsidekiq
のPod
のイベントログに表示されます。
3回失敗したところでコンテナがKillされて再起動し、再びSidekiqのWeb UI上に新しいワーカーが
現れるのを確認できます。
$ kubectl get pods --watch
NAME READY STATUS RESTARTS AGE
demoapp-mysql-57d56b47cd-ztc5k 1/1 Running 0 3m
demoapp-puma-749c456c87-6vhrl 1/1 Running 0 3m
demoapp-redis-58f795f487-v2x4j 1/1 Running 0 3m
demoapp-sidekiq-669cd7cb6c-bfhpf 1/1 Running 0 3m
demoapp-sidekiq-669cd7cb6c-bfhpf 1/1 Running 1 5m
$ kubectl describe pod demoapp-sidekiq-669cd7cb6c-bfhpf
(省略)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 5m default-scheduler Successfully assigned demoapp-sidekiq-669cd7cb6c-bfhpf to minikube
Normal SuccessfulMountVolume 5m kubelet, minikube MountVolume.SetUp succeeded for volume "default-token-dzvgp"
Warning Unhealthy 42s (x3 over 1m) kubelet, minikube Liveness probe failed:
Normal Pulled 11s (x2 over 5m) kubelet, minikube Container image "demoapp:0.0.1" already present on machine
Normal Created 11s (x2 over 5m) kubelet, minikube Created container
Normal Started 11s (x2 over 5m) kubelet, minikube Started container
Normal Killing 11s kubelet, minikube Killing container with id docker://sidekiq:Container failed liveness probe.. Container will be killed and recreated.
まとめ
Step1では、第2回Docker Compose/Dockerfile編
のdocker-compose-preview.yml
に相当する構成をDeployment
, Service
, ConfigMap
, Secret
の4種のAPIオブジェクトで記述しました。
この時点では下記に挙げる三つの制約があります。
- pumaコンテナを複数起動すると
rails db:setup
が並列実行されてエラーになる。 - MySQLやRedisのデータが永続化されていないため、コンテナを停止するとデータも消える。
- pumaに外部からアクセスするためのエンドポイントを本番環境で運用するのが難しい。
これらの制約を解消する方法は次回Kubernetes応用編で取り上げたいと思います。