diff --git a/packages/@aws-cdk/aws-apprunner/README.md b/packages/@aws-cdk/aws-apprunner/README.md index f6619f99bd149..e9c01f631d4b8 100644 --- a/packages/@aws-cdk/aws-apprunner/README.md +++ b/packages/@aws-cdk/aws-apprunner/README.md @@ -9,6 +9,14 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- @@ -18,3 +26,108 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw ```ts import apprunner = require('@aws-cdk/aws-apprunner'); ``` + +## Introduction + +AWS App Runner is a fully managed service that makes it easy for developers to quickly deploy containerized web applications and APIs, at scale and with no prior infrastructure experience required. Start with your source code or a container image. App Runner automatically builds and deploys the web application and load balances traffic with encryption. App Runner also scales up or down automatically to meet your traffic needs. With App Runner, rather than thinking about servers or scaling, you have more time to focus on your applications. + +## Service + +The `Service` construct allows you to create AWS App Runner services with `ECR Public`, `ECR` or `Github` with the `source` property in the following scenarios: + +- `Source.fromEcr()` - To define the source repository from `ECR`. +- `Source.fromEcrPublic()` - To define the source repository from `ECR Public`. +- `Source.fromGitHub()` - To define the source repository from the `Github repository`. +- `Source.fromAsset()` - To define the source from local asset directory. + + +## ECR Public + +To create a `Service` with ECR Public: + +```ts +new Service(stack, 'Service', { + source: Source.fromEcrPublic({ + imageConfiguration: { port: 8000 }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), +}); +``` + +## ECR + +To create a `Service` from an existing ECR repository: + +```ts +new Service(stack, 'Service', { + source: Source.fromEcr({ + imageConfiguration: { port: 80 }, + repository: ecr.Repository.fromRepositoryName(stack, 'NginxRepository', 'nginx'), + tag: 'latest', + }), +}); +``` + +To create a `Service` from local docker image asset directory built and pushed to Amazon ECR: + +```ts +const imageAsset = new assets.DockerImageAsset(stack, 'ImageAssets', { + directory: path.join(__dirname, './docker.assets'), +}); +new Service(stack, 'Service', { + source: Source.fromAsset({ + imageConfiguration: { port: 8000 }, + asset: imageAsset, + }), +}); +``` + +## GitHub + +To create a `Service` from the GitHub repository, you need to specify an existing App Runner `Connection`. + +See [Managing App Runner connections](https://docs.aws.amazon.com/apprunner/latest/dg/manage-connections.html) for more details. + +```ts +new Service(stack, 'Service', { + source: Source.fromGitHub({ + repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', + branch: 'main', + configurationSource: ConfigurationSourceType.REPOSITORY, + connection: GitHubConnection.fromConnectionArn('CONNECTION_ARN'), + }), +}); +``` + +Use `codeConfigurationValues` to override configuration values with the `API` configuration source type. + +```ts +new Service(stack, 'Service', { + source: Source.fromGitHub({ + repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', + branch: 'main', + configurationSource: ConfigurationSourceType.API, + codeConfigurationValues: { + runtime: Runtime.PYTHON_3, + port: '8000', + startCommand: 'python app.py', + buildCommand: 'yum install -y pycairo && pip install -r requirements.txt', + }, + connection: GitHubConnection.fromConnectionArn('CONNECTION_ARN'), + }), +}); +``` + + +## IAM Roles + +You are allowed to define `instanceRole` and `accessRole` for the `Service`. + +`instanceRole` - The IAM role that provides permissions to your App Runner service. These are permissions that +your code needs when it calls any AWS APIs. + +`accessRole` - 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). If not defined, a new access role will be generated +when required. + +See [App Runner IAM Roles](https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html#security_iam_service-with-iam-roles) for more details. diff --git a/packages/@aws-cdk/aws-apprunner/lib/index.ts b/packages/@aws-cdk/aws-apprunner/lib/index.ts index bebaa074b0dd0..1aedf192186b1 100644 --- a/packages/@aws-cdk/aws-apprunner/lib/index.ts +++ b/packages/@aws-cdk/aws-apprunner/lib/index.ts @@ -1,2 +1,3 @@ // AWS::AppRunner CloudFormation Resources: export * from './apprunner.generated'; +export * from './service'; diff --git a/packages/@aws-cdk/aws-apprunner/lib/service.ts b/packages/@aws-cdk/aws-apprunner/lib/service.ts new file mode 100644 index 0000000000000..ce944ed420e86 --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/lib/service.ts @@ -0,0 +1,831 @@ +import * as ecr from '@aws-cdk/aws-ecr'; +import * as assets from '@aws-cdk/aws-ecr-assets'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnService } from './'; + +/** + * 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 { + /** + * 1 vCPU + */ + public static readonly ONE_VCPU = Cpu.of('1 vCPU') + + /** + * 2 vCPU + */ + public static readonly TWO_VCPU = Cpu.of('2 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) { 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 { + /** + * 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') + + /** + * 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) { return new Memory(unit); } + + /** + * + * @param unit The unit of memory. + */ + private constructor(public readonly unit: string) { } +} + +/** + * The code runtimes + */ +export class Runtime { + /** + * NodeJS 12 + */ + public static readonly NODEJS_12 = Runtime.of('NODEJS_12') + + /** + * Python 3 + */ + public static readonly PYTHON_3 = Runtime.of('PYTHON_3') + + /** + * 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) { } +} + + +/** + * 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' + */ + readonly tag?: 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; +} + + +/** + * 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.repositoryUriForTag(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 + */ + readonly environment?: { [key: string]: string }; + + /** + * 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 for more details. + */ + 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; + + /** + * 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). + * + * @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. + * + * @default - no instance role attached. + */ + readonly instanceRole?: iam.IRole; + + /** + * Name of the service. + * + * @default - auto-generated if undefined. + */ + readonly serviceName?: string; +} + +/** + * 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). + * + * @defult - 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. + */ + readonly environment?: { [key: string]: string }; + + /** + * 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; + } +} + +/** + * 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. + */ + readonly serviceArn: string; +} + +/** + * The App Runner Service. + */ +export class Service extends cdk.Resource { + /** + * 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); + } + private readonly props: ServiceProps; + private accessRole?: iam.IRole; + private source: SourceConfig; + + /** + * 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. + * @attribute + */ + readonly serviceName: string; + + public constructor(scope: Construct, id: string, props: ServiceProps) { + super(scope, id); + + const source = props.source.bind(this); + this.source = source; + this.props = props; + + // generate an IAM role only when ImageRepositoryType is ECR and props.role is undefined + this.accessRole = (this.source.imageRepository?.imageRepositoryType == ImageRepositoryType.ECR) ? + this.props.accessRole ? this.props.accessRole : this.generateDefaultRole() : undefined; + + if (source.codeRepository?.codeConfiguration.configurationSource == ConfigurationSourceType.REPOSITORY && + source.codeRepository?.codeConfiguration.configurationValues) { + throw new Error('configurationValues cannot be provided if the ConfigurationSource is Repository'); + } + + const resource = new CfnService(this, 'Resource', { + instanceConfiguration: { + cpu: props.cpu?.unit, + memory: props.memory?.unit, + instanceRoleArn: props.instanceRole?.roleArn, + }, + sourceConfiguration: { + authenticationConfiguration: this.renderAuthenticationConfiguration(), + imageRepository: source.imageRepository ? this.renderImageRepository() : undefined, + codeRepository: source.codeRepository ? this.renderCodeConfiguration() : undefined, + }, + }); + + // grant required privileges for the role + if (source.ecrRepository && this.accessRole) { + source.ecrRepository.grantPull(this.accessRole); + } + + this.serviceArn = resource.attrServiceArn; + this.serviceId = resource.attrServiceId; + this.serviceUrl = resource.attrServiceUrl; + this.serviceStatus = resource.attrStatus; + this.serviceName = resource.ref; + } + private renderAuthenticationConfiguration(): AuthenticationConfiguration { + return { + accessRoleArn: this.accessRole?.roleArn, + connectionArn: this.source.codeRepository?.connection?.connectionArn, + }; + } + private renderCodeConfiguration() { + return { + codeConfiguration: { + configurationSource: this.source.codeRepository!.codeConfiguration.configurationSource, + // codeConfigurationValues will be ignored if configurationSource is REPOSITORY + codeConfigurationValues: this.source.codeRepository!.codeConfiguration.configurationValues ? + this.renderCodeConfigurationValues(this.source.codeRepository!.codeConfiguration.configurationValues) : undefined, + }, + repositoryUrl: this.source.codeRepository!.repositoryUrl, + sourceCodeVersion: this.source.codeRepository!.sourceCodeVersion, + }; + + } + private renderCodeConfigurationValues(props: CodeConfigurationValues): any { + return { + ...props, + runtime: props.runtime.name, + }; + } + private renderImageRepository(): any { + const repo = this.source.imageRepository!; + if (repo.imageConfiguration?.port) { + // convert port from type number to string + return Object.assign(repo, { + imageConfiguration: { + port: repo.imageConfiguration.port.toString(), + }, + }); + } else { + return repo; + } + } + + 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; + } +} diff --git a/packages/@aws-cdk/aws-apprunner/package.json b/packages/@aws-cdk/aws-apprunner/package.json index eb70477718a20..a662f36841659 100644 --- a/packages/@aws-cdk/aws-apprunner/package.json +++ b/packages/@aws-cdk/aws-apprunner/package.json @@ -77,22 +77,32 @@ "license": "Apache-2.0", "devDependencies": { "@types/jest": "^26.0.24", + "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/assertions": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/core": "0.0.0" + "constructs": "^3.3.69", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", + "@aws-cdk/aws-ecr-assets": "0.0.0" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-ecr-assets": "0.0.0", + "constructs": "^3.3.69", + "@aws-cdk/aws-ecr": "0.0.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-apprunner/test/apprunner.test.ts b/packages/@aws-cdk/aws-apprunner/test/apprunner.test.ts deleted file mode 100644 index 465c7bdea0693..0000000000000 --- a/packages/@aws-cdk/aws-apprunner/test/apprunner.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assertions'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-apprunner/test/docker.assets/Dockerfile b/packages/@aws-cdk/aws-apprunner/test/docker.assets/Dockerfile new file mode 100644 index 0000000000000..878fb18669506 --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/docker.assets/Dockerfile @@ -0,0 +1,2 @@ +# image from https://gallery.ecr.aws/aws-containers/hello-app-runner +FROM public.ecr.aws/aws-containers/hello-app-runner:latest diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service.expected.json b/packages/@aws-cdk/aws-apprunner/test/integ.service.expected.json new file mode 100644 index 0000000000000..ed37fa7666d4c --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service.expected.json @@ -0,0 +1,364 @@ +{ + "Resources": { + "Service1EDCC8134": { + "Type": "AWS::AppRunner::Service", + "Properties": { + "SourceConfiguration": { + "AuthenticationConfiguration": {}, + "ImageRepository": { + "ImageConfiguration": { + "Port": "8000" + }, + "ImageIdentifier": "public.ecr.aws/aws-containers/hello-app-runner:latest", + "ImageRepositoryType": "ECR_PUBLIC" + } + }, + "InstanceConfiguration": {} + } + }, + "Service2AccessRole759CA73D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "build.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Service2AccessRoleDefaultPolicy08C28479": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/nginx" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Service2AccessRoleDefaultPolicy08C28479", + "Roles": [ + { + "Ref": "Service2AccessRole759CA73D" + } + ] + } + }, + "Service2AB4D14D8": { + "Type": "AWS::AppRunner::Service", + "Properties": { + "SourceConfiguration": { + "AuthenticationConfiguration": { + "AccessRoleArn": { + "Fn::GetAtt": [ + "Service2AccessRole759CA73D", + "Arn" + ] + } + }, + "ImageRepository": { + "ImageConfiguration": { + "Port": "80" + }, + "ImageIdentifier": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::AccountId" + }, + ".dkr.ecr.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/nginx:latest" + ] + ] + }, + "ImageRepositoryType": "ECR" + } + }, + "InstanceConfiguration": {} + } + }, + "Service3AccessRole3ACBAAA0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "build.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Service3AccessRoleDefaultPolicy57B9744E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/aws-cdk/assets" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Service3AccessRoleDefaultPolicy57B9744E", + "Roles": [ + { + "Ref": "Service3AccessRole3ACBAAA0" + } + ] + } + }, + "Service342D067F2": { + "Type": "AWS::AppRunner::Service", + "Properties": { + "SourceConfiguration": { + "AuthenticationConfiguration": { + "AccessRoleArn": { + "Fn::GetAtt": [ + "Service3AccessRole3ACBAAA0", + "Arn" + ] + } + }, + "ImageRepository": { + "ImageConfiguration": { + "Port": "8000" + }, + "ImageIdentifier": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::AccountId" + }, + ".dkr.ecr.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/aws-cdk/assets:77284835684772d19c95f4f5a37e7618d5f9efc40db9321d44ac039db457b967" + ] + ] + }, + "ImageRepositoryType": "ECR" + } + }, + "InstanceConfiguration": {} + } + }, + "Service429949929": { + "Type": "AWS::AppRunner::Service", + "Properties": { + "SourceConfiguration": { + "AuthenticationConfiguration": { + "ConnectionArn": "MOCK" + }, + "CodeRepository": { + "CodeConfiguration": { + "ConfigurationSource": "REPOSITORY" + }, + "RepositoryUrl": "https://github.com/aws-containers/hello-app-runner", + "SourceCodeVersion": { + "Type": "BRANCH", + "Value": "main" + } + } + }, + "InstanceConfiguration": {} + } + }, + "Service5AD92B5A5": { + "Type": "AWS::AppRunner::Service", + "Properties": { + "SourceConfiguration": { + "AuthenticationConfiguration": { + "ConnectionArn": "MOCK" + }, + "CodeRepository": { + "CodeConfiguration": { + "CodeConfigurationValues": { + "BuildCommand": "yum install -y pycairo && pip install -r requirements.txt", + "Port": "8000", + "Runtime": "PYTHON_3", + "StartCommand": "python app.py" + }, + "ConfigurationSource": "API" + }, + "RepositoryUrl": "https://github.com/aws-containers/hello-app-runner", + "SourceCodeVersion": { + "Type": "BRANCH", + "Value": "main" + } + } + }, + "InstanceConfiguration": {} + } + } + }, + "Outputs": { + "URL1": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "Service1EDCC8134", + "ServiceUrl" + ] + } + ] + ] + } + }, + "URL2": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "Service2AB4D14D8", + "ServiceUrl" + ] + } + ] + ] + } + }, + "URL3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "Service342D067F2", + "ServiceUrl" + ] + } + ] + ] + } + }, + "URL4": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "Service429949929", + "ServiceUrl" + ] + } + ] + ] + } + }, + "URL5": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "Service5AD92B5A5", + "ServiceUrl" + ] + } + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service.ts b/packages/@aws-cdk/aws-apprunner/test/integ.service.ts new file mode 100644 index 0000000000000..2df2dab9301aa --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service.ts @@ -0,0 +1,71 @@ +import * as path from 'path'; +import * as ecr from '@aws-cdk/aws-ecr'; +import * as assets from '@aws-cdk/aws-ecr-assets'; +import * as cdk from '@aws-cdk/core'; +import { Service, Source, GitHubConnection, ConfigurationSourceType, Runtime } from '../lib'; + + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'integ-apprunner'); + +// Scenario 1: Create the service from ECR public +const service1 = new Service(stack, 'Service1', { + source: Source.fromEcrPublic({ + imageConfiguration: { + port: 8000, + }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), +}); +new cdk.CfnOutput(stack, 'URL1', { value: `https://${service1.serviceUrl}` }); + +// Scenario 2: Create the service from existing ECR repository, make sure you have `nginx` ECR repo in your account. +const service2 = new Service(stack, 'Service2', { + source: Source.fromEcr({ + imageConfiguration: { port: 80 }, + repository: ecr.Repository.fromRepositoryName(stack, 'NginxRepository', 'nginx'), + }), +}); +new cdk.CfnOutput(stack, 'URL2', { value: `https://${service2.serviceUrl}` }); + +// Scenario 3: Create the service from local code assets +const imageAsset = new assets.DockerImageAsset(stack, 'ImageAssets', { + directory: path.join(__dirname, './docker.assets'), +}); +const service3 = new Service(stack, 'Service3', { + source: Source.fromAsset({ + imageConfiguration: { port: 8000 }, + asset: imageAsset, + }), +}); +new cdk.CfnOutput(stack, 'URL3', { value: `https://${service3.serviceUrl}` }); + +// Scenario 4: Create the service from Github. Make sure you specify a valid connection ARN. +const connectionArn = stack.node.tryGetContext('CONNECTION_ARN') || 'MOCK'; +const service4 = new Service(stack, 'Service4', { + source: Source.fromGitHub({ + repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', + branch: 'main', + configurationSource: ConfigurationSourceType.REPOSITORY, + connection: GitHubConnection.fromConnectionArn(connectionArn), + }), +}); +new cdk.CfnOutput(stack, 'URL4', { value: `https://${service4.serviceUrl}` }); + +// Scenario 5: Create the service from Github with configuration values override. +const service5 = new Service(stack, 'Service5', { + source: Source.fromGitHub({ + repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', + branch: 'main', + configurationSource: ConfigurationSourceType.API, + codeConfigurationValues: { + runtime: Runtime.PYTHON_3, + port: '8000', + startCommand: 'python app.py', + buildCommand: 'yum install -y pycairo && pip install -r requirements.txt', + }, + connection: GitHubConnection.fromConnectionArn(connectionArn), + }), +}); +new cdk.CfnOutput(stack, 'URL5', { value: `https://${service5.serviceUrl}` }); diff --git a/packages/@aws-cdk/aws-apprunner/test/service.test.ts b/packages/@aws-cdk/aws-apprunner/test/service.test.ts new file mode 100644 index 0000000000000..a36cc97950119 --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/service.test.ts @@ -0,0 +1,419 @@ +import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; +import * as ecr from '@aws-cdk/aws-ecr'; +import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Service, GitHubConnection, Runtime, Source, Cpu, Memory, ConfigurationSourceType } from '../lib'; + +test('create a service with ECR Public(image repository type: ECR_PUBLIC)', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + new Service(stack, 'DemoService', { + source: Source.fromEcrPublic({ + imageConfiguration: { port: 8000 }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + }); + // we should have the service + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: {}, + ImageRepository: { + ImageConfiguration: { + Port: '8000', + }, + ImageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + ImageRepositoryType: 'ECR_PUBLIC', + }, + }, + }); +}); + +test('create a service from existing ECR repository(image repository type: ECR)', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + new Service(stack, 'Service', { + source: Source.fromEcr({ + imageConfiguration: { port: 80 }, + repository: ecr.Repository.fromRepositoryName(stack, 'NginxRepository', 'nginx'), + }), + }); + + // THEN + // we should have an IAM role + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'build.apprunner.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + // we should have the service + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: { + AccessRoleArn: { + 'Fn::GetAtt': [ + 'ServiceAccessRole4763579D', + 'Arn', + ], + }, + }, + ImageRepository: { + ImageConfiguration: { + Port: '80', + }, + ImageIdentifier: { + 'Fn::Join': [ + '', + [ + { + Ref: 'AWS::AccountId', + }, + '.dkr.ecr.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/nginx:latest', + ], + ], + }, + ImageRepositoryType: 'ECR', + }, + }, + }); +}); + +test('create a service with local assets(image repository type: ECR)', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + const dockerAsset = new ecr_assets.DockerImageAsset(stack, 'Assets', { + directory: path.join(__dirname, './docker.assets'), + }); + new Service(stack, 'DemoService', { + source: Source.fromAsset({ + imageConfiguration: { port: 8000 }, + asset: dockerAsset, + }), + }); + + // THEN + // we should have an IAM role + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'build.apprunner.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + // we should have the service + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: { + AccessRoleArn: { + 'Fn::GetAtt': [ + 'DemoServiceAccessRoleE7F08742', + 'Arn', + ], + }, + }, + ImageRepository: { + ImageConfiguration: { + Port: '8000', + }, + ImageIdentifier: { + 'Fn::Join': [ + '', + [ + { + Ref: 'AWS::AccountId', + }, + '.dkr.ecr.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/aws-cdk/assets:e9db95c5eb5c683b56dbb8a1930ab8b028babb58b58058d72fa77071e38e66a4', + ], + ], + }, + ImageRepositoryType: 'ECR', + }, + }, + }); +}); + + +test('create a service with github repository', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + new Service(stack, 'DemoService', { + source: Source.fromGitHub({ + repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', + branch: 'main', + configurationSource: ConfigurationSourceType.REPOSITORY, + connection: GitHubConnection.fromConnectionArn('MOCK'), + }), + }); + + // THEN + // we should have the service + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: { + ConnectionArn: 'MOCK', + }, + CodeRepository: { + CodeConfiguration: { + ConfigurationSource: 'REPOSITORY', + }, + RepositoryUrl: 'https://github.com/aws-containers/hello-app-runner', + SourceCodeVersion: { + Type: 'BRANCH', + Value: 'main', + }, + }, + }, + }); +}); + +test('create a service with github repository - undefined branch name is allowed', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + new Service(stack, 'DemoService', { + source: Source.fromGitHub({ + repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', + configurationSource: ConfigurationSourceType.API, + codeConfigurationValues: { + runtime: Runtime.PYTHON_3, + port: '8000', + }, + connection: GitHubConnection.fromConnectionArn('MOCK'), + }), + }); + + // THEN + // we should have the service with the branch value as 'main' + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: { + ConnectionArn: 'MOCK', + }, + CodeRepository: { + CodeConfiguration: { + CodeConfigurationValues: { + Port: '8000', + Runtime: 'PYTHON_3', + }, + ConfigurationSource: 'API', + }, + RepositoryUrl: 'https://github.com/aws-containers/hello-app-runner', + SourceCodeVersion: { + Type: 'BRANCH', + Value: 'main', + }, + }, + }, + }); +}); + + +test('import from service name', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + const svc = Service.fromServiceName(stack, 'ImportService', 'ExistingService'); + // THEN + expect(svc).toHaveProperty('serviceName'); + expect(svc).toHaveProperty('serviceArn'); +}); + +test('import from service attributes', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + const svc = Service.fromServiceAttributes(stack, 'ImportService', { + serviceName: 'mock', + serviceArn: 'mock', + serviceStatus: 'mock', + serviceUrl: 'mock', + }); + // THEN + expect(svc).toHaveProperty('serviceName'); + expect(svc).toHaveProperty('serviceArn'); + expect(svc).toHaveProperty('serviceStatus'); + expect(svc).toHaveProperty('serviceUrl'); +}); + + +test('undefined imageConfiguration port is allowed', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + new Service(stack, 'Service', { + source: Source.fromEcrPublic({ + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + }); + + // THEN + // we should have the service + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: {}, + ImageRepository: { + ImageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + ImageRepositoryType: 'ECR_PUBLIC', + }, + }, + }); +}); + +test('custom IAM access role and instance role are allowed', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + const dockerAsset = new ecr_assets.DockerImageAsset(stack, 'Assets', { + directory: path.join(__dirname, './docker.assets'), + }); + new Service(stack, 'DemoService', { + source: Source.fromAsset({ + asset: dockerAsset, + imageConfiguration: { port: 8000 }, + }), + accessRole: new iam.Role(stack, 'AccessRole', { + assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppRunnerServicePolicyForECRAccess'), + ], + }), + instanceRole: new iam.Role(stack, 'InstanceRole', { + assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'), + }), + }); + // THEN + // we should have the service with the branch value as 'main' + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: { + AccessRoleArn: { + 'Fn::GetAtt': [ + 'AccessRoleEC309AE6', + 'Arn', + ], + }, + }, + ImageRepository: { + ImageConfiguration: { + Port: '8000', + }, + ImageIdentifier: { + 'Fn::Join': [ + '', + [ + { + Ref: 'AWS::AccountId', + }, + '.dkr.ecr.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/aws-cdk/assets:e9db95c5eb5c683b56dbb8a1930ab8b028babb58b58058d72fa77071e38e66a4', + ], + ], + }, + ImageRepositoryType: 'ECR', + }, + }, + InstanceConfiguration: { + InstanceRoleArn: { + 'Fn::GetAtt': [ + 'InstanceRole3CCE2F1D', + 'Arn', + ], + }, + }, + }); +}); + +test('cpu and memory properties are allowed', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + new Service(stack, 'DemoService', { + source: Source.fromEcrPublic({ + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + cpu: Cpu.ONE_VCPU, + memory: Memory.THREE_GB, + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + InstanceConfiguration: { + Cpu: '1 vCPU', + Memory: '3 GB', + }, + }); +}); + +test('custom cpu and memory units are allowed', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + new Service(stack, 'DemoService', { + source: Source.fromEcrPublic({ + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + cpu: Cpu.of('Some vCPU'), + memory: Memory.of('Some GB'), + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + InstanceConfiguration: { + Cpu: 'Some vCPU', + Memory: 'Some GB', + }, + }); +});