AWS CloudFormation でセキュリティグループを更新する際の罠（注意点）

この記事は[レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)の4日目の記事となります。

## はじめに
はじめまして。中途採用3年目でエンジニアをしている徐と申します。
現在は、主に[Eggs Pass（旧TOWER CLOUD）](https://towercloud.jp/)のバックエンド領域で業務をしております。
好きなプログラミング言語はPythonで、お気に入りのAmazon Web Services（以下、AWS）サービスはAWS Lambdaになります。

Eggs Pass（旧TOWER CLOUD）は、2020年5月にリリースされて以来、たくさんのユーザーが楽曲を配信しています。Eggs Pass（旧TOWER CLOUD）では、楽曲を登録するだけで、Apple Music、Spotify、LINE MUSIC、レコチョクなど、世界中の音楽配信ストアで楽曲を販売することができます。

これにより、アーティストは販売や再生に応じて収益を得ることができます。今後も、たくさんの機能を追加する予定ですので、ぜひご注目ください。

さらに詳しく知りたい方は、以下のリンクをご確認ください。
https://towercloud.jp/distribution/

## 今回のテーマ
AWS CloudFormation でセキュリティグループ（以下、SG）の定義して環境反映をする際に、自分たちの期待値と違った動きによりトラブルが発生しました。その発生原因が、少し気付きにくいポイントでしたので、簡単にまとめて共有したいと思います。

## 目次
  -  背景（作業内容と発生した問題）
  -  CloudFormationのテンプレートサンプル
  -  何の罠があるのか
  -  発生した事象の詳細
  -  この問題への対策
  -  まとめ
  -  最後に

## 背景（作業内容と発生した問題）
Eggs Pass（旧TOWER CLOUD）のバックエンドシステムのインフラ環境ではAWSで構築しており、その一環としてインフラ構成管理をAWS CloudFormation（以下、CloudFormation） で実現しています。

今回、社内のネットワークの切り替えに伴い、SGの通信許可を変更するためにCloudFormationのテンプレートを修正して検証および本番環境へデプロイする作業がありました。その作業の中で、検証環境では想定通りに動作したのですが、本番環境のプレリリースの時には想定外の動きになり、環境の切り戻し作業が発生しました。

具体的には、本番環境のプレリリース時には、削除されて欲しくないインバウンドの通信許可設定が削除されてしまい、特定の経路から通信が出来ない状態となりました。

なので、本記事では「なぜ、SGの通信許可が消えたのか！？」、CloudFormationとSGの仕様に隠された罠について解説をいたします。

## CloudFormationのテンプレートサンプル
今回、発生した問題を検証するためにCloudFormationのテンプレートとして、以下を準備しました。

【新規作成として実行するサンプルのテンプレート】
```
Parameters:

  EnvironmentName:
    Description: An environment name that will be prefix to resource names
    Type: String
    AllowedValues:
      - dev
      - dvm
      - pro

Conditions:

  isDevelop:
    !Equals [ !Ref EnvironmentName, dev ]

  isDevmaster:
    !Equals [ !Ref EnvironmentName, dvm ]

  isProduction:
    !Equals [ !Ref EnvironmentName, pro ]

  MyIP:
    Description: my IP
    Type: String
    Default: 155.155.155.10/32

SecurityGroupIngress:
        - CidrIp: !If
            - isDevelop
            - !Ref MyIP
            - 0.0.0.0/0
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
          Description: my ip
```
### 【更新作業として実行するサンプルのテンプレート】
```
Parameters:

  EnvironmentName:
    Description: An environment name that will be prefix to resource names
    Type: String
    AllowedValues:
      - dev
      - dvm
      - pro

Conditions:

  isDevelop:
    !Equals [ !Ref EnvironmentName, dev ]

  isDevmaster:
    !Equals [ !Ref EnvironmentName, dvm ]

  isProduction:
    !Equals [ !Ref EnvironmentName, pro ]

  MyNewIP1:
    Description: my MyNewIP1
    Type: String
    Default: 155.155.155.11/32

  MyNewIP2:
    Description: my MyNewIP2
    Type: String
    Default: 155.155.155.12/32

SecurityGroupIngress:
        - CidrIp: !If
            - isDevelop
            - !Ref MyNewIP1
            - 0.0.0.0/0
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
          Description: my new ip 1
        - CidrIp: !If
            - isDevelop
            - !Ref MyNewIP2
            - 0.0.0.0/0
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
          Description: my new ip 2
```
### 【このテンプレートで実現したいこと】
- dev環境には、社内の特定のIPアドレスで通信制限をしたい。
- dev環境以外は、通信制限がないためANY（0.0.0.0/0）を設定したい。
- MyIPがこれまでのIPアドレス、MyNewIP1とMyNewIP2が新しいIPアドレスとなる。
- 更新用テンプレートでは、MyIPを取り除いてMyNewIP1とMyNewIP2を追加する。

## 何の罠があるのか
上記のテンプレートについて、YAMLの形式やCloudFormationのテンプレートとして基本的な書き方にはなにも問題はありません。このテンプレートにおける注意点は、２つあります。

**一つ目はSGにおける１レコードを特定するための主キーがIPアドレス＋ポート番号である点**になります。
**二つ目は、CloudFormationにおける更新処理は、①リソース作成、②リソース削除の順に動作する点**になります。

CloudFormationのテンプレートで変数のように定義されているため、MyIPとMyNewIP1とMyNewIP2は別々の要素として取り扱われて影響が分離されているように見えていますが、SGの仕様としては同じ要素として扱われています。また、CloudFormationの仕様として最後にリソース削除をするため、結果的に0.0.0.0/0＋443ポートのレコードが削除されてしまいます。

## 発生した事象の詳細
では、上記の罠に関して、もう少し具体的に説明をさせて下さい。

### 【 凡例】
![image-20231030015310377.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2423004/a101da44-ba08-d5c8-b1c1-909c96ec0355.png)

### ①新規作成用テンプレートを実行する。
dev環境以外で「【新規作成として実行するサンプルのテンプレート】」を実行した場合、以下のように0.0.0.0/0＋443ポートのレコードが１つ存在した状態となります。

![image-20231030015220098.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2423004/c6a91e21-6fd5-fbb4-248c-4c2b594ee6c5.png)


### ②更新用テンプレートを実行する。
手順①の後に「【更新作業として実行するサンプルのテンプレート】」を実行します。
まず、新しい定義であるMyNewIP1とMyNewIP2を作成する処理が実行されます。
ただし、SGの仕様としては、IPアドレス＋ポート番号でユニークとするため、実際の処理はスキップ（上書き更新）となり、結果的には１レコードのみ残ります。

![image-20231030015020329.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2423004/a49811af-049b-8337-5136-0385c62a3bb5.png)


そして、my IPの削除として0.0.0.0/0＋443ポートのレコードを削除するため、更新用のCloudFormationのテンプレートの実行結果は、0.0.0.0/0＋443ポートの削除となってしまいます。

![image-20231030014924358.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2423004/4f6116cf-13c8-bee8-e401-180afbbe8891.png)


## この問題への対策
重複したリソースの定義は CloudFormation のアンチパターンの一つかもしれません。
CloudFormation で取り扱う各リソースが何を持ってどのリソースとして特定するのか正しく理解して利用する必要があると改めて理解を深めました。

そのため、今回のテンプレートのように、開発環境以外は 0.0.0.0/0 を設定するようなリソースを重複して定義することをやめて、環境ごとに明示的な設定を行うように改善しました。つまり、dev環境の場合はdev環境向けの設定を行い、それ以外の場合は、それぞれ環境向けの設定をするようにCloudFormationのテンプレートを修正しました。

## まとめ
CloudFormationは非常に便利です。簡単にインフラを構築が出来てAWSのリソースの変更記録やバージョン管理も容易に行えます。しかしながらメリットがある一方で、正しく仕組みや仕様を理解しないと意図しない動作になり、障害になることもあるため、理解深めて取り扱いたいと思います。

## 最後に
最後まで読んでいただき、ありがとうございます。
レコチョクでは、フルクラウド（AWS）環境でのWEBシステム開発にチャレンジする環境があります。それの中でシステム開発にチャレンジしたいメンバーを募集しています。少しでも興味のある持ってくれた方は、レコチョク （採用ページ）からぜひご応募下さい！

■レコチョク （採用ページ）
https://recruit.recochoku.jp/

明日の [レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku) は5日目「ChatGPTで仕様に沿ったコードレビューをしてくれる GPTを作成してみた」です。お楽しみに！

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。 
 
https://techblog.recochoku.jp/9855