Used AWS Copilot to Generate a Highly Available, Durable ECS Cluster Environment

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.

Ready to Chat?

Let’s Socialize!

+1 (754) 214-7728

    3 + 1 =

    Share This