Skip to content

Commit 9ae57d3

Browse files
committed
Merge branch 'mr/trespeuch/add-example' into 'master'
Add an example using CFNProjectMain See merge request it/e3-aws!39
2 parents 3e3246c + a46bf0d commit 9ae57d3

File tree

3 files changed

+365
-0
lines changed

3 files changed

+365
-0
lines changed

examples/deploy_simple_stack.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python
2+
"""Provide a Command Line Interface to manage MySimpleStack stack.
3+
4+
The stack consists of a VPC with private and public subnets in eu-west-1a AZ
5+
and a NAT Gateway to route traffic from the private subnet to the Internet.
6+
It also deploys an instance in the private subnet with its IAM profile, and
7+
a security group.
8+
9+
As it relies on CFNProjectMain it requires a deployment and a
10+
CloudFormation roles named respectively cfn-user/CFNAllowDeployOfMySimpleStack
11+
and cfn-service/CFNServiceRoleForMySimpleStack.
12+
13+
The 'CFNAllow' role must be assumable by the user deploying the stack.
14+
The 'CFNServiceRole' must trust the CloudFormation service.
15+
16+
For more details on how to manage the stack run:
17+
./deploy_simple_stack.py --help
18+
"""
19+
from __future__ import annotations
20+
from functools import cached_property
21+
import sys
22+
from typing import TYPE_CHECKING
23+
24+
from e3.aws.troposphere import CFNProjectMain, Construct, name_to_id, Stack
25+
from e3.aws.troposphere.ec2 import VPCv2
26+
from e3.aws.troposphere.iam.role import Role
27+
from e3.aws.troposphere.iam.policy_statement import Trust
28+
from troposphere import ec2, iam, Ref, GetAtt, Tags
29+
30+
if TYPE_CHECKING:
31+
from troposphhere import AWSObject
32+
33+
STACK_NAME = "MySimpleStack"
34+
ACCOUNT_ID = "012345678910"
35+
REGION = "eu-west-1"
36+
AZ = "eu-west-1a"
37+
IAM_PATH = "/my-simple-stack/"
38+
INSTANCE_AMI = "ami-1234"
39+
40+
# S3 Bucket where templates are pushed for deployment
41+
# The "CFNAllowDeployOf" role must be allowed to push files to:
42+
# my-cfn-bucket/my-simple-stack/*
43+
# The "CFNServiceRole" must be allowed to read files from:
44+
# my-cfn-bucket/my-simple-stack/*
45+
CFN_BUCKET = "my-cfn-bucket"
46+
47+
48+
class SimpleInstance(Construct):
49+
"""Provide a construct deploying a simple instance."""
50+
51+
def __init__(self, name: str, vpc: VPCv2, ami: str, instance_type: str) -> None:
52+
"""Initialize a SimpleInstance instance.
53+
54+
:param name: name of the instance
55+
:param vpc: a vpc to host the instance
56+
:param ami: AMI for the instance
57+
:param instance_type: the EC2 instance type
58+
"""
59+
self.name = name
60+
self.vpc = vpc
61+
self.ami = ami
62+
self.instance_type = instance_type
63+
64+
@cached_property
65+
def role(self) -> Role:
66+
"""Return a role for the simple instance."""
67+
return Role(
68+
name=f"{self.name}InstanceRole",
69+
description="Simple instance instance role",
70+
path=IAM_PATH,
71+
trust=Trust(services=["ec2"]),
72+
managed_policy_arns=[
73+
# Access to CloudWatch and SSM
74+
"arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy",
75+
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
76+
"arn:aws:iam::aws:policy/AmazonSSMPatchAssociation",
77+
],
78+
)
79+
80+
@cached_property
81+
def profile(self) -> iam.InstanceProfile:
82+
"""Return an instance profile for the simple instance."""
83+
profile_name = f"{self.name}InstanceProfile"
84+
return iam.InstanceProfile(
85+
title=name_to_id(profile_name),
86+
InstanceProfileName=profile_name,
87+
Path=IAM_PATH,
88+
Roles=[self.role.name],
89+
DependsOn=self.role.name,
90+
)
91+
92+
@cached_property
93+
def security_group(self) -> ec2.SecurityGroup:
94+
"""Return instance security group.
95+
96+
Allow no inbound and all outbound.
97+
"""
98+
group_name = f"{self.name}SG"
99+
return ec2.SecurityGroup(
100+
name_to_id(group_name),
101+
GroupDescription=f"Security group for {self.name} instance",
102+
GroupName=group_name,
103+
SecurityGroupEgress=[
104+
ec2.SecurityGroupRule(CidrIp="0.0.0.0/0", IpProtocol="-1"),
105+
ec2.SecurityGroupRule(CidrIpv6="::/0", IpProtocol="-1"),
106+
],
107+
SecurityGroupIngress=[],
108+
VpcId=Ref(self.vpc.vpc),
109+
)
110+
111+
@cached_property
112+
def instance(self) -> ec2.Instance:
113+
"""Return a simple instance."""
114+
return ec2.Instance(
115+
title=name_to_id(self.name),
116+
ImageId=self.ami,
117+
IamInstanceProfile=Ref(self.profile),
118+
InstanceType=self.instance_type,
119+
SubnetId=Ref(self.vpc.private_subnets[AZ]),
120+
# Use default security group that comes with the VPC
121+
SecurityGroupIds=[GetAtt(self.security_group, "GroupId")],
122+
PropagateTagsToVolumeOnCreation=True,
123+
BlockDeviceMappings=[
124+
ec2.BlockDeviceMapping(
125+
Ebs=ec2.EBSBlockDevice(VolumeType="gp3", VolumeSize="20"),
126+
DeviceName="/dev/sda1",
127+
)
128+
],
129+
Tags=Tags({"Name": self.name}),
130+
)
131+
132+
def resources(self, stack: Stack) -> list[AWSObject | Construct]:
133+
"""Return resources for this construct."""
134+
return [
135+
self.role,
136+
self.profile,
137+
self.security_group,
138+
self.instance,
139+
]
140+
141+
142+
class MySimpleStackMain(CFNProjectMain):
143+
"""Provide CLI to manage MySimpleStack stack."""
144+
145+
def create_stack(self) -> list[Stack]:
146+
"""Create MySimpleStack stack."""
147+
vpc = VPCv2(
148+
name_prefix=self.stack.name,
149+
cidr_block="10.50.0.0/16",
150+
availability_zones=[AZ],
151+
)
152+
self.add(vpc)
153+
self.add(
154+
SimpleInstance(
155+
name="MySimpleInstance",
156+
vpc=vpc,
157+
ami="MYAMi-1234",
158+
instance_type="t4g.small",
159+
)
160+
)
161+
return self.stack
162+
163+
164+
def main(args: list[str] | None = None) -> None:
165+
"""Entry point.
166+
167+
:param args: the list of positional parameters. If None then
168+
``sys.argv[1:]`` is used
169+
"""
170+
project = MySimpleStackMain(
171+
name=STACK_NAME,
172+
account_id=ACCOUNT_ID,
173+
stack_description="Stack deploying an instance",
174+
s3_bucket=f"cfn-gitlab-adacore-{REGION}",
175+
regions=[REGION],
176+
)
177+
sys.exit(project.execute(args))
178+
179+
180+
if __name__ == "__main__":
181+
main()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Test the deploy_simple_stack example."""
2+
3+
from __future__ import annotations
4+
from pathlib import Path
5+
import pytest
6+
import sys
7+
from typing import TYPE_CHECKING
8+
import yaml
9+
10+
TEST_DIR = Path(__file__).parent
11+
EXAMPLES_DIR = TEST_DIR.parent.parent.parent / "examples"
12+
sys.path.insert(0, str(EXAMPLES_DIR))
13+
from deploy_simple_stack import main # type: ignore # noqa: E402
14+
15+
if TYPE_CHECKING:
16+
from pytest import CaptureFixture
17+
18+
19+
def test_deploy_simple_stack_example(capsys: CaptureFixture) -> None:
20+
"""Ensure the stack generated by deploy_simple_stack.py is the one expected."""
21+
with pytest.raises(SystemExit) as exit_info:
22+
main(["show"])
23+
24+
captured = capsys.readouterr()
25+
expected_stack = yaml.safe_load(Path(TEST_DIR / "simple_stack.yml").read_text())
26+
assert yaml.safe_load(captured.out) == expected_stack
27+
assert exit_info.value.code == 0
28+
assert captured.err == ""
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Description: Stack deploying an instance
3+
Resources:
4+
MySimpleInstance:
5+
Properties:
6+
BlockDeviceMappings:
7+
- DeviceName: /dev/sda1
8+
Ebs:
9+
VolumeSize: '20'
10+
VolumeType: gp3
11+
IamInstanceProfile:
12+
Ref: MySimpleInstanceInstanceProfile
13+
ImageId: MYAMi-1234
14+
InstanceType: t4g.small
15+
PropagateTagsToVolumeOnCreation: true
16+
SecurityGroupIds:
17+
- Fn::GetAtt:
18+
- MySimpleInstanceSG
19+
- GroupId
20+
SubnetId:
21+
Ref: MySimpleStackPrivateSubnetA
22+
Tags:
23+
- Key: Name
24+
Value: MySimpleInstance
25+
Type: AWS::EC2::Instance
26+
MySimpleInstanceInstanceProfile:
27+
DependsOn: MySimpleInstanceInstanceRole
28+
Properties:
29+
InstanceProfileName: MySimpleInstanceInstanceProfile
30+
Path: /my-simple-stack/
31+
Roles:
32+
- MySimpleInstanceInstanceRole
33+
Type: AWS::IAM::InstanceProfile
34+
MySimpleInstanceInstanceRole:
35+
Properties:
36+
AssumeRolePolicyDocument:
37+
Statement:
38+
- Action: sts:AssumeRole
39+
Effect: Allow
40+
Principal:
41+
Service:
42+
- ec2.amazonaws.com
43+
Version: '2012-10-17'
44+
Description: Simple instance instance role
45+
ManagedPolicyArns:
46+
- arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
47+
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
48+
- arn:aws:iam::aws:policy/AmazonSSMPatchAssociation
49+
Path: /my-simple-stack/
50+
RoleName: MySimpleInstanceInstanceRole
51+
Tags:
52+
- Key: Name
53+
Value: MySimpleInstanceInstanceRole
54+
Type: AWS::IAM::Role
55+
MySimpleInstanceSG:
56+
Properties:
57+
GroupDescription: Security group for MySimpleInstance instance
58+
GroupName: MySimpleInstanceSG
59+
SecurityGroupEgress:
60+
- CidrIp: 0.0.0.0/0
61+
IpProtocol: '-1'
62+
- CidrIpv6: ::/0
63+
IpProtocol: '-1'
64+
SecurityGroupIngress: []
65+
VpcId:
66+
Ref: MySimpleStackVPC
67+
Type: AWS::EC2::SecurityGroup
68+
MySimpleStackEIPA:
69+
Type: AWS::EC2::EIP
70+
MySimpleStackInternetGW:
71+
Type: AWS::EC2::InternetGateway
72+
MySimpleStackInternetGWAttachment:
73+
Properties:
74+
InternetGatewayId:
75+
Ref: MySimpleStackInternetGW
76+
VpcId:
77+
Ref: MySimpleStackVPC
78+
Type: AWS::EC2::VPCGatewayAttachment
79+
MySimpleStackNatGatewayA:
80+
Properties:
81+
AllocationId:
82+
Fn::GetAtt:
83+
- MySimpleStackEIPA
84+
- AllocationId
85+
SubnetId:
86+
Ref: MySimpleStackPublicSubnetA
87+
Type: AWS::EC2::NatGateway
88+
MySimpleStackPrivateRouteAToInternet:
89+
Properties:
90+
DestinationCidrBlock: 0.0.0.0/0
91+
NatGatewayId:
92+
Ref: MySimpleStackNatGatewayA
93+
RouteTableId:
94+
Ref: MySimpleStackPrivateRouteTableA
95+
Type: AWS::EC2::Route
96+
MySimpleStackPrivateRouteTableA:
97+
Properties:
98+
VpcId:
99+
Ref: MySimpleStackVPC
100+
Type: AWS::EC2::RouteTable
101+
MySimpleStackPrivateRouteTableAssocA:
102+
Properties:
103+
RouteTableId:
104+
Ref: MySimpleStackPrivateRouteTableA
105+
SubnetId:
106+
Ref: MySimpleStackPrivateSubnetA
107+
Type: AWS::EC2::SubnetRouteTableAssociation
108+
MySimpleStackPrivateSubnetA:
109+
Properties:
110+
AvailabilityZone: eu-west-1a
111+
CidrBlock: 10.50.0.0/19
112+
Tags:
113+
- Key: Name
114+
Value: MySimpleStackPrivateSubnetA
115+
VpcId:
116+
Ref: MySimpleStackVPC
117+
Type: AWS::EC2::Subnet
118+
MySimpleStackPublicRouteTable:
119+
Properties:
120+
VpcId:
121+
Ref: MySimpleStackVPC
122+
Type: AWS::EC2::RouteTable
123+
MySimpleStackPublicRouteTableAssocA:
124+
Properties:
125+
RouteTableId:
126+
Ref: MySimpleStackPublicRouteTable
127+
SubnetId:
128+
Ref: MySimpleStackPublicSubnetA
129+
Type: AWS::EC2::SubnetRouteTableAssociation
130+
MySimpleStackPublicRouteToInternet:
131+
Properties:
132+
DestinationCidrBlock: 0.0.0.0/0
133+
GatewayId:
134+
Ref: MySimpleStackInternetGW
135+
RouteTableId:
136+
Ref: MySimpleStackPublicRouteTable
137+
Type: AWS::EC2::Route
138+
MySimpleStackPublicSubnetA:
139+
Properties:
140+
AvailabilityZone: eu-west-1a
141+
CidrBlock: 10.50.32.0/19
142+
Tags:
143+
- Key: Name
144+
Value: MySimpleStackPublicSubnetA
145+
VpcId:
146+
Ref: MySimpleStackVPC
147+
Type: AWS::EC2::Subnet
148+
MySimpleStackVPC:
149+
Properties:
150+
CidrBlock: 10.50.0.0/16
151+
EnableDnsHostnames: true
152+
EnableDnsSupport: true
153+
Tags:
154+
- Key: Name
155+
Value: MySimpleStackVPC
156+
Type: AWS::EC2::VPC

0 commit comments

Comments
 (0)