AWS CloudFormationで循環依存エラーを解決する書き方を整理してみた

AWS CloudFormationのテンプレートでセキュリティグループなどを作成している際に、循環依存エラーが発生する場合があります。このエラーを解決する場合は、循環的に発生している依存関係を断ち切る必要があります。今回はそちらのやり方を紹介します。
2024.01.26

AWS CloudFormationで循環依存エラーを解決したい

おのやんです。

みなさん、AWS CloudFormation (以下、CFn) で循環依存エラーに遭遇したことはありませんか?私は何度かあります。

セキュリティグループなどのリソースでは、CFnで記述する際に自らを参照するケースが多々あります。この際、CFnの仕様によってリソースの作成が失敗する場合があります。この解消方法についていくつか試行錯誤してみたので、今回はそちらを紹介したいと思います。

循環依存によるエラーとは?

循環依存とは、1つのリソースがそれ自体に依存していること、あるいは2つのリソースが互いに依存していることを指します。

まず例として、リソースAとリソースBがお互いに依存しているケースを考えます。

テンプレートで複数のリソースを定義すると、CFnはそれらのリソースを並列に作成しようとします。ここで、リソース内のDependsOn属性を利用することで、リソースの作成順序を制御できます。DependsOnを使用すると、リソースBの前にリソースAを作成することが可能です。また組み込み関数Refなどを使用して別のリソースを参照する場合にも、暗黙の依存関係が生まれます。リソースAのプロパティにリソースBへのRefがある場合、リソースBはリソースAより先に作成されます。

この依存関係が2つのリソース間でお互いに存在している場合、どちらのリソースを最初に作成すべきかCFnが判断できなくなります。これにより、循環依存エラーが発生するというわけです。

1つのリソースがそれ自体に依存しているケース

それでは、実際に循環依存エラーが出るケースと、それに対する対処法をまとめていきたと思います。

こちらの画像のように1つのリソースの設定にそれ自身をしていている場合、CFnでは循環依存エラーになります。

CFnテンプレートの例がこちらです。あるセキュリティグループのインバウンドルールで、セキュリティグループそれ自体を許可しています。

Resources:
  SecurityGroupA: # Circular Dependencies!
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: security-group-a
      GroupDescription: security-group-a
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !GetAtt SecurityGroupA.GroupId # Circular Dependencies!

CFnの仕様を考えると、インバウンドルールが適用されるSecurityGroupAよりも、インバウンドルールで許可する接続元のリソースSecurityGroupAが先に作成されるはずです。しかしSecurityGroupAが作成されようとしても、SecurityGroupAが作成されていないのでSecurityGroupAは作成されません (以下無限ループ) ...

このように、1つのリソースがそれ自体に依存している場合は、循環依存が発生して永遠にリソースが作成されません。こちらのようにCircular Dependencies for resource SecurityGroupAエラーが出て作成に失敗します。

E3004: Circular Dependencies for resource SecurityGroupA. Circular dependency with [SecurityGroupA]

こちらを解決する書き方がこちらです。AWS::EC2::SecurityGroupIngressを別途追加し、そこにルールを記述しています。

Resources:
  SecurityGroupA:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: security-group-a
      GroupDescription: security-group-a
  SecurityGroupAIngress: # インバウンドルールを外に出す
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref SecurityGroupA
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      SourceSecurityGroupId: !GetAtt SecurityGroupA.GroupId

この書き方で記述すれば、SecurityGroupAが作成された後に、SecurityGroupAに依存しているSecurityGroupAIngressが作成されます。循環依存も発生せずに、正常にリソースが作成されます。

2つのリソースが互いに依存しているケース

こちらの画像のように2つのリソースの設定にお互いを指定している場合、CFnでは循環依存としてエラーになります。

CFnテンプレートの例がこちらです。SecurityGroupAのルールにてSecurityGroupBからのアクセスを許可しています。同時に、SecurityGroupBのルールにてSecurityGroupAからのアクセスを許可しています

Resources:
  SecurityGroupA:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: security-group-a
      GroupDescription: security-group-a
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !GetAtt SecurityGroupB.GroupId # Circular Dependencies!
  SecurityGroupB:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: security-group-b
      GroupDescription: security-group-b
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !GetAtt SecurityGroupA.GroupId # Circular Dependencies!

SecurityGroupA・Bの間で双方向の暗黙的な依存関係が生まれています。そのため、どちらを先に作成するべきかCFnが判断できなくなります。結果、Circular Dependencies for resource SecurityGroupXの循環依存エラーとなります。

E3004: Circular Dependencies for resource SecurityGroupA. Circular dependency with [SecurityGroupB]
E3004: Circular Dependencies for resource SecurityGroupB. Circular dependency with [SecurityGroupA]

こちらも同様の書き方で修正できます。AWS::EC2::SecurityGroupIngressを外に出して記述することで、循環依存を断ち切っています。

Resources:
  SecurityGroupA:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: security-group-a
      GroupDescription: security-group-a
  SecurityGroupB:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: security-group-b
      GroupDescription: security-group-b

  SecurityGroupAIngress: # インバウンドルールを外に出す
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref SecurityGroupA
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      SourceSecurityGroupId: !GetAtt SecurityGroupB.GroupId
  SecurityGroupBIngress: # インバウンドルールを外に出す
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref SecurityGroupB
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      SourceSecurityGroupId: !GetAtt SecurityGroupA.GroupId

この場合、何にも依存していないSecurityGroupBが先に作成されます。その後に、SecurityGroupBに依存しているSecurityGroupAが作成されます。

循環依存エラーは、依存関係を断ち切ることで解決できる

セキュリティグループを作っているときあるあるのネタですが、意外と情報が少なかったのでまとめてみました。

AWSの公式ブログにも循環依存エラーについて紹介されていますので、参考になれば幸いです。

では!