実際のプロジェクトでDocker Composeを初めて使ったとき、今思えば笑えるような基本的なミスを多々犯した——コンテナを再起動する前にトラフィックをdrainするのを忘れて、deployが終わったらサイトが1時間もダウンしてしまった。チームに「なんでダウンしてるの?」と聞かれても説明できなかった。それがきっかけで、Docker Swarm上級編を深く掘り下げるようになった:制御されたRolling Update、Placement Constraints、そしてDocker Configだ。
この記事は、Swarmの基礎をすでに知っていて、本当の本番環境設定をしたい人向けだ——デモのラボではなく、実際のユーザーが使う24時間365日稼働するシステムのために。
背景:なぜこの設定レイヤーが必要なのか?
Swarmのデフォルト設定はそこそこ問題ないが、本番環境に移行すると、よく遭遇する問題にすぐぶつかる:
- デフォルトのRolling updateがダウンタイムを引き起こす:Swarmは新しいreplicaを起動する前に古いreplicaを停止するため、サービスするインスタンスが存在しない時間的な空白が生まれ——ユーザーには502が表示される。
- Containerの配置が制御できない:重いデータベースがRAMの少ないノードにスケジュールされたり、APIのreplicaがすべて1つのノードに集中してそのノードがダウンしたら終わり、ということになりかねない。
- ConfigとSecretが環境変数に入っている:
docker inspect、ログの集約、またはホスト上のps auxを通じて漏洩しやすい。 - 自動ロールバックがない:deployが終わってからエラーに気づき、ユーザーが実際のエラーを見ている間に手動で対応しなければならない。
3つの機能——Placement Constraints、Docker Config/Secret、そしてorder: start-firstを使ったRolling Update——が、まさにこの3つの問題を解決する。
準備:ノードへのラベル付け
ラベルはPlacement Constraintsの基盤だ。stackファイルを書く前に、役割とハードウェアの特性に応じて各ノードにラベルを付ける必要がある。多くのチュートリアルがこのステップを省略しているため、読者はconstraintがなぜ機能しないのか分からず途方に暮れることになる:
# workerノードにroleラベルを付与する
docker node update --label-add role=worker node-1
docker node update --label-add role=worker node-2
# ストレージタイプを付与——データベースにとって重要
docker node update --label-add storage=ssd node-1
docker node update --label-add storage=hdd node-2
# マルチリージョンクラスターの場合はアベイラビリティゾーンを付与
docker node update --label-add zone=az-1 node-1
docker node update --label-add zone=az-2 node-2
# ラベルが正しく付与されたか確認する
docker node inspect node-1 --format '{{json .Spec.Labels}}'
docker node ls --format 'table {{.Hostname}}\t{{.Status}}\t{{.ManagerStatus}}'
ラベルを付与したら、docker node ls -q | xargs docker node inspect --format '{{.Description.Hostname}}: {{.Spec.Labels}}'で確認し、stackをdeployする前にすべてのノードにラベルがあることを確かめよう。
詳細設定
Docker ConfigとSecret——正しい設定管理
環境変数でconfigを渡す代わりに、Docker Configは静的な設定ファイル(nginx.conf、app.yamlなど)を保存し、Docker Secretは機密情報(パスワード、APIキー)を保存する。どちらも保存時に暗号化され、実行中のコンテナのRAM内でのみ復号される:
# ファイルからconfigを作成する
docker config create nginx_conf ./nginx.conf
docker config create app_settings ./app.yaml
# ファイルからsecretを作成する(shellのhistoryへの保存を避けるためstdinより推奨)
docker secret create db_password ./db_password.txt
docker secret create jwt_secret ./jwt_secret.txt
# 確認する
docker config ls
docker secret ls
stackファイルで宣言してコンテナにマウントする:
configs:
nginx_conf:
external: true
app_settings:
external: true
secrets:
db_password:
external: true
services:
nginx:
image: nginx:1.25-alpine
configs:
- source: nginx_conf
target: /etc/nginx/nginx.conf
mode: 0440 # オーナーとグループに読み取り専用、othersはアクセス不可
api:
image: myapp/api:latest
configs:
- source: app_settings
target: /app/config/settings.yaml
secrets:
- source: db_password
target: db_password
mode: 0400 # オーナーのみ読み取り可——secretはconfigより厳格にすべき
Secretはコンテナ内の/run/secrets/<secret_name>にマウントされる。アプリは環境変数の代わりにこのファイルを読み取る——この方法はsecretがプロセスのenvironmentに現れず、docker inspectで漏洩しないため、はるかに安全だ。
Placement Constraints——ワークロードを適切なノードへ
上記で付与したラベルを使えば、どのコンテナがどのノードで動くかを正確に制御できる。constraintsはハードルール(必ず満たすべき)で、preferencesはソフトルール(可能な限り満たすよう努力)だ:
services:
api:
deploy:
replicas: 4
placement:
constraints:
- node.role == worker # APIはmanagerノードで動かさない
- node.labels.role == worker # カスタムラベルでダブルチェック
preferences:
- spread: node.labels.zone # AZ全体に均等に分散させ、1箇所に集中させない
database:
deploy:
replicas: 1
placement:
constraints:
- node.labels.storage == ssd # DBはSSDを持つノードでのみ動かす
- node.role == worker # managerでは動かさない
redis:
deploy:
replicas: 1
placement:
constraints:
- node.labels.zone == az-1 # 必要に応じてRedisを特定のzoneに固定する
ダウンタイムなしのRolling Update——詳細設定
ここが最も設定ミスが多い部分だ。重要なパラメータはorder: start-firstで——Swarmは新しいreplicaを起動し、healthcheckが通過するのを待ってから、古いreplicaを停止する。ダウンタイムを引き起こすデフォルトのstop-firstとは逆だ。
しかし、order: start-firstは適切に設定されたhealthcheckと組み合わせた場合にのみ正しく機能する:
services:
api:
image: myapp/api:${VERSION:-latest}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s # アプリにhealth評価を開始する前に30秒の起動時間を与える
deploy:
replicas: 4
update_config:
parallelism: 1 # 一度に1つのreplicaを更新——保守的だが安全
delay: 15s # 各バッチ更新の間に15秒待つ
order: start-first # 新しいreplicaを先にSTART、古いreplicaはその後にSTOP
failure_action: rollback # update失敗時は全体を自動的にロールバックする
monitor: 30s # 各更新後30秒間監視して遅延エラーを検出する
max_failure_ratio: 0.3 # ロールバックをトリガーする前に最大30%のreplica失敗を許容する
rollback_config:
parallelism: 0 # 0 = すべてのreplicaを同時にロールバック
delay: 0s # ロールバック時は遅延なし——迅速さが必要
failure_action: continue # エラーが発生してもロールバックを続ける
order: stop-first # ロールバック時:まず新バージョンを停止し、次に旧バージョンを復元
healthcheckのstart_periodは、正しくチューニングするのに最も時間がかかった部分だ。アプリがデータベースへの接続、configのロード、キャッシュのウォームアップに20秒かかる場合は——バッファのためにstart_period: 25sに設定しよう。このパラメータがないと、Swarmは起動直後にコンテナが失敗したと判断する——コンテナは再起動を繰り返し、rolling updateが完了することはない。
検証とモニタリング
stackのdeployとrolling updateの監視
# stackを初めてdeployする
docker stack deploy -c docker-stack.yml myapp
# stack内のすべてのserviceを確認する
docker stack services myapp
# APIを新バージョンに更新——rolling updateが自動的に実行される
VERSION=v2.1.0 docker stack deploy -c docker-stack.yml myapp
# rolling updateの進行をリアルタイムで監視する
# 観察点:古いreplicaがShutdownになる前に新しいreplicaがRunningになっていること
watch -n2 'docker service ps myapp_api --format "table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.DesiredState}}"'
更新が正しく進んでいる場合、replicas+1個のタスクが同時に存在する瞬間が見られる:新しいreplicaがRunning状態にある一方で、古いreplicaはまだShutdownに移行していない。これがゼロダウンタイムが機能している証拠だ。
ロールバックと配置の確認
# serviceを前バージョンにロールバックする(手動介入が必要な場合)
docker service rollback myapp_api
# データベースがSSDノード上で正しく動いているか確認する
docker service ps myapp_database --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}'
# ノード全体のAPIreplicaの分布を確認する
docker service ps myapp_api --filter 'desired-state=running'
# serviceのすべてのreplicaからログを集約する
docker service logs -f --tail 100 myapp_api
リソース使用量の監視
# service内のすべてのコンテナのリソース使用量を確認する
docker stats $(docker ps --filter 'name=myapp_api' -q)
# 設定済みのリソース制限を確認する
docker service inspect myapp_api --pretty | grep -A 8 Resources
# すべてのタスクのhealth状態を確認する
docker service ps myapp_api --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.Error}}'
設定が完了したら、よくやるのが「ドライ」ローリングアップデートの実行だ:同じコードを新しいバージョン番号でイメージに再タグ付けし、更新をトリガーして、watch docker service psを眺める。古いreplicaがShutdownに移行する前に新しいreplicaがRunningになれば——ゼロダウンタイムdeploymentが正しく動いている。
この3つ——ワークロードを適切な場所に配置するPlacement Constraints、設定を保護するDocker Config/Secret、中断なしでdeployするためのorder: start-firstを使ったRolling Update——は、SwarmをProductionに持ち込む初日から必要なものだ。複雑ではないが、3つのうち1つでも欠けると確実にインシデントが発生する。筆者はこれを難しい方法で学んだ。
