CloudFormationで罠に引っかかりまくったので解決法を供養する

# はじめに

この記事は、[TDCソフト株式会社 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/tdc)の13日目です。

業務内でCloudFormationを触る機会があり、その際にかなり苦労してしまったのではまりポイントと解決法をつらつらと書きます。

## CloudFormationとは？

ECSやEC2などのAWSリソースをまとめて自動で作成することが出来るサービスです。
`json`、`yaml`で構成されるリソースファイルに記載されているAWSリソースをまとめて自動作成してくれます。リソース間の依存関係も考慮して作成してくれるため、既存の環境と同じものを作りたい場合に重宝します。

# アーキテクチャ
下図の黄色枠内のリソースを作成するCloudFormationを作りました。
フロントはS3、バックエンドはECS経由して別VPCにあるDBにアクセスする感じです。
VPC間の通信はVPCピアリング接続をしています。

![aaa.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3535910/032e7fa1-43a6-c3a7-da90-003d15215f60.png)

また、今回はFormer2を利用してCloudFormationを作成しています。
Former2については以下の記事をご参照ください。

https://qiita.com/y_inoue15/items/26c245c9e9fdf42c75a4

# つまづいたポイント

## サブネットとインターネットゲートウェイが繋がらない

### 背景

VPCピアリング接続を行う際に、対象のVPCに対してお互いのルートを双方に追加する必要がある。

### 問題点

リソースとしてルートテーブルとルートを追加したが、何故かルートテーブルが2つ作られておりサブネット⇔IGWが繋がっていない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3535910/9f45b95e-81e5-6e13-c959-fa1db9706a1d.png)



```yml
  EC2RouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref EC2VPC

  EC2Route1a:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      VpcPeeringConnectionId: !Ref EC2VPCPeeringConnection
      RouteTableId: !Ref EC2RouteTable
```

### 原因

VPCリソースを新規作成すると、**自動でメインルートテーブルが作成される**ようです。
そのため、複数のルートテーブルを必要としない場合、ルートテーブルのリソース作成は不要になります。

となると自動作成されたメインルートテーブルにルートを追加すればいいのですが、CloudFormationは自力でメインルートテーブルを参照できません。

>CloudFormation を使用して Amazon VPC を作成する場合、CloudFormation はデフォルトで作成されるメインルートテーブルを認識しません。そのため、ルートテーブルに関する情報を Amazon VPC と CloudFormation の間で渡すことはできません。その結果、CloudFormation テンプレートからルートテーブルを参照できないため、メインルートテーブルにルートを追加または削除することはできません。

https://repost.aws/ja/knowledge-center/cloudformation-route-table-vpc

そのため、ルートを追加したい場合はLambdaを経由してメインルートテーブルのIDを取ってこないといけません。
上記リンク内のテンプレートをそのまま流用して、`RouteTableId`で`Lambdatrigger.RouteTableID`を呼び出せばメインルートテーブルにルートを追加できます、

```yml
  EC2Route1a:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      VpcPeeringConnectionId: !Ref EC2VPCPeeringConnection
      RouteTableId: !GetAtt Lambdatrigger.RouteTableID
```



## InternetGatewayがアタッチできない

### 原因

Gateway Attachmentが記述されていない。


```yml
  EC2VPCGatewayAttachment:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      InternetGatewayId: !Ref EC2InternetGateway
      VpcId: !Ref EC2VPC
```

これによってVPCとInternetGatewayが繋がります。
Former2だけでは作成できないリソースなので、忘れずに追記しましょう。

## VPCピアリングのアクセプタ側のルートテーブルにリクエスタ側のルートを追加したい

### 背景

CloudFormationのスタック作成時に上記の操作を自動で行いたい。
また、アクセプタ側にあるEC2内のDBにアクセスしたいため、セキュリティグループのインバウンド追加も行いたい。

### 解決策

リクエスタ側のルートテーブルを指定してルートのリソースを作成します。

```yml
  # DB側ルートテーブルへのルート追加
  EC2RouteDB1a:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: !Select
        - 0
        - Fn::Cidr: [!Ref VpcCidrBlock, 1, 8]
      VpcPeeringConnectionId: !Ref EC2VPCPeeringConnection
      RouteTableId: "アクセプタ側のルートテーブルID"
```
`SecurityGroupIngress`でセキュリティグループへのインバウンド追加を行います。

```yml
  # DB側セキュリティグループのインバウンド追加
  EC2SecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      CidrIp: !Ref VpcCidrBlock
      FromPort: 5432
      GroupId: "セキュリティグループID"
      IpProtocol: "tcp"
      ToPort: 5432
```

## Cloudformationで作成したCloudFrontリソースのタグをスタック更新によって変更できない

### 問題点

CloudFormationで作成したリソースのタグを一括で切り替えたかったが、CloudFrontディストリビューションのみタグが変更できない。

### 原因

スタック更新時のタグ変更は`Resource Tags`プロパティを使用しており、リソースグループを参照しています。
CloudFormationはスタック作成時に自動でリソースグループを作成している、これを利用してタグの変更処理を行っています。

しかし、**CloudFrontはリソースグループでサポートされていない**ため、タグ変更の対象にならないとのこと。

>次の点に注意してください。
>
>- ディストリビューションをタグ付けできますが、オリジンアクセスアイデンティティや無効化をタグ付けすることはできません。
>- [タグエディタ](https://docs.aws.amazon.com/ARG/latest/userguide/tag-editor.html)および [Resource Groups](https://docs.aws.amazon.com/ARG/latest/userguide/resource-groups.html) は、現在のところ CloudFront でサポートされていません。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/tagging.html

### 解決策

スタックによるタグ変更が（おそらく）出来ないため、CLIで個別に変更します。

```sh
# CloudFrontディストリビューションを一覧取得
resources=$(aws cloudfront list-distributions --query 'DistributionList.Items[].{ARN: ARN, DomainName: DomainName, Id: Id}' --output json)
    for resource in $(echo "$resources" | jq -c '.[]'); do
        # arnを抽出
        arn=$(echo "$resource" | jq -r '.ARN')
        # 付いているタグ一覧を取得
        tags=$(aws cloudfront list-tags-for-resource --resource "$arn" --output json)
        # environmentタグの値がreleaseか確認
        release_cloudfront_tag=$(echo "$tags" | jq '.Tags.Items[] | select(.Key == "environment" and .Value == "release")')
        if [ -n "$release_cloudfront_tag" ]; then
            RELEASE_DISTRIBUTION_ARN=$arn
        fi
    done
# environmentタグを削除
aws cloudfront untag-resource --resource "$RELEASE_DISTRIBUTION_ARN" --tag-keys Items=environment,release
# environmentタグを値mainとして追加
aws cloudfront tag-resource --resource "$RELEASE_DISTRIBUTION_ARN" --tags 'Items=[{Key=environment,Value="main"}]'
```

もっとスマートな方法があるかもしれません…

# おわりに

CloudFormationは非常に便利ですが、如何せん参考文献が少なく、ドキュメントも読みにくいのでかなり手探りで進めてしまい苦労しました。
特にCloudFrontのタグ更新については各々のドキュメントを読み漁り、「これだと合点がいくかな？」という結論に至っただけで、もしかしたら普通にスタックからタグ更新出来る裏ワザがあるのかもしれません。

（余談）今回AWS周りの知識が皆無の状態でCloudFormationを作成してしまったのですが、リソース間の依存関係や全体の構成を考慮して作る必要があったので知識だけはモリモリ身についてました。なのでAWS初学者はCloudFormation触るのが意外にいいのかもしれません。