# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 Description: CloudFormation template that represents a static site backed by S3. Metadata: Version: v1.29.0 Parameters: AppName: Type: String EnvName: Type: String WorkloadName: Type: String AddonsTemplateURL: Description: URL of the addons nested stack template within the S3 bucket. Type: String Default: "" Conditions: HasAddons: !Not [!Equals [!Ref AddonsTemplateURL, ""]] Resources: Bucket: Metadata: aws:copilot:description: An S3 Bucket to store the static site's assets Type: AWS::S3::Bucket Properties: VersioningConfiguration: Status: Enabled AccessControl: Private BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced BucketPolicyForCloudFront: Metadata: 'aws:copilot:description': 'A bucket policy to grant CloudFront read access to the Static Site bucket' Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref Bucket PolicyDocument: Version: 2012-10-17 Statement: - Sid: ForceHTTPS Effect: Deny Principal: "*" Action: s3:* Resource: - !Sub ${Bucket.Arn} - !Sub ${Bucket.Arn}/* Condition: Bool: aws:SecureTransport: false - Sid: AllowCloudFrontServicePrincipalReadOnly Effect: Allow Principal: Service: cloudfront.amazonaws.com Action: s3:GetObject Resource: - !Sub - arn:${AWS::Partition}:s3:::${bucket} - bucket: !Ref Bucket - !Sub - arn:${AWS::Partition}:s3:::${bucket}/* - bucket: !Ref Bucket Condition: StringEquals: AWS:SourceArn: !Sub - arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${cfDistributionID} - cfDistributionID: !Ref CloudFrontDistribution CloudFrontOriginAccessControl: Metadata: 'aws:copilot:description': 'Access control to make the content in the S3 bucket only accessible through CloudFront' Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Description: !Sub 'Access control for static s3 origin for ${AppName}-${EnvName}-${WorkloadName}' Name: my-app-my-env-static OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 CloudFrontViewerRequestRewriteFunction: Metadata: 'aws:copilot:description': 'CloudFront Function to rewrite viewer request to index.html' Type: AWS::CloudFront::Function Properties: AutoPublish: true FunctionCode: | function handler(event){var request=event.request;var uri=request.uri;if(uri.endsWith('/')){request.uri+='index.html'}else if(!uri.includes('.')){request.uri+='/index.html'}return request} FunctionConfig: Comment: CloudFront Function to rewrite viewer request to index.html Runtime: cloudfront-js-1.0 Name: my-app-my-env-static CloudFrontDistribution: Metadata: 'aws:copilot:description': 'A CloudFront distribution for global content delivery' Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Aliases: ["*.example.com"] DefaultCacheBehavior: Compress: true AllowedMethods: ["GET", "HEAD"] FunctionAssociations: - EventType: viewer-request FunctionARN: !GetAtt CloudFrontViewerRequestRewriteFunction.FunctionARN ViewerProtocolPolicy: redirect-to-https CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # See https://go.aws/3bJid3k TargetOriginId: !Sub 'copilot-${AppName}-${EnvName}-${WorkloadName}' Enabled: true IPV6Enabled: true Origins: - Id: !Sub 'copilot-${AppName}-${EnvName}-${WorkloadName}' DomainName: !GetAtt Bucket.RegionalDomainName OriginAccessControlId: !Ref CloudFrontOriginAccessControl # Workaround for using Origin Access Control as Origin Access Identity is still # required when the origin is an S3 bucket. S3OriginConfig: OriginAccessIdentity: '' ViewerCertificate: AcmCertificateArn: !Ref CertificateValidatorAction MinimumProtocolVersion: TLSv1 SslSupportMethod: sni-only TriggerStateMachineFunction: Metadata: aws:copilot:description: A lambda that starts the process of moving files to the S3 bucket Type: AWS::Lambda::Function Properties: Handler: index.handler Role: !GetAtt TriggerStateMachineFunctionRole.Arn Runtime: nodejs16.x Timeout: 900 MemorySize: 512 TriggerStateMachineFunctionRole: Metadata: aws:copilot:description: An IAM Role for the lambda that starts the process of moving files to the S3 bucket Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: TriggerStateMachine PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: states:StartSyncExecution Resource: !GetAtt CopyAssetsStateMachine.Arn ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole TriggerStateMachineAction: Metadata: aws:copilot:description: A custom resource that starts the process of moving files to the S3 bucket Type: Custom::TriggerStateMachine DependsOn: - Bucket - BucketPolicyForCloudFront - CloudFrontOriginAccessControl - CloudFrontDistribution - TriggerStateMachineFunction - TriggerStateMachineFunctionRole - CopyAssetsStateMachine - CopyAssetsStateMachineRole - EnvManagerS3Access - CustomDomainAction - CustomDomainFunction - CustomDomainRole - CertificateValidatorAction - CertificateValidationFunction - CertificateValidatorRole Properties: ServiceToken: !GetAtt TriggerStateMachineFunction.Arn StateMachineARN: !GetAtt CopyAssetsStateMachine.Arn AssetMappingFilePath: mappingfile CopyAssetsStateMachine: Metadata: aws:copilot:description: A state machine that moves source files to the S3 bucket Type: AWS::StepFunctions::StateMachine Properties: RoleArn: !GetAtt CopyAssetsStateMachineRole.Arn StateMachineType: EXPRESS Definition: Comment: A state machine that moves source files to the S3 bucket StartAt: GetMappingFile States: GetMappingFile: Type: Task Parameters: Bucket: stackset-bucket Key: mappingfile Resource: arn:aws:states:::aws-sdk:s3:getObject ResultSelector: files.$: States.StringToJson($.Body) ResultPath: $.GetMappingFile Next: CopyFiles CopyFiles: Type: Map Next: InvalidateCache ItemsPath: $.GetMappingFile.files ItemProcessor: ProcessorConfig: Mode: INLINE StartAt: ContentTypeChoice States: ContentTypeChoice: Type: Choice Choices: - Or: - Variable: $.contentType IsPresent: false - Variable: $.contentType StringMatches: '' Next: CopyFile Default: CopyFileWithContentType CopyFile: Type: Task End: true Resource: arn:aws:states:::aws-sdk:s3:copyObject Parameters: CopySource.$: States.Format('stackset-bucket/{}', $.path) Bucket: !Ref Bucket Key.$: $.destPath MetadataDirective: 'REPLACE' CopyFileWithContentType: Type: Task End: true Resource: arn:aws:states:::aws-sdk:s3:copyObject Parameters: CopySource.$: States.Format('stackset-bucket/{}', $.path) Bucket: !Ref Bucket Key.$: $.destPath ContentType.$: $.contentType # Required otherwise ContentType won't be applied. # See https://github.com/aws/aws-sdk-js/issues/1092 for more. MetadataDirective: 'REPLACE' InvalidateCache: Type: Task End: true Resource: arn:aws:states:::aws-sdk:cloudfront:createInvalidation Parameters: DistributionId: !Ref CloudFrontDistribution InvalidationBatch: CallerReference.$: States.UUID() Paths: Quantity: 1 Items: - "/*" CopyAssetsStateMachineRole: Metadata: aws:copilot:description: An IAM Role for the state machine that moves source files to the S3 bucket Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - states.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: ArtifactBucketAccess PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: s3:GetObject Resource: - arn:aws:s3:::stackset-bucket/mappingfile - arn:aws:s3:::stackset-bucket/local-assets/* - PolicyName: ServiceBucketAccess PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:PutObject Resource: !Sub arn:aws:s3:::${Bucket}/* - PolicyName: CacheInvalidation PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - acm:ListCertificates - cloudfront:GetDistribution - cloudfront:GetStreamingDistribution - cloudfront:GetDistributionConfig - cloudfront:ListDistributions - cloudfront:ListCloudFrontOriginAccessIdentities - cloudfront:CreateInvalidation - cloudfront:GetInvalidation - cloudfront:ListInvalidations - elasticloadbalancing:DescribeLoadBalancers - iam:ListServerCertificates - sns:ListSubscriptionsByTopic - sns:ListTopics - waf:GetWebACL - waf:ListWebACLs Resource: "*" Condition: StringEquals: 'aws:ResourceTag/copilot-application': !Sub '${AppName}' 'aws:ResourceTag/copilot-environment': !Sub '${EnvName}' 'aws:ResourceTag/copilot-service': !Sub '${WorkloadName}' - Effect: Allow Action: - s3:ListAllMyBuckets Resource: arn:aws:s3:::* EnvManagerS3Access: Metadata: aws:copilot:description: A policy that gives the Env Manager role access to this site's S3 Bucket Type: AWS::IAM::Policy Properties: Roles: - !Sub "${AppName}-${EnvName}-EnvManagerRole" PolicyName: !Sub "${WorkloadName}-S3Access" PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:ListBucket - s3:ListBucketVersions - s3:DeleteObject - s3:DeleteObjectVersion Resource: - !Sub arn:aws:s3:::${Bucket} - !Sub arn:aws:s3:::${Bucket}/* CustomDomainAction: Metadata: 'aws:copilot:description': "Add A-records for your Static Site alias" Type: Custom::CustomDomainFunction Properties: ServiceToken: !GetAtt CustomDomainFunction.Arn PublicAccessHostedZoneID: Z2FDTNDATAQYW2 # See https://go.aws/3cPhvlX PublicAccessDNS: !GetAtt CloudFrontDistribution.DomainName EnvHostedZoneId: Fn::ImportValue: !Sub "${AppName}-${EnvName}-HostedZone" EnvName: !Ref EnvName AppName: !Ref AppName ServiceName: !Ref WorkloadName RootDNSRole: arn:aws:iam::123456789123:role/my-app-DNSDelegationRole DomainName: example.com Aliases: ["*.example.com"] CustomDomainFunction: Type: AWS::Lambda::Function Properties: Handler: "index.handler" Timeout: 900 MemorySize: 512 Role: !GetAtt 'CustomDomainRole.Arn' Runtime: nodejs16.x CustomDomainRole: Metadata: 'aws:copilot:description': "An IAM role to update the Route 53 hosted zone" Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: "CustomDomainPolicy" PolicyDocument: Version: '2012-10-17' Statement: - Sid: AllowAssumeRole Effect: Allow Action: sts:AssumeRole Resource: arn:aws:iam::123456789123:role/my-app-DNSDelegationRole - Sid: HostedZoneAccess Effect: Allow Action: - "route53:ChangeResourceRecordSets" - "route53:Get*" - "route53:Describe*" - "route53:ListResourceRecordSets" - "route53:ListHostedZonesByName" Resource: "*" ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole CertificateValidatorAction: Metadata: 'aws:copilot:description': "Request and validate the certificate for your Static Site" Type: Custom::CertificateValidationFunction Properties: ServiceToken: !GetAtt CertificateValidationFunction.Arn EnvHostedZoneId: Fn::ImportValue: !Sub "${AppName}-${EnvName}-HostedZone" EnvName: !Ref EnvName AppName: !Ref AppName ServiceName: !Ref WorkloadName RootDNSRole: arn:aws:iam::123456789123:role/my-app-DNSDelegationRole DomainName: example.com IsCloudFrontCertificate: true Aliases: ["*.example.com"] CertificateValidationFunction: Type: AWS::Lambda::Function Properties: Handler: "index.handler" Timeout: 900 MemorySize: 512 Role: !GetAtt 'CertificateValidatorRole.Arn' Runtime: nodejs16.x CertificateValidatorRole: Metadata: 'aws:copilot:description': "An IAM role to request and validate a certificate for your service" Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: "CertValidatorPolicy" PolicyDocument: Version: '2012-10-17' Statement: - Sid: AllowAssumeRole Effect: Allow Action: sts:AssumeRole Resource: arn:aws:iam::123456789123:role/my-app-DNSDelegationRole - Sid: HostedZoneUpdateAndWait Effect: Allow Action: route53:ChangeResourceRecordSets Resource: "*" - Sid: HostedZoneRead Effect: Allow Action: - route53:ListResourceRecordSets - route53:GetChange Resource: "*" - Sid: ServiceCertificateDelete Effect: Allow Action: acm:DeleteCertificate Resource: "*" Condition: StringEquals: 'aws:ResourceTag/copilot-application': !Sub '${AppName}' 'aws:ResourceTag/copilot-environment': !Sub '${EnvName}' 'aws:ResourceTag/copilot-service': !Sub '${WorkloadName}' - Sid: TaggedResourcesRead Effect: Allow Action: tag:GetResources Resource: "*" - Sid: ServiceCertificateCreate Effect: Allow Action: - acm:RequestCertificate - acm:AddTagsToCertificate Resource: "*" Condition: StringEquals: 'aws:ResourceTag/copilot-application': !Sub '${AppName}' 'aws:ResourceTag/copilot-environment': !Sub '${EnvName}' 'aws:ResourceTag/copilot-service': !Sub '${WorkloadName}' - Sid: CertificateRead Effect: Allow Action: acm:DescribeCertificate Resource: "*" ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Outputs: CloudFrontDistributionDomainName: Value: !GetAtt CloudFrontDistribution.DomainName Export: Name: !Sub ${AWS::StackName}-CloudFrontDistributionDomainName CloudFrontDistributionAlternativeDomainName: Value: "*.example.com" Export: Name: !Sub ${AWS::StackName}-CloudFrontDistributionAlternativeDomainName