データベースに触れずにイメージを構築する

みなさん、こんにちは。

私はかなり小規模な Discourse インスタンスを長年運用しています(実際、ほとんど問題なく数年間稼働しています):https://discuss.cubeisland.de/。
これまでは、専用 VM(自社のデータセンター内のハードウェア上)で標準的なランチャーベースのデプロイプロセスを使用していました。長年にわたって変更したのは、外部の共有 PostgreSQL データベースへ移行しただけです。

最近、リソースの節約とインフラの一部をより「弾力的」にするため、最終的には Kubernetes クラスターへの移行を見据えて、アプリケーションを専用 VM から Docker Swarm へ移行する作業を開始しました。

今日は、残りの専用アプリケーション VM の一つであるこの小さな Discourse インスタンスに着手しました。「すでに Docker で動いているし、Swarm へのデプロイなんて簡単だろう」と思いました。読んでみたところ、実際そうでした。現在稼働しているインスタンスからイメージを取得し、社内のレジストリにプッシュして Swarm で実行するだけで、すべて問題なく動作するはずです。これは素晴らしいことです。

ランチャーファイル、特にテンプレートとサンプルを見て回ったところ、このようなデプロイでは Redis を分離しておくのが良いアイデアかもしれないと考えました。また、プラグインを追加したり更新したりする際に、新しいイメージをビルドする CI ジョブを設定できるかもしれません。そこで、discourse_docker をローカルにチェックアウトし、既存の app.yml コンテナ定義をクローンにコピーして、./launcher bootstrap app を実行し、すぐにデプロイするのではなく、レジストリにプッシュ可能なイメージをビルドしようと試みました。

驚いたことに、スクリプトは「本番」の PostgreSQL サーバーに接続し、データベースのマイグレーションを試みました(幸い、私のローカルワークステーションからはアクセス権限がなかったため、実行されませんでした)。

ここでいろいろ調べてみたところ、これが本来の動作のようです。これにより、以下の点が疑問に思われます:

  1. データベースがまだ存在しない新しいインスタンス用のコンテナをビルドするにはどうすればよいでしょうか?イメージをビルドする前に、本番データベースをセットアップする必要があるのでしょうか?
  2. db:migrate はこの一度だけ実行されると推測されます。つまり、本番やテストなど複数の類似インスタンスがある場合、新しいイメージをビルドするためにいずれかのインスタンスをアップグレードすると、イメージが同一であっても、そのイメージを 2 番目のインスタンスでは使用できなくなります。
  3. イメージをビルドするシステムからデータベースサーバーにアクセスできないインスタンス用のイメージをビルドするにはどうすればよいでしょうか(これはそれほど珍しくないはずです)。

いくつかの投稿(もちろん これ も含む)を読んだ後、現在のビルドプロセスの理由については十分に理解しています。また、標準的なフルスペックの VM に Discourse をカジュアルにデプロイする 99% の人々にとっての価値も理解しています。「オールインワン」のコンテナモデルには慣れていますし、それに反対するつもりもありません。結局のところ、Docker の最大の価値は、ソフトウェアベンダーが高度に最適化された設定を事前に組み込み、再現可能なランタイム環境にバンドルすることで、運用側で多くのアプリケーション固有の知識を必要としない点にあります。したがって、提供されているツールを完全に活用することに賛成です。なぜソフトウェアベンダー自身が作るよりも良いコンテナを他者に期待する必要があるのでしょうか?Nginx と Ruby アプリケーションを分割することに 0 のメリットがあるのに、デプロイをより「純粋」にする(それが何を意味するにせよ)ために分割したいと思うでしょうか?

しかし、実行されていない段階でランタイム状態を変更するコンテナを見るのは奇妙です。私はすでに多くのアプリケーションをコンテナ化して運用しており、自分自身でもいくつかのコンテナ化を行ってきました。その中には、本来コンテナで動かすことを想定していなかったものもありました。

Discourse と同様の要件や課題を同様に処理しているアプリケーションの代表例として、GitLab が挙げられます。GitLab は現在、完全に分解された「あるべき姿」の Kubernetes デプロイのための洗練された Helm チャートを提供していますが、数値を確認していない推測に過ぎませんが、中小規模のデプロイの同様に 99% は GitLab の Omnibus Docker イメージ(または事実上同じである OS パッケージ)を使用していると思われます。彼らも同様のブートストラッププロセスを持っていますが、これはコンテナ内の Chef ベースで、起動時に毎回実行され、データベースのマイグレーションやアセットのコンパイルなどの通常の処理を行います。

確かに、このため GitLab の起動には数分かかることがありますが、私が目にしたデプロイ(大企業内のものも含む)では、それが問題になったことはありません。特に、古いインスタンスが停止するのは新しいインスタンスが稼働し、正常なヘルスチェックとレディチェックが完了した後だけというローリングアップグレードを実行できる、Docker Swarm や Kubernetes などの現代のオーケストレーションシステムを使えば、長いデプロイプロセスは実際には問題にならないかもしれません。しかし、ローリングアップグレードのような高度な機能を使わなくても(動作するかどうかは別として)、多くの状況ではかなりのダウンタイムを許容することも可能です。

そこで質問です:イメージビルド中にデータベース依存の操作をスキップし、代わりにコンテナ起動時にそれらの操作を行うようにランチャーを設定することは可能でしょうか?

私自身もここに時間を割く用意はありますが、夜の時間は限られているため、任何の指針でも非常に歓迎します。

もしこれが愚かだ、あるいは不可能だとお考えであれば、全く異なるプロセスについてもオープンです。

フィードバックをお待ちしています!

私も同じことを目指していました。私たちは Amazon ECS 上で Discourse を運用しているため、Web イメージだけをビルドしてレジストリにプッシュできるようにする必要がありました。Discourse のビルドプロセスをいじるのは避けたいと考えていたため、サポートされているインストール方法に極力近づけたいと考えていました。

その代わり、通常使用される launcher スクリプトを使ってローカルマシンで 2 つのコンテナ構成 を構築し、データコンテナは無視して Web コンテナだけをレジストリにプッシュしています。実行時には環境変数を通じて Postgres と Redis の接続情報を上書きしています。

新しいイメージのデプロイは以下の 3 ステップで行います:

  1. 安全な事前マイグレーションを実行する。ECS に新しいイメージを使ってこのコマンドを実行させます:

     SKIP_POST_DEPLOYMENT_MIGRATIONS=1 rake db:migrate
    
  2. 新しいイメージをデプロイする。ECS サービスを更新します。

  3. 事後マイグレーションを実行する。ECS にこのコマンドを実行させます:

     SKIP_POST_DEPLOYMENT_MIGRATIONS=0 rake db:migrate
    

イメージをビルドしている間もローカルのデータコンテナを実行させるのはおそらく非効率ですが、そのおかげでデータベースや Redis に接続しようとする部分について気にすることなく、標準的な web.template.yml をそのまま使用できます。

ありがとうございます!イメージビルド中に一時的に Postgres を起動し、実際のビルドが完了したら破棄してもよいと私も考えました。

ついにこれを実装する時間を取ることができました!

ビルド中に Postgres と Redis をサービスとして実行し、ビルド後は破棄する GitLab CI パイプラインを使用して、イメージのビルドを実装しました。

次は、DB マイグレーション付きのデプロイを自動化するだけです。

このものは、2.8リリースにさえ触れることなく、1年以上稼働し続けています。

イメージビルドをgithubに移動しました: GitHub - pschichtel/discourse-docker: A reusable Discourse container built using the launcher tool.

イメージはpschichtel/discourse:stable-web_onlyに公開されています。

ついにこれが壊れたようです。3.0.6から3.1.0にアップグレードした際、DBのマイグレーションは実行されませんでした。実行中のコンテナ内で最後に bundle exec rake db:migrate を実行したところ、別のコンテナ再起動後に動作しました。

新しいイメージがその環境設定なしで開始された場合、再度移行する必要があります。それを実行するrakeタスクがありますが、携帯からは思い出せませんし、見つけることもできません。ensure_post_migrationsのようなものです。

お伝えしておくと、私は何も問題に気づいていません。主にベータリリースブランチをフォローしていますが、私の知る限り、3.1.0.beta…シリーズのすべてのステップでマイグレーションは正しく実行されています。

rake -ATdb:ensure_post_migrations を見つけました。

SKIP_POST_DEPLOYMENT_MIGRATIONS=0 を指定した db:migratedb:ensure_post_migrations の違いは何ですか?

コードを確認したところ、db:ensure_post_migrations が何をするのか理解できました。これは、db:migrate と同じ Rake 実行内で、SKIP_POST_DEPLOYMENT_MIGRATIONS0 に設定されていることを保証するために使用されるはずです。私のスクリプトはすでにそれを保証しています。

.gitlab-ci.yml

./migrate.sh pre || echo "Redis not running during pre migrations, skipping..."
docker stack deploy --prune --resolve-image always -c "$STACK.yml" "$STACK"
./docker-stack-wait.sh -t 180 "$STACK"
./migrate.sh post

migrate.sh

#!/usr/bin/env sh

if [ "$(docker ps -q --filter "label=com.docker.stack.namespace=${STACK}" --filter "label=com.docker.swarm.service.name=${STACK}_${DISCOURSE_REDIS_HOST}" | wc -l)" = "0" ]
then
    echo "No redis container found, unable to run migrations!"
    exit 1
fi

if [ "$1" = "pre" ]
then
    skip_post=1
else
    skip_post=0
fi


docker run \
    --rm \
    --name "discourse-migration-${DISCOURSE_DB_HOST}-${DISCOURSE_DB_NAME}" \
    --network "${STACK}_discourse" \
    --workdir /var/www/discourse \
    -u discourse \
    -e SKIP_POST_DEPLOYMENT_MIGRATIONS="$skip_post" \
    -e LANG="${LANG}" \
    -e DISCOURSE_DEFAULT_LOCALE="${DISCOURSE_DEFAULT_LOCALE}" \
    -e DISCOURSE_HOSTNAME="${DISCOURSE_HOSTNAME}" \
    -e DISCOURSE_DEVELOPER_EMAILS="${DISCOURSE_DEVELOPER_EMAILS}" \
    -e DISCOURSE_SMTP_ADDRESS="${DISCOURSE_SMTP_ADDRESS}" \
    -e DISCOURSE_SMTP_PORT="${DISCOURSE_SMTP_PORT}" \
    -e DISCOURSE_DB_USERNAME="${DISCOURSE_DB_USERNAME}" \
    -e DISCOURSE_DB_PASSWORD="${DISCOURSE_DB_PASSWORD}" \
    -e DISCOURSE_DB_HOST="${DISCOURSE_DB_HOST}" \
    -e DISCOURSE_DB_NAME="${DISCOURSE_DB_NAME}" \
    -e DISCOURSE_REDIS_HOST="${DISCOURSE_REDIS_HOST}" \
    "$DISCOURSE_IMAGE" \
    bundle exec rake db:migrate

これは、古いバージョンの Discourse が実行されている間に、Docker Swarm 上で新しいイメージで SKIP_POST_DEPLOYMENT_MIGRATIONS=1 を指定して db:migrate を実行します。その後、新しいイメージを Swarm にデプロイし、それが収束するのを待ちます。最後に、SKIP_POST_DEPLOYMENT_MIGRATIONS=0 を指定して db:migrate を再度実行します。

これは、2 年以上にわたり、すべてのバージョンで確実に機能しています。@simonk さん、あなたの場合も機能したとのことですが、私のスクリプトとは根本的に異なることをしましたか?

いいえ、私はまだ私がここに概説したのと同じプロセスに従っています。これは、私の知る限り、あなたのものとほぼ同じです。ベア rake db:migrate を使用していますが、bundle exec rake db:migrate ではありません。しかし、それが大きな違いを生むとは想像できません。

Docker stack や swarm を使用したことはありません。migrate.sh スクリプトが新しいイメージではなく古いイメージを使用する原因となる、スクリプト内のどこかにバグがある可能性はありますか?

それを明示的に確認していません。確認してみます。スウォームは確実に最新のイメージを使用しますが、何らかの理由でCIスクリプトが最新のものを使用しなかった可能性があります。

3.1.1 のアップデートで確認しましたが、確かに CI スクリプトが古いバージョンのコンテナを使用していました。