CDKでセキュリティグループにルールを追加するときはConnectionsを使用しよう

もくじ

書くこと

こんにちは。あつしです。

AWS CDK(以下CDK)でセキュリティグループにルールを追加するときにハマってしまったことの備忘録です。

言語はPythonになります。

結論からいうと、CDKでセキュリティグループを追加するときは、SecurityGroupクラスのadd_egress_ruleメソッドやadd_ingress_ruleメソッドを使用せずに、Connectionsクラスのallow_fromメソッドなどを使おうという話です。

※こういう風にした方がいいんじゃない、や、表現の誤りなどがございましたらコメントお願いします。

構成図

本件で扱うトピックにおいて、構成図として意識するのは以下だけです。

EC2インスタンスとEFSが2049/TCPポートで通信するとき、EC2とEFSそれぞれのセキュリティグループに通信の許可設定を入れる、という話になります。

失敗例

コード(失敗例)

失敗したコードです。成功例は記事の後ろに記載しています。

  • app.py
#!/usr/bin/env python3
import os

import aws_cdk as cdk

from test2.test2_stack import Test2Stack

app = cdk.App()
Test2Stack(app, "Test2Stack",)

app.synth()
  • test2_stack.py
from aws_cdk import (
    aws_ec2 as ec2,
    Stack,
    aws_efs as efs
)
from constructs import Construct
import aws_cdk as cdk

class Test2Stack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # VPC & Subnet
        vpc = ec2.Vpc(self, "Vpc",
            vpc_name="dev-vpc",
            cidr="10.0.0.0/16",
            availability_zones=["ap-northeast-1a", "ap-northeast-1c"],
            nat_gateways=0,
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    name="dev-public",
                    cidr_mask=24,
                    subnet_type=ec2.SubnetType.PUBLIC
                ), ec2.SubnetConfiguration(
                    name="dev-private",
                    cidr_mask=24,
                    subnet_type=ec2.SubnetType.PRIVATE_ISOLATED
                )
            ]
        )

        # Create Security Group(EC2)
        sg_ec2 = ec2.SecurityGroup(self, "SgEC2",
            vpc=vpc,
            security_group_name="dev-sg-ec2",
            description="Security Group for EC2",
            allow_all_outbound=False
        )

        # Create Security Group(EFS)
        sg_efs = ec2.SecurityGroup(self, "SgEFS",
            vpc=vpc,
            security_group_name="dev-sg-efs",
            description="Security Group for EFS",
            allow_all_outbound=False
        )

        # Add Rules Security Groups(EC2)
        sg_ec2.add_ingress_rule(
            ec2.Peer.security_group_id(sg_efs.security_group_id),
            ec2.Port.tcp(2049)
        )
        sg_ec2.add_egress_rule(
            ec2.Peer.security_group_id(sg_efs.security_group_id),
            ec2.Port.tcp(2049)
        )

        # Add Rules Security Groups(EFS)
        sg_efs.add_ingress_rule(
            ec2.Peer.security_group_id(sg_ec2.security_group_id),
            ec2.Port.tcp(2049)
        )
        sg_efs.add_egress_rule(
            ec2.Peer.security_group_id(sg_ec2.security_group_id),
            ec2.Port.tcp(2049)
        )

コード解説(失敗例)

見ていただきたいのは34行目からになります。

34〜39行目でEC2のセキュリティグループを作成しています。

42〜47行目でEFSのセキュリティグループを作成しています。

50〜57行目でEC2のセキュリティグループにルールを追加しています。

60〜67行目でEFSのセキュリティグループにルールを追加しています。

コードとしては一見よさそうに見えます。cdk synthコマンドでエラーは出ませんし、cdk deployもできます。しかしエラーが発生してロールバックされます。

デプロイ(失敗例)

試しにデプロイしてみます。

% cdk deploy
✨  Synthesis time: 5.04s
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
Security Group Changes
┌───┬──────────────────┬─────┬──────────┬──────────────────┐
│   │ Group            │ Dir │ Protocol │ Peer             │
├───┼──────────────────┼─────┼──────────┼──────────────────┤
│ + │ ${SgEC2.GroupId} │ In  │ TCP 2049 │ ${SgEFS.GroupId} │
│ + │ ${SgEC2.GroupId} │ Out │ TCP 2049 │ ${SgEFS.GroupId} │
├───┼──────────────────┼─────┼──────────┼──────────────────┤
│ + │ ${SgEFS.GroupId} │ In  │ TCP 2049 │ ${SgEC2.GroupId} │
│ + │ ${SgEFS.GroupId} │ Out │ TCP 2049 │ ${SgEC2.GroupId} │
└───┴──────────────────┴─────┴──────────┴──────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)? y
Test2Stack: deploying...
[0%] start: Publishing 36484472ea785a4c01d087d5b99ec19d62835aec0683865c158c47456b86edf0:current_account-current_region
[100%] success: Published 36484472ea785a4c01d087d5b99ec19d62835aec0683865c158c47456b86edf0:current_account-current_region
Test2Stack: creating CloudFormation changeset...
 ❌  Test2Stack failed: Error [ValidationError]: Circular dependency between resources: [SgEC23ABC55F9, SgEFS6BEFFE85]
    at Request.extractError (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/protocol/query.js:50:29)
    at Request.callListeners (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/request.js:686:14)
    at Request.transition (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request. (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/request.js:38:9)
    at Request. (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/request.js:688:12)
    at Request.callListeners (/Users/atsushi/.nodebrew/node/v18.4.0/lib/node_modules/aws-sdk/lib/sequential_executor.js:116:18) {
  code: 'ValidationError',
  time: 2022-07-31T07:49:53.118Z,
  requestId: '2fcc00ec-60ad-40a7-a5cf-890b008f0b8b',
  statusCode: 400,
  retryable: false,
  retryDelay: 121.17283778729515
}
Circular dependency between resources: [SgEC23ABC55F9, SgEFS6BEFFE85]

最終行にCircular dependency between resources: [SgEC23ABC55F9, SgEFS6BEFFE85]とエラーが出ています。EC2とEFSのセキュリティグループで、依存関係のエラーが発生しています。

エラーの理由

エラーの理由は記載されている通り依存関係です。

以下の記事に詳しいことが書かれていました。

Handling circular dependency errors in AWS CloudFormation

一部引用します。

Resource A is dependent on Resource B, and Resource B is dependent on Resource A. When AWS CloudFormation assesses that this type of condition exists, you will get a circular dependency error because AWS CloudFormation is unable to clearly determine which resource should be created first.

本件の事象に照らし合わせて説明します。

CDKを実行すると、CloudFormationでリソースが作成されます。(セキュリティグループなどのリソース)

このときCloudFormationは並行してリソースを作成しようとします。

EC2とEFSのセキュリティグループを並行して作成しようとしますが、EC2はEFSのセキュリティグループに依存していて、EFSはEC2のセキュリティグループに依存しているので、エラーメッセージにもあるとおりCircular Dependencyが発生してしまいます。

成功例

コード(成功例)

ベストプラクティスに則ったコードを記載します。このコードで問題なくセキュリティグループのルールを追加することができました。

  • app.py
#!/usr/bin/env python3
import os

import aws_cdk as cdk

from test.test_stack import TestStack

app = cdk.App()
TestStack(app, "TestStack",)

app.synth()
  • test_stack.py
from aws_cdk import (
    Stack,
    aws_ec2 as ec2,
    aws_efs as efs
)
from constructs import Construct
import aws_cdk as cdk

class TestStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # VPC & Subnet
        vpc = ec2.Vpc(self, "Vpc",
            vpc_name="dev-vpc",
            cidr="10.0.0.0/16",
            availability_zones=["ap-northeast-1a", "ap-northeast-1c"],
            nat_gateways=0,
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    name="dev-public",
                    cidr_mask=24,
                    subnet_type=ec2.SubnetType.PUBLIC
                ), ec2.SubnetConfiguration(
                    name="dev-private",
                    cidr_mask=24,
                    subnet_type=ec2.SubnetType.PRIVATE_ISOLATED
                )
            ]
        )

        # Create Security Group(EC2)
        sg_ec2 = ec2.SecurityGroup(self, "SgEC2",
            vpc=vpc,
            security_group_name="dev-sg-ec2",
            description="Security Group for EC2",
            allow_all_outbound=False
        )

        # Create Security Group(EFS)
        sg_efs = ec2.SecurityGroup(self, "SgEFS",
            vpc=vpc,
            security_group_name="dev-sg-efs",
            description="Security Group for EFS",
            allow_all_outbound=False
        )

        # Create EC2 Instance
        instance_ec2 = ec2.Instance(self, "Ec2",
            vpc=vpc,
            instance_type=ec2.InstanceType.of(ec2.InstanceClass.MEMORY3, ec2.InstanceSize.LARGE),
            machine_image=ec2.MachineImage.latest_amazon_linux(),
            vpc_subnets=ec2.SubnetType.PUBLIC,
            security_group=sg_ec2
        )

        # Create EFS File System
        fs_efs = efs.FileSystem(self, "Efs",
            vpc=vpc,
            encrypted=True,
            file_system_name="dev-efs",
            security_group=sg_efs,
            vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
            removal_policy=cdk.RemovalPolicy.DESTROY
        )
        
        
        # 2022/8/9 追記。
        # 下記のようにSecurity Groupを指定してConnectionsクラスを生成することは必須ではありませんでした。
        """
              # Create Connection to EC2
              ec2_connection = ec2.Connections(
                  security_groups=[sg_ec2]
              )

              # Create Connection to EFS
              efs_connection = ec2.Connections(
                  security_groups=[sg_efs]
              )
              """

        """
              # Add Connections to EC2
              # instance_ec2.connections.allow_from(efs_connection, ec2.Port.tcp(2049))
              """
        # 2022/8/9修正。下記のようにSecurity Groupを直で指定することができました。
        instance_ec2.connections.allow_from(sg_efs, ec2.Port.tcp(2049))

        """
              # Add Connections to EFS
              # fs_efs.connections.allow_from(ec2_connection, ec2.Port.tcp(2049))
              """
        # 2022/8/9修正。下記のようにSecurity Groupを直で指定することができました。
        fs_efs.connections.allow_from(sg_ec2, ec2.Port.tcp(2049))

コード解説(成功例)

34〜39行目でEC2のセキュリティグループを作成しています。

42〜47行目でEFSのセキュリティグループを作成しています。

ここまでは失敗例と同じです。

50〜56行目でEC2インスタンスを作成しています。

59〜66行目でEFSファイルシステムを作成しています。

69〜71行目でEC2インスタンスのコネクションを作成しています。(このConnectionsクラスにEC2セキュリティグループを所属させてEC2の接続を管理します)

74〜76行目でEFSのコネクションを作成しています。(このConnectionsクラスにEFSセキュリティグループを所属させて、EFSへの接続を管理します)

79行目でEC2インスタンスへの接続許可設定を入れています。(EC2コネクションにEFSコネクションからの2049/TCPからの接続を許可しています)

82行目でEFSへの接続許可設定を入れています。(EFSコネクションにEC2コネクションからの2049/TCPからの接続を許可しています)

88行目でEFSからEC2への接続許可設定を入れています。(Connectionsallow_fromメソッドを呼び出して引数にEFSのセキュリティグループを指定することにより、EFSからEC2への接続許可設定を入れています)

95行目でEC2からEFSへの接続許可設定を入れています。(Connectionsallow_fromメソッドを呼び出して引数にEC2のセキュリティグループを指定することにより、EC2からEFSへの接続許可設定を入れています)
↑EFSからEC2に接続を試行することはありませんが、ここでは検証のため入れています。

デプロイ(成功例)

デプロイしてみます。

% cdk deploy

✨  Synthesis time: 3.6s

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬─────────────────────────┬────────┬────────────────┬───────────────────────────────┬───────────┐
│   │ Resource                │ Effect │ Action         │ Principal                     │ Condition │
├───┼─────────────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤
│ + │ ${Ec2/InstanceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:ec2.${AWS::URLSuffix} │           │
└───┴─────────────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘
Security Group Changes
┌───┬──────────────────┬─────┬─────────────┬────────────────────┐
│   │ Group            │ Dir │ Protocol    │ Peer               │
├───┼──────────────────┼─────┼─────────────┼────────────────────┤
│ + │ ${SgEC2.GroupId} │ In  │ TCP 2049    │ ${SgEFS.GroupId}   │
│ + │ ${SgEC2.GroupId} │ Out │ ICMP 252-86 │ 255.255.255.255/32 │
├───┼──────────────────┼─────┼─────────────┼────────────────────┤
│ + │ ${SgEFS.GroupId} │ Out │ TCP 2049    │ ${SgEC2.GroupId}   │
└───┴──────────────────┴─────┴─────────────┴────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
TestStack: deploying...
[0%] start: Publishing 35727a88748948d352a00a17595920d684a724c5df8b0d81ad16c60303135a94:current_account-current_region
[100%] success: Published 35727a88748948d352a00a17595920d684a724c5df8b0d81ad16c60303135a94:current_account-current_region
TestStack: creating CloudFormation changeset...

 ✅  TestStack

✨  Deployment time: 197.75s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:738452829225:stack/TestStack/89715be0-179f-11ed-a4b9-06e698612593

✨  Total time: 201.35s

セキュリティグループのところを見ていただきたいのですが、失敗例と同じルールとなっています。

しかし今回は成功しています。

理由はConnectionsクラスでセキュリティグループのルール追加を行っているためです。(失敗例ではSecurityGroupクラスでルール追加を行っていました)

公式ドキュメントの見解

公式ドキュメントに、SecurityGroupクラスではなくConnectionsクラスを使用するほうが良い、ということが書いてありました。

SecurityGroup

引用します。

If you are defining new infrastructure in CDK, there is a good chance you won’t have to interact with this class at all. Like IAM Roles, Security Groups need to exist to control access between AWS resources, but CDK will automatically generate and populate them with least-privilege permissions for you so you can concentrate on your business logic.

All Constructs that require Security Groups will create one for you if you don’t specify one at construction. After construction, you can selectively allow connections to and between constructs via–for example– the instance.connections object. Think of it as “allowing connections to your instance”, rather than “adding ingress rules a security group”. See the Allowing Connections section in the library documentation for examples.

Direct manipulation of the Security Group through addIngressRule and addEgressRule is possible, but mutation through the .connections object is recommended. If you peer two constructs with security groups this way, appropriate rules will be created in both.

重要な箇所だけピックアップして訳してみます。

  1. CDK で新しいインフラストラクチャを定義している場合は、このクラスをまったく操作する必要がない可能性が高くなります。
  2. たとえば instance.connections オブジェクトを介して、構築物への接続および構築物間の接続を選択的に許可できます。
  3. 「イングレス ルールをセキュリティ グループに追加する」というよりは、「インスタンスへの接続を許可する」と考えてください。
  4. addIngressRule および addEgressRule によるセキュリティ グループの直接操作は可能ですが、.connections オブジェクトによる変更が推奨されます。

❷が一番重要だと思います。

要するに、

「セキュリティグループにルールを追加する」、つまり、SecurityGroup.add_ingress_rule()などでルールを追加するというやり方ではなく、

「インスタンスへの接続を許可する」、つまり、Instance.connections.allow_from()などでインスタンスに対して接続を許可するというやり方がベストプラクティスのようです。

最後に

AWS CDK楽しい!

誤記や表現の誤りがありましたらご指摘ください!

IaC最高!!

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!

コメント

コメントする

コメントは日本語で入力してください。(スパム対策)

CAPTCHA

もくじ
閉じる