0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Djangoで作ったWebアプリをAWS Fargateでデプロイした

Posted at

はじめに

Djangoで作ったWebアプリをAWS Fargateでデプロイした時のポイントをまとめました。

アーキテクチャ

Architecture4.jpg

  • 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
Dockerfile.nginx
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.conf
upstream django_tw {
  server localhost:8000 max_fails=3 fail_timeout=10s;
}

ポイント:
serverはnginx自身のポート、ファイルへのルーティング、リクエスト先を指定する。

web.conf
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からのリクエストを許可する。

production.py
# 本番用に差分のみ記載する
DEBUG = False
CSRF_TRUSTED_ORIGINS = [
    'http://localhost:80',  # Nginx経由のURLを追加
]

ポイント:
本番用に.envから.env.productionを作成し、DBへのリクエストURLのIPをlocalhostとする。

.env.production
 # コンテナ名ではなくlocalhostを指定する点注意
DATABASE_URL="postgres://<ユーザ名>:<パスワード>@localhost:5432/<DB名>"

ポイント:
本番用にdocker-compose.ymlからdocker-compose.production.ymlを作成し、

  • production.pyを読み込むようにする
  • nginxコンテナを設定し、Djangoの静的ファイルを共有するためにstatic-dataを定義する
  • ※これらは本番ではAWS上で設定するため注意
docker-compose.production.yml
# 本番用に差分のみ記載する
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に分離

start.production.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タスクには反映されないため、コンソール上で再度設定する
e.g. webコンテナ
            "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を使う
dbコンテナ
            "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にする
.env.production
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バケットを作成
  • 必要なライブラリのインストール
requirements.txt
pip install boto3 django-storages
  • 環境変数の設定(ローカル上で設定)
production.py
# 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/は今回は使用しない)
storage_backends.py
from storages.backends.s3boto3 import S3Boto3Storage


class StaticStorage(S3Boto3Storage):
    """S3のstatic/ディレクトリに保存"""
    location = 'static'

class MediaStorage(S3Boto3Storage):
    """S3のmedia/ディレクトリに保存"""
    location = 'media'

S3のmedia/に画像が保存されることを確認しました。

image.png

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 エンドポイントとクォータ

  • 必要なライブラリのインストール
requirements.txt
pip install django-ses
  • 環境変数の設定(ローカル上で設定)
production.py
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")

メールの送付を確認しました。

image.png

動作確認

問題なく起動・操作できることを確認しました。

image.png

おわりに

構築するのにかなり時間がかかり苦労しました・・・。ただ、本番環境へのデプロイの難しさがわかり、苦労した分理解も深まりました。
今後のTODOとして以下を記します。

  • EC2で構築しFargateとの違いを確認
  • 必要な通信のみに絞る、環境変数を見直す等セキュリティ面の強化
  • NATやRDSなど今回使用しなかったリソースを使ってみる
  • SPAのデプロイ
  • Terraformで自動化
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?