Skip to content

Commit ce4fb12

Browse files
authored
Refactor 'Save Outputs to SSM' to avoid excess CodeCommit GetFile requests (#818)
* refactor Save Outputs to SSM to use S3 as a cache for CodeCommit GetFile * remove the DynamoDB account cache and switch working bucket identity policy to resource policy * add SSL DENY S3 resource policy and ensure removal policy is RETAIN
1 parent 91e89d9 commit ce4fb12

File tree

7 files changed

+162
-6
lines changed

7 files changed

+162
-6
lines changed

src/core/cdk/src/initial-setup.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as secrets from '@aws-cdk/aws-secretsmanager';
77
import * as dynamodb from '@aws-cdk/aws-dynamodb';
88
import * as sfn from '@aws-cdk/aws-stepfunctions';
99
import * as tasks from '@aws-cdk/aws-stepfunctions-tasks';
10+
import * as s3 from '@aws-cdk/aws-s3';
1011
import { CdkDeployProject, PrebuiltCdkDeployProject } from '@aws-accelerator/cdk-accelerator/src/codebuild';
1112
import { AcceleratorStack, AcceleratorStackProps } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-stack';
1213
import { createRoleName, createName } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator';
@@ -125,6 +126,42 @@ export namespace InitialSetup {
125126
maxSessionDuration: buildTimeout,
126127
});
127128

129+
// S3 working bucket
130+
const s3WorkingBucket = new s3.Bucket(this, 'WorkingBucket', {
131+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
132+
encryption: s3.BucketEncryption.S3_MANAGED,
133+
removalPolicy: cdk.RemovalPolicy.RETAIN,
134+
lifecycleRules: [
135+
{
136+
id: '7DaysDelete',
137+
enabled: true,
138+
expiration: cdk.Duration.days(7),
139+
},
140+
],
141+
});
142+
s3WorkingBucket.addToResourcePolicy(
143+
new iam.PolicyStatement({
144+
actions: ['s3:GetObject*', 's3:PutObject*', 's3:DeleteObject*', 's3:GetBucket*', 's3:List*'],
145+
resources: [s3WorkingBucket.arnForObjects('*'), s3WorkingBucket.bucketArn],
146+
principals: [pipelineRole],
147+
}),
148+
);
149+
// Allow only https requests
150+
s3WorkingBucket.addToResourcePolicy(
151+
new iam.PolicyStatement({
152+
actions: ['s3:*'],
153+
resources: [s3WorkingBucket.bucketArn, s3WorkingBucket.arnForObjects('*')],
154+
principals: [new iam.AnyPrincipal()],
155+
conditions: {
156+
Bool: {
157+
'aws:SecureTransport': 'false',
158+
},
159+
},
160+
effect: iam.Effect.DENY,
161+
}),
162+
);
163+
//
164+
128165
// Add a suffix to the CodeBuild project so it creates a new project as it's not able to update the `baseImage`
129166
const projectNameSuffix = enablePrebuiltProject ? 'Prebuilt' : '';
130167
const projectConstructor = enablePrebuiltProject ? PrebuiltCdkDeployProject : CdkDeployProject;
@@ -611,6 +648,7 @@ export namespace InitialSetup {
611648
'configCommitId.$': '$.configCommitId',
612649
outputUtilsTableName: outputUtilsTable.tableName,
613650
accountsTableName: parametersTable.tableName,
651+
s3WorkingBucket: s3WorkingBucket.bucketName,
614652
}),
615653
resultPath: 'DISCARD',
616654
});

src/core/cdk/src/tasks/store-outputs-to-ssm-task.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ export class StoreOutputsToSSMTask extends sfn.StateMachineFragment {
3030
}),
3131
);
3232

33+
const fetchConfigData = new CodeTask(scope, `Load All Config`, {
34+
comment: 'Load All Config',
35+
resultPath: '$.configDetails',
36+
functionPayload,
37+
functionProps: {
38+
role,
39+
code: lambdaCode,
40+
handler: 'index.loadAllConfig',
41+
},
42+
});
43+
3344
const storeAccountOutputs = new sfn.Map(this, `Store Account Outputs To SSM`, {
3445
itemsPath: `$.accounts`,
3546
resultPath: 'DISCARD',
@@ -45,6 +56,7 @@ export class StoreOutputsToSSMTask extends sfn.StateMachineFragment {
4556
'configCommitId.$': '$.configCommitId',
4657
'outputUtilsTableName.$': '$.outputUtilsTableName',
4758
'accountsTableName.$': '$.accountsTableName',
59+
'configDetails.$': '$.configDetails',
4860
},
4961
});
5062

@@ -74,9 +86,11 @@ export class StoreOutputsToSSMTask extends sfn.StateMachineFragment {
7486
'configCommitId.$': '$.configCommitId',
7587
'outputUtilsTableName.$': '$.outputUtilsTableName',
7688
'accountsTableName.$': '$.accountsTableName',
89+
'configDetails.$': '$.configDetails',
7790
},
7891
});
7992

93+
fetchConfigData.next(storeAccountOutputs);
8094
getAccountInfoTask.next(storeAccountRegionOutputs);
8195
const storeOutputsTask = new CodeTask(scope, `Store Outputs To SSM`, {
8296
resultPath: '$.storeOutputsOutput',
@@ -93,7 +107,7 @@ export class StoreOutputsToSSMTask extends sfn.StateMachineFragment {
93107
storeAccountRegionOutputs.iterator(storeOutputsTask);
94108
const chain = sfn.Chain.start(storeAccountOutputs).next(pass);
95109

96-
this.startState = chain.startState;
110+
this.startState = fetchConfigData.startState;
97111
this.endStates = chain.endStates;
98112
}
99113
}

src/core/runtime/src/get-account-info.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,44 @@
11
import { DynamoDB } from '@aws-accelerator/common/src/aws/dynamodb';
22
import { Organizations } from '@aws-accelerator/common/src/aws/organizations';
3-
import { loadAcceleratorConfig } from '@aws-accelerator/common-config/src/load';
3+
import { loadAcceleratorConfigWithS3Attempt } from '@aws-accelerator/common-config/src/load';
44
import { LoadConfigurationInput } from './load-configuration-step';
55
import { Account } from '@aws-accelerator/common-outputs/src/accounts';
66
import { equalIgnoreCase } from '@aws-accelerator/common/src/util/common';
77
import { MandatoryAccountType } from '@aws-accelerator/common-config';
88
import { loadAccounts } from './utils/load-accounts';
9+
import { LoadConsolidatedResult } from './load-consolidated';
910

1011
export interface GetAccountInfoInput extends LoadConfigurationInput {
1112
accountId?: string;
1213
accountType?: MandatoryAccountType;
1314
accountsTableName?: string;
15+
configDetails?: LoadConsolidatedResult;
1416
}
1517

1618
const organizations = new Organizations();
1719
const dynamodb = new DynamoDB();
20+
1821
export const handler = async (input: GetAccountInfoInput) => {
1922
console.log(`Get Account Info...`);
2023
console.log(JSON.stringify(input, null, 2));
2124

22-
const { accountId, configCommitId, configFilePath, configRepositoryName, accountType, accountsTableName } = input;
25+
const {
26+
accountId,
27+
configCommitId,
28+
configFilePath,
29+
configRepositoryName,
30+
accountType,
31+
accountsTableName,
32+
configDetails,
33+
} = input;
2334

2435
// Retrieve Configuration from Code Commit with specific commitId
25-
const acceleratorConfig = await loadAcceleratorConfig({
36+
const acceleratorConfig = await loadAcceleratorConfigWithS3Attempt({
2637
repositoryName: configRepositoryName,
2738
filePath: configFilePath,
2839
commitId: configCommitId,
40+
s3BucketName: configDetails?.bucket,
41+
s3KeyName: configDetails?.configKey,
2942
});
3043
if (accountType) {
3144
const mandatoryAccountKey = acceleratorConfig.getMandatoryAccountKey(accountType);

src/core/runtime/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export { handler as notifySMSuccess } from './notify-statemachine-success';
2424
export { handler as getAccountInfo } from './get-account-info';
2525
export { handler as saveOutputsToSSM } from './save-outputs-to-ssm';
2626
export { handler as getBootstrapOutput } from './get-bootstrap-output';
27+
export { handler as loadAllConfig } from './load-consolidated';
2728

2829
// TODO Replace with
2930
// export * as codebuild from './codebuild';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { LoadConfigurationInput } from './load-configuration-step';
2+
import { CodeCommit } from '@aws-accelerator/common/src/aws/codecommit';
3+
import { S3 } from '@aws-accelerator/common/src/aws/s3';
4+
5+
export interface LoadConsolidatedInput extends LoadConfigurationInput {
6+
s3WorkingBucket?: string;
7+
}
8+
9+
export interface LoadConsolidatedResult {
10+
bucket?: string;
11+
configKey?: string;
12+
}
13+
14+
const s3 = new S3();
15+
16+
export const handler = async (input: LoadConsolidatedInput) => {
17+
console.log(`Loading Configuration Info...`);
18+
console.log(JSON.stringify(input, null, 2));
19+
20+
const { configCommitId, configFilePath, configRepositoryName, s3WorkingBucket } = input;
21+
22+
const result: LoadConsolidatedResult = {};
23+
24+
if (s3WorkingBucket) {
25+
result.bucket = s3WorkingBucket;
26+
const path = `${configCommitId}`;
27+
28+
const codecommit = new CodeCommit(undefined, undefined);
29+
try {
30+
const file = await codecommit.getFile(configRepositoryName, configFilePath, configCommitId);
31+
const source = file.fileContent.toString();
32+
33+
const key = `${path}/config.json`;
34+
const fileUploadResult = await s3.putObject({
35+
Body: source,
36+
Key: key,
37+
Bucket: s3WorkingBucket,
38+
});
39+
console.log(JSON.stringify(fileUploadResult));
40+
result.configKey = key;
41+
} catch (e) {
42+
throw new Error(
43+
`Unable to load configuration file "${configFilePath}" in Repository ${configRepositoryName}\n${e.message} code:${e.code}`,
44+
);
45+
}
46+
}
47+
48+
console.log(`Result: ${JSON.stringify(result)}`);
49+
return result;
50+
};

src/core/runtime/src/save-outputs-to-ssm/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DynamoDB } from '@aws-accelerator/common/src/aws/dynamodb';
2-
import { loadAcceleratorConfig } from '@aws-accelerator/common-config/src/load';
2+
import { loadAcceleratorConfigWithS3Attempt } from '@aws-accelerator/common-config/src/load';
33
import { LoadConfigurationInput } from '../load-configuration-step';
44
import { Account } from '@aws-accelerator/common-outputs/src/accounts';
55
import { saveNetworkOutputs } from './network-outputs';
@@ -9,6 +9,7 @@ import { saveEventOutputs } from './event-outputs';
99
import { saveEncryptsOutputs } from './encrypt-outputs';
1010
import { saveFirewallReplacementOutputs } from './firewall-outputs';
1111
import { loadAccounts } from './../utils/load-accounts';
12+
import { LoadConsolidatedResult } from './../load-consolidated';
1213

1314
export interface SaveOutputsToSsmInput extends LoadConfigurationInput {
1415
acceleratorPrefix: string;
@@ -18,6 +19,7 @@ export interface SaveOutputsToSsmInput extends LoadConfigurationInput {
1819
assumeRoleName: string;
1920
outputUtilsTableName: string;
2021
accountsTableName: string;
22+
configDetails?: LoadConsolidatedResult;
2123
}
2224

2325
const dynamodb = new DynamoDB();
@@ -36,17 +38,20 @@ export const handler = async (input: SaveOutputsToSsmInput) => {
3638
region,
3739
outputUtilsTableName,
3840
accountsTableName,
41+
configDetails,
3942
} = input;
4043
// Remove - if prefix ends with -
4144
const acceleratorPrefix = input.acceleratorPrefix.endsWith('-')
4245
? input.acceleratorPrefix.slice(0, -1)
4346
: input.acceleratorPrefix;
4447

4548
// Retrieve Configuration from Code Commit with specific commitId
46-
const config = await loadAcceleratorConfig({
49+
const config = await loadAcceleratorConfigWithS3Attempt({
4750
repositoryName: configRepositoryName,
4851
filePath: configFilePath,
4952
commitId: configCommitId,
53+
s3BucketName: configDetails?.bucket,
54+
s3KeyName: configDetails?.configKey,
5055
});
5156

5257
// Retrive Accounts from DynamoDB

src/lib/common-config/src/load.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { CodeCommit } from '@aws-accelerator/common/src/aws/codecommit';
22
import { AcceleratorConfig } from '.';
3+
import { S3 } from '@aws-accelerator/common/src/aws/s3';
4+
5+
const s3 = new S3();
36

47
/**
58
* Retrieve the configuration from CodeCommit.
@@ -22,3 +25,35 @@ export async function loadAcceleratorConfig(props: {
2225
);
2326
}
2427
}
28+
29+
export async function loadAcceleratorConfigWithS3Attempt(props: {
30+
repositoryName: string;
31+
filePath: string;
32+
commitId: string;
33+
defaultRegion?: string;
34+
s3BucketName?: string;
35+
s3KeyName?: string;
36+
}): Promise<AcceleratorConfig> {
37+
const { repositoryName, filePath, commitId, defaultRegion, s3BucketName, s3KeyName } = props;
38+
39+
if (s3BucketName && s3KeyName) {
40+
try {
41+
console.log(`Loading configuration from S3 working bucket.`);
42+
const s3GetResponseString = await s3.getObjectBodyAsString({
43+
Bucket: s3BucketName,
44+
Key: s3KeyName,
45+
});
46+
47+
return AcceleratorConfig.fromString(s3GetResponseString);
48+
} catch (e) {
49+
console.log(`Unable to load configuration file "${s3KeyName}" from S3\n${e.message} code:${e.code}`);
50+
}
51+
}
52+
53+
console.log(`Loading configuration from CodeCommit.`);
54+
return loadAcceleratorConfig({
55+
repositoryName,
56+
filePath,
57+
commitId,
58+
});
59+
}

0 commit comments

Comments
 (0)