packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts (618 lines of code) (raw):

import * as ecr from 'aws-cdk-lib/aws-ecr'; import * as assets from 'aws-cdk-lib/aws-ecr-assets'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as kms from 'aws-cdk-lib/aws-kms'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as cdk from 'aws-cdk-lib/core'; import { Lazy } from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; import { CfnService } from 'aws-cdk-lib/aws-apprunner'; import { IVpcConnector } from './vpc-connector'; import { IAutoScalingConfiguration } from './auto-scaling-configuration'; import { IObservabilityConfiguration } from './observability-configuration'; import { addConstructMetadata, MethodMetadata } from 'aws-cdk-lib/core/lib/metadata-resource'; /** * The image repository types */ export enum ImageRepositoryType { /** * Amazon ECR Public */ ECR_PUBLIC = 'ECR_PUBLIC', /** * Amazon ECR */ ECR = 'ECR', } /** * The number of CPU units reserved for each instance of your App Runner service. * */ export class Cpu { /** * 0.25 vCPU */ public static readonly QUARTER_VCPU = Cpu.of('0.25 vCPU'); /** * 0.5 vCPU */ public static readonly HALF_VCPU = Cpu.of('0.5 vCPU'); /** * 1 vCPU */ public static readonly ONE_VCPU = Cpu.of('1 vCPU'); /** * 2 vCPU */ public static readonly TWO_VCPU = Cpu.of('2 vCPU'); /** * 4 vCPU */ public static readonly FOUR_VCPU = Cpu.of('4 vCPU'); /** * Custom CPU unit * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-instanceconfiguration.html#cfn-apprunner-service-instanceconfiguration-cpu * * @param unit custom CPU unit */ public static of(unit: string): Cpu { const numericPatterns = ['256', '512', '1024', '2048', '4096']; const unitPatterns = ['0.25 vCPU', '0.5 vCPU', '1 vCPU', '2 vCPU', '4 vCPU']; const allowedPatterns = numericPatterns.concat(unitPatterns); const isValidValue = allowedPatterns.some( (pattern) => pattern === unit, ); if (!isValidValue) { throw new cdk.UnscopedValidationError('CPU value is invalid'); } return new Cpu(unit); } /** * * @param unit The unit of CPU. */ private constructor(public readonly unit: string) { } } /** * The amount of memory reserved for each instance of your App Runner service. */ export class Memory { /** * 0.5 GB(for 0.25 vCPU) */ public static readonly HALF_GB = Memory.of('0.5 GB'); /** * 1 GB(for 0.25 or 0.5 vCPU) */ public static readonly ONE_GB = Memory.of('1 GB'); /** * 2 GB(for 1 vCPU) */ public static readonly TWO_GB = Memory.of('2 GB'); /** * 3 GB(for 1 vCPU) */ public static readonly THREE_GB = Memory.of('3 GB'); /** * 4 GB(for 1 or 2 vCPU) */ public static readonly FOUR_GB = Memory.of('4 GB'); /** * 6 GB(for 2 vCPU) */ public static readonly SIX_GB = Memory.of('6 GB'); /** * 8 GB(for 4 vCPU) */ public static readonly EIGHT_GB = Memory.of('8 GB'); /** * 10 GB(for 4 vCPU) */ public static readonly TEN_GB = Memory.of('10 GB'); /** * 12 GB(for 4 vCPU) */ public static readonly TWELVE_GB = Memory.of('12 GB'); /** * Custom Memory unit * * @param unit custom Memory unit * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-instanceconfiguration.html#cfn-apprunner-service-instanceconfiguration-memory */ public static of(unit: string): Memory { const numericPatterns = ['512', '1024', '2048', '3072', '4096', '6144', '8192', '10240', '12288']; const unitPatterns = ['0.5 GB', '1 GB', '2 GB', '3 GB', '4 GB', '6 GB', '8 GB', '10 GB', '12 GB']; const allowedPatterns = numericPatterns.concat(unitPatterns); const isValidValue = allowedPatterns.some( (pattern) => pattern === unit, ); if (!isValidValue) { throw new cdk.UnscopedValidationError('Memory value is invalid'); } return new Memory(unit); } /** * * @param unit The unit of memory. */ private constructor(public readonly unit: string) { } } /** * The code runtimes */ export class Runtime { /** * CORRETTO 8 */ public static readonly CORRETTO_8 = Runtime.of('CORRETTO_8'); /** * CORRETTO 11 */ public static readonly CORRETTO_11 = Runtime.of('CORRETTO_11'); /** * .NET 6 */ public static readonly DOTNET_6 = Runtime.of('DOTNET_6'); /** * Go 1.18 */ public static readonly GO_1 = Runtime.of('GO_1'); /** * NodeJS 12 */ public static readonly NODEJS_12 = Runtime.of('NODEJS_12'); /** * NodeJS 14 */ public static readonly NODEJS_14 = Runtime.of('NODEJS_14'); /** * NodeJS 16 */ public static readonly NODEJS_16 = Runtime.of('NODEJS_16'); /** * NodeJS 18 */ public static readonly NODEJS_18 = Runtime.of('NODEJS_18'); /** * PHP 8.1 */ public static readonly PHP_81 = Runtime.of('PHP_81'); /** * Python 3 */ public static readonly PYTHON_3 = Runtime.of('PYTHON_3'); /** * Python 3.11 */ public static readonly PYTHON_311 = Runtime.of('PYTHON_311'); /** * Ruby 3.1 */ public static readonly RUBY_31 = Runtime.of('RUBY_31'); /** * Other runtimes * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-codeconfigurationvalues.html#cfn-apprunner-service-codeconfigurationvalues-runtime for all available runtimes. * * @param name runtime name * */ public static of(name: string) { return new Runtime(name); } /** * * @param name The runtime name. */ private constructor(public readonly name: string) { } } /** * The environment secret for the service. */ interface EnvironmentSecret { readonly name: string; readonly value: string; } /** * The environment variable for the service. */ interface EnvironmentVariable { readonly name: string; readonly value: string; } /** * Result of binding `Source` into a `Service`. */ export interface SourceConfig { /** * The image repository configuration (mutually exclusive with `codeRepository`). * * @default - no image repository. */ readonly imageRepository?: ImageRepository; /** * The ECR repository (required to grant the pull privileges for the iam role). * * @default - no ECR repository. */ readonly ecrRepository?: ecr.IRepository; /** * The code repository configuration (mutually exclusive with `imageRepository`). * * @default - no code repository. */ readonly codeRepository?: CodeRepositoryProps; } /** * Properties of the Github repository for `Source.fromGitHub()` */ export interface GithubRepositoryProps { /** * The code configuration values. Will be ignored if configurationSource is `REPOSITORY`. * @default - no values will be passed. The `apprunner.yaml` from the github reopsitory will be used instead. */ readonly codeConfigurationValues?: CodeConfigurationValues; /** * The source of the App Runner configuration. */ readonly configurationSource: ConfigurationSourceType; /** * The location of the repository that contains the source code. */ readonly repositoryUrl: string; /** * The branch name that represents a specific version for the repository. * * @default main */ readonly branch?: string; /** * ARN of the connection to Github. Only required for Github source. */ readonly connection: GitHubConnection; } /** * Properties of the image repository for `Source.fromEcrPublic()` */ export interface EcrPublicProps { /** * The image configuration for the image from ECR Public. * @default - no image configuration will be passed. The default `port` will be 8080. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-imageconfiguration.html#cfn-apprunner-service-imageconfiguration-port */ readonly imageConfiguration?: ImageConfiguration; /** * The ECR Public image URI. */ readonly imageIdentifier: string; } /** * Properties of the image repository for `Source.fromEcr()` */ export interface EcrProps { /** * The image configuration for the image from ECR. * @default - no image configuration will be passed. The default `port` will be 8080. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-imageconfiguration.html#cfn-apprunner-service-imageconfiguration-port */ readonly imageConfiguration?: ImageConfiguration; /** * Represents the ECR repository. */ readonly repository: ecr.IRepository; /** * Image tag. * @default - 'latest' * @deprecated use `tagOrDigest` */ readonly tag?: string; /** * Image tag or digest (digests must start with `sha256:`). * @default - 'latest' */ readonly tagOrDigest?: string; } /** * Properties of the image repository for `Source.fromAsset()` */ export interface AssetProps { /** * The image configuration for the image built from the asset. * @default - no image configuration will be passed. The default `port` will be 8080. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-imageconfiguration.html#cfn-apprunner-service-imageconfiguration-port */ readonly imageConfiguration?: ImageConfiguration; /** * Represents the docker image asset. */ readonly asset: assets.DockerImageAsset; } /** * Specify the secret's version id or version stage */ export interface SecretVersionInfo { /** * version id of the secret * * @default - use default version id */ readonly versionId?: string; /** * version stage of the secret * * @default - use default version stage */ readonly versionStage?: string; } /** * Represents the App Runner service source. */ export abstract class Source { /** * Source from the GitHub repository. */ public static fromGitHub(props: GithubRepositoryProps): GithubSource { return new GithubSource(props); } /** * Source from the ECR repository. */ public static fromEcr(props: EcrProps): EcrSource { return new EcrSource(props); } /** * Source from the ECR Public repository. */ public static fromEcrPublic(props: EcrPublicProps): EcrPublicSource { return new EcrPublicSource(props); } /** * Source from local assets. */ public static fromAsset(props: AssetProps): AssetSource { return new AssetSource(props); } /** * Called when the Job is initialized to allow this object to bind. */ public abstract bind(scope: Construct): SourceConfig; } /** * Represents the service source from a Github repository. */ export class GithubSource extends Source { private readonly props: GithubRepositoryProps; constructor(props: GithubRepositoryProps) { super(); this.props = props; } public bind(_scope: Construct): SourceConfig { return { codeRepository: { codeConfiguration: { configurationSource: this.props.configurationSource, configurationValues: this.props.codeConfigurationValues, }, repositoryUrl: this.props.repositoryUrl, sourceCodeVersion: { type: 'BRANCH', value: this.props.branch ?? 'main', }, connection: this.props.connection, }, }; } } /** * Represents the service source from ECR. */ export class EcrSource extends Source { private readonly props: EcrProps; constructor(props: EcrProps) { super(); this.props = props; } public bind(_scope: Construct): SourceConfig { return { imageRepository: { imageConfiguration: this.props.imageConfiguration, imageIdentifier: this.props.repository.repositoryUriForTagOrDigest( this.props.tagOrDigest || this.props.tag || 'latest', ), imageRepositoryType: ImageRepositoryType.ECR, }, ecrRepository: this.props.repository, }; } } /** * Represents the service source from ECR Public. */ export class EcrPublicSource extends Source { private readonly props: EcrPublicProps; constructor(props: EcrPublicProps) { super(); this.props = props; } public bind(_scope: Construct): SourceConfig { return { imageRepository: { imageConfiguration: this.props.imageConfiguration, imageIdentifier: this.props.imageIdentifier, imageRepositoryType: ImageRepositoryType.ECR_PUBLIC, }, }; } } /** * Represents the source from local assets. */ export class AssetSource extends Source { private readonly props: AssetProps; constructor(props: AssetProps) { super(); this.props = props; } public bind(_scope: Construct): SourceConfig { return { imageRepository: { imageConfiguration: this.props.imageConfiguration, imageIdentifier: this.props.asset.imageUri, imageRepositoryType: ImageRepositoryType.ECR, }, ecrRepository: this.props.asset.repository, }; } } /** * Describes the configuration that AWS App Runner uses to run an App Runner service * using an image pulled from a source image repository. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-imageconfiguration.html */ export interface ImageConfiguration { /** * The port that your application listens to in the container. * * @default 8080 */ readonly port?: number; /** * Environment variables that are available to your running App Runner service. * * @default - no environment variables * @deprecated use environmentVariables. */ readonly environment?: { [key: string]: string }; /** * Environment variables that are available to your running App Runner service. * * @default - no environment variables */ readonly environmentVariables?: { [key: string]: string }; /** * Environment secrets that are available to your running App Runner service. * * @default - no environment secrets */ readonly environmentSecrets?: { [key: string]: Secret }; /** * An optional command that App Runner runs to start the application in the source image. * If specified, this command overrides the Docker image’s default start command. * * @default - no start command */ readonly startCommand?: string; } /** * Describes a source image repository. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-imagerepository.html */ export interface ImageRepository { /** * The identifier of the image. For `ECR_PUBLIC` imageRepositoryType, the identifier domain should * always be `public.ecr.aws`. For `ECR`, the pattern should be * `([0-9]{12}.dkr.ecr.[a-z\-]+-[0-9]{1}.amazonaws.com\/.*)`. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-imagerepository.html */ readonly imageIdentifier: string; /** * The type of the image repository. This reflects the repository provider and whether * the repository is private or public. */ readonly imageRepositoryType: ImageRepositoryType; /** * Configuration for running the identified image. * @default - no image configuration will be passed. The default `port` will be 8080. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-imageconfiguration.html#cfn-apprunner-service-imageconfiguration-port */ readonly imageConfiguration?: ImageConfiguration; } /** * Identifies a version of code that AWS App Runner refers to within a source code repository. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-sourcecodeversion.html */ export interface SourceCodeVersion { /** * The type of version identifier. */ readonly type: string; /** * A source code version. */ readonly value: string; } /** * Properties of the CodeRepository. */ export interface CodeRepositoryProps { /** * Configuration for building and running the service from a source code repository. */ readonly codeConfiguration: CodeConfiguration; /** * The location of the repository that contains the source code. */ readonly repositoryUrl: string; /** * The version that should be used within the source code repository. */ readonly sourceCodeVersion: SourceCodeVersion; /** * The App Runner connection for GitHub. */ readonly connection: GitHubConnection; } /** * Properties of the AppRunner Service */ export interface ServiceProps { /** * The source of the repository for the service. */ readonly source: Source; /** * Specifies whether to enable continuous integration from the source repository. * * If true, continuous integration from the source repository is enabled for the App Runner service. * Each repository change (including any source code commit or new image version) starts a deployment. * By default, App Runner sets to false for a source image that uses an ECR Public repository or an ECR repository that's in an AWS account other than the one that the service is in. * App Runner sets to true in all other cases (which currently include a source code repository or a source image using a same-account ECR repository). * * @default - no value will be passed. */ readonly autoDeploymentsEnabled?: boolean; /** * Specifies an App Runner Auto Scaling Configuration. * * A default configuration is either the AWS recommended configuration, * or the configuration you set as the default. * * @see https://docs.aws.amazon.com/apprunner/latest/dg/manage-autoscaling.html * * @default - the latest revision of a default auto scaling configuration is used. */ readonly autoScalingConfiguration?: IAutoScalingConfiguration; /** * The number of CPU units reserved for each instance of your App Runner service. * * @default Cpu.ONE_VCPU */ readonly cpu?: Cpu; /** * The amount of memory reserved for each instance of your App Runner service. * * @default Memory.TWO_GB */ readonly memory?: Memory; /** * The IAM role that grants the App Runner service access to a source repository. * It's required for ECR image repositories (but not for ECR Public repositories). * * The role must be assumable by the 'build.apprunner.amazonaws.com' service principal. * * @see https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html#security_iam_service-with-iam-roles-service.access * * @default - generate a new access role. */ readonly accessRole?: iam.IRole; /** * The IAM role that provides permissions to your App Runner service. * These are permissions that your code needs when it calls any AWS APIs. * * The role must be assumable by the 'tasks.apprunner.amazonaws.com' service principal. * * @see https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html#security_iam_service-with-iam-roles-service.instance * * @default - generate a new instance role. */ readonly instanceRole?: iam.IRole; /** * Name of the service. * * @default - auto-generated if undefined. */ readonly serviceName?: string; /** * Settings for an App Runner VPC connector to associate with the service. * * @default - no VPC connector, uses the DEFAULT egress type instead */ readonly vpcConnector?: IVpcConnector; /** * Specifies whether your App Runner service is publicly accessible. * * If you use `VpcIngressConnection`, you must set this property to `false`. * * @default true */ readonly isPubliclyAccessible?: boolean; /** * Settings for the health check that AWS App Runner performs to monitor the health of a service. * * You can specify it by static methods `HealthCheck.http` or `HealthCheck.tcp`. * * @default - no health check configuration */ readonly healthCheck?: HealthCheck; /** * The customer managed key that AWS App Runner uses to encrypt copies of the source repository and service logs. * * @default - Use an AWS managed key */ readonly kmsKey?: kms.IKey; /** * The IP address type for your incoming public network configuration. * * @default - IpAddressType.IPV4 */ readonly ipAddressType?: IpAddressType; /** * Settings for an App Runner observability configuration. * * @default - no observability configuration resource is associated with the service. */ readonly observabilityConfiguration?: IObservabilityConfiguration; } /** * The source of the App Runner configuration. */ export enum ConfigurationSourceType { /** * App Runner reads configuration values from `the apprunner.yaml` file in the source code repository * and ignores `configurationValues`. */ REPOSITORY = 'REPOSITORY', /** * App Runner uses configuration values provided in `configurationValues` and ignores the `apprunner.yaml` * file in the source code repository. */ API = 'API', } /** * Describes the configuration that AWS App Runner uses to build and run an App Runner service * from a source code repository. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-codeconfiguration.html */ export interface CodeConfiguration { /** * The basic configuration for building and running the App Runner service. * Use it to quickly launch an App Runner service without providing a apprunner.yaml file in the * source code repository (or ignoring the file if it exists). * * @default - not specified. Use `apprunner.yaml` instead. */ readonly configurationValues?: CodeConfigurationValues; /** * The source of the App Runner configuration. */ readonly configurationSource: ConfigurationSourceType; } /** * Describes resources needed to authenticate access to some source repositories. * The specific resource depends on the repository provider. */ interface AuthenticationConfiguration { /** * The Amazon Resource Name (ARN) of the IAM role that grants the App Runner service access to a * source repository. It's required for ECR image repositories (but not for ECR Public repositories). * * @default - no access role. */ readonly accessRoleArn?: string; /** * The Amazon Resource Name (ARN) of the App Runner connection that enables the App Runner service * to connect to a source repository. It's required for GitHub code repositories. * * @default - no connection. */ readonly connectionArn?: string; } /** * Describes the basic configuration needed for building and running an AWS App Runner service. * This type doesn't support the full set of possible configuration options. Fur full configuration capabilities, * use a `apprunner.yaml` file in the source code repository. */ export interface CodeConfigurationValues { /** * The command App Runner runs to build your application. * * @default - no build command. */ readonly buildCommand?: string; /** * The port that your application listens to in the container. * * @default 8080 */ readonly port?: string; /** * A runtime environment type for building and running an App Runner service. It represents * a programming language runtime. */ readonly runtime: Runtime; /** * The environment variables that are available to your running App Runner service. * * @default - no environment variables. * @deprecated use environmentVariables. */ readonly environment?: { [key: string]: string }; /** * The environment variables that are available to your running App Runner service. * * @default - no environment variables. */ readonly environmentVariables?: { [key: string]: string }; /** * The environment secrets that are available to your running App Runner service. * * @default - no environment secrets. */ readonly environmentSecrets?: { [key: string]: Secret }; /** * The command App Runner runs to start your application. * * @default - no start command. */ readonly startCommand?: string; } /** * Represents the App Runner connection that enables the App Runner service to connect * to a source repository. It's required for GitHub code repositories. */ export class GitHubConnection { /** * Using existing App Runner connection by specifying the connection ARN. * @param arn connection ARN * @returns Connection */ public static fromConnectionArn(arn: string): GitHubConnection { return new GitHubConnection(arn); } /** * The ARN of the Connection for App Runner service to connect to the repository. */ public readonly connectionArn: string; constructor(arn: string) { this.connectionArn = arn; } } /** * The health check protocol type */ export enum HealthCheckProtocolType { /** * HTTP protocol */ HTTP = 'HTTP', /** * TCP protocol */ TCP = 'TCP', } /** * Describes the settings for the health check that AWS App Runner performs to monitor the health of a service. */ interface HealthCheckCommonOptions { /** * The number of consecutive checks that must succeed before App Runner decides that the service is healthy. * * @default 1 */ readonly healthyThreshold?: number; /** * The time interval, in seconds, between health checks. * * @default Duration.seconds(5) */ readonly interval?: cdk.Duration; /** * The time, in seconds, to wait for a health check response before deciding it failed. * * @default Duration.seconds(2) */ readonly timeout?: cdk.Duration; /** * The number of consecutive checks that must fail before App Runner decides that the service is unhealthy. * * @default 5 */ readonly unhealthyThreshold?: number; } /** * Properties used to define HTTP Based healthchecks. */ export interface HttpHealthCheckOptions extends HealthCheckCommonOptions { /** * The URL that health check requests are sent to. * * @default / */ readonly path?: string; } /** * Properties used to define TCP Based healthchecks. */ export interface TcpHealthCheckOptions extends HealthCheckCommonOptions { } /** * Contains static factory methods for creating health checks for different protocols */ export class HealthCheck { /** * Construct a HTTP health check */ public static http(options: HttpHealthCheckOptions = {}): HealthCheck { return new HealthCheck( HealthCheckProtocolType.HTTP, options.healthyThreshold, options.interval, options.timeout, options.unhealthyThreshold, options.path, ); } /** * Construct a TCP health check */ public static tcp(options: TcpHealthCheckOptions = {}): HealthCheck { return new HealthCheck( HealthCheckProtocolType.TCP, options.healthyThreshold, options.interval, options.timeout, options.unhealthyThreshold, ); } private constructor( public readonly healthCheckProtocolType: HealthCheckProtocolType, public readonly healthyThreshold: number = 1, public readonly interval: cdk.Duration = cdk.Duration.seconds(5), public readonly timeout: cdk.Duration = cdk.Duration.seconds(2), public readonly unhealthyThreshold: number = 5, public readonly path?: string, ) { if (this.healthCheckProtocolType === HealthCheckProtocolType.HTTP) { if (this.path !== undefined && this.path.length === 0) { throw new cdk.UnscopedValidationError('path length must be greater than 0'); } if (this.path === undefined) { this.path = '/'; } } if (this.healthyThreshold < 1 || this.healthyThreshold > 20) { throw new cdk.UnscopedValidationError(`healthyThreshold must be between 1 and 20, got ${this.healthyThreshold}`); } if (this.unhealthyThreshold < 1 || this.unhealthyThreshold > 20) { throw new cdk.UnscopedValidationError(`unhealthyThreshold must be between 1 and 20, got ${this.unhealthyThreshold}`); } if (this.interval.toSeconds() < 1 || this.interval.toSeconds() > 20) { throw new cdk.UnscopedValidationError(`interval must be between 1 and 20 seconds, got ${this.interval.toSeconds()}`); } if (this.timeout.toSeconds() < 1 || this.timeout.toSeconds() > 20) { throw new cdk.UnscopedValidationError(`timeout must be between 1 and 20 seconds, got ${this.timeout.toSeconds()}`); } } public bind(): CfnService.HealthCheckConfigurationProperty { return { healthyThreshold: this.healthyThreshold, interval: this.interval?.toSeconds(), path: this.path, protocol: this.healthCheckProtocolType, timeout: this.timeout?.toSeconds(), unhealthyThreshold: this.unhealthyThreshold, }; } } /** * The IP address type for your incoming public network configuration. */ export enum IpAddressType { /** * IPV4 */ IPV4 = 'IPV4', /** * DUAL_STACK */ DUAL_STACK = 'DUAL_STACK', } /** * Attributes for the App Runner Service */ export interface ServiceAttributes { /** * The name of the service. */ readonly serviceName: string; /** * The ARN of the service. */ readonly serviceArn: string; /** * The URL of the service. */ readonly serviceUrl: string; /** * The status of the service. */ readonly serviceStatus: string; } /** * Represents the App Runner Service. */ export interface IService extends cdk.IResource { /** * The Name of the service. */ readonly serviceName: string; /** * The ARN of the service. * @attribute */ readonly serviceArn: string; } /** * A secret environment variable. */ export abstract class Secret { /** * Creates an environment variable value from a parameter stored in AWS * Systems Manager Parameter Store. */ public static fromSsmParameter(parameter: ssm.IParameter): Secret { return { arn: parameter.parameterArn, grantRead: grantee => parameter.grantRead(grantee), }; } /** * Creates a environment variable value from a secret stored in AWS Secrets * Manager. * * @param secret the secret stored in AWS Secrets Manager * @param field the name of the field with the value that you want to set as * the environment variable value. Only values in JSON format are supported. * If you do not specify a JSON field, then the full content of the secret is * used. */ public static fromSecretsManager(secret: secretsmanager.ISecret, field?: string): Secret { return { arn: field ? `${secret.secretArn}:${field}::` : secret.secretArn, hasField: !!field, grantRead: grantee => secret.grantRead(grantee), }; } /** * Creates a environment variable value from a secret stored in AWS Secrets * Manager. * * @param secret the secret stored in AWS Secrets Manager * @param versionInfo the version information to reference the secret * @param field the name of the field with the value that you want to set as * the environment variable value. Only values in JSON format are supported. * If you do not specify a JSON field, then the full content of the secret is * used. */ public static fromSecretsManagerVersion(secret: secretsmanager.ISecret, versionInfo: SecretVersionInfo, field?: string): Secret { return { arn: `${secret.secretArn}:${field ?? ''}:${versionInfo.versionStage ?? ''}:${versionInfo.versionId ?? ''}`, hasField: !!field, grantRead: grantee => secret.grantRead(grantee), }; } /** * The ARN of the secret */ public abstract readonly arn: string; /** * Whether this secret uses a specific JSON field */ public abstract readonly hasField?: boolean; /** * Grants reading the secret to a principal */ public abstract grantRead(grantee: iam.IGrantable): iam.Grant; } /** * The App Runner Service. */ export class Service extends cdk.Resource implements IService, iam.IGrantable { /** * Import from service name. */ public static fromServiceName(scope: Construct, id: string, serviceName: string): IService { class Import extends cdk.Resource { public serviceName = serviceName; public serviceArn = cdk.Stack.of(this).formatArn({ resource: 'service', service: 'apprunner', resourceName: serviceName, }); } return new Import(scope, id); } /** * Import from service attributes. */ public static fromServiceAttributes(scope: Construct, id: string, attrs: ServiceAttributes): IService { const serviceArn = attrs.serviceArn; const serviceName = attrs.serviceName; const serviceUrl = attrs.serviceUrl; const serviceStatus = attrs.serviceStatus; class Import extends cdk.Resource { public readonly serviceArn = serviceArn; public readonly serviceName = serviceName; public readonly serviceUrl = serviceUrl; public readonly serviceStatus = serviceStatus; } return new Import(scope, id); } public readonly grantPrincipal: iam.IPrincipal; private readonly props: ServiceProps; private accessRole?: iam.IRole; private instanceRole: iam.IRole; private source: SourceConfig; /** * Environment variables for this service. * * @deprecated use environmentVariables. */ readonly environment: { [key: string]: string } = {}; /** * Environment secrets for this service. */ private readonly secrets: EnvironmentSecret[] = []; /** * Environment variables for this service. */ private readonly variables: EnvironmentVariable[] = []; /** * The ARN of the Service. * @attribute */ readonly serviceArn: string; /** * The ID of the Service. * @attribute */ readonly serviceId: string; /** * The URL of the Service. * @attribute */ readonly serviceUrl: string; /** * The status of the Service. * @attribute */ readonly serviceStatus: string; /** * The name of the service. */ readonly serviceName: string; public constructor(scope: Construct, id: string, props: ServiceProps) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); const source = props.source.bind(this); this.source = source; this.props = props; this.instanceRole = this.props.instanceRole ?? this.createInstanceRole(); this.grantPrincipal = this.instanceRole; const environmentVariables = this.getEnvironmentVariables(); const environmentSecrets = this.getEnvironmentSecrets(); for (const [key, value] of Object.entries(environmentVariables)) { this.addEnvironmentVariable(key, value); } for (const [key, value] of Object.entries(environmentSecrets)) { this.addSecret(key, value); } // generate an IAM role only when ImageRepositoryType is ECR and props.accessRole is undefined this.accessRole = (this.source.imageRepository?.imageRepositoryType == ImageRepositoryType.ECR) ? this.props.accessRole ?? this.generateDefaultRole() : undefined; if (this.source.codeRepository?.codeConfiguration.configurationSource == ConfigurationSourceType.REPOSITORY && this.source.codeRepository?.codeConfiguration.configurationValues) { throw new cdk.ValidationError('configurationValues cannot be provided if the ConfigurationSource is Repository', this); } if (props.serviceName !== undefined && !cdk.Token.isUnresolved(props.serviceName)) { if (props.serviceName.length < 4 || props.serviceName.length > 40) { throw new cdk.ValidationError( `\`serviceName\` must be between 4 and 40 characters, got: ${props.serviceName.length} characters.`, this, ); } if (!/^[A-Za-z0-9][A-Za-z0-9\-_]*$/.test(props.serviceName)) { throw new cdk.ValidationError( `\`serviceName\` must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores after that, got: ${props.serviceName}.`, this, ); } } const resource = new CfnService(this, 'Resource', { serviceName: this.props.serviceName, instanceConfiguration: { cpu: this.props.cpu?.unit, memory: this.props.memory?.unit, instanceRoleArn: Lazy.string({ produce: () => this.instanceRole?.roleArn }), }, sourceConfiguration: { authenticationConfiguration: this.renderAuthenticationConfiguration(), autoDeploymentsEnabled: this.props.autoDeploymentsEnabled, imageRepository: this.source.imageRepository ? this.renderImageRepository(this.source.imageRepository!) : undefined, codeRepository: this.source.codeRepository ? this.renderCodeConfiguration(this.source.codeRepository!.codeConfiguration.configurationValues!) : undefined, }, encryptionConfiguration: this.props.kmsKey ? { kmsKey: this.props.kmsKey.keyArn, } : undefined, autoScalingConfigurationArn: this.props.autoScalingConfiguration?.autoScalingConfigurationArn, networkConfiguration: { egressConfiguration: { egressType: this.props.vpcConnector ? 'VPC' : 'DEFAULT', vpcConnectorArn: this.props.vpcConnector?.vpcConnectorArn, }, ingressConfiguration: props.isPubliclyAccessible !== undefined ? { isPubliclyAccessible: props.isPubliclyAccessible } : undefined, ipAddressType: this.props.ipAddressType, }, healthCheckConfiguration: this.props.healthCheck ? this.props.healthCheck.bind() : undefined, observabilityConfiguration: props.observabilityConfiguration ? { observabilityEnabled: true, observabilityConfigurationArn: props.observabilityConfiguration.observabilityConfigurationArn, } : undefined, }); // grant required privileges for the role to access an image in Amazon ECR // See https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html#security_iam_service-with-iam-roles if (this.source.ecrRepository && this.accessRole) { this.source.ecrRepository.grantPull(this.accessRole); this.source.ecrRepository.grant(this.accessRole, 'ecr:DescribeImages'); } this.serviceArn = resource.attrServiceArn; this.serviceId = resource.attrServiceId; this.serviceUrl = resource.attrServiceUrl; this.serviceStatus = resource.attrStatus; /** * Cloudformaton does not return the serviceName attribute so we extract it from the serviceArn. * The ARN comes with this format: * arn:aws:apprunner:us-east-1:123456789012:service/SERVICE_NAME/SERVICE_ID */ // First, get the last element by splitting with ':' const resourceFullName = cdk.Fn.select(5, cdk.Fn.split(':', this.serviceArn)); // Now, split the resourceFullName with '/' to get the serviceName this.serviceName = cdk.Fn.select(1, cdk.Fn.split('/', resourceFullName)); } /** * Adds a statement to the instance role. */ @MethodMetadata() public addToRolePolicy(statement: iam.PolicyStatement) { this.instanceRole.addToPrincipalPolicy(statement); } /** * This method adds an environment variable to the App Runner service. */ @MethodMetadata() public addEnvironmentVariable(name: string, value: string) { if (name.startsWith('AWSAPPRUNNER')) { throw new cdk.ValidationError(`Environment variable key ${name} with a prefix of AWSAPPRUNNER is not allowed`, this); } this.variables.push({ name: name, value: value }); } /** * This method adds a secret as environment variable to the App Runner service. */ @MethodMetadata() public addSecret(name: string, secret: Secret) { if (name.startsWith('AWSAPPRUNNER')) { throw new cdk.ValidationError(`Environment secret key ${name} with a prefix of AWSAPPRUNNER is not allowed`, this); } secret.grantRead(this.instanceRole); this.secrets.push({ name: name, value: secret.arn }); } /** * This method generates an Instance Role. Needed if using secrets and props.instanceRole is undefined * @returns iam.IRole */ private createInstanceRole(): iam.IRole { return new iam.Role(this, 'InstanceRole', { assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'), roleName: cdk.PhysicalName.GENERATE_IF_NEEDED, }); } /** * This method generates an Access Role only when ImageRepositoryType is ECR and props.accessRole is undefined * @returns iam.IRole */ private generateDefaultRole(): iam.Role { const accessRole = new iam.Role(this, 'AccessRole', { assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'), }); accessRole.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['ecr:GetAuthorizationToken'], resources: ['*'], })); this.accessRole = accessRole; return accessRole; } private getEnvironmentSecrets(): { [key: string]: Secret } { let secrets = this.source.codeRepository?.codeConfiguration.configurationValues?.environmentSecrets ?? this.source.imageRepository?.imageConfiguration?.environmentSecrets; return secrets || {}; } private getEnvironmentVariables(): { [key: string]: string } { let codeEnv = [ this.source.codeRepository?.codeConfiguration.configurationValues?.environmentVariables, this.source.codeRepository?.codeConfiguration.configurationValues?.environment, ]; let imageEnv = [ this.source.imageRepository?.imageConfiguration?.environmentVariables, this.source.imageRepository?.imageConfiguration?.environment, ]; if (codeEnv.every(el => el !== undefined) || imageEnv.every(el => el !== undefined)) { throw new cdk.ValidationError([ 'You cannot set both \'environmentVariables\' and \'environment\' properties.', 'Please only use environmentVariables, as environment is deprecated.', ].join(' '), this); } return codeEnv.find(el => el !== undefined) || imageEnv.find(el => el !== undefined) || {}; } private renderAuthenticationConfiguration(): AuthenticationConfiguration { return { accessRoleArn: this.accessRole?.roleArn, connectionArn: this.source.codeRepository?.connection?.connectionArn, }; } private renderCodeConfiguration(props: CodeConfigurationValues) { return { codeConfiguration: { configurationSource: this.source.codeRepository!.codeConfiguration.configurationSource, // codeConfigurationValues will be ignored if configurationSource is REPOSITORY codeConfigurationValues: this.source.codeRepository!.codeConfiguration.configurationValues ? this.renderCodeConfigurationValues(props) : undefined, }, repositoryUrl: this.source.codeRepository!.repositoryUrl, sourceCodeVersion: this.source.codeRepository!.sourceCodeVersion, }; } private renderCodeConfigurationValues(props: CodeConfigurationValues): any { return { port: props.port, buildCommand: props.buildCommand, runtime: props.runtime.name, runtimeEnvironmentVariables: Lazy.any({ produce: () => this.renderEnvironmentVariables() }), runtimeEnvironmentSecrets: Lazy.any({ produce: () => this.renderEnvironmentSecrets() }), startCommand: props.startCommand, }; } private renderEnvironmentVariables(): EnvironmentVariable[] | undefined { if (this.variables.length > 0) { return this.variables; } else { return undefined; } } private renderEnvironmentSecrets(): EnvironmentSecret[] | undefined { if (this.secrets.length > 0 && this.instanceRole) { return this.secrets; } else { return undefined; } } private renderImageRepository(repo: ImageRepository): any { return Object.assign(repo, { imageConfiguration: { port: repo.imageConfiguration?.port?.toString(), startCommand: repo.imageConfiguration?.startCommand, runtimeEnvironmentVariables: Lazy.any({ produce: () => this.renderEnvironmentVariables() }), runtimeEnvironmentSecrets: Lazy.any({ produce: () => this.renderEnvironmentSecrets() }), }, }); } }