--- Parameters: EnvironmentName: Type: String Description: Environment name that joins all the stacks AppMeshMeshName: Type: String Description: Name of mesh EnvoyImageName: Type: String Description: The image to use for the Envoy container ColorTellerImageName: Description: The name for the color teller image Type: String Metadata: cfn-lint: config: ignore_checks: - E3012 - E3002 Resources: ColorTellerServiceDiscoveryRecord: Type: 'AWS::ServiceDiscovery::Service' Properties: Name: "colorteller" DnsConfig: NamespaceId: 'Fn::ImportValue': !Sub "${EnvironmentName}:ECSServiceDiscoveryNamespace" DnsRecords: - Type: A TTL: 300 HealthCheckCustomConfig: FailureThreshold: 1 ColorTellerTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: RequiresCompatibilities: - 'FARGATE' Family: 'colorteller' NetworkMode: 'awsvpc' Cpu: 256 Memory: 512 TaskRoleArn: 'Fn::ImportValue': !Sub "${EnvironmentName}:TaskIamRoleArn" ExecutionRoleArn: 'Fn::ImportValue': !Sub "${EnvironmentName}:TaskExecutionIamRoleArn" ProxyConfiguration: Type: 'APPMESH' ContainerName: 'envoy' ProxyConfigurationProperties: - Name: 'IgnoredUID' Value: '1337' - Name: 'ProxyIngressPort' Value: '15000' - Name: 'ProxyEgressPort' Value: '15001' - Name: 'AppPorts' Value: '9080' - Name: 'EgressIgnoredIPs' Value: '169.254.170.2,169.254.169.254' ContainerDefinitions: - Name: 'app' Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ColorTellerImageName}' Essential: true DependsOn: - ContainerName: 'envoy' Condition: 'HEALTHY' LogConfiguration: LogDriver: 'awslogs' Options: awslogs-group: Fn::ImportValue: !Sub "${EnvironmentName}:ECSServiceLogGroup" awslogs-region: !Ref AWS::Region awslogs-stream-prefix: 'colorteller' PortMappings: - ContainerPort: 9080 Protocol: 'http' Environment: - Name: 'PORT' Value: 9080 - Name: 'COLOR' Value: 'yellow' - Name: envoy Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EnvoyImageName}' Essential: true User: '1337' Ulimits: - Name: "nofile" HardLimit: 15000 SoftLimit: 15000 PortMappings: - ContainerPort: 9901 Protocol: 'tcp' HealthCheck: Command: - 'CMD-SHELL' - 'curl -s http://localhost:9901/server_info | grep state | grep -q LIVE' Interval: 5 Timeout: 2 Retries: 3 LogConfiguration: LogDriver: 'awslogs' Options: awslogs-group: Fn::ImportValue: !Sub "${EnvironmentName}:ECSServiceLogGroup" awslogs-region: !Ref AWS::Region awslogs-stream-prefix: 'colorteller' Environment: - Name: 'APPMESH_RESOURCE_ARN' Value: !Sub 'mesh/${AppMeshMeshName}/virtualNode/colorteller-vn' - Name: AWS_REGION Value: !Ref 'AWS::Region' - Name: ENVOY_LOG_LEVEL Value: warning # Set secret environment variable for the container Secrets: - Name: CertSecret ValueFrom: {'Fn::ImportValue': !Sub "${EnvironmentName}:SecretCertArn"} ColorTellerService: Type: 'AWS::ECS::Service' Properties: Cluster: 'Fn::ImportValue': !Sub "${EnvironmentName}:ECSCluster" DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 100 DesiredCount: 1 LaunchType: FARGATE ServiceRegistries: - RegistryArn: 'Fn::GetAtt': ColorTellerServiceDiscoveryRecord.Arn NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED SecurityGroups: - 'Fn::ImportValue': !Sub "${EnvironmentName}:ECSServiceSecurityGroup" Subnets: - 'Fn::ImportValue': !Sub "${EnvironmentName}:PrivateSubnet1" - 'Fn::ImportValue': !Sub "${EnvironmentName}:PrivateSubnet2" TaskDefinition: { Ref: ColorTellerTaskDefinition } GatewayServiceDiscoveryRecord: Type: 'AWS::ServiceDiscovery::Service' Properties: Name: "gateway" DnsConfig: NamespaceId: 'Fn::ImportValue': !Sub "${EnvironmentName}:ECSServiceDiscoveryNamespace" DnsRecords: - Type: A TTL: 300 HealthCheckCustomConfig: FailureThreshold: 1 GatewayTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: RequiresCompatibilities: - 'FARGATE' Family: 'gateway' NetworkMode: 'awsvpc' Cpu: 256 Memory: 512 TaskRoleArn: 'Fn::ImportValue': !Sub "${EnvironmentName}:TaskIamRoleArn" ExecutionRoleArn: 'Fn::ImportValue': !Sub "${EnvironmentName}:TaskExecutionIamRoleArn" ContainerDefinitions: - Name: envoy Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EnvoyImageName}' Essential: true User: '1337' Ulimits: - Name: "nofile" HardLimit: 15000 SoftLimit: 15000 PortMappings: - ContainerPort: 9080 Protocol: 'tcp' - ContainerPort: 9901 Protocol: 'tcp' HealthCheck: Command: - 'CMD-SHELL' - 'curl -s http://localhost:9901/server_info | grep state | grep -q LIVE' Interval: 5 Timeout: 2 Retries: 3 LogConfiguration: LogDriver: 'awslogs' Options: awslogs-group: Fn::ImportValue: !Sub "${EnvironmentName}:ECSServiceLogGroup" awslogs-region: !Ref AWS::Region awslogs-stream-prefix: 'gateway' Environment: - Name: 'APPMESH_RESOURCE_ARN' Value: !Sub 'mesh/${AppMeshMeshName}/virtualGateway/gateway-vgw' - Name: AWS_REGION Value: !Ref 'AWS::Region' - Name: ENVOY_LOG_LEVEL Value: warning # Set secret environment variable for the container Secrets: - Name: CertSecret ValueFrom: {'Fn::ImportValue': !Sub "${EnvironmentName}:SecretCertArn"} GatewayService: Type: 'AWS::ECS::Service' DependsOn: - PublicLoadBalancer Properties: Cluster: 'Fn::ImportValue': !Sub "${EnvironmentName}:ECSCluster" DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 100 DesiredCount: 1 LaunchType: FARGATE ServiceRegistries: - RegistryArn: 'Fn::GetAtt': GatewayServiceDiscoveryRecord.Arn NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED SecurityGroups: - 'Fn::ImportValue': !Sub "${EnvironmentName}:ECSServiceSecurityGroup" Subnets: - 'Fn::ImportValue': !Sub "${EnvironmentName}:PrivateSubnet1" - 'Fn::ImportValue': !Sub "${EnvironmentName}:PrivateSubnet2" LoadBalancers: - ContainerName: envoy ContainerPort: 9080 TargetGroupArn: !Ref WebTargetGroup TaskDefinition: { Ref: GatewayTaskDefinition } # public NLB for gateway PublicLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing Type: network Subnets: - { 'Fn::ImportValue': !Sub "${EnvironmentName}:PublicSubnet1" } - { 'Fn::ImportValue': !Sub "${EnvironmentName}:PublicSubnet2" } WebTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup DependsOn: - PublicLoadBalancer Properties: Protocol: TCP TargetType: ip Name: !Sub "${EnvironmentName}-web2" Port: 80 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 120 VpcId: 'Fn::ImportValue': !Sub "${EnvironmentName}:VPC" PublicLoadBalancerListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - TargetGroupArn: !Ref WebTargetGroup Type: 'forward' LoadBalancerArn: !Ref 'PublicLoadBalancer' Port: 80 Protocol: TCP CertExpirationEvent: Type: AWS::Events::Rule Properties: Description: "AWS ACM Certificate Expiration Event for Color Teller and Color Gateway" EventPattern: source: - aws.acm resources: - 'Fn::ImportValue': !Sub "${EnvironmentName}:AcmPcaColorTellerEndpointCertArn" - 'Fn::ImportValue': !Sub "${EnvironmentName}:AcmPcaColorGatewayEndpointCertArn" detail-type: - ACM Certificate Approaching Expiration State: "ENABLED" Targets: - Arn: !GetAtt CertRotateFunction.Arn Id: "TargetFunctionV1" PermissionForEventsToInvokeLambda: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref CertRotateFunction Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: !GetAtt CertExpirationEvent.Arn SourceAccount: !Ref 'AWS::AccountId' CertRotateRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: [lambda.amazonaws.com] Action: sts:AssumeRole ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' Policies: - PolicyName: acm-secretsmanager-access PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - acm:ExportCertificate - acm:RenewCertificate Resource: - 'Fn::ImportValue': !Sub "${EnvironmentName}:AcmPcaColorGatewayEndpointCertArn" - 'Fn::ImportValue': !Sub "${EnvironmentName}:AcmPcaColorTellerEndpointCertArn" - Effect: Allow Action: - secretsmanager:GetRandomPassword Resource: '*' - Effect: Allow Action: secretsmanager:PutSecretValue Resource: {'Fn::ImportValue': !Sub "${EnvironmentName}:SecretCertArn"} - Effect: Allow Action: ecs:UpdateService Resource: - !Ref GatewayService - !Ref ColorTellerService CertRotateFunction: Type: AWS::Lambda::Function Properties: Description: Initial function to populate secrets manager from ACM Handler: index.lambda_handler Role: !GetAtt CertRotateRole.Arn Runtime: python3.8 Timeout: 900 Environment: Variables: COLOR_GATEWAY_ACM_ARN: {'Fn::ImportValue': !Sub "${EnvironmentName}:AcmPcaColorGatewayEndpointCertArn"} COLOR_TELLER_ACM_PCA_ARN: {'Fn::ImportValue': !Sub "${EnvironmentName}:AcmPcaColorTellerRootCAArn"} COLOR_GATEWAY_ACM_PCA_ARN: {'Fn::ImportValue': !Sub "${EnvironmentName}:AcmPcaColorGatewayRootCAArn"} COLOR_TELLER_ACM_ARN: {'Fn::ImportValue': !Sub "${EnvironmentName}:AcmPcaColorTellerEndpointCertArn"} CLUSTER: {'Fn::ImportValue': !Sub "${EnvironmentName}:ECSCluster"} SVC_TELLER: !Ref GatewayService SVC_GATEWAY: !Ref ColorTellerService SECRET: {'Fn::ImportValue': !Sub "${EnvironmentName}:SecretCertArn"} Code: ZipFile: | import json import boto3 import base64 import time import os sm = boto3.client('secretsmanager') cm = boto3.client('acm') ecs = boto3.client('ecs') ecs_cluster = os.environ['CLUSTER'] color_gateway_svc = os.environ['SVC_GATEWAY'] color_teller_svc = os.environ['SVC_TELLER'] gate_cm = os.environ['COLOR_GATEWAY_ACM_ARN'] teller_cm = os.environ['COLOR_TELLER_ACM_ARN'] secret = os.environ['SECRET'] def lambda_handler(event, context): print (json.dumps(event)) cm.renew_certificate(CertificateArn=teller_cm) cm.renew_certificate(CertificateArn=gate_cm) time.sleep(5) # allow time for acm to renew cert from acm-pca passphrase = sm.get_random_password(ExcludePunctuation=True)['RandomPassword'] passphrase_enc = base64.b64encode(passphrase.encode('utf-8')) cm.export_certificate(CertificateArn=teller_cm, Passphrase=passphrase_enc) gate_rsp = cm.export_certificate(CertificateArn=gate_cm, Passphrase=passphrase_enc) sm_value={} sm_value['GatewayCertificate']=gate_rsp['Certificate'] sm_value['GatewayCertificateChain']=gate_rsp['CertificateChain'] sm_value['GatewayPrivateKey']=gate_rsp['PrivateKey'] sm_value['Passphrase']=passphrase sm.put_secret_value(SecretId=secret, SecretString=json.dumps(sm_value)) ecs.update_service( cluster=ecs_cluster, service=color_teller_svc, forceNewDeployment=True) ecs.update_service( cluster=ecs_cluster, service=color_gateway_svc, forceNewDeployment=True) Outputs: ColorAppEndpoint: Description: Public endpoint for Color App service Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']]