Docker+Caddyでblue/greenデプロイメント
はじめに
ダウンタイムのないデプロイがしたくて、blue/greeデプロイメント[1]を知り、さっそくDocker+Caddy[2]で構築した。
やりたいこと
caddyコンテナを前段に立て、コンテナ化したアプリケーション2つ(blue/green)を後ろで走らせておき、デプロイのときにそれぞれをdown/upさせながらデプロイさせる。caddyはupしているどちらか一方のコンテナに自動で接続させる。
検証環境
- ロードバランサ: Caddy
- アプリケーション(blue/green): frankenphp
frankenphpはCaddy+phpで動いているので、ロードバランサのcaddyサーバーからアプリケーションのcaddyサーバーに飛ばしている。
ロードバランサコンテナを作る
compose.yaml
services:
load-balancer:
image: caddy:2
ports:
- "80:80"
- "443:443"
environment:
SERVER_NAME: ${SERVER_NAME:-localhost}
depends_on:
- app-blue
- app-green
volumes:
- $PWD/load-balancer/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
SERVER_NAME
はCaddy固有の環境変数で、ここにexample.com
などを設定しておけば、コンテナ初回起動時に自動で証明書取得し設定してくれるようになります。
なお、https接続が不要の場合は:80
を設定すればよいです。
Caddyfile
{
admin localhost:2019
}
{$SERVER_NAME:localhost} {
push
encode zstd gzip
reverse_proxy * {
to app-blue:80 app-green:80
fail_duration 5s
lb_policy first
lb_try_duration 8s
health_uri /
}
}
大事なのは以下の部分
reverse_proxy * {
# blue/greenコンテナを指定
to app-blue:80 app-green:80
# blue/greenコンテナがダウンしていると判断する秒数
fail_duration 5s
# health_checkが有効な一方に接続する。優先順位はtoの昇順(blueコンテナ)
lb_policy first
# 失敗した場合8秒おきに再試行
lb_try_duration 8s
# status 200を返すエンドポイントを指定
health_uri /
}
blue/greenコンテナをつくる
compose.yaml
services:
load-balancer:
...
app-blue:
image: dunglas/frankenphp
environment:
SERVER_NAME: :80
volumes:
- $PWD/src:/app/public
- $PWD/app/Caddyfile:/etc/caddy/Caddyfile
app-green:
image: dunglas/frankenphp
environment:
SERVER_NAME: :80
volumes:
- $PWD/src:/app/public
- $PWD/app/Caddyfile:/etc/caddy/Caddyfile
volumes:
...
SERVER_NAME
に:80
を指定しておくことで、ロードバランサを待ち受けてます。
なお、ブラウザで表示する画面はblue/greenが切り替わっていることを確認したいので、envを表示しておくようにします。
<?php
print_r($_ENV);
Caddyfile
{
servers {
trusted_proxies static private_ranges
}
admin localhost:2019
frankenphp
}
{$SERVER_NAME:localhost} {
route {
root /app/public
encode zstd br gzip
php_server {
resolve_root_symlink
}
}
}
trusted_proxies static private_ranges
を指定しておくことで、ロードバランサコンテナをプロキシとして信頼するようにします。
実行スクリプトをつくる
#!/bin/bash
export `cat .env | grep ^SERVER_NAME`
LB="load-balancer"
BLUE="app-blue"
GREEN="app-green"
setup () {
echo "実行中のコンテナが見つからないため、セットアップを行います"
echo "${BLUE}コンテナを起動します..."
docker compose up -d ${BLUE}
echo "${LB}コンテナを起動します..."
docker compose up -d ${LB}
echo "${SERVER_NAME}への接続をテストします..."
timeout -sKILL 30 sh -c "until (curl https://${SERVER_NAME:-localhost} -o /dev/null -w '%{http_code}\n' -s | grep 200) do sleep 1; done"
if [ $? != 0 ]; then
echo "${SERVER_NAME}への接続に失敗しました"
echo "${BLUE}コンテナを停止します..."
docker compose rm -fsv "${BLUE}"
else
echo "${SERVER_NAME}への接続が成功しました"
fi
}
new_active () {
ACTIVE=$(docker compose ps --services | grep -v "${LB}" | grep "${BLUE}")
if [ $ACTIVE = $BLUE ]; then
echo "${GREEN}"
else
echo "${BLUE}"
fi
}
deployment () {
NEW_ACTIVE=$1
NEW_STANDBY=$2
echo "${NEW_ACTIVE}コンテナを起動します..."
docker compose pull "${NEW_ACTIVE}"
docker compose up -d "${NEW_ACTIVE}"
echo "${SERVER_NAME}への接続をテストします..."
timeout -sKILL 30 sh -c "until (curl https://${SERVER_NAME:-localhost} -o /dev/null -w '%{http_code}\n' -s | grep 200) do sleep 1; done"
if [ $? != 0 ]; then
echo "${SERVER_NAME}への接続に失敗しました"
echo "${NEW_ACTIVE}コンテナを停止します..."
docker compose rm -fsv "${NEW_ACTIVE}"
exit 0
else
echo "${SERVER_NAME}への接続が成功しました"
echo "${NEW_STANDBY}コンテナを停止します..."
docker compose rm -fsv "${NEW_STANDBY}"
fi
}
if [ "$(docker compose ps --services | wc -l)" -eq 1 ]; then
setup
elif [ $(new_active) = $BLUE ]; then
deployment "${BLUE}" "${GREEN}"
else
deployment "${GREEN}" "${BLUE}"
fi
cat <<EOS
デプロイが完了しました!
===== 現在起動しているコンテナリスト =====
$(docker compose ps)
EOS
blue/greenコンテナを起動する都度、接続確認を行っています。
また、timeout 30
にすることで30秒以内に200が返ってこない場合、コンテナの切り替えを実行しないようにしています。
動作確認
初回実行時(コンテナが何も起動していないとき)
$ sh deploy.sh
実行中のコンテナが見つからないため、セットアップを行います
app-blueコンテナを起動します...
[+] Running 1/1
✔ Network test_default Created 0.1s
✔ Container test-app-blue-1 Started 1.0s
[+] Running 3/3
✔ Container test-app-green-1 Started 0.5s
✔ Container test-app-blue-1 Running 0.0s
✔ Container test-load-balancer-1 Started 0.7s
localhostへの接続をテストします...
200
localhostへの接続が成功しました!
app-greenコンテナを停止します...
[+] Stopping 1/1
✔ Container test-app-green-1 Stopped 10.5s
Going to remove test-app-green-1
[+] Removing 1/0
✔ Container test-app-green-1 Removed 0.0s
デプロイが完了しました!
===== 現在起動しているコンテナリスト =====
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
test-app-blue-1 test-app-blue "docker-php-entrypoi…" app-blue 18 seconds ago Up 17 seconds (health: starting) 80/tcp, 443/tcp, 2019/tcp, 443/udp
test-load-balancer-1 test-load-balancer "caddy run --config …" load-balancer 17 seconds ago Up 16 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 443/udp, 2019/tcp
blueコンテナが起動しました
2回目実行時(blueコンテナが起動しているとき)
app-blueが起動している状態でsh deploy.sh
するとapp-greenに切り替わる。
「app-green起動」→「app-blue停止」という流れでデプロイしている。
逆に、app-greenが起動している状態だとapp-blueに切り替わります。
おわりに
今回はdocker pullするだけのシンプルなコマンドとしましたが、本番環境ではこれをベースにむにゃむにゃと改造して運用したいと思います。
参考
ありがとうござました。
Discussion