From 7e4292c06a305548ef639b682bfcd4045cc54b6a Mon Sep 17 00:00:00 2001 From: Luca Pizzini Date: Tue, 27 Jun 2023 09:56:53 +0200 Subject: [PATCH 1/4] feat(apprunner): expose instanceRole from Service instance --- .../aws-apprunner-alpha/lib/service.ts | 11 ++++++++++ .../aws-apprunner-alpha/test/service.test.ts | 21 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts b/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts index 9d46156370d91..885630751f201 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts +++ b/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts @@ -1140,6 +1140,17 @@ export class Service extends cdk.Resource { this.secrets.push({ name: name, value: secret.arn }); } + /** + * This method exposes the Instance Role after creating it if not set. + * @returns iam.IRole + */ + public obtainInstanceRole(): iam.IRole { + if (!this.instanceRole) { + this.instanceRole = this.createInstanceRole(); + } + return this.instanceRole; + } + /** * This method generates an Instance Role. Needed if using secrets and props.instanceRole is undefined * @returns iam.IRole diff --git a/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts b/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts index 6e0df962a7461..01b14178fc9bb 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts +++ b/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts @@ -1252,4 +1252,23 @@ testDeprecated('Using both environmentVariables and environment should throw an }), }); }).toThrow(/You cannot set both \'environmentVariables\' and \'environment\' properties./); -}); \ No newline at end of file +}); + +test('Service exposes instanceRole via obtainInstanceRole()', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + const service = new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + instanceRole: new iam.Role(stack, 'InstanceRole', { + assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'), + }), + }); + // THEN + expect(stack.resolve(service.obtainInstanceRole().roleArn)).toEqual({ + 'Fn::GetAtt': ['InstanceRole3CCE2F1D', 'Arn'], + }); +}); From b784781c5f3d36069afd1e6af006d0b590fe49fd Mon Sep 17 00:00:00 2001 From: Luca Pizzini Date: Wed, 28 Jun 2023 09:38:28 +0200 Subject: [PATCH 2/4] made Service implement IGrantable --- .../aws-apprunner-alpha/lib/service.ts | 15 ++-------- .../aws-apprunner-alpha/test/service.test.ts | 29 +++++++++++++++++-- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts b/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts index 885630751f201..dd754f6f811f7 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts +++ b/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts @@ -959,7 +959,7 @@ export abstract class Secret { /** * The App Runner Service. */ -export class Service extends cdk.Resource { +export class Service extends cdk.Resource implements iam.IGrantable { /** * Import from service name. */ @@ -993,6 +993,7 @@ export class Service extends cdk.Resource { return new Import(scope, id); } + public readonly grantPrincipal: iam.IPrincipal; private readonly props: ServiceProps; private accessRole?: iam.IRole; private instanceRole?: iam.IRole; @@ -1052,6 +1053,7 @@ export class Service extends cdk.Resource { this.props = props; this.instanceRole = this.props.instanceRole; + this.grantPrincipal = this.instanceRole || new iam.UnknownPrincipal({ resource: this }); const environmentVariables = this.getEnvironmentVariables(); const environmentSecrets = this.getEnvironmentSecrets(); @@ -1140,17 +1142,6 @@ export class Service extends cdk.Resource { this.secrets.push({ name: name, value: secret.arn }); } - /** - * This method exposes the Instance Role after creating it if not set. - * @returns iam.IRole - */ - public obtainInstanceRole(): iam.IRole { - if (!this.instanceRole) { - this.instanceRole = this.createInstanceRole(); - } - return this.instanceRole; - } - /** * This method generates an Instance Role. Needed if using secrets and props.instanceRole is undefined * @returns iam.IRole diff --git a/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts b/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts index 01b14178fc9bb..6c84ad692ce2c 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts +++ b/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts @@ -6,6 +6,7 @@ import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as ssm from 'aws-cdk-lib/aws-ssm'; +import * as s3 from 'aws-cdk-lib/aws-s3'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from 'aws-cdk-lib'; import * as apprunner from '../lib'; @@ -1254,11 +1255,12 @@ testDeprecated('Using both environmentVariables and environment should throw an }).toThrow(/You cannot set both \'environmentVariables\' and \'environment\' properties./); }); -test('Service exposes instanceRole via obtainInstanceRole()', () => { +test('Service is grantable', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'demo-stack'); // WHEN + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); const service = new apprunner.Service(stack, 'DemoService', { source: apprunner.Source.fromEcrPublic({ imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', @@ -1267,8 +1269,29 @@ test('Service exposes instanceRole via obtainInstanceRole()', () => { assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'), }), }); + + bucket.grantRead(service); + // THEN - expect(stack.resolve(service.obtainInstanceRole().roleArn)).toEqual({ - 'Fn::GetAtt': ['InstanceRole3CCE2F1D', 'Arn'], + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + Resource: [ + 'arn:aws:s3:::my-bucket', + 'arn:aws:s3:::my-bucket/*', + ], + }, + ], + }, + PolicyName: 'InstanceRoleDefaultPolicy1531605C', + Roles: [ + { Ref: 'InstanceRole3CCE2F1D' }, + ], }); }); From 1c0bdcd44e8cb88735b2a24e3ebe1bb6675b31d2 Mon Sep 17 00:00:00 2001 From: Luca Pizzini Date: Fri, 30 Jun 2023 09:59:03 +0200 Subject: [PATCH 3/4] added automatic instance role creation and addToRolePolicy method --- .../aws-apprunner-alpha/lib/service.ts | 18 ++++--- .../integ-apprunner-ecr-public.template.json | 28 +++++++++- .../integ-apprunner.template.json | 54 +++++++++++++++++-- .../integ-apprunner.template.json | 52 +++++++++++++++++- .../integ-apprunner.template.json | 54 +++++++++++++++++-- .../aws-apprunner-alpha/test/service.test.ts | 35 ++++++++++++ 6 files changed, 224 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts b/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts index dd754f6f811f7..ef414b1f94528 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts +++ b/packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts @@ -689,7 +689,7 @@ export interface ServiceProps { * * @see https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html#security_iam_service-with-iam-roles-service.instance * - * @default - no instance role attached. + * @default - generate a new instance role. */ readonly instanceRole?: iam.IRole; @@ -996,7 +996,7 @@ export class Service extends cdk.Resource implements iam.IGrantable { public readonly grantPrincipal: iam.IPrincipal; private readonly props: ServiceProps; private accessRole?: iam.IRole; - private instanceRole?: iam.IRole; + private instanceRole: iam.IRole; private source: SourceConfig; /** @@ -1052,8 +1052,8 @@ export class Service extends cdk.Resource implements iam.IGrantable { this.source = source; this.props = props; - this.instanceRole = this.props.instanceRole; - this.grantPrincipal = this.instanceRole || new iam.UnknownPrincipal({ resource: this }); + this.instanceRole = this.props.instanceRole ?? this.createInstanceRole(); + this.grantPrincipal = this.instanceRole; const environmentVariables = this.getEnvironmentVariables(); const environmentSecrets = this.getEnvironmentSecrets(); @@ -1118,6 +1118,13 @@ export class Service extends cdk.Resource implements iam.IGrantable { this.serviceName = cdk.Fn.select(1, cdk.Fn.split('/', resourceFullName)); } + /** + * Adds a statement to the instance role. + */ + public addToRolePolicy(statement: iam.PolicyStatement) { + this.instanceRole.addToPrincipalPolicy(statement); + } + /** * This method adds an environment variable to the App Runner service. */ @@ -1135,9 +1142,6 @@ export class Service extends cdk.Resource implements iam.IGrantable { if (name.startsWith('AWSAPPRUNNER')) { throw new Error(`Environment secret key ${name} with a prefix of AWSAPPRUNNER is not allowed`); } - if (!this.instanceRole) { - this.instanceRole = this.createInstanceRole(); - } secret.grantRead(this.instanceRole); this.secrets.push({ name: name, value: secret.arn }); } diff --git a/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-ecr-public.js.snapshot/integ-apprunner-ecr-public.template.json b/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-ecr-public.js.snapshot/integ-apprunner-ecr-public.template.json index 67d76b4027d6e..e8d7ecd160aa8 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-ecr-public.js.snapshot/integ-apprunner-ecr-public.template.json +++ b/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-ecr-public.js.snapshot/integ-apprunner-ecr-public.template.json @@ -14,14 +14,38 @@ "ImageRepositoryType": "ECR_PUBLIC" } }, - "InstanceConfiguration": {}, + "InstanceConfiguration": { + "InstanceRoleArn": { + "Fn::GetAtt": [ + "Service1InstanceRole8CBC81F1", + "Arn" + ] + } + }, "NetworkConfiguration": { "EgressConfiguration": { "EgressType": "DEFAULT" } } } - } + }, + "Service1InstanceRole8CBC81F1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + } }, "Outputs": { "URL1": { diff --git a/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-ecr.js.snapshot/integ-apprunner.template.json b/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-ecr.js.snapshot/integ-apprunner.template.json index 83013f96c6e09..f3c01fbb4534b 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-ecr.js.snapshot/integ-apprunner.template.json +++ b/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-ecr.js.snapshot/integ-apprunner.template.json @@ -92,7 +92,14 @@ "ImageRepositoryType": "ECR" } }, - "InstanceConfiguration": {}, + "InstanceConfiguration": { + "InstanceRoleArn": { + "Fn::GetAtt": [ + "Service3InstanceRoleD40BEE82", + "Arn" + ] + } + }, "NetworkConfiguration": { "EgressConfiguration": { "EgressType": "DEFAULT" @@ -211,14 +218,55 @@ "ImageRepositoryType": "ECR" } }, - "InstanceConfiguration": {}, + "InstanceConfiguration": { + "InstanceRoleArn": { + "Fn::GetAtt": [ + "Service2InstanceRole3F57F2AA", + "Arn" + ] + } + }, "NetworkConfiguration": { "EgressConfiguration": { "EgressType": "DEFAULT" } } } - } + }, + "Service3InstanceRoleD40BEE82": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Service2InstanceRole3F57F2AA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + } }, "Outputs": { "URL3": { diff --git a/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-github.js.snapshot/integ-apprunner.template.json b/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-github.js.snapshot/integ-apprunner.template.json index 07b02b87671e9..fbf688bbfb608 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-github.js.snapshot/integ-apprunner.template.json +++ b/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-github.js.snapshot/integ-apprunner.template.json @@ -18,7 +18,14 @@ } } }, - "InstanceConfiguration": {}, + "InstanceConfiguration": { + "InstanceRoleArn": { + "Fn::GetAtt": [ + "Service4InstanceRole26B443A0", + "Arn" + ] + } + }, "NetworkConfiguration": { "EgressConfiguration": { "EgressType": "DEFAULT" @@ -50,13 +57,54 @@ } } }, - "InstanceConfiguration": {}, + "InstanceConfiguration": { + "InstanceRoleArn": { + "Fn::GetAtt": [ + "Service5InstanceRole94C07D84", + "Arn" + ] + } + }, "NetworkConfiguration": { "EgressConfiguration": { "EgressType": "DEFAULT" } } } + }, + "Service4InstanceRole26B443A0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Service5InstanceRole94C07D84": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-vpc-connector.js.snapshot/integ-apprunner.template.json b/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-vpc-connector.js.snapshot/integ-apprunner.template.json index ed52375306ed4..bd3398fca1d38 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-vpc-connector.js.snapshot/integ-apprunner.template.json +++ b/packages/@aws-cdk/aws-apprunner-alpha/test/integ.service-vpc-connector.js.snapshot/integ-apprunner.template.json @@ -442,7 +442,14 @@ "ImageRepositoryType": "ECR_PUBLIC" } }, - "InstanceConfiguration": {}, + "InstanceConfiguration": { + "InstanceRoleArn": { + "Fn::GetAtt": [ + "Service6InstanceRole7220D460", + "Arn" + ] + } + }, "NetworkConfiguration": { "EgressConfiguration": { "EgressType": "VPC", @@ -469,7 +476,14 @@ "ImageRepositoryType": "ECR_PUBLIC" } }, - "InstanceConfiguration": {}, + "InstanceConfiguration": { + "InstanceRoleArn": { + "Fn::GetAtt": [ + "Service7InstanceRoleFD40F312", + "Arn" + ] + } + }, "NetworkConfiguration": { "EgressConfiguration": { "EgressType": "VPC", @@ -482,7 +496,41 @@ } } } - } + }, + "Service6InstanceRole7220D460": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Service7InstanceRoleFD40F312": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "tasks.apprunner.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + } }, "Outputs": { "URL6": { diff --git a/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts b/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts index 6c84ad692ce2c..fa6a69c5a6ea3 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts +++ b/packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts @@ -1295,3 +1295,38 @@ test('Service is grantable', () => { ], }); }); + +test('addToRolePolicy', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'demo-stack'); + // WHEN + const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); + const service = new apprunner.Service(stack, 'DemoService', { + source: apprunner.Source.fromEcrPublic({ + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), + }); + + service.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:GetObject'], + resources: [bucket.bucketArn], + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:GetObject', + Resource: 'arn:aws:s3:::my-bucket', + }, + ], + }, + PolicyName: 'DemoServiceInstanceRoleDefaultPolicy9600BEA1', + Roles: [ + { Ref: 'DemoServiceInstanceRoleFCED1725' }, + ], + }); +}); \ No newline at end of file From b550db3d98a20e1563711bb1daeff95a3ea7a729 Mon Sep 17 00:00:00 2001 From: Luca Pizzini Date: Sat, 15 Jul 2023 17:24:35 +0200 Subject: [PATCH 4/4] updated README --- .../@aws-cdk/aws-apprunner-alpha/README.md | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-apprunner-alpha/README.md b/packages/@aws-cdk/aws-apprunner-alpha/README.md index 80e3bbf100830..74989c792bfda 100644 --- a/packages/@aws-cdk/aws-apprunner-alpha/README.md +++ b/packages/@aws-cdk/aws-apprunner-alpha/README.md @@ -35,6 +35,8 @@ The `Service` construct allows you to create AWS App Runner services with `ECR P - `Source.fromAsset()` - To define the source from local asset directory. +The `Service` construct implements `IGrantable`. + ## ECR Public To create a `Service` with ECR Public: @@ -124,7 +126,27 @@ new apprunner.Service(this, 'Service', { 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. +your code needs when it calls any AWS APIs. If not defined, a new instance role will be generated +when required. + +To add IAM policy statements to this role, use `addToRolePolicy()`: + +```ts +import * as iam from 'aws-cdk-lib/aws-iam'; + +const service = new apprunner.Service(this, 'Service', { + source: apprunner.Source.fromEcrPublic({ + imageConfiguration: { port: 8000 }, + imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', + }), +}); + +service.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:GetObject'], + resources: ['*'], +})) +``` `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