GitHub Actionsを活用したDjangoアプリ開発：CloudFormationテンプレートでECRからECSへの自動本番デプロイ

## はじめに
この記事では、GitHub Actionsを活用したDjangoアプリ開発において、CloudFormationを用いてインフラをコード化し、ECSがECRからDockerイメージを自動的にプルして本番環境にデプロイする手順を紹介します。

:::note
***この記事は、過去の技術検証内容を組み合わせた実践的な応用例です。***
:::

今回の内容は少し難易度が高いかもしれませんが、自分の備忘録も兼ねて、手順や実施内容を詳しくまとめていきます。

## 補足：過去記事の組み合わせによる検証

:::note warn
**本記事では、以下の記事で取り上げた技術検証をマージして進めています。**
:::

***CloudFormationでインフラをコード化し、ECSからECRのイメージを自動プルするデプロイ手順***

https://qiita.com/free-honda/items/c7a68a216eef2161c567

***DjangoプロジェクトとDocker関連ファイルを一括作成するシェルスクリプトで環境構築を自動化***

https://qiita.com/free-honda/items/b489cfab873861392de0

***GitHub Actionsを使用したECRへの自動プッシュ設定（CI/CD）***

https://qiita.com/free-honda/items/293d2bbeef10fe4547a8

## やりたいこと
今回のゴールを言葉で説明するのは少し難しいため、以下のイラストで概要を示しています。

![スクリーンショット 2024-12-08 11.47.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/372271a6-1cd0-cbb8-9a7c-c35e552e827b.png)

CloudFormationテンプレートを使用して、ECSクラスタやタスク定義を自動で作成し、GitHub Actionsを利用してDjangoアプリのDockerイメージをECRに自動プッシュしています。

:::note alert
***最終的に、ECSがECRからイメージを取得し、本番環境へデプロイする流れとなります（以下、CloudFormationテンプレートは一部簡略化していますので、ご了承ください）。***
:::

## CloudFormationテンプレートの準備

以下は、今回使用したCloudFormationテンプレートです。このテンプレートには、ECSクラスター、タスク定義、IAMロールなどの必要なリソース設定が含まれています。

```test.yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  # VPCの作成
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: "Name"
          Value: "honda-vpc"

  # サブネットの作成
  PublicSubnet1:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.0.0/20"
      AvailabilityZone: "ap-northeast-1a"
      Tags:
        - Key: "Name"
          Value: "honda-subnet-public1-ap-northeast-1a"

  # インターネットゲートウェイの設定
  InternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
        - Key: "Name"
          Value: "honda-igw"

  AttachInternetGateway:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  # ルートテーブルの設定
  PublicRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: "Name"
          Value: "honda-rtb-public"

  PublicRoute:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

  SubnetRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  # ECS用セキュリティグループ
  SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Allow HTTP traffic"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: "tcp"
          FromPort: 80
          ToPort: 80
          CidrIp: "0.0.0.0/0"
        - IpProtocol: "tcp"
          FromPort: 8000
          ToPort: 8000
          CidrIp: "0.0.0.0/0"  # 追加: ポート8000も許可

  # ECSクラスターの作成
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: "my-ecs-cluster"

  # ECSタスク定義
  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      Family: "my-app-task"
      RequiresCompatibilities:
        - FARGATE
      NetworkMode: "awsvpc"
      Cpu: "256"
      Memory: "512"
      ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: "my-docker-repo"
          Image: "xxx.dkr.ecr.ap-northeast-1.amazonaws.com/my-docker-repo:latest"
          PortMappings:
            - ContainerPort: 8000  # 変更: コンテナのポートを8000に
              HostPort: 8000       # 変更: ホストのポートも8000に
              Protocol: "tcp"

  # ECSタスク実行ロール
  ECSTaskExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "ecs-tasks.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "ecsTaskExecutionPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "ecr:GetAuthorizationToken"
                  - "ecr:GetDownloadUrlForLayer"
                  - "ecr:BatchGetImage"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "s3:GetObject"
                Resource: "*"

  # ECSサービスの作成
  ECSService:
    Type: "AWS::ECS::Service"
    Properties:
      Cluster: !Ref ECSCluster
      TaskDefinition: !Ref ECSTaskDefinition
      DesiredCount: 1
      LaunchType: "FARGATE"
      NetworkConfiguration:
        AwsvpcConfiguration:
          Subnets:
            - !Ref PublicSubnet1
          SecurityGroups:
            - !Ref SecurityGroup
          AssignPublicIp: "ENABLED"
```

:::note warn
***※今回は、[過去の記事](https://qiita.com/free-honda/items/c7a68a216eef2161c567)で作成したテンプレートを基に、コンテナとホストのポート設定、およびセキュリティグループ周りの設定を修正しました。***
:::

## 補足事項：Django アプリケーションの ```ALLOWED_HOSTS``` 設定について

本番環境へのデプロイに際し、Djangoプロジェクトの準備や設定は以下の記事で実施済みであることを想定しています。

:::note alert
**しかし、本番環境でのデプロイ時に設定ファイルの修正が必要となった箇所がありましたので、その内容を紹介します。**
:::

### 発生したエラー
本番環境にCloudFormationテンプレートを使ってデプロイした際、以下のエラーが発生しました。

```エラー.
DisallowedHost at /
Invalid HTTP_HOST header: '43.207.164.191:8000'. You may need to add '43.207.164.191' to ALLOWED_HOSTS.
```

このエラーは、DjangoのALLOWED_HOSTS設定が適切に設定されていないことを示しています。

具体的には、HTTPリクエストのHostヘッダーに含まれる値（例: ```IPアドレス:8000```）が、Djangoの許可リストに登録されていないため、DisallowedHostエラーが発生しました。

### エラーの修正方法
開発環境などで、IPアドレスが変動する場合、一時的な対応としてすべてのホストを許可する方法があります。以下のように、settings.pyのALLOWED_HOSTSを修正します。

```/my_django_project/myproject/settings.py
# settings.py
ALLOWED_HOSTS = ['*']
```

この設定により、すべてのホストからのリクエストが許可されるようになります。

:::note alert
**ただし、本番環境ではセキュリティリスクを回避するため、具体的なホスト名またはIPアドレスのみを設定することを強く推奨します。**
:::

本番環境では、以下のように明確にホストを指定するようにしてください。
```/my_django_project/myproject/settings.py
# settings.py
ALLOWED_HOSTS = ['43.207.164.191', 'example.com']
```

## 実際にやってみた

上記のCloudFormationテンプレートを使って、ECSクラスターを作成し、タスク定義とサービスを設定しました。

CloudFormationのサービス画面からスタックの作成に進み、作成した``yamlファイル``をアップロードします。

![スクリーンショット 2024-12-08 12.12.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/57c8348a-9aa1-a063-bab1-233df05b1729.png)


スタック名は任意で設定できます。ここでは私の名前「``honda``」としています。

![スクリーンショット 2024-12-08 12.12.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/2665607f-7044-ad40-319c-e3ee9dc8d8e5.png)

画面遷移後のスタックオプション設定については、個々の設定に依存するため、私は設定せずに進めていきます。

:::note warn
**ただ、スタックの中でIAMを使う場合は、以下のように確認が求められますでの、✅を入れて進んでください。**
:::

![スクリーンショット 2024-11-13 20.19.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/160ece74-1b4f-808c-d3c5-9407125141fc.png)

最後に確認を行い、問題がなければ最下部までスクロールし、「``送信``」をクリックして完了です。

![スクリーンショット 2024-12-08 12.13.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/136fe109-2495-374e-168c-3c2b404d9233.png)

スタックを実行後、イベントタブで成功したことが確認できました（しばらく時間がかかります）。

![スクリーンショット 2024-12-08 12.15.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/00ee3642-331d-a7e7-6a5a-96acd6b2fcfc.png)


## デプロイ後の動作確認

デプロイが完了したら、タスクが正常に実行されていることを確認します。ここでは、ECRのイメージコンテナからプルされていることも合わせて確認しました。

![スクリーンショット 2024-12-08 12.16.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/f055905e-145e-003d-d184-74cbb130bafe.png)


ECSページの「クラスター」→「タスク」タブから、実行中のタスクを選択し、タスクのパブリックIPを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/fb8f76e8-ef20-d76d-b8fb-6e84fc545be3.png)

ここで、タスクが使用しているパブリックIP（例：``54.250.2.18``）を確認したら、ブラウザでアクセスしてみます。

```bash
http://54.250.2.18:8000/
```

:::note warn
***Djangoの初期ページ（```"The install worked successfully!"```）が表示され、アプリケーションが正しく動作していることを確認できました！***
:::

![スクリーンショット 2024-12-08 12.18.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/48feadec-4526-53bd-9abb-0141f60fe2c8.png)

***技術検証自体はスムーズに行えましたが、複雑なアウトプットを1つの記事として整理するのは非常に力を使いました！笑***

## まとめ
この記事では、CloudFormation、GitHub Actions、ECR、ECSを組み合わせて、Djangoアプリケーションのインフラ構築と自動デプロイの手順を解説しました。

:::note 
***これらの仕組みを取り入れることで、本番環境へのデプロイが大幅に簡略化されます。***
:::

今後も技術検証を続けていきますので、この記事がどなたかの技術的な支えとなれば幸いです。

## おまけ：私はLambda関数がめちゃくちゃ好き

私はLambda関数が大好きで、自宅でもいろいろな検証を行っています。興味のある方は、ぜひ読んで技術検証に挑戦してみてください！

https://qiita.com/free-honda/items/a83050dbac5622103a5b

https://qiita.com/free-honda/items/eef40d5c20acdc191550

https://qiita.com/free-honda/items/f9489f39c90488e733f3

https://qiita.com/free-honda/items/d5c8502cc019b94aa061

https://qiita.com/free-honda/items/ab519bba23545063f021

https://qiita.com/free-honda/items/071fc55d2c8f2aee57c3

https://qiita.com/free-honda/items/be9a9de2c7e3564c0b5c