èæ¯
ããã«ã¡ã¯ã
æ®æ®µãã䜿ãIaCããŒã«ã«AWS Cloud Development Kit (AWS CDK)ããããŸããã é·ãéæ°ã«ãªã£ãŠãããã®ã«ãã¹ãæ©èœããããŸãã
ãã£ãããªã®ã§ãå匷ãå
ŒããŠCDKã®ãã¹ãæ©èœã詊ããŠã¿ãŸããã
æ€èšŒç°å¢
ä»åã¯ä»¥äžã®ç°å¢ã§æ€èšŒããŠããŸãããŸããCDKã®ã³ãŒãã¯PythonãçšããŠå®è£ ããŸãã
> aws --version aws-cli/2.15.16 Python/3.11.6 Windows/10 exe/AMD64 prompt/off > cdk --version 2.164.0 (build 75cf2e0) > Python -V Python 3.11.8 > pytest --version pytest 6.2.5 platform win32 -- Python 3.11.8, pytest-6.2.5, py-1.11.0, pluggy-1.5.0
ããã¥ã¡ã³ããèªã
ãŸãã¯å
¬åŒããã¥ã¡ã³ããèªãã§ã¿ãŸããã
CDKã«ã¯AWSã®ãªãœãŒã¹ãå®çŸ©ããã¢ãžã¥ãŒã«ä»¥å€ã«ãããã¹ãç®çã§äœ¿çšããã¢ãžã¥ãŒã«ãassertionsãšããŠçšæãããŠããŸãããã®ã¢ãžã¥ãŒã«ã䜿çšããããšã§ãCDKã«ãã£ãŠäœæãããCloudFormationã®ãã³ãã¬ãŒããæ³å®éãã«äœæãããŠããã確èªããããšãã§ããŸãããŸããassertionsãçšããŠå®è£
ããããã¹ãã³ãŒãã¯pytestãªã©ã®äžè¬çãªãã¹ããã¬ãŒã ã¯ãŒã¯ã䜿ãããããä»ã®ãã¹ãã³ãŒããšåãæèŠã§ãã¹ããè¡ãããšãã§ããŸãã
CDKã®ãã¹ãã«ã¯2çš®é¡ã®ã¢ãããŒãããããŸãã
- ãã现ããªã¢ãµãŒã·ã§ã³ïŒCDKã«ãã£ãŠåºåãããCloudFormationãã³ãã¬ãŒãã«å¯ŸããŠãæ³å®ããããã©ã¡ãŒã¿ãšäžèŽããŠããã確èªãããã¹ãã§ãã
- ã¹ãããã·ã§ãããã¹ãïŒCDKã®ã³ãŒãã倿Žããå Žåã«ãæ°ããåºåãããCloudFormationã®ãã³ãã¬ãŒããšå€æŽåã®CloudFormationãã³ãã¬ãŒãã®éã§å·®åãçºçããªãã確èªãããã¹ãã§ããCloudFormationã®ãã³ãã¬ãŒãã«å€æŽãçºçããªãããšã確èªã§ãããããCDKã®ã³ãŒãã«å¯Ÿãããªãã¡ã¯ã¿ãªã³ã°ã容æã«ãªãããã§ãã
ããã现ããªã¢ãµãŒã·ã§ã³ãã®æ¹ã¯äœ¿ãæ©äŒãå€ããã§ããããããã¡ããæ·±å ãããŠãããŸãã
ãã¹ã察象ã³ãŒã
ä»åã®æ€èšŒã§äœ¿çšãããã¹ã察象ã®CDKã³ãŒãã¯ãVPCåšããšEC2ã€ã³ã¹ã¿ã³ã¹2ã€ãIAMããŒã«ãæ§ç¯ããåçŽãªã³ãŒãã§ããé·ããªããããæããããã§ããŸãã
ãã¹ã察象ã®ãœãŒã¹ã³ãŒã
from constructs import Construct
from aws_cdk import (
Stack,
aws_ec2 as ec2,
aws_iam as iam
)
class CdkTestStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# VPCãšãµãããããå®çŸ©
vpc = ec2.Vpc(
self,
id="test-vpc",
vpc_name="test-vpc",
availability_zones=["us-west-2a","us-west-2b"],
ip_addresses=ec2.IpAddresses.cidr("192.168.0.0/16"),
subnet_configuration=[
ec2.SubnetConfiguration(
name="test-subnet-public",
subnet_type=ec2.SubnetType.PUBLIC
),
ec2.SubnetConfiguration(
name="test-subnet-private",
subnet_type=ec2.SubnetType.PRIVATE_ISOLATED
),
]
)
# EC2ã€ã³ã¹ã¿ã³ã¹çšIAMããŒã«
iam_role = iam.Role(
self,
"test-ec2-role",
assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
managed_policies=[
iam.ManagedPolicy.from_aws_managed_policy_name(
"AmazonSSMManagedInstanceCore"
),
iam.ManagedPolicy.from_aws_managed_policy_name(
"AmazonS3FullAccess"
),
],
)
# EC2ã€ã³ã¹ã¿ã³ã¹å
±éèšå®
ec2_common_params = dict(
vpc=vpc,
role=iam_role,
instance_type=ec2.InstanceType.of(
instance_class=ec2.InstanceClass.T3,
instance_size=ec2.InstanceSize.MICRO
),
machine_image=ec2.MachineImage.latest_amazon_linux2()
)
# EC2ã€ã³ã¹ã¿ã³ã¹1å°ç®
ec2.Instance(
self,
id="test-ec2-instance01",
instance_name="test-ec2-instance01",
vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC, availability_zones=["us-west-2a"]),
**ec2_common_params
)
# EC2ã€ã³ã¹ã¿ã³ã¹2å°ç®
ec2.Instance(
self,
id="test-ec2-instance02",
instance_name="test-ec2-instance02",
vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC, availability_zones=["us-west-2b"]),
**ec2_common_params
)
ãã¹ãã³ãŒãå®è£
å®éã«CDKã®ãã¹ãã³ãŒããæžããpytestã§ãã¹ãå®è¡ããã£ãŠã¿ãŸããã
ãã³ãã¬ãŒãã®åŒã³åºã
CDKã®ãã¹ãã§ã¯ãçæãããCloudFormationã®ãã³ãã¬ãŒããå©çšããŸãããã®ãããäºåæºåãšããŠãã¹ãã³ãŒãäžã§ãã³ãã¬ãŒããæ±ãããã®ã¹ã¿ãã¯åã³ãã³ãã¬ãŒããäœæããŸãã
import aws_cdk as cdk import aws_cdk.assertions as assertions from cdk_code.cdk_stack import CdkTestStack app = cdk.App() # ãã¹ãã§ã®ç¢ºèªå¯Ÿè±¡ãšãªãã¹ã¿ãã¯ã»ãã³ãã¬ãŒãã®äœæ stack = CdkTestStack(app, "CdkTestStack", env=cdk.Environment(region="us-west-2")) template = assertions.Template.from_stack(stack)
ãªãœãŒã¹æ°ã®ç¢ºèª
æå®ãããªãœãŒã¹ã¿ã€ãã«ãããŠããã³ãã¬ãŒãã§å®çŸ©ãããŠãããªãœãŒã¹æ°ãäžèŽããã確èªãããã¹ãã§ããä»åã¯2çš®é¡ã®ãµããããã2ã€ã®AZã§äœæãããããåèš4ã€ã®ãµãããããäœæãããŸãããã£ãŠä»¥äžã®ãã¹ãã¯æåããŸãã
def test_subnet_resource_count_is():
# æå®ãããªãœãŒã¹ã¿ã€ãã®ãªãœãŒã¹æ°ãæ³å®æ°ãšäžèŽããã確èª
template.resource_count_is(
type="AWS::EC2::Subnet",
count=4
)
æå®ãããã©ã¡ãŒã¿ãå«ãŸããŠããã確èª
æå®ãããªãœãŒã¹ã¿ã€ãã«ãããŠããã³ãã¬ãŒãå
ã«ãã©ã¡ãŒã¿ãå«ãŸããŠããã確èªãããã¹ãã§ãã
ãã¹ãã³ãŒãå
ã§æå®ããCloudFormationã®Propertiesã®èšå®å
容ãå«ãŸããŠããå Žåã«ãã¹ãã¯æåãããããç¹å®ã®èšå®å€ãæ³å®éãã«èšå®ãããŠããããšã確èªã§ããŸãã
def test_vpc_has_resource_properties():
# æå®ãããªãœãŒã¹ã¿ã€ãã®ãã³ãã¬ãŒãã«ãã©ã¡ãŒã¿ãå«ãŸããŠããã確èª
template.has_resource_properties(
type="AWS::EC2::VPC",
props={
"CidrBlock": "192.168.0.0/16",
"EnableDnsHostnames": True
}
)
ãªããæ³šæç¹ãšããŠã¯ãäžèšã®ãã¹ãã¯æ¡ä»¶ã«äžèŽãããªãœãŒã¹ãïŒã€ã§ãååšãããšãã¹ãã¯æåããç¹ã§ãã
äŸãã°ãäžèšã®ãã¹ãã¯EC2ã€ã³ã¹ã¿ã³ã¹ã®AZãus-west-2bã§ããã確èªãããã¹ãã§ãã
ä»åã®ãã³ãã¬ãŒãã§ã¯us-west-2aãšus-west-2bã®AZã«1å°ãã€ã€ã³ã¹ã¿ã³ã¹ãå®çŸ©ããŠããŸããããã®ãã¹ãã¯æåããŸãã
def test_ec2_has_resource_properties():
# ïŒã€ã§ãäžèŽãããªãœãŒã¹ãããã°ãã¹ãèªäœã¯PASSãã
template.has_resource_properties(
type="AWS::EC2::Instance",
props={
"InstanceType": "t3.micro",
"AvailabilityZone": "us-west-2b"
}
)
ãã®ãããåããªãœãŒã¹ã¿ã€ãã®ãªãœãŒã¹ãè€æ°ååšããå Žåã¯ããã®ãã¹ãã ã察象ãªãœãŒã¹ããã¹ããã¿ãŒã³éãã®èšå®ã«ãªã£ãŠããä¿èšŒã¯ãããŸããããã¹ãŠã®ãªãœãŒã¹ã§å ±éããèšå®å€ã«å¯ŸããŠãã¹ãããå Žåã«ã¯æ¬¡ç¯ã§ç޹ä»ãããã¹ãã¡ãœããã䜿çšããŸãã
ãã¹ãŠã®ãªãœãŒã¹ã§åãèšå®ã«ãªã£ãŠããã確èª
ãªãœãŒã¹åã®åœåèŠåããã¹ãŠã®ãªãœãŒã¹ã§å
±éããèšå®å€ã«å¯ŸããŠç¢ºèªããéã«å©çšãããã¹ãã§ãããã¹ãã³ãŒãå
ã§æå®ããCloudFormationã®Propertiesã®èšå®å
容ãæå®ãããªãœãŒã¹ã¿ã€ãã«ããããã¹ãŠã®ãªãœãŒã¹ã«å«ãŸããŠããå Žåã«ãã¹ãã¯æåããŸãã
def test_ec2_all_resource_properties():
# ãã¹ãŠã®ãªãœãŒã¹ã§åãèšå®ã«ãªã£ãŠãããããããã€æã«æ±ºãŸãå€ã«ã€ããŠã¢ãµãŒããããã
isinstance_name_capture = assertions.Capture()
template.all_resources_properties(
type="AWS::EC2::Instance",
props={
"InstanceType": "t3.micro",
"Tags":[
{
"Key": "Name",
"Value": isinstance_name_capture
}
]
}
)
# ã€ã³ã¹ã¿ã³ã¹åã®åœ¢åŒã確èª
while (isinstance_name_capture.next()):
assert re.match(r"^test-ec2-instance[0-9]{2}", isinstance_name_capture.as_string())
ãŸããäžèšã®ãã¹ãã³ãŒãã§ã¯Capture()ã䜿çšããŠããŸããããã¯ãã¹ãã³ãŒãå
ã§ãã³ãã¬ãŒããæ¯èŒããéã«ã¯ã¯ã€ã«ãã«ãŒããšããŠæ©èœãã€ã€ãçæããããã³ãã¬ãŒãããäžèŽããéšåã®å€ãååŸã§ããæ©èœã§ããå¥éassertãçšããããšã§ããã³ãã¬ãŒãçææã«æ±ºãŸãå€ããã³ãã¬ãŒãã«å
¥ãå€ã«ã€ããŠç¢ºèªããããšã«ãå©çšã§ããŸãã
ç¹æ®ãªäžèŽ
é
åã«å«ãŸããŠããäžéšã®å€ããã¹ããããŠããäžã®äžéšãªããžã§ã¯ããªã©ç¹å®ã®å€ã®ã¿ç¢ºèªããå Žåã«å©çšã§ããæ©èœããããŸããMatch()ãå©çšããããšã§ãæè»ãªãã³ãã¬ãŒãã®ç¢ºèªãè¡ããŸãã
以äžã¯IAMããŒã«ãå®çŸ©ãããã³ãã¬ãŒãã§ãããããªã³ã·ãã«ãšãããŒãžãããªã·ãŒã«èšå®ãããå€ã®ã¿ã確èªãããã¹ãã³ãŒãã§ããMatch.object_like()ã§ã¯ãPrincipalã«å¯Ÿããå€ãæ³å®ããŠããå€ãã©ããã®ã¿ç¢ºèªããŠãããStatementå
ã«å«ãŸããä»ã®é
ç®ã«ã€ããŠã¯ãã¹ãçµæã«åœ±é¿ããŸããããŸãMatch.array_with()ã§ã¯ããããŒãžãããªã·ãŒãåæããManagedPolicyArnsã®é
åå
ã«ãAmazonSSMManagedInstanceCoreããå«ãŸããŠãããã®ã¿ç¢ºèªããŠããŸãã
def test_iamrole_match():
# matchãçšãããã©ã¡ãŒã¿ç¢ºèª
template.has_resource_properties(
type="AWS::IAM::Role",
props= {
"AssumeRolePolicyDocument":
{
"Statement": [
# æå®ããPrincipalãååšããŠãããã®ã¿ç¢ºèªïŒä»ã®é
ç®ã«ã€ããŠã¯èæ
®ããªãïŒ
assertions.Match.object_like(
{
"Principal": {
"Service": "ec2.amazonaws.com",
},
}
),
],
},
"ManagedPolicyArns": assertions.Match.array_with(
# é
åå
ã«æå®ããå€ãå«ãŸããŠãããã®ã¿ç¢ºèª
[
{
"Fn::Join": [
"",
[
"arn:",
assertions.Match.any_value(),
":iam::aws:policy/AmazonSSMManagedInstanceCore"
]
]
}
]
)
}
)
ãŸãšã
ä»åã¯CDKã®ãã¹ãã«ã€ããŠèª¿æ»ããŠã¿ãŸãããCDKã§çšæãããŠãããã¹ãçšã®assertionsã¢ãžã¥ãŒã«ã䜿çšããããšã§ãäœæãããCloudFormationã®ãã³ãã¬ãŒããæ³å®éãã®èšå®å€ã§æ§æãããŠããã確èªããããšãã§ããŸãã
pytestãªã©ã®äœ¿ãæ
£ãããã¹ãããŒã«ããã®ãŸãŸå©çšã§ããããšãããã€ãã©ã€ã³ã®ãã¹ããã§ãŒãºãªã©ã«çµã¿èŸŒãããšã§ããããã€æã«æ³å®å€ã®å€æŽãçºçããŠããªãã確èªã§ããããšãããªãœãŒã¹ã®ä¿è·æ©èœãšããŠãå©çšã§ããã¡ãªãããããäžæ¹ãCDKã®ãã¹ãã«ã¯CloudFormationã®ãã³ãã¬ãŒããçšããããããã¹ãã³ãŒããå®è£
ããã«ããã£ãŠã¯CloudFormationã®ç¥èãå¿
èŠã«ãªããåŠç¿ã³ã¹ããé«ããªããã¡ãªããã¯æããããŸããã
ãã¹ãã³ãŒãå šäœ
import aws_cdk as cdk
import aws_cdk.assertions as assertions
from cdk_code.cdk_stack import CdkTestStack
import re
app = cdk.App()
# ãã¹ãã§ã®ç¢ºèªå¯Ÿè±¡ãšãªãã¹ã¿ãã¯ã»ãã³ãã¬ãŒãã®äœæ
stack = CdkTestStack(app, "CdkTestStack", env=cdk.Environment(region="us-west-2"))
template = assertions.Template.from_stack(stack)
def test_vpc_resource_count_is():
# æå®ãããªãœãŒã¹ã¿ã€ãã®ãªãœãŒã¹æ°ãæ³å®æ°ãšäžèŽããã確èª
template.resource_count_is(
type="AWS::EC2::Subnet",
count=4
)
def test_vpc_has_resource_properties():
# æå®ãããªãœãŒã¹ã¿ã€ãã®ãã³ãã¬ãŒãã«ãã©ã¡ãŒã¿ãå«ãŸããŠããã確èª
template.has_resource_properties(
type="AWS::EC2::VPC",
props={
"CidrBlock": "192.168.0.0/16",
"EnableDnsHostnames": True
}
)
def test_ec2_has_resource_properties():
# ïŒã€ã§ãäžèŽãããªãœãŒã¹ãããã°ãã¹ãèªäœã¯PASSãã
template.has_resource_properties(
type="AWS::EC2::Instance",
props={
"InstanceType": "t3.micro",
"AvailabilityZone": "us-west-2b"
}
)
def test_ec2_all_resource_properties():
# ãã¹ãŠã®ãªãœãŒã¹ã§åãèšå®ã«ãªã£ãŠãããããããã€æã«æ±ºãŸãå€ã«ã€ããŠã¢ãµãŒããããã
isinstance_name_capture = assertions.Capture()
template.all_resources_properties(
type="AWS::EC2::Instance",
props={
"InstanceType": "t3.micro",
"Tags":[
{
"Key": "Name",
"Value": isinstance_name_capture
}
]
}
)
# isinstance_name_capture.next()
while (isinstance_name_capture.next()):
assert re.match(r"^test-ec2-instance[0-9]{2}", isinstance_name_capture.as_string())
def test_ec2_resource_properties_count_is():
# æå®ãããã©ã¡ãŒã¿ããã€ãªãœãŒã¹æ°ã確èª
template.resource_properties_count_is(
type="AWS::EC2::Instance",
props={
"InstanceType": "t3.micro",
"AvailabilityZone": "us-west-2a"
},
count=1
)
def test_iamrole_match():
# matchãçšãããã©ã¡ãŒã¿ç¢ºèª
template.has_resource_properties(
type="AWS::IAM::Role",
props= {
"AssumeRolePolicyDocument":
{
"Statement": [
# æå®ããPrincipalãååšããŠãããã®ã¿ç¢ºèªïŒä»ã®é
ç®ã«ã€ããŠã¯èæ
®ããªãïŒ
assertions.Match.object_like(
{
"Principal": {
"Service": "ec2.amazonaws.com",
},
}
),
],
},
"ManagedPolicyArns": assertions.Match.array_with(
# é
åå
ã«æå®ããå€ãå«ãŸããŠãããã®ã¿ç¢ºèª
[
{
"Fn::Join": [
"",
[
"arn:",
assertions.Match.any_value(),
":iam::aws:policy/AmazonSSMManagedInstanceCore"
]
]
}
]
)
}
)