CodeDeployとデプロイ先のEC2が違うクロスアカウントな構成をCDKとCode Pipelineで実現する記事

記事タイトルとURLをコピーする

CS1の石井です。

タイトルの通り、CodeDeployとデプロイ先の対象が別々のクロスアカウントの状態で、CodePipelineを通じてデプロイ可能な実装を行なってみます。

実装のパターンは二通りあると考えており、一つは以下のようなデプロイ先(以下Infraアカウント)とデプロイ先(以下Stgアカウント)の二つのアカウントでパイプラインを作るやり方です。

もう一つはデプロイ元のInfraアカウントだけパイプラインを作り、デプロイ先のStgアカウントにはCodeDeployのアプリケーションだけ作成して、Infraアカウントのパイプラインから実行する、といった実装です。

弊プロジェクトではパイプラインの通知の仕組みなどはInfraアカウントに作成しており、デプロイ先のアカウントごとにSNSなどを作成したくなかったので、今回の記事はパターン2の構成を記載したいと思います。

対象者

  1. CDKワークショップを終わらせている人
  2. アカウントが2つあり、どちらのアカウントもbootstrapを完了させている人

結論だけ欲しい人向け:

この記事のコードでパターン2の構成を作成できます。 目次の2と3のコードを貼り付け、Deployec2Stack→pipelineStackの順番でデプロイしてください。

1. 具体的な実装のイメージ

一口にデプロイ元のInfraアカウントからデプロイ先のStgアカウントのCodeDeployのアプリケーションを実行するといっても、具体的にどのような権限設定などが必要なのか考える必要があります。

以下のリソースの信頼関係やassume roleをまとめてみました。

デプロイ元アカウントでの主要なリソース

リソース 役割
S3 CDKによる自動生成されるCFn格納S3
配布用htmlファイルやappspecを格納する。
KMS CDKによる自動生成S3暗号化用キー
デプロイ先ロールからアクセスを許可する。
パイプライン実行ロール CDKにより自動生成されるロール
デプロイ先のCodeDeployを実行する。

デプロイ先アカウントでの主要なリソース

リソース 役割
CFn実行ロール CDKデプロイ用のロール
bootsrap時に生成されCFnを実行する。
EC2ロール S3とKMSにアクセスし
デプロイ用資材をサーバ内に配置する。
CodeDeploy用ロール Code PipelineのAssumeRoleを許可し
EC2のCodeDeploy-Agentに指令を出す。

図にすると以下のようなイメージです

なんだか複雑そうに感じます。

内心ここまで複雑になるならパターン1の実装でもいいかなと思ったのですが、パイプラインの通知の作り込みを各アカウントにばらまきたくないと思ったので頑張ってみます。

2. CDKの準備をする

CDKのinit

早速コードを書くため、プロジェクトをinitします。

mkdir deployec2; cd $_
npx cdk init --language typescript
touch lib/pipeline.ts
mkdir src

initが完了したら次にbin/deployec2.tsを編集します。

#!/usr/bin/env node
# bin/deployec2.ts
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { pipelineStack } from '../lib/pipeline';

const app = new cdk.App();
new pipelineStack(app, 'Deployec2Stack', {
  env: {
    account: 'NNNNNNNNNNN',
    region: 'ap-northeast-1'
  }
});

※NNNNNNNNNの部分はデプロイ元アカウントを記載してください

デプロイ予定の資材とappspecの準備

appspecに関しては下記のAWSのサンプルを参考に資材をダウンロードします。

https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/tutorials-simple-codecommit.html

このリンクからサンプルアプリケーションをダウンロード

上記のサンプルアプリケーションは以下の説明になっています。

サンプルアプリケーションには、CodeDeployでデプロイするための次のファイルが含まれています。

  • appspec.yml— アプリケーション仕様ファイル (AppSpecファイル) は、CodeDeployがデプロイを管理するために使用される YAML 形式のファイルです。AppSpecファイルの詳細については、AWS CodeDeployユーザーガイドの「CodeDeployAppSpecファイルリファレンス」を参照してください。

  • index.html— インデックスファイルには、デプロイされたサンプルアプリケーションのホームページが含まれます。

  • LICENSE.txt— ライセンスファイルには、サンプルアプリケーションのライセンス情報が含まれています。

  • スクリプト用ファイル — サンプルアプリケーションは、スクリプトを使用してインスタンス上の場所にテキストファイルを書き込みます。次のように、CodeDeploy複数のデプロイライフサイクルイベントごとに 1 つのファイルが書き込まれます。

    • (Linux サンプルのみ) scripts フォルダー — このフォルダーには、依存関係をインストールしたり、自動デプロイ用のサンプルアプリケーションを起動および停止したりするための次のシェルスクリプトが含まれています。install_dependencies、start_server、stop_server

資材の配置

上記サンプルを端的に言えばappspecとデプロイ用のhtmlファイルがセットになっています。appspecはルート直下に配置し、デプロイ資材はsrcディレクトリ配下に配置します。

サンプルのzipファイルを展開し、以下のファイル構造になっていれば問題ありません。

deployec2 % tree src
src
├── LICENSE.txt
├── index.html
└── scripts
    ├── install_dependencies
    ├── start_server
    └── stop_server

1 directory, 5 files

deployec2 % ls appspec.yml 
appspec.yml

また、appspec.ymlは以下のように編集して保存してください。

version: 0.0
os: linux
files:
  - source: src/index.html
    destination: /var/www/html/
hooks:
  BeforeInstall:
    - location: src/scripts/install_dependencies
      timeout: 300
      runas: root
    - location: src/scripts/start_server
      timeout: 300
      runas: root
  ApplicationStop:
    - location: src/scripts/stop_server
      timeout: 300
      runas: root


3. いきなり結論のコードを記載

今回使うコードを全て貼ります。 lib/pipeline.tsに以下のように編集してください。

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam'; 
import { Construct } from 'constructs';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy';
import {
  aws_codecommit as codecommit,
  pipelines as pipelines,
} from "aws-cdk-lib";
import { Stage, StageProps } from "aws-cdk-lib";

const ec2RoleName = "Ec2ExecRole";
const codeDeployRoleName = 'CodeDeployExecRole'
const deployAccount = 'YYYYYYYYYYYYY'
const pipelineAccount = 'NNNNNNNNNNN'
export class pipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const repo = new codecommit.Repository(this, `ccr-multideployec2`, {
      repositoryName: "ccr-multideployec2",
    });

    const Pipeline = new pipelines.CodePipeline(this, "Pipeline", {
      pipelineName: "multideployec2Pipeline",
      crossAccountKeys: true,
      enableKeyRotation: true,
      dockerEnabledForSynth: true,
      synth: new pipelines.CodeBuildStep("Synth", {
        input: pipelines.CodePipelineSource.codeCommit(repo, "main"),
        installCommands: ["npm ci"],
        commands: [
          "npm run build",
          "npx cdk synth",
          "cp -r src cdk.out",  // アーティファクトSynth_Outputにデプロイするhtmlファイルが格納されたsrcを含める
          "cp appspec.yml cdk.out"
          ],
      })
    });


    const stgStage = Pipeline.addStage(
      new AppStage(this, `DeployStage`, {
        env: {
          account: deployAccount,
          region: "ap-northeast-1",
        },
      })
    );

    Pipeline.buildPipeline()


    // 既存のCodeDeployの実行ロールを指定
    const existingDeployRole = iam.Role.fromRoleArn(this, 'existingDeployRole',
    `arn:aws:iam::${deployAccount}:role/${codeDeployRoleName}`);
    // 既存のEc2の実行ロールを指定
    const existingEc2Role = iam.Role.fromRoleArn(this, 'existingEc2Role',
    `arn:aws:iam::${deployAccount}:role/${ec2RoleName}`);

    // CodePipelineにステージとアクションを追加
    const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'MyDeploymentGroup2');
    Pipeline.pipeline.addStage({
      stageName: 'Codedeploystage',
      actions:[
        new codepipeline_actions.CodeDeployServerDeployAction({
          actionName: 'CodeDeploy',
          input: codepipeline.Artifact.artifact("Synth_Output"),
          deploymentGroup: codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(this, 'ExistingDG', {
            deploymentGroupName: 'MyDeploymentGroup2',
            application: codedeploy.ServerApplication.fromServerApplicationName(this, 'ExistingApp', 'MyApplication2'),
          }),
          role: existingDeployRole
        })
      ]
    })


    Pipeline.pipeline.artifactBucket.encryptionKey!.grantDecrypt(existingDeployRole);
    Pipeline.pipeline.artifactBucket.encryptionKey!.grantDecrypt(existingEc2Role);

    Pipeline.pipeline.artifactBucket.addToResourcePolicy(new iam.PolicyStatement({
      actions: ['s3:GetObject*', 's3:List*',"s3:GetBucket*",],
      resources: [
        Pipeline.pipeline.artifactBucket.arnForObjects('*'),
        Pipeline.pipeline.artifactBucket.bucketArn
      ],
      effect: iam.Effect.ALLOW,
      principals: [existingEc2Role.grantPrincipal],
    }));
  }
}


export class AppStage extends Stage {
  constructor(scope: Construct, id: string, props: StageProps) {
    super(scope, id, props);

    new Deployec2Stack(this, "id");
  }
}

export class Deployec2Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StageProps) {
    super(scope, id, props);


    // VPCの作成
    const vpc = new ec2.Vpc(this, 'CustomVPC', {
      maxAzs: 2,
      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
      subnetConfiguration: [
        {
          subnetType: ec2.SubnetType.PUBLIC,
          name: 'PublicSubnet',
          cidrMask: 24,
        },
        {
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          name: 'PrivateSubnet',
          cidrMask: 24,
        },
      ],
      natGateways: 1,
    });

    // EC2に付与するIAMロールの作成
    const ec2Role = new iam.Role(this, 'EC2Role', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      roleName: ec2RoleName
    });

    // SSMへのアクセス権限
    ec2Role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMFullAccess'));

    const codeDeployPermissions = new iam.PolicyStatement({
      actions: [
        "codedeploy:CreateDeployment",
        "codedeploy:GetDeploymentConfig",
        "codedeploy:RegisterApplicationRevision",
        "codedeploy:GetApplicationRevision",
        "codedeploy:GetDeployment",
        "codedeploy:CreateDeploymentConfig"
      ],
      resources: ['*'],
      effect: iam.Effect.ALLOW
    });

    const kmsPermissions = new iam.PolicyStatement({
      actions: ["kms:Decrypt","kms:DescribeKey"],
      resources: ['*'],
      effect: iam.Effect.ALLOW
    });
    
    const s3Permissions = new iam.PolicyStatement({
      actions: ['s3:GetObject*', 's3:List*',"s3:GetBucket*",],
      resources: ['*'],  // ここではすべてのS3バケットを対象としていますが、特定のバケットに絞ることもできます
      effect: iam.Effect.ALLOW
    });

    const ec2Policy = new iam.Policy(this, 'Ec2Policy', {
      statements: [codeDeployPermissions,kmsPermissions, s3Permissions]
    });


    ec2Policy.attachToRole(ec2Role);


    
    // CodeDeployに付与するIAMロールの作成
    const codeDeployRole = new iam.Role(this, 'codeDeployRole', {
      assumedBy: new iam.CompositePrincipal(
        new iam.ServicePrincipal('codedeploy.amazonaws.com'),
        new iam.ServicePrincipal('codepipeline.amazonaws.com'),
        new iam.ArnPrincipal(`arn:aws:iam::${pipelineAccount}:root`) 
      ),
      roleName: codeDeployRoleName
    });

    const codeDeployPolicy = new iam.Policy(this, 'CodeDeployPolicy', {
      statements: [s3Permissions,codeDeployPermissions,kmsPermissions]
    });
    codeDeployPolicy.attachToRole(codeDeployRole)

    


    // EC2インスタンスの作成
    const ami = new ec2.AmazonLinuxImage({
      generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      cpuType: ec2.AmazonLinuxCpuType.X86_64,
    });
    const instanceName = 'MyInstanceName2';
    const instance = new ec2.Instance(this, 'EC2Instance', {
      vpc,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: ami,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      role: ec2Role,  // 作成したIAMロールを使用
      userData: ec2.UserData.custom(`
      #!/bin/bash
      sudo yum update -y
      sudo yum install -y ruby wget
      cd /home/ec2-user
      wget https://aws-codedeploy-${this.region}.s3.${this.region}.amazonaws.com/latest/install
      chmod +x ./install
      sudo ./install auto
    `),
    });
    // インスタンスに 'Name' タグを追加
    cdk.Tags.of(instance).add('Name', instanceName);

    // セキュリティグループの作成とインスタンスへの割り当て
    const webServerSG = new ec2.SecurityGroup(this, 'WebServerSG', {
      vpc,
      description: 'Allow HTTP and SSH access to EC2 instance',
      allowAllOutbound: true,
    });

    webServerSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'allow HTTP access');
    webServerSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'allow SSH access');
    instance.addSecurityGroup(webServerSG);


    // CodeDeploy アプリケーションとデプロイメントグループの作成
    const application = new codedeploy.ServerApplication(this, 'MyApplication2', {
      applicationName: 'MyApplication2',
    });




    // DeploymentGroupの変更
    const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'MyDeploymentGroup2', {
      application,
      deploymentGroupName: 'MyDeploymentGroup2',
      ec2InstanceTags: new codedeploy.InstanceTagSet(
        {
          'Name': [instanceName],
        },
      ),
      role: codeDeployRole,
      deploymentConfig: codedeploy.ServerDeploymentConfig.ALL_AT_ONCE,
    });
  }
}

NNNNNNとYYYYYYYはデプロイ元、デプロイ先のアカウント番号をそれぞれ修正してください。

コード概要

いきなり長いコードを出しましたが、作成するリソースとしては非常に単純です。

まずクラス単位で分割すると以下のようになります。

  • デプロイ先のEC2を作成するDeployec2Stack
  • Deployec2Stackのデプロイするためのステージ※となるAppStage
  • デプロイ元のパイプラインを構成するpipelineStack

※ステージとはリソースのデプロイ対象となる特定の環境やコンテキストを定める要素です、実際にリソースを作成するDeployec2Stackをどこの環境に流すかを定義します。

図にすると以下のような形です。なお、CDKが自動で作成するKMSなどは図に含まれていますが、コードには記載されていません。

4. Deployec2Stackのデプロイ実行

パイプラインを使った構成のため、最初にデプロイ元のパイプラインから記載すると思いますが、今回はデプロイ先のコードから記載します。

理由はKMSポリシーにデプロイ先のCodeDeployとEC2のロールに許可ポリシーを記載する必要があるのですが、AWS上に実際に存在するロールでないとエラーになってしまうため、先にEC2のスタックをデプロイします。

まずは個別でスタックをデプロイするためにスタック名を確認します。

ishiinobuaki@ishiinokinoMBP8 pipeline-deployec2 % npx cdk list                                                 
Deployec2Stack
Deployec2Stack/DeployStage/id

ishiinobuaki@ishiinokinoMBP8 pipeline-deployec2 % 

二つスタック名が出てきますが、後者のスタックがパイプラインから認識されているスタックです。 cdkコマンドで直接デプロイしても、パイプラインからのスタック名が同じであれば、後でパイプラインからcdkが実行されても問題ありません。そのため、後者のスタック名を使用します。

デプロイするスタック名が確認できたため、下記コマンドでデプロイします。

npx cdk deploy Deployec2Stack/DeployStage/id --profile デプロイ先のプロファイル名

5. pipelineStackのデプロイ実行

次はパイプラインを構成します。

これは通常通りbinから呼ばれるため、下記のコマンドでdeployを行います。

npx cdk deploy --profile デプロイ元のプロファイル

デプロイ完了後、CodeCommitにコードをpushします。

git remote set-url origin codecommit::ap-northeast-1://デプロイ元プロファイル名@ccr-multideployec2
git push --set-upstream origin main

これでpushが完了したため、パイプラインが稼働してhtmlファイルのデプロイが完了しているはずです。

EC2のアドレスにアクセスして正常にデプロイされていることを確認します。

無事デプロイできているようです。

またEC2のパブリックIPでアクセスすると以下の画像が出ます。 ※サンプルのhtmlファイルにHogehogeを追加しています

これでクロスアカウントのCodeDeployの基盤はできました。

コードと結論だけ欲しい方は上記のコードから後は自己流で改造してみてください。 以降の話はパイプラインとEC2を構成するスタックのコードの解説となります。

6. Deployec2Stackコード解説

EC2を構成するスタックで重要な部分について解説します。

EC2作成

EC2の作成部分はCodeDeploy-Agentをuserdataでインストールするぐらいで、他は普通にEC2作成するだけです。

重要なのはIAMロールの作成部分です。

基本的にCDKの場合、対象のオブジェクトに.grant何某とかaddPermissionとかで構成したいのですが、対象のオブジェクトが別スタックだったり、未作成だったりするので普通のIAM Policyの記載を行っています。

また、前述の通りデプロイ元のKMSポリシーにデプロイ先のCodeDeployの実行ロールをDecryptの許可を記載する必要があるのですが、自動生成されたロール名の情報をpipelineStackで取得できませんでした。

そのため、tsファイルの冒頭でグローバル変数にてロール名を記載して、KMSやIAMポリシーにはarnのフォーマットを記載してロール名を入れ込む構図となっております。

const ec2RoleName = "Ec2ExecRole";
const codeDeployRoleName = 'CodeDeployExecRole'
〜中略〜
    // EC2に付与するIAMロールの作成
    const ec2Role = new iam.Role(this, 'EC2Role', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      roleName: ec2RoleName
    });

    // SSMへのアクセス権限
    ec2Role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMFullAccess'));

    const codeDeployPermissions = new iam.PolicyStatement({
      actions: [
        "codedeploy:CreateDeployment",
        "codedeploy:GetDeploymentConfig",
        "codedeploy:RegisterApplicationRevision",
        "codedeploy:GetApplicationRevision",
        "codedeploy:GetDeployment",
        "codedeploy:CreateDeploymentConfig"
      ],
      resources: ['*'],
      effect: iam.Effect.ALLOW
    });

    const kmsPermissions = new iam.PolicyStatement({
      actions: ["kms:Decrypt","kms:DescribeKey"],
      resources: ['*'],
      effect: iam.Effect.ALLOW
    });
    
    const s3Permissions = new iam.PolicyStatement({
      actions: ['s3:GetObject*', 's3:List*',"s3:GetBucket*",],
      resources: ['*'],  // ここではすべてのS3バケットを対象としていますが、特定のバケットに絞ることもできます
      effect: iam.Effect.ALLOW
    });

    const ec2Policy = new iam.Policy(this, 'Ec2Policy', {
      statements: [codeDeployPermissions,kmsPermissions, s3Permissions]
    });


    ec2Policy.attachToRole(ec2Role);

    // SSMへのアクセス権限
    ec2Role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMFullAccess'));

〜中略〜
    const instance = new ec2.Instance(this, 'EC2Instance', {
      vpc,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: ami,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      role: ec2Role,  // 作成したIAMロールを使用
      userData: ec2.UserData.custom(`
      #!/bin/bash
      sudo yum update -y
      sudo yum install -y ruby wget
      cd /home/ec2-user
      wget https://aws-codedeploy-${this.region}.s3.${this.region}.amazonaws.com/latest/install
      chmod +x ./install
      sudo ./install auto
    `),
    });

CodeDeploy作成

こちらもEC2と同じく、アプリケーションの作成よりもIAM Roleの作り込みが重要です。

本来はあまりやりたくないのですが、CodeDeployのロールを自動生成されたものではなく、以下のコードで明示的にロールを作成しています。理由はEC2のロールと同じです。

    // CodeDeployに付与するIAMロールの作成
    const codeDeployRole = new iam.Role(this, 'codeDeployRole', {
      assumedBy: new iam.CompositePrincipal(
        new iam.ServicePrincipal('codedeploy.amazonaws.com'),
        new iam.ServicePrincipal('codepipeline.amazonaws.com'),
        new iam.ArnPrincipal(`arn:aws:iam::${pipelineAccount}:root`) 
      ),
      roleName: codeDeployRoleName
    });

    const codeDeployPolicy = new iam.Policy(this, 'CodeDeployPolicy', {
      statements: [s3Permissions,codeDeployPermissions,kmsPermissions]
    });
    codeDeployPolicy.attachToRole(codeDeployRole)

    

    // CodeDeployに付与するIAMロールの作成
    const codeDeployRole = new iam.Role(this, 'codeDeployRole', {
      assumedBy: new iam.CompositePrincipal(
        new iam.ServicePrincipal('codedeploy.amazonaws.com'),
        new iam.ServicePrincipal('codepipeline.amazonaws.com'),
        new iam.ArnPrincipal(`arn:aws:iam::${pipelineAccount}:root`) 
      ),
      roleName: codeDeployRoleName
    });

なお、余談ですがEC2のスタックを流し、パイプラインのスタックを流した後、EC2のスタックを消すとパイプラインのスタックのバケットポリシーに記載したARNは実体のIAM Roleがなくなってしまうため、下記のような記載になってしまいます。

元々はEC2のロールとCodeDeployのロール名が記載されていたのですが、UUIDみたいな記載になってしまいました。

こうなってしまうと再度EC2スタックを流し直して、パイプラインスタックも流し直した方が早いです。

7. pipelineStackコード解説

こちらも重要なのはパイプラインの構成よりもIAM Roleとバケットポリシーになります。

アーティファクトバケットとKMS作成

まずはパイプラインのビルド時に生成されたアーティファクトバケットと、バケットの暗号化を行うKMSの構成について記載します。

この二つの要素はpipelinesモジュールが自動生成するバケットを使います。

アーティファクトバケットはpipelinesモジュールを使った時点で自動的に作成されますが、暗号化は以下の記載で有効化されます。

    const Pipeline = new pipelines.CodePipeline(this, "Pipeline", {
      pipelineName: "multideployec2Pipeline",
      crossAccountKeys: true,
      enableKeyRotation: true,

crossAccountKeysとenableKeyRotationでKMS暗号化と自動ローテーションがONの状態でアーティファクトがKMSによって暗号化されます。

そして下記のコードでデプロイ先のEC2とCodeDeployロールに対して許可を行います。

    // 既存のCodeDeployの実行ロールを指定
    const existingDeployRole = iam.Role.fromRoleArn(this, 'existingDeployRole',
    `arn:aws:iam::${deployAccount}:role/${codeDeployRoleName}`);
    // 既存のEc2の実行ロールを指定
    const existingEc2Role = iam.Role.fromRoleArn(this, 'existingEc2Role',
    `arn:aws:iam::${deployAccount}:role/${ec2RoleName}`);

〜中略〜

    Pipeline.pipeline.artifactBucket.encryptionKey!.grantDecrypt(existingDeployRole);
    Pipeline.pipeline.artifactBucket.encryptionKey!.grantDecrypt(existingEc2Role);

    Pipeline.pipeline.artifactBucket.addToResourcePolicy(new iam.PolicyStatement({
      actions: ['s3:GetObject*', 's3:List*',"s3:GetBucket*",],
      resources: [
        Pipeline.pipeline.artifactBucket.arnForObjects('*'),
        Pipeline.pipeline.artifactBucket.bucketArn
      ],
      effect: iam.Effect.ALLOW,
      principals: [existingEc2Role.grantPrincipal],
    }));

Appspecとデプロイ資材をアーティファクトバケットに含める

pipelinesモジュールはsynthステップで生成したCfnはcdk.outというディレクトリを作成し、格納します。

pipelinesモジュールのアーティファクト格納用ディレクトリはデフォルトでcdk.outの内容を全て格納し、S3バケットの"Synth_Output"というディレクトリに格納します。

そのため、synthステップでcdk.outにデプロイ対象のsrcディレクトリとappspecをcpで移動させます。

      synth: new pipelines.CodeBuildStep("Synth", {
        input: pipelines.CodePipelineSource.codeCommit(repo, "main"),
        installCommands: ["npm ci"],
        commands: [
          "npm run build",
          "npx cdk synth",
          "cp -r src cdk.out",  // アーティファクトSynth_Outputにデプロイするhtmlファイルが格納されたsrcを含める
          "cp appspec.yml cdk.out"
          ],
      })

Code Deployのステージ追加

アーティファクトにデプロイ資材の格納を行なったら以下のコードでpipelinesモジュールが作成したパイプラインに新しいデプロイステージを追加します。

    const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'MyDeploymentGroup2');
    Pipeline.pipeline.addStage({
      stageName: 'Codedeploystage',
      actions:[
        new codepipeline_actions.CodeDeployServerDeployAction({
          actionName: 'CodeDeploy',
          input: codepipeline.Artifact.artifact("Synth_Output"),
          deploymentGroup: codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(this, 'ExistingDG', {
            deploymentGroupName: 'MyDeploymentGroup2',
            application: codedeploy.ServerApplication.fromServerApplicationName(this, 'ExistingApp', 'MyApplication2'),
          }),
          role: existingDeployRole
        })
      ]
    })

インプットのアーティファクトバケットを指定する部分では input: codepipeline.Artifact.artifact("Synth_Output"), と記載しましたが、Synth_OutputはpipelinesモジュールがCFnをsynthしたとき、"Synth_Output"という名前のディレクトリで格納を行うためです。

先ほどのsynthステージのcommandでcpでsrcとaapspecをコピーした資材は、全てSynth_Outputの中に格納されているため、inputに指定しています。

なお、role: existingDeployRoleという記載がありますが、AWSのGUIのコンソール画面からはこのプロパティは設定することができません。

たまにパイプラインはGUIで設定できない項目があるのですが、今後のアップデートに期待したいところです。

以上で主要なコードの解説は終わりです。重要な部分は各リソースの作成よりIAMロールの連携部分となります。

また、このコード自体CDKっぽい書き方していません。そのためまだまだ改良の余地はあると思います。

8. まとめ

最後にまとめですが、特段の事情がなければパターン1の実装方法が良いと思います。

理由はシンプルだからです。

パイプラインの通知をアカウントごとにばら撒きたくないという願望からパターン2の実装で頑張りましたが、正直なところ以下の画像を思い出してました。

htmlファイルをデプロイするだけでここまで頑張る意味も薄いなぁと感じてました。

もしパターン2の実装方法でももっとシンプルにCDKかける人いたら教えてください。