CDKを使用した案件のまとめ記事 - その2(クロスアカウントのパイプライン構成)

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

はじめに

CI1-1の石井です。 前回に引き続きCDKを案件で使ってみた内容をまとめた記事となります。

対象

  • その1の記事を読んだ方
  • CDKを用いてクロスアカウントのパイプラインを組んでみたい方

本記事で記載すること

本記事では本番環境とステージング環境の二つに分けてパイプラインとそれに紐づくブランチを作成します。

本番環境とステージング環境分離について

私はプロジェクトで主にインフラ部分を担当しているのですが、前々から下記の点が課題と考えていました。

  • マネージドコンソールから手作業で作成することが手間だし危険
  • アプリ担当者がインフラチームに断りなく勝手にリソースを変更する
  • リソースの修正が入るとドキュメントを2環境分修正しなければならないため、非常に手間

そのため、パラメータシート通りに機械が行い、環境差分は変数を用いてコントロールしたいと考えており、CDKで表現できないか模索していました。

本記事では上記の課題を解決すべく、下記の要素を前提にCDKでどう表現するかを記載します。

  • CDKを用いてクロスアカウントのパイプラインを作成
  • cdk.jsonを用いて環境分離を行う

本記事で紹介・作成する内容

本記事では別のアカウントに対してCloudForntmationを実行するパイプラインを作成します。 パイプラインは下記の要件で作成します。

  • ブランチは本番環境用の「Live」とステージング用の「Stg」を作成する
  • 環境固有の値はcdk.jsonに記載する
  • 本番環境へのリリースは必ずStgブランチをLiveブランチへマージする。これにより本番とステージングの差分をなくす。

最終的に下図の構成を作成します。

環境準備

前回と同様、CDKコマンドが実行できる環境があれば問題ありません。 アカウントは3つ準備しておきます。各アカウントは次のような役割を持ちます。

アカウント名 役割
インフラカウント Gitリポジトリとパイプラインが存在するアカウント
Stgアカウント ステージング用のアカウント
Liveアカウント 本番環境用のアカウント

bootstrapの実行

クロスアカウントでCDKを実行するにはアカウント同士の信頼関係を作成する必要があります。 具体的にはcdk bootstrapコマンドにてCloudFormationを実行するロールとインフラアカウントとの信頼関係を設定します。

なお、クロスアカウントでCDKを実行する手順は下記のドキュメントを参考にしています。 https://aws.amazon.com/jp/blogs/news/deploying-a-cdk-application-using-the-cdk-pipelines-modern-api/

インフラアカウントでのbootstrap

前回の記事でパイプラインが構築したアカウントを「インフラアカウント」とします。 その1の記事を読んだだけで実際に構築していない方は下記のコマンドを実行してください。

$ npx cdk bootstrap \
  --profile インフラアカウントプロファイル \
  --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
  aws://インフラアカウントID/ap-northeast-1

なお、公式ドキュメントにも記載がありますが、bootstrap自体は何度実行しても特に影響はありません。

これは、CDK アプリケーションをデプロイする環境ごとに 1回だけ行う必要があります。環境がすでにブートストラップされているかどうかが不明な場合は、再度コマンドを実行しても問題ありません。

Stgアカウントでのbootstrap

Stgアカウントに対してbootstrapを実行します。 Stgアカウントには事前にクレデンシャルを発行するか、Stgアカウント内で作成したCloud9などで実行してください。

$ npx cdk bootstrap \
  --profile Stgアカウントプロファイル \
  --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
  --trust インフラアカウントID \
  aws://StgアカウントID/ap-northeast-1

Liveアカウントでのbootstrap

Liveアカウントに対してbootstrapを実行します。 Liveアカウントには事前にクレデンシャルを発行するか、Liveアカウント内で作成したCloud9などで実行してください。

$ npx cdk bootstrap \
  --profile Liveアカウントプロファイル \
  --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \ 
  --trust インフラアカウントID \
  aws://LiveアカウントID/ap-northeast-1

パイプラインの編集

次にパイプラインにLive用とStg用のパイプラインを作成します。 内容的としては前回記事で記載した内容に下記の要素を追加します。

  • CodeCommitのコミットをチェックするブランチ名をmasterからStgに変更
  • Stg用のパイプラインの記載をコピーし、Stgの要素に記載を変更する
  • crossAccountKeysにtureを指定
//ファイル名:lib/pipeline-stacks.ts

import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as pipelines from 'aws-cdk-lib/pipelines';
import { CodePipelineSource } from 'aws-cdk-lib/pipelines';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import { ANE1Deploy } from './apnortheast1stage';
export class DemoPipelineStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // CodeCommitのリポジトリを作成
    const repo = new codecommit.Repository(this, 'DemoRepo', {
      repositoryName: "DemoRepo"
    });

    // CDKをデプロイするためのパイプラインを構成
    const StgPipeline = new pipelines.CodePipeline(this, 'StgPipeline', {
      // クロスアカウントを利用する場合に必要です。
      crossAccountKeys: true,
      // CDKのコードをCodeBUildで生成する
      synth: new pipelines.CodeBuildStep('Synth', {
        input: CodePipelineSource.codeCommit(repo, 'Stg'),
        installCommands: [
          'npm i -g npm@latest',
          'npm install -g aws-cdk',
        ],
        commands: [
          'npm ci',
          'npm run build',
          'npx cdk synth -c stage=Stg'
        ],
      }),
    });
    // 東京リージョンにデプロイするステージをパイプラインに追加
    const ANE1Live = new ANE1Deploy(this, 'Ane1StageStg', {
      env: {account: 'StgアカウントID',region: 'ap-northeast-1'}
    });
    StgPipeline.addStage(ANE1Stg);

    // CDKをデプロイするためのパイプラインを構成
    const LivePipeline = new pipelines.CodePipeline(this, 'LivePipeline', {
      // クロスアカウントを利用する場合に必要です。
      crossAccountKeys: true,
      // CDKのコードをCodeBUildで生成する
      synth: new pipelines.CodeBuildStep('Synth', {
        input: CodePipelineSource.codeCommit(repo, 'Live'),
        installCommands: [
          'npm i -g npm@latest',
          'npm install -g aws-cdk',
        ],
        commands: [
          'npm ci',
          'npm run build',
          'npx cdk synth -c stage=Live'
        ],
      }),
    });
    // 東京リージョンにデプロイするステージをパイプラインに追加
    const ANE1Live = new ANE1Deploy(this, 'Ane1StageLive', {
      env: {account: 'LiveアカウントID',region: 'ap-northeast-1'}
    });
    LivePipeline.addStage(ANE1Live);

  }
}

デプロイ先が二つのアカウントになるため、証明書をStg用とLive用の二つを配置しておき、cdk.jsonに記載を追加しておきます。

// ファイル名:cdk.json
    "Stg": {
      "$comment": "Stg環境",
      "Aliases": {
        "demodomain": ["staging-cfdemo.hogehoge.tech"]
      },
      "CertArn": "stg環境のus-east-1の証明書ARN"
    },
    "Live": {
      "$comment": "Live環境",
      "Aliases": {
        "demodomain": ["live-cfdemo.hogehoge.tech"]
      },
      "CertArn": "Live環境のus-east-1の証明書ARN"
    }

ここで一旦コミットし、最後にgit上にLive用とStg用のブランチを作成します。

git checkout -b Live
git checkout -b Stg
git checkout master
#ブランチ一覧の確認
git branch -a
  Live
  Stg
* master

この内容でmasterブランチからプッシュしてみます。

パイプラインはUpdatePipelineのステージでパイプライン自身のアップデートを行い、Stg用とLive用のパイプラインが作成されます。

UpdatePipelineを経て新しい二つのパイプラインが完成と同時に動き出し、S3とCloudFrontのリソースも両アカウントにデプロイされます。

masterブランチに紐づくリソースについて

前回の記事で作成したmasterのブランチに紐づくパイプラインは今後Stgブランチに紐づき、Stgアカウントにデプロイされるようになります。

そのため、S3とCloudFrontはパイプラインとの関係は無くなってしまいますが、リソース自体は削除されません。

使用予定がなければ、パイプラインが稼働中にインフラアカウントのS3+CloudFrontのリソースをCloudFormationの画面から削除しておきましょう。

assumeroleでの失敗が発生した場合

UpdatePipelineのステージでありがちな失敗は下記のようなassume roleの失敗です。 原因はbootstrapコマンドのやり忘れです。

アカウント間に信頼関係がなくCloudFormationの実行ロールにassume roleができなかったり、そもそもcdk用のロールがアカウント内に作成されていない時にエラーとなります。

個人的にありがちなエラーなので記載しておきました。

not authorized to perform AssumeRole on role 

デプロイ確認

問題なくデプロイできれば二つの環境にS3とCloudFrontのWebサイト公開ができたと思います。

※下記画像はドメインで接続していますが、CloudFrontのドメインは別途手動でroute53に登録しています。

クロスアカウントパイプラインの個人的にハマったところ

クロスアカウントのパイプラインは作成できました。

ただ、パイプラインを構成するコードはLiveとStg用のブランチの内容は常に1:1の関係である必要があります。

以前CDKのパイプラインのUpdatePipelineのステージが原因でパイプラインを無限ループさせてしまったので注意事項として記載します。

無限ループ実験

まずStgブランチでパイプラインをアップデートしてみます。

CodePipelinesの「dockerEnabledForSynth」という要素をtrueにしてみます。下記のように編集します。

ファイル名:pipeline-stacks.ts

    // CDKをデプロイするためのパイプラインを構成
    const StgPipeline = new pipelines.CodePipeline(this, 'StgPipeline', {
      // クロスアカウントを利用する場合に必要です。
      crossAccountKeys: true,
      // CodeBuildの特権モードをtrueにする
      dockerEnabledForSynth: true,
〜中略〜
    // CDKをデプロイするためのパイプラインを構成
    const LivePipeline = new pipelines.CodePipeline(this, 'LivePipeline', {
      // クロスアカウントを利用する場合に必要です。
      crossAccountKeys: true,
      // CodeBuildの特権モードをtrueにする
      dockerEnabledForSynth: true,

これで一旦コミットしてプッシュすると、通常通りパイプラインが動き始めます。

実際の無限ループの動き

UpdatePipelineのステージで通常通りパイプラインにアップデートを始めます。

UpdatePipelineステージにより、パイプラインに対して自己アップデートを始めるのですが、Liveブランチ用のパイプラインも一緒に動き始めてしまいます。

Live用のパイプラインも同じくUpdatePipelineのステージがあるため、Liveブランチのコードでアップデートが始まります。

一方Stg用のパイプラインは最初にアップデートを行った内容でパイプラインが更新されています。 Liveブランチのパイプラインのコードとは異なるパイプラインで構成されています。 そのため、Liveブランチ発のパイプライン更新の対象となり、Liveブランチの「dockerEnabledForSynth」が記載されていない状態のコードでパイプラインが更新されてしまいますが、Stgブランチのパイプラインのコード内容と実態が一致していないため再度UpdatePipelineステージでアップデートがかかり無限ループに陥ります。

原因

結論としてはLiveブランチとStgブランチのパイプラインのコードが同一でないため、古いコードと新しいコードが更新しあってしまい無限ループに陥ってしまいました。

図にすると下記となります。

対策

今の所、下記の対策のいずれかを実施しています。 1. パイプラインの修正は両ブランチともに常に同一の状態であることを維持する 2. Stgのブランチを更新してプッシュした後、PipelineUpdateで動き出すliveブランチのパイプラインを手動で停止する。 3. Liveブランチのソースステージからパイプラインの遷移を無効化しておく

私は2の手法を使っていますが、もしかしたら機械的に抑制する術があるかもしれません。

知ってたらどなたか教えてください。

まとめ

ステージング用の環境と本番用の環境で二つのパイプラインを組むことができました。 これでマネージドコンソールに入らずともAWSリソースがデプロイできるようになったため、統制周りを整備すれば誰も実際のアカウントにログインしてGUI作業を行う必要がなくなります。

統制の部分は別の機会に記事にまとめたいと思います。 次回はApiGatewayとLambdaについて記載したいと思います。