はじめに
Djangoで作ったWebアプリをAWS Fargateでデプロイした時のポイントをまとめました。
アーキテクチャ
- Public subnetから外部への接続はIGWを使う
- Private subnetからVPC外への接続はVPC Endpointを使う
- ロードバランサーはALBを使う
- マルチAZ構成にする
- ECS FargateはPrivate subnetに構築する
- デプロイ対象のコンテナは3つで、WebサーバのNginx、AppサーバのGunicorn・Django、DBサーバのPostgres
- アプリはフロントエンドとバックエンドをDjangoで作成したTwitterクローンのWebアプリ
- ECSの同一タスク内に3つのコンテナをたてる
- Route53でドメインを購入しhttpsで通信できるようにする
- CloudWatchでコンテナのログを取得する
- S3に画像を保存する
- SSMでコンテナをデバッグする
- EFSでDBデータを永続化する
- SESでSMTPによりメールを送信する
まずはローカル上で開発環境から本番環境への移行準備
Nginx, Gunicornの導入
Djangoの簡易サーバではなく本番用のサーバとして設定が簡単なNginx, Gunicornを採用しました。
Gunicornはpip installしてgunicornコマンドで簡単に起動できました。
gunicorn config.wsgi:application --bind 0.0.0.0:8000
しかし、Nginxは設定が複雑で戸惑いました。
以下を参考にconfファイルを作成し、Gunicornへのリクエストを設定しました。
ポイント:
etc以下にconfファイルを作成し、nginxコンテナにコピーし、かつ、default.confを削除する。
etc
|-nginx
|-conf.d
|-upstream.conf
|-web.conf
FROM nginx:1.25.1
COPY ./etc/nginx/conf.d/* /etc/nginx/conf.d/
RUN rm /etc/nginx/conf.d/default.conf
ポイント:
upstreamはリクエスト先を指定するため、Gunicornのportを指定し、今回はECSの同一タスク内にコンテナをたてるため、同一ホストとみなし、コンテナ間通信はlocalhostで行う(以下同様)。
参考:Fargate 起動タイプの Amazon ECS タスクのネットワークオプション
upstream django_tw {
server localhost:8000 max_fails=3 fail_timeout=10s;
}
ポイント:
serverはnginx自身のポート、ファイルへのルーティング、リクエスト先を指定する。
server {
listen 80;
server_name nginx;
# favicon.icoのログを無効化
location = /favicon.ico {
access_log off;
log_not_found off;
}
# 静的ファイルへのルーティング
location /static/ {
alias /var/www/assets/static/;
}
# メディアファイルへのルーティング
# S3のURLをそのまま利用するので以下は設定しない
# location /media/ {
# alias /var/www/assets/media/;
# }
# アプリケーションへのリクエスト
location / {
proxy_pass http://django_tw;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
access_log /dev/stdout main;
error_log /dev/stderr;
client_max_body_size 10M;
}
}
なお、上記はTCPソケット通信を前提として設定しましたが、今回はECSの同一タスク内にコンテナをたてるため、同一ホストとみなし、Unixドメインソケット通信も可能と考えられます。
設定ファイルの編集
開発用の各設定ファイルから差分を記載する形で本番用の各設定ファイルを作りました。
ポイント:
本番用にsettings.pyからproduction.pyを作成し、DEBUGをFalseにし、Nginxからのリクエストを許可する。
# 本番用に差分のみ記載する
DEBUG = False
CSRF_TRUSTED_ORIGINS = [
'http://localhost:80', # Nginx経由のURLを追加
]
ポイント:
本番用に.envから.env.productionを作成し、DBへのリクエストURLのIPをlocalhostとする。
# コンテナ名ではなくlocalhostを指定する点注意
DATABASE_URL="postgres://<ユーザ名>:<パスワード>@localhost:5432/<DB名>"
ポイント:
本番用にdocker-compose.ymlからdocker-compose.production.ymlを作成し、
- production.pyを読み込むようにする
- nginxコンテナを設定し、Djangoの静的ファイルを共有するためにstatic-dataを定義する
- ※これらは本番ではAWS上で設定するため注意
# 本番用に差分のみ記載する
services:
web
DJANGO_SETTINGS_MODULE: config.settings.production
volumes:
- static-data:/code/staticfiles
nginx
volumes:
- static-data:/var/www/assets/static
depends_on:
web:
condition: service_healthy
volumes:
# 静的ファイル用のボリュームを定義
static-data:
ポイント:
DockerfileのCMDで実行されるが複数行あるためshに分離
python manage.py migrate
python manage.py collectstatic --noinput
gunicorn config.wsgi:application --bind 0.0.0.0:8000
config
|-settings.py
.env
docker-compose.yml
Dockerfile
config
|-settings
|-base.py
|-local.py
|-production.py
.env
.env.production
docker-compose.yml
docker-compose.production.yml
Dockerfile
Dockerfile.db
Dockerfile.nginx
start.sh
start.production.sh
ビルド・デプロイ
ポイント:
AWS用にビルド・デプロイをする前に、docker-compose.production.ymlを使ってローカル上で擬似的に本番用の設定を読み込み手戻りが少なくなるようする。
docker compose -f docker-compose.yml -f docker-compose.production.yml up -d --build
docker compose -f docker-compose.yml -f docker-compose.production.yml down
ポイント:
AWS用にビルド・デプロイをする時は、docker-compose.yml, docker-compose.production.ymlでの環境変数等の設定はAWSコンソールで行うためdocker composeコマンドは使用しない。
docker build -t django-tw/web -f Dockerfile .
docker build -t django-tw/db -f Dockerfile.db .
docker build -t django-tw/db -f Dockerfile.nginx .
ここからAWS上の設定
全体的に以下を参考にしました。
AWSでDockerを本番運用!AmazonECSを使って低コストでコンテナを運用する実践コース
ECSタスク定義
ポイント:
- docker-compose.production.ymlで設定した環境変数等はECSタスクには反映されないため、コンソール上で再度設定する
"environment": [
{
"name": "DJANGO_SETTINGS_MODULE",
"value": "config.settings.production"
},
{
"name": "DJANGO_ENV",
"value": "production"
}
],
"mountPoints": [
{
"sourceVolume": "static-data",
"containerPath": "/code/staticfiles",
"readOnly": false
}
],
"dependsOn": [
{
"containerName": "db",
"condition": "HEALTHY"
}
],
- dbのhealthCheckはpsql,-U,postgresだと対話型モードに移行し短時間で終了しないためエラーとなる。よってpg_isreadyを使う
"healthCheck": {
"command": [
"CMD-SHELL",
"pg_isready -U postgres || exit 1"
],
プライベートサブネットからECRへのアクセス
NATゲートウェイかエンドポイント経由のアクセスがあり、コスト面からエンドポイント経由のアクセスを選択しました(ただ、結局は複数個設定したためNATの方がよいかも・・・)。
ECRからイメージをpullする場合は以下上から3つのエンドポイントを作成する必要があり、ログも含めると計4つになります。
ポイント:
プライベートサブネットからVPC外へのアクセスはエンドポイントを設定する
- com.amazonaws.ap-northeast-1.ecr.api
- com.amazonaws.ap-northeast-1.ecr.dkr
- com.amazonaws.ap-northeast-1.s3
- com.amazonaws.ap-northeast-1.logs
同一タスク内のコンテナ間通信
DjangoコンテナからPostgresコンテナへのアクセスが何度やってもエラーとなりました。そこでFargateでもexecコマンドでコンテナの中に入りデバッグできるので、それで原因を突き止めました。
ポイント:
- 同一タスク内のコンテナ間通信は、ホスト名をコンテナ名(db)ではなくlocalhostにする
DATABASE_URL="postgres://<ユーザー名>:<パスワード>@<ホスト名>:<ポート>/<DB名>"
- execコマンドは必要な権限をロールに付与し、SSM経由でVPC外からのアクセスとなるためエンドポイントを作成する
- com.amazonaws.ap-northeast-1.ssmmessages
- com.amazonaws.ap-northeast-1.ec2messages
参考:Amazon VPC エンドポイントを作成することで、インターネットへのアクセスを必要とせずに、Systems Manager を使用してプライベート Amazon EC2 インスタンスを管理できるようにする方法を教えてください。
ECS Exec を使用して Amazon ECS コンテナをモニタリングする
aws ecs execute-command --region ap-northeast-1 --cluster <クラスター名> --task <タスクID> --container <コンテナ名> --interactive --command "/bin/sh"
IPやPortの設定
ルートテーブルやセキュリティグループでアクセスを制限しますが、ここもはまりポイントで、適切に設定しないとすぐエラーになります。そのため、まずは定義を確認し、初めは前広にアクセスを授受できるように設定しました。
ルートテーブル
ポイント:
- サブネット間の通信経路
- Public subnetは外部向けの送信先0.0.0.0/0 IGWと内部向けの送信先0.0.0.0/16 localを設定
- Private subnetはS3向けのものと内部向けの送信先0.0.0.0/16 localを設定
セキュリティグループ
ポイント:
- VPC内のリソースのアクセス制限
- デフォルトのインバウンドは外部通信ではなく内部通信となるため、VPC内の通信のインバウンドはデフォルトを使う
- アウトバウンドは基本的に0.0.0.0を使えばよい
参考:デフォルトのセキュリティグループとは何かを改めて整理してみた
S3に画像を保存する
ポイント:
- S3ロールの作成
- コンソールからS3バケットを作成
- 必要なライブラリのインストール
pip install boto3 django-storages
- 環境変数の設定(ローカル上で設定)
# DEFAULT_FILE_STORAGE, MEDIA_URLの設定がポイント
DEFAULT_FILE_STORAGE = 'config.settings.storage_backends.MediaStorage'
MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/'
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
- S3にstatic/とmedia/ディレクトリを作成するためにconfig/settings/storage_backends.pyを作成(static/は今回は使用しない)
from storages.backends.s3boto3 import S3Boto3Storage
class StaticStorage(S3Boto3Storage):
"""S3のstatic/ディレクトリに保存"""
location = 'static'
class MediaStorage(S3Boto3Storage):
"""S3のmedia/ディレクトリに保存"""
location = 'media'
S3のmedia/に画像が保存されることを確認しました。
EFSでDBデータを永続化する
コンテナのvolume領域だとタスク更新時に消えてしまうので、永続化するためにコスト面からRDSではなくEFSとしました。
ポイント:
- コンソールからファイルシステムの作成
- Private subnetへのアタッチ
- エンドポイントのPrivate subnetだとエラーとなるためdbコンテナのPrivate subnetとする
-
エンドポイントの作成
- com.amazonaws.region.elasticfilesystem
参考:Amazon EFS でのインターフェイス VPC エンドポイントの使用
SESでSMTPによりメールを送信する
ポイント:
- コンソールから使用を開始
-
エンドポイントの作成
- com.amazonaws.ap-northeast-1.email-smtp
参考:Amazon Simple Email Service エンドポイントとクォータ
- 必要なライブラリのインストール
pip install django-ses
- 環境変数の設定(ローカル上で設定)
EMAIL_HOST = "email-smtp.ap-northeast-1.amazonaws.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = env("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = True
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL")
メールの送付を確認しました。
動作確認
問題なく起動・操作できることを確認しました。
おわりに
構築するのにかなり時間がかかり苦労しました・・・。ただ、本番環境へのデプロイの難しさがわかり、苦労した分理解も深まりました。
今後のTODOとして以下を記します。
- EC2で構築しFargateとの違いを確認
- 必要な通信のみに絞る、環境変数を見直す等セキュリティ面の強化
- NATやRDSなど今回使用しなかったリソースを使ってみる
- SPAのデプロイ
- Terraformで自動化