Amazon Copilot は自社内の API の置き換えに試して便利だなと思ったものの、ちょっとブラックボックス過ぎるかなあと感じており、案件に使うにはちょっと時期尚早かなとおもっていたんですが、実際作ってみたらあっさり出来てしまったのでそのまま案件のインフラに採用することになりました。
「あっさり出来た」とは書きましたがちょっとハマったところもあったので、同じような構成をとりたい人の参考になるかもしれないと思い気付いたところを記載しておきたいと思います。
前提としてアプリの構成を書いておくと、メインのアプリはタイトル通り Django でつくったものです。で、リバースプロキシで Nginx を立てており、これらふたつのコンテナがひとつのサービスのタスクです(Django がメインコンテナ、Nginx がサイドカーコンテナとして起動)。コンテナの外部には S3 と RDS(Aurora) があり、アプリケーションとデータのやりとりをします。今回は Pipeline も作成しました。マージしたら開発環境に自動的にデプロイされるところまで、Copilot で作りました。
初期ユーザの作成どうする問題
Django の初期ユーザは
python manage.py createsuperuser
で作成しますので、デプロイしたあとに
copilot svc exec
でコンテナの中に入って実行するだけです。このexecコマンドがあるおかげで助かりました。コンテナの中に入らずに1回だけ実行するのは……よくわかりません。どうやってやるんだろう?
ALLOWED_HOSTS どうする問題
Django には DEBUG という設定値があります。クラウド環境ではこれを false にしてデプロイしていますが、そのばあい ALLOWED_HOSTS という設定値が有効になります。これはアプリケーションがリクエストを受け付けるホストをホワイトリスト方式で設定することで、それ以外からのアクセスを拒否し、アプリをセキュアに保つ仕組みです。
hoge.com というドメインでアプリをホストすることを考えた場合、
ALLOWED_HOSTS = ['localhost','hoge.com']
のような設定になるでしょう。しかしクラウド環境ではこれだけではダメでした。
前にリバースプロキシとして Nginx を立てているので、Django 自体は Nginx からアクセスされるわけで、それはつまり Nginx のコンテナの IP が ALLOWED_HOSTS に必要なのです。
それって、どうやって知るの?
ってわけで、実は ECS はコンテナの IP をメタデータとして返す API を持っています。そのエンドポイントはコンテナが暗黙的な環境変数 ECS_CONTAINER_METADATA_URI として取得 できます。
というわけで最終的には次のように記載しました。
ALLOWED_HOSTS = ['localhost','hoge.com','dev.hoge.com','django.dev.hoge.hoge.com']
METADATA_URI = env.str('ECS_CONTAINER_METADATA_URI', default = None)
if METADATA_URI is not None:
container_metadata = requests.get(METADATA_URI).json()
ALLOWED_HOSTS.append(container_metadata['Networks'][0]['IPv4Addresses'][0])
これでアクセス元の IP を直接知らなくても動的に設定することが出来るようになりました。
マイグレーションどうする問題
データベースのマイグレーションは Django に限らずこの手のアプリであれば考える必要がありそうです。実はここでマイグレーションと同時に collectstatic というコマンドを実行していますがこれについては Django 特有の問題がありまして、これは後述しますのでいったんマイグレーションについて考えます。
Copilot を使うと copilot svc exec とかでコンテナに入って作業が出来てしまうので、コンテナのお腹の中で python manager.py migration
とか打てば出来てしまいますが、せっかく Pipeline で自動化したのに手でマイグレーションを実行するのはナシにしたいですよね。
まず考えたのは CodePipeline 中に行えば良いのかなと言うことでした。ここで問題になるのがマイグレーション時の DB への接続情報で、たとえば CodePipeline の途中でこれをやろうとすると CodePipeline が動いている環境内にDB接続情報を入れなくてはいけなくなります。というか、接続情報だけじゃなくて python manager.py migration
が動くためには Django のアプリそのものが正常に動く状態でないとダメで、デプロイ中にデプロイするためのコードを別環境で起動させるというよくわからないことをしないといけないため、この方法は止めました。
次にサイドカーコンテナでおなじイメージを実行させてマイグレーションを行う方法でした。こちらの記事を参考 にしています。これは、本体のWebアプリとおなじイメージを使ってコンテナを起動させて実行させて終わったら落とす、というかなりスマートなやりかたです。しかし、なぜかDBへの接続情報を環境変数から上手く取得してくれませんでした。もう少し工夫をすればなんとかなるかもしれないと思いつつもいったんこの方法は諦めました(追記:サイドカーには環境変数は継承されないようです https://github.com/aws/copilot-cli/issues/5639 )
というわけで、最終的には entorypoint.sh を作成してそのなかでマイグレーションを実行させるやり方に落ち着きました。
entorypoint.sh はこんな感じになります。
#!/bin/sh
set -e
echo 'Collect Static'
python manage.py collectstatic --noinput
echo 'Migrate DB'
python manage.py migrate
exec "$@"
Django の Dockerfile はこう。
FROM public.ecr.aws/docker/library/python:3.8
ENV PYTHONUNBUFFERED 1
ENV APP_HOME=/hogehoge
WORKDIR $APP_HOME
COPY apps/requirements.txt $APP_HOME/
RUN pip3 install -r requirements.txt
COPY apps/ $APP_HOME/
COPY apps/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"]
ENTRYPOINT と CMD の使い分けの知識が必要かもしれません。ENTRYPOINT に CMD の内容が引数になって渡ってくるので、CMD を上書きしていても動きます。 copilot のマニフェストをもとにしたクラウド側での実行だけではなく、ローカルで docer-compose.yml で CMD を上書きしてもちゃんとマイグレーションが動きます。
collectstatic どうする問題
Django では app という単位でいくつもの機能をフォルダに作成します。その際、各フォルダに静的ファイルが作成されたりします。また管理画面は実行時に生成された静的ファイルを使ったりします。
これら静的ファイル群を一つの場所にまとめて移動してくれるのが python manager.py collectstatic
というコマンドです。このコマンドを打つと、各フォルダ毎の静的ファイルを全て1箇所にまとめてくれます。
このとき、どのパスにまとめるか、というのは settings.py にある次のような記述に従います。
STATIC_URL = '/static/'
STATICFILES_DIRS = [ BASE_DIR / "config/static" ]
STATIC_ROOT = '/static'
最初は STATIC_ROOT の記述が存在しないのと、STATICFILES_DIRS が static のルートを指していたことで上手くいきませんでした(特定のアプリの static のフォルダと、それらをまとめるためのフォルダが同じ場所を指していたのが良くなかったらしい)
これを設定したうえで、先の entrypoint.sh で実行時に /static にファイルをまとめることでローカルの Docker Compose では動くようになりました。
が。ECS では CSS がまったく見つけられなくなっていました。
これは実は当然と言えば当然でした。今回構成上、Django の前にリバースプロキシとして Nginx を立てています。コンテナとしては sidecar の設定で動かしています。
Nginx.conf の設定はこんな感じでした。
upstream django {
server localhost:8000;
}
server {
listen 80;
listen [::]:80;
server_name 0.0.0.0;
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
}
location /static/ {
alias /static/;
}
}
Ngnix の Dockerfile はシンプルです。
FROM public.ecr.aws/nginx/nginx:1.24-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d/nginx.conf
設定にある alias の設定の通り static ディレクトリにあるデータはDjangoには到達せずこの Nginx が直接返すのですが、当然 Nginx のコンテナには python manager.py collectstatic
の結果はありませんからファイルを参照することが出来ないのです(ローカルでテストする際は Nignx を使っていなかったので問題ありませんでした)
そこで Django と Nginx で static フォルダを共有することにしました。共有には Copilot のマニフェストで EFS の設定を記述します。
メインのコンテナの storage 設定に次のように書き、
storage:
volumes:
django-static:
efs: true
path: /static
read_only: false
サイドカーの storage で同じ場所をマウントします。
sidecars:
nginx:
port: 80
image:
build:
dockerfile: nginx/Dockerfile
context: /
mount_points:
- source_volume: django-static
path: /static
read_only: false
variables:
NGINX_PORT: 80
これで python manager.py collectstatic
の結果が /static に集約され、静的ファイルは Nginx がそのまま返すようになりました。
(これは、ルートにまんま /static でマウントされちゃうのでパスはもうすこしWeb サーバぽいものにしてもよかったかもしれないですね)
途中で Fargate Spot に変えられない問題
Fargate には Fargate Spot という EC2 での spot instance にあたる安価なサービスがあります。これも Copilot で設定出来、count.spot という設定値を変えます。ある数以上レプリカが作成されると一部を spot インスタンスにする、などの複雑な設定出来ますが、今回はとくに冗長化をする気は無かったので、コンテナは一つ、かつそのコンテナを Fargate Spot にするという意味で、1に設定してみました。
count:
spot: 1
ところがこれはエラーになります。Fargate か Fargate Spot かの設定をキャパシティプロバイダ戦略というようですが、すでにキャパシティープロバイダ戦略が Fargate でデプロイされているアプリを更新するとエラーになるらしいのです。ここでやりとりされていましたがこれ自体はバグのようで、1.13.2 現在直っていないようです。
ワークアラウンドとして、
network:
connect: true
この設定を true->false にすることで変更が出来ることのこと。ただ、この設定はサービスコネクトの設定値なので、これを false にしてしまうとコンテナ間の通信に問題が生じます(たぶん?)。そこでややこしいですが、いったん false にして spot:1 でデプロイ、そのあとで、trueにして再デプロイをする(2段階デプロイ)ことにしました。これでうまく変更できました。
これはさすがにバッドノウハウ過ぎるので、修正を待ちたいと思います。
Pipeline のターゲットブランチがひとつしかない問題
Pipeline のマニフェストは
name: zenk-nuhw-aws-develop
version: 1
source:
provider: CodeCommit
properties:
branch: develop
repository: https://ap-northeast-1.console.aws.amazon.com/codesuite/codecommit/repositories/hogehoge/browse
stages:
-
name: dev
こんなふうになっていて、ステージの下にブランチの設定はありません。この設定ファイルでは develop ブランチをどこの環境にデプロイするか、しか設定出来ないんですよね。でもやりたいのは develop は dev 環境へ、master は prod 環境へ、みたいなことかと思います。
その場合はパイプラインをブランチの数だけ作るのが現時点での正解っぽいです(パイプラインの数分料金もかかりますのであまりしたくはないのですが……)