Files
e3-aws/examples/deploy_simple_stack.py

182 lines
5.9 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python
"""Provide a Command Line Interface to manage MySimpleStack stack.
The stack consists of a VPC with private and public subnets in eu-west-1a AZ
and a NAT Gateway to route traffic from the private subnet to the Internet.
It also deploys an instance in the private subnet with its IAM profile, and
a security group.
As it relies on CFNProjectMain it requires a deployment and a
CloudFormation roles named respectively cfn-user/CFNAllowDeployOfMySimpleStack
and cfn-service/CFNServiceRoleForMySimpleStack.
The 'CFNAllow' role must be assumable by the user deploying the stack.
The 'CFNServiceRole' must trust the CloudFormation service.
For more details on how to manage the stack run:
./deploy_simple_stack.py --help
"""
from __future__ import annotations
from functools import cached_property
import sys
from typing import TYPE_CHECKING
from e3.aws.troposphere import CFNProjectMain, Construct, name_to_id, Stack
from e3.aws.troposphere.ec2 import VPCv2
from e3.aws.troposphere.iam.role import Role
from e3.aws.troposphere.iam.policy_statement import Trust
from troposphere import ec2, iam, Ref, GetAtt, Tags
if TYPE_CHECKING:
from troposphhere import AWSObject
STACK_NAME = "MySimpleStack"
ACCOUNT_ID = "012345678910"
REGION = "eu-west-1"
AZ = "eu-west-1a"
IAM_PATH = "/my-simple-stack/"
INSTANCE_AMI = "ami-1234"
# S3 Bucket where templates are pushed for deployment
# The "CFNAllowDeployOf" role must be allowed to push files to:
# my-cfn-bucket/my-simple-stack/*
# The "CFNServiceRole" must be allowed to read files from:
# my-cfn-bucket/my-simple-stack/*
CFN_BUCKET = "my-cfn-bucket"
class SimpleInstance(Construct):
"""Provide a construct deploying a simple instance."""
def __init__(self, name: str, vpc: VPCv2, ami: str, instance_type: str) -> None:
"""Initialize a SimpleInstance instance.
:param name: name of the instance
:param vpc: a vpc to host the instance
:param ami: AMI for the instance
:param instance_type: the EC2 instance type
"""
self.name = name
self.vpc = vpc
self.ami = ami
self.instance_type = instance_type
@cached_property
def role(self) -> Role:
"""Return a role for the simple instance."""
return Role(
name=f"{self.name}InstanceRole",
description="Simple instance instance role",
path=IAM_PATH,
trust=Trust(services=["ec2"]),
managed_policy_arns=[
# Access to CloudWatch and SSM
"arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy",
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
"arn:aws:iam::aws:policy/AmazonSSMPatchAssociation",
],
)
@cached_property
def profile(self) -> iam.InstanceProfile:
"""Return an instance profile for the simple instance."""
profile_name = f"{self.name}InstanceProfile"
return iam.InstanceProfile(
title=name_to_id(profile_name),
InstanceProfileName=profile_name,
Path=IAM_PATH,
Roles=[self.role.name],
DependsOn=self.role.name,
)
@cached_property
def security_group(self) -> ec2.SecurityGroup:
"""Return instance security group.
Allow no inbound and all outbound.
"""
group_name = f"{self.name}SG"
return ec2.SecurityGroup(
name_to_id(group_name),
GroupDescription=f"Security group for {self.name} instance",
GroupName=group_name,
SecurityGroupEgress=[
ec2.SecurityGroupRule(CidrIp="0.0.0.0/0", IpProtocol="-1"),
ec2.SecurityGroupRule(CidrIpv6="::/0", IpProtocol="-1"),
],
SecurityGroupIngress=[],
VpcId=Ref(self.vpc.vpc),
)
@cached_property
def instance(self) -> ec2.Instance:
"""Return a simple instance."""
return ec2.Instance(
title=name_to_id(self.name),
ImageId=self.ami,
IamInstanceProfile=Ref(self.profile),
InstanceType=self.instance_type,
SubnetId=Ref(self.vpc.private_subnets[AZ]),
# Use default security group that comes with the VPC
SecurityGroupIds=[GetAtt(self.security_group, "GroupId")],
PropagateTagsToVolumeOnCreation=True,
BlockDeviceMappings=[
ec2.BlockDeviceMapping(
Ebs=ec2.EBSBlockDevice(VolumeType="gp3", VolumeSize="20"),
DeviceName="/dev/sda1",
)
],
Tags=Tags({"Name": self.name}),
)
def resources(self, stack: Stack) -> list[AWSObject | Construct]:
"""Return resources for this construct."""
return [
self.role,
self.profile,
self.security_group,
self.instance,
]
class MySimpleStackMain(CFNProjectMain):
"""Provide CLI to manage MySimpleStack stack."""
def create_stack(self) -> list[Stack]:
"""Create MySimpleStack stack."""
vpc = VPCv2(
name_prefix=self.stack.name,
cidr_block="10.50.0.0/16",
availability_zones=[AZ],
)
self.add(vpc)
self.add(
SimpleInstance(
name="MySimpleInstance",
vpc=vpc,
ami="MYAMi-1234",
instance_type="t4g.small",
)
)
return self.stack
def main(args: list[str] | None = None) -> None:
"""Entry point.
:param args: the list of positional parameters. If None then
``sys.argv[1:]`` is used
"""
project = MySimpleStackMain(
name=STACK_NAME,
account_id=ACCOUNT_ID,
stack_description="Stack deploying an instance",
s3_bucket=f"cfn-gitlab-adacore-{REGION}",
regions=[REGION],
)
sys.exit(project.execute(args))
if __name__ == "__main__":
main()