Secret Manager を使用したセキュアな CI/CD パイプライン構築
はじめに
こんにちは、クラウドエースの岸本です。
今回は、Secret Manager を使用して Cloud Build でセキュアなデプロイを行う手順を紹介します。
CI/CD パイプラインを構築し Cloud Functions にデプロイする前回の記事からの続編となります。
サービスのデプロイ時における Secret Manager の使用方法は変わらないため、前回の記事を読んでいない方も参考にしていただけます。
前回の記事はこちらです。
構成図
構成図は以下の通りです。
今回使用する主要なサービスは以下の通りです。
Cloud Build
Cloud Build は、Google Cloud のマネージド型ビルドサービスで、コードのビルド、テスト、デプロイを自動化できます。
Cloud Build から様々なプロダクトを対象としてデプロイすることができますが、今回のケースでは、Cloud Functions を対象としてデプロイします。
Cloud Build のリポジトリは、第 1 世代と第 2 世代があり、今回は第 2 世代を使用します。
世代の違いは、弊社のテックブログまたは、公式ドキュメントを参照してください。
Secret Manager
Secret Manager は、Google Cloud のマネージド型のシークレット管理サービスで、データベースパスワード、API キーなどの機密情報を安全に管理できます。
今回は、Cloud Functions の環境変数としてシークレットを設定します。
上記の設定を行うことでソースコードにシークレット情報を直接記述することなく、セキュアなデプロイを行うことができます。
詳しい機能については公式ドキュメントを参照してください。
構築手順
構築手順は以下の通りです。
- Secret Manager にシークレットを作成
- Secret Manager からシークレットを取得するコードを追加
- サービスアカウントにロール付与
- ブラウザでシークレットの値を確認
1. Secret Manager にシークレットを作成
Cloud Functions にデプロイする際に使用するシークレットを Secret Manager に作成します。
以降の手順で実行するコマンドで繰り返し参照する変数を環境変数として設定します。
Cloud Shell で以下のコマンドを実行します。
export _PROJECT_ID='Project ID'
export _REGION='us-central1'
次に、Secret Manager にシークレットを作成するため、以下のコマンドを実行します。
// シークレットの作成
printf "This is Super Secret" | gcloud secrets create my-first-secret \
--data-file=- \
--project=${_PROJECT_ID}
// シークレットの一覧表示
gcloud secrets list --project=${_PROJECT_ID}
// 出力例
NAME: my-first-secret
CREATED: 2024-07-28T11:37:20
REPLICATION_POLICY: automatic
LOCATIONS: -
// シークレットの値を取得
gcloud secrets versions access latest --secret=my-first-secret --project=${_PROJECT_ID}
// 出力例
This is Super Secret
-
printf "This is Super Secret"
- 改行を行末につけないため
printf
を使用しています。これはシークレットの値を文字列で標準出力しています。
- 改行を行末につけないため
-
my-first-secret
- シークレットの名前を指定しています。
-
data-file=-
- シークレットの値を標準入力から受け取ることを示しています。
その他のオプションについては、公式ドキュメントを参照してください。
これでシークレットの作成が完了しました。
次に、Cloud Build がビルドを実行する際に Secret Manager からシークレットを取得するための設定を行います。
2. Secret Manager からシークレットを取得するコードを追加
前回の記事で作成したソースコードのディレクトリ構成は以下の通りです。
.
├── cloudbuild.yaml
└── src
├── main.py
└── requirements.txt
Secret Manager からシークレットを取得するために、cloudbuild.yaml
を編集します。
また、取得したシークレットを表示するために、main.py
も編集します。
修正後の cloudbuild.yaml
steps:
- id: "CICD"
name: "gcr.io/google.com/cloudsdktool/cloud-sdk"
entrypoint: bash
args:
- -c
- |
gcloud beta functions deploy ${_FUNCTIONS_NAME} \
--region=${_REGION} \
--source=src/ \
--runtime=${_RUN_TIME} \
--trigger-http \
--project=${PROJECT_ID} \
--service-account=${_SERVICE_ACCOUNT_FUNCTIONS} \
--build-service-account=projects/${PROJECT_ID}/serviceAccounts/${_SERVICE_ACCOUNT_TRIGGER} \
--allow-unauthenticated \
--set-env-vars SECRET="$${MYSECRET}"
secretEnv:
- MYSECRET
availableSecrets:
secretManager:
- versionName: projects/${PROJECT_ID}/secrets/my-first-secret/versions/1
env: "MYSECRET"
substitutions:
_FUNCTIONS_NAME: "functions_cicd"
_REGION: "us-central1"
_SERVICE_ACCOUNT_FUNCTIONS: "sa-functions@${PROJECT_ID}.iam.gserviceaccount.com"
_SERVICE_ACCOUNT_TRIGGER: "sa-trigger@${PROJECT_ID}.iam.gserviceaccount.com"
_RUN_TIME: "python312"
logsBucket: "UNIQUE_ID"
serviceAccount: "projects/${PROJECT_ID}/serviceAccounts/sa-build@${PROJECT_ID}.iam.gserviceaccount.com"
options:
logging: GCS_ONLY
cloudbuild.yaml の編集内容は以下の通りです。
-
--set-env-vars=SECRET=$${my-first-secret}
- シークレットを環境変数として Cloud Functions に渡します。
- args フィールドにシークレットを指定する場合は、
$$
を接頭辞とする環境変数を使用して指定する必要があります。詳しくは公式ドキュメントを参照してください。
-
secretEnv
- シークレットの値を Cloud Build 実行環境に追加し、スクリプトまたはプロセスから環境変数を介してこの値にアクセスするために使用します。
-
availableSecrets
- シークレットに使用するシークレットバージョンと環境変数を指定できるようにします。
-
versionName
- シークレットのバージョンを指定します。先ほど作成した、
my-first-secret
のバージョンを指定します。
- シークレットのバージョンを指定します。先ほど作成した、
-
env
- シークレットを環境変数として設定します。
修正後の main.py
import functions_framework
import os
@functions_framework.http
def functions_cicd(request):
"""HTTP Cloud Function.
Args:
request (flask.Request): The request object.
<https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
Returns:
The response text, or any set of values that can be turned into a
Response object using `make_response`
<https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
"""
request_json = request.get_json(silent=True)
request_args = request.args
secret_value = os.getenv('SECRET')
if request_json and 'name' in request_json:
name = request_json['name']
elif request_args and 'name' in request_args:
name = request_args['name']
else:
name = 'World'
return 'Hello {}! Secret value: {}'.format(name, secret_value)
main.py の編集内容は以下の通りです。
-
import os
- 環境変数を取得するために os モジュールをインポートします。
-
secret_value = os.getenv('SECRET')
- 環境変数からシークレットの値を取得し、
secret_value
に代入します。
- 環境変数からシークレットの値を取得し、
-
return 'Hello {}! Secret value: {}'.format(name, secret_value)
- シークレットの値をレスポンスに追加します。今回は、
Hello World! Secret value: This is Super Secret
というレスポンスが返されます。
- シークレットの値をレスポンスに追加します。今回は、
requirements.txt(前回の記事と同じです)
functions-framework==3.*
以上で、Secret Manager からシークレットを取得するためのファイル編集が完了しました。
次に、サービスアカウントにロールを付与します。
3. サービスアカウントにロール付与
Secret Manager からシークレットを取得するために Secret Manager のシークレットアクセサー(roles/secretmanager.secretAccessor)ロールが必要です。
付与するサービスアカウントは Cloud Build 用サービスアカウントです。
構成図で言うと以下のサービスアカウントになります。
以下のコマンドを実行して、サービスアカウントに特定のシークレットへのアクセス権限を付与します。
今回の場合は、Cloud Build 用サービスアカウントに対して my-first-secret
へのアクセス権限を付与します。
gcloud secrets add-iam-policy-binding my-first-secret \
--member="user:serviceAccount:sa-build@${_PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
4. ブラウザでシークレットの値を確認
最後に、Cloud Functions が正常に動作し Secret Manager からシークレットを取得できるか確認します。
GitHub リポジトリにソースコードをプッシュし、CI/CD パイプラインを実行します。
git add .
git commit -m "check secret manager value"
git push origin main
CI/CD パイプラインが正常に実行されると、Cloud Functions がデプロイされます。
デプロイが完了したら、Cloud Functions が発行する URL にアクセスして、レスポンスを確認します。
まとめ
以上で、Secret Manager を使用して Cloud Build でセキュアなデプロイを行う手順を紹介しました。
Secret Manager を使用することで、ソースコードにシークレット情報を直接記述することなく、セキュアなデプロイを行うことができます。
また、今回ソースコードに設定した内容は Cloud Functions に限らず使用することができます。
CI/CD パイプラインでシークレット情報を扱う際には、Secret Manager の利用を検討してみてはいかがでしょうか。
最後までお読みいただき、ありがとうございました。
Discussion