Skip to content

Commit 4acccf3

Browse files
committed
fix(cdk): grant ec2:DescribeAvailabilityZones to ECS agent task role (cdk synth on fresh clone)
A CDK-based target repo's build gate runs `cdk synth`. When the stack is wired to a concrete env ({account, region}), CDK does a synth-time availability-zone context lookup (ec2:DescribeAvailabilityZones). A developer box caches the result in the gitignored cdk.context.json so synth is hermetic; the agent clones fresh, so there is no cache and the live lookup fires. The ECS task role lacked the action, so synth failed with: ERROR ...EcsAgentClusterTaskRole... is not authorized to perform: ec2:DescribeAvailabilityZones ... Synthesis finished with errors → a FALSE build-gate failure on code that builds fine everywhere else. Same ECS-parity class as ABCA-488 (GetSecretValue) and F-2 (CreateEvent): a permission the AgentCore path had but the ECS task role didn't. Live-caught on the ABCA fork: baseline `mise run build` passed all 3095 tests then died at `cdk synth -q`, surfaced only by the live-streaming build log. Grant is a read-only describe (Resource:* — EC2 describe actions have no resource-level scoping; IAM5 suppressed, no mutation/data access). +1 regression test pins the statement. (cherry picked from commit 3fc9060) (cherry picked from commit 2bf12f4)
1 parent 9add784 commit 4acccf3

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

cdk/src/constructs/ecs-agent-cluster.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,22 @@ export class EcsAgentCluster extends Construct {
283283
resources: bedrockResources,
284284
}));
285285

286+
// ECS-parity: a CDK-based target repo's build gate runs `cdk synth`, and a
287+
// stack wired to a concrete env ({account, region}) does a synth-time
288+
// availability-zone context lookup (ec2:DescribeAvailabilityZones). On a
289+
// developer box the gitignored cdk.context.json caches the answer so synth
290+
// is hermetic; the agent clones fresh, so there's no cache and synth fires
291+
// the live lookup. Without this grant the ECS task role hit AccessDenied →
292+
// "Synthesis finished with errors" → a FALSE build-gate failure on code that
293+
// builds fine everywhere else (live-caught on the ABCA fork; same class as
294+
// the ABCA-488 GetSecretValue and F-2 CreateEvent ECS-parity gaps). This is a
295+
// read-only describe with no resource-level scoping in IAM, so Resource:* is
296+
// required (suppressed below); it grants no mutation and no data access.
297+
taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
298+
actions: ['ec2:DescribeAvailabilityZones'],
299+
resources: ['*'],
300+
}));
301+
286302
// CloudWatch Logs write
287303
logGroup.grantWrite(taskRole);
288304

@@ -293,7 +309,7 @@ export class EcsAgentCluster extends Construct {
293309
NagSuppressions.addResourceSuppressions(this.taskDefinition, [
294310
{
295311
id: 'AwsSolutions-IAM5',
296-
reason: 'DynamoDB index/* wildcards from CDK grantReadWriteData (UserConcurrencyTable, and task tables only when no SessionRole is wired); Secrets Manager wildcards from CDK grantRead (GitHub token) and the bgagent-linear-oauth-*/bgagent-jira-oauth-* prefix grant (ABCA-488 — per-workspace channel OAuth tokens are created by the CLI at setup, name unknown at synth, GetSecretValue only); CloudWatch Logs wildcards from CDK grantWrite; S3 object/* wildcard from CDK grantRead on the ECS payload bucket (read-only, scoped to that bucket — #502) and from grantReadWrite on the artifacts bucket (scoped to that bucket — coding/decompose-v1 delivers its plan artifact there, #299). Bedrock InvokeModel is scoped to explicit model/inference-profile ARNs (no wildcard resource).',
312+
reason: 'DynamoDB index/* wildcards from CDK grantReadWriteData (UserConcurrencyTable, and task tables only when no SessionRole is wired); Secrets Manager wildcards from CDK grantRead (GitHub token) and the bgagent-linear-oauth-*/bgagent-jira-oauth-* prefix grant (ABCA-488 — per-workspace channel OAuth tokens are created by the CLI at setup, name unknown at synth, GetSecretValue only); CloudWatch Logs wildcards from CDK grantWrite; S3 object/* wildcard from CDK grantRead on the ECS payload bucket (read-only, scoped to that bucket — #502) and from grantReadWrite on the artifacts bucket (scoped to that bucket — coding/decompose-v1 delivers its plan artifact there, #299). Bedrock InvokeModel is scoped to explicit model/inference-profile ARNs (no wildcard resource). ec2:DescribeAvailabilityZones requires Resource:* (EC2 describe actions have no resource-level scoping) — read-only, no mutation/data access; needed so a CDK target repo\'s `cdk synth` build gate can resolve AZ context on a fresh clone (ECS-parity, no cdk.context.json cache in the container).',
297313
},
298314
{
299315
id: 'AwsSolutions-ECS2',

cdk/test/constructs/ecs-agent-cluster.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,26 @@ describe('EcsAgentCluster construct', () => {
195195
expect(serialized).toContain('anthropic.claude-haiku-4-5-20251001-v1:0');
196196
});
197197

198+
test('task role can DescribeAvailabilityZones so a CDK target repo can `cdk synth` on a fresh clone (ECS-parity)', () => {
199+
// REGRESSION: `mise run build` on a CDK-based target repo runs `cdk synth`,
200+
// and a stack wired to a concrete env does a synth-time AZ context lookup
201+
// (ec2:DescribeAvailabilityZones). A dev box caches the answer in the
202+
// gitignored cdk.context.json; the agent clones fresh (no cache) → the live
203+
// lookup fires. Without this grant the ECS task role hit AccessDenied →
204+
// "Synthesis finished with errors" → a FALSE build-gate failure. Pin the
205+
// read-only describe (Resource:* — EC2 describe has no resource scoping).
206+
const policies = baseTemplate.findResources('AWS::IAM::Policy');
207+
let azStatement: { Resource: unknown } | undefined;
208+
for (const p of Object.values(policies)) {
209+
for (const s of p.Properties.PolicyDocument.Statement) {
210+
const actions = Array.isArray(s.Action) ? s.Action : [s.Action];
211+
if (actions.includes('ec2:DescribeAvailabilityZones')) azStatement = s;
212+
}
213+
}
214+
expect(azStatement).toBeDefined();
215+
expect(azStatement!.Resource).toEqual('*');
216+
});
217+
198218
test('bedrockModels context override changes the granted model ARNs (#433)', () => {
199219
const template = createStack({ bedrockModels: ['anthropic.claude-opus-4-8'] }).template;
200220
const policies = template.findResources('AWS::IAM::Policy');

0 commit comments

Comments
 (0)