インフラのコード化：CloudFormationを使ったECRとECSの自動化とCI/CDパイプライン設計

## はじめに
この記事では、前回手動で作成したリソースをCloudFormationを使ってコード化する方法について解説します。

:::note warn
**具体的には、ECRにDockerイメージをプッシュした後、そのイメージをECSでデプロイし、実際に動作させる流れをCloudFormationを使って実装します。**
:::

記事内容は将来の自分への備忘録として記録したものであるため、ご了承ください。

## 前回のおさらい

前回の記事では、ECRのプライベートリポジトリに以下のような構成のDockerfileと簡単なHTMLファイルを準備し、そのDockerイメージをECRにプッシュしました。

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

**プロジェクト構成**

```css
├── Dockerfile
└── html/
    └── index.html
```

このDockerイメージをECRにプッシュした後、そのイメージをECSでデプロイし、実際に動作させました。

https://qiita.com/free-honda/items/813b0baf8d993b300cc3

:::note warn
**本記事では、VPCなどのシンプルなネットワーク構成からECS周りの設定までをCloudFormationでコード化し、より簡単にデプロイする方法を紹介します。**
:::

## CloudFormationによるECSとECRの設定

以下のCloudFormationテンプレートを使用して、ECSの設定をコードで管理していきます。これにより、手動での設定作業を減らし、インフラの再現性を高めることができます。

```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"

  # 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-app-container"
          Image: "xxx.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-repo:latest"
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              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
``# ECSタスク定義``の``Image: "xxx.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-repo:latest"``の部分は、実際のECRのURIに置き換えて修正してください。
:::

### CloudFormationテンプレートの説明

``AWS::ECS::Cluster`` リソースを使って、Fargateを使ったECSクラスターを作成します。

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

``AWS::ECS::TaskDefinition`` リソースを使って、コンテナの設定（イメージ、メモリ、CPUなど）を記述したタスク定義を作成します。

```yaml
  # 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-app-container"
          Image: "340823193247.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-repo:latest"
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              Protocol: "tcp"
```

``AWS::ECS::Service`` リソースで、作成したクラスター上にタスクを実行するためのサービスを作成します。ネットワーキング設定には、必要なVPCやセキュリティグループを設定します。

```yaml
  # 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"
```

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/1f238e06-6895-82e7-2628-fa85366f6ac7.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/1c75259d-d21d-c09a-f4ca-e962527617fb.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)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/0f0a08f4-d6e4-cf96-b900-11e27e0176db.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/e2fb4a3a-b0a5-2604-3d21-95865667c542.png)

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

デプロイが完了したら、タスクが正常に実行されていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/dc1a3708-a499-ca1c-d714-e8eb5ea4d632.png)

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

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

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

```bash
http://18.183.70.246/
```

実際に、前回ECRにプッシュした``index.html``ファイルの内容が表示されていることを確認できました。この確認をもって、検証は大成功です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/56d10eb2-2b47-2894-1a71-144159c663d8.png)

:::note warn
**今回は検証のため、ホストポートとコンテナポートを両方 80 に設定しているため、HTTP通信で接続しています。**
:::

## まとめ

ECSなどのコンテナをコード化するのは、思った以上に手間がかかり、何度もエラーに直面しました。

:::note
**特に、CloudFormationを使ったリソースの設定には多くの調整が必要で、時間がかかりました。**
:::

最初は手動で設定した方が早いと思いましたが、最終的にはコード化することで再利用性が高まり、運用面での効率化が実現できました。

## 関連記事

CloudFormationによるインフラのコード化についても過去の記事で解説しているため、より深く知りたい方は読んでみてください！

https://qiita.com/free-honda/items/5a8c69df40c631549663

https://qiita.com/free-honda/items/06d092c82d1dcb1e77fe

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

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

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

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