※本記事は Kubernetes3 Advent Calendar 2019 18 日目の記事となっています.
docker-compose で構築した mastodon を k8s 上に 12 時間かけて構築し直す
一言でいうと
この記事では「素の k8s 環境にアプリ構築するのめっちゃ大変ですね」という話を書く.
思いのほか分量が多くなってしまいとても申し訳ない気持ちでいっぱい
動機
勉強がてら, 以前ローカルマシン上に docker-compose で構築した test 用 mastodon を k8s PaaS 上に構築する. ローカルマシンがなくても test 環境を用意できるようにしたい.
k8s 周辺のツール紹介などはよく見かけるが, 大きなアプリを構築したよという話が(コード込みで)あまり目に触れないというのも動機. トラブった時のデバッグ込みでどういう流れでやるのか確認したい.
目標
細かいところまではよくわからんが, k8s にデプロイした mastodon が見た目上まともに動いとるぞ,という状態にする. 少なくとも自分が立てたインスタンス同士で連合を組める状態にしたい.
留意事項
- Mastodon の運用環境を構築して公開する話ではない. 末尾へ運用周りの話をリンクしてあるので参考にどうぞ
- データ移行の話は書けなかった
- k8s の細かい動きとか内部実装については触れない
- kustomize などの個別ツールについては特に解説しない
筆者情報.
- 普段は EKS (AWS の k8s PaaS) を使っている
- Dockerfile 自分で書いたりする
- helm や kustomize もそこそこ触る
- k8s 上でアプリを作ってサービス公開したりとかを一から全部やったことはない
- Network の専門家とかではない. Ingress も触ったことない
k8s で動かしたい container image:mastodon-http-local
Tootsuite 公式の Mastodon docker image を改造したもの. 「Mastodon 何それ」って方はこちら.
主に自宅内ネットワークに閉じたテスト用途で開発した. これを作った動機は以下.
- Mastodon REST API からデータを読み取るテストクライアントを作成していた. のだが連合の挙動込みでローカルに閉じてテストする環境が欲しかった
- 証明書の取得管理に手間がかかる問題
- わざと http (NO TLS) で連合を組むように改造
- ドメインの取得管理に手間がかかる問題
- 代わりに IP アドレスでインスタンス同士が認識できるようにする
SMTP server 情報と有効な Email address を準備して script を叩くと docker-compose で立ち上がる.
そのうち詳細を書く予定だけど k8s とは関係ないので割愛.
作業の流れ
現構成は大雑把に言うと5つのコンテナと1つのリバースプロキシ nginx コンテナからなる. それらをどうやって k8s 上にデプロイするか(追加でどういう設定やツールなどが必要になるか)を書いていく.
- GKE の利用開始
- k8s へ container image をデプロイするための設定を書いた, manifest ファイルを作る
- それぞれ docker-compose でコンテナだったものを k8s へ pod としてデプロイ
- ウェブブラウザから Mastodon を開ける状態にする
- LoadBalancer へのアクセス元を制限する
※以降は実際の作業にかかった時間を h (時間) min(分)で表記
※調べながら作業しているので調査にかかった時間も入っている. 熟達しているならすぐ終わる作業もたくさんあるだろうし, 何ならスクリプトを書いて自動化してもよい, と思う. 要領が悪すぎるという指摘はごもっともで後から考えると色々とさぼれる部分はあった気がする
現構成
- 括弧内は image 名を示す
- web, streaming と sidekiq は共通 image となっているが役割ごとにコンテナを分けてある
- 主要な役割は web が担っている
- Mastodon が載っている VM の port 80 へリクエストが来たら nginx に forward する(ように docker run の
-p
option で指定してある)- nginx は飛んできたリクエストを web と streaming の API に振り分ける
- sidekiq は web から受け取った非同期 job を捌く
今回の作業で得られた最終構成
k8s network の部分はいろいろごまかして書いている(実際には複数の node で構成されている). container が pod に代わっていることに注意. また, あくまで今回の作業結果であって最適な構成ではない.
k8s へ移す際に新たに使うことになったサービスやツール
- GKE (GCP の k8s PaaS)
- 気づかぬうちに GCP 無料枠が $300 になっていたので
- google cloud storage
- kubectl. GCP の CloudShell に最初から入っている
- kompose. なくても良かったかも
- kustomize
- helm v2. GCP の CloudShell に最初から入っている
- kustomize に統一したかったがやむなく利用
GKE の利用開始(大体 15min 程度)
まず GKE アカウント(クレカ)を登録する.
クラスタを作成
上記に沿ってやれば難なく終わる. 以降はほぼ CloudShell 上で作業. 目を離すと割とすぐにセッションが切れるが、つながるのも早いのであまり気にならなかった. 他にやったこととして
- logging 用に BigQuery dataset 作成
- 今回の作業範囲ではなくてもよかった
- gcloud tool インストール(振り返ってみるとほとんど使わなかった)
Spec
k8s へ container image をデプロイするための設定を書いた manifest ファイルを作る
kompose で docker-compose.yaml を kustomize 向けの manifest file へ変換(大体 1h 程度)
-
ここの Binary installation の手順で kompose をインストール
- そんなに何度も使うものでもないのですぐに消して問題ない
-
kompose convert -f docker-compose.yaml
で kustomize 向けの manifest file を生成- docker-compose が参照する
.env
ファイルなども同一ディレクトリへ配置した上で実行する
- docker-compose が参照する
-
kustomize build .
で出力された manifest が文法エラーなくビルド可能なことを確認- GKE の k8s が 1.13 だったので CloudShell 上に kustomize をインストール
- 1.14 以降であれば kubectl に kustomize の機能が同梱されている
当初のファイル構成はこんな感じだった.
.
├── kustomization.yaml
├── db-claim0-persistentvolumeclaim.yaml
├── db-deployment.yaml
├── redis-claim0-persistentvolumeclaim.yaml
├── redis-deployment.yaml
├── sidekiq-claim0-persistentvolumeclaim.yaml
├── sidekiq-deployment.yaml
├── sidekiq-env-development-configmap.yaml
├── streaming-deployment.yaml
├── streaming-env-development-configmap.yaml
├── web-claim0-persistentvolumeclaim.yaml
├── web-deployment.yaml
└── web-env-development-configmap.yaml
各種ファイルのとても大雑把な説明を書くと
- kustomization.yaml はそのデプロイでどの yaml ファイルを読み込むか等を記述
- db-claim0-persistentvolumeclaim から web-env-development-configmap まですべてのファイルがリストアップされていた
- deployment.yaml には pod をどの image を使って立ち上げるかなどを記述
- 似たような役割を持つものに replicaset とか statefulset とかあるのだが今回は割愛
- configmap には pod の環境変数などの設定情報を記述
- deployment に直接記述することも可能だが分けるのが一般的?
- persistentvolumeclaim (pvc) には pod に割り当てる永続化ボリュームの情報を記述
kustomize 向け manifest ファイルを試しにデプロイ
試しに kustomize build . | kubectl apply -f -
ですべての pod をデプロイしてみると当然のように動かない.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
db-54dd975db5-njctb 0/1 CrashLoopBackOff 41 3h10m
redis-99bd86b67-n9c79 1/1 Running 0 3h10m
sidekiq-6b86d8f95f-lvvrk 0/1 CrashLoopBackOff 41 3h10m
streaming-666564bbcf-lmlwg 0/1 CrashLoopBackOff 65 3h10m
web-d644c8c9f-9c9gb 0/1 CrashLoopBackOff 45 3h10m
ので地獄のデバッグが始まる. 被依存層である datastore からひとつひとつ, 次の順番で疎通確認していく.
- db (postgresql)
- redis
- web
- streaming
- sidekiq
デバッグしながらの作業となるので次をどうやるのかも確認する.
- pod の動作(疎通)確認方法
- 個別の pod を立てたり消したりするための pod の uninstall 方法
manifest ファイル最終構成
先に書いてしまうと作業後のファイル構成はこんな感じになった(途中経過を記録し忘れてしまった).
.
├── db(最終的に helm で代用したため使用せず)
│ ├── db-claim0-persistentvolumeclaim.yaml
│ ├── db-deployment.yaml
│ ├── db-service.yaml
│ └── kustomization.yaml
├── ingress
│ ├── ingress.yaml
│ ├── kustomization.yaml
│ ├── nginx.conf
│ ├── nginx-deployment.yaml
│ └── nginx-service.yaml
├── kustomization.yaml
├── redis
│ ├── kustomization.yaml
│ ├── redis-claim0-persistentvolumeclaim.yaml
│ ├── redis.conf
│ ├── redis-deployment.yaml
│ └── redis-service.yaml
├── sidekiq
│ ├── kustomization.yaml
│ ├── sidekiq-claim0-persistentvolumeclaim.yaml
│ ├── sidekiq-deployment.yaml
│ ├── sidekiq-env-development-configmap.yaml
│ └── sidekiq-service.yaml
├── streaming
│ ├── kustomization.yaml
│ ├── streaming-deployment.yaml
│ ├── streaming-env-development-configmap.yaml
│ └── streaming-service.yaml
└── web
├── kustomization.yaml
├── web-claim0-persistentvolumeclaim.yaml
├── web-deployment.yaml
├── web-env-development-configmap.yaml
└── web-service.yaml
ディレクトリを分けたのは pod を個別にデプロイ削除したかったため. それぞれのディレクトリについて, どのファイルをデプロイに使用するのか kustomization に書いておく.
ほとんどの pod で次の修正が必要となった.
- kompose はサービス間通信を実現するための service を作成してくれないので自力で追加
- deployment に書きだされた command の値が怪しいので修正
- persistentvolumeclaim (pvc) の情報に不足があったので追記
db (postgresql) の manifest をデバッグ(大体 2.5h 程度)
Postgresql 公式 image がそのままだと k8s 上で動きそうになかったので次の3通りを試す羽目になった.
- kompose の生成した manifest yaml を採用
- GKE が配布している helm manifest を採用
- helm が配布している manifest を採用
結論を書くと helm v2 で helm が配布している stable/postgresql をインストールした. helm v3 は過渡期でちゃんと調べてから使わないとハマりそうだったため断念.
helm が配布している manifest を採用
helm ではそれぞれの application (例えば postgres や mysql, traefik など)を charts という単位で管理しており, k8s 上にデプロイされている charts のバージョンなどを一括して管理する仕組みがある. helm v2 では tiller という ServiceAccount を k8s 上に作って charts を管理しているのだが, tiller は先に準備しておく必要がある.
tiller の設定はこちらを参考にした. postgresql インストールには次のコマンドを使用した.
helm install --name db stable/postgresql --tiller-namespace tiller-world --namespace tiller-
world
postgresql のパラメータはデフォルトのものを使用している. namespace が tiller-world となっているのは上の記事のサンプルにあった, tiller がアクセス可能な namespace を変更するのが面倒だったため. 本当は他の pod と揃えるべきだった.
インストールすると postgres へアクセスするためのパスワード情報が自動生成されるため次のコマンドで取得しておく.
これは後で web pod (mastodon) の configmap へ設定することになる.
export POSTGRES_PASSWORD=$(kubectl get secret --namespace tiller-world db-postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode)
echo ${POSTGRES_PASSWORD}
動作確認
- まず
kubectl get svc -n tiller-world
で postgres に対応するサービス名を取得する - psql を使える pod を別途立てて次を実行
-
kubectl exec -it ${psql を使える pod} -- psql -U postgres -h ${サービス名}.tiller-world.svc.cluster.local
- パスワードを聞かれるので入力しログインできることを確認する
Uninstall
helm delete --purge db --tiller-namespace tiller-world
pvc も消えているかどうか kubectl get pvc --all-namespaces
で確認しておく.
pvc が消えていないと以前のデータや postgres のパスワードがうまくリセットされずに残ってしまう.
何故 helm の manifest 採用に至ったかの経緯(読み飛ばし推奨)
kompose の生成した yaml を採用?
まず kubectl describe pods
で何が起きたか確認したところ pvc に問題があるように見えた.
下記を参考にしつつ storageClassName を指定するよう db-claim0-persistentvolumeclaim を修正. ついでに他の pod についても pvc に関する manifest を修正した.
db-claim0-persistentvolumeclaim.yaml 修正前:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
labels:
io.kompose.service: db-claim0
name: db-claim0
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 100Mi
db-claim0-persistentvolumeclaim.yaml 修正後:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
labels:
io.kompose.service: db-claim0
name: db-claim0
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 10Gi
上記のエラーは出なくなったが, 何のログも吐かずに pod が落ちるようになってしまった. kubectl describe pods
で確認するとプロセスが落ちたことしか記録されていない. kubectl logs
には特にエラーログもなく突然処理が途切れている.
initdb: directory "/var/lib/postgresql/data" exists but is not empty
It contains a lost+found directory, perhaps due to it being a mount point.
Using a mount point directly as the data directory is not recommended.
docker-compose で動かしたときのコンテナ内ログと比較すると permission 周りの処理を実施しようとして落ちているように見える. 次の issue が関係しそう?
権限周りだろうか, よくわからん.
試しに initContainers フィールドに chmod 処理を追加してみたが変わらず.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
io.kompose.service: db
name: db
spec:
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
io.kompose.service: db
spec:
initContainers:
- name: init-db
image: debian
imagePullPolicy: "Always"
resources:
requests:
cpu: 250m
memory: 256Mi
command:
- /bin/sh
- -c
- |
mkdir -p /var/lib/postgresql/data
chmod 700 /var/lib/postgresql/data
find /var/lib/postgresql -mindepth 0 -maxdepth 1 -not -name ".snapshot" -not -name "lost+found" | \
xargs chown -R 1001:1001
chmod -R 777 /dev/shm
securityContext:
runAsUser: 0
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: db-claim0
containers: 以下略
GKE や helm の配布している manifest が参照している image を見ると, postgres 公式の image をそのまま k8s に持ってきているわけではなさそう. おそらくそのままだと動かないのだろう..
GKE が配布している helm manifest を採用?
パラメータがやたらと多かったので(時間の都合上)断念.
README には postgresql.image
を指定しろと書いてあるが, helm template ファイルを見たところ実際には postgresql.image.repo
と postgresql.image.tag
を指定する必要があるっぽい.
redis の manifest をデバッグ(大体 1h 程度)
なんか知らんが redis だけは特に何もせずとも動いているように見える.
が, この状態だと web pod から見えない.以下の作業を行う.
- service を追加する(当初これを忘れて文字通り小一時間悩んでしまった)
- これをやらず web pod 上で
rails db:migrate
すると name or service unknown と言われてしまう
- これをやらず web pod 上で
- redis.conf に bind の設定を追加する
- これをやらず web pod 上で
rails db:migrate
すると connection refused と言われてしまう
- これをやらず web pod 上で
解決するために web と redis を行ったり来たりして, この2つの修正に結局2時間程度かかってしまった.
service を追加する
redis-deployment.yaml:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: redis
io.kompose.service: redis
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
strategy:
type: Recreate
template:
metadata:
labels:
app: redis
io.kompose.service: redis
spec:
containers: 以下略
redis-service.yaml: selector の部分に注意. これを追加することで, k8s cluster 内のサービスから redis:6379
もしくは redis.default.svc.cluster.local:6379
へのアクセスがあると, label が app: redis
に一致する pod を適当に選んで 6379 port へ通信を橋渡ししてくれるようになる. 負荷分散させたい場合は追加の設定が必要になるが今回は割愛.
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
app: redis
spec:
type: ClusterIP
ports:
- port: 6379
selector:
app: redis
redis.conf の追加
以下を参考にしつつ redis.conf を configmap 経由で pod 上に mount する.
- https://kubernetes.io/ja/docs/tutorials/configuration/configure-redis-using-configmap/
- https://hub.docker.com/_/redis
redis.conf:
bind 0.0.0.0
kustomization.yaml: redis.conf を redis-config という名前の configmap へファイルとして格納する.
resources:
- redis-claim0-persistentvolumeclaim.yaml
- redis-deployment.yaml
- redis-service.yaml
configMapGenerator:
- name: redis-config
files:
- redis.conf
redis-deployment.yaml: redis-config を /etc/redis/
へ volume mount すると, redis.conf がその直下(/etc/redis/redis.conf
)へ展開される.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: redis
io.kompose.service: redis
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
strategy:
type: Recreate
template:
metadata:
labels:
app: redis
io.kompose.service: redis
spec:
containers:
- args:
- redis-server
- /etc/redis/redis.conf
image: redis:5.0-alpine
livenessProbe:
exec:
command:
- redis-cli
- ping
name: redis
resources: {}
volumeMounts:
- mountPath: /data
name: redis-claim0
volumeMounts:
- mountPath: /etc/redis/
name: config
restartPolicy: Always
volumes:
- name: redis-claim0
persistentVolumeClaim:
claimName: redis-claim0
- name: config
configMap:
name: redis-config
items:
- key: redis.conf
path: redis.conf
動作確認
- redis pod の replica 数を一時的に2つにして相互に疎通確認
- 先に redis pod の名前を
kubectl get pods
で確認し kubectl exec -it ${redis pod の名前} -- redis-cli -h redis.default.svc.cluster.local
- 先に redis pod の名前を
- 後述する web pod から
rails db:migrate
を実行して connection refused とならないことを確認- 先に web pod の名前を
kubectl get pods
で確認し kubectl exec -it ${web pod の名前} -- rails db:migrate
- 先に web pod の名前を
Uninstall
kustomize build ./redis | kubectl delete -f -
で redis のみを削除できる.
Uninstall 時に中途半端に消えたものが残ってしまったのか, 途中ゾンビ状態の pods や replicasets, また pvc が乱立した状態になることがあった. それぞれ以下のコマンドで確認可能.
kubectl get pods --all-namespaces
kubectl get replicasets --all-namespaces
kubectl get pvc --all-namespaces
web の manifest を debug(大体 1h 程度)
- configmap を設定する
- Mastodon の必要とする SECRET_KEY_BASE と OTP_SECRET の2つは先に手元で作成しておく.
- db pod 作成時に取得したパスワード情報を DB_PASS へ指定
- あとは変動しない情報なので docker-compose 時と同じ
- deployment manifest へ initContainers を追加
- 移行元の docker-compose 内に書かれていない手順として, コンテナ内で
rails db:migrate
というコマンドなどを叩く必要があったのだが, これを initContainers フィールドで実行する. initContainers には読んで字のごとく container 起動前の DB 初期化処理などを記述できる - 作業中に initContainers 処理の成否がわからんという問題が発生した. initContainers 内で処理が失敗したら exit 1 とか返すようにしておく必要があるが, やっつけで書いたので成否の判定ができていなかった
- initContainers 内の処理に関しては interactive に作業ができないので debug がつらい..
- 移行元の docker-compose 内に書かれていない手順として, コンテナ内で
- デバッグ中は livenessProbe をコメントアウトしていた
- livenessProbe の処理がこけると pod が落ちてしまい,
kubectl exec
などで container に入って interactive に debug することができないため
- livenessProbe の処理がこけると pod が落ちてしまい,
web-depoyment.yaml: initContainers の command に rails db:migrate
などの処理を記述している.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
io.kompose.service: web
app: web
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web
strategy:
type: Recreate
template:
metadata:
labels:
io.kompose.service: web
app: web
spec:
initContainers:
- name: init-db
image: novsyama/mastodon-http-local
imagePullPolicy: "Always"
resources:
requests:
cpu: 250m
memory: 256Mi
command:
- /bin/sh
- -c
- |
rails db:migrate
rails assets:precompile
chown -hR mastodon public
securityContext:
runAsUser: 0
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
key: DB_HOST
name: web-env-development
- name: DB_NAME
valueFrom:
configMapKeyRef:
key: DB_NAME
name: web-env-development
- name: DB_PASS
valueFrom:
configMapKeyRef:
key: DB_PASS
name: web-env-development
- name: DB_PORT
valueFrom:
configMapKeyRef:
key: DB_PORT
name: web-env-development
- name: DB_USER
valueFrom:
configMapKeyRef:
key: DB_USER
name: web-env-development
- name: LOCAL_HTTPS
valueFrom:
configMapKeyRef:
key: LOCAL_HTTPS
name: web-env-development
略
web-env-development-configmap.yaml: data に定義した値は web-deployment.yaml の configMapKeyRef で参照されている. configmap の情報を書き換えても pod へ直ちに反映されないため, 書き換えの際には一旦 pod を削除する必要がある.
apiVersion: v1
data:
DB_HOST: db-postgresql.tiller-world.svc.cluster.local
DB_NAME: postgres
DB_PASS: hogehogepi
DB_PORT: "5432"
DB_USER: postgres
LOCAL_HTTPS: "false"
中略
kind: ConfigMap
metadata:
creationTimestamp: null
labels:
io.kompose.service: web-env-development
name: web-env-development
動作確認
- web pod から
rails db:migrate
を実行して task が成功することを確認- 先に web pod の名前を
kubectl get pods
で確認し kubectl exec -it ${web pod の名前} -- rails db:migrate
- 先に web pod の名前を
- wget が通ることを確認
kubectl exec -it ${web pod の名前} -- wget -v localhost:3000/api/v1/instance
- コメントアウトした livenessProbe を元に戻して web pod を再起動し pod が落ちないことを確認
Uninstall
kustomize build ./web | kubectl delete -f -
で web pod のみを削除できる.
pvc も消えているかどうか kubectl get pvc --all-namespaces
で確認しておく.
streaming の manifest を debug(大体 30min 程度)
web とほぼ同じでつまるところはなかった.
動作確認
先に streaming pod の名前を kubectl get pods
で確認し, kubectl logs ${streaming pod の名前}
でエラーが出ていないか確認.
Uninstall
kustomize build ./streaming | kubectl delete -f -
で streaming pod のみを削除できる.
pvc も消えているかどうか kubectl get pvc --all-namespaces
で確認しておく.
sidekiq の manifest を debug(大体 30 min 程度)
ローカルマシンに docker-compose で mastodon 構築した際には, web container と sidekiq container が host filesystem 上の /public 配下を共有していた. しかし pod 間で filesystem 共有を実現するのには難があり, どうするか少し悩んだ.
- 結論としては, mastodon が filesystem の代わりに s3 (または google cloud storage) bucket を利用したファイル共有をサポートしているので, それを使うことにした
- kompose が生成した sidekiq-deployment-...yaml の volume mount 定義を眺めると別々の pvc を作成しており, そもそも docker-compose で構築した際に実現していた container 間のデータの共有ができていない.
- k8s で ReadWriteMany での PVC 共有(同時に複数の pod から同じストレージ領域に書き込む)は悪手に見え, s3 などのストレージサービスを頼ったほうが良さそう
- もし sidekiq が /public 配下への書き込みをしないのであれば, readonly での pvc 共有が使えそう
s3 設定
- バケットを作成して
- アクセストークンを発行する
- web, streaming, sidekiq の configmap および deployment manifest に s3 設定を追記して再起動
- db 消したつもりで pvc 残っているのに気づかず
- 1h 程度これで費やした. config を変えても直ちに反映されないので pod は毎回 delete していた
動作確認
先に sidekiq pod の名前を kubectl get pods
で確認し, kubectl logs ${sidekiq pod の名前}
でエラーが出ていないか確認.
Uninstall
kustomize build ./sidekiq | kubectl delete -f -
で sidekiq pod のみを削除できる.
pvc も消えているかどうか kubectl get pvc --all-namespaces
で確認しておく.
ウェブブラウザから Mastodon を開ける状態にする
ここまでの作業で pod をすべて runnig な状態へもっていくことができた.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5499564888-vnhtf 1/1 Running 0 28h
redis-6486c997cb-kxrtl 1/1 Running 0 29h
sidekiq-7857f54cd6-cjj7l 1/1 Running 0 28h
streaming-6578b4bc4b-gssb9 1/1 Running 0 28h
web-5d9ddc8ccf-q5qpj 1/1 Running 0 28h
$ kubectl get pods -n tiller-world
NAME READY STATUS RESTARTS AGE
db-postgresql-0 1/1 Running 0 28h
tiller-deploy-7fb7c8f8bf-495g7 1/1 Running 0 39h
いよいよウェブブラウザから mastodon を開ける状態へもっていく.
Public network から k8s 内に立てた mastodon を参照する手段を調査(大体 1h 程度).
以下のいずれかが使えそうなことを確認. 今回は前者2つについて検討.
- Ingress
-
type LoadBalancer
で nginx などのリバースプロキシを立てる - ingress controller
Ingress の挙動を確認(大体 1.5h 程度)
- https://cloud.google.com/kubernetes-engine/docs/how-to/load-balance-ingress?hl=ja
- https://kubernetes.io/docs/concepts/services-networking/ingress/#single-service-ingress
上記を読みつつ挙動を確認したところ, 結論としては今話題の nginx か traefik を type LoadBalancer
で立てたほうが良さそうに見えた.
- GKE の Ingress は frontend (Public network から見える IPv4 address) と backend service (k8s cluster 内の service へのルーティング情報) と呼ばれるものをセットにしたものらしい
- 今回の用途だとリバースプロキシとして使うには役不足に見える
- Mastodon では web と streaming pod の WebAPI を, ひとまとめのホスト配下の API として参照する必要がある
- backend service ごとに IPv4 が振られてしまうので, ひとつの backend service から web, streaming pod 両方へのルーティングを実現する必要がある
- backend service は host 名での url-mapping に対応しているのだが, その host 名には IPv4 が指定できないという制約がある. 冒頭に書いた, ドメインを取得せずに IPv4 だけで mastodon 同士の識別を可能にするような用途では使えない
nginx を type LoadBalancer
で立てた時の挙動を確認(大体1時間程度)
nginx の manifest を作成
kustomization.yaml:リバースプロキシの設定を行うため, nginx.conf を configmap で経由で pod 上に mount する.
resources:
- nginx-deployment.yaml
- nginx-service.yaml
configMapGenerator:
- name: nginx-config
files:
- nginx.conf
nginx-deployment.yaml: redis と違って直接 config file path を指定して server 起動することができないため, 起動前に /tmp
に mount した conf file を /etc/nginx/conf.d
上へコピーするというちょっと回りくどいことをしている.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy: {}
template:
metadata:
labels:
app: nginx
spec:
containers:
- args:
- bash
- -c
- cp /tmp/nginx/nginx.conf /etc/nginx/conf.d/mastodon.nginx.conf; nginx -g 'daemon off;'
image: nginx
name: nginx
resources: {}
volumeMounts:
- mountPath: /tmp/nginx/
name: config
restartPolicy: Always
volumes:
- name: config
configMap:
name: nginx-config
items:
- key: nginx.conf
path: nginx.conf
nginx-service.yaml: redis-service.yaml で type: ClusterIP
となっていた部分を LoadBalancer
に書き換えている.
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
type: LoadBalancer
ports:
- port: 80
selector:
app: nginx
動作確認
取得した IPv4 アドレスをブラウザで開いてみる.
ローカルマシンがなくても mastodon を使ったテストができる環境を構築できた.
ついでに notification メールも飛んでくるのを確認した.
s3 や streaming API へのアクセスも通っているっぽい.
nginx.conf のルーティング設定にミスがあった場合の挙動
kubectl get pods
で nginx の pod 名を確認後, kubectl logs ${nginx の pod 名}
すると次のようなログが出ていた.
kubectl logs nginx-5687bf498f-n2p25
2019/12/16 17:21:27 [error] 8#8: *3 "/home/mastodon/live/public/index.html" is not found (2: No such file or directory), clien
t: 10.128.0.3, server: 34.66.70.59, request: "GET / HTTP/1.1", host: "34.66.70.59:80"
10.128.0.3 - - [16/Dec/2019:17:21:27 +0000] "GET / HTTP/1.1" 404 555 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537
.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" "-"
2019/12/16 17:22:21 [error] 8#8: *4 "/home/mastodon/live/public/index.html" is not found (2: No such file or directory), clien
t: 10.128.0.3, server: 34.66.70.59, request: "GET / HTTP/1.1", host: "34.66.70.59"
10.128.0.3 - - [16/Dec/2019:17:22:21 +0000] "GET / HTTP/1.1" 404 124 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gec
ko/20100101 Firefox/70.0" "-"
2019/12/16 17:22:35 [error] 8#8: *4 "/home/mastodon/live/public/index.html" is not found (2: No such file or directory), clien
t: 10.128.0.3, server: 34.66.70.59, request: "GET / HTTP/1.1", host: "34.66.70.59"
10.128.0.3 - - [16/Dec/2019:17:22:35 +0000] "GET / HTTP/1.1" 404 124 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gec
ko/20100101 Firefox/70.0" "-"
2019/12/16 17:24:09 [error] 8#8: *5 "/home/mastodon/live/public/index.html" is not found (2: No such file or directory), clien
t: 10.40.0.1, server: 34.66.70.59, request: "GET / HTTP/1.1", host: "34.66.70.59"
10.40.0.1 - - [16/Dec/2019:17:24:09 +0000] "GET / HTTP/1.1" 404 124 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Geck
o/20100101 Firefox/70.0" "-"
IP アドレスで Mastodon 同士が認識できるように IP アドレスを web pod の設定に追記(大体 2h 程度)
Mastodon インスタンス同士が連合を組むためには, Mastodon それぞれに, 自分がどういうホスト名で参照されるのかを設定しておく必要がある. 今回はドメインを発行せずに IPv4 を Mastodon 名とするので, 先に IPv4 アドレスを確保して web pod の設定にその値を書き, 再起動して web pod へ反映する必要があった.
確保した IP アドレスを確認して web pod の configmap へ追記
kubectl get svc
で nginx service が確保した external IP を確認する. 下記で xx.xx.xx.xx
となっている部分.
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 2d11h
nginx LoadBalancer 10.0.7.76 xx.xx.xx.xx 80:32760/TCP 43h
もしくは LoadBalancer のページで IPv4 を確認する.
確認した値を web-env-development-configmap.yaml へ記述する.
web pod を再起動して設定を反映
するとリソースが枯渇したのか kubectl get pods
の結果が pending のまま進まない. kubectl describe pods
すると次のような記録が残っており, cpu が足りないと言われている.
0/3 nodes are available: 3 Insufficient cpu.
Node(VM) への pod 配置バランスが偏っているのが原因かと思い, すべての pod を uninstall 後, 再配置したら running 状態に戻った.
おまけ : LoadBalancer へのアクセス元を制限する(大体 1h 程度)
GKE で service を type LoadBalancer
で立ち上げると,その瞬間から全世界に向け, 立ち上げた Mastodon インスタンスが公開されてしまう. GCP の場合は nginx service の type LoadBalancer
を ClusterIP
等に変更し, LoadBalancer の間に Ingress を挟むことで Cloud Armor を用いた firewall rule が書けるようになるらしい.
アクセス制限をかけると自宅からのアクセスしか受け付けないようにはなるが,TLS 無効にしてあるので通信経路(public インターネッツ)上に流れるデータは覗き放題である(今回は主にメールアドレスとランダム生成されたパスワードくらいなので無視). 例えば VPC 内に踏み台 VM を建てて ssh tunneling するとか VPC 内に GUI とブラウザが使える踏み台 VM を立てるとかで, この辺の問題は回避できる.
まとめ
この記事では「素の k8s 環境にアプリ構築するのめっちゃ大変ですね」という話を書いた. docker-compose で構築していた mastodon を GKE で構築しなおすのにざっくり12時間ほどの時間を要した. 「ベースラインに立てたか立ててないか」な状態のため, ここから k8s の恩恵を受けられるような形にちょっとずつ変えていく必要がありそう.
積み残し
今回は時間切れになったためできなかったが, 本当は手を付けるべきだった諸々.
- コードの公開
- 手順が自動化されていない
- 本当は
kusutomize build . | kubectl apply -f -
で全部立ち上がるようにすべきなのだが, 先に env manifest に指定すべき情報(postgres 起動したり IPv4 予約したり)を手動で確保する流れになっている
- 本当は
- secrets configmap への移行
- 全部 configmap で書いてしまっているのでパスワードなどの sensitive な情報は secrets へ移したい
- replica 数1で動かしている. replica 数増やしたい
- たぶん pvc 共有の問題が発生するのでそれも対処. postgres と redis 以外は pvc 要らない気もする
- kompose が吐いた deployment manifests をそのまま使っている. Statefulset 使うべきだったのでは
- k8s 1.16 での deprecated API への対処
- データストアの管理が雑(耐障害性とパフォーマンスの追求)
- Redis operator, postgres operator の導入
- あるいは Cloud SQL for PostgreSQL の利用
Mastodon 固有の問題
- Update, data backup. 次の記事が参考になりそう
- Local filesystem から s3 へのデータ移行はどうすればよいのか, まだちゃんと調べていない.
参考リンク
上記の作業を実施するときにはほとんど参照していなかった. 参考情報があればもう少し早く終わっていたのかも.
Manifests for mastodon
-
https://github.com/ericflo/kube-mastodon
- ドメイン保持している場合は external-dns を使うといろいろ楽できるらしい
- https://github.com/jviide/kubedon
とても参考になる先達
- 宅内 k8s での構築 : KubernetesインフラにMastodonエンジンを突っ込む
- k3s 上での構築 : Kubernetesで隔離Mastodonネットワークを作った
- GKE 上で構築 : Kubernetesの学習のためにMastodonを構築したら勉強になった