NTTドコモR&Dの技術ブログです。

訳あってCI/CDをCloud BuildからGitHub Actionsに変えてみた

TL;DR

  • CI/CDパイプラインを、Cloud BuildベースからGitHub Actionsベースに移行してみた
  • プロジェクトの制約上、CI/CDパイプラインを継続的に作成する必要があり、リポジトリの手動接続が必要なCloud Buildは運用側の負荷が高かった
  • 移行のコストは考慮しつつも、Github ActionsでCI/CDの自動化に挑戦したことで、運用負荷を最小限に抑えることができるようになった
  • (付録)Cloud Deployやコンテナの脆弱性検出等をCI/CDパイプラインに組み込むことを検討したが、今回の構成には合致しなかった

自己紹介

NTTドコモ データプラットフォーム部(以下DP部)黒須です。

本記事は、「社員1000人以上が使う、Streamlit in Google Cloudのサーバレスプラットフォームを完全内製してみた」で言及しているプラットフォーム上で開発するアプリケーションの、CI/CD部分を取り上げて紹介させていただきます。

なお、検討・実装についてはNTTデータの支援メンバーである三浦さんに進めてもらっており、以降のセクションは三浦さんに執筆いただいています。

三浦さんはこの取り組みに参画した時点でGoogle Cloud初心者でした。元々同じチームにいたことから、プロジェクトの拡大に合わせて参画いただく運びとなりました。主な話題はCI/CDのモダナイゼーションではありますが、「初心者がどうやってこの結論にたどり着いたのか」という過程も含めて、お楽しみいただければ幸いです。

writer: NTTデータ デジタルサクセスコンサルティングユニット 三浦

モチベーション

CI/CDって何?

CI/CDとは、顧客に提供するアプリケーション・サービスの継続的なリリース・デプロイを実現する、ソースコードのパッケージ化・ビルド・テスト・デプロイといった一連のパイプラインを指します。

Google Cloudでは、Cloud BuildがCI/CDの中核を担っており、ソースコードの変更を検出して資材をビルドし、ビルドしたアーティファクトをArtifact Registryに登録し、Cloud BuildのビルドトリガーからCloud Run Serviceへ登録したアーティファクトをデプロイする、といったCI/CDの一連のプロセスをコントロールします。

と自分の理解を書いてみましたが、そもそもGoogleさんが分かりやすい図を書いてくれています。

引用元・参考:

元々のCI/CD

私が参画した時点で、本プラットフォームにおいてもCloud Buildを使ったCI/CDパイプラインを構築していました(下図)。

大まかな流れを説明すると、

1: 作業環境(Cloud Workstations)でアプリケーションのソースコードを作成し、GitHubリポジトリにpushする

2: Buildトリガーがブランチのpushを検知し、資材のビルドとArtifact Registryへのパッケージ化、Cloud Run Serviceへのデプロイを行う

3: リビジョンタグに記載されたタグから環境を判別し、開発環境と本番環境へのデプロイが行われる

というステップで進みます。本プロジェクトの独自の制約として、ポイントが2点あります。

  1. プロジェクト内に開発環境と本番環境が存在している
    • ベストプラクティスとしては、環境ごとにプロジェクトを分けるべきですが、諸般の事情にて1プロジェクト内での運用となっています
  2. プラットフォーム上で多数のアプリを管理しており、1アプリ=1Cloud Run Serviceの構成になっている
    • アプリごとの開発者が異なっているため、アプリごとにCI/CDパイプラインを発行する体制を作っています

このことから、Cloud Buildのトリガーを利用して、GitHubリポジトリのブランチにpushされると、対応する環境に向けてリリースされるよう、trafficタグを付け分けてデプロイする仕組みとなっていました。

改善点・モチベーション

このCI/CD自体、世間的に見ればモダナイズされているんじゃないか、と初心者の自分は思っていました。いざ自分がCI/CDパイプラインの発行・メンテナンスを担当するようになると、実務において負荷がかかる点に気づきます。それは、アプリが1つできる度に下記作業を手動で作成する必要がある、という点です。

  • GitHubリポジトリの作成
  • Cloud Build トリガーの作成
    • リポジトリとの接続操作を、手動で行う必要がありました
  • Artifact Registry リポジトリの作成

もちろん、これらの作業は疎通確認まで含めても10分程度しかかからず、さほど大きい負荷ではないように見えます。しかし、先ほどのポイントで述べたように、1アプリ=1Cloud Run Serviceとなっているため、アプリが増える度にこの対応が必要となります。

更に、手動対応が多いということは人的なミスも介在しやすくなります。これらの作業のミスによって起こりえる事象としては「ビルドされない(トリガーがリポジトリと紐づいていない)」「ビルドが失敗する(指定した資材の名称が間違っていて、存在しない資材をビルドしようとした)」といった比較的影響範囲の小さいものではありますが、原因究明にかかる稼働も馬鹿にはできません。

これらの負荷・リスクを考えて、現状のCI/CDをより人手の介さないフローにしよう! と思ったのがこの記事の発端になっています。

また、CI/CDの手動部分を削減する取り組みに合わせて

  • Cloud Deployを使ったCDのモダナイゼーション
  • Binary Authorizationを用いたセキュリティ面の強化

へチャレンジした内容も末尾に併記します。

CI/CDのモダナイゼーション

CI/CDパイプラインを再考するにあたって、アプリケーションのソースコード管理は既にGitHubで行っていることに着目しました。全てGoogle Cloud側に寄せることも1つの案ではありましたが、現状のパイプラインを大きく崩さず、手動作業の改善を考えたときに、GitHubの提供するCI/CDツールのGitHub Actionsを使うことが適していると判断しました。

最終的に実現したパイプラインは下図の通りとなります(CI/CDのセキュリティ面を担保するために、Binary Authorizationも追加しています。こちらは付録2にて説明しているため、ここでは割愛します)。

なにがうれしいのか?

  • CI/CDパイプラインにおける手動対応がほとんどなくなった

    リポジトリに対するpushをトリガーとして、各サービスの新規作成から連携までをGitHub Actionsのジョブ内で完結させたことで、リポジトリさえ作ってしまえば以降の手動対応が不要となるパイプラインが完成しました。

    name: Deploy App

    on:
      # branchesに記載されたブランチ名でpushすると、jobs以下のフローが走る
      # リポジトリが作成された時点でワークフローが走るため、以降の手動対応が不要になる
      push:
        branches:
          - main
          - dev

    env:
      # ここに事前に定義しておきたい値を入れる

    jobs:
      deploy:
        permissions:
          contents: 'read'
          id-token: 'write'

        runs-on: ubuntu-latest
        # パイプラインを定義
        steps:
          ...
          - name: Set up Cloud SDK
            uses: google-github-actions/setup-gcloud@v1
          ...
          - name: 'Docker Build'
            run: |-
              docker build ...
          ...
          - name: 'Cloud Run Deploy'
            run: |-
              gcloud run deploy ...
  • (課題解決とは別に、便利だったこと)細かい設定ができる

    cloudbuild.yamlでは、ブランチごとに個別の設定を行いたい場合、bash変換を駆使する必要がありました(下記一部抜粋)

    - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
      id: Deploy_cloud_run
      entrypoint: gcloud
      args:
        - 'run'
        - 'deploy'
        - ${SERVICE_NAME} 
        - '--image=${IMAGE_PATH}:$SHORT_SHA'
        - '--tag=${STAGE}'
    substitutions:
      SERVICE_NAME: XXX
      STAGE: ${BRANCH_NAME//main/prod}

また、Buildトリガーは、シンプルな正規表現のみ対応しています(if文が使えない)。 一方で、GitHub Actionsでは柔軟な設定ができます。

    env:
      SERVICE_NAME: XXX
      MAIN_TAG: (tag_name)

    jobs:
      steps:
      - name: 'Deploy cloud run in main'
            if: ${{github.ref_name == 'main'}}
            run: |-
              gcloud run deploy ${{ env.SERVICE_NAME }} \
              --image=${IMAGE_PATH}:${GITHUB_SHA::7} \
              --tag=MAIN_TAG

上記のような、ブランチによる分岐処理以外にも、Artifact Registryが作られていなければ新規作成する、といったリソースごとのステータスに依存する作業も自動化できます。 まだまだ初心者なので、もっとスマートな分岐方法もあるかもしれませんが、CLIベースで処理を書けるので、細やかな設定ができることに感動していました。

    - job:
    - step:    
      # 既にArtifact Registryのリポジトリが作られていないか確認
      - id: 'artifact-registry'
        name: Check Artifact Registry
        run: |-
          ARTIFACT=$(gcloud artifacts repositories list --location=${{ env.REGION }} --format=yaml --filter=name:projects/${{ env.PROJECT_ID }}/locations/${{ env.REGION }}/repositories/${{ steps.env-repository.outputs.IMAGE_REPOSITORY }})
          echo "ARTIFACT_LENGTH=${#ARTIFACT}" >> $GITHUB_OUTPUT
          
      # まだArtifact Registryのリポジトリが無い場合、新規に作成する
      - name: Create Artifact Registry
        if : steps.artifact-registry.outputs.ARTIFACT_LENGTH == 0
        run: |-
          gcloud artifacts repositories create ${{ steps.env-repository.outputs.IMAGE_REPOSITORY }} --repository-format=docker --location=${{ env.REGION }}

CI/CDパイプラインを変更したことでデメリットは無いのか?

まず考えられるデメリットとしては、CI/CDのベースがGitHubに移行したことで、セキュリティ面で新たな対応が必要なのでは?という観点です。

確かにこれまでは、強力な認証情報を持つサービスアカウントキーをGitHubに渡す必要があり、管理が必要なことも含めてセキュリティリスクとなっていました。

job:
  job_id:
    steps:
    - id: 'auth'
      uses: 'google-github-actions/auth@v1'
      with:
        # 事前に、secretsにサービスアカウントキーを入れる必要がある
        credentials_json: '${{ secrets.GCP_CREDENTIALS }}'

ここで登場するのが、Workload Identityです。

Workload Identityは、OIDC連携によって指定したGoogleサービスアカウントの権限を外部IDに付与することができます。今回の場合は、GitHub上の信頼された特定のGitHub Actionsに対して、サービスアカウントキーを発行することなくGoogle Cloudのリソースへのリクエストを実現できるようになっています。

(下記はWorkload IdentityをGitHub Actions内で利用する場合の一例となっています。特定のワークフローのみリクエストを有効化することもでき、その場合はWorkload Identity側の設定変更が必要となります)

jobs:
  job_id:
    ...
    steps:
    - id: 'auth'
      uses: 'google-github-actions/auth@v1'
      with:
        workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
        service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
        ...

引用元・参考:

あとがき

CI/CDパイプラインの構築は1回作れば以降の開発サイクルは自動化されるため、普通に使っている分にはわざわざGitHub Actionsを頑張らなくてもCloud Buildで十分だと思います。

一方で、今回のように次々とCI/CDパイプラインを作らないといけない環境においては、自由度の高いGitHub Actionsを使う方が運用する側の負荷を抑えることができるという結果になりました。

Workload Identityによって、Googleのサービス外のツールを利用する際のハードルが下がったことで、GitHub Actionsのワークフロー作成に集中できたことも良かったです。

また、末尾には今回のCI/CDパイプラインの構築に合わせて検討したことも記載しています。実際には諦めてしまったものもありますが、何故諦めてしまったのかも含めて、何かの参考になれば幸いです。

付録1: Cloud Deployを使ったCDのモダナイゼーション

Google Cloudには、CDに特化したサービスである、Cloud Deployがあります。

Cloud Run Serviceの手前に位置するこのサービスは、準備した資材を各環境へスムーズにデプロイ・ロールバックすることに特化しており、昨今のCI・CDそれぞれを分けて考えて実装する考えに基づくと、こちらの方がよりモダンなのではないか、と思って興味本位で検討を始めました。

結論から言うと、今回のような構成(大規模ではなく、厳密な成果物管理が要求されないケース)において、Cloud Deployを適用することは向いていなかったです。特に致命的だったのは、デプロイ先をtrafficタグで識別させる仕組みで、Cloud Run Serviceでは仕様上実現できるが、Cloud Deployの様に環境がプロジェクト単位で分かれていることを想定している場合、デプロイの度にtrafficタグが消えてしまいます

具体的には下図のように、dev環境にデプロイするとprod環境が見えなくなるため、開発している間はユーザがアプリを利用できない事態を招いてしまいます。したがって、今回のチャレンジでは、Cloud Deployを導入しないという結論に至りました。

引用元・参考:

付録2: Binary Authorizationを用いたセキュリティ面の強化

元々の構成では、外部から任意のCI/CDパイプラインを実行できてしまう懸念があります。脆弱性の高いイメージや、悪意のあるスクリプトを含んだイメージが適用されてしまう恐れがあったため、今回のモダナイゼーションに合わせてCI/CDのセキュリティ面の強化についても並行して取り組んでいました。

信頼できるアーティファクトだけをデプロイするための仕組みとして、Google CloudにはBinary Authorizationというサービスがあります。デプロイする前に、イメージに特定の署名がされていなければ、デプロイを防ぐという機能を持っており、これをGitHub Actionsを使ったCI/CDパイプラインに適用していきます。

Binary Authorizationには、ポリシーに従った署名だけでなく、コンテナの脆弱性の結果が示された証明書を用いた署名方法もあります。CI/CDパイプラインに流れるイメージの品質担保のためにも、GitHub Actions内で実現できないか検討していました。

コンテナの脆弱性の結果を用いる署名方法は主にCloud Buildを使ったCI/CDで提供されていますが、Kritis、Voucherといったサードパーティツールを使った証明書を用いて、署名する方法が提示されていたため、こちらをGitHub Actions内に組み込む形を検討しました。

結論として、両サードパーティツールのメンテナンスが十分ではなかったことを理由に、コンテナの脆弱性検証をCI/CDパイプラインに組み込む試みは見送った形となりました。

現時点では、ポリシーに従った署名だけを導入し、コンテナの脆弱性に関するフローを組み込むかの検討は継続中です。

ポリシーに従った署名を導入することで、CI/CDパイプラインを通過したイメージのみデプロイされるようになったため、以前に比べてCI/CDパイプラインのセキュリティ面は向上したと言えます。

引用元・参考: