Cfnで作るECS on Fargateの最小構成

シンプルなECS on Fargateの構成をCloudFrontから作るのに少々苦労したので、テンプレート的にまとめようと思います。

## 構成図
![スクリーンショット 2024-08-17 19.00.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412378/2874a2c7-5a64-5741-bbc5-7a69957447d8.png)


## 前提
- aws-cliのインストール済
- AWSアカウント作成済
- aws-cliのconfigureをセットアップ済

## ディレクトリ構成

```
.
├── architecture.drawio
├── ecr
│   ├── Dockerfile
│   ├── deploy.ecr.sh
│   ├── parameter.ecr.json
│   ├── push.ecr.sh
│   └── template.ecr.yaml
├── ecs
│   ├── deploy.ecs.sh
│   ├── parameter.ecs.json
│   └── template.ecs.yaml
└── vpc
    ├── deploy.vpc.sh
    ├── parameter.vpc.json
    └── template.vpc.yaml
```
なお、コードは[こちら](https://github.com/Shiba-You/qiita-ecs-cfn-test)をご覧ください。
各ディレクトリ内の `parameter.<リソース>.json` および `ecr/push.ecr.sh` を書き換えることでご自身の環境でもご利用できます。

## 各種ポイント
コードは詰まったポイントのみ抜粋します。
詳細は[こちら](https://github.com/Shiba-You/qiita-ecs-cfn-test)をご覧ください。

### ECS -> ECRの通信
ECSがECRからimageをpullする際、以下の2つの方法が存在します。

||NAT Gatewayを利用|各種エンドポイントを利用|
|:-|:-|:-|
|構成図|![スクリーンショット 2024-08-17 19.01.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412378/907142ce-30ea-ccb4-6016-3c4bf9e5fbae.png)|![スクリーンショット 2024-08-17 19.00.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412378/2874a2c7-5a64-5741-bbc5-7a69957447d8.png)|
|メリット|<ul><li>シンプルな構成</li><li>可用性が高い</li><li>スケーラブル</li></ul>|<ul><li>インターネットを経由しないためセキュア</li><li>セキュリティグループによる細かなアクセス制御が可能</li><li>NAT Gatewayより低コスト</li></ul>|
|デメリット|<ul><li>各種エンドポイントより高コスト</li><li>インターネットを経由する</li></ul>|<ul><li>多くのエンドポイントを管理する必要あり</li></ul>|
|コスト|<ul><li>稼働時間: $0.062/h</li><li>データ量: $0.062/GB</li></ul>|<ul><li>稼働時間:$0.042/h（合算値）</li><li>データ量: 約$0.01/GB</li>|

上記からケースにもよりますが、基本的には各種エンドポイントを使用する方がセキュアかつ安価に構築することが可能です。
そのため、今回は各種エンドポイントを利用しています。
なお、必要となるエンドポイントと用途は以下のとおりです。

|エンドポイント|用途|
|:-|:-|
|s3(Gateway)|ECRからのimage pull|
|ecr.dkr|ECRからのiamge pull|
|ecr.api|ECRからのiamge pull|
|logs <br/> (必須ではない)|ECSサービスのログ取得|

logsに関しては必須ではありませんが、これがなければサービスのログを取得することができなくなるので、特別な理由がなければ作成することを推奨します。

S3エンドポイントのみGateway型であるため、VPCのstackで記述しており、それ以外のエンドポイントはECSのstackで記述しています。

```vpc/template.vpc.yaml
  S3Endpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      ....
```

```ecs/template.ecs.yaml
  DkrEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ....
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref Endpointsg
      ....

  ApiEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ....
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref Endpointsg
      ....

  LogsEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ....
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref Endpointsg
      ....
```

ここで注意してほしい点が、ecsで記述しているエンドポイントに `PrivateDnsEnabled` を有効化していることです。
`PrivateDnsEnabled` パラメータはインターフェースVPCエンドポイントがプライベートDNSを使用するかどうかを制御します。
つまり、有効化するとVPC内のみで通信が解決し、無効化すると通信がインターネットを経由することになります。
今回は、VPC内のみで通信が解決してほしいので有効化しています。

また、それぞれのインターフェースVPCエンドポイントにはセキュリティグループを付与できます。
全て以下に従うセキュリティグループを付与します。
- インバウンドルール:
  - ポート範囲: 443 (HTTPS)
  - プロトコル: TCP
  - 送信元: Fargateタスクが存在するサブネットのセキュリティグループ
- アウトバウンドルール:
  - 基本的にすべてのトラフィックを許可（0.0.0.0/0）（必要に応じて特定の範囲を許可）

```ecs/template.ecs.yaml
  Endpointsg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      ....
      SecurityGroupIngress:
        - SourceSecurityGroupId: !GetAtt ECSsg.GroupId
          Description: from ecs
          IpProtocol: tcp
          FromPort: 443
          ToPort: 443
      ....
```

### ECSのExecutionRole
ECSにはTaskを実行するためのExecutionRoleを設定する必要があります。
必要に応じて権限を付与することになりますが、最小の権限は `arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy` に全て用意されています。
そのため、以下のようにIAM Roleを作成してあげます。

```ecs/template.ecs.yaml
  ECSRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "ecs-tasks.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
```

こちらで作成したIAM RoleをTaskDefinitionで指定すればOKです。

### ECSとALBの紐付け
ALBでTargetGroupを設定する際、一般的には以下のようにTargetsパラメータでバランシング対象を設定すると思います。

```ecs/template.vpc.yaml
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      ....
      Targets:
        - Id: !Ref EC2Instance
```

しかしECSでは異なり、ECSサービスからALBのTargetGroupを参照しにいきます。
そのため、以下のようにTargetGroupでは特に指定せず、ECSServiceのLoadBalancers.TargetGroupArnで紐づけるようにします。

```ecs/template.ecs.yaml
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId:
        "Fn::ImportValue": { "Fn::Sub": "${ProjectName}-VpcId" }
      Name: !Sub ${ProjectName}-alb-tg
      Protocol: HTTP
      Port: 80
      TargetType: ip
      
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: ALBListener
    Properties:
      ....
      LoadBalancers:
        - ContainerName: !Sub ${ProjectName}-container
          ContainerPort: 80
          TargetGroupArn: !Ref TargetGroup
      ....
```