書くこと
こんにちは。あつしです。
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への接続許可設定を入れています。(Connections
のallow_from
メソッドを呼び出して引数にEFSのセキュリティグループを指定することにより、EFSからEC2への接続許可設定を入れています)
95行目でEC2からEFSへの接続許可設定を入れています。(Connections
のallow_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
クラスを使用するほうが良い、ということが書いてありました。
引用します。
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
andaddEgressRule
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.
重要な箇所だけピックアップして訳してみます。
- CDK で新しいインフラストラクチャを定義している場合は、このクラスをまったく操作する必要がない可能性が高くなります。
- たとえば instance.connections オブジェクトを介して、構築物への接続および構築物間の接続を選択的に許可できます。
- 「イングレス ルールをセキュリティ グループに追加する」というよりは、「インスタンスへの接続を許可する」と考えてください。
- addIngressRule および addEgressRule によるセキュリティ グループの直接操作は可能ですが、.connections オブジェクトによる変更が推奨されます。
❷が一番重要だと思います。
要するに、
「セキュリティグループにルールを追加する」、つまり、SecurityGroup.add_ingress_rule()
などでルールを追加するというやり方ではなく、
「インスタンスへの接続を許可する」、つまり、Instance.connections.allow_from()
などでインスタンスに対して接続を許可するというやり方がベストプラクティスのようです。
最後に
AWS CDK楽しい!
誤記や表現の誤りがありましたらご指摘ください!
IaC最高!!
コメント