LoginSignup
1
0
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

Amazon Copilot で Django アプリを ECS にデプロイしたときに気付いたいろいろ

Posted at

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への接続情報を環境変数から上手く取得してくれませんでした。もう少し工夫をすればなんとかなるかもしれないと思いつつもいったんこの方法は諦めました。

というわけで、最終的には 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 環境へ、みたいなことかと思います。

その場合はパイプラインをブランチの数だけ作るのが現時点での正解っぽいです(パイプラインの数分料金もかかりますのであまりしたくはないのですが……)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0