💬

Docker+Caddyでblue/greenデプロイメント

2024/08/20に公開

はじめに

ダウンタイムのないデプロイがしたくて、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

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

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を表示しておくようにします。

$PWD/src/index.php
<?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を指定しておくことで、ロードバランサコンテナをプロキシとして信頼するようにします。

実行スクリプトをつくる

deploy.sh
#!/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するだけのシンプルなコマンドとしましたが、本番環境ではこれをベースにむにゃむにゃと改造して運用したいと思います。

参考

ありがとうござました。
https://caddy.community/t/blue-green-deployment-without-downtime/23511
https://hackernoon.com/lang/ja/ゼロ-ダウンタイム-デプロイメント-ブルー-グリーン-テクニックを使用して-Docker-化されたアプリをアップグレードする
https://rsym1290.hatenablog.jp/entry/2022/03/16/222754

脚注
  1. 稼働中のソフトウェア(Blue)と同等のプロダクション構成を持つアップデート版(Green)を準備してテストを行い、アップデート時にDNSルーティング等を用いてBlueからGreenへ全てのリクエストを切り替える手法 - wikipediaより引用 ↩︎

  2. Caddy はオープンソースのHTTP/2、HTTP/3に対応したWebサーバである。Caddy Webサーバ と呼ばれることもある。 CaddyはGo言語で記述されており、HTTP機能にはGo標準ライブラリを使用している。 Caddyの特徴的な機能の1つに、デフォルトでのHTTPSの有効化がある 。 - wikipediaより引用 ↩︎

Discussion