AWS上で動くKubernetesクラスタのためのCI/CDパイプライン


  • いろいろと苦戦したのでまとめます

前提

  • クラウドはAWS
  • ソース管理はCodeCommit
  • ビルド環境はCodeBuild
  • 同じAWSのアカウント上で稼働しているKubernetesクラスタがデプロイターゲット
  • マニフェストファイルはHelmチャートで管理
  • アプリケーション毎にHelmチャートのリポジトリが存在する
  • Spinnakerが使える状態にある

却下案1(CodePipelineですべて管理)

  • CodeCommit
  • CodeBuild(ビルド用)
  • CodeBuild(デプロイ用)

概要

  • 大前提として、AWSにはKubernetesにデプロイするサービスが無い
  • そのため、デプロイの実装 or デプロイツールの構築が必要になる
  • ここではデプロイ用のCodeBuildを作成し、BuildSpecにデプロイコマンドを記載(Helmを使用しているのでHelmコマンド)
  • デプロイ用のBuildSpecもアプリ毎に作る必要がある
  • helm install(初回のデプロイ)とhelm upgrade(2回目以降のデプロイ)を使い分けないといけない
    • これはHelmを使わなければ回避できる
  • CodeBuildの本来の使用用途に合ってない感がすごい

却下案2(全てSpinnakerで管理)

  • CodeCommit
  • Spinnaker
    • CodeCommitへのPushをトリガーにAPIでSpinnakerのパイプラインをスタート
    • API(POST)→LambdaでCodeBuildを叩き、随時API(GET)→LambdaでCodeBuildのStatusを取得
      • Spinnakerのステージとしては「Webhook」で作成する
    • StatusがSucceededになったらのデプロイ
      • Spinnakerのステージとしては「Bake」と「Deploy」

概要

  • CDツールのSpinnakerだが、こちらでBuildの状況なども管理するやり方(こちらも本来の使い方とは違うと思いますが)

  • アプリ毎にAPI GatewayとLambda3つ(パイプライン開始用、CodeBuild開始用、CodeBuildのStatus取得用)を作成する必要がある、、、管理したくない

採用案

  • CodePipeline(CI)
    • CodeCommit
    • CodeBuild
  • Spinnaker(CD)

概要

  • 却下案1と却下案2の良いところを合わせたような構成
  • CodePipelineでCodeBuildでのビルドまでを管理し、CDはSpinnakerで実行
  • SpinnakerのパイプラインでトリガーをWebhookに設定しておき、CodeBuildの最後にそのURLを叩く
  • 開発環境・本場環境で別々のCodeBuildを作成し、それぞれのCodePipelineを定義する

CI(開発環境)

CodeBuildの作成

  • CodeBuildでやりたいことは以下の通り

    • ソースコードのビルド、テスト
    • Dockerイメージのビルド、プッシュ
    • S3へHelmChartをアップロード
      • S3経由でSpinnakerに渡すため
    • Spinnakerのパイプライン開始
  • CodeBuildの設定

    • 対象のソースコードと、Helmチャートが格納されているリポジトリのdevelopブランチを「Source」に指定する
    • 公式で提供されているCodeBuild用のDockerイメージ を基に、以下のパッケージを追加
      • helm
      • yq
        • yamlを操作するツール
    • NAT Gatewayが作成されているSubnetを含むVPC内で稼働させる
      • Spinnakerに紐付くセキュリティグループにこのIPアドレスを指定してパイプラインを始められるようにするため
    • HelmチャートをアップロードするS3バケットをArtifactsに指定しておく

Buildspec

  • コメントで概要を記載
buildspec.yaml
version: 0.2

env: 
  variables: 
     REGION_NAME: "ap-northeast-1" 
     REPOSITORY_NAME: "test" 
     BRANCH_NAME: "develop" 
     ECR_REPOSITORY: "XXXXXXXXXXX.dkr.ecr.$REGION_NAME.amazonaws.com/xxxxxxx" 
     # Helmチャートの名前
     CHART_NAME: "test" 
     # Helmチャートをアップロードするバケット内のパス
     ARTIFACT_PATH: "test-dev"
     # Spinnakerのパイプラインでトリガーに設定しているURL
     URL : "deploy-test-dev" 

phases:
  install:
    # Dockerデーモンの起動
    runtime-versions:
      docker: 18
    commands: 
      - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --storage-driver=overlay&  
      - timeout 15 sh -c "until docker info; do echo .; sleep 1; done"
  pre_build: 
    commands:
      - $(aws ecr get-login --no-include-email --region $REGION_NAME) 
  build:
    commands:
    # - ソースのビルドがあればここに記載
      # CODEBUILD_SRC_DIRは後述
      - cd $CODEBUILD_SRC_DIR
      - docker build -t tmp:latest . 
  post_build:
    commands:
      # CODEBUILD_RESOLVED_SOURCE_VERSIONは、CodeBuildの環境にデフォルトで設定されている環境変数
      # 対象のソースのコミットIDが設定されている
      # ここではビルドするDockerイメージのタグとして使用する
      - COMMIT_ID_APP=`echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8`
      - docker tag tmp:latest $ECR_REPOSITORY:$COMMIT_ID_APP
      - docker push $ECR_REPOSITORY:$COMMIT_ID_APP
      # CODEBUILD_SRC_DIR_infは後述
      - cd $CODEBUILD_SRC_DIR_inf
      # Helmチャートのvalues.yamlにデプロイ対象のDockerイメージのタグが記載されている
      # これを先ほど取得したコミットIDに書き換え
      # 更新ができなかったので、一時的にtmp.yamlという名前で保存→既存のvalues.yamlと置き換えを行なっている
      - yq -y '.image.tag |=  "'$COMMIT_ID_APP'"' $CHART_NAME/values.yaml  > $CHART_NAME/tmp.yaml 
      - mv $CHART_NAME/tmp.yaml  $CHART_NAME/values.yaml
      # packageコマンドでtgz形式に変換
      # この形式でSpinnakerに渡すことができる
      - helm package $CHART_NAME
      # S3にアップロード
      - aws s3 cp $CHART_NAME*.tgz s3://[backet name]/$ARTIFACT_PATH/$CHART_NAME.tgz
      # Spinnakerのパイプラインを開始
      - curl https://XXXXXXXXXX.com/webhooks/webhook/$URL -X POST -d "{ }" -H "content-type:application/json" 

CODEBUILD_SRC_DIRについて

  • CodeBuildでは、Sourceに指定したソースコードやファイルがCodeBuildの環境にマウントされている
  • CODEBUILD_SRC_DIRにはSource1に指定したSourceのパスが設定されており、例えばリポジトリを設定しておけば、cd $CODEBUILD_SRC_DIRコマンドでそのレポジトリのソースが格納されたパスに移動できる
  • 2つ以上のSourceを指定することもでき、2つ目以降はSource identiferを指定する。ここでは2つ目のSourceにinfというSource identiferを指定したため、CODEBUILD_SRC_DIR_infにそのSourceのパスが設定されている。つまり、CODEBUILD_SRC_DIR_[自分で設定したidentifer]という環境変数で呼び出すことができる

CodePipeline

  • 対象のCodeCommitのリポジトリのdevelopブランチと対象のCodeBuildを指定して保存
  • これにより、developブランチへのpushをトリガーにこのパイプラインか開始される

CD(開発環境)

Spinnakerでのパイプランの作成

  • Application→Actions→「Create Application」からApplicationを作成

  • 作成したApplicationの画面に遷移し、PIPELINES→「Configure a new pipeline」をクリック

  • パイプラインの名前を入力してCreate

Configuration

  • 最初にConfigurationの画面が表示されるので必要な設定を行う

Automated Triggers

  • パイプラインを開始するトリガーを設定する
  • 今回はCodeBuildからURLを叩いて開始したいので、「Webhook」を指定する
  • 「Source」にCodeBuildで指定した文字列を入力する

Expected Artifacts

  • Spinnakerに渡したいArtifactを指定する
  • 今回はCodeBuildでS3にアップロードしたHelmチャートを使用したいのでそのパスを入力する
  • 「Use Sefault Artifact」にチェックを入れ、同じパスを入力する

ステージ追加

  • 「Add stage」を押してステージを追加する

  • 今回必要なステージは2つ

    • Bake (Manifest)
      • Helmチャートを読み込ませてマニフェストファイルを生成する
    • Deploy(Manifest)
      • Bakeステージで作成したマニフェストファイルをKubernetesクラスタに適用する

Bake (Manifest)

Bake (Manifest) Configuration

  • Template Render
    • リソースの名前やNamespaceを指定する
  • Template Artifact
    • パイプラインのConfigurationで指定したArtifactがプルダウン形式で選択できる
  • Overrides
    • Helmチャートのvalues.yamlで上書きしたい値がある場合はここで指定できる

Produces Artifacts

  • Base64を指定する
  • 名前を適当に入力

Deploy (Manifest)

  • Bakeステージが終わってから始めるので「Depends On」に該当のステージ名を入力する
  • パラレルで複数のステージを実行することもできる

Deploy (Manifest) Configuration

  • Manifest Configuration
    • Artifactを選択して、Bakeステージで生成したArtifactをプルダウンから選択する

  • 「Save Changes」を押下して完了!
  • 作成したパイプラインが表示されている

CI(本番環境)

CodeBuildの作成

  • CodeBuildでやりたいことは以下の通り
    • 開発環境のパイプラインで作成したHelmチャートをS3から取得し、対象のDockerイメージのタグを取得する
    • 本番環境用のHelmチャートに取得したDockerイメージのタグを上書きする
    • 編集したHelmチャートをS3へアップロード
    • Spinnakerのパイプライン(本番環境用)開始
  • CodeBuildの設定
    • 基本的には開発環境用に作成したCodeBuildと同じ
    • 対象のソースコードと、Helmチャートが格納されているリポジトリのmasterブランチを「Source」に指定する

Buildspec

  • コメントで概要を記載
buildspec.yaml
version: 0.2 

env: 
  variables: 
     REPOSITORY_NAME_INF: "test_inf" 
     BRANCH_NAME: "master" 
     CHART_NAME: "test" 
     ARTIFACT_PATH: "test" 
     URL : "deploy-test-prod" 

phases: 
  post_build: 
    commands: 
      # developのパイプラインで作成したHelmチャートをS3から取得し、対象のDockerイメージのタグを取得する
      - cd $CODEBUILD_SRC_DIR_dev 
      - tar -xvf $CHART_NAME.tgz 
      - CURRENT_COMMIT_ID=`yq -r '.image.tag' $CHART_NAME/values.yaml` 
      # 本番環境用のHelmチャートに取得したDockerイメージのタグを上書きする
      - cd $CODEBUILD_SRC_DIR_inf 
      - yq -y '.image.tag |=  "'$CURRENT_COMMIT_ID'"' $CHART_NAME/values.yaml  > $CHART_NAME/tmp.yaml  
      - mv $CHART_NAME/tmp.yaml  $CHART_NAME/values.yaml 
      # packageコマンドでtgz形式に変換
      - helm package $CHART_NAME 
      - mv $CHART_NAME*.tgz $CHART_NAME.tgz 
      # S3にアップロード
      - aws s3 cp $CHART_NAME*.tgz s3://[backet name]/$ARTIFACT_PATH/$CHART_NAME.tgz
      # Spinnakerのパイプラインを開始
      - curl https://XXXXXXXXXX.com/webhooks/webhook/$URL -X POST -d "{ }" -H "content-type:application/json" 

Sourceの設定

  • CodeBuildのSourceについてはCODEBUILD_SRC_DIRについてを参照
  • CodeBuildのSourceには以下を指定している
    • Source identifer:無
      • 該当リポジトリのmasterブランチ
    • Source identifer:dev
      • 開発環境用のパイプラインでアップロードしたHelmチャートが格納されているバケット
    • Source identifer:inf
      • Helmチャートが格納されているリポジトリのmasterブランチ

CodePipeline

  • 対象のCodeCommitのリポジトリのmasterブランチと対象のCodeBuildを指定して保存
  • masterブランチへのpush(merge)をトリガーにこのパイプラインか開始される

CD(本番環境)

  • 本番環境用のパイプラインをSpinnaker上で作成
  • 作成方法は開発環境用に作成したものと同様
  • 変更する点は以下
    • Triggerに設定するURL(CodeBuildeの最後に叩いているURLとあわせる)
    • Expected Artifactsに指定するパス
    • デプロイターゲットとなるKubernetesクラスタ

まとめ

  • AWS上のKubernetsクラスタにデプロイするためのパイプラインを作成
  • リポジトリのpushとCodeBuildeでのビルドはCodePipelineで管理し、KubernetesクラスタへのデプロイはSpinnakerで管理する方法を紹介
  • 改善点があれば随時更新予定!