Django開発備忘録：Docker を使って Nginx + Django のコンテナを ECR にプッシュし、ECS にデプロイしてみた

## はじめに
まず、大前提として、ここから先は現在キャッチアップしているコンテナ関連の学習のために、ECR や ECS を活用しながら、さまざまなコードを書いていきます。

:::note alert
**※補足事項**
***そのため、内容は完全に自分向けとなっています。***
:::

また、このブログ投稿サイトは、自分自身の備忘録としても活用する予定です。***記録を積み重ねながら、メモを残していきます。***

## 実際の作業の流れ

**実際の作業の流れをまとめましたが、本記事ではすべての手順を詳しく紹介するわけではありません。あらかじめご了承ください。**

- CloudWatch にロググループ作成
- 対象のリポジトリをクローンする
- ECR に Docker イメージをプッシュする
- ECS でクラスターを作成する
- カスタム JSON を使ってタスク定義を作成する
- ECS のサービスを作成する
- ブラウザからタスク定義のパブリック IP アドレスにアクセスする


## ディレクトリ構成

```bash
reverse-proxy-django/
│── .env                   # 環境変数ファイル（SECRET_KEY など）
│── docker-compose.yml     # Docker Compose ファイル
│── nginx/
│   ├── default.conf       # Nginx 設定ファイル（リバースプロキシ設定）
│── django_app/
│   ├── Dockerfile         # Django の Dockerfile
│   ├── requirements.txt   # Django の依存関係（Django, Gunicorn）
│   ├── manage.py          # Django 管理コマンド
│   ├── templates/         # Django のテンプレートディレクトリ（将来的に使用）
│   └── mysite/            # Django プロジェクト
│       ├── __init__.py    
│       ├── settings.py    # Django の設定ファイル
│       ├── urls.py        # ルーティング設定
│       ├── wsgi.py        # WSGI アプリケーションのエントリーポイント
│── nginx.Dockerfile       # Nginx の Dockerfile
```

## それぞれのソースコードはこちら

**ローカル開発用リポジトリ**

https://github.com/free-honda/reverse-proxy-django-dev

**ローカル開発の記事**

https://qiita.com/free-honda/items/57bd55b1f70348295a13

**本番 ECS 用リポジトリ**

https://github.com/free-honda/reverse-proxy-django-prd

現在、それぞれのリポジトリには README などの説明は記載していませんが、今後追加していく予定です。

## Django の通信経路
Nginx は Django へのリバースプロキシとして機能します。

```css
[ブラウザ] → [ECS上のNginx (ポート80)] → [Django (ポート8000)]
```

## CloudWatch にロググループ作成

**CloudWatch にロググループが存在するか確認**

まず、ロググループがすでに存在しているかを確認します。

```baah
aws logs describe-log-groups --query "logGroups[*].logGroupName"
```

- ここに `"/aws/ecs/reverse-proxy-cluster"`（設定したロググループ）が存在しない場合、次のステップへ進みます。

**ロググループを作成**

ロググループが存在しない場合、以下のコマンドで作成します。

```bash
aws logs create-log-group --log-group-name "/aws/ecs/reverse-proxy-cluster"
```

:::note alert
**※注意点**
カスタム JSON ポリシーを使用する場合、この作業を事前に行わないと、以下のエラーが発生します。
:::

**エラー内容：**
`ResourceInitializationError: failed to validate logger args: create stream has been retried 1 times: failed to create CloudWatch log stream: ResourceNotFoundException: The specified log group does not exist. : exit status 1`

## ECR にプッシュする際のコツ

**Django アプリケーションのビルド**

`django_app` ディレクトリに移動してから、以下のコマンドを実行します。

```bash
docker build -t reverse-proxy-django-app .
```

**Nginx のビルド**

Nginx の Dockerfile はプロジェクトルートにあるため、以下のように `-f` オプションを指定してビルドします。

```bash
# Nginx のビルド
docker build -t reverse-proxy-django-nginx -f nginx.Dockerfile .
```

**ECR へのプッシュ**

その他のコマンドについては、ECR のリポジトリ作成時に表示されるプッシュコマンドをそのまま使用しています。

## ECS のタスク定義が重要

これまでは AWS マネジメントコンソール（マネコン）から設定を行っていましたが、カスタム JSON を使うことで、一発で作成することができます。

```json
{
  "family": "reverse-proxy-task",
  "executionRoleArn": "arn:aws:iam::xxx:role/ecsTaskExecutionRole",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "2048",
  "containerDefinitions": [
      {
          "name": "django",
          "image": "xxx.dkr.ecr.ap-northeast-1.amazonaws.com/reverse-proxy-django-app",
          "cpu": 512,
          "memory": 1024,
          "portMappings": [
              {
                  "containerPort": 8000,
                  "protocol": "tcp"
              }
          ],
          "essential": true,
          "environment": [
              {
                  "name": "DJANGO_SETTINGS_MODULE",
                  "value": "mysite.settings"
              },
              {
                  "name": "DJANGO_SECRET_KEY",
                  "value": "your-secret-key"
              },
              {
                  "name": "STATIC_ROOT",
                  "value": "/app/staticfiles"
              }
          ],
          "logConfiguration": {
              "logDriver": "awslogs",
              "options": {
                  "awslogs-group": "/aws/ecs/reverse-proxy-cluster",
                  "awslogs-region": "ap-northeast-1",
                  "awslogs-stream-prefix": "ecs"
              }
          }
      },
      {
          "name": "nginx",
          "image": "xxx.dkr.ecr.ap-northeast-1.amazonaws.com/reverse-proxy-django-nginx",
          "cpu": 512,
          "memory": 1024,
          "portMappings": [
              {
                  "containerPort": 80,
                  "protocol": "tcp"
              }
          ],
          "essential": true,
          "logConfiguration": {
              "logDriver": "awslogs",
              "options": {
                  "awslogs-group": "/aws/ecs/reverse-proxy-cluster",
                  "awslogs-region": "ap-northeast-1",
                  "awslogs-stream-prefix": "ecs"
              }
          }
      }
  ]
}
```

## ECS（Fargate）でのセキュリティグループ設定
現在の ECS 環境では、Nginx + Django（Gunicorn） の構成で動作しており、適切な セキュリティグループ（SG） の設定が必要です。

:::note info
***現時点では、ブラウザからのアクセスを目的として以下のルールを許可しました。***
:::

（**※ただし、一部不要な設定も含まれているため、適宜見直しが必要と考えています。**）

| プロトコル | ポート        | 方向       | 許可元 / 許可先                 | 説明 |
|------------|-------------|------------|----------------------------|-----------------------------|
| TCP        | 80         | インバウンド  | 0.0.0.0/0 または ::/0      | インターネットから Nginx へ HTTP アクセスを許可 |
| TCP        | 443        | インバウンド  | 0.0.0.0/0 または ::/0      | (HTTPS を使用する場合) SSL 通信を許可 |
| TCP        | 8000       | インバウンド  | nginx のセキュリティグループ | Nginx から Django (Gunicorn) への接続を許可 |
| TCP        | 80         | アウトバウンド | 0.0.0.0/0                   | Nginx が外部へ通信する場合（API, S3 など） |
| TCP        | 443        | アウトバウンド | 0.0.0.0/0                   | Nginx が外部の HTTPS リソースへアクセスする場合 |
| TCP        | 5432 / 3306 | インバウンド  | Django のプライベート IP     | (データベースが別のサーバーにある場合) Django から DB への接続を許可 |
| ALL        | ALL        | アウトバウンド | 0.0.0.0/0                   | ECS タスクが外部のリソースにアクセスするため |

## その他で使ったコマンド

**タスクのARNを取得**

ECS クラスター内のタスク ARN を取得します。

```bash
aws ecs list-tasks --cluster reverse-proxy-cluster --query 'taskArns' --output text
```

**取得したタスクARNを使って詳細を確認**

上記で取得したタスク ARN を使用し、ECS タスクのエラー理由を確認します。

```bash
aws ecs describe-tasks --cluster reverse-proxy-cluster --tasks arn:aws:ecs:ap-northeast-1:881490128743:task/reverse-proxy-cluster/xxxxxxxxxxxxxxxxxxxx --query 'tasks[*].containers[*].reason'
```

**CloudWatch Logs でエラーの詳細を確認**

エラーの詳細は `reason` に表示されますが、CloudWatch Logs でログを確認することで、より詳しい情報を取得できます。

**ロググループの一覧を取得**

```bash
aws logs describe-log-groups --query "logGroups[*].logGroupName"
```

**コンテナのログを取得**

対象のロググループを見つけたら、以下のコマンドで直近 10 分間のコンテナログを取得します。

```bash
aws logs tail /aws/ecs/containerinsights/reverse-proxy-cluster/application --since 10m
```

**CloudWatch Logs で詳細エラーを確認**

```bash
aws logs describe-log-groups --query "logGroups[*].logGroupName"
```

**ログストリームを確認**

特定のロググループ内のログストリームを確認する場合は、以下のコマンドを使用します。

```bash
aws logs describe-log-streams --log-group-name "/aws/ecs/reverse-proxy-cluster" --query "logStreams[*].logStreamName"
```

**ECS タスク定義にログ設定を追加**

ECS のタスク定義に CloudWatch Logs の設定 を追加することで、コンテナのログを取得できるようになります。

```json
"containerDefinitions": [
  {
    "name": "django",
    "image": "xxx.dkr.ecr.ap-northeast-1.amazonaws.com/reverse-proxy-django-app",
    "memory": 1024,
    "cpu": 512,
    "essential": true,
    "portMappings": [{ "containerPort": 8000 }],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/aws/ecs/reverse-proxy-cluster",
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "ecs"
      }
    }
  }
]
```

**直近のログを取得**

最新のログを確認するには、以下のコマンドを実行します。

```bash
aws logs tail /aws/ecs/reverse-proxy-cluster --since 10m
```

### 最新のログストリームを自動で取得する方法

**Django の最新ログストリームを取得**

以下のコマンドを実行すると、Django の最新のログストリームを取得し、ログを表示できます。

```bash
LOG_STREAM=$(aws logs describe-log-streams --log-group-name "/aws/ecs/reverse-proxy-cluster" --query "logStreams[?starts_with(logStreamName, 'ecs/django')].logStreamName | [0]" --output text)
aws logs tail /aws/ecs/reverse-proxy-cluster --log-stream-names "$LOG_STREAM" --since 10m
```

**Nginx の最新ログストリームを取得**

Nginx の最新のログストリームを取得し、ログを表示するには以下のコマンドを使用します。

```bash
LOG_STREAM=$(aws logs describe-log-streams --log-group-name "/aws/ecs/reverse-proxy-cluster" --query "logStreams[?starts_with(logStreamName, 'ecs/nginx')].logStreamName | [0]" --output text)
aws logs tail /aws/ecs/reverse-proxy-cluster --log-stream-names "$LOG_STREAM" --since 10m
```

## 実際に遭遇したエラーたち

**修正内容一覧**

| 修正箇所            | 修正内容 |
|---------------------|------------------------------|
| `default.conf`      | `proxy_pass` を `127.0.0.1:8000` に変更 |
| `nginx.Dockerfile`  | `COPY default.conf /etc/nginx/conf.d/default.conf` に修正 |
| `settings.py`       | `STATIC_ROOT` を `/app/staticfiles` に設定 |
| `task-def.json`     | `STATIC_ROOT` の環境変数を追加 |
| `docker-compose.yml` | `depends_on` を削除 |
| `task-def.json`     | `networkMode` を `awsvpc` に変更 |


## まとめ
この記事は、自分用の備忘録としてまとめたものです。その点をご理解いただけると幸いです。

:::note info
***もし、この記事の内容が少しでも参考になれば嬉しく思います。***
:::

今後も同様の内容を継続して投稿していきますので、温かく見守っていただけるとありがたいです。