This repository has been archived by the owner on Jun 14, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 196
/
cicd-stack.ts
167 lines (151 loc) · 6.44 KB
/
cicd-stack.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import { Stack, StackProps, Stage, StageProps, SecretValue, DefaultStackSynthesizer } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
import { config, SharedIniFileCredentials, Organizations, STS, CloudFormation, AWSError } from 'aws-sdk';
import { PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { LandingPageStack } from './landing-page-stack';
export class LandingPageStage extends Stage {
constructor(scope: Construct, id: string, props: StageProps) {
super(scope, id, props);
new LandingPageStack(this, 'LandingPageStack', { ...props, stage: id.toLowerCase() });
}
}
export class LandingPagePipelineStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const qualifier = DefaultStackSynthesizer.DEFAULT_QUALIFIER;
const source = CodePipelineSource.gitHub(
`${this.node.tryGetContext('github_alias')}/${this.node.tryGetContext('github_repo_name')}`,
this.node.tryGetContext('github_repo_branch'),
{
authentication: SecretValue.secretsManager('GITHUB_TOKEN'),
}
);
const pipelineName = 'AWSBootstrapKit-LandingZone';
const codePipelineRole = new Role(this, 'CodePipelineRole', {
assumedBy: new ServicePrincipal('codepipeline.amazonaws.com'),
});
codePipelineRole.addToPolicy(
new PolicyStatement({
actions: ['organizations:ListAccounts', 'organizations:ListTagsForResource', 'cloudformation:DescribeStacks'],
resources: ['*'],
})
);
codePipelineRole.addToPolicy(
new PolicyStatement({
actions: ['sts:AssumeRole'],
resources: [`arn:aws:iam::*:role/cdk-${qualifier}-deploy-role-*`],
})
);
const pipeline = new CodePipeline(this, 'LandingPagePipeline', {
pipelineName: pipelineName,
crossAccountKeys: true,
role: codePipelineRole,
synth: new ShellStep('Synth', {
input: source,
commands: [`cd source/3-landing-page-cicd/cdk`, 'npm install', 'npx cdk synth'],
primaryOutputDirectory: 'source/3-landing-page-cicd/cdk/cdk.out',
env: {
NPM_CONFIG_UNSAFE_PERM: 'true',
},
}),
});
const AWS_PROFILE = 'cicd';
if (!process.env.CODEBUILD_BUILD_ID) {
config.credentials = new SharedIniFileCredentials({ profile: AWS_PROFILE });
}
const orgClient = new Organizations({ region: 'us-east-1' });
orgClient
.listAccounts()
.promise()
.then(async (results) => {
let stagesDetails = [];
if (results.Accounts) {
for (const account of results.Accounts) {
const tags = (await orgClient.listTagsForResource({ ResourceId: account.Id! }).promise()).Tags;
if (tags && tags.length > 0) {
const accountType = tags.find((tag) => tag.Key === 'AccountType')!.Value;
if (accountType === 'STAGE') {
const stageName = tags.find((tag) => tag.Key === 'StageName')!.Value;
const stageOrder = tags.find((tag) => tag.Key === 'StageOrder')!.Value;
stagesDetails.push({
name: stageName,
accountId: account.Id,
order: parseInt(stageOrder),
});
}
}
}
}
stagesDetails.sort((a, b) => (a.order > b.order ? 1 : -1));
for (let stageDetailsIndex in stagesDetails) {
let stageDetails = stagesDetails[stageDetailsIndex];
pipeline.addStage(
new LandingPageStage(this, stageDetails.name, { env: { account: stageDetails.accountId } })
);
}
return stagesDetails.map((s) => s.accountId!);
})
.then(async (accountIds) => await this.CheckTargetEnvironments(accountIds, this.region, qualifier))
.catch((error: AWSError) => {
switch (error.code) {
case 'CredentialsError': {
console.error(
'\x1b[31m',
`Failed to get credentials for "${AWS_PROFILE}" profile. Make sure to run "aws configure sso --profile ${AWS_PROFILE} && aws sso login --profile ${AWS_PROFILE}"\n\n`
);
break;
}
case 'ExpiredTokenException': {
console.error('\x1b[31m', `Token expired, run "aws sso login --profile ${AWS_PROFILE}"\n\n`);
break;
}
case 'AccessDeniedException': {
console.error(
'\x1b[31m',
`Unable to call the AWS Organizations ListAccounts API. Make sure to add a PolicyStatement with the organizations:ListAccounts action to your synth action`
);
break;
}
default: {
console.error(error.message);
}
}
//force CDK to fail in case of an unknown exception
process.exit(1);
});
}
private async CheckTargetEnvironments(accounts: Iterable<string>, region: string, qualifier: string): Promise<void> {
const stsClient = new STS();
for (const account of accounts) {
console.log(`Checking whether the target environment aws://${account}/${region} is deployable...`);
if (!(await this.checkTargetEnvironment(stsClient, account, region, qualifier))) {
const message = `Account ${account} is not bootstrapped in ${region}. Make sure you deploy the pipeline in a deployable region.`;
throw new Error(message);
}
}
}
private async checkTargetEnvironment(
stsClient: STS,
accountId: string,
region: string,
qualifier: string
): Promise<boolean> {
try {
const targetRoleArn = `arn:aws:iam::${accountId}:role/cdk-${qualifier}-deploy-role-${accountId}-${region}`;
const assumedRole = await stsClient.assumeRole({ RoleArn: targetRoleArn, RoleSessionName: accountId }).promise();
const cred = assumedRole.Credentials!;
const targetAccountCredentials = {
accessKeyId: cred.AccessKeyId,
secretAccessKey: cred.SecretAccessKey,
sessionToken: cred.SessionToken,
};
const cfnClient = new CloudFormation({ credentials: targetAccountCredentials });
const stacks = await cfnClient.describeStacks({ StackName: 'CDKToolkit' }).promise();
return stacks.Stacks![0].Parameters!.find((t) => t.ParameterKey == 'Qualifier')!.ParameterValue === qualifier;
} catch (error) {
console.log((error as Error).message);
return false;
}
}
}