Used AWS Copilot to Generate a Highly Available, Durable ECS Cluster Environment
In this blog article, I will explore how AWS Copilot enables me to effortlessly create and deploy a robust 3-tier environment for a WordPress application on Amazon Elastic Container Service (ECS) with AWS Fargate. The architecture includes Elasticache Redis, Aurora DB for MySQL, Amazon EFS for the application’s file system, and an Application Load Balancer (ALB) to distribute traffic. Additionally, I will utilize various AWS services like Route 53 for DNS, CloudFront for CDN, ACM for SSL certificates, and CloudWatch for monitoring. AWS Copilot will automate the deployment process through CodePipeline and manage container resources in Amazon Elastic Container Registry (ECR) efficiently. MySQL credentials will be securely stored in Secrets Manager, and Redis will operate in a multi-AZ environment with a Primary and Replica node.
AWS Copilot Introduction:
AWS Copilot is an open-source command-line interface (CLI) tool that simplifies the deployment and management of containerized applications on AWS. It abstracts the complexities of infrastructure provisioning, deployment, scaling, and monitoring, making it an ideal choice for efficiently managing a 3-tier environment.
Setting up the Environment:
First, I set up the environment using AWS Copilot CLI. I create a new application and service, specifying that I want to use ECS and Fargate. Copilot will handle the ECS cluster and task definition creation automatically.
Deploying WordPress on ECS and Fargate:
Using the Copilot CLI, I deploy the WordPress application as a service on ECS with Fargate. Copilot generates the necessary task definitions and launches containers for the application.
Using Elasticache Redis:
I integrate Elasticache Redis into the WordPress application to enhance performance and caching capabilities. Copilot helps configure the Redis cluster, including setting up a Redis Primary node complemented by a Redis Replica in another Availability Zone (AZ) for high availability.
Aurora MySQL Database:
Next, I set up an Aurora MySQL database using Copilot, creating a Primary DB in one AZ and a Standby DB in another AZ for redundancy and disaster recovery.
Amazon EFS for File Storage:
Copilot enables me to utilize Amazon EFS as the shared file storage for our WordPress application, allowing seamless file access from multiple ECS tasks and ensuring data persistence.
Application Load Balancer (ALB):
I leverage an ALB in front of the ECS service to distribute incoming traffic across multiple containers and ensure high availability.
Route 53 and CloudFront Integration:
Copilot facilitates integrating Route 53 for DNS management and CloudFront for content delivery, providing global availability and accelerated content delivery through CDN.
ACM for SSL Certificates:
Using ACM, I automatically generate and manage SSL certificates to secure communications between the clients and the WordPress application.
Auto Scaling with Spot Instances:
With Copilot, I configure auto scaling for the ECS service, utilizing a mix of on-demand and spot instances. The scaling group will maintain a minimum of 1 to 4 instances, with 2-4 of them being spot instances when available. When spot instances are not available, additional standard instances will supplement the cluster for high availability until spot instances are available.
CloudWatch Metrics Activation:
Copilot automatically activates CloudWatch metrics, allowing me to monitor the performance of our ECS tasks, services, and containers.
CodePipeline for Automation:
AWS Copilot simplifies the CI/CD process by generating a CodePipeline. This pipeline automates the workflow, building Docker images, pushing them to ECR, and deploying the changes to the ECS environment.
MySQL Credentials in Secrets Manager:
Copilot ensures the secure management of MySQL credentials by storing them in AWS Secrets Manager, adding an extra layer of protection to sensitive data.
Conclusion:
By utilizing AWS Copilot, I were able to create a highly scalable and reliable 3-tier environment for our WordPress application. The combination of ECS, Fargate, Elasticache Redis, Aurora MySQL, Amazon EFS, ALB, Route 53, CloudFront, and ACM provided me with a robust and performant architecture. With Copilot handling the automation and deployment processes, I could focus more on the application’s functionality and less on managing the underlying infrastructure. AWS Copilot proved to be a powerful tool in simplifying the entire deployment journey and streamlining the maintenance of our AWS resources.
Dockerfile
code
FROM ubuntu:latest as installer RUN apt-get update && apt-get install curl --yes RUN curl -Lo /usr/local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 RUN chmod +0755 /usr/local/bin/jq FROM public.ecr.aws/bitnami/wordpress:latest as app COPY --from=installer /usr/local/bin/jq /usr/bin/jq COPY startup.sh /opt/copilot/scripts/startup.sh ENTRYPOINT ["/bin/sh", "-c"] CMD ["/opt/copilot/scripts/startup.sh"] EXPOSE 8080
Startup.sh
code
#!/bin/bash # Exit if the secret wasn't populated by the ECS agent [ -z $WP_SECRET ] && echo "Secret WP_SECRET not populated in environment" && exit 1 export WORDPRESS_DATABASE_HOST=`echo $WP_SECRET | jq -r '.host'` export WORDPRESS_DATABASE_PORT_NUMBER=`echo $WP_SECRET | jq -r '.port'` export WORDPRESS_DATABASE_NAME=`echo $WP_SECRET | jq -r '.dbname'` export WORDPRESS_DATABASE_USER=`echo $WP_SECRET | jq -r '.username'` export WORDPRESS_DATABASE_PASSWORD=`echo $WP_SECRET | jq -r '.password'` /opt/bitnami/scripts/wordpress/entrypoint.sh /opt/bitnami/scripts/apache/run.sh
Manifest.yml
code
# The manifest for the "fe" service. # Read the full specification for the "Load Balanced Web Service" type at: # https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/ # Your service name will be used in naming your resources like log groups, ECS services, etc. name: fe type: Load Balanced Web Service # Distribute traffic to your service. http: # Requests to this path will be forwarded to your service. # To match all requests you can use the "/" path. path: '/' # You can specify a custom health check path. The default is "/". healthcheck: path: / success_codes: '200-399' interval: 60s timeout: 5s healthy_threshold: 3 unhealthy_threshold: 5 stickiness: true # Configuration for your containers and service. image: build: Dockerfile # Port exposed through your container to route traffic to it. port: 8080 cpu: 512 # Number of CPU units for the task. memory: 1024 # Amount of memory in MiB used by the task. count: # Number of tasks that should be running in your service. range: min: 1 max: 4 spot_from: 3 cpu_percentage: 75 exec: true # Enable running commands in your container. storage: volumes: wpUserData: path: /bitnami/wordpress read_only: false efs: true variables: MYSQL_CLIENT_FLAVOR: mysql WORDPRESS_BLOG_NAME: TEST APP
Wp.yml (AWS Copilot addon CloudFormation template)
code
Parameters: App: Type: String Description: Your application's name. Env: Type: String Description: The environment name your service, job, or workflow is being deployed to. Name: Type: String Description: The name of the service, job, or workflow being deployed. # Customize your Aurora Serverless cluster by setting the default value of the following parameters. wpDBName: Type: String Description: The name of the initial database to be created in the DB cluster. Default: main # Cannot have special characters # Naming constraints: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints wpDBAutoPauseSeconds: Type: Number Description: The duration in seconds before the cluster pauses. Default: 1000 Mappings: wpEnvScalingConfigurationMap: test: "DBMinCapacity": 1 # AllowedValues: [1, 2, 4, 8, 16, 32, 64, 128, 256] "DBMaxCapacity": 8 # AllowedValues: [1, 2, 4, 8, 16, 32, 64, 128, 256] prod: "DBMinCapacity": 1 # AllowedValues: [1, 2, 4, 8, 16, 32, 64, 128, 256] "DBMaxCapacity": 8 # AllowedValues: [1, 2, 4, 8, 16, 32, 64, 128, 256] Resources: wpDBSubnetGroup: Type: 'AWS::RDS::DBSubnetGroup' Properties: DBSubnetGroupDescription: Group of Copilot private subnets for Aurora cluster. SubnetIds: !Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' }] wpSecurityGroup: Metadata: 'aws:copilot:description': 'A security group for your workload to access the DB cluster wp' Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: !Sub 'The Security Group for ${Name} to access DB cluster wp.' VpcId: Fn::ImportValue: !Sub '${App}-${Env}-VpcId' Tags: - Key: Name Value: !Sub 'copilot-${App}-${Env}-${Name}-Aurora' wpDBClusterSecurityGroup: Metadata: 'aws:copilot:description': 'A security group for the database cluster.' Type: AWS::EC2::SecurityGroup Properties: GroupDescription: The Security Group for the database cluster. SecurityGroupIngress: - ToPort: 3306 FromPort: 3306 IpProtocol: tcp Description: !Sub 'From the Aurora Security Group of the workload ${Name}.' SourceSecurityGroupId: !Ref wpSecurityGroup VpcId: Fn::ImportValue: !Sub '${App}-${Env}-VpcId' wpAuroraSecret: Metadata: 'aws:copilot:description': 'A secret to hold information about the database' Type: AWS::SecretsManager::Secret Properties: Description: !Sub Aurora main user secret for ${AWS::StackName} GenerateSecretString: SecretStringTemplate: '{"username": "admin"}' GenerateStringKey: "password" ExcludePunctuation: true IncludeSpace: false PasswordLength: 16 wpDBClusterParameterGroup: Type: 'AWS::RDS::DBClusterParameterGroup' Properties: Description: !Ref 'AWS::StackName' Family: 'aurora-mysql5.7' Parameters: character_set_client: 'utf8' wpDBCluster: Metadata: 'aws:copilot:description': 'A serverless Aurora cluster with autoscaling and autopause.' Type: 'AWS::RDS::DBCluster' Properties: MasterUsername: !Join [ "", [ '{{resolve:secretsmanager:', !Ref wpAuroraSecret, ":SecretString:username}}" ]] MasterUserPassword: !Join [ "", [ '{{resolve:secretsmanager:', !Ref wpAuroraSecret, ":SecretString:password}}" ]] DatabaseName: !Ref wpDBName Engine: 'aurora-mysql' EngineVersion: '5.7.mysql_aurora.2.07.1' EngineMode: serverless DBClusterParameterGroupName: !Ref wpDBClusterParameterGroup DBSubnetGroupName: !Ref wpDBSubnetGroup VpcSecurityGroupIds: - !Ref wpDBClusterSecurityGroup ScalingConfiguration: AutoPause: true MinCapacity: !FindInMap [wpEnvScalingConfigurationMap, !Ref Env, DBMinCapacity] MaxCapacity: !FindInMap [wpEnvScalingConfigurationMap, !Ref Env, DBMaxCapacity] SecondsUntilAutoPause: !Ref wpDBAutoPauseSeconds wpSecretAuroraClusterAttachment: Type: AWS::SecretsManager::SecretTargetAttachment Properties: SecretId: !Ref wpAuroraSecret TargetId: !Ref wpDBCluster TargetType: AWS::RDS::DBCluster Outputs: wpSecret: # injected as WP_SECRET environment variable by Copilot. Description: "The JSON secret that holds the database username and password. Fields are 'host', 'port', 'dbname', 'username', 'password', 'dbClusterIdentifier' and 'engine'" Value: !Ref wpAuroraSecret wpSecurityGroup: Description: "The security group to attach to the workload." Value: !Ref wpSecurityGroup
Redis.yml (AWS Copilot addon CloudFormation template)
code
Parameters: App: Type: String Description: Your application's name. Env: Type: String Description: The environment name your service, job, or workflow is being deployed to. Name: Type: String Description: The name of the service, job, or workflow being deployed. Resources: # Subnet group to control where the Redis gets placed RedisSubnetGroup: Type: AWS::ElastiCache::SubnetGroup Properties: Description: Group of subnets to place Redis into SubnetIds: !Split [ ',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' } ] # Security group to add the Redis cluster to the VPC, # and to allow the Fargate containers to talk to Redis on port 6379 RedisSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "Redis Security Group" VpcId: { 'Fn::ImportValue': !Sub '${App}-${Env}-VpcId' } # Enable ingress from other ECS services created within the environment. RedisIngress: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Ingress from Fargate containers GroupId: !Ref 'RedisSecurityGroup' IpProtocol: tcp FromPort: 6379 ToPort: 6379 SourceSecurityGroupId: { 'Fn::ImportValue': !Sub '${App}-${Env}-EnvironmentSecurityGroup' } # The multi-az 2 node cluster itself. Redis: Type: AWS::ElastiCache::CacheCluster Properties: Engine: redis CacheNodeType: cache.t2.micro NumCacheNodes: 2 AZMode: cross-az MultiAZEnabled: true CacheSubnetGroupName: !Ref 'RedisSubnetGroup' VpcSecurityGroupIds: - !GetAtt 'RedisSecurityGroup.GroupId' # Redis endpoint stored in SSM so that other services can retrieve the endpoint. RedisEndpointAddressParam: Type: AWS::SSM::Parameter Properties: Name: !Sub '/${App}/${Env}/${Name}/redis' # Other services can retrieve the endpoint from this path. Type: String Value: !GetAtt 'Redis.RedisEndpoint.Address' Outputs: RedisEndpoint: Description: The endpoint of the redis cluster Value: !GetAtt 'Redis.RedisEndpoint.Address'
CloudFormation Package YAML Template (AWS Copilot’s Template Generated After Deployment)
code
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 Description: CloudFormation environment template for infrastructure shared among Copilot workloads. Metadata: Version: v1.13.0 Manifest: | # The manifest for the "test" environment. # Read the full specification for the "Environment" type at: # https://aws.github.io/copilot-cli/docs/manifest/environment/ # Your environment name will be used in naming your resources like VPC, cluster, etc. name: test type: Environment # Import your own VPC and subnets or configure how they should be created. # network: # vpc: # id: # Configure the load balancers in your environment, once created. # http: # public: # private: # Configure observability for your environment resources. observability: container_insights: false Parameters: AppName: Type: String EnvironmentName: Type: String ALBWorkloads: Type: String InternalALBWorkloads: Type: String EFSWorkloads: Type: String NATWorkloads: Type: String AppRunnerPrivateWorkloads: Type: String ToolsAccountPrincipalARN: Type: String AppDNSName: Type: String AppDNSDelegationRole: Type: String Aliases: Type: String CreateHTTPSListener: Type: String AllowedValues: [true, false] CreateInternalHTTPSListener: Type: String AllowedValues: [true, false] ServiceDiscoveryEndpoint: Type: String Conditions: CreateALB: !Not [!Equals [ !Ref ALBWorkloads, "" ]] CreateInternalALB: !Not [!Equals [ !Ref InternalALBWorkloads, "" ]] DelegateDNS: !Not [!Equals [ !Ref AppDNSName, "" ]] ExportHTTPSListener: !And - !Condition CreateALB - !Equals [ !Ref CreateHTTPSListener, true ] ExportInternalHTTPSListener: !And - !Condition CreateInternalALB - !Equals [ !Ref CreateInternalHTTPSListener, true ] CreateEFS: !Not [!Equals [ !Ref EFSWorkloads, ""]] CreateNATGateways: !Not [!Equals [ !Ref NATWorkloads, ""]] CreateAppRunnerVPCEndpoint: !Not [!Equals [ !Ref AppRunnerPrivateWorkloads, ""]] ManagedAliases: !And - !Condition DelegateDNS - !Not [!Equals [ !Ref Aliases, "" ]] Resources: # The CloudformationExecutionRole definition must be immediately followed with DeletionPolicy: Retain. # See #1533. CloudformationExecutionRole: Metadata: 'aws:copilot:description': 'An IAM Role for AWS CloudFormation to manage resources' DeletionPolicy: Retain Type: AWS::IAM::Role Properties: RoleName: !Sub ${AWS::StackName}-CFNExecutionRole AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - 'cloudformation.amazonaws.com' Action: sts:AssumeRole Path: / Policies: - PolicyName: executeCfn # This policy is more permissive than the managed PowerUserAccess # since it allows arbitrary role creation, which is needed for the # ECS task role specified by the customers. PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow NotAction: - 'organizations:*' - 'account:*' Resource: '*' - Effect: Allow Action: - 'organizations:DescribeOrganization' - 'account:ListRegions' Resource: '*' EnvironmentManagerRole: Metadata: 'aws:copilot:description': 'An IAM Role to describe resources in your environment' DeletionPolicy: Retain Type: AWS::IAM::Role DependsOn: CloudformationExecutionRole Properties: RoleName: !Sub ${AWS::StackName}-EnvManagerRole AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} Action: sts:AssumeRole Path: / Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Sid: CloudwatchLogs Effect: Allow Action: [ "logs:GetLogRecord", "logs:GetQueryResults", "logs:StartQuery", "logs:GetLogEvents", "logs:DescribeLogStreams", "logs:StopQuery", "logs:TestMetricFilter", "logs:FilterLogEvents", "logs:GetLogGroupFields", "logs:GetLogDelivery" ] Resource: "*" - Sid: Cloudwatch Effect: Allow Action: [ "cloudwatch:DescribeAlarms" ] Resource: "*" - Sid: ECS Effect: Allow Action: [ "ecs:ListAttributes", "ecs:ListTasks", "ecs:DescribeServices", "ecs:DescribeTaskSets", "ecs:ListContainerInstances", "ecs:DescribeContainerInstances", "ecs:DescribeTasks", "ecs:DescribeClusters", "ecs:UpdateService", "ecs:PutAttributes", "ecs:StartTelemetrySession", "ecs:StartTask", "ecs:StopTask", "ecs:ListServices", "ecs:ListTaskDefinitionFamilies", "ecs:DescribeTaskDefinition", "ecs:ListTaskDefinitions", "ecs:ListClusters", "ecs:RunTask" ] Resource: "*" - Sid: ExecuteCommand Effect: Allow Action: [ "ecs:ExecuteCommand" ] Resource: "*" Condition: StringEquals: 'aws:ResourceTag/copilot-application': !Sub '${AppName}' 'aws:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' - Sid: StartStateMachine Effect: Allow Action: - "states:StartExecution" - "states:DescribeStateMachine" Resource: - !Sub "arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${AppName}-${EnvironmentName}-*" - Sid: CloudFormation Effect: Allow Action: [ "cloudformation:CancelUpdateStack", "cloudformation:CreateChangeSet", "cloudformation:CreateStack", "cloudformation:DeleteChangeSet", "cloudformation:DeleteStack", "cloudformation:Describe*", "cloudformation:DetectStackDrift", "cloudformation:DetectStackResourceDrift", "cloudformation:ExecuteChangeSet", "cloudformation:GetTemplate", "cloudformation:GetTemplateSummary", "cloudformation:UpdateStack", "cloudformation:UpdateTerminationProtection" ] Resource: "*" - Sid: GetAndPassCopilotRoles Effect: Allow Action: [ "iam:GetRole", "iam:PassRole" ] Resource: "*" Condition: StringEquals: 'iam:ResourceTag/copilot-application': !Sub '${AppName}' 'iam:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' - Sid: ECR Effect: Allow Action: [ "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability", "ecr:CompleteLayerUpload", "ecr:DescribeImages", "ecr:DescribeRepositories", "ecr:GetDownloadUrlForLayer", "ecr:InitiateLayerUpload", "ecr:ListImages", "ecr:ListTagsForResource", "ecr:PutImage", "ecr:UploadLayerPart", "ecr:GetAuthorizationToken" ] Resource: "*" - Sid: ResourceGroups Effect: Allow Action: [ "resource-groups:GetGroup", "resource-groups:GetGroupQuery", "resource-groups:GetTags", "resource-groups:ListGroupResources", "resource-groups:ListGroups", "resource-groups:SearchResources" ] Resource: "*" - Sid: SSM Effect: Allow Action: [ "ssm:DeleteParameter", "ssm:DeleteParameters", "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath" ] Resource: "*" - Sid: SSMSecret Effect: Allow Action: [ "ssm:PutParameter", "ssm:AddTagsToResource" ] Resource: - !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/copilot/${AppName}/${EnvironmentName}/secrets/*' - Sid: ELBv2 Effect: Allow Action: [ "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeSSLPolicies", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeTargetGroupAttributes", "elasticloadbalancing:DescribeListeners", "elasticloadbalancing:DescribeTags", "elasticloadbalancing:DescribeTargetHealth", "elasticloadbalancing:DescribeTargetGroups", "elasticloadbalancing:DescribeRules" ] Resource: "*" - Sid: BuiltArtifactAccess Effect: Allow Action: [ "s3:ListBucketByTags", "s3:GetLifecycleConfiguration", "s3:GetBucketTagging", "s3:GetInventoryConfiguration", "s3:GetObjectVersionTagging", "s3:ListBucketVersions", "s3:GetBucketLogging", "s3:ListBucket", "s3:GetAccelerateConfiguration", "s3:GetBucketPolicy", "s3:GetObjectVersionTorrent", "s3:GetObjectAcl", "s3:GetEncryptionConfiguration", "s3:GetBucketRequestPayment", "s3:GetObjectVersionAcl", "s3:GetObjectTagging", "s3:GetMetricsConfiguration", "s3:HeadBucket", "s3:GetBucketPublicAccessBlock", "s3:GetBucketPolicyStatus", "s3:ListBucketMultipartUploads", "s3:GetBucketWebsite", "s3:ListJobs", "s3:GetBucketVersioning", "s3:GetBucketAcl", "s3:GetBucketNotification", "s3:GetReplicationConfiguration", "s3:ListMultipartUploadParts", "s3:GetObject", "s3:GetObjectTorrent", "s3:GetAccountPublicAccessBlock", "s3:ListAllMyBuckets", "s3:DescribeJob", "s3:GetBucketCORS", "s3:GetAnalyticsConfiguration", "s3:GetObjectVersionForReplication", "s3:GetBucketLocation", "s3:GetObjectVersion", "kms:Decrypt" ] Resource: "*" - Sid: PutObjectsToArtifactBucket Effect: Allow Action: - s3:PutObject - s3:PutObjectAcl Resource: - arn:aws:s3:::stackset-wordpress-infra-pipelinebuiltartifactbuc-k48b94r6jl3h - arn:aws:s3:::stackset-wordpress-infra-pipelinebuiltartifactbuc-k48b94r6jl3h/* - Sid: EncryptObjectsInArtifactBucket Effect: Allow Action: - kms:GenerateDataKey Resource: arn:aws:kms:us-east-1:723540195465:key/60b8207f-f381-4430-8e8e-6861de15b9c0 - Sid: EC2 Effect: Allow Action: [ "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeNetworkInterfaces", "ec2:DescribeRouteTables" ] Resource: "*" - Sid: AppRunner Effect: Allow Action: [ "apprunner:DescribeService", "apprunner:ListOperations", "apprunner:ListServices", "apprunner:PauseService", "apprunner:ResumeService", "apprunner:StartDeployment", "apprunner:DescribeObservabilityConfiguration", "apprunner:DescribeVpcIngressConnection" ] Resource: "*" - Sid: Tags Effect: Allow Action: [ "tag:GetResources" ] Resource: "*" - Sid: ApplicationAutoscaling Effect: Allow Action: [ "application-autoscaling:DescribeScalingPolicies" ] Resource: "*" - Sid: DeleteRoles Effect: Allow Action: [ "iam:DeleteRole", "iam:ListRolePolicies", "iam:DeleteRolePolicy" ] Resource: - !GetAtt CloudformationExecutionRole.Arn - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AWS::StackName}-EnvManagerRole" - Sid: DeleteEnvStack Effect: Allow Action: - 'cloudformation:DescribeStacks' - 'cloudformation:DeleteStack' Resource: - !Sub 'arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*' VPC: Metadata: 'aws:copilot:description': 'A Virtual Private Cloud to control networking of your AWS resources' Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsHostnames: true EnableDnsSupport: true InstanceTenancy: default Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}' PublicRouteTable: Metadata: 'aws:copilot:description': "A custom route table that directs network traffic for the public subnets" Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}' DefaultPublicRoute: Type: AWS::EC2::Route DependsOn: InternetGatewayAttachment Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway InternetGateway: Metadata: 'aws:copilot:description': 'An Internet Gateway to connect to the public internet' Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}' InternetGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGateway VpcId: !Ref VPC PublicSubnet1: Metadata: 'aws:copilot:description': 'Public subnet 1 for resources that can access the internet' Type: AWS::EC2::Subnet Properties: CidrBlock: 10.0.0.0/24 VpcId: !Ref VPC AvailabilityZone: !Select [ 0, !GetAZs '' ] MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-pub0' PublicSubnet2: Metadata: 'aws:copilot:description': 'Public subnet 2 for resources that can access the internet' Type: AWS::EC2::Subnet Properties: CidrBlock: 10.0.1.0/24 VpcId: !Ref VPC AvailabilityZone: !Select [ 1, !GetAZs '' ] MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-pub1' PrivateSubnet1: Metadata: 'aws:copilot:description': 'Private subnet 1 for resources with no internet access' Type: AWS::EC2::Subnet Properties: CidrBlock: 10.0.2.0/24 VpcId: !Ref VPC AvailabilityZone: !Select [ 0, !GetAZs '' ] MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-priv0' PrivateSubnet2: Metadata: 'aws:copilot:description': 'Private subnet 2 for resources with no internet access' Type: AWS::EC2::Subnet Properties: CidrBlock: 10.0.3.0/24 VpcId: !Ref VPC AvailabilityZone: !Select [ 1, !GetAZs '' ] MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-priv1' PublicSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet1 PublicSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet2 NatGateway1Attachment: Metadata: 'aws:copilot:description': 'An Elastic IP for NAT Gateway 1' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment Properties: Domain: vpc NatGateway1: Metadata: 'aws:copilot:description': 'NAT Gateway 1 enabling workloads placed in private subnet 1 to reach the internet' Type: AWS::EC2::NatGateway Condition: CreateNATGateways Properties: AllocationId: !GetAtt NatGateway1Attachment.AllocationId SubnetId: !Ref PublicSubnet1 Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-0' PrivateRouteTable1: Type: AWS::EC2::RouteTable Condition: CreateNATGateways Properties: VpcId: !Ref 'VPC' PrivateRoute1: Type: AWS::EC2::Route Condition: CreateNATGateways Properties: RouteTableId: !Ref PrivateRouteTable1 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway1 PrivateRouteTable1Association: Type: AWS::EC2::SubnetRouteTableAssociation Condition: CreateNATGateways Properties: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 NatGateway2Attachment: Metadata: 'aws:copilot:description': 'An Elastic IP for NAT Gateway 2' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment Properties: Domain: vpc NatGateway2: Metadata: 'aws:copilot:description': 'NAT Gateway 2 enabling workloads placed in private subnet 2 to reach the internet' Type: AWS::EC2::NatGateway Condition: CreateNATGateways Properties: AllocationId: !GetAtt NatGateway2Attachment.AllocationId SubnetId: !Ref PublicSubnet2 Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-1' PrivateRouteTable2: Type: AWS::EC2::RouteTable Condition: CreateNATGateways Properties: VpcId: !Ref 'VPC' PrivateRoute2: Type: AWS::EC2::Route Condition: CreateNATGateways Properties: RouteTableId: !Ref PrivateRouteTable2 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway2 PrivateRouteTable2Association: Type: AWS::EC2::SubnetRouteTableAssociation Condition: CreateNATGateways Properties: RouteTableId: !Ref PrivateRouteTable2 SubnetId: !Ref PrivateSubnet2 # Creates a service discovery namespace with the form provided in the parameter. # For new environments after 1.5.0, this is "env.app.local". For upgraded environments from # before 1.5.0, this is app.local. ServiceDiscoveryNamespace: Metadata: 'aws:copilot:description': 'A private DNS namespace for discovering services within the environment' Type: AWS::ServiceDiscovery::PrivateDnsNamespace Properties: Name: !Ref ServiceDiscoveryEndpoint Vpc: !Ref VPC Cluster: Metadata: 'aws:copilot:description': 'An ECS cluster to group your services' Type: AWS::ECS::Cluster Properties: CapacityProviders: ['FARGATE', 'FARGATE_SPOT'] Configuration: ExecuteCommandConfiguration: Logging: DEFAULT ClusterSettings: - Name: containerInsights Value: disabled PublicHTTPLoadBalancerSecurityGroup: Metadata: 'aws:copilot:description': 'A security group for your load balancer allowing HTTP traffic' Condition: CreateALB Type: AWS::EC2::SecurityGroup Properties: GroupDescription: HTTP access to the public facing load balancer SecurityGroupIngress: - CidrIp: 0.0.0.0/0 Description: Allow from anyone on port 80 FromPort: 80 IpProtocol: tcp ToPort: 80 VpcId: !Ref VPC Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-lb-http' PublicHTTPSLoadBalancerSecurityGroup: Metadata: 'aws:copilot:description': 'A security group for your load balancer allowing HTTPS traffic' Condition: ExportHTTPSListener Type: AWS::EC2::SecurityGroup Properties: GroupDescription: HTTPS access to the public facing load balancer SecurityGroupIngress: - CidrIp: 0.0.0.0/0 Description: Allow from anyone on port 443 FromPort: 443 IpProtocol: tcp ToPort: 443 VpcId: !Ref VPC Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-lb-https' InternalLoadBalancerSecurityGroup: Metadata: 'aws:copilot:description': 'A security group for your internal load balancer allowing HTTP traffic from within the VPC' Condition: CreateInternalALB Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Access to the internal load balancer VpcId: !Ref VPC Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-internal-lb' # Only accept requests coming from the public ALB, internal ALB, or other containers in the same security group. EnvironmentSecurityGroup: Metadata: 'aws:copilot:description': 'A security group to allow your containers to talk to each other' Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Join ['', [!Ref AppName, '-', !Ref EnvironmentName, EnvironmentSecurityGroup]] VpcId: !Ref VPC Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-env' EnvironmentHTTPSecurityGroupIngressFromPublicALB: Type: AWS::EC2::SecurityGroupIngress Condition: CreateALB Properties: Description: HTTP ingress from the public ALB GroupId: !Ref EnvironmentSecurityGroup IpProtocol: -1 SourceSecurityGroupId: !Ref PublicHTTPLoadBalancerSecurityGroup EnvironmentHTTPSSecurityGroupIngressFromPublicALB: Type: AWS::EC2::SecurityGroupIngress Condition: ExportHTTPSListener Properties: Description: HTTPS ingress from the public ALB GroupId: !Ref EnvironmentSecurityGroup IpProtocol: -1 SourceSecurityGroupId: !Ref PublicHTTPSLoadBalancerSecurityGroup EnvironmentSecurityGroupIngressFromInternalALB: Type: AWS::EC2::SecurityGroupIngress Condition: CreateInternalALB Properties: Description: Ingress from the internal ALB GroupId: !Ref EnvironmentSecurityGroup IpProtocol: -1 SourceSecurityGroupId: !Ref InternalLoadBalancerSecurityGroup EnvironmentSecurityGroupIngressFromSelf: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Ingress from other containers in the same security group GroupId: !Ref EnvironmentSecurityGroup IpProtocol: -1 SourceSecurityGroupId: !Ref EnvironmentSecurityGroup InternalALBIngressFromEnvironmentSecurityGroup: Type: AWS::EC2::SecurityGroupIngress Condition: CreateInternalALB Properties: Description: Ingress from the env security group GroupId: !Ref InternalLoadBalancerSecurityGroup IpProtocol: -1 SourceSecurityGroupId: !Ref EnvironmentSecurityGroup PublicLoadBalancer: Metadata: 'aws:copilot:description': 'An Application Load Balancer to distribute public traffic to your services' Condition: CreateALB Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing SecurityGroups: - !GetAtt PublicHTTPLoadBalancerSecurityGroup.GroupId - !If [ExportHTTPSListener, !GetAtt PublicHTTPSLoadBalancerSecurityGroup.GroupId, !Ref "AWS::NoValue"] Subnets: [ !Ref PublicSubnet1, !Ref PublicSubnet2, ] Type: application # Assign a dummy target group that with no real services as targets, so that we can create # the listeners for the services. DefaultHTTPTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: CreateALB Properties: # Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds. HealthCheckIntervalSeconds: 10 # Default is 30. HealthyThresholdCount: 2 # Default is 5. HealthCheckTimeoutSeconds: 5 Port: 80 Protocol: HTTP TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 # Default is 300. TargetType: ip VpcId: !Ref VPC HTTPListener: Metadata: 'aws:copilot:description': 'A load balancer listener to route HTTP traffic' Type: AWS::ElasticLoadBalancingV2::Listener Condition: CreateALB Properties: DefaultActions: - TargetGroupArn: !Ref DefaultHTTPTargetGroup Type: forward LoadBalancerArn: !Ref PublicLoadBalancer Port: 80 Protocol: HTTP HTTPSListener: Metadata: 'aws:copilot:description': 'A load balancer listener to route HTTPS traffic' Type: AWS::ElasticLoadBalancingV2::Listener Condition: ExportHTTPSListener Properties: Certificates: - CertificateArn: !Ref HTTPSCert DefaultActions: - TargetGroupArn: !Ref DefaultHTTPTargetGroup Type: forward LoadBalancerArn: !Ref PublicLoadBalancer Port: 443 Protocol: HTTPS InternalLoadBalancer: Metadata: 'aws:copilot:description': 'An internal Application Load Balancer to distribute private traffic from within the VPC to your services' Condition: CreateInternalALB Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internal SecurityGroups: [ !GetAtt InternalLoadBalancerSecurityGroup.GroupId ] Subnets: [ !Ref PrivateSubnet1, !Ref PrivateSubnet2, ] Type: application # Assign a dummy target group that with no real services as targets, so that we can create # the listeners for the services. DefaultInternalHTTPTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: CreateInternalALB Properties: # Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds. HealthCheckIntervalSeconds: 10 # Default is 30. HealthyThresholdCount: 2 # Default is 5. HealthCheckTimeoutSeconds: 5 Port: 80 Protocol: HTTP TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 # Default is 300. TargetType: ip VpcId: !Ref VPC InternalHTTPListener: Metadata: 'aws:copilot:description': 'An internal load balancer listener to route HTTP traffic' Type: AWS::ElasticLoadBalancingV2::Listener Condition: CreateInternalALB Properties: DefaultActions: - TargetGroupArn: !Ref DefaultInternalHTTPTargetGroup Type: forward LoadBalancerArn: !Ref InternalLoadBalancer Port: 80 Protocol: HTTP InternalHTTPSListener: Metadata: 'aws:copilot:description': 'An internal load balancer listener to route HTTPS traffic' Type: AWS::ElasticLoadBalancingV2::Listener Condition: ExportInternalHTTPSListener Properties: DefaultActions: - TargetGroupArn: !Ref DefaultInternalHTTPTargetGroup Type: forward LoadBalancerArn: !Ref InternalLoadBalancer Port: 443 Protocol: HTTPS InternalWorkloadsHostedZone: Metadata: 'aws:copilot:description': 'A hosted zone named test.wordpress.internal for backends behind a private load balancer' Condition: CreateInternalALB Type: AWS::Route53::HostedZone Properties: Name: !Sub ${EnvironmentName}.${AppName}.internal VPCs: - VPCId: !Ref VPC VPCRegion: !Ref AWS::Region FileSystem: Condition: CreateEFS Type: AWS::EFS::FileSystem Metadata: 'aws:copilot:description': 'An EFS filesystem for persistent task storage' Properties: BackupPolicy: Status: ENABLED Encrypted: true FileSystemPolicy: Version: '2012-10-17' Id: CopilotEFSPolicy Statement: - Sid: AllowIAMFromTaggedRoles Effect: Allow Principal: AWS: '*' Action: - elasticfilesystem:ClientWrite - elasticfilesystem:ClientMount Condition: Bool: 'elasticfilesystem:AccessedViaMountTarget': true StringEquals: 'iam:ResourceTag/copilot-application': !Sub '${AppName}' 'iam:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' - Sid: DenyUnencryptedAccess Effect: Deny Principal: '*' Action: 'elasticfilesystem:*' Condition: Bool: 'aws:SecureTransport': false LifecyclePolicies: - TransitionToIA: AFTER_30_DAYS PerformanceMode: generalPurpose ThroughputMode: bursting EFSSecurityGroup: Metadata: 'aws:copilot:description': 'A security group to allow your containers to talk to EFS storage' Type: AWS::EC2::SecurityGroup Condition: CreateEFS Properties: GroupDescription: !Join ['', [!Ref AppName, '-', !Ref EnvironmentName, EFSSecurityGroup]] VpcId: !Ref VPC Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-efs' EFSSecurityGroupIngressFromEnvironment: Type: AWS::EC2::SecurityGroupIngress Condition: CreateEFS Properties: Description: Ingress from containers in the Environment Security Group. GroupId: !Ref EFSSecurityGroup IpProtocol: -1 SourceSecurityGroupId: !Ref EnvironmentSecurityGroup MountTarget1: Type: AWS::EFS::MountTarget Condition: CreateEFS Properties: FileSystemId: !Ref FileSystem SubnetId: !Ref PrivateSubnet1 SecurityGroups: - !Ref EFSSecurityGroup MountTarget2: Type: AWS::EFS::MountTarget Condition: CreateEFS Properties: FileSystemId: !Ref FileSystem SubnetId: !Ref PrivateSubnet2 SecurityGroups: - !Ref EFSSecurityGroup CustomResourceRole: Metadata: 'aws:copilot:description': 'An IAM role to manage certificates and Route53 hosted zones' Type: AWS::IAM::Role Condition: DelegateDNS Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: "DNSandACMAccess" PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "acm:ListCertificates" - "acm:RequestCertificate" - "acm:DescribeCertificate" - "acm:GetCertificate" - "acm:DeleteCertificate" - "acm:AddTagsToCertificate" - "sts:AssumeRole" - "logs:*" - "route53:ChangeResourceRecordSets" - "route53:Get*" - "route53:Describe*" - "route53:ListResourceRecordSets" - "route53:ListHostedZonesByName" Resource: - "*" EnvironmentHostedZone: Metadata: 'aws:copilot:description': "A Route 53 Hosted Zone for the environment's subdomain" Type: "AWS::Route53::HostedZone" Condition: DelegateDNS Properties: HostedZoneConfig: Comment: !Sub "HostedZone for environment ${EnvironmentName} - ${EnvironmentName}.${AppName}.${AppDNSName}" Name: !Sub ${EnvironmentName}.${AppName}.${AppDNSName} CertificateValidationFunction: Type: AWS::Lambda::Function Condition: DelegateDNS Properties: Code: S3Bucket: stackset-wordpress-infra-pipelinebuiltartifactbuc-k48b94r6jl3h S3Key: manual/scripts/custom-resources/certificatevalidationfunction/c4017f3014f2f835cb4eb85dd59a887d2b3e0e1d92ab03ac3afaae23a5d0b463.zip Handler: "index.certificateRequestHandler" Timeout: 900 MemorySize: 512 Role: !GetAtt 'CustomResourceRole.Arn' Runtime: nodejs16.x CustomDomainFunction: Condition: ManagedAliases Type: AWS::Lambda::Function Properties: Code: S3Bucket: stackset-wordpress-infra-pipelinebuiltartifactbuc-k48b94r6jl3h S3Key: manual/scripts/custom-resources/customdomainfunction/fb963b15af12338aaf15a45bddc51c3b848066b9a14d4e9a46580fb7def5d7ba.zip Handler: "index.handler" Timeout: 600 MemorySize: 512 Role: !GetAtt 'CustomResourceRole.Arn' Runtime: nodejs16.x DNSDelegationFunction: Type: AWS::Lambda::Function Condition: DelegateDNS Properties: Code: S3Bucket: stackset-wordpress-infra-pipelinebuiltartifactbuc-k48b94r6jl3h S3Key: manual/scripts/custom-resources/dnsdelegationfunction/bb990039ea2e930f30878644182d2cb4e480ccf90815f215da01fc204510ce76.zip Handler: "index.domainDelegationHandler" Timeout: 600 MemorySize: 512 Role: !GetAtt 'CustomResourceRole.Arn' Runtime: nodejs16.x DelegateDNSAction: Metadata: 'aws:copilot:description': 'Delegate DNS for environment subdomain' Condition: DelegateDNS Type: Custom::DNSDelegationFunction DependsOn: - DNSDelegationFunction - EnvironmentHostedZone Properties: ServiceToken: !GetAtt DNSDelegationFunction.Arn DomainName: !Sub ${AppName}.${AppDNSName} SubdomainName: !Sub ${EnvironmentName}.${AppName}.${AppDNSName} NameServers: !GetAtt EnvironmentHostedZone.NameServers RootDNSRole: !Ref AppDNSDelegationRole HTTPSCert: Metadata: 'aws:copilot:description': 'Request and validate an ACM certificate for your domain' Condition: DelegateDNS Type: Custom::CertificateValidationFunction DependsOn: - CertificateValidationFunction - EnvironmentHostedZone - DelegateDNSAction Properties: ServiceToken: !GetAtt CertificateValidationFunction.Arn AppName: !Ref AppName EnvName: !Ref EnvironmentName DomainName: !Ref AppDNSName Aliases: !Ref Aliases EnvHostedZoneId: !Ref EnvironmentHostedZone Region: !Ref AWS::Region RootDNSRole: !Ref AppDNSDelegationRole CustomDomainAction: Metadata: 'aws:copilot:description': 'Add an A-record to the hosted zone for the domain alias' Condition: ManagedAliases Type: Custom::CustomDomainFunction Properties: ServiceToken: !GetAtt CustomDomainFunction.Arn AppName: !Ref AppName EnvName: !Ref EnvironmentName Aliases: !Ref Aliases AppDNSRole: !Ref AppDNSDelegationRole DomainName: !Ref AppDNSName PublicAccessDNS: !GetAtt PublicLoadBalancer.DNSName PublicAccessHostedZone: !GetAtt PublicLoadBalancer.CanonicalHostedZoneID AppRunnerVpcEndpointSecurityGroup: Metadata: 'aws:copilot:description': 'A security group for App Runner private services' Type: AWS::EC2::SecurityGroup Condition: CreateAppRunnerVPCEndpoint Properties: GroupDescription: wordpress-test-AppRunnerVpcEndpointSecurityGroup VpcId: !Ref VPC Tags: - Key: Name Value: copilot-wordpress-test-app-runner-vpc-endpoint AppRunnerVpcEndpointSecurityGroupIngressFromEnvironment: Type: AWS::EC2::SecurityGroupIngress Condition: CreateAppRunnerVPCEndpoint Properties: Description: Ingress from services in the environment GroupId: !Ref AppRunnerVpcEndpointSecurityGroup IpProtocol: -1 SourceSecurityGroupId: !Ref EnvironmentSecurityGroup AppRunnerVpcEndpoint: Metadata: 'aws:copilot:description': 'VPC Endpoint to connect environment to App Runner for private services' Type: AWS::EC2::VPCEndpoint Condition: CreateAppRunnerVPCEndpoint Properties: VpcEndpointType: Interface VpcId: !Ref VPC SecurityGroupIds: - !Ref AppRunnerVpcEndpointSecurityGroup ServiceName: !Sub 'com.amazonaws.${AWS::Region}.apprunner.requests' SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 Outputs: VpcId: Value: !Ref VPC Export: Name: !Sub ${AWS::StackName}-VpcId PublicSubnets: Value: !Join [ ',', [ !Ref PublicSubnet1, !Ref PublicSubnet2, ] ] Export: Name: !Sub ${AWS::StackName}-PublicSubnets PrivateSubnets: Value: !Join [ ',', [ !Ref PrivateSubnet1, !Ref PrivateSubnet2, ] ] Export: Name: !Sub ${AWS::StackName}-PrivateSubnets InternetGatewayID: Value: !Ref InternetGateway Export: Name: !Sub ${AWS::StackName}-InternetGatewayID PublicRouteTableID: Value: !Ref PublicRouteTable Export: Name: !Sub ${AWS::StackName}-PublicRouteTableID PrivateRouteTableIDs: Condition: CreateNATGateways Value: !Join [ ',', [ !Ref PrivateRouteTable1, !Ref PrivateRouteTable2, ] ] Export: Name: !Sub ${AWS::StackName}-PrivateRouteTableIDs ServiceDiscoveryNamespaceID: Value: !GetAtt ServiceDiscoveryNamespace.Id Export: Name: !Sub ${AWS::StackName}-ServiceDiscoveryNamespaceID EnvironmentSecurityGroup: Value: !Ref EnvironmentSecurityGroup Export: Name: !Sub ${AWS::StackName}-EnvironmentSecurityGroup PublicLoadBalancerDNSName: Condition: CreateALB Value: !GetAtt PublicLoadBalancer.DNSName Export: Name: !Sub ${AWS::StackName}-PublicLoadBalancerDNS PublicLoadBalancerFullName: Condition: CreateALB Value: !GetAtt PublicLoadBalancer.LoadBalancerFullName Export: Name: !Sub ${AWS::StackName}-PublicLoadBalancerFullName PublicLoadBalancerHostedZone: Condition: CreateALB Value: !GetAtt PublicLoadBalancer.CanonicalHostedZoneID Export: Name: !Sub ${AWS::StackName}-CanonicalHostedZoneID HTTPListenerArn: Condition: CreateALB Value: !Ref HTTPListener Export: Name: !Sub ${AWS::StackName}-HTTPListenerArn HTTPSListenerArn: Condition: ExportHTTPSListener Value: !Ref HTTPSListener Export: Name: !Sub ${AWS::StackName}-HTTPSListenerArn DefaultHTTPTargetGroupArn: Condition: CreateALB Value: !Ref DefaultHTTPTargetGroup Export: Name: !Sub ${AWS::StackName}-DefaultHTTPTargetGroup InternalLoadBalancerDNSName: Condition: CreateInternalALB Value: !GetAtt InternalLoadBalancer.DNSName Export: Name: !Sub ${AWS::StackName}-InternalLoadBalancerDNS InternalLoadBalancerFullName: Condition: CreateInternalALB Value: !GetAtt InternalLoadBalancer.LoadBalancerFullName Export: Name: !Sub ${AWS::StackName}-InternalLoadBalancerFullName InternalLoadBalancerHostedZone: Condition: CreateInternalALB Value: !GetAtt InternalLoadBalancer.CanonicalHostedZoneID Export: Name: !Sub ${AWS::StackName}-InternalLoadBalancerCanonicalHostedZoneID InternalWorkloadsHostedZone: Condition: CreateInternalALB Value: !Ref InternalWorkloadsHostedZone Export: Name: !Sub ${AWS::StackName}-InternalWorkloadsHostedZoneID InternalWorkloadsHostedZoneName: Condition: CreateInternalALB Value: !Sub ${EnvironmentName}.${AppName}.internal Export: Name: !Sub ${AWS::StackName}-InternalWorkloadsHostedZoneName InternalHTTPListenerArn: Condition: CreateInternalALB Value: !Ref InternalHTTPListener Export: Name: !Sub ${AWS::StackName}-InternalHTTPListenerArn InternalHTTPSListenerArn: Condition: ExportInternalHTTPSListener Value: !Ref InternalHTTPSListener Export: Name: !Sub ${AWS::StackName}-InternalHTTPSListenerArn InternalLoadBalancerSecurityGroup: Condition: CreateInternalALB Value: !Ref InternalLoadBalancerSecurityGroup Export: Name: !Sub ${AWS::StackName}-InternalLoadBalancerSecurityGroup ClusterId: Value: !Ref Cluster Export: Name: !Sub ${AWS::StackName}-ClusterId EnvironmentManagerRoleARN: Value: !GetAtt EnvironmentManagerRole.Arn Description: The role to be assumed by the ecs-cli to manage environments. Export: Name: !Sub ${AWS::StackName}-EnvironmentManagerRoleARN CFNExecutionRoleARN: Value: !GetAtt CloudformationExecutionRole.Arn Description: The role to be assumed by the Cloudformation service when it deploys application infrastructure. Export: Name: !Sub ${AWS::StackName}-CFNExecutionRoleARN EnvironmentHostedZone: Condition: DelegateDNS Value: !Ref EnvironmentHostedZone Description: The HostedZone for this environment's private DNS. Export: Name: !Sub ${AWS::StackName}-HostedZone EnvironmentSubdomain: Condition: DelegateDNS Value: !Sub ${EnvironmentName}.${AppName}.${AppDNSName} Description: The domain name of this environment. Export: Name: !Sub ${AWS::StackName}-SubDomain EnabledFeatures: Value: !Sub '${ALBWorkloads},${InternalALBWorkloads},${EFSWorkloads},${NATWorkloads},${Aliases},${AppRunnerPrivateWorkloads}' Description: Required output to force the stack to update if mutating feature params, like ALBWorkloads, does not change the template. ManagedFileSystemID: Condition: CreateEFS Value: !Ref FileSystem Description: The ID of the Copilot-managed EFS filesystem. Export: Name: !Sub ${AWS::StackName}-FilesystemID PublicALBAccessible: Condition: CreateALB Value: true LastForceDeployID: Value: "" Description: Optionally force the template to update when no immediate resource change is present. AppRunnerVpcEndpointId: Condition: CreateAppRunnerVPCEndpoint Value: !Ref AppRunnerVpcEndpoint Description: VPC Endpoint to App Runner for private services Export: Name: !Sub ${AWS::StackName}-AppRunnerVpcEndpointId
Cross-regional resources to support CodePipeline (Template Generated After Deployment)
code
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' # Cross-regional resources deployed via a stackset in the tools account # to support the CodePipeline for a workspace Description: Cross-regional resources to support the CodePipeline for a workspace Metadata: TemplateVersion: 'v1.2.0' Version: 2 Workloads: - Name: fe WithECR: true Accounts: - 723540195465 Resources: KMSKey: Metadata: 'aws:copilot:description': 'KMS key to encrypt pipeline artifacts between stages' # Used by the CodePipeline in the tools account to en/decrypt the # artifacts between stages Type: AWS::KMS::Key Properties: EnableKeyRotation: true KeyPolicy: Version: '2012-10-17' Id: !Ref AWS::StackName Statement: - # Allows the key to be administered in the tools account Effect: Allow Principal: AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root Action: - "kms:Create*" - "kms:Describe*" - "kms:Enable*" - "kms:List*" - "kms:Put*" - "kms:Update*" - "kms:Revoke*" - "kms:Disable*" - "kms:Get*" - "kms:Delete*" - "kms:ScheduleKeyDeletion" - "kms:CancelKeyDeletion" - "kms:Tag*" - "kms:UntagResource" Resource: "*" - # Allow use of the key in the tools account and all environment accounts Effect: Allow Principal: AWS: - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root - !Sub arn:${AWS::Partition}:iam::723540195465:root Action: - kms:Encrypt - kms:Decrypt - kms:ReEncrypt* - kms:GenerateDataKey* - kms:DescribeKey Resource: "*" PipelineBuiltArtifactBucketPolicy: Metadata: 'aws:copilot:description': 'S3 Bucket to store local artifacts' Type: AWS::S3::BucketPolicy DependsOn: PipelineBuiltArtifactBucket Properties: Bucket: !Ref PipelineBuiltArtifactBucket PolicyDocument: Version: '2012-10-17' Statement: - Action: - s3:* Effect: Allow Resource: - !Sub arn:${AWS::Partition}:s3:::${PipelineBuiltArtifactBucket} - !Sub arn:${AWS::Partition}:s3:::${PipelineBuiltArtifactBucket}/* Principal: AWS: - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root - !Sub arn:${AWS::Partition}:iam::723540195465:root PipelineBuiltArtifactBucket: Type: AWS::S3::Bucket Properties: VersioningConfiguration: Status: Enabled BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced LifecycleConfiguration: Rules: - Id: ExpireLocalAssets Status: Enabled Prefix: local-assets ExpirationInDays: 30 NoncurrentVersionExpirationInDays: 1 AbortIncompleteMultipartUpload: DaysAfterInitiation: 1 ECRRepofe: Metadata: 'aws:copilot:description': 'ECR container image repository for "fe"' Type: AWS::ECR::Repository Properties: RepositoryName: wordpress/fe Tags: - Key: copilot-service Value: fe RepositoryPolicyText: Version: '2012-10-17' Statement: - Sid: AllowPushPull Effect: Allow Principal: AWS: - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root - !Sub arn:${AWS::Partition}:iam::723540195465:root Action: - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage - ecr:BatchCheckLayerAvailability - ecr:PutImage - ecr:InitiateLayerUpload - ecr:UploadLayerPart - ecr:CompleteLayerUpload Outputs: KMSKeyARN: Description: KMS Key used by CodePipeline for encrypting artifacts. Value: !GetAtt KMSKey.Arn Export: Name: wordpress-ArtifactKey PipelineBucket: Description: "A bucket used for any Copilot artifacts that must be stored in S3 (pipelines, env files, etc)." Value: !Ref PipelineBuiltArtifactBucket ECRRepofe: Description: ECR Repo used to store images of the fe workload. Value: !GetAtt ECRRepofe.Arn TemplateVersion: Description: Required output to force the stackset to update if mutating version. Value: v1.2.0 StackSetOpId: Description: Required output to force stackset instances to update on every operation. Value: 2
Roles and Delegations Stackset (Template Generated After Deployment)
code
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetAdministrationRole to enable use of AWS CloudFormation StackSets. Metadata: TemplateVersion: 'v1.2.0' Parameters: AdminRoleName: Type: String ExecutionRoleName: Type: String DNSDelegationRoleName: Type: String Default: "" AppDNSDelegatedAccounts: Type: CommaDelimitedList Default: "" AppDomainName: Type: String Default: "" AppDomainHostedZoneID: Type: String Default: "" AppName: Type: String Conditions: DelegateDNS: !Not [!Equals [ !Ref AppDomainName, "" ]] Resources: AdministrationRole: Metadata: 'aws:copilot:description': 'A StackSet admin role assumed by CloudFormation to manage regional stacks' Type: AWS::IAM::Role Properties: RoleName: !Ref AdminRoleName AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: cloudformation.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: AssumeRole-AWSCloudFormationStackSetExecutionRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sts:AssumeRole Resource: - !Sub 'arn:${AWS::Partition}:iam::*:role/${ExecutionRoleName}' ExecutionRole: Metadata: 'aws:copilot:description': 'An IAM role assumed by the admin role to create ECR repositories, KMS keys, and S3 buckets' Type: AWS::IAM::Role Properties: RoleName: !Ref ExecutionRoleName AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: AWS: !GetAtt AdministrationRole.Arn Action: - sts:AssumeRole Path: / Policies: - PolicyName: ExecutionRolePolicy PolicyDocument: Version: '2012-10-17' Statement: - Sid: StackSetRequiredPermissions # https://go.aws/3Sa5Tcf Effect: Allow Action: - cloudformation:* - s3:* - sns:* Resource: "*" - Sid: KeyAdminPermissions # https://go.aws/3BIqf7a Effect: Allow Action: - kms:Create* - kms:Describe* - kms:Enable* - kms:List* - kms:Put* - kms:Update* - kms:Revoke* - kms:Disable* - kms:Get* - kms:Delete* - kms:TagResource - kms:UntagResource - kms:ScheduleKeyDeletion - kms:CancelKeyDeletion Resource: "*" - Sid: ManageECRRepos Effect: Allow Action: - ecr:DescribeImageScanFindings - ecr:GetLifecyclePolicyPreview - ecr:CreateRepository - ecr:GetDownloadUrlForLayer - ecr:GetAuthorizationToken - ecr:ListTagsForResource - ecr:ListImages - ecr:DeleteLifecyclePolicy - ecr:DeleteRepository - ecr:SetRepositoryPolicy - ecr:BatchGetImage - ecr:DescribeImages - ecr:DescribeRepositories - ecr:BatchCheckLayerAvailability - ecr:GetRepositoryPolicy - ecr:GetLifecyclePolicy - ecr:TagResource Resource: "*" DNSDelegationRole: Metadata: 'aws:copilot:description': 'A DNS delegation role to allow accounts: 723540195465 to manage your domain' Type: AWS::IAM::Role Condition: DelegateDNS Properties: RoleName: !Ref DNSDelegationRoleName AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root Action: - sts:AssumeRole - Effect: Allow Principal: AWS: - !Sub arn:${AWS::Partition}:iam::723540195465:root Action: - sts:AssumeRole Path: / Policies: - PolicyName: DNSDelegationPolicy PolicyDocument: Version: '2012-10-17' Statement: - Sid: HostedZoneReadRecords Effect: Allow Action: - route53:Get* - route53:List* Resource: "*" - Sid: HostedZoneUpdate Effect: Allow Action: - route53:ChangeResourceRecordSets Resource: - !Sub arn:${AWS::Partition}:route53:::hostedzone/${AppHostedZone} - !Sub arn:${AWS::Partition}:route53:::hostedzone/${AppDomainHostedZoneID} AppHostedZone: Metadata: 'aws:copilot:description': 'A hosted zone for wordpress.' Type: AWS::Route53::HostedZone Condition: DelegateDNS Properties: HostedZoneConfig: Comment: !Sub "Hosted zone for copilot application ${AppName}: ${AppName}.${AppDomainName}" Name: !Sub ${AppName}.${AppDomainName} AppDomainDelegationRecordSet: Metadata: 'aws:copilot:description': 'Add NS records to delegate responsibility to the wordpress. subdomain' Type: AWS::Route53::RecordSet Condition: DelegateDNS Properties: HostedZoneName: !Sub ${AppDomainName}. Comment: !Sub "Record for copilot domain delegation for application ${AppDomainName}" Name: !Sub ${AppName}.${AppDomainName}. Type: NS TTL: '900' ResourceRecords: !GetAtt AppHostedZone.NameServers Outputs: ExecutionRoleARN: Description: ExecutionRole used by this application to set up ECR Repos, KMS Keys and S3 buckets Value: !GetAtt ExecutionRole.Arn AdministrationRoleARN: Description: AdministrationRole used by this application to manage this application's StackSet Value: !GetAtt AdministrationRole.Arn TemplateVersion: Description: Required output to force the stack to update if mutating version. Value: v1.2.0
Ralph Quick Cloud Engineer
Ralph Quick is a professional Cloud Engineer specializing in the management, maintenance, and deployment of web service applications and infrastructure for operations. His experience ensures services are running efficiently and securely meeting the needs of your organization or clients.