
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.

