-
Notifications
You must be signed in to change notification settings - Fork 41
feat(core): add shared execution role + executionRole getter #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2a28d9b
f86b25c
b60ad50
4945953
87c39ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --- | ||
| "@aws-blocks/core": patch | ||
| --- | ||
|
|
||
| feat(core): add a shared Blocks execution role and `Scope.executionRole` getter | ||
|
|
||
| The Blocks stack/backend now provisions one explicit IAM role (with | ||
| `AWSLambdaBasicExecutionRole` attached) that the handler assumes, and exposes it | ||
| as `executionRole`. A new `Scope.executionRole` getter resolves the role from | ||
| any Building Block. Additive and non-breaking: the same handler is created, now | ||
| backed by an explicit role instead of an auto-generated one, with block grants | ||
| sitting on the role's default policy exactly as before. | ||
|
|
||
| Migration note: on an existing deployed stack, upgrading replaces the Lambda | ||
| execution role — CloudFormation deletes the old auto-generated role and creates | ||
| the new `BlocksRole`. This is runtime-equivalent (the same grants re-attach to | ||
| the new role) and needs no action, but a change-set diff will show a role | ||
| delete+create rather than a no-op. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| import * as cdk from 'aws-cdk-lib'; | ||
| import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs'; | ||
| import * as apigateway from 'aws-cdk-lib/aws-apigateway'; | ||
| import * as iam from 'aws-cdk-lib/aws-iam'; | ||
| import { CfnGroup } from 'aws-cdk-lib/aws-resourcegroups'; | ||
| import { Construct } from 'constructs'; | ||
| import { pathToFileURL } from 'node:url'; | ||
|
|
@@ -48,10 +49,29 @@ export interface BlocksBackendProps { | |
|
|
||
| /** Shared infra setup — creates Lambda + API Gateway on the given scope. */ | ||
| export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id?: string) { | ||
| // ── Shared execution role ────────────────────────────────────────────── | ||
| // A single IAM role that every Building Block grants to. Provisioned here so | ||
| // it exists before the backend module is imported (Building Blocks reach it | ||
| // via `scope.executionRole`). Block grants sit on the role's default (inline) | ||
| // policy, exactly as they did on the auto-generated NodejsFunction role. | ||
| // | ||
| // AWSLambdaBasicExecutionRole is attached explicitly because the auto-role | ||
| // included it by default — omitting it would silently break CloudWatch Logs. | ||
| const executionRole = new iam.Role(scope, 'BlocksRole', { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Non-breaking / synth output unchanged" is true at the runtime level but not at the CloudFormation level, and the gap matters for anyone upgrading a live stack. On base, the handler used Consequence: on an existing deployed stack,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — you are right that this is a CloudFormation-level role replacement, not a true no-op. Added a migration note to the changeset: upgrading an existing stack deletes the auto-generated role and creates |
||
| // CompositePrincipal (rather than a bare ServicePrincipal) so additional | ||
| // compute types can assume this same shared role as they are introduced | ||
| // (e.g. ECS tasks via ecs-tasks.amazonaws.com), by adding principals here. | ||
| assumedBy: new iam.CompositePrincipal(new iam.ServicePrincipal('lambda.amazonaws.com')), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional scaffolding — went with your lean and added a comment. It is there so additional compute types can assume the same shared role by adding principals (e.g. ecs-tasks.amazonaws.com) rather than restructuring the role later. Comment now makes that legible at the call site. |
||
| managedPolicies: [ | ||
| iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), | ||
| ], | ||
| }); | ||
|
|
||
| const handler = new lambda.NodejsFunction(scope, 'Handler', { | ||
| entry: props.backendHandlerPath, | ||
| runtime: DEFAULT_NODE_RUNTIME, | ||
| handler: 'handler', | ||
| role: executionRole, | ||
| memorySize: 2048, | ||
| timeout: cdk.Duration.seconds(60 * 15), | ||
| environment: { | ||
|
|
@@ -152,7 +172,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id | |
|
|
||
| registerBuiltinRoutes(); | ||
|
|
||
| return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}` }; | ||
| return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}`, executionRole }; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -177,6 +197,8 @@ export class BlocksBackend extends Construct { | |
| public readonly gateway: apigateway.RestApi; | ||
| public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction; | ||
| public readonly backendHandlerPath: string; | ||
| /** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */ | ||
| public readonly executionRole: iam.IRole; | ||
|
|
||
| /** | ||
| * The fullId used by child Scopes to compute their env var names, | ||
|
|
@@ -221,6 +243,7 @@ export class BlocksBackend extends Construct { | |
| this.handler = infra.handler; | ||
| this.gateway = infra.gateway; | ||
| this.apiUrl = infra.apiUrl; | ||
| this.executionRole = infra.executionRole; | ||
|
|
||
| // Override BLOCKS_STACK_NAME to include the parent stack name so runtime | ||
| // resource lookups (DynamoDB table names) match the CDK-time fullId | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,8 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack { | |
| public readonly gateway: cdk.aws_apigateway.RestApi; | ||
| public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction; | ||
| public readonly backendHandlerPath: string; | ||
| /** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */ | ||
| public readonly executionRole: cdk.aws_iam.IRole; | ||
|
|
||
| private constructor(scope: Construct, id: string, props: BlocksStackProps) { | ||
| super(scope, id, props); | ||
|
|
@@ -43,6 +45,7 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack { | |
| this.handler = infra.handler; | ||
| this.gateway = infra.gateway; | ||
| this.apiUrl = infra.apiUrl; | ||
| this.executionRole = infra.executionRole; | ||
| } | ||
|
|
||
| static async create(scope: Construct, id: string, props: BlocksStackProps) { | ||
|
|
@@ -103,6 +106,28 @@ export class Scope extends Construct { | |
| return ((globalThis as any).CURRENT_BLOCKS_STACK as { handler: cdk.aws_lambda_nodejs.NodejsFunction }).handler; | ||
| } | ||
|
|
||
| /** | ||
| * The shared IAM role assumed by all Blocks compute. Building Blocks grant | ||
| * their permissions to this role instead of to an individual function's | ||
| * auto-role. CDK's `grant*()` / `addToPrincipalPolicy()` route those grants | ||
| * to the role's default (inline) policy — exactly where they landed on the | ||
| * auto-generated role before. | ||
| * | ||
| * Resolves the same way as {@link handler}: walk up to the owning | ||
| * BlocksStack/BlocksBackend, falling back to the ambient stack. | ||
| */ | ||
| get executionRole(): cdk.aws_iam.IRole { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplication: this getter's tree-walk is now a verbatim copy of Extract the shared traversal, e.g.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deliberately leaving these as two copies. |
||
| let current: Construct = this; | ||
| while (current.node.scope) { | ||
| current = current.node.scope as Construct; | ||
| if (current instanceof BlocksStack || current instanceof BlocksBackend) { | ||
| return current.executionRole; | ||
| } | ||
| } | ||
| // Fallback to globalThis for backward compatibility | ||
| return ((globalThis as any).CURRENT_BLOCKS_STACK as { executionRole: cdk.aws_iam.IRole }).executionRole; | ||
| } | ||
|
|
||
| get fullId(): string { | ||
| return computeScopeFullId(this); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test-coverage gaps (non-blocking, but this is the foundation the whole A-series builds on, so worth locking down now):
BlocksStackpath is untested. All four new tests exerciseBlocksBackend. Butindex.tsBlocksStackgained the identicalexecutionRolesurface + wiring, and it has its ownblocks-stack.test.ts.setupBlocksInfrais shared so behavior should match, but "should" is exactly what a test pins — a mirror test inblocks-stack.test.ts(or a note explaining why the shared-infra test covers both) closes it.return ((globalThis as any).CURRENT_BLOCKS_STACK...).executionRolefallback — the branch that fires for a parent-less Scope with no owner in the tree — is never exercised. That's the branch most likely to throw (undefined deref) in real misuse, so it's the one most worth a test.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both gaps now covered in blocks-stack.test.ts: (1) a BlocksStack mirror test asserting the executionRole surface, the handler assuming BlocksRole, and nested-block resolution landing a grant on the role inline policy; (2) a test for the getter globalThis fallback branch — a Scope parented under a plain cdk.Stack (no Blocks owner in the tree) resolves via globalThis.CURRENT_BLOCKS_STACK.