From f3806453f6ec851fa0b3b188a8a9e5e67e2bd2f0 Mon Sep 17 00:00:00 2001 From: msirull Date: Mon, 19 Sep 2016 14:22:52 -0700 Subject: [PATCH 1/2] Create ALB-ECS-WithElasticSearchLogs-NetworkStack This is the network stack which containers an ECS cluster with ALB and ElasticSearch cluster. This also leverages the new cross-stack functionality --- ...ALB-ECS-WithElasticSearchLogs-NetworkStack | 548 ++++++++ ...ithElasticSearchLogs-NetworkStack.template | 1138 +++++++++++++++++ 2 files changed, 1686 insertions(+) create mode 100644 examples/ALB-ECS-WithElasticSearchLogs-NetworkStack create mode 100644 tests/examples_output/ALB-ECS-WithElasticSearchLogs-NetworkStack.template diff --git a/examples/ALB-ECS-WithElasticSearchLogs-NetworkStack b/examples/ALB-ECS-WithElasticSearchLogs-NetworkStack new file mode 100644 index 000000000..b31d93042 --- /dev/null +++ b/examples/ALB-ECS-WithElasticSearchLogs-NetworkStack @@ -0,0 +1,548 @@ +import troposphere.elasticloadbalancingv2 as elb +from troposphere.ecs import Cluster as ECSCluster +from troposphere.elasticsearch import Domain, ElasticsearchClusterConfig +from troposphere.elasticsearch import EBSOptions +from troposphere import Join, Select, GetAZs, Equals, GetAtt, Base64 +from troposphere import FindInMap, Output, If, Or +from troposphere import Parameter, Ref, Tags, Template, autoscaling +from troposphere import cloudformation, Export +from troposphere.ec2 import InternetGateway +from troposphere.ec2 import VPCGatewayAttachment +from troposphere.ec2 import SubnetRouteTableAssociation, Subnet +from troposphere.ec2 import SecurityGroup, SecurityGroupRule +from troposphere.ec2 import RouteTable, Route +from troposphere.ec2 import VPC +from troposphere.ec2 import EIP +from troposphere.ec2 import NatGateway +from troposphere.autoscaling import LaunchConfiguration, AutoScalingGroup, Tag +from troposphere.policies import UpdatePolicy, AutoScalingRollingUpdate +from troposphere.policies import CreationPolicy, ResourceSignal +from troposphere.iam import Role, InstanceProfile + +ref_region = Ref('AWS::Region') +ref_stack_id = Ref('AWS::StackId') +ref_stack_name = Ref('AWS::StackName') +no_value = Ref("AWS::NoValue") +ref_account = Ref("AWS::AccountId") + + +template = Template() +template.add_version("2010-09-09") + +template.add_description( + "ALB-ECS Network Template") + +template.add_mapping( + 'AWSRegion2AMI4ECS', { + "us-east-1": {"AMI": "ami-67a3a90d"}, + "us-west-2": {"AMI": "ami-c7a451a7"} + }) + +template.add_description("""\ +Parent VPC CloudFormation Template""") + +# Set up parameters + +vpc_cidr_prefix = template.add_parameter(Parameter( + "VPCCIDRPrefix", + Description="IP Address range for the VPN connected VPC", + Default="172.31", + Type="String", +)) + +env = template.add_parameter(Parameter( + "Env", + Type="String", + Description="The environment being deployed into", + Default="dev" +)) + +keypair = template.add_parameter(Parameter( + "KeyPair", + Type="String", + Description="Name of an existing EC2 KeyPair to enable SSH access", + MinLength="1", + AllowedPattern="[\x20-\x7E]*", + MaxLength="255", + ConstraintDescription="can contain only ASCII characters.", + Default="None" +)) + +template.add_condition( + "NoKeyPair", Equals(Ref(keypair), "None") +) + + +template.add_condition( + "ProdCheck", Or(Equals(Ref(env), "prod"), + Equals(Ref(env), "staging") + ) +) + +# Start resource creation + +es_domain = template.add_resource(Domain( + 'GlobalElasticSearch', + ElasticsearchClusterConfig=ElasticsearchClusterConfig( + DedicatedMasterEnabled=True, + InstanceCount=2, + ZoneAwarenessEnabled=True, + InstanceType="t2.micro.elasticsearch", + DedicatedMasterType="t2.micro.elasticsearch", + DedicatedMasterCount=2 + ), + EBSOptions=EBSOptions(EBSEnabled=True, + Iops=0, + VolumeSize=20, + VolumeType="gp2"), + AccessPolicies={ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": [ + Join("", ["arn:aws:iam::", ref_account, ":root"]) + ] + }, + "Action": [ + "es:*" + ], + "Resource": "*" + } + ] + } +)) + + +vpc = template.add_resource(VPC( + "VPC", + EnableDnsSupport="true", + CidrBlock=Join('', [Ref(vpc_cidr_prefix), '.0.0/16']), + EnableDnsHostnames="true", + Tags=Tags( + Application=Ref("AWS::StackName"), + Network="VPC", + ) +)) + +igw = template.add_resource(InternetGateway( + "InternetGateway", + Tags=[{"Key": "Network", "Value": "igw"}] +)) +igw_attachment = template.add_resource(VPCGatewayAttachment( + "AttachGateway", + VpcId=Ref(vpc), + InternetGatewayId=Ref("InternetGateway") +)) +public_route_table = template.add_resource(RouteTable( + "PublicRouteTable", + VpcId=Ref(vpc), + Tags=[{"Key": "Network", "Value": "public"}] +)) +route_to_internet_for_public_subnets = template.add_resource(Route( + "RouteToInternetForPublicSubnets", + RouteTableId=Ref(public_route_table), + DestinationCidrBlock="0.0.0.0/0", + GatewayId=Ref(igw) +)) +public_subnet_a = template.add_resource(Subnet( + 'PublicSubnetA', + CidrBlock=Join('', [Ref(vpc_cidr_prefix), '.0.0/26']), + VpcId=Ref(vpc), + AvailabilityZone=Select("0", GetAZs(ref_region)) +) +) +subnet_a_route_table_association = template.add_resource( + SubnetRouteTableAssociation( + 'PublicSubnetRouteTableAssociationA', + SubnetId=Ref(public_subnet_a), + RouteTableId=Ref(public_route_table))) +public_subnet_b = template.add_resource(Subnet( + 'PublicSubnetB', + CidrBlock=Join('', [Ref(vpc_cidr_prefix), '.0.64/26']), + VpcId=Ref(vpc), + AvailabilityZone=Select("1", GetAZs(ref_region)) +)) +subnet_b_route_table_association = template.add_resource( + SubnetRouteTableAssociation( + 'PublicSubnetRouteTableAssociationB', + SubnetId=Ref(public_subnet_b), + RouteTableId=Ref(public_route_table))) +public_subnet_c = template.add_resource(Subnet( + 'PublicSubnetC', + CidrBlock=Join('', [Ref(vpc_cidr_prefix), '.0.128/26']), + VpcId=Ref(vpc), + AvailabilityZone=Select("2", GetAZs(ref_region)) +)) +subnet_c_route_table_association = template.add_resource( + SubnetRouteTableAssociation( + 'PublicSubnetRouteTableAssociationC', + SubnetId=Ref(public_subnet_c), + RouteTableId=Ref(public_route_table))) + +nat_eip = template.add_resource(EIP( + 'NatEip', + Domain="vpc" +)) + +nat_a = template.add_resource(NatGateway( + 'NATa', + AllocationId=GetAtt(nat_eip, 'AllocationId'), + SubnetId=Ref(public_subnet_a) +)) + +nat_b = template.add_resource(NatGateway( + 'NATb', + AllocationId=GetAtt(nat_eip, 'AllocationId'), + SubnetId=Ref(public_subnet_b), + Condition="ProdCheck" +)) + +nat_c = template.add_resource(NatGateway( + 'NATc', + AllocationId=GetAtt(nat_eip, 'AllocationId'), + SubnetId=Ref(public_subnet_c), + Condition="ProdCheck" +)) + + +private_route_table_a = template.add_resource(RouteTable( + "PrivateRouteTableA", + VpcId=Ref(vpc), + Tags=[{"Key": "Network", "Value": "private"}] +)) + +private_route_table_b = template.add_resource(RouteTable( + "PrivateRouteTableB", + VpcId=Ref(vpc), + Tags=[{"Key": "Network", "Value": "private"}] +)) + +private_route_table_c = template.add_resource(RouteTable( + "PrivateRouteTableC", + VpcId=Ref(vpc), + Tags=[{"Key": "Network", "Value": "private"}] +)) + +route_to_internet_for_private_subnet_a = template.add_resource(Route( + "RouteToInternetForPrivateSubnetA", + RouteTableId=Ref(private_route_table_a), + DestinationCidrBlock="0.0.0.0/0", + NatGatewayId=Ref(nat_a) +)) + +route_to_internet_for_private_subnet_b = template.add_resource(Route( + "RouteToInternetForPrivateSubnetB", + RouteTableId=Ref(private_route_table_b), + DestinationCidrBlock="0.0.0.0/0", + NatGatewayId=If("ProdCheck", Ref(nat_b), Ref(nat_a)) +)) + +route_to_internet_for_private_subnet_c = template.add_resource(Route( + "RouteToInternetForPrivateSubnetC", + RouteTableId=Ref(private_route_table_c), + DestinationCidrBlock="0.0.0.0/0", + NatGatewayId=If("ProdCheck", Ref(nat_c), Ref(nat_a)) +)) + +private_subnet_a = template.add_resource(Subnet( + 'PrivateSubnetA', + CidrBlock=Join('', [Ref(vpc_cidr_prefix), '.1.0/26']), + VpcId=Ref(vpc), + AvailabilityZone=Select("0", GetAZs(ref_region)) +)) + +private_subnet_a_route_table_association = template.add_resource( + SubnetRouteTableAssociation( + 'PrivateSubnetRouteTableAssociationA', + SubnetId=Ref(private_subnet_a), + RouteTableId=Ref(private_route_table_a))) + +private_subnet_b = template.add_resource(Subnet( + 'PrivateSubnetB', + CidrBlock=Join('', [Ref(vpc_cidr_prefix), '.1.64/26']), + VpcId=Ref(vpc), + AvailabilityZone=Select("1", GetAZs(ref_region)) +)) + +private_subnet_b_route_table_association = template.add_resource( + SubnetRouteTableAssociation( + 'PrivateSubnetRouteTableAssociationB', + SubnetId=Ref(private_subnet_b), + RouteTableId=Ref(private_route_table_b))) + +private_subnet_c = template.add_resource(Subnet( + 'PrivateSubnetC', + CidrBlock=Join('', [Ref(vpc_cidr_prefix), '.1.128/26']), + VpcId=Ref(vpc), + AvailabilityZone=Select("2", GetAZs(ref_region)) +)) + +private_subnet_c_route_table_association = template.add_resource( + SubnetRouteTableAssociation( + 'PrivateSubnetRouteTableAssociationC', + SubnetId=Ref(private_subnet_c), + RouteTableId=Ref(private_route_table_c))) + +app_role = template.add_resource(Role( + "AppInstanceRole", + ManagedPolicyArns=["arn:aws:iam::aws:policy/service-role/" + "AmazonEC2ContainerServiceforEC2Role"], + AssumeRolePolicyDocument={ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + Path="/" +)) + +service_instance_profile = template.add_resource(InstanceProfile( + "ServiceInstanceProfile", + Path="/", + Roles=[Ref(app_role)] +)) + + +elb_security_group = template.add_resource(SecurityGroup( + "ELBSecurityGroup", + GroupDescription="ELB Security Group", + SecurityGroupIngress=[ + SecurityGroupRule( + IpProtocol='tcp', + FromPort='80', + ToPort='80', + CidrIp='0.0.0.0/0') + ], + VpcId=Ref(vpc) +)) + +instance_security_group = template.add_resource(SecurityGroup( + "InstanceSG", + GroupDescription="Instance_SG", + SecurityGroupIngress=[ + SecurityGroupRule( + IpProtocol='TCP', + FromPort='443', + ToPort='443', + SourceSecurityGroupId=Ref(elb_security_group) + ), + SecurityGroupRule( + IpProtocol='-1', + FromPort="-1", + ToPort="-1", + SourceSecurityGroupId=Ref(elb_security_group) + ), + SecurityGroupRule( + IpProtocol='-1', + FromPort='-1', + ToPort='-1', + CidrIp=Join('', [Ref(vpc_cidr_prefix), '.0.0/0']) + ) + ], + VpcId=Ref(vpc) +)) + +ecs_cluster = template.add_resource(ECSCluster( + "ECSServiceCluster" +)) + +# Add the application ELB +app_load_balancer = template.add_resource(elb.LoadBalancer( + "ApplicationElasticLB", + Name="ApplicationElasticLB", + Scheme="internet-facing", + Subnets=[Ref(public_subnet_a), Ref(public_subnet_b), Ref(public_subnet_c)], + SecurityGroups=[Ref(elb_security_group)] +)) + +default_target_group = template.add_resource(elb.TargetGroup( + "DefaultTargetGroup", + HealthCheckIntervalSeconds="30", + HealthCheckProtocol="HTTP", + HealthCheckTimeoutSeconds="10", + HealthyThresholdCount="4", + Matcher=elb.Matcher( + HttpCode="200"), + Name="DefaultRedirect", + Port="80", + Protocol="HTTP", + UnhealthyThresholdCount="3", + VpcId=Ref(vpc) +)) + +http_listener = template.add_resource(elb.Listener( + "HTTPListener", + Port="80", + Protocol="HTTP", + LoadBalancerArn=Ref(app_load_balancer), + DefaultActions=[elb.Action( + Type="forward", + TargetGroupArn=Ref(default_target_group) + )] +)) + +ecs_launch_configuration = template.add_resource(LaunchConfiguration( + "ECSLaunchConfiguration", + Metadata=autoscaling.Metadata( + cloudformation.Init( + cloudformation.InitConfigSets( + default=['ecs'] + ), + ecs=cloudformation.InitConfig( + commands={ + "RegisterWithECS": { + "command": Join("", [ + "echo ECS_CLUSTER=", Ref(ecs_cluster), + " >> /etc/ecs/ecs.config" + ]) + } + } + ) + ) + ), + UserData=Base64(Join('', [ + "#cloud-config\n", + "repo_upgrade: all\n", + "runcmd:\n", + " - echo '05-22-2016-1652'\n", + " - yum install -y aws-cfn-bootstrap aws-cli\n", + " - /opt/aws/bin/cfn-init -v", + " --resource 'ECSLaunchConfiguration'", + " -c 'default'" + " --stack ", ref_stack_name, + " --region ", ref_region, "\n", + " - /opt/aws/bin/cfn-signal -e $?", + " --resource ECSServiceASG", + " --stack ", ref_stack_name, + " --region ", ref_region, "\n" + ])), + ImageId=FindInMap("AWSRegion2AMI4ECS", ref_region, "AMI"), + KeyName=If("ProdCheck", no_value, If("NoKeyPair", no_value, Ref(keypair))), + IamInstanceProfile=Ref(service_instance_profile), + SecurityGroups=[Ref(instance_security_group)], + InstanceType=If("ProdCheck", "m4.large", "t2.medium") +)) + +ecs_service_asg = template.add_resource(AutoScalingGroup( + "ECSServiceASG", + CreationPolicy=CreationPolicy( + ResourceSignal=ResourceSignal( + Count="1", + Timeout='PT10M' + ) + ), + DesiredCapacity=If("ProdCheck", "3", "2"), + Tags=[ + Tag("Environment", Ref(env), True), + Tag("Name", "ECS Cluster", True) + ], + LaunchConfigurationName=Ref(ecs_launch_configuration), + MinSize="2", + MaxSize="10", + VPCZoneIdentifier=[Ref(private_subnet_a), Ref(private_subnet_b), + Ref(private_subnet_c)], + AvailabilityZones=[ + GetAtt(private_subnet_a, "AvailabilityZone"), + GetAtt(private_subnet_b, "AvailabilityZone"), + GetAtt(private_subnet_c, "AvailabilityZone") + ], + HealthCheckType="EC2", + UpdatePolicy=UpdatePolicy( + AutoScalingRollingUpdate=AutoScalingRollingUpdate( + PauseTime='PT5M', + MinInstancesInService="1", + MaxBatchSize='1', + WaitOnResourceSignals=True + ) + ) +)) + +template.add_output(Output( + "VPCId", + Description="VPC ID", + Value=Ref(vpc), + Export=Export(Join("", ["ECSClusterVPC-", Ref(env)])) +)) + +template.add_output(Output( + "ECSClusterID", + Description="ECS Cluster ID", + Value=Ref(ecs_cluster), + Export=Export(Join("", ["ECSClusterID-", Ref(env)])) +)) + +template.add_output(Output( + "HTTPListener", + Description="ALB HTTP Listener", + Value=Ref(http_listener), + Export=Export(Join("", ["ALBHttpListener-", Ref(env)])) +)) + +template.add_output(Output( + "ECSClusterALB", + Description="ALB for ECS Cluster", + Value=Ref(app_load_balancer), + Export=Export(Join("", ["ECSALB-", Ref(env)])) +)) + +template.add_output(Output( + "GlobalESClusterEndpoint", + Description="Global ElasticSearch Cluster Endpoint", + Value=GetAtt(es_domain, "DomainEndpoint"), + Export=Export(Join("", ["GlobalESClusterEndpoint-", Ref(env)])) +)) + +template.add_output(Output( + "ESClusterARN", + Description="Global ElasticSearch Cluster ARN", + Value=GetAtt(es_domain, "DomainArn"), + Export=Export(Join("", ["GlobalESClusterARN-", Ref(env)])) +)) + +template.add_output(Output( + "VPCCidrPrefix", + Description="VPC Cidr Prefix", + Value=Ref(vpc_cidr_prefix), + Export=Export(Join("", ["VPCCidrPrefix-", Ref(env)])) +)) + +template.add_output(Output( + "NATa", + Description="NAT A", + Value=Ref(nat_a), + Export=Export(Join("", ["NATa-", Ref(env)])) +)) + +template.add_output(Output( + "NATb", + Description="NAT B", + Value=Ref(nat_b), + Export=Export(Join("", ["NATb-", Ref(env)])) +)) + +template.add_output(Output( + "NATc", + Description="NAT C", + Value=Ref(nat_c), + Export=Export(Join("", ["NATc-", Ref(env)])) +)) + +template.add_output(Output( + "URL", + Description="URL of the ELB", + Value=Join("", ["http://", GetAtt(app_load_balancer, "DNSName")]) +)) + +print(template.to_json()) diff --git a/tests/examples_output/ALB-ECS-WithElasticSearchLogs-NetworkStack.template b/tests/examples_output/ALB-ECS-WithElasticSearchLogs-NetworkStack.template new file mode 100644 index 000000000..cf1c5bb68 --- /dev/null +++ b/tests/examples_output/ALB-ECS-WithElasticSearchLogs-NetworkStack.template @@ -0,0 +1,1138 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Conditions": { + "NoKeyPair": { + "Fn::Equals": [ + { + "Ref": "KeyPair" + }, + "None" + ] + }, + "ProdCheck": { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "Env" + }, + "prod" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "Env" + }, + "staging" + ] + } + ] + } + }, + "Description": "Parent VPC CloudFormation Template", + "Mappings": { + "AWSRegion2AMI4ECS": { + "us-east-1": { + "AMI": "ami-67a3a90d" + }, + "us-west-2": { + "AMI": "ami-c7a451a7" + } + } + }, + "Outputs": { + "ECSClusterALB": { + "Description": "ALB for ECS Cluster", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "ECSALB-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Ref": "ApplicationElasticLB" + } + }, + "ECSClusterID": { + "Description": "ECS Cluster ID", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "ECSClusterID-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Ref": "ECSServiceCluster" + } + }, + "ESClusterARN": { + "Description": "Global ElasticSearch Cluster ARN", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "GlobalESClusterARN-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Fn::GetAtt": [ + "GlobalElasticSearch", + "DomainArn" + ] + } + }, + "GlobalESClusterEndpoint": { + "Description": "Global ElasticSearch Cluster Endpoint", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "GlobalESClusterEndpoint-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Fn::GetAtt": [ + "GlobalElasticSearch", + "DomainEndpoint" + ] + } + }, + "HTTPListener": { + "Description": "ALB HTTP Listener", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "ALBHttpListener-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Ref": "HTTPListener" + } + }, + "NATa": { + "Description": "NAT A", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "NATa-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Ref": "NATa" + } + }, + "NATb": { + "Description": "NAT B", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "NATb-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Ref": "NATb" + } + }, + "NATc": { + "Description": "NAT C", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "NATc-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Ref": "NATc" + } + }, + "URL": { + "Description": "URL of the ELB", + "Value": { + "Fn::Join": [ + "", + [ + "http://", + { + "Fn::GetAtt": [ + "ApplicationElasticLB", + "DNSName" + ] + } + ] + ] + } + }, + "VPCCidrPrefix": { + "Description": "VPC Cidr Prefix", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "VPCCidrPrefix-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Ref": "VPCCIDRPrefix" + } + }, + "VPCId": { + "Description": "VPC ID", + "Export": { + "Name": { + "Fn::Join": [ + "", + [ + "ECSClusterVPC-", + { + "Ref": "Env" + } + ] + ] + } + }, + "Value": { + "Ref": "VPC" + } + } + }, + "Parameters": { + "Env": { + "Default": "dev", + "Description": "The environment being deployed into", + "Type": "String" + }, + "KeyPair": { + "AllowedPattern": "[ -~]*", + "ConstraintDescription": "can contain only ASCII characters.", + "Default": "None", + "Description": "Name of an existing EC2 KeyPair to enable SSH access", + "MaxLength": "255", + "MinLength": "1", + "Type": "String" + }, + "VPCCIDRPrefix": { + "Default": "172.31", + "Description": "IP Address range for the VPN connected VPC", + "Type": "String" + } + }, + "Resources": { + "AppInstanceRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" + ], + "Path": "/" + }, + "Type": "AWS::IAM::Role" + }, + "ApplicationElasticLB": { + "Properties": { + "Name": "ApplicationElasticLB", + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Ref": "ELBSecurityGroup" + } + ], + "Subnets": [ + { + "Ref": "PublicSubnetA" + }, + { + "Ref": "PublicSubnetB" + }, + { + "Ref": "PublicSubnetC" + } + ] + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" + }, + "AttachGateway": { + "Properties": { + "InternetGatewayId": { + "Ref": "InternetGateway" + }, + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::VPCGatewayAttachment" + }, + "DefaultTargetGroup": { + "Properties": { + "HealthCheckIntervalSeconds": "30", + "HealthCheckProtocol": "HTTP", + "HealthCheckTimeoutSeconds": "10", + "HealthyThresholdCount": "4", + "Matcher": { + "HttpCode": "200" + }, + "Name": "DefaultRedirect", + "Port": "80", + "Protocol": "HTTP", + "UnhealthyThresholdCount": "3", + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" + }, + "ECSLaunchConfiguration": { + "Metadata": { + "AWS::CloudFormation::Init": { + "configSets": { + "default": [ + "ecs" + ] + }, + "ecs": { + "commands": { + "RegisterWithECS": { + "command": { + "Fn::Join": [ + "", + [ + "echo ECS_CLUSTER=", + { + "Ref": "ECSServiceCluster" + }, + " >> /etc/ecs/ecs.config" + ] + ] + } + } + } + } + } + }, + "Properties": { + "IamInstanceProfile": { + "Ref": "ServiceInstanceProfile" + }, + "ImageId": { + "Fn::FindInMap": [ + "AWSRegion2AMI4ECS", + { + "Ref": "AWS::Region" + }, + "AMI" + ] + }, + "InstanceType": { + "Fn::If": [ + "ProdCheck", + "m4.large", + "t2.medium" + ] + }, + "KeyName": { + "Fn::If": [ + "ProdCheck", + { + "Ref": "AWS::NoValue" + }, + { + "Fn::If": [ + "NoKeyPair", + { + "Ref": "AWS::NoValue" + }, + { + "Ref": "KeyPair" + } + ] + } + ] + }, + "SecurityGroups": [ + { + "Ref": "InstanceSG" + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#cloud-config\n", + "repo_upgrade: all\n", + "runcmd:\n", + " - echo '05-22-2016-1652'\n", + " - yum install -y aws-cfn-bootstrap aws-cli\n", + " - /opt/aws/bin/cfn-init -v", + " --resource 'ECSLaunchConfiguration'", + " -c 'default' --stack ", + { + "Ref": "AWS::StackName" + }, + " --region ", + { + "Ref": "AWS::Region" + }, + "\n", + " - /opt/aws/bin/cfn-signal -e $?", + " --resource ECSServiceASG", + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --region ", + { + "Ref": "AWS::Region" + }, + "\n" + ] + ] + } + } + }, + "Type": "AWS::AutoScaling::LaunchConfiguration" + }, + "ECSServiceASG": { + "CreationPolicy": { + "ResourceSignal": { + "Count": "1", + "Timeout": "PT10M" + } + }, + "Properties": { + "AvailabilityZones": [ + { + "Fn::GetAtt": [ + "PrivateSubnetA", + "AvailabilityZone" + ] + }, + { + "Fn::GetAtt": [ + "PrivateSubnetB", + "AvailabilityZone" + ] + }, + { + "Fn::GetAtt": [ + "PrivateSubnetC", + "AvailabilityZone" + ] + } + ], + "DesiredCapacity": { + "Fn::If": [ + "ProdCheck", + "3", + "2" + ] + }, + "HealthCheckType": "EC2", + "LaunchConfigurationName": { + "Ref": "ECSLaunchConfiguration" + }, + "MaxSize": "10", + "MinSize": "2", + "Tags": [ + { + "Key": "Environment", + "PropagateAtLaunch": true, + "Value": { + "Ref": "Env" + } + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "ECS Cluster" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "PrivateSubnetA" + }, + { + "Ref": "PrivateSubnetB" + }, + { + "Ref": "PrivateSubnetC" + } + ] + }, + "Type": "AWS::AutoScaling::AutoScalingGroup", + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "MaxBatchSize": "1", + "MinInstancesInService": "1", + "PauseTime": "PT5M", + "WaitOnResourceSignals": "true" + } + } + }, + "ECSServiceCluster": { + "Type": "AWS::ECS::Cluster" + }, + "ELBSecurityGroup": { + "Properties": { + "GroupDescription": "ELB Security Group", + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "FromPort": "80", + "IpProtocol": "tcp", + "ToPort": "80" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "GlobalElasticSearch": { + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": [ + "es:*" + ], + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + ] + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EBSOptions": { + "EBSEnabled": "true", + "Iops": 0, + "VolumeSize": 20, + "VolumeType": "gp2" + }, + "ElasticsearchClusterConfig": { + "DedicatedMasterCount": 2, + "DedicatedMasterEnabled": "true", + "DedicatedMasterType": "t2.micro.elasticsearch", + "InstanceCount": 2, + "InstanceType": "t2.micro.elasticsearch", + "ZoneAwarenessEnabled": "true" + } + }, + "Type": "AWS::Elasticsearch::Domain" + }, + "HTTPListener": { + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "DefaultTargetGroup" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "ApplicationElasticLB" + }, + "Port": "80", + "Protocol": "HTTP" + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener" + }, + "InstanceSG": { + "Properties": { + "GroupDescription": "Instance_SG", + "SecurityGroupIngress": [ + { + "FromPort": "443", + "IpProtocol": "TCP", + "SourceSecurityGroupId": { + "Ref": "ELBSecurityGroup" + }, + "ToPort": "443" + }, + { + "FromPort": "-1", + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "ELBSecurityGroup" + }, + "ToPort": "-1" + }, + { + "CidrIp": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCCIDRPrefix" + }, + ".0.0/0" + ] + ] + }, + "FromPort": "-1", + "IpProtocol": "-1", + "ToPort": "-1" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "InternetGateway": { + "Properties": { + "Tags": [ + { + "Key": "Network", + "Value": "igw" + } + ] + }, + "Type": "AWS::EC2::InternetGateway" + }, + "NATa": { + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "NatEip", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "PublicSubnetA" + } + }, + "Type": "AWS::EC2::NatGateway" + }, + "NATb": { + "Condition": "ProdCheck", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "NatEip", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "PublicSubnetB" + } + }, + "Type": "AWS::EC2::NatGateway" + }, + "NATc": { + "Condition": "ProdCheck", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "NatEip", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "PublicSubnetC" + } + }, + "Type": "AWS::EC2::NatGateway" + }, + "NatEip": { + "Properties": { + "Domain": "vpc" + }, + "Type": "AWS::EC2::EIP" + }, + "PrivateRouteTableA": { + "Properties": { + "Tags": [ + { + "Key": "Network", + "Value": "private" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::RouteTable" + }, + "PrivateRouteTableB": { + "Properties": { + "Tags": [ + { + "Key": "Network", + "Value": "private" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::RouteTable" + }, + "PrivateRouteTableC": { + "Properties": { + "Tags": [ + { + "Key": "Network", + "Value": "private" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::RouteTable" + }, + "PrivateSubnetA": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "0", + { + "Fn::GetAZs": { + "Ref": "AWS::Region" + } + } + ] + }, + "CidrBlock": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCCIDRPrefix" + }, + ".1.0/26" + ] + ] + }, + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PrivateSubnetB": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "1", + { + "Fn::GetAZs": { + "Ref": "AWS::Region" + } + } + ] + }, + "CidrBlock": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCCIDRPrefix" + }, + ".1.64/26" + ] + ] + }, + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PrivateSubnetC": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "2", + { + "Fn::GetAZs": { + "Ref": "AWS::Region" + } + } + ] + }, + "CidrBlock": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCCIDRPrefix" + }, + ".1.128/26" + ] + ] + }, + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PrivateSubnetRouteTableAssociationA": { + "Properties": { + "RouteTableId": { + "Ref": "PrivateRouteTableA" + }, + "SubnetId": { + "Ref": "PrivateSubnetA" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "PrivateSubnetRouteTableAssociationB": { + "Properties": { + "RouteTableId": { + "Ref": "PrivateRouteTableB" + }, + "SubnetId": { + "Ref": "PrivateSubnetB" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "PrivateSubnetRouteTableAssociationC": { + "Properties": { + "RouteTableId": { + "Ref": "PrivateRouteTableC" + }, + "SubnetId": { + "Ref": "PrivateSubnetC" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "PublicRouteTable": { + "Properties": { + "Tags": [ + { + "Key": "Network", + "Value": "public" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::RouteTable" + }, + "PublicSubnetA": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "0", + { + "Fn::GetAZs": { + "Ref": "AWS::Region" + } + } + ] + }, + "CidrBlock": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCCIDRPrefix" + }, + ".0.0/26" + ] + ] + }, + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PublicSubnetB": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "1", + { + "Fn::GetAZs": { + "Ref": "AWS::Region" + } + } + ] + }, + "CidrBlock": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCCIDRPrefix" + }, + ".0.64/26" + ] + ] + }, + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PublicSubnetC": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "2", + { + "Fn::GetAZs": { + "Ref": "AWS::Region" + } + } + ] + }, + "CidrBlock": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCCIDRPrefix" + }, + ".0.128/26" + ] + ] + }, + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PublicSubnetRouteTableAssociationA": { + "Properties": { + "RouteTableId": { + "Ref": "PublicRouteTable" + }, + "SubnetId": { + "Ref": "PublicSubnetA" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "PublicSubnetRouteTableAssociationB": { + "Properties": { + "RouteTableId": { + "Ref": "PublicRouteTable" + }, + "SubnetId": { + "Ref": "PublicSubnetB" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "PublicSubnetRouteTableAssociationC": { + "Properties": { + "RouteTableId": { + "Ref": "PublicRouteTable" + }, + "SubnetId": { + "Ref": "PublicSubnetC" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "RouteToInternetForPrivateSubnetA": { + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "NATa" + }, + "RouteTableId": { + "Ref": "PrivateRouteTableA" + } + }, + "Type": "AWS::EC2::Route" + }, + "RouteToInternetForPrivateSubnetB": { + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Fn::If": [ + "ProdCheck", + { + "Ref": "NATb" + }, + { + "Ref": "NATa" + } + ] + }, + "RouteTableId": { + "Ref": "PrivateRouteTableB" + } + }, + "Type": "AWS::EC2::Route" + }, + "RouteToInternetForPrivateSubnetC": { + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Fn::If": [ + "ProdCheck", + { + "Ref": "NATc" + }, + { + "Ref": "NATa" + } + ] + }, + "RouteTableId": { + "Ref": "PrivateRouteTableC" + } + }, + "Type": "AWS::EC2::Route" + }, + "RouteToInternetForPublicSubnets": { + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "InternetGateway" + }, + "RouteTableId": { + "Ref": "PublicRouteTable" + } + }, + "Type": "AWS::EC2::Route" + }, + "ServiceInstanceProfile": { + "Properties": { + "Path": "/", + "Roles": [ + { + "Ref": "AppInstanceRole" + } + ] + }, + "Type": "AWS::IAM::InstanceProfile" + }, + "VPC": { + "Properties": { + "CidrBlock": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCCIDRPrefix" + }, + ".0.0/16" + ] + ] + }, + "EnableDnsHostnames": "true", + "EnableDnsSupport": "true", + "Tags": [ + { + "Key": "Application", + "Value": { + "Ref": "AWS::StackName" + } + }, + { + "Key": "Network", + "Value": "VPC" + } + ] + }, + "Type": "AWS::EC2::VPC" + } + } +} From 2d91f97b43a3e5cffa35ac02f202d26c5c051b08 Mon Sep 17 00:00:00 2001 From: msirull Date: Mon, 19 Sep 2016 14:26:56 -0700 Subject: [PATCH 2/2] Create ALB-ECS-WithElasticSearchLogs-AppStack This is the app stack which contains an ECS Task Definition that attaches to an ECS cluster and a CloudWatch Logs group which streams logs to an ElasticSearch cluster from the network template. This also leverages the new cross-stack functionality --- .../ALB-ECS-WithElasticSearchLogs-AppStack.py | 301 ++++++++++++ ...ECS-WithElasticSearchLogs-NetworkStack.py} | 0 ...CS-WithElasticSearchLogs-AppStack.template | 456 ++++++++++++++++++ 3 files changed, 757 insertions(+) create mode 100644 examples/ALB-ECS-WithElasticSearchLogs-AppStack.py rename examples/{ALB-ECS-WithElasticSearchLogs-NetworkStack => ALB-ECS-WithElasticSearchLogs-NetworkStack.py} (100%) create mode 100644 tests/examples_output/ALB-ECS-WithElasticSearchLogs-AppStack.template diff --git a/examples/ALB-ECS-WithElasticSearchLogs-AppStack.py b/examples/ALB-ECS-WithElasticSearchLogs-AppStack.py new file mode 100644 index 000000000..02440ec53 --- /dev/null +++ b/examples/ALB-ECS-WithElasticSearchLogs-AppStack.py @@ -0,0 +1,301 @@ +from troposphere import Join, GetAtt, ImportValue +from troposphere import Parameter, Ref, Template +from troposphere.ecs import Service as ECSService +from troposphere.ecs import LoadBalancer as ECSLoadBalancer +from troposphere.ecs import TaskDefinition, ContainerDefinition +from troposphere.ecs import Environment, PortMapping, LogConfiguration +from troposphere.ecs import Volume as ECSVolume +from troposphere.iam import Role, PolicyType +import troposphere.elasticloadbalancingv2 as elb +from troposphere.logs import LogGroup + +template = Template() +template.add_version("2010-09-09") + +template.add_description( + "ALB-ECS App Template") + +ref_region = Ref('AWS::Region') +ref_stack_id = Ref('AWS::StackId') +ref_stack_name = Ref('AWS::StackName') +no_value = Ref("AWS::NoValue") +ref_account = Ref("AWS::AccountId") + +# Parameters + +docker_image = template.add_parameter(Parameter( + "DockerImage", + Type="String", + Description="Docker image to deploy" +)) + +priority = template.add_parameter(Parameter( + "Priority", + Type="String", + Description="ALB Listener Rule Priority. Can't conflict with another rule" +)) + +service_name = template.add_parameter(Parameter( + "ServiceName", + Type="String", + Description="Service Name" +)) + +number_of_containers = template.add_parameter(Parameter( + "NumberOfContainers", + Type="String", + Description="Optionally specify the number of containers of your " + "service you want to run", + Default="1" +)) + +health_check_path = template.add_parameter(Parameter( + "HealthCheckPath", + Type="String", + Description="Health Check Path. Don't include the base path of your " + "service name. For example, just write: '/ping" +)) + +container_port = template.add_parameter(Parameter( + "ContainerPort", + Type="String", + Description="This is the port specified in the Dockerfile" +)) + +env = template.add_parameter(Parameter( + "Environment", + Type="String", + Description="Deployment Environment" +)) + +database_endpoint = template.add_parameter(Parameter( + "DatabaseEndpoint", + Type="String", + Description="Customer database endpoint" +)) +# Imports + +vpc = ImportValue(Join("", ["ECSClusterVPC-", Ref(env)])) +ecs_cluster = ImportValue(Join("", ["ECSClusterID-", Ref(env)])) +http_listener = ImportValue(Join("", ["ALBHttpListener-", Ref(env)])) +app_load_balancer = ImportValue(Join("", ["ECSALB-", Ref(env)])) +global_es_cluster_endpoint = ImportValue(Join("", + ["GlobalESClusterEndpoint-", + Ref(env)])) +global_es_cluster_arn = ImportValue(Join("", ["GlobalESClusterARN-", + Ref(env)])) + +# Resources + +app_log_group = template.add_resource(LogGroup( + "AppLogGroup", + RetentionInDays=7 +)) + + +service_ecs_role = template.add_resource(Role( + "ECSServiceRole", + AssumeRolePolicyDocument={ + "Version": "2008-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "ecs.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + ManagedPolicyArns=["arn:aws:iam::aws:policy/service-role/" + "AmazonEC2ContainerServiceRole"], + Path="/" +)) + +ecs_task_role = template.add_resource(Role( + "ECSTaskRole", + AssumeRolePolicyDocument={ + "Version": "2008-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "ecs-tasks.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + Path="/" +)) + +basic_app_permissions = template.add_resource(PolicyType( + "BasicAppPermissions", + PolicyName="BasicAppPermissions", + PolicyDocument={ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:Create*", + "logs:PutLogEvents" + ], + "Resource": [ + Join("", [ + "arn:aws:logs:", ref_region, ":", + ref_account, ":log-group:", + Ref(app_log_group), ":*"]), + "*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "cloudwatch:PutMetricData" + ], + "Resource": [ + "*" + ] + } + ] + }, + Roles=[Ref(ecs_task_role)] +)) + +docker_containers = [ContainerDefinition( + Cpu=512, + Essential=True, + Image=Ref(docker_image), + Memory=512, + Name=Join("", [Ref(service_name), "-api"]), + Environment=[ + Environment( + Name="DB_ENDPOINT", + Value=Ref(database_endpoint) + ), + Environment( + Name="CW_LOGS_GROUP", + Value=Ref(app_log_group) + ), + Environment( + Name="REGION", + Value=ref_region + ) + ], + PortMappings=[ + PortMapping( + ContainerPort=Ref(container_port) + ) + ], + LogConfiguration=LogConfiguration( + LogDriver="awslogs", + Options={ + "awslogs-group": Ref(app_log_group), + "awslogs-region": ref_region + } + ) +)] + +service_task = template.add_resource(TaskDefinition( + "ServiceTask", + ContainerDefinitions=docker_containers, + Volumes=[ + ECSVolume( + Name="default" + ) + ], + TaskRoleArn=GetAtt(ecs_task_role, "Arn") +)) + +target_group_api = template.add_resource(elb.TargetGroup( + "APITargetGroup", + HealthCheckIntervalSeconds="10", + HealthCheckProtocol="HTTP", + HealthCheckTimeoutSeconds="9", + HealthyThresholdCount="2", + HealthCheckPath=Join("", ["/api/", Ref(service_name), + Ref(health_check_path)]), + Matcher=elb.Matcher( + HttpCode="200"), + Name=Join("", [Ref(service_name), "-TargetGroup"]), + Port=Ref(container_port), + Protocol="HTTP", + UnhealthyThresholdCount="2", + VpcId=vpc +)) + +ecs_service = template.add_resource(ECSService( + "ECSService", + Cluster=ecs_cluster, + DesiredCount=Ref(number_of_containers), + LoadBalancers=[(ECSLoadBalancer( + ContainerName=Join("", [Ref(service_name), "-api"]), + ContainerPort=Ref(container_port), + TargetGroupArn=Ref(target_group_api) + ))], + TaskDefinition=Ref(service_task), + Role=Ref(service_ecs_role) +)) + +template.add_resource(elb.ListenerRule( + "ListenerRuleApi", + ListenerArn=http_listener, + Conditions=[elb.Condition( + Field="path-pattern", + Values=[Join("", ["/api/", Ref(service_name), "/*"])] + )], + Actions=[elb.Action( + Type="forward", + TargetGroupArn=Ref(target_group_api) + )], + Priority=Ref(priority) +)) + +cw_lambda_role = template.add_resource(Role( + "CWLogsRole", + AssumeRolePolicyDocument={ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + Path="/" +)) + +es_pemissions = template.add_resource(PolicyType( + "ESPostingPermissions", + PolicyName="ESPostingPermissions", + PolicyDocument={ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["es:ESHttpPost", "es:ESHttpPut"], + "Resource": [global_es_cluster_arn, + Join("", [global_es_cluster_arn, "/*"])] + } + ] + }, + Roles=[Ref(cw_lambda_role)] +)) + +print(template.to_json()) diff --git a/examples/ALB-ECS-WithElasticSearchLogs-NetworkStack b/examples/ALB-ECS-WithElasticSearchLogs-NetworkStack.py similarity index 100% rename from examples/ALB-ECS-WithElasticSearchLogs-NetworkStack rename to examples/ALB-ECS-WithElasticSearchLogs-NetworkStack.py diff --git a/tests/examples_output/ALB-ECS-WithElasticSearchLogs-AppStack.template b/tests/examples_output/ALB-ECS-WithElasticSearchLogs-AppStack.template new file mode 100644 index 000000000..1ef4f82cb --- /dev/null +++ b/tests/examples_output/ALB-ECS-WithElasticSearchLogs-AppStack.template @@ -0,0 +1,456 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "ALB-ECS App Template", + "Parameters": { + "ContainerPort": { + "Description": "This is the port specified in the Dockerfile", + "Type": "String" + }, + "DatabaseEndpoint": { + "Description": "Customer database endpoint", + "Type": "String" + }, + "DockerImage": { + "Description": "Docker image to deploy", + "Type": "String" + }, + "Environment": { + "Description": "Deployment Environment", + "Type": "String" + }, + "HealthCheckPath": { + "Description": "Health Check Path. Don't include the base path of your service name. For example, just write: '/ping", + "Type": "String" + }, + "NumberOfContainers": { + "Default": "1", + "Description": "Optionally specify the number of containers of your service you want to run", + "Type": "String" + }, + "Priority": { + "Description": "ALB Listener Rule Priority. Can't conflict with another rule", + "Type": "String" + }, + "ServiceName": { + "Description": "Service Name", + "Type": "String" + } + }, + "Resources": { + "APITargetGroup": { + "Properties": { + "HealthCheckIntervalSeconds": "10", + "HealthCheckPath": { + "Fn::Join": [ + "", + [ + "/api/", + { + "Ref": "ServiceName" + }, + { + "Ref": "HealthCheckPath" + } + ] + ] + }, + "HealthCheckProtocol": "HTTP", + "HealthCheckTimeoutSeconds": "9", + "HealthyThresholdCount": "2", + "Matcher": { + "HttpCode": "200" + }, + "Name": { + "Fn::Join": [ + "", + [ + { + "Ref": "ServiceName" + }, + "-TargetGroup" + ] + ] + }, + "Port": { + "Ref": "ContainerPort" + }, + "Protocol": "HTTP", + "UnhealthyThresholdCount": "2", + "VpcId": { + "Fn::ImportValue": { + "Fn::Join": [ + "", + [ + "ECSClusterVPC-", + { + "Ref": "Environment" + } + ] + ] + } + } + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" + }, + "AppLogGroup": { + "Properties": { + "RetentionInDays": 7 + }, + "Type": "AWS::Logs::LogGroup" + }, + "BasicAppPermissions": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:Create*", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "AppLogGroup" + }, + ":*" + ] + ] + }, + "*" + ] + }, + { + "Action": [ + "cloudwatch:PutMetricData" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BasicAppPermissions", + "Roles": [ + { + "Ref": "ECSTaskRole" + } + ] + }, + "Type": "AWS::IAM::Policy" + }, + "CWLogsRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Path": "/" + }, + "Type": "AWS::IAM::Role" + }, + "ECSService": { + "Properties": { + "Cluster": { + "Fn::ImportValue": { + "Fn::Join": [ + "", + [ + "ECSClusterID-", + { + "Ref": "Environment" + } + ] + ] + } + }, + "DesiredCount": { + "Ref": "NumberOfContainers" + }, + "LoadBalancers": [ + { + "ContainerName": { + "Fn::Join": [ + "", + [ + { + "Ref": "ServiceName" + }, + "-api" + ] + ] + }, + "ContainerPort": { + "Ref": "ContainerPort" + }, + "TargetGroupArn": { + "Ref": "APITargetGroup" + } + } + ], + "Role": { + "Ref": "ECSServiceRole" + }, + "TaskDefinition": { + "Ref": "ServiceTask" + } + }, + "Type": "AWS::ECS::Service" + }, + "ECSServiceRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "ecs.amazonaws.com" + ] + } + } + ], + "Version": "2008-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole" + ], + "Path": "/" + }, + "Type": "AWS::IAM::Role" + }, + "ECSTaskRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "ecs-tasks.amazonaws.com" + ] + } + } + ], + "Version": "2008-10-17" + }, + "Path": "/" + }, + "Type": "AWS::IAM::Role" + }, + "ESPostingPermissions": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "es:ESHttpPost", + "es:ESHttpPut" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::ImportValue": { + "Fn::Join": [ + "", + [ + "GlobalESClusterARN-", + { + "Ref": "Environment" + } + ] + ] + } + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::ImportValue": { + "Fn::Join": [ + "", + [ + "GlobalESClusterARN-", + { + "Ref": "Environment" + } + ] + ] + } + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ESPostingPermissions", + "Roles": [ + { + "Ref": "CWLogsRole" + } + ] + }, + "Type": "AWS::IAM::Policy" + }, + "ListenerRuleApi": { + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "APITargetGroup" + }, + "Type": "forward" + } + ], + "Conditions": [ + { + "Field": "path-pattern", + "Values": [ + { + "Fn::Join": [ + "", + [ + "/api/", + { + "Ref": "ServiceName" + }, + "/*" + ] + ] + } + ] + } + ], + "ListenerArn": { + "Fn::ImportValue": { + "Fn::Join": [ + "", + [ + "ALBHttpListener-", + { + "Ref": "Environment" + } + ] + ] + } + }, + "Priority": { + "Ref": "Priority" + } + }, + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule" + }, + "ServiceTask": { + "Properties": { + "ContainerDefinitions": [ + { + "Cpu": 512, + "Environment": [ + { + "Name": "DB_ENDPOINT", + "Value": { + "Ref": "DatabaseEndpoint" + } + }, + { + "Name": "CW_LOGS_GROUP", + "Value": { + "Ref": "AppLogGroup" + } + }, + { + "Name": "REGION", + "Value": { + "Ref": "AWS::Region" + } + } + ], + "Essential": "true", + "Image": { + "Ref": "DockerImage" + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "AppLogGroup" + }, + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, + "Memory": 512, + "Name": { + "Fn::Join": [ + "", + [ + { + "Ref": "ServiceName" + }, + "-api" + ] + ] + }, + "PortMappings": [ + { + "ContainerPort": { + "Ref": "ContainerPort" + } + } + ] + } + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "ECSTaskRole", + "Arn" + ] + }, + "Volumes": [ + { + "Name": "default" + } + ] + }, + "Type": "AWS::ECS::TaskDefinition" + } + } +}