# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 AWSTemplateFormatVersion: 2010-09-09 Description: CloudFormation template that represents a request driven web service on AWS App Runner. Metadata: Version: {{ .Version }} {{- if .SerializedManifest }} Manifest: | {{indent 4 .SerializedManifest}} {{- end }} Parameters: AppName: Type: String EnvName: Type: String WorkloadName: Type: String ContainerImage: Type: String ContainerPort: Type: Number ImageRepositoryType: Type: String InstanceCPU: Type: String InstanceMemory: Type: String HealthCheckPath: Type: String Default: '' HealthCheckInterval: Type: String Default: '' HealthCheckTimeout: Type: String Default: '' HealthCheckHealthyThreshold: Type: String Default: '' HealthCheckUnhealthyThreshold: Type: String Default: '' AddonsTemplateURL: Description: 'URL of the addons nested stack template within the S3 bucket.' Type: String Default: '' Conditions: # App Runner will not accept an AccessRole for ImageRepositoryTypes other than ECR. # In this scenario the request will fail. NeedsAccessRole: !Equals [!Ref ImageRepositoryType, 'ECR'] HasHealthCheckPath: !Not [!Equals [!Ref HealthCheckPath, '']] HasHealthCheckInterval: !Not [!Equals [!Ref HealthCheckInterval, '']] HasHealthCheckTimeout: !Not [!Equals [!Ref HealthCheckTimeout, '']] HasHealthCheckHealthyThreshold: !Not [!Equals [!Ref HealthCheckHealthyThreshold, '']] HasHealthCheckUnhealthyThreshold: !Not [!Equals [!Ref HealthCheckUnhealthyThreshold, '']] HasAddons: # If a bucket URL is specified, that means the template exists. !Not [!Equals [!Ref AddonsTemplateURL, '']] Resources: {{include "accessrole" . | indent 2}} {{include "instancerole" . | indent 2}} Service: Metadata: 'aws:copilot:description': 'An App Runner service to run and manage your containers' Type: AWS::AppRunner::Service Properties: ServiceName: !Sub '${AppName}-${EnvName}-${WorkloadName}' SourceConfiguration: AuthenticationConfiguration: !If - NeedsAccessRole - AccessRoleArn: !GetAtt AccessRole.Arn - !Ref AWS::NoValue AutoDeploymentsEnabled: false ImageRepository: ImageIdentifier: !Ref ContainerImage ImageRepositoryType: !Ref ImageRepositoryType ImageConfiguration: Port: !Ref ContainerPort {{- if hasSecrets .}} RuntimeEnvironmentSecrets: {{- range $name, $secret := .Secrets}} - Name: {{$name}} {{- if $secret.RequiresImport}} Value: Fn::ImportValue: {{ quote $secret.ValueFrom }} {{- else}} Value: {{if not $secret.RequiresSub }} {{$secret.ValueFrom}} {{- else}} !Sub 'arn:${AWS::Partition}:{{$secret.Service}}:${AWS::Region}:${AWS::AccountId}:{{$secret.ValueFrom}}' {{- end}} {{- end}} {{- end}} {{- end}} {{- if .NestedStack}}{{$stackName := .NestedStack.StackName}} {{- range $secret := .NestedStack.SecretOutputs}} - Name: {{toSnakeCase $secret}} Value: Fn::GetAtt: [{{$stackName}}, Outputs.{{$secret}}] {{- end}} {{- end}} RuntimeEnvironmentVariables: - Name: COPILOT_APPLICATION_NAME Value: !Ref AppName - Name: COPILOT_ENVIRONMENT_NAME Value: !Ref EnvName - Name: COPILOT_SERVICE_NAME Value: !Ref WorkloadName {{- if requiresVPCConnector .}} - Name: COPILOT_SERVICE_DISCOVERY_ENDPOINT Value: {{.ServiceDiscoveryEndpoint}} {{- end}} {{- if .Publish }} {{- if .Publish.Topics }} - Name: COPILOT_SNS_TOPIC_ARNS Value: '{{jsonSNSTopics .Publish.Topics}}' {{- end }} {{- end }} {{- if .Variables}} {{include "variables" . | indent 14}} {{- end}} {{- if .NestedStack}}{{$stackName := .NestedStack.StackName}} {{- range $var := .NestedStack.VariableOutputs}} - Name: {{toSnakeCase $var}} Value: Fn::GetAtt: [ {{$stackName}}, Outputs.{{$var}}] {{- end }} {{- range $var := .NestedStack.SecretOutputs }} - Name: {{toSnakeCase $var}}_ARN Value: Fn::GetAtt: [ {{$stackName}}, Outputs.{{$var}}] {{- end }} {{- end}} {{- if .StartCommand }} StartCommand: {{.StartCommand}} {{- end }} InstanceConfiguration: Cpu: !Ref InstanceCPU Memory: !Ref InstanceMemory InstanceRoleArn: !GetAtt InstanceRole.Arn {{- if .EnableHealthCheck }} HealthCheckConfiguration: Path: !If [HasHealthCheckPath, !Ref HealthCheckPath, !Ref AWS::NoValue] Protocol: HTTP Interval: !If [HasHealthCheckInterval, !Ref HealthCheckInterval, !Ref AWS::NoValue] Timeout: !If [HasHealthCheckTimeout, !Ref HealthCheckTimeout, !Ref AWS::NoValue] HealthyThreshold: !If [HasHealthCheckHealthyThreshold, !Ref HealthCheckHealthyThreshold, !Ref AWS::NoValue] UnhealthyThreshold: !If [HasHealthCheckUnhealthyThreshold, !Ref HealthCheckUnhealthyThreshold, !Ref AWS::NoValue] {{- end }} NetworkConfiguration: EgressConfiguration: {{- if requiresVPCConnector .}} EgressType: VPC VpcConnectorArn: !Ref VpcConnector {{- else }} EgressType: DEFAULT {{- end}} {{- if .Private}} IngressConfiguration: IsPubliclyAccessible: false {{- end}} {{- if eq .Observability.Tracing "AWSXRAY"}} ObservabilityConfiguration: ObservabilityEnabled: true ObservabilityConfigurationArn: !Sub 'arn:aws:apprunner:${AWS::Region}:${AWS::AccountId}:observabilityconfiguration/DefaultConfiguration/1/00000000000000000000000000000001' {{- end }} {{- if .Count}} AutoScalingConfigurationArn: !Sub 'arn:${AWS::Partition}:apprunner:${AWS::Region}:${AWS::AccountId}:autoscalingconfiguration/{{.Count}}' {{- end}} Tags: - Key: copilot-application Value: !Ref AppName - Key: copilot-environment Value: !Ref EnvName - Key: copilot-service Value: !Ref WorkloadName{{if .Tags}}{{range $name, $value := .Tags}} - Key: {{$name}} Value: {{$value}}{{end}}{{end}} {{- if .Private}} AppRunnerVpcIngressConnection: Metadata: 'aws:copilot:description': 'The ingress connection from your environment to this service' Type: AWS::AppRunner::VpcIngressConnection Properties: ServiceArn: !GetAtt Service.ServiceArn IngressVpcConfiguration: VpcId: Fn::ImportValue: !Sub '${AppName}-${EnvName}-VpcId' {{- if .AppRunnerVPCEndpoint}} VpcEndpointId: {{.AppRunnerVPCEndpoint}} {{- else}} VpcEndpointId: !GetAtt EnvControllerAction.AppRunnerVpcEndpointId {{- end}} Tags: - Key: copilot-application Value: !Ref AppName - Key: copilot-environment Value: !Ref EnvName - Key: copilot-service Value: !Ref WorkloadName {{- if .Tags}} {{- range $name, $value := .Tags}} - Key: {{$name}} Value: {{$value}} {{- end}} {{- end}} {{- end}} {{include "addons" . | indent 2}} {{if .Alias}} CustomDomainFunction: Type: AWS::Lambda::Function Properties: {{- with $cr := index .CustomResources "CustomDomainFunction" }} Code: S3Bucket: {{$cr.Bucket}} S3Key: {{$cr.Key}} {{- end }} Handler: "index.handler" Timeout: 900 MemorySize: 512 Role: !GetAtt CustomResourceRole.Arn Runtime: nodejs16.x Layers: - {{ .AWSSDKLayer }} CustomDomainAction: Metadata: 'aws:copilot:description': 'Associate the domain with the service as well as upserting the domain record and validation records' DependsOn: CustomDomainFunction Type: Custom::CustomDomainFunction Properties: ServiceToken: !GetAtt CustomDomainFunction.Arn ServiceARN: !GetAtt Service.ServiceArn CustomDomain: {{ .Alias }} AppDNSRole: {{ .AppDNSDelegationRole }} AppDNSName: {{ .AppDNSName }} CustomResourceRole: Metadata: 'aws:copilot:description': 'An IAM role {{- if .PermissionsBoundary}} with permissions boundary {{.PermissionsBoundary}} {{- end}} used by custom resources to associate the custom domain' Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole {{- if .PermissionsBoundary}} PermissionsBoundary: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/{{.PermissionsBoundary}}' {{- end}} Path: / ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: "DNSandACMAccess" PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "apprunner:AssociateCustomDomain" - "apprunner:DisassociateCustomDomain" - "apprunner:DescribeCustomDomains" - "sts:AssumeRole" - "route53:ChangeResourceRecordSets" - "route53:ListHostedZonesByName" Resource: - "*" {{- end }} {{include "publish" . | indent 2}} {{- if gt (len (envControllerParams .)) 0}} {{include "env-controller" . | indent 2}} {{- end}} {{- if requiresVPCConnector .}} {{include "vpc-connector" . | indent 2 }} {{- end }}