diff --git a/packages/@aws-cdk/aws-apprunner/README.md b/packages/@aws-cdk/aws-apprunner/README.md index de5aa782b26df..ffbec457413ab 100644 --- a/packages/@aws-cdk/aws-apprunner/README.md +++ b/packages/@aws-cdk/aws-apprunner/README.md @@ -160,3 +160,39 @@ new apprunner.Service(this, 'Service', { vpcConnector, }); ``` + +## Secrets Manager + +To include environment variables integrated with AWS Secrets Manager, use the `environmentSecrets` attribute. +You can use the `addSecret` method from the App Runner `Service` class to include secrets from outside the +service definition. + +```ts +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as ssm from '@aws-cdk/aws-ssm'; + +declare const stack: Stack; + +const secret = new secretsmanager.Secret(stack, 'Secret'); +const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/name', + version: 1, +}); + +const service = new apprunner.Service(stack, 'Service', { + source: apprunner.Source.fromEcrPublic({ + imageConfiguration: { + port: 8000, + environmentSecrets: { + SECRET: apprunner.Secret.fromSecretsManager(secret), + PARAMETER: apprunner.Secret.fromSsmParameter(parameter), + SECRET_ID: apprunner.Secret.fromSecretsManagerVersion(secret, { versionId: 'version-id' }), + SECRET_STAGE: apprunner.Secret.fromSecretsManagerVersion(secret, { versionStage: 'version-stage' }), + }, + }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }) +}); + +service.addSecret('LATER_SECRET', apprunner.Secret.fromSecretsManager(secret, 'field')); +``` diff --git a/packages/@aws-cdk/aws-apprunner/lib/service.ts b/packages/@aws-cdk/aws-apprunner/lib/service.ts index a37b5b6d40104..4ab5553ad0442 100644 --- a/packages/@aws-cdk/aws-apprunner/lib/service.ts +++ b/packages/@aws-cdk/aws-apprunner/lib/service.ts @@ -1,6 +1,8 @@ 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 secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnService } from './apprunner.generated'; @@ -160,6 +162,14 @@ export class Runtime { 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. */ @@ -227,7 +237,6 @@ export interface GithubRepositoryProps { readonly connection: GitHubConnection; } - /** * Properties of the image repository for `Source.fromEcrPublic()` */ @@ -238,6 +247,7 @@ export interface EcrPublicProps { * @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. */ @@ -254,16 +264,19 @@ export interface EcrProps { * @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' @@ -281,12 +294,31 @@ export interface AssetProps { * @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. @@ -298,24 +330,28 @@ export abstract class Source { 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. */ @@ -348,6 +384,7 @@ export class GithubSource extends Source { }; } } + /** * Represents the service source from ECR. */ @@ -430,8 +467,23 @@ export interface ImageConfiguration { * Environment variables that are available to your running App Runner service. * * @default - no environment variables + * @deprecated use environmentVariables. */ - readonly environment?: { [key: string]: string }; + 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. @@ -666,9 +718,24 @@ export interface CodeConfigurationValues { * 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. * @@ -690,6 +757,7 @@ export class GitHubConnection { public static fromConnectionArn(arn: string): GitHubConnection { return new GitHubConnection(arn); } + /** * The ARN of the Connection for App Runner service to connect to the repository. */ @@ -739,6 +807,74 @@ export interface IService extends cdk.IResource { 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. */ @@ -778,11 +914,35 @@ export class Service extends cdk.Resource { } private readonly props: ServiceProps; private accessRole?: iam.IRole; + private instanceRole?: iam.IRole; private source: SourceConfig; + /** - * Environment variables for this service + * Environment variables for this service. + * + * @deprecated use environmentVariables. */ - private environment?: { [key: string]: string } = {}; + readonly environment: { [key: string]: string } = {}; + + /** + * Environment variables for this service. + */ + private environmentVariables: { [key: string]: string } = {}; + + /** + * Environment secrets for this service. + */ + private environmentSecrets: { [key: string]: Secret; } = {}; + + /** + * Environment secrets for this service. + */ + private readonly secrets: EnvironmentSecret[] = [] + + /** + * Environment variables for this service. + */ + private readonly variables: EnvironmentVariable[] = [] /** * The ARN of the Service. @@ -821,25 +981,36 @@ export class Service extends cdk.Resource { this.source = source; this.props = props; - // generate an IAM role only when ImageRepositoryType is ECR and props.role is undefined + this.environmentVariables = this.getEnvironmentVariables(); + this.environmentSecrets = this.getEnvironmentSecrets(); + + // 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.props.accessRole : this.generateDefaultRole() : undefined; + this.props.accessRole ?? this.generateDefaultRole() : undefined; - if (source.codeRepository?.codeConfiguration.configurationSource == ConfigurationSourceType.REPOSITORY && - source.codeRepository?.codeConfiguration.configurationValues) { + // generalte an IAM role only when environmentSecrets has values and props.instanceRole is undefined + this.instanceRole = (Object.keys(this.environmentSecrets).length > 0 && !this.props.instanceRole) ? + this.createInstanceRole() : this.props.instanceRole; + + if (this.source.codeRepository?.codeConfiguration.configurationSource == ConfigurationSourceType.REPOSITORY && + this.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, + cpu: this.props.cpu?.unit, + memory: this.props.memory?.unit, + instanceRoleArn: this.instanceRole?.roleArn, }, sourceConfiguration: { authenticationConfiguration: this.renderAuthenticationConfiguration(), - imageRepository: source.imageRepository ? this.renderImageRepository() : undefined, - codeRepository: source.codeRepository ? this.renderCodeConfiguration() : undefined, + imageRepository: this.source.imageRepository ? + this.renderImageRepository(this.source.imageRepository!) : + undefined, + codeRepository: this.source.codeRepository ? + this.renderCodeConfiguration(this.source.codeRepository!.codeConfiguration.configurationValues!) : + undefined, }, networkConfiguration: { egressConfiguration: { @@ -850,8 +1021,8 @@ export class Service extends cdk.Resource { }); // grant required privileges for the role - if (source.ecrRepository && this.accessRole) { - source.ecrRepository.grantPull(this.accessRole); + if (this.source.ecrRepository && this.accessRole) { + this.source.ecrRepository.grantPull(this.accessRole); } this.serviceArn = resource.attrServiceArn; @@ -860,71 +1031,149 @@ export class Service extends cdk.Resource { this.serviceStatus = resource.attrStatus; this.serviceName = resource.ref; } + + /** + * This method adds an environment variable to the App Runner service. + */ + public addEnvironmentVariable(name: string, value: string) { + this.variables.push({ name: name, value: value }); + } + + /** + * This method adds a secret as environment variable to the App Runner service. + */ + public addSecret(name: string, secret: Secret) { + if (!this.instanceRole) { + this.instanceRole = this.createInstanceRole(); + } + 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 Error([ + 'You cannot set both \'environmentVariables\' and \'environment\' properties.', + 'Please only use environmentVariables, as environment is deprecated.', + ].join(' ')); + } + + 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() { + + 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(this.source.codeRepository!.codeConfiguration.configurationValues) : undefined, + this.renderCodeConfigurationValues(props) : + undefined, }, repositoryUrl: this.source.codeRepository!.repositoryUrl, sourceCodeVersion: this.source.codeRepository!.sourceCodeVersion, }; - } + private renderCodeConfigurationValues(props: CodeConfigurationValues): any { - this.environment = props.environment; return { port: props.port, buildCommand: props.buildCommand, runtime: props.runtime.name, runtimeEnvironmentVariables: this.renderEnvironmentVariables(), + runtimeEnvironmentSecrets: this.renderEnvironmentSecrets(), startCommand: props.startCommand, }; } - private renderImageRepository(): any { - const repo = this.source.imageRepository!; - this.environment = repo.imageConfiguration?.environment; - return Object.assign(repo, { - imageConfiguration: { - port: repo.imageConfiguration?.port?.toString(), - startCommand: repo.imageConfiguration?.startCommand, - runtimeEnvironmentVariables: this.renderEnvironmentVariables(), - }, - }); - } private renderEnvironmentVariables(): EnvironmentVariable[] | undefined { - if (this.environment) { - let env: EnvironmentVariable[] = []; - for (const [key, value] of Object.entries(this.environment)) { + if (Object.keys(this.environmentVariables).length > 0) { + for (const [key, value] of Object.entries(this.environmentVariables)) { if (key.startsWith('AWSAPPRUNNER')) { throw new Error(`Environment variable key ${key} with a prefix of AWSAPPRUNNER is not allowed`); } - env.push({ name: key, value: value }); + this.variables.push({ name: key, value: value }); } - return env; + return this.variables; } else { return undefined; } } - private generateDefaultRole(): iam.Role { - const accessRole = new iam.Role(this, 'AccessRole', { - assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'), + private renderEnvironmentSecrets(): EnvironmentSecret[] | undefined { + if (Object.keys(this.environmentSecrets).length > 0 && this.instanceRole) { + for (const [key, value] of Object.entries(this.environmentSecrets)) { + if (key.startsWith('AWSAPPRUNNER')) { + throw new Error(`Environment secret key ${key} with a prefix of AWSAPPRUNNER is not allowed`); + } + + value.grantRead(this.instanceRole); + this.secrets.push({ name: key, value: value.arn }); + } + 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: this.renderEnvironmentVariables(), + runtimeEnvironmentSecrets: this.renderEnvironmentSecrets(), + }, }); - 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 e05b1991509b8..e4c4e7747b193 100644 --- a/packages/@aws-cdk/aws-apprunner/package.json +++ b/packages/@aws-cdk/aws-apprunner/package.json @@ -83,27 +83,39 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-ecr-assets": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-ssm": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", - "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.5.2" + "@types/jest": "^27.5.2", + "constructs": "^10.0.0" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", - "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-ssm": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, "peerDependencies": { "@aws-cdk/aws-ec2": "0.0.0", - "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-ssm": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, diff --git a/packages/@aws-cdk/aws-apprunner/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-apprunner/rosetta/default.ts-fixture index b74a57c217fbd..585b54c64cf81 100644 --- a/packages/@aws-cdk/aws-apprunner/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-apprunner/rosetta/default.ts-fixture @@ -1,5 +1,5 @@ // Fixture with packages imported, but nothing else -import { Stack } from '@aws-cdk/core'; +import { Stack, SecretValue } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as apprunner from '@aws-cdk/aws-apprunner'; import * as path from 'path'; diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.assets.json b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.assets.json new file mode 100644 index 0000000000000..6a1de7aa5e5dd --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.template.json b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/cdk.out b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/cdk.out new file mode 100644 index 0000000000000..d8b441d447f8a --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ-apprunner-secrets-manager.assets.json b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ-apprunner-secrets-manager.assets.json new file mode 100644 index 0000000000000..0d2a4279683d7 --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ-apprunner-secrets-manager.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "9c8d514c785fb19cbd23269b19753175ccbb3324a5998af72a0855c5dff09e83": { + "source": { + "path": "integ-apprunner-secrets-manager.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "9c8d514c785fb19cbd23269b19753175ccbb3324a5998af72a0855c5dff09e83.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ-apprunner-secrets-manager.template.json b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ-apprunner-secrets-manager.template.json new file mode 100644 index 0000000000000..48ed28d733e26 --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ-apprunner-secrets-manager.template.json @@ -0,0 +1,225 @@ +{ + "Resources": { + "SecretA720EF05": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "SecretString": "{\"password\":\"mySecretPassword\",\"apikey\":\"mySecretApiKey\"}" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "String0BA8456E": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "Abc123", + "Name": "/My/Public/Parameter" + } + }, + "Service8InstanceRole6CC2A03A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Service8InstanceRoleDefaultPolicy7BC4087D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue" + ], + "Effect": "Allow", + "Resource": { + "Ref": "SecretA720EF05" + } + }, + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory", + "ssm:GetParameters" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/My/Public/Parameter" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Service8InstanceRoleDefaultPolicy7BC4087D", + "Roles": [ + { + "Ref": "Service8InstanceRole6CC2A03A" + } + ] + } + }, + "Service86269A78B": { + "Type": "AWS::AppRunner::Service", + "Properties": { + "SourceConfiguration": { + "AuthenticationConfiguration": {}, + "ImageRepository": { + "ImageConfiguration": { + "Port": "8000", + "RuntimeEnvironmentSecrets": [ + { + "Name": "PASSWORD", + "Value": { + "Fn::Join": [ + "", + [ + { + "Ref": "SecretA720EF05" + }, + ":password::" + ] + ] + } + }, + { + "Name": "PARAMETER", + "Value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/My/Public/Parameter" + ] + ] + } + }, + { + "Name": "API_KEY", + "Value": { + "Fn::Join": [ + "", + [ + { + "Ref": "SecretA720EF05" + }, + ":apikey::" + ] + ] + } + } + ] + }, + "ImageIdentifier": "public.ecr.aws/aws-containers/hello-app-runner:latest", + "ImageRepositoryType": "ECR_PUBLIC" + } + }, + "InstanceConfiguration": { + "InstanceRoleArn": { + "Fn::GetAtt": [ + "Service8InstanceRole6CC2A03A", + "Arn" + ] + } + }, + "NetworkConfiguration": { + "EgressConfiguration": { + "EgressType": "DEFAULT" + } + } + } + } + }, + "Outputs": { + "URL8": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "Service86269A78B", + "ServiceUrl" + ] + } + ] + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ.json b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ.json new file mode 100644 index 0000000000000..0be359a9018cc --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "29.0.0", + "testCases": { + "AppRunnerSecretsManger/DefaultTest": { + "stacks": [ + "integ-apprunner-secrets-manager" + ], + "assertionStack": "AppRunnerSecretsManger/DefaultTest/DeployAssert", + "assertionStackName": "AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/manifest.json b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/manifest.json new file mode 100644 index 0000000000000..6161ee5dd7214 --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/manifest.json @@ -0,0 +1,141 @@ +{ + "version": "29.0.0", + "artifacts": { + "integ-apprunner-secrets-manager.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-apprunner-secrets-manager.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-apprunner-secrets-manager": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-apprunner-secrets-manager.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/9c8d514c785fb19cbd23269b19753175ccbb3324a5998af72a0855c5dff09e83.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-apprunner-secrets-manager.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integ-apprunner-secrets-manager.assets" + ], + "metadata": { + "/integ-apprunner-secrets-manager/Secret/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "SecretA720EF05" + } + ], + "/integ-apprunner-secrets-manager/String/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "String0BA8456E" + } + ], + "/integ-apprunner-secrets-manager/Service8/InstanceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Service8InstanceRole6CC2A03A" + } + ], + "/integ-apprunner-secrets-manager/Service8/InstanceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Service8InstanceRoleDefaultPolicy7BC4087D" + } + ], + "/integ-apprunner-secrets-manager/Service8/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Service86269A78B" + } + ], + "/integ-apprunner-secrets-manager/URL8": [ + { + "type": "aws:cdk:logicalId", + "data": "URL8" + } + ], + "/integ-apprunner-secrets-manager/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-apprunner-secrets-manager/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-apprunner-secrets-manager" + }, + "AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "AppRunnerSecretsMangerDefaultTestDeployAssert6B977D95.assets" + ], + "metadata": { + "/AppRunnerSecretsManger/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/AppRunnerSecretsManger/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "AppRunnerSecretsManger/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/tree.json b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/tree.json new file mode 100644 index 0000000000000..dea6f76b6f788 --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.js.snapshot/tree.json @@ -0,0 +1,382 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "integ-apprunner-secrets-manager": { + "id": "integ-apprunner-secrets-manager", + "path": "integ-apprunner-secrets-manager", + "children": { + "Secret": { + "id": "Secret", + "path": "integ-apprunner-secrets-manager/Secret", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-apprunner-secrets-manager/Secret/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SecretsManager::Secret", + "aws:cdk:cloudformation:props": { + "secretString": "{\"password\":\"mySecretPassword\",\"apikey\":\"mySecretApiKey\"}" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-secretsmanager.CfnSecret", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-secretsmanager.Secret", + "version": "0.0.0" + } + }, + "String": { + "id": "String", + "path": "integ-apprunner-secrets-manager/String", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-apprunner-secrets-manager/String/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SSM::Parameter", + "aws:cdk:cloudformation:props": { + "type": "String", + "value": "Abc123", + "name": "/My/Public/Parameter" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ssm.CfnParameter", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ssm.StringParameter", + "version": "0.0.0" + } + }, + "Parameter": { + "id": "Parameter", + "path": "integ-apprunner-secrets-manager/Parameter", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Service8": { + "id": "Service8", + "path": "integ-apprunner-secrets-manager/Service8", + "children": { + "InstanceRole": { + "id": "InstanceRole", + "path": "integ-apprunner-secrets-manager/Service8/InstanceRole", + "children": { + "ImportInstanceRole": { + "id": "ImportInstanceRole", + "path": "integ-apprunner-secrets-manager/Service8/InstanceRole/ImportInstanceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-apprunner-secrets-manager/Service8/InstanceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "integ-apprunner-secrets-manager/Service8/InstanceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-apprunner-secrets-manager/Service8/InstanceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue" + ], + "Effect": "Allow", + "Resource": { + "Ref": "SecretA720EF05" + } + }, + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory", + "ssm:GetParameters" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/My/Public/Parameter" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "Service8InstanceRoleDefaultPolicy7BC4087D", + "roles": [ + { + "Ref": "Service8InstanceRole6CC2A03A" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-apprunner-secrets-manager/Service8/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppRunner::Service", + "aws:cdk:cloudformation:props": { + "sourceConfiguration": { + "authenticationConfiguration": {}, + "imageRepository": { + "imageConfiguration": { + "port": "8000", + "runtimeEnvironmentSecrets": [ + { + "name": "PASSWORD", + "value": { + "Fn::Join": [ + "", + [ + { + "Ref": "SecretA720EF05" + }, + ":password::" + ] + ] + } + }, + { + "name": "PARAMETER", + "value": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/My/Public/Parameter" + ] + ] + } + }, + { + "name": "API_KEY", + "value": { + "Fn::Join": [ + "", + [ + { + "Ref": "SecretA720EF05" + }, + ":apikey::" + ] + ] + } + } + ] + }, + "imageIdentifier": "public.ecr.aws/aws-containers/hello-app-runner:latest", + "imageRepositoryType": "ECR_PUBLIC" + } + }, + "instanceConfiguration": { + "instanceRoleArn": { + "Fn::GetAtt": [ + "Service8InstanceRole6CC2A03A", + "Arn" + ] + } + }, + "networkConfiguration": { + "egressConfiguration": { + "egressType": "DEFAULT" + } + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apprunner.CfnService", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-apprunner.Service", + "version": "0.0.0" + } + }, + "URL8": { + "id": "URL8", + "path": "integ-apprunner-secrets-manager/URL8", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-apprunner-secrets-manager/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-apprunner-secrets-manager/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "AppRunnerSecretsManger": { + "id": "AppRunnerSecretsManger", + "path": "AppRunnerSecretsManger", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "AppRunnerSecretsManger/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "AppRunnerSecretsManger/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.209" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "AppRunnerSecretsManger/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "AppRunnerSecretsManger/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "AppRunnerSecretsManger/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.209" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.ts b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.ts new file mode 100644 index 0000000000000..2adc7193cfee1 --- /dev/null +++ b/packages/@aws-cdk/aws-apprunner/test/integ.service-secrets-manager.ts @@ -0,0 +1,50 @@ +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as ssm from '@aws-cdk/aws-ssm'; +import * as cdk from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import * as apprunner from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'integ-apprunner-secrets-manager'); + +// Scenario 8: Create the service from ECR public with secrets manager environment variable +const secret = new secretsmanager.Secret(stack, 'Secret', { + secretObjectValue: { + password: cdk.SecretValue.unsafePlainText('mySecretPassword'), + apikey: cdk.SecretValue.unsafePlainText('mySecretApiKey'), + }, +}); + +new ssm.StringParameter(stack, 'String', { + parameterName: '/My/Public/Parameter', + stringValue: 'Abc123', +}); + +const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/My/Public/Parameter', + version: 1, +}); + +const service8 = new apprunner.Service(stack, 'Service8', { + source: apprunner.Source.fromEcrPublic({ + imageConfiguration: { + port: 8000, + environmentSecrets: { + PASSWORD: apprunner.Secret.fromSecretsManager(secret, 'password'), + PARAMETER: apprunner.Secret.fromSsmParameter(parameter), + }, + }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), +}); + +service8.addSecret('API_KEY', apprunner.Secret.fromSecretsManager(secret, 'apikey')); + +new cdk.CfnOutput(stack, 'URL8', { value: `https://${service8.serviceUrl}` }); + +new integ.IntegTest(app, 'AppRunnerSecretsManger', { + testCases: [stack], +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apprunner/test/service.test.ts b/packages/@aws-cdk/aws-apprunner/test/service.test.ts index ed41689c9f255..a5e4604278194 100644 --- a/packages/@aws-cdk/aws-apprunner/test/service.test.ts +++ b/packages/@aws-cdk/aws-apprunner/test/service.test.ts @@ -4,20 +4,25 @@ import * as ec2 from '@aws-cdk/aws-ec2'; 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 secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as ssm from '@aws-cdk/aws-ssm'; +import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; -import { Service, GitHubConnection, Runtime, Source, Cpu, Memory, ConfigurationSourceType, VpcConnector } from '../lib'; +import * as apprunner 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({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ imageConfiguration: { port: 8000 }, imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', }), }); + + // THEN // we should have the service Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { SourceConfiguration: { @@ -43,19 +48,22 @@ test('custom environment variables and start commands are allowed for imageConfi const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'DemoService', { - source: Source.fromEcrPublic({ + const service = new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ imageConfiguration: { port: 8000, - environment: { - foo: 'fooval', - bar: 'barval', + environmentVariables: { + TEST_ENVIRONMENT_VARIABLE: 'test environment variable value', }, startCommand: '/root/start-command.sh', }, imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', }), }); + + service.addEnvironmentVariable('SECOND_ENVIRONEMENT_VARIABLE', 'second test value'); + + // THEN // we should have the service Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { SourceConfiguration: { @@ -65,12 +73,136 @@ test('custom environment variables and start commands are allowed for imageConfi Port: '8000', RuntimeEnvironmentVariables: [ { - Name: 'foo', - Value: 'fooval', + Name: 'TEST_ENVIRONMENT_VARIABLE', + Value: 'test environment variable value', + }, + { + Name: 'SECOND_ENVIRONEMENT_VARIABLE', + Value: 'second test value', + }, + ], + StartCommand: '/root/start-command.sh', + }, + ImageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + ImageRepositoryType: 'ECR_PUBLIC', + }, + }, + NetworkConfiguration: { + EgressConfiguration: { + EgressType: 'DEFAULT', + }, + }, + }); +}); + +test('custom environment secrets and start commands are allowed for imageConfiguration with defined port', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + const secret = new secretsmanager.Secret(stack, 'Secret'); + const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/name', + version: 1, + }); + + const service = new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ + imageConfiguration: { + port: 8000, + environmentSecrets: { + SECRET: apprunner.Secret.fromSecretsManager(secret), + PARAMETER: apprunner.Secret.fromSsmParameter(parameter), + SECRET_ID: apprunner.Secret.fromSecretsManagerVersion(secret, { versionId: 'version-id' }), + SECRET_STAGE: apprunner.Secret.fromSecretsManagerVersion(secret, { versionStage: 'version-stage' }), + }, + startCommand: '/root/start-command.sh', + }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + }); + + service.addSecret('LATER_SECRET', apprunner.Secret.fromSecretsManager(secret, 'field')); + + // THEN + // we should have the service + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: {}, + ImageRepository: { + ImageConfiguration: { + Port: '8000', + RuntimeEnvironmentSecrets: [ + { + Name: 'SECRET', + Value: { + Ref: 'SecretA720EF05', + }, + }, + { + Name: 'PARAMETER', + Value: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter/name', + ], + ], + }, }, { - Name: 'bar', - Value: 'barval', + Name: 'SECRET_ID', + Value: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + ':::version-id', + ], + ], + }, + }, + { + Name: 'SECRET_STAGE', + Value: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + '::version-stage:', + ], + ], + }, + }, + { + Name: 'LATER_SECRET', + Value: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + ':field::', + ], + ], + }, }, ], StartCommand: '/root/start-command.sh', @@ -92,18 +224,21 @@ test('custom environment variables and start commands are allowed for imageConfi const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'DemoService', { - source: Source.fromEcrPublic({ + const service = new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ imageConfiguration: { - environment: { - foo: 'fooval', - bar: 'barval', + environmentVariables: { + TEST_ENVIRONMENT_VARIABLE: 'test environment variable value', }, startCommand: '/root/start-command.sh', }, imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', }), }); + + service.addEnvironmentVariable('SECOND_ENVIRONEMENT_VARIABLE', 'second test value'); + + // THEN // we should have the service Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { SourceConfiguration: { @@ -112,12 +247,134 @@ test('custom environment variables and start commands are allowed for imageConfi ImageConfiguration: { RuntimeEnvironmentVariables: [ { - Name: 'foo', - Value: 'fooval', + Name: 'TEST_ENVIRONMENT_VARIABLE', + Value: 'test environment variable value', }, { - Name: 'bar', - Value: 'barval', + Name: 'SECOND_ENVIRONEMENT_VARIABLE', + Value: 'second test value', + }, + ], + StartCommand: '/root/start-command.sh', + }, + ImageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + ImageRepositoryType: 'ECR_PUBLIC', + }, + }, + NetworkConfiguration: { + EgressConfiguration: { + EgressType: 'DEFAULT', + }, + }, + }); +}); + +test('custom environment secrets and start commands are allowed for imageConfiguration with port undefined', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + const secret = new secretsmanager.Secret(stack, 'Secret'); + const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/name', + version: 1, + }); + + const service = new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ + imageConfiguration: { + environmentSecrets: { + SECRET: apprunner.Secret.fromSecretsManager(secret), + PARAMETER: apprunner.Secret.fromSsmParameter(parameter), + SECRET_ID: apprunner.Secret.fromSecretsManagerVersion(secret, { versionId: 'version-id' }), + SECRET_STAGE: apprunner.Secret.fromSecretsManagerVersion(secret, { versionStage: 'version-stage' }), + }, + startCommand: '/root/start-command.sh', + }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + }); + + service.addSecret('LATER_SECRET', apprunner.Secret.fromSecretsManager(secret, 'field')); + + // THEN + // we should have the service + Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { + SourceConfiguration: { + AuthenticationConfiguration: {}, + ImageRepository: { + ImageConfiguration: { + RuntimeEnvironmentSecrets: [ + { + Name: 'SECRET', + Value: { + Ref: 'SecretA720EF05', + }, + }, + { + Name: 'PARAMETER', + Value: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ssm:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':parameter/name', + ], + ], + }, + }, + { + Name: 'SECRET_ID', + Value: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + ':::version-id', + ], + ], + }, + }, + { + Name: 'SECRET_STAGE', + Value: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + '::version-stage:', + ], + ], + }, + }, + { + Name: 'LATER_SECRET', + Value: { + 'Fn::Join': [ + '', + [ + { + Ref: 'SecretA720EF05', + }, + ':field::', + ], + ], + }, }, ], StartCommand: '/root/start-command.sh', @@ -139,8 +396,8 @@ test('create a service from existing ECR repository(image repository type: ECR)' const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'Service', { - source: Source.fromEcr({ + new apprunner.Service(stack, 'Service', { + source: apprunner.Source.fromEcr({ imageConfiguration: { port: 80 }, repository: ecr.Repository.fromRepositoryName(stack, 'NginxRepository', 'nginx'), }), @@ -215,8 +472,8 @@ test('create a service with local assets(image repository type: ECR)', () => { const dockerAsset = new ecr_assets.DockerImageAsset(stack, 'Assets', { directory: path.join(__dirname, './docker.assets'), }); - new Service(stack, 'DemoService', { - source: Source.fromAsset({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromAsset({ imageConfiguration: { port: 8000 }, asset: dockerAsset, }), @@ -273,12 +530,12 @@ test('create a service with github repository', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'DemoService', { - source: Source.fromGitHub({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromGitHub({ repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', branch: 'main', - configurationSource: ConfigurationSourceType.REPOSITORY, - connection: GitHubConnection.fromConnectionArn('MOCK'), + configurationSource: apprunner.ConfigurationSourceType.REPOSITORY, + connection: apprunner.GitHubConnection.fromConnectionArn('MOCK'), }), }); @@ -313,15 +570,15 @@ test('create a service with github repository - undefined branch name is allowed const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'DemoService', { - source: Source.fromGitHub({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromGitHub({ repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', - configurationSource: ConfigurationSourceType.API, + configurationSource: apprunner.ConfigurationSourceType.API, codeConfigurationValues: { - runtime: Runtime.PYTHON_3, + runtime: apprunner.Runtime.PYTHON_3, port: '8000', }, - connection: GitHubConnection.fromConnectionArn('MOCK'), + connection: apprunner.GitHubConnection.fromConnectionArn('MOCK'), }), }); @@ -360,24 +617,25 @@ test('create a service with github repository - buildCommand, environment and st const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'DemoService', { - source: Source.fromGitHub({ + const service = new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromGitHub({ repositoryUrl: 'https://github.com/aws-containers/hello-app-runner', - configurationSource: ConfigurationSourceType.API, + configurationSource: apprunner.ConfigurationSourceType.API, codeConfigurationValues: { - runtime: Runtime.PYTHON_3, + runtime: apprunner.Runtime.PYTHON_3, port: '8000', buildCommand: '/root/build.sh', - environment: { - foo: 'fooval', - bar: 'barval', + environmentVariables: { + TEST_ENVIRONMENT_VARIABLE: 'test environment variable value', }, startCommand: '/root/start.sh', }, - connection: GitHubConnection.fromConnectionArn('MOCK'), + connection: apprunner.GitHubConnection.fromConnectionArn('MOCK'), }), }); + service.addEnvironmentVariable('SECOND_ENVIRONEMENT_VARIABLE', 'second test value'); + // THEN // we should have the service with the branch value as 'main' Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { @@ -393,12 +651,12 @@ test('create a service with github repository - buildCommand, environment and st BuildCommand: '/root/build.sh', RuntimeEnvironmentVariables: [ { - Name: 'foo', - Value: 'fooval', + Name: 'TEST_ENVIRONMENT_VARIABLE', + Value: 'test environment variable value', }, { - Name: 'bar', - Value: 'barval', + Name: 'SECOND_ENVIRONEMENT_VARIABLE', + Value: 'second test value', }, ], StartCommand: '/root/start.sh', @@ -426,7 +684,7 @@ test('import from service name', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - const svc = Service.fromServiceName(stack, 'ImportService', 'ExistingService'); + const svc = apprunner.Service.fromServiceName(stack, 'ImportService', 'ExistingService'); // THEN expect(svc).toHaveProperty('serviceName'); expect(svc).toHaveProperty('serviceArn'); @@ -437,7 +695,7 @@ test('import from service attributes', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - const svc = Service.fromServiceAttributes(stack, 'ImportService', { + const svc = apprunner.Service.fromServiceAttributes(stack, 'ImportService', { serviceName: 'mock', serviceArn: 'mock', serviceStatus: 'mock', @@ -456,8 +714,8 @@ test('undefined imageConfiguration port is allowed', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'Service', { - source: Source.fromEcrPublic({ + new apprunner.Service(stack, 'Service', { + source: apprunner.Source.fromEcrPublic({ imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', }), }); @@ -488,8 +746,8 @@ test('custom IAM access role and instance role are allowed', () => { const dockerAsset = new ecr_assets.DockerImageAsset(stack, 'Assets', { directory: path.join(__dirname, './docker.assets'), }); - new Service(stack, 'DemoService', { - source: Source.fromAsset({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromAsset({ asset: dockerAsset, imageConfiguration: { port: 8000 }, }), @@ -546,12 +804,12 @@ test('cpu and memory properties are allowed', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'DemoService', { - source: Source.fromEcrPublic({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', }), - cpu: Cpu.ONE_VCPU, - memory: Memory.THREE_GB, + cpu: apprunner.Cpu.ONE_VCPU, + memory: apprunner.Memory.THREE_GB, }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { @@ -572,12 +830,12 @@ test('custom cpu and memory units are allowed', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN - new Service(stack, 'DemoService', { - source: Source.fromEcrPublic({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', }), - cpu: Cpu.of('Some vCPU'), - memory: Memory.of('Some GB'), + cpu: apprunner.Cpu.of('Some vCPU'), + memory: apprunner.Memory.of('Some GB'), }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::Service', { @@ -600,10 +858,10 @@ test('environment variable with a prefix of AWSAPPRUNNER should throw an error', // WHEN // we should have the service expect(() => { - new Service(stack, 'DemoService', { - source: Source.fromEcrPublic({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ imageConfiguration: { - environment: { + environmentVariables: { AWSAPPRUNNER_FOO: 'bar', }, }, @@ -613,6 +871,28 @@ test('environment variable with a prefix of AWSAPPRUNNER should throw an error', }).toThrow('Environment variable key AWSAPPRUNNER_FOO with a prefix of AWSAPPRUNNER is not allowed'); }); +test('environment secrets with a prefix of AWSAPPRUNNER should throw an error', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // WHEN + // we should have the service + expect(() => { + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ + imageConfiguration: { + environmentSecrets: { + AWSAPPRUNNER_FOO: apprunner.Secret.fromSecretsManager(secret), + }, + }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + }); + }).toThrow('Environment secret key AWSAPPRUNNER_FOO with a prefix of AWSAPPRUNNER is not allowed'); +}); + test('specifying a vpcConnector should assign the service to it and set the egressType to VPC', () => { // GIVEN const app = new cdk.App(); @@ -624,15 +904,15 @@ test('specifying a vpcConnector should assign the service to it and set the egre const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); - const vpcConnector = new VpcConnector(stack, 'VpcConnector', { + const vpcConnector = new apprunner.VpcConnector(stack, 'VpcConnector', { securityGroups: [securityGroup], vpc, vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }), vpcConnectorName: 'MyVpcConnector', }); // WHEN - new Service(stack, 'DemoService', { - source: Source.fromEcrPublic({ + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', }), vpcConnector, @@ -671,4 +951,25 @@ test('specifying a vpcConnector should assign the service to it and set the egre ], VpcConnectorName: 'MyVpcConnector', }); +}); + +testDeprecated('Using both environmentVariables and environment should throw an error', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + + expect(() => { + new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ + imageConfiguration: { + environmentVariables: { + AWSAPPRUNNER_FOO: 'bar', + }, + environment: { + AWSAPPRUNNER_FOO: 'bar', + }, + }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + }); + }).toThrow(/You cannot set both \'environmentVariables\' and \'environment\' properties./); }); \ No newline at end of file