今回はクロスアカウントのCodePipeline構成をCDKで構築したいと思います。
下地として、クラスメソッドさんの下記記事を参考にしています。
最終的に出来上がるのは以下のような構成のリソース群です。
意図としては、ソース管理は親となるアカウントで一元管理したいけど、パイプラインを含めて環境依存のリソースは全て、子となる環境アカウント側で持ちたいため、このような構成を目指しています。
まずは実際にパイプラインが動く環境側で、IAM
ロール等のリソースを作成していきます。
CICDが参照するCodeCommit
のリポジトリがあるアカウント側のIDを取得しておきましょう。
CodePipeline
用のサービスロールを作成します。
このロールが後で作成する、親アカウント側のCodeCommit
を操作するロールをAssumeRole
するため、ポリシー内で親アカウントのIDを指定して宣言しています。
import {Stack, CfnOutput} from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
/**
* CodePipelineのサービスロール作成
* @param sourceAccountId CodeCommitのリポジトリを所有しているアカウントID
*/
const createCodePipelineRole = (stack: Stack, sourceAccountId: string): iam.Role => {
const role = new iam.Role(stack, `CodePipelineServiceRole`, {
roleName: '<ロール名>',
// CodePipelineからAssumeRoleされる
assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'),
inlinePolicies: {
thing: new iam.PolicyDocument({
statements: [
// CodeCommitリポジトリを保有しているアカウントのロールにAssumeRoleを行う権限
new iam.PolicyStatement({
sid: 'AssumeRolePolicy',
actions: ['sts:AssumeRole'],
resources: [`arn:aws:iam::${sourceAccountId}:role/*`],
}),
new iam.PolicyStatement({
sid: 'S3Policy',
actions: [
's3:PutObject',
's3:GetObject',
's3:GetObjectVersion',
's3:GetBucketVersioning'
],
resources: ['*']
}),
new iam.PolicyStatement({
sid: 'CodeBuildPolicy',
actions: [
'codebuild:BatchGetBuilds',
'codebuild:StartBuild'
],
resources: ['*']
}),
],
}),
}
});
new CfnOutput(stack, 'CodePipelineServiceRoleArn', {
value: role.roleArn,
});
return role;
}
CodeBuild用のロールを作成
用のサービスロールを作成します。
このロールは特にクロスアカウント固有の設定もなく、CodeBuild
を動かす上で必要な権限を宣言しています。
import {Stack, CfnOutput} from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
/**
* CodeBuildのサービスロール作成
*/
const createCodeBuildRole = (stack: Stack): iam.Role => {
const role = new iam.Role(stack, `CodeBuildServiceRole`, {
roleName: '<ロール名>',
assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
inlinePolicies: {
thing: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
sid: 'CloudWatchLogsPolicy',
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
resources: [`*`],
}),
new iam.PolicyStatement({
sid: 'S3ObjectPolicy',
actions: [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion"
],
resources: [`*`],
}),
],
}),
}
});
new CfnOutput(stack, 'CodeBuildServiceRoleArn', {
value: role.roleArn,
});
return role;
}
続いてCodePipeline
中で各アクションがソースのやりとりに使用するArtifact
リソースを作成します。
特にCodeCommit
のロールは親のアカウント側にあるので、そちらからも参照できるようにS3
のバケットポリシーを設定し、かつ暗号化をするためKMS
のKey
を用いています。
import {Stack} from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
/**
* アーティファクト用のKMS Keyを作成
*/
const createArtifactKey = (stack: Stack, sourceAccountId: string, codePipelineServiceRole: iam.Role, codeBuildServiceRole: iam.Role) => {
// 環境アカウントからの操作権限
cryptKey.addToResourcePolicy(new iam.PolicyStatement({
sid: 'Enable IAM User Permissions',
effect: iam.Effect.ALLOW,
principals: [
new iam.ArnPrincipal(
`arn:aws:iam::${Stack.of(stack).account}:root`,
)
],
actions: ['kms:*'],
resources: ['*']
}));
// CI/CDの各ステージ + 親アカウントからの操作権限
cryptKey.addToResourcePolicy(new iam.PolicyStatement({
sid: 'Allow use of the key',
effect: iam.Effect.ALLOW,
principals: [
new iam.ArnPrincipal(codePipelineServiceRole.roleArn),
new iam.ArnPrincipal(codeBuildServiceRole.roleArn),
new iam.ArnPrincipal(`arn:aws:iam::${sourceAccountId}:root`)
],
actions: [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
resources: ['*']
}));
cryptKey.addToResourcePolicy(new iam.PolicyStatement({
sid: 'Allow attachment of persistent resources',
effect: iam.Effect.ALLOW,
principals: [
new iam.ArnPrincipal(codePipelineServiceRole.roleArn),
new iam.ArnPrincipal(codeBuildServiceRole.roleArn),
new iam.ArnPrincipal(`arn:aws:iam::${sourceAccountId}:root`)
],
actions: [
"kms:CreateGrant",
"kms:ListGrants",
"kms:RevokeGrant"
],
resources: ['*'],
conditions: {
Bool: {
'kms:GrantIsForAWSResource': true
}
}
}));
new CfnOutput(stack, `ArtifactCryptKeyArn`, {
value: cryptKey.keyArn,
});
return cryptKey;
}
import {Stack} from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as s3 from 'aws-cdk-lib/aws-s3';
/**
* アーティファクト用のS3バケットを作成
*/
const createArtifactBucket = (stack: Stack, sourceAccountId: string, cryptKey: kms.Key) => {
// CodePipelineで使用するアーティファクト用バケットを作成sakusei
const artifactBucket = new s3.Bucket(stack, `BuildArtifactBucket`, {
bucketName: `<バケット名>`,
encryption: s3.BucketEncryption.KMS,
encryptionKey: cryptKey
});
artifactBucket.addToResourcePolicy(new iam.PolicyStatement({
sid: 'DenyUnEncryptedObjectUploads',
effect: iam.Effect.DENY,
principals: [new iam.StarPrincipal()],
actions: ['s3:PutObject'],
resources: [`arn:aws:s3:::${artifactBucket.bucketName}/*`],
conditions: {
StringNotEquals: {
's3:x-amz-server-side-encryption': 'aws:kms'
}
}
}));
artifactBucket.addToResourcePolicy(new iam.PolicyStatement({
sid: 'DenyInsecureConnections',
effect: iam.Effect.DENY,
principals: [new iam.StarPrincipal()],
actions: ['s3:*'],
resources: [`arn:aws:s3:::${artifactBucket.bucketName}/*`],
conditions: {
Bool: {
'aws:SecureTransport': false
}
}
}));
artifactBucket.addToResourcePolicy(new iam.PolicyStatement({
sid: 'CrossAccountS3GetPutPolicy',
effect: iam.Effect.ALLOW,
principals: [new iam.ArnPrincipal(`arn:aws:iam::${sourceAccountId}:root`)],
actions: [
's3:Get*',
's3:Put*'
],
resources: [`arn:aws:s3:::${artifactBucket.bucketName}/*`],
}));
artifactBucket.addToResourcePolicy(new iam.PolicyStatement({
sid: 'CrossAccountS3ListPolicy',
effect: iam.Effect.ALLOW,
principals: [new iam.ArnPrincipal(`arn:aws:iam::${sourceAccountId}:root`)],
actions: ['s3:ListBucket'],
resources: [`arn:aws:s3:::${artifactBucket.bucketName}`],
}));
new CfnOutput(stack, `ArtifactBucketArn`, {
value: artifactBucket.bucketArn
});
}
ここまでのリソースを作成するためにスタックにまとめます。 パラメータとしては親アカウントのIDを渡してあげる必要があります。
export class Stack1 extends Stack {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props);
const SOURCE_ACCOUNT_ID = "XXXXXXXXXXX";
const codePipelineServiceRole = createCodePipelineRole(this, SOURCE_ACCOUNT_ID);
const codeBuildServiceRole = createCodeBuildRole(this)
const artifactEncryptKey = createArtifactKey(this, SOURCE_ACCOUNT_ID, codePipelineServiceRole, codeBuildServiceRole);
const artifactBucket = createArtifactBucket(this, SOURCE_ACCOUNT_ID, artifactEncryptKey);
}
}
今度は親アカウント側でCodeCommit
を触れる権限+先ほど作成した環境アカウント側のArtifact
リソースを触れる権限を持ったロールを作成します。
このロールが2つのアカウントを跨いでリソースをやりとりすることで、クロスアカウントのパイプラインを実現しています。
そのため、記述は簡素ですが要となるロールです。
import {Stack, CfnOutput} from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
/**
* CodeCommitのアクセスロールを作成する
* @summary CodeCommitの操作 + 環境アカウント側のS3バケットとKMSキーにアクセスするロールを作成
*/
const createCodeCommitAccessRole = (stack: Stack, envAccountId: string, sourceRepositoryArn: string, artifactBucketArn: string, artifactCryptKeyArn: string): iam.Role => {
const role = new iam.Role(stack, `CodeCommitRole`, {
roleName: `<ロール名>`,
assumedBy: new iam.ArnPrincipal(`arn:aws:iam::${envAccountId}:root`),
inlinePolicies: {
thing: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
sid: 'UploadArtifactPolicy',
actions: [
"s3:PutObject",
"s3:PutObjectAcl"
],
resources: [`${artifactBucketArn}/*`]
}),
new iam.PolicyStatement({
sid: 'KMSAccessPolicy',
actions: [
"kms:DescribeKey",
"kms:GenerateDataKey*",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:Decrypt"
],
resources: [artifactCryptKeyArn]
}),
new iam.PolicyStatement({
sid: 'CodeCommitAccessPolicy',
actions: [
"codecommit:GetBranch",
"codecommit:GetCommit",
"codecommit:UploadArchive",
"codecommit:GetUploadArchiveStatus",
"codecommit:CancelUploadArchive",
"codecommit:GetRepository"
],
resources: [sourceRepositoryArn]
}),
],
}),
}
});
new CfnOutput(this, `CodeCommitAccessRoleArn`, {
value: codeCommitAccessRole.roleArn
});
return role;
}
先ほどのcreateCodeCommitAccessRole
を呼び出すロールです。
Stack1
で作成したリソースの情報を渡しています。
export class Stack2 extends Stack {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props);
const ENV_ACCOUNT_ID = "XXXXXXXXXXX";
const SOURCE_REPOSITORY_ARN = "XXXXXXXXXXX";
const ARTIFACT_BUCKET_ARN = "XXXXXXXXXXX";
const ARTIFACT_ENCRYPT_KEY_ARN = "XXXXXXXXXXX";
createCodeCommitAccessRole(this, SOURCE_REPOSITORY_ARN, ARTIFACT_BUCKET_ARN, ARTIFACT_ENCRYPT_KEY_ARN);
}
}
最後に実際にパイプラインを環境アカウント側で構築します。
基本的に今までのスタックで作成したリソースを指定してCodePipeline
を構築するだけですが、パラメータ多いため長いコードになっています。
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as actions from 'aws-cdk-lib/aws-codepipeline-actions';
type Props = {
// 対象ブランチ
targetBranch: string;
// ArtifactバケットARN
artifactBucketArn: string;
// Artifact暗号鍵ARN
artifactEncryptKeyArn: string;
// ソースリポジトリARN
sourceRepositoryArn: string;
// CodeCommitのRole ARN
codeCommitAccessRoleArn: string;
// CodeBuildのRole ARN
codeBuildServiceRoleArn: string;
// CodePipelineのRole ARN
codePipelineServiceRoleArn: string;
// 出力先バケット ARN
exportBucketArn: string;
}
/**
* CI/CD構築
*/
const createPipeline = (stack: cdk.Stack, {
targetBranch,
artifactBucketArn,
artifactEncryptKeyArn,
sourceRepositoryArn,
codeCommitAccessRoleArn,
codeBuildServiceRoleArn,
codePipelineServiceRoleArn,
exportBucketArn,
}: Props) => {
// ソースコードのリポジトリを取得
const repository = codecommit.Repository.fromRepositoryArn(stack, `SourceRepository`, sourceRepositoryArn);
// CodeCommit用のロールを取得
const codeCommitRole = iam.Role.fromRoleArn(stack, 'CodeCommitRole', codeCommitAccessRoleArn, {
mutable: false
});
// CodeBuildのサービスロールを取得
const codeBuildRole = iam.Role.fromRoleArn(stack, 'CodeBuildRole', codeBuildServiceRoleArn);
// CodePipelineのサービスロールを取得
const codePipelineRole = iam.Role.fromRoleArn(stack, 'CodePipelineRole', codePipelineServiceRoleArn);
// Artifactバケットの鍵を取得
const encryptionKey = kms.Key.fromKeyArn(stack, 'EncryptKey', artifactEncryptKeyArn);
// CodePipelineで使用するArtifactを定義
const sourceOutput = new codepipeline.Artifact(); // ソースファイルのアウトプット先
const buildOutput = new codepipeline.Artifact(); // ビルド結果のアウトプット先
// フロントエンドのソースコード用
const frontBucket = s3.Bucket.fromBucketArn(stack, `FrontBucket`, exportBucketArn);
const artifactBucket = s3.Bucket.fromBucketAttributes(stack, 'ArtifactBucket', {
bucketArn: artifactBucketArn,
encryptionKey,
});
// CodePipelineの設定
const pipeline = new codepipeline.Pipeline(stack, `FrontCodePipeline`, {
pipelineName: `FrontCodePipeline`,
role: codePipelineRole,
artifactBucket,
crossAccountKeys: true,
// パイプラインの各ステージを設定
stages: [
// ソース取得
{
stageName: 'Source',
actions: [
new actions.CodeCommitSourceAction({
actionName: 'CodeCommit',
repository: repository,
output: sourceOutput,
branch: targetBranch,
role: codeCommitRole,
})
]
},
// ビルド
{
stageName: 'Build',
actions: [
new actions.CodeBuildAction({
actionName: 'CodeBuild',
project: new codebuild.PipelineProject(stack, `BuildProject`, {
environment: {
buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3,
},
role: codeBuildRole,
encryptionKey,
environmentVariables: {
// 何かCodeBuildの環境変数があれば
}
}),
runOrder: 2,
input: sourceOutput,
outputs: [buildOutput],
})
]
},
// デプロイ
{
stageName: 'Deploy',
actions: [
new actions.S3DeployAction({
actionName: 'S3_Deploy',
bucket: frontBucket,
input: buildOutput,
}),
],
}
]
});
}
createPipeline
を呼び出すためにスタック化しています。
export class Stack3 extends Stack {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props);
const TARGET_BRANCH = "master";
const SOURCE_REPOSITORY_ARN = "XXXXXXXXXXX";
const ARTIFACT_BUCKET_ARN = "XXXXXXXXXXX";
const ARTIFACT_ENCRYPT_KEY_ARN = "XXXXXXXXXXX";
const CODE_COMMIT_ACCESS_ROLE_ARN = "XXXXXXXXXXX";
const CODE_BUILD_SERVICE_ROLE_ARN = "XXXXXXXXXXX";
const CODE_PIPELINE_SERVICE_ROLE_ARN = "XXXXXXXXXXX";
const EXPORT_BUCKET_ARN = "XXXXXXXXXXX";
createPipeline(this, {
targetBranch: TARGET_BRANCH,
artifactBucketArn: ARTIFACT_BUCKET_ARN,
artifactEncryptKeyArn: ARTIFACT_ENCRYPT_KEY_ARN,
sourceRepositoryArn: SOURCE_REPOSITORY_ARN,
codeCommitAccessRoleArn: CODE_COMMIT_ACCESS_ROLE_ARN,
codeBuildServiceRoleArn: CODE_BUILD_SERVICE_ROLE_ARN,
codePipelineServiceRoleArn: CODE_PIPELINE_SERVICE_ROLE_ARN,
exportBucketArn: EXPORT_BUCKET_ARN,
});
}
}
これでクロスアカウントのパイプラインの構築ができました。 出来上がった構成図をあらためて載せておきます。
今回はクラメソさんの記事を参考に、クロスアカウントでCI/CDを構築するのをCDKを用いて行いました。 あとは環境アカウントを何個も作って「開発環境」「検証環境」といった具合に、それぞれの環境でCI/CDを組み上げることで、互いの環境に影響を及ぼさないようなCI/CDを構築することができます。
おそらく、人によってはデプロイ先がS3
でなくECS
やEC2
だったりするケースもあると思うので、その際はこの記事をベースにいくらか改良が必要になります。
今回の内容が役立ちましたら幸いです。