├── .gitignore ├── README.md └── cdk ├── app.py ├── cdk.json ├── requirements.txt └── stresstool_user_data.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *env 2 | cdk/cdk.out 3 | cdk.context.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Base VPC module for the ECS workshop 2 | 3 | This repository is part of the ECS Workshop. 4 | 5 | This repository will deploy a VPC to prepare your environment for the [ECS Workshop](https://ecsworkshop.com/). 6 | 7 | Instructions on how to use the code in this repository can be found here: [https://ecsworkshop.com/microservices/](https://ecsworkshop.com/microservices/) 8 | 9 | -------------------------------------------------------------------------------- /cdk/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from constructs import Construct 4 | import os 5 | import aws_cdk as cdk 6 | from aws_cdk import ( 7 | # Duration, 8 | App, CfnOutput, Stack, Environment, Fn, 9 | aws_ec2 as ec2, 10 | aws_ecs as ecs, 11 | aws_iam as iam, 12 | aws_ssm as ssm, 13 | aws_autoscaling as autoscaling, 14 | aws_appmesh as appmesh, 15 | aws_ecs_patterns as ecs_patterns, 16 | aws_logs as logs) 17 | 18 | 19 | class BaseVPCStack(Stack): 20 | 21 | def __init__(self, scope: Construct, construct_id: str, **kwargs): 22 | super().__init__(scope, construct_id, **kwargs) 23 | 24 | # This resource alone will create a private/public subnet in each AZ as well as nat/internet gateway(s) 25 | self.vpc = ec2.Vpc( 26 | self, "BaseVPC", 27 | ip_addresses = ec2.IpAddresses.cidr('10.0.0.0/24'), 28 | ) 29 | 30 | # Creating ECS Cluster in the VPC created above 31 | self.ecs_cluster = ecs.Cluster( 32 | self, "ECSCluster", 33 | vpc=self.vpc, 34 | cluster_name="container-demo", 35 | container_insights=True 36 | ) 37 | 38 | # Adding service discovery namespace to cluster 39 | self.ecs_cluster.add_default_cloud_map_namespace( 40 | name="service.local", 41 | ) 42 | 43 | ###### CAPACITY PROVIDERS SECTION ##### 44 | # Adding EC2 capacity to the ECS Cluster 45 | # asg = self.ecs_cluster.add_capacity( 46 | # "ECSEC2Capacity", 47 | # instance_type=ec2.InstanceType(instance_type_identifier='t3.small'), 48 | # min_capacity=0, 49 | # max_capacity=10 50 | #) 51 | 52 | # CfnOutput(self, "EC2AutoScalingGroupName", value=asg.auto_scaling_group_name, export_name="EC2ASGName") 53 | ##### END CAPACITY PROVIDER SECTION ##### 54 | 55 | # ##### EC2 SPOT CAPACITY PROVIDER SECTION ###### 56 | 57 | # # As of today, AWS CDK doesn't support Launch Templates on the AutoScaling construct, hence it 58 | # # doesn't support Mixed Instances Policy to combine instance types on Auto Scaling and adhere to Spot best practices 59 | # # In the meantime, CfnLaunchTemplate and CfnAutoScalingGroup resources are used to configure Spot capacity 60 | # # https://github.com/aws/aws-cdk/issues/6734 61 | 62 | # self.ecs_spot_instance_role = iam.Role( 63 | # self, "ECSSpotECSInstanceRole", 64 | # assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"), 65 | # managed_policies=[ 66 | # iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AmazonEC2ContainerServiceforEC2Role"), 67 | # iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AmazonEC2RoleforSSM") 68 | # ] 69 | # ) 70 | 71 | # self.ecs_spot_instance_profile = iam.CfnInstanceProfile( 72 | # self, "ECSSpotInstanceProfile", 73 | # roles = [ 74 | # self.ecs_spot_instance_role.role_name 75 | # ] 76 | # ) 77 | 78 | # # This creates a Launch Template for the Auto Scaling group 79 | # self.lt = ec2.CfnLaunchTemplate( 80 | # self, "ECSEC2SpotCapacityLaunchTemplate", 81 | # launch_template_data={ 82 | # "instanceType": "m5.large", 83 | # "imageId": ssm.StringParameter.value_for_string_parameter( 84 | # self, 85 | # "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id"), 86 | # "securityGroupIds": [ x.security_group_id for x in self.ecs_cluster.connections.security_groups ], 87 | # "iamInstanceProfile": {"arn": self.ecs_spot_instance_profile.attr_arn}, 88 | 89 | # # Here we configure the ECS agent to drain Spot Instances upon catching a Spot Interruption notice from instance metadata 90 | # "userData": Fn.base64( 91 | # Fn.sub( 92 | # "#!/usr/bin/bash\n" 93 | # "echo ECS_CLUSTER=${cluster_name} >> /etc/ecs/ecs.config\n" 94 | # "sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\n" 95 | # "sudo service iptables save\n" 96 | # "echo ECS_ENABLE_SPOT_INSTANCE_DRAINING=true >> /etc/ecs/ecs.config\n" 97 | # "echo ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config\n" 98 | # "cat /etc/ecs/ecs.config", 99 | # variables = { 100 | # "cluster_name":self.ecs_cluster.cluster_name 101 | # } 102 | # ) 103 | # ) 104 | # }, 105 | # launch_template_name="ECSEC2SpotCapacityLaunchTemplate") 106 | 107 | # self.ecs_ec2_spot_mig_asg = autoscaling.CfnAutoScalingGroup( 108 | # self, "ECSEC2SpotCapacity", 109 | # min_size = "0", 110 | # max_size = "10", 111 | # vpc_zone_identifier = [ x.subnet_id for x in self.vpc.private_subnets ], 112 | # mixed_instances_policy = { 113 | # "instancesDistribution": { 114 | # "onDemandAllocationStrategy": "prioritized", 115 | # "onDemandBaseCapacity": 0, 116 | # "onDemandPercentageAboveBaseCapacity": 0, 117 | # "spotAllocationStrategy": "capacity-optimized" 118 | # }, 119 | # "launchTemplate": { 120 | # "launchTemplateSpecification": { 121 | # "launchTemplateId": self.lt.ref, 122 | # "version": self.lt.attr_default_version_number 123 | # }, 124 | # "overrides": [ 125 | # {"instanceType": "m5.large"}, 126 | # {"instanceType": "m5d.large"}, 127 | # {"instanceType": "m5a.large"}, 128 | # {"instanceType": "m5ad.large"}, 129 | # {"instanceType": "m5n.large"}, 130 | # {"instanceType": "m5dn.large"}, 131 | # {"instanceType": "m3.large"}, 132 | # {"instanceType": "m4.large"}, 133 | # {"instanceType": "t3.large"}, 134 | # {"instanceType": "t2.large"} 135 | # ] 136 | # } 137 | # } 138 | # ) 139 | 140 | # Tags.of(self.ecs_ec2_spot_mig_asg).add("Name", self.ecs_ec2_spot_mig_asg.node.path) 141 | # CfnOutput(self, "EC2SpotAutoScalingGroupName", value=self.ecs_ec2_spot_mig_asg.ref, export_name="EC2SpotASGName") 142 | 143 | # #### END EC2 SPOT CAPACITY PROVIDER SECTION ##### 144 | 145 | # Namespace details as CFN output 146 | self.namespace_outputs = { 147 | 'ARN': self.ecs_cluster.default_cloud_map_namespace.private_dns_namespace_arn, 148 | 'NAME': self.ecs_cluster.default_cloud_map_namespace.private_dns_namespace_name, 149 | 'ID': self.ecs_cluster.default_cloud_map_namespace.private_dns_namespace_id, 150 | } 151 | 152 | # Cluster Attributes 153 | self.cluster_outputs = { 154 | 'NAME': self.ecs_cluster.cluster_name, 155 | 'SECGRPS': str(self.ecs_cluster.connections.security_groups) 156 | } 157 | 158 | # When enabling EC2, we need the security groups "registered" to the cluster for imports in other service stacks 159 | if self.ecs_cluster.connections.security_groups: 160 | self.cluster_outputs['SECGRPS'] = str([x.security_group_id for x in self.ecs_cluster.connections.security_groups][0]) 161 | 162 | # Frontend service to backend services on 3000 163 | self.services_3000_sec_group = ec2.SecurityGroup( 164 | self, "FrontendToBackendSecurityGroup", 165 | allow_all_outbound=True, 166 | description="Security group for frontend service to talk to backend services", 167 | vpc=self.vpc 168 | ) 169 | 170 | # Allow inbound 3000 from ALB to Frontend Service 171 | self.sec_grp_ingress_self_3000 = ec2.CfnSecurityGroupIngress( 172 | self, "InboundSecGrp3000", 173 | ip_protocol='TCP', 174 | source_security_group_id=self.services_3000_sec_group.security_group_id, 175 | from_port=3000, 176 | to_port=3000, 177 | group_id=self.services_3000_sec_group.security_group_id 178 | ) 179 | 180 | # Creating an EC2 bastion host to perform load test on private backend services 181 | self.amzn_linux = ec2.MachineImage.latest_amazon_linux( 182 | generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 183 | edition=ec2.AmazonLinuxEdition.STANDARD, 184 | virtualization=ec2.AmazonLinuxVirt.HVM, 185 | storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE 186 | ) 187 | 188 | # Instance Role/profile that will be attached to the ec2 instance 189 | # Enabling service role so the EC2 service can use ssm 190 | role = iam.Role(self, "InstanceSSM", assumed_by=iam.ServicePrincipal("ec2.amazonaws.com")) 191 | 192 | # Attaching the SSM policy to the role so we can use SSM to ssh into the ec2 instance 193 | role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AmazonEC2RoleforSSM")) 194 | 195 | # Reading user data, to install siege into the ec2 instance. 196 | with open("stresstool_user_data.sh") as f: 197 | user_data = f.read() 198 | 199 | # Instance creation 200 | self.instance = ec2.Instance( 201 | self, "Instance", 202 | instance_name="{}-stresstool".format(stack_name), 203 | instance_type=ec2.InstanceType("t3.medium"), 204 | machine_image=self.amzn_linux, 205 | vpc = self.vpc, 206 | role = role, 207 | user_data=ec2.UserData.custom(user_data), 208 | security_group=self.services_3000_sec_group 209 | ) 210 | 211 | # App Mesh Configuration 212 | # appmesh() 213 | 214 | # All Outputs required for other stacks to build 215 | CfnOutput(self, "NSArn", value=self.namespace_outputs['ARN'], export_name="NSARN") 216 | CfnOutput(self, "NSName", value=self.namespace_outputs['NAME'], export_name="NSNAME") 217 | CfnOutput(self, "NSId", value=self.namespace_outputs['ID'], export_name="NSID") 218 | CfnOutput(self, "FE2BESecGrp", value=self.services_3000_sec_group.security_group_id, export_name="SecGrpId") 219 | CfnOutput(self, "ECSClusterName", value=self.cluster_outputs['NAME'], export_name="ECSClusterName") 220 | CfnOutput(self, "ECSClusterSecGrp", value=self.cluster_outputs['SECGRPS'], export_name="ECSSecGrpList") 221 | CfnOutput(self, "ServicesSecGrp", value=self.services_3000_sec_group.security_group_id, export_name="ServicesSecGrp") 222 | CfnOutput(self, "StressToolEc2Id",value=self.instance.instance_id) 223 | CfnOutput(self, "StressToolEc2Ip",value=self.instance.instance_private_ip) 224 | 225 | 226 | # function to create app mesh 227 | def appmesh(self): 228 | 229 | # This will create the app mesh (control plane) 230 | self.mesh = appmesh.Mesh(self,"EcsWorkShop-AppMesh", mesh_name="ecs-mesh") 231 | 232 | # We will create a App Mesh Virtual Gateway 233 | self.mesh_vgw = appmesh.VirtualGateway( 234 | self, 235 | "Mesh-VGW", 236 | mesh=self.mesh, 237 | listeners=[appmesh.VirtualGatewayListener.http( 238 | port=3000 239 | )], 240 | virtual_gateway_name="ecsworkshop-vgw" 241 | ) 242 | 243 | # Creating the mesh gateway task for the frontend app 244 | # For more info related to App Mesh Proxy check https://docs.aws.amazon.com/app-mesh/latest/userguide/getting-started-ecs.html 245 | self.mesh_gw_proxy_task_def = ecs.FargateTaskDefinition( 246 | self, 247 | "mesh-gw-proxy-taskdef", 248 | cpu=256, 249 | memory_limit_mib=512, 250 | family="mesh-gw-proxy-taskdef", 251 | ) 252 | 253 | # LogGroup for the App Mesh Proxy Task 254 | self.logGroup = logs.LogGroup(self,"ecsworkshopMeshGateway", 255 | #log_group_name="ecsworkshop-mesh-gateway", 256 | retention=logs.RetentionDays.ONE_WEEK 257 | ) 258 | 259 | # App Mesh Virtual Gateway Envoy proxy Task definition 260 | # For a use specific ECR region, please check https://docs.aws.amazon.com/app-mesh/latest/userguide/envoy.html 261 | container = self.mesh_gw_proxy_task_def.add_container( 262 | "mesh-gw-proxy-contdef", 263 | image=ecs.ContainerImage.from_registry("public.ecr.aws/appmesh/aws-appmesh-envoy:v1.18.3.0-prod"), 264 | container_name="envoy", 265 | memory_reservation_mib=256, 266 | environment={ 267 | "REGION": os.getenv('AWS_DEFAULT_REGION'), 268 | "ENVOY_LOG_LEVEL": "info", 269 | "ENABLE_ENVOY_STATS_TAGS": "1", 270 | # "ENABLE_ENVOY_XRAY_TRACING": "1", 271 | "APPMESH_RESOURCE_ARN": self.mesh_vgw.virtual_gateway_arn 272 | }, 273 | essential=True, 274 | logging=ecs.LogDriver.aws_logs( 275 | stream_prefix='/mesh-gateway', 276 | log_group=self.logGroup 277 | ), 278 | health_check=ecs.HealthCheck( 279 | command=["CMD-SHELL","curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"], 280 | ) 281 | ) 282 | 283 | # Default port where frontend app is listening 284 | container.add_port_mappings( 285 | ecs.PortMapping( 286 | container_port=3000 287 | ) 288 | ) 289 | 290 | #appmesh-xray-uncomment 291 | # xray_container = self.mesh_gw_proxy_task_def.add_container( 292 | # "FrontendServiceXrayContdef", 293 | # image=ecs.ContainerImage.from_registry("amazon/aws-xray-daemon"), 294 | # logging=ecs.LogDriver.aws_logs( 295 | # stream_prefix='/xray-container', 296 | # log_group=logGroup 297 | # ), 298 | # essential=True, 299 | # container_name="xray", 300 | # memory_reservation_mib=256, 301 | # user="1337" 302 | # ) 303 | 304 | # container.add_container_dependencies(ecs.ContainerDependency( 305 | # container=xray_container, 306 | # condition=ecs.ContainerDependencyCondition.START 307 | # ) 308 | # ) 309 | #appmesh-xray-uncomment 310 | 311 | # For environment variables check https://docs.aws.amazon.com/app-mesh/latest/userguide/envoy-config.html 312 | self.mesh_gateway_proxy_fargate_service = ecs_patterns.NetworkLoadBalancedFargateService( 313 | self, 314 | "MeshGW-Proxy-Fargate-Service", 315 | service_name='mesh-gw-proxy', 316 | cpu=256, 317 | memory_limit_mib=512, 318 | desired_count=1, 319 | listener_port=80, 320 | assign_public_ip=True, 321 | task_definition=self.mesh_gw_proxy_task_def, 322 | cluster=self.ecs_cluster, 323 | public_load_balancer=True, 324 | cloud_map_options=ecs.CloudMapOptions( 325 | cloud_map_namespace=self.ecs_cluster.default_cloud_map_namespace, 326 | name='mesh-gw-proxy' 327 | ) 328 | ) 329 | 330 | # For testing purposes we will open any ipv4 requests to port 3000 331 | self.mesh_gateway_proxy_fargate_service.service.connections.allow_from_any_ipv4( 332 | port_range=ec2.Port(protocol=ec2.Protocol.TCP, string_representation="vtw_proxy", from_port=3000, to_port=3000), 333 | description="Allow NLB connections on port 3000" 334 | ) 335 | 336 | self.mesh_gw_proxy_task_def.default_container.add_ulimits(ecs.Ulimit( 337 | hard_limit=15000, 338 | name=ecs.UlimitName.NOFILE, 339 | soft_limit=15000 340 | ) 341 | ) 342 | 343 | #Adding necessary policies for Envoy proxy to communicate with required services 344 | self.mesh_gw_proxy_task_def.execution_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEC2ContainerRegistryReadOnly")) 345 | self.mesh_gw_proxy_task_def.execution_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchLogsFullAccess")) 346 | 347 | self.mesh_gw_proxy_task_def.task_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchFullAccess")) 348 | # mesh_gw_proxy_task_def.task_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("AWSXRayDaemonWriteAccess")) 349 | self.mesh_gw_proxy_task_def.task_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("AWSAppMeshEnvoyAccess")) 350 | 351 | self.mesh_gw_proxy_task_def.execution_role.add_to_policy( 352 | iam.PolicyStatement( 353 | actions=['ec2:DescribeSubnets'], 354 | resources=['*'] 355 | ) 356 | ) 357 | 358 | CfnOutput(self, "MeshGwNlbDns",value=self.mesh_gateway_proxy_fargate_service.load_balancer.load_balancer_dns_name,export_name="MeshGwNlbDns") 359 | CfnOutput(self, "MeshArn",value=self.mesh.mesh_arn,export_name="MeshArn") 360 | CfnOutput(self, "MeshName",value=self.mesh.mesh_name,export_name="MeshName") 361 | CfnOutput(self, "MeshEnvoyServiceArn",value=self.mesh_gateway_proxy_fargate_service.service.service_arn,export_name="MeshEnvoyServiceArn") 362 | CfnOutput(self, "MeshVGWArn",value=self.mesh_vgw.virtual_gateway_arn,export_name="MeshVGWArn") 363 | CfnOutput(self, "MeshVGWName",value=self.mesh_vgw.virtual_gateway_name,export_name="MeshVGWName") 364 | 365 | _env = Environment(account=os.getenv('AWS_ACCOUNT_ID'), region=os.getenv('AWS_DEFAULT_REGION')) 366 | stack_name = "ecsworkshop-base" 367 | app = App() 368 | BaseVPCStack(app, stack_name, env=_env) 369 | app.synth() 370 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py" 3 | } 4 | -------------------------------------------------------------------------------- /cdk/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib>=2.50.0,<2.59.0 2 | constructs>=10.0.0,<11.0.0 -------------------------------------------------------------------------------- /cdk/stresstool_user_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Install the tool 4 | sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 5 | sudo yum-config-manager --enable epel 6 | sudo yum install -y siege 7 | --------------------------------------------------------------------------------