セキュリティグループからの通信許可はAWS::EC2::SecurityGroupIngressに書く

AWS CloudFormationでセキュリティグループを作成する場合、通信許可の設定は、以下のどちらでも書ける。

* [AWS::EC2::SecurityGroup](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html) リソースの `SecurityGroupIngress` または `SecurityGroupEgress` プロパティ
* [AWS::EC2::SecurityGroupIngress](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group-ingress.html) リソースまたは [AWS::EC2::SecurityGroupEgress](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-security-group-egress.html) リソース

セキュリティグループをつくろうとしているのだから、どちらにしてもAWS::EC2::SecurityGroupリソースは作るので、そこのプロパティに纏めてしまう方が手軽。ただし、インバウンド通信（Ingress）の通信元やアウトバウンド通信（Egress）の通信先にセキュリティグループを指定するときは、AWS::EC2::SecurityGroupIngressリソースやAWS::EC2::SecurityGroupEgressリソースとして外だしした方がよいかもしれない。

# SecurityGroupプロパティに書くことの問題点

次のようなケースで、問題が発生する。

* **セキュリティグループAに、セキュリティグループA自身からのアクセスを許可する**。接続元に `!Ref セキュリティグループA` のように自分自身を指定することになり、自己参照になる。この構成は、例えば[AWS Glue](https://docs.aws.amazon.com/ja_jp/glue/latest/dg/setup-vpc-for-glue-access.html)や[Amazon MWAA](https://docs.aws.amazon.com/ja_jp/mwaa/latest/userguide/vpc-security.html#vpc-security-sg-example)の設定でも出てくる。
* **セキュリティグループAはセキュリティグループBはからのアクセスを、BはAからのアクセスを許可する**。セキュリティグループAでは接続元に `!Ref セキュリティグループB` を、 セキュリティグループBは接続元に `!Ref セキュリティグループA` を指定することになり、循環参照になる。例えばWebサーバーAと監視兼ファイルサーバーBがあり、サーバーAはファイル取得のためにサーバーBにアクセスし、サーバーBはプロセスやリソース監視のためにサーバAにアクセスする、といった構成がある。

実際にCloudFormationスタックを作成してみると、セキュリティグループAの作成中にどちらのケースでも、通信許可対象のリソースの作成完了を待機する。前者では、通信許可対象のリソースが自分自身だから、自分自身が作成完了するまで自分自身が作成完了しないというパラドックスめいたことになる。後者では、通信許可対象のセキュリティグループBが作成完了するまで待機するけど、セキュリティグループBも通信元に設定するセキュリティグループAが作成完了するのを待機してしまうので、デッドロックのような形になってしまう。

最終的に、CloudFormationスタック作成はタイムアウトするまで待たされた上に、失敗する。

# AWS::EC2::SecurityGroupIngressに外だし

上記の問題を回避するため、セキュリティグループからの通信許可はAWS::EC2::SecurityGroupリソース内にプロパティとして書くのではなく、AWS::EC2::SecurityGroupIngressリソースとして外だしする。これにより、自己参照や循環参照がなくなり、またリソースの依存関係も適切に処理されて、CloudFormationスタックの問題が起きなくなる。

## 自分自身からのアクセスを許可

次のようにAWS::EC2::SecurityGroupリソース内にプロパティとして書きたくなるケース。最後の行で自分自身を参照（自己参照）している。

```YAML
Resources:

  SecurityGroupA:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Self Reference Test
      VpcId: vpc-06e4ab6c6cEXAMPLE
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        SourceSecurityGroupId: !Ref SecurityGroupA
```

`SecurityGroupIngress` プロパティの指定をやめ `AWS::EC2::SecurityGroupIngress` リソースを追加する。

```YAML
Resources:

  SecurityGroupA:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Self Reference Test
      VpcId: vpc-06e4ab6c6cEXAMPLE

  SecurityGroupIgressAtoA22:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
      FromPort: 22
      ToPort: 22
      GroupId: !Ref SecurityGroupA
      SourceSecurityGroupId: !Ref SecurityGroupA
```

## 相互にアクセスを許可

次のようにAWS::EC2::SecurityGroupリソース内にプロパティとして書きたくなるケース。それぞれのリソースの最後の行で、お互いを参照（循環参照している。

```YAML
Resources:

  SecurityGroupA:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Circular Reference Test - Web Server
      VpcId: vpc-06e4ab6c6cEXAMPLE
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        SourceSecurityGroupId: !Ref SecurityGroupB

  SecurityGroupB:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Circular Reference Test - File and Management Server
      VpcId: vpc-06e4ab6c6cEXAMPLE
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        SourceSecurityGroupId: !Ref SecurityGroupA
```

`SecurityGroupIngress` プロパティの指定をやめ `AWS::EC2::SecurityGroupIngress` リソースを追加する。

```YAML
Resources:

  SecurityGroupA:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Circular Reference Test - Web Server
      VpcId: vpc-06e4ab6c6cEXAMPLE

  SecurityGroupB:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Circular Reference Test - File and Management Server
      VpcId: vpc-06e4ab6c6cEXAMPLE

  SecurityGroupIgressAtoB22:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
      FromPort: 22
      ToPort: 22
      GroupId: !Ref SecurityGroupB
      SourceSecurityGroupId: !Ref SecurityGroupA

  SecurityGroupIgressBtoA22:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
      FromPort: 22
      ToPort: 22
      GroupId: !Ref SecurityGroupA
      SourceSecurityGroupId: !Ref SecurityGroupB
```

# プロパティとAWS::EC2::SecurityGroupIngressの使い分け

通信元や通信先の指定対象が他のセキュリティグループで、お互いに指定しあっているのでなければ、セキュリティグループをAWS::EC2::SecurityGroupリソースのプロパティとして指定しても問題は起きなかった。どのような場合に、プロパティとへの記述をやめてAWS::EC2::SecurityGroupIngressに外だしするか、方針を決めておくといいと思う。例えば以下が考えられる。

* 常にAWS::EC2::SecurityGroupIngressリソースとして外だしで定義する。プロパティは使わない。
* 通信元や通信先の指定対象がセキュリティグループの時は、常にAWS::EC2::SecurityGroupIngressリソースとして外だしで定義する。セキュリティグループ以外であれば、プロパティで指定する。
* 通信元や通信先の指定対象がセキュリティグループで自己参照や循環参照になった時だけ、AWS::EC2::SecurityGroupIngressリソースとして外だしで定義する。基本的にはプロパティで指定する。

個人的には、プロパティのまとまりの良さをすべて捨てるのは惜しいし、外だしの判断をできるだけ簡単にしたいので、2番目の方針を取りたい。

# 参考

* [AWS CloudformationでSecurity Groupの自己参照の設定を行う - YOMON8.NET](https://yomon.hatenablog.com/entry/2019/10/securitygroupselfref)
* CloudFormationリファレンス
  * [AWS::EC2::SecurityGroup](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html)
  * [AWS::EC2::SecurityGroupIngress](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group-ingress.html)
  * [AWS::EC2::SecurityGroupEgress](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-security-group-egress.html)