Skip to content

Commit f11766e

Browse files
authored
fix(apigateway): race condition between Stage and CfnAccount (#18011)
When `cloudWatchRole` is enabled, a `CfnAccount` is created for it. Since there is no explicit dependency between the the stages and the account, CloudFormation may deploy them in the wrong order, causing the deployment to fail. Add an explicit dependency between `Stage`s (whether defined by the user or created automatically) and the CloudWatch `CfnAccount`, if it exists. Fixes #10722. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 890c4c5 commit f11766e

20 files changed

+110
-26
lines changed

packages/@aws-cdk/aws-apigateway/lib/restapi.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ export abstract class RestApiBase extends Resource implements IRestApi {
319319
private _latestDeployment?: Deployment;
320320
private _domainName?: DomainName;
321321

322+
protected cloudWatchAccount?: CfnAccount;
323+
322324
constructor(scope: Construct, id: string, props: RestApiBaseProps = { }) {
323325
super(scope, id);
324326
this.restApiName = props.restApiName ?? id;
@@ -500,6 +502,17 @@ export abstract class RestApiBase extends Resource implements IRestApi {
500502
ignore(deployment);
501503
}
502504

505+
/**
506+
* Associates a Stage with this REST API
507+
*
508+
* @internal
509+
*/
510+
public _attachStage(stage: Stage) {
511+
if (this.cloudWatchAccount) {
512+
stage.node.addDependency(this.cloudWatchAccount);
513+
}
514+
}
515+
503516
/**
504517
* @internal
505518
*/
@@ -509,11 +522,11 @@ export abstract class RestApiBase extends Resource implements IRestApi {
509522
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs')],
510523
});
511524

512-
const resource = new CfnAccount(this, 'Account', {
525+
this.cloudWatchAccount = new CfnAccount(this, 'Account', {
513526
cloudWatchRoleArn: role.roleArn,
514527
});
515528

516-
resource.node.addDependency(apiResource);
529+
this.cloudWatchAccount.node.addDependency(apiResource);
517530
}
518531

519532
/**

packages/@aws-cdk/aws-apigateway/lib/stage.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Construct } from 'constructs';
33
import { AccessLogFormat, IAccessLogDestination } from './access-log';
44
import { CfnStage } from './apigateway.generated';
55
import { Deployment } from './deployment';
6-
import { IRestApi } from './restapi';
6+
import { IRestApi, RestApiBase } from './restapi';
77
import { parseMethodOptionsPath } from './util';
88

99
/**
@@ -256,6 +256,10 @@ export class Stage extends Resource implements IStage {
256256

257257
this.stageName = resource.ref;
258258
this.restApi = props.deployment.api;
259+
260+
if (RestApiBase._isRestApiBase(this.restApi)) {
261+
this.restApi._attachStage(this);
262+
}
259263
}
260264

261265
/**

packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.expected.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@
3434
"myauthorizer23CB99DD": {
3535
"Type": "AWS::ApiGateway::Authorizer",
3636
"Properties": {
37+
"Name": "CognitoUserPoolsAuthorizerIntegmyauthorizer10C804C1",
3738
"RestApiId": {
3839
"Ref": "myrestapi551C8392"
3940
},
4041
"Type": "COGNITO_USER_POOLS",
4142
"IdentitySource": "method.request.header.Authorization",
42-
"Name": "CognitoUserPoolsAuthorizerIntegmyauthorizer10C804C1",
4343
"ProviderARNs": [
4444
{
4545
"Fn::GetAtt": [
@@ -123,7 +123,10 @@
123123
"Ref": "myrestapiDeployment419B1464b903292b53d7532ca4296973bcb95b1a"
124124
},
125125
"StageName": "prod"
126-
}
126+
},
127+
"DependsOn": [
128+
"myrestapiAccountA49A05BE"
129+
]
127130
},
128131
"myrestapiANY94B0497F": {
129132
"Type": "AWS::ApiGateway::Method",

packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.lit.expected.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,10 @@
198198
"Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb"
199199
},
200200
"StageName": "prod"
201-
}
201+
},
202+
"DependsOn": [
203+
"MyRestApiAccount2FB6DB7A"
204+
]
202205
},
203206
"MyRestApiANY05143F93": {
204207
"Type": "AWS::ApiGateway::Method",
@@ -239,6 +242,7 @@
239242
"MyAuthorizer6575980E": {
240243
"Type": "AWS::ApiGateway::Authorizer",
241244
"Properties": {
245+
"Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5",
242246
"RestApiId": {
243247
"Ref": "MyRestApi2D1F47A9"
244248
},
@@ -266,8 +270,7 @@
266270
]
267271
]
268272
},
269-
"IdentitySource": "method.request.header.Authorization,method.request.querystring.allow",
270-
"Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5"
273+
"IdentitySource": "method.request.header.Authorization,method.request.querystring.allow"
271274
}
272275
}
273276
},

packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"MyAuthorizer6575980E": {
106106
"Type": "AWS::ApiGateway::Authorizer",
107107
"Properties": {
108+
"Name": "TokenAuthorizerIAMRoleIntegMyAuthorizer1DFDE3B5",
108109
"RestApiId": {
109110
"Ref": "MyRestApi2D1F47A9"
110111
},
@@ -138,8 +139,7 @@
138139
]
139140
]
140141
},
141-
"IdentitySource": "method.request.header.Authorization",
142-
"Name": "TokenAuthorizerIAMRoleIntegMyAuthorizer1DFDE3B5"
142+
"IdentitySource": "method.request.header.Authorization"
143143
}
144144
},
145145
"MyAuthorizerauthorizerInvokePolicy0F88B8E1": {
@@ -241,7 +241,10 @@
241241
"Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb"
242242
},
243243
"StageName": "prod"
244-
}
244+
},
245+
"DependsOn": [
246+
"MyRestApiAccount2FB6DB7A"
247+
]
245248
},
246249
"MyRestApiANY05143F93": {
247250
"Type": "AWS::ApiGateway::Method",

packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.lit.expected.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,10 @@
198198
"Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb"
199199
},
200200
"StageName": "prod"
201-
}
201+
},
202+
"DependsOn": [
203+
"MyRestApiAccount2FB6DB7A"
204+
]
202205
},
203206
"MyRestApiANY05143F93": {
204207
"Type": "AWS::ApiGateway::Method",
@@ -239,6 +242,7 @@
239242
"MyAuthorizer6575980E": {
240243
"Type": "AWS::ApiGateway::Authorizer",
241244
"Properties": {
245+
"Name": "TokenAuthorizerIntegMyAuthorizer793B1D5F",
242246
"RestApiId": {
243247
"Ref": "MyRestApi2D1F47A9"
244248
},
@@ -266,8 +270,7 @@
266270
]
267271
]
268272
},
269-
"IdentitySource": "method.request.header.Authorization",
270-
"Name": "TokenAuthorizerIntegMyAuthorizer793B1D5F"
273+
"IdentitySource": "method.request.header.Authorization"
271274
}
272275
}
273276
},

packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@
7777
"Ref": "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d"
7878
},
7979
"StageName": "prod"
80-
}
80+
},
81+
"DependsOn": [
82+
"corsapitestAccount7D1D6854"
83+
]
8184
},
8285
"corsapitesttwitch0E3D1559": {
8386
"Type": "AWS::ApiGateway::Resource",

packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,10 @@
377377
"Ref": "deployment3338197541aef5f15bf9a60b10e06fdbe72854f4"
378378
},
379379
"StageName": "prod"
380-
}
380+
},
381+
"DependsOn": [
382+
"lambdarestapiAccount856938D8"
383+
]
381384
}
382385
}
383386
}

packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,10 @@
229229
"Ref": "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5"
230230
},
231231
"StageName": "prod"
232-
}
232+
},
233+
"DependsOn": [
234+
"booksapiAccountDBA98FB9"
235+
]
233236
},
234237
"booksapiANYApiPermissionrestapibooksexamplebooksapi4538F335ANY73B3CDDC": {
235238
"Type": "AWS::Lambda::Permission",

packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@
7373
"Ref": "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a"
7474
},
7575
"StageName": "prod"
76-
}
76+
},
77+
"DependsOn": [
78+
"myapiAccountEC421A0A"
79+
]
7780
},
7881
"myapiGETF990CE3C": {
7982
"Type": "AWS::ApiGateway::Method",

packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@
101101
}
102102
],
103103
"StageName": "beta"
104-
}
104+
},
105+
"DependsOn": [
106+
"myapiAccountEC421A0A"
107+
]
105108
},
106109
"myapiv113487378": {
107110
"Type": "AWS::ApiGateway::Resource",

packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@
144144
"Ref": "BooksApiDeployment86CA39AF4ff82f86c127f53c9de94d266b1906be"
145145
},
146146
"StageName": "prod"
147-
}
147+
},
148+
"DependsOn": [
149+
"BooksApiAccount9C44AF8E"
150+
]
148151
},
149152
"BooksApiANY0C4EABE3": {
150153
"Type": "AWS::ApiGateway::Method",

packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json

+8-2
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@
124124
"Ref": "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138"
125125
},
126126
"StageName": "prod"
127-
}
127+
},
128+
"DependsOn": [
129+
"helloapiAccountD8C38BCE"
130+
]
128131
},
129132
"helloapihello4AA00177": {
130133
"Type": "AWS::ApiGateway::Resource",
@@ -333,7 +336,10 @@
333336
"Ref": "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a"
334337
},
335338
"StageName": "prod"
336-
}
339+
},
340+
"DependsOn": [
341+
"secondapiAccountDF729874"
342+
]
337343
},
338344
"secondapihello7264EB69": {
339345
"Type": "AWS::ApiGateway::Resource",

packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,10 @@
700700
"Ref": "MyApiDeploymentECB0D05E58dcfc85d01f2b81270e177f5347476d"
701701
},
702702
"StageName": "prod"
703-
}
703+
},
704+
"DependsOn": [
705+
"MyApiAccount13882D84"
706+
]
704707
},
705708
"MyApiGETD0C7AA0C": {
706709
"Type": "AWS::ApiGateway::Method",

packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,10 @@
255255
"Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8"
256256
},
257257
"StageName": "prod"
258-
}
258+
},
259+
"DependsOn": [
260+
"StepFunctionsRestApiAccountBD0CCC0E"
261+
]
259262
}
260263
},
261264
"Outputs": {

packages/@aws-cdk/aws-apigateway/test/restapi.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe('restapi', () => {
5050
DeploymentId: { Ref: 'myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a' },
5151
StageName: 'prod',
5252
},
53+
DependsOn: ['myapiAccountEC421A0A'],
5354
},
5455
myapiCloudWatchRole095452E5: {
5556
Type: 'AWS::IAM::Role',

packages/@aws-cdk/aws-apigateway/test/stage.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import '@aws-cdk/assert-internal/jest';
2+
import { ResourcePart } from '@aws-cdk/assert-internal';
23
import * as logs from '@aws-cdk/aws-logs';
34
import * as cdk from '@aws-cdk/core';
45
import * as apigateway from '../lib';
@@ -69,6 +70,21 @@ describe('stage', () => {
6970
});
7071
});
7172

73+
test('stage depends on the CloudWatch role when it exists', () => {
74+
// GIVEN
75+
const stack = new cdk.Stack();
76+
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: true, deploy: false });
77+
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
78+
api.root.addMethod('GET');
79+
80+
// WHEN
81+
new apigateway.Stage(stack, 'my-stage', { deployment });
82+
83+
expect(stack).toHaveResourceLike('AWS::ApiGateway::Stage', {
84+
DependsOn: ['testapiAccount9B907665'],
85+
}, ResourcePart.CompleteDefinition);
86+
});
87+
7288
test('common method settings can be set at the stage level', () => {
7389
// GIVEN
7490
const stack = new cdk.Stack();

packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@
8080
"Ref": "apiDeployment149F1294891f10d69bae7c4d19bdee7af013a950"
8181
},
8282
"StageName": "prod"
83-
}
83+
},
84+
"DependsOn": ["apiAccount57E28B43"]
8485
},
8586
"apiCloudWatchRoleAC81D93E": {
8687
"Type": "AWS::IAM::Role",

packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@
7373
"Ref": "MyRestApiDeploymentB555B582d61dc696e12272a0706c826196fa8d62"
7474
},
7575
"StageName": "prod"
76-
}
76+
},
77+
"DependsOn": ["MyRestApiAccount2FB6DB7A"]
7778
},
7879
"MyRestApiANYApiPermissionCallRestApiIntegMyRestApiB570839CANY0C27C1E3": {
7980
"Type": "AWS::Lambda::Permission",

packages/decdk/test/__snapshots__/synth.test.js.snap

+6
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,9 @@ Object {
439439
"Type": "AWS::ApiGateway::Deployment",
440440
},
441441
"MyApiDeploymentStageprodE1054AF0": Object {
442+
"DependsOn": Array [
443+
"MyApiAccount13882D84",
444+
],
442445
"Properties": Object {
443446
"DeploymentId": Object {
444447
"Ref": "MyApiDeploymentECB0D05E0597cc4592870e54c401cccc6090bd86",
@@ -1214,6 +1217,9 @@ Object {
12141217
"Type": "AWS::ApiGateway::Deployment",
12151218
},
12161219
"lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FDeploymentStageprod6A86C016": Object {
1220+
"DependsOn": Array [
1221+
"lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FAccountF7734F1E",
1222+
],
12171223
"Properties": Object {
12181224
"DeploymentId": Object {
12191225
"Ref": "lambdaeventsHelloWorldFunctionAB27BB65ApiEventSourceA7A86A4FDeploymentFF3F4A1Ac00dde791d2719be3e8ea69f9a61a5cd",

0 commit comments

Comments
 (0)