Google Sheets から Terraform の実行
クラウドエースの北野です。
Google Sheets から Terraform を実行し、Google Cloud を管理する方法を紹介します。
要約
以下の順序で、タスクを実施することで、Google Sheets から Terraform を実施させます。
- Google Apps Script で Google Sheets のデータを Cloud Storage (GCS) へアップロード
- GCS の変更を Eventarc で検知し、Workflows の実行
- Workflows から Cloud Build トリガーを実行し、Google Cloud の変更
システム構成は、以下のようになります。
背景
構成管理ツールである Terraform を使い継続的デプロイメントによりシステムを運用管理すると、運用作業の再現性が高くなり運用作業において失敗が起きにくくなります。
そのため、システムの運用管理には構成管理ツールが欠かせなくなりつつあります。しかし、 Terraform などの構成管理ツールは学習コストが発生するため、全員の利用は難しいです。
システムの運用管理のすべてをツールに長けたチームができれば問題ないですが、管理者権限を必要とする作業はツールに長けていない作業者が実施する必要が多いかと思います。
そうすると、管理者権限を必要とする作業を大量に実施する場合、オペレーションミスが起きる可能性が高くなります。
そこで、もし、Google Sheets などのユーザーフレンドリーな UI を有しているツールから構成管理ツールを操作できれば、全員が構成管理ツールを使いシステムの運用管理を実現できるようになり、管理者権限での作業ミスの可能性が低くなります。
本記事では、 Google Apps Script (GAS) を使い Google Sheets から Terraform を操作し、 プロジェクトを作成する方法を説明します。以下では、 当該処理の実現方法と、実装方法について説明します。
システムの設計
GAS から Terraform を実行するシステムは、以下のようになります。
処理は以下のようになります。
- GAS で Google Sheets のデータを CSV 化し、そのデータを GCS にアップロード
- GCS の変更を監視する Eventarc がファイルのアップロードを検知し、Workflows に処理命令
- Workflows が Terraform を実行する Cloud Build トリガーの実行し、プロジェクトの作成
以下では、GAS での GCS へのファイルのアップロード方法、Eventarc で監視をし Workflows から Cloud Build トリガーを実行する方法を説明します。
本記事でGASから実行する Terraform コード
本記事では、以下の Google Cloud のプロジェクトを作成する Terraform コードを Google Sheets から実行します。
terraform {
backend "gcs" {
bucket = <GCSBUCKET>
}
required_providers {
google = {
source = "hashicorp/google"
}
google-beta = {
source = "hashicorp/google-beta"
}
}
}
locals {
projects = csvdecode(file("../configs/projects.csv"))
}
resource "google_project" "main" {
for_each = { for v in local.projects : v.project_id => v }
name = each.value.name
project_id = each.value.project_id
folder_id = each.value.folder_id
billing_account = each.value.billing_account
}
GCSBUCKET は、Terraform の State を管理するバケット名です。
当該 google_project
リソースの設定値である local.projects
は、CSV ファイルで管理しており、以下のようなデータとなっています。
No,name,project_id,folder_id,billing_account
1,sample,sample-pj,,
2,test,test-pj,,
本記事では、上記の projects.csv
を Google Sheets で管理し、GAS を使い Google Sheets から当該 Terraform コードを実行させます。
GAS で Google Sheets のデータを GCS にアップロードする方法
GAS で Google Sheets のデータを GCS にアップロードするには、以下をおこないます。
- GAS のコード作成
- Google Cloud で ウェブアプリケーションの OAuth クライアント ID の作成
- GAS に認証情報の登録
GAS のコード作成
Google Sheets の拡張機能から Apps Script を選択し、 Apps Script のスクリプトエディタを起動させます。
コードを記載する以下の赤枠にコードを記載し、保存します。
var SHEET_NAME = 'Project';
var GCS = PropertiesService.getScriptProperties().getProperty("GCS");
var FILE = 'pj_config.csv'
var CLIENTID = PropertiesService.getScriptProperties().getProperty("CLIENTID");
var CLIENTSECRET = PropertiesService.getScriptProperties().getProperty("CLIENTSECRET");
function convertToCSV(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
var values = sheet.getDataRange().getValues();
var csv = values.join("\r\n");
return csv;
}
function requestGCBJob(){
var csv = convertToCSV();
var content = Utilities.newBlob(csv)
if(storeContentsIntoGCS(content, GCS, FILE)){
Browser.msgBox('プロジェクト作成依頼を実行しました。 Cloud Buildの履歴を確認してください。');
}
}
function getService() {
return OAuth2.createService('provisioning')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId(CLIENTID)
.setClientSecret(CLIENTSECRET)
.setCallbackFunction('auth')
.setPropertyStore(PropertiesService.getScriptProperties())
.setScope('https://www.googleapis.com/auth/devstorage.full_control')
.setParam('login_hint', Session.getActiveUser().getEmail())
}
function auth(request) {
var service = getService();
var isAuthorized = service.handleCallback(request);
if (isAuthorized) {
Logger.log("認証成功");
return HtmlService.createHtmlOutput('<center>認証完了<br>タブを閉じてください。</center>');
} else {
Logger.log("認証失敗");
return HtmlService.createHtmlOutput('認証エラー<br>認証情報を確かめてください。');
}
}
function storeContentsIntoGCS(content, gcs, file) {
var service = getService();
if (!service.hasAccess()) {
var authorizationUrl = service.getAuthorizationUrl();
var template = HtmlService.createTemplate(
'<center><a href="<?= authorizationUrl ?>" target="_blank">認証実行</a></center>' +
'<br><br><center>Google Accountの認証が必要です。<br>認証後に再度タスクを実行ください。</center>'
);
template.authorizationUrl = authorizationUrl;
var page = template.evaluate();
SpreadsheetApp.getUi().showModalDialog(page, "Google API認証");
return false
} else {
var url='https://storage.googleapis.com/' + gcs + '/' + file;
UrlFetchApp.fetch(url,{
headers: {
Authorization: "Bearer " + service.getAccessToken(),
},
method: "PUT",
contentType: "application/javascript;charset=utf-8",
host: gcs + ".storage.googleapis.com",
payload: content
});
}
return true;
}
function onOpen(){
var ui = SpreadsheetApp.getUi();
ui.createMenu('タスク実行')
.addItem('プロジェクト作成', 'requestGCBJob')
.addToUi();
}
-
SHEET_NAME
: データを登録するシート名 -
GCS
: CSV ファイルをアップロードする GCS バケット名 -
FILE
: GCS にアップロードするときのファイル名 -
CLIENTID
: OAuth2 のクライアントID -
CLIENTSECRET
: OAuth2 のクライアントシークレット
上記パラメータのうち、 GCS
,CLIENTID
,CLIENTSECRET
は、スクリプトプロパティとなっています。これらの値は、スクリプトエディタのプロジェクトの設定から設定します。
画面下のスクリプトプロパティから設定します。
CLIENTID
と CLIENTSECRET
は Google Cloud から生成し入力します。 oauth2.provisioning
は、 OAuth2 のライブラリを追加すると自動的に入力されます。
しばらくすると、ヘルプの横に タスク実行
表示され、選択すると プロジェクト生成
のボタンが表示されます。
OAuth2 の CLIENTID と CLIENTSECRET の生成方法
OAuth2 の CLIENTID
と CLIENTSECRET
は、Google Cloud の API とサービスの認証情報から作成します。
認証情報を作成
のボタンを選択し、OAuth クライアント ID
を選択し、クライアント ID を生成します。
アプリケーションの種類では、ウェブアプリケーション
を選択し、承認済みのリダイレクト URI に GAS の URI を入力します。
以下の値を入力し、保存を選択します。
https://script.google.com/macros/d/<GAS_ID>/usercallback
- GAS_ID: GAS の ID で GAS のプロジェクト設定から確認できる値
保存して生成される クライアント ID
と クライアントシークレット
のそれぞれが、 CLIENTID
,CLIENTSECRET
となっています。
GAS の Oauth2 ライブラリの追加
本スクリプトは、Google Cloud の GCS にデータを CSV に変換し、アップロードします。
そのため、 Google Cloud への認証が必要となります。本コードは、 OAuth2 のライブラリを使い認証させファイルをアップロードするため、OAuth2 のライブラリを追加する必要があります。
スクリプトエディタのライブラリの追加ボタンを選択し、スクリプトIDに 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
を入力し、
検索ボタンを押し OAuth2 検索し、最新のバージョンを選び追加します。
Terraform コードを実行するための Google Cloud リソース
Terraform を実行するための以下の Google Cloud のリソースの作成方法を説明します。
- Cloud Build
- GCS
- Workflows
- Eventarc
Eventarc は、GCS と Workflows の情報を確認して作成されるので、最後に作成する必要があるので、作成の順序に気をつけてください。
また、Workflows の flows の設定には、 Cloud Build IDが必要となり、当該 ID もまた、トリガーの作成の後にしか確認できないので、ご注意ください。
Terraform を実行する Cloud Build の作成
Terraform コードの実行について、今回は Cloud Build を用いておこなう。
Cloud Build は、 terraform plan
を実行し変更内容を確認するトリガーと、 terraform apply
を実行し変更を実行する2つのトリガーを作成します。
terraform apply
を実行し変更するトリガーは、 Approve を有効にし、変更内容の確認の後に承認者が明示的に実行するようします。
今回は検証のため、手動実行するトリガーを作成するものとします。
そのため、連携した Cloud Build に連携した GitHub リポシトリに変更があっても、起動しません。
terraform plan
を実行する変更内容を確認するトリガーは、以下のコマンドで実行します。
gcloud alpha builds triggers create manual --name $TFPLANTRIGGER \
--project $PJ \
--region $LOCATION \
--branch main \
--repo $REPO \
--repo-type $TYPE \
--build-config $TFPLANFILE
当該トリガーのビルド構成ファイルの内容は、以下となっています。
steps:
- id: get pj config files
name: 'gcr.io/cloud-builders/gcloud'
entrypoint: /bin/sh
dir: src/org/project/configs
args:
- -c
- |
gcloud storage cp gs://${PROJECT_ID}-mng-pj-configs/pj_config.csv projects.csv
- id: terraform init
name: 'hashicorp/terraform'
entrypoint: /bin/sh
dir: src/org/project/create_project_terraform_codes
args:
- -c
- |
terraform init
- id: terraform plan
name: 'hashicorp/terraform'
entrypoint: /bin/sh
dir: src/org/project/create_project_terraform_codes
args:
- -c
- |
terraform plan
続いて、 terraform apply
を実行し、構成変更をおこなうトリガーは以下のコマンドで作成します。
gcloud alpha builds triggers create manual --name $TFAPPLYTRIGGER \
--project $PJ \
--region $LOCATION \
--branch main \
--repo $REPO \
--repo-type $TYPE \
--build-config $TFAPPLYFILE \
--require-approval
構成変更をおこなうトリガーのビルド構成ファイルの内容は、以下となっています。
steps:
- id: get pj config files
name: 'gcr.io/cloud-builders/gcloud'
entrypoint: /bin/sh
dir: src/org/project/configs
args:
- -c
- |
gcloud storage cp gs://${PROJECT_ID}-mng-pj-configs/pj_config.csv projects.csv
- id: terraform init
name: 'hashicorp/terraform'
entrypoint: /bin/sh
dir: src/org/project/create_project_terraform_codes
args:
- -c
- |
terraform init
- id: terraform apply
name: 'hashicorp/terraform'
entrypoint: /bin/sh
dir: src/org/project/create_project_terraform_codes
args:
- -c
- |
terraform apply --autoapprove
当該 Cloud Build トリガーは、 GCS バケットにアクセスするため、GCS バケットへの権限を付与させます。
権限の付与は以下でおこないます。
gcloud projects add-iam-policy-binding $PJ \
--member serviceAccount:$(gcloud beta services identity create --service cloudbuild.googleapis.com --format value'(email)') \
--role roles/storage.objectAdmin
また、今回、プロジェクトの作成をおこなうので、プロジェクトの作成権限を付与します。
gcloud organizations add-iam-policy-binding $ORGID\
--member serviceAccount:$(gcloud beta services identity create --service cloudbuild.googleapis.com --format value'(email)') \
--role roles/resourcemanager.projectCreator
GAS から送信される GCS の作成
GAS から送信される CSV ファイルを保存する GCS は、以下のコマンドで作成します。
gcloud storage buckets create --project $PJ gs://$GCS
また、 GCS のサービスエージェントは、変更を Pub/Sub に通知するの pubsub パブリッシャーの権限が必要となります。
以下のコマンドで権限を付与します。
gcloud projects add-iam-policy-binding $PJ \
--member serviceAccount:$(gcloud storage service-agent --project $PJ) \
--role roles/pubsub.publisher
Cloud Build トリガーを実行する Workflows の作成
Cloud Build トリガーは、 Workflows から発火させます。
今回、変更を実行するトリガーは Approve ボタンを押さないと、実行されないので、
Workflows からは両者のトリガーを並行に実行させます。
Workflows の作成は、 以下のコマンドで作成します。
gcloud workflows deploy $WF \
--project $PJ \
--location $LOCATION \
--source $FILE
ソースのファイルの内容は以下となります。
- init:
assign:
- project_id: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
- location: "asia-northeast1"
- branch: "main"
- plan_trigger: <Terraform Plan Cloud Build Trigger ID>
- apply_trigger: <Terraform Apply Cloud Build Trigger ID>
- invoke_builds:
parallel:
shared: [project_id,location,branch,plan_trigger,apply_trigger]
branches:
- terrafom_plan:
steps:
- plan_tr_cloudbuild_run:
call: googleapis.cloudbuild.v1.projects.locations.triggers.run
args:
name: ${"projects/" + project_id + "/locations/" + location + "/triggers/" + plan_trigger}
body:
projectId: ${project_id}
source:
branchName: ${branch}
triggerId: ${plan_trigger}
- terrafom_apply:
steps:
- apply_tr_cloudbuild_run:
call: googleapis.cloudbuild.v1.projects.locations.triggers.run
args:
name: ${"projects/" + project_id + "/locations/" + location + "/triggers/" + apply_trigger}
body:
projectId: ${project_id}
source:
branchName: ${branch}
triggerId: ${apply_trigger}
- the_end:
return: "SUCCESS"
上記のうち以下の内容は、 Cloud Build トリガーのIDです。
- <Terraform Plan Cloud Build Trigger ID>: 変更を確認する Cloud Build トリガーの ID
- <Terraform Apply Cloud Build Trigger ID>: 変更を実行する Cloud Build トリガーの ID
以上より先に作成した Cloud Build のトリガーIDを確認します。
トリガーIDの確認は、以下のコマンドでおこないます。
gcloud builds triggers list --region $LOCATION --project $PJ --format value'(name,id)'
Workflows を起動する Eventarc の作成
Workflows は、 Eventarc から起動させます。 Eventarc は、GCS の変更を監視し、 先に作成した Workflows を起動するように作成します。
Eventarc の作成では、サービスアカウントを指定する必要があるので、サービスアカウントを作成し、起動に必要な権限を付与してから、Workflows を作成します。
サービスアカウントには、 Workflows Invoker
と Eventarc Event Reciver
の権限を付与させます。
サービスアカウントの作成は、以下のコマンドでおこないます。
gcloud iam service-accounts create --project $PJ $SA
サービスアカウントへの権限付与は、以下のコマンドでおこないます。
gcloud projects add-iam-policy-binding $PJ \
--member serviceAccount:$SA@$PJ.iam.gserviceaccount.com \
--role roles/eventarc.eventReceiver
gcloud projects add-iam-policy-binding $PJ \
--member serviceAccount:$SA@$PJ.iam.gserviceaccount.com \
--role roles/workflows.invoker
Workflows は以下のコマンドで作成します。
gcloud eventarc triggers create $EVENTARC \
--project $PJ \
--location $LOCATION \
--destination-workflow $WF \
--destination-workflow-location $LOCATION \
--event-filters="type=google.cloud.storage.object.v1.finalized" \
--event-filters="bucket=$GCS" \
--service-account $SA@$PJ.iam.gserviceaccount.com
GAS の 実行方法
GAS の作成をおこない、 Google Cloud 側のリソースを作成すると、実行できるようになります。Google Sheets の タスク実行 の プロジェクト作成 ボタンを選択すると、認証画面が表示されるので、認証ボタンを押し認証させます。認証完了の後に再度、プロジェクト作成 ボタンを押すと、処理が成功するので、Cloud Build の履歴画面を確認ください。
しばらくすると、2つのタスクが実行されるので、実行中の履歴を確認し、問題がなければ、承認待ちのタスクを実行すると、プロジェクトの作成が実行されます。
まとめ
Google Sheets から Terraform を実行し、 Google Cloud のプロジェクトを作成する方法を説明しました。Google Sheets のようなユーザーフレンドリーな UI を使えば、誰でも Terraform を使えるようになるので組織全体で一貫した構成管理が可能となります。しかし、表形式のため配列などの表現はできないので、視認性が下がる点はあるので、すべてを当該方法で管理するのはおすすめしませんが、 Google Cloud のプロジェクトの作成や、 Google アカウントの作成など高権限を必要とするタスクは当該方法で実行するのもよいかもしれません。
最後に、当該 Google Cloud のリソースを作成する Terraform コードと Workflows の yaml ファイルを紹介して終わりにいたします。もし、気になった方は、活用ください。
variable "project" {}
locals {
region = "asia-northeast1"
roles = [
"roles/eventarc.eventReceiver",
"roles/workflows.invoker"
]
wf_config = yamldecode(file("../workflows/create_project.yaml"))
_init = {
init = {
assign = flatten(concat([for v in local.wf_config : try(v.init.assign, [])], [{
plan_trigger = google_cloudbuild_trigger.main["pj-terraform-plan"].trigger_id
}, {
apply_trigger = google_cloudbuild_trigger.main["pj-terraform-apply"].trigger_id
}]))
}
}
_wf_config = concat([local._init], [element(local.wf_config, 1)], [element(local.wf_config, 2)])
tr_config = {
repo = "https://github.com/REPONAME"
ref = "refs/heads/main"
type = "GITHUB"
}
triggers = [
{
name = "pj-terraform-plan"
file = "BUILDFILEPATH_TERRAFORMPLAN"
},
{
name = "pj-terraform-apply"
file = "BUILDFILEPATH_TERRAFORMAPPLY"
approval = true
}
]
}
resource "google_service_account" "main" {
account_id = "evt-wf-kick"
project = var.project
}
resource "google_project_iam_member" "main" {
for_each = toset(local.roles)
project = var.project
role = each.value
member = format("serviceAccount:%s", google_service_account.main.email)
}
resource "google_storage_bucket" "main" {
name = format("%s-mng-pj-configs", var.project)
location = local.region
force_destroy = true
project = var.project
storage_class = "STANDARD"
}
resource "google_eventarc_trigger" "main" {
depends_on = [
google_project_iam_member.main,
google_project_iam_member.gcs_sa
]
name = "pj-create-wf-kick"
project = var.project
location = local.region
matching_criteria {
attribute = "type"
value = "google.cloud.storage.object.v1.finalized"
}
matching_criteria {
attribute = "bucket"
value = format("%s-mng-pj-configs", var.project)
}
destination {
workflow = google_workflows_workflow.main.id
}
service_account = google_service_account.main.email
}
resource "google_workflows_workflow" "main" {
name = "invoke-pj-tf-plan-job"
project = var.project
region = local.region
source_contents = yamlencode(local._wf_config)
}
resource "google_cloudbuild_trigger" "main" {
for_each = { for v in local.triggers : v.name => v }
name = each.value.name
location = local.region
project = var.project
source_to_build {
uri = local.tr_config.repo
ref = local.tr_config.ref
repo_type = local.tr_config.type
}
git_file_source {
path = local.tr_config.file
uri = local.tr_config.repo
revision = local.tr_config.ref
repo_type = local.tr_config.type
}
dynamic "approval_config" {
for_each = try(each.value.approval, null) != null ? [each.value.name] : []
content {
approval_required = each.value.approval
}
}
}
data "google_storage_project_service_account" "main" {
project = var.project
}
resource "google_project_iam_member" "gcs_sa" {
project = var.project
role = "roles/pubsub.publisher"
member = format("serviceAccount:%s", data.google_storage_project_service_account.main.id)
}
- init:
assign:
- project_id: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
- location: "asia-northeast1"
- branch: "main"
- invoke_builds:
parallel:
shared: [project_id,location,branch,plan_trigger,apply_trigger]
branches:
- terrafom_plan:
steps:
- plan_tr_cloudbuild_run:
call: googleapis.cloudbuild.v1.projects.locations.triggers.run
args:
name: ${"projects/" + project_id + "/locations/" + location + "/triggers/" + plan_trigger}
body:
projectId: ${project_id}
source:
branchName: ${branch}
triggerId: ${plan_trigger}
- terrafom_apply:
steps:
- apply_tr_cloudbuild_run:
call: googleapis.cloudbuild.v1.projects.locations.triggers.run
args:
name: ${"projects/" + project_id + "/locations/" + location + "/triggers/" + apply_trigger}
body:
projectId: ${project_id}
source:
branchName: ${branch}
triggerId: ${apply_trigger}
- the_end:
return: "SUCCESS"
- REPONAME: GitHub のリポシトリ名
- BUILDFILEPATH_TERRAFORMPLAN: Terraform Plan を実行する Build 構成ファイルの格納先
- BUILDFILEPATH_TERRAFORMAPPLY: Terraform Apply を実行する Build 構成ファイルの格納先
Discussion