Open13

【Terraform】terraform-cloud functions

YuichiYuichi

https://www.youtube.com/watch?v=0sn9kPUPegs
https://zukkie.link/【aws】terraformでインフラリソースを管理/
https://zenn.dev/fuuukeee3/articles/e3d425fd71df215c3e0e
Homebrew
公式サイトでは Homebrew を利用したインストールが案内されている。

$ brew tap hashicorp/tap
$ brew install hashicorp/tap/terraform

tfenv
tfenv というバージョンマネージャを利用してインストールすることもできる。

$ brew install tfenv

$ tfenv --version
=> tfenvのバージョン番号出力

$ tfenv list-remote
=> インストール可能なterraformのバージョン番号が一覧表示

$ tfenv install バージョン番号
=> バージョン番号記載しなかった最新の安定版が入る

# どのバージョンのterraformを利用するか指定する
$ tfenv use バージョン番号

$ terraform --vesion
=> terraformのバージョン番号出力

ディレクトリ配下に置いた.terrafrom-versionにバージョン番号を書いておけば、tfenv でインストール済みのバージョンに切り替わる。

  • GCSを手動作成する
terraform {
    required_version = "1.4.6"
  backend "s3" {
    bucket = "1で作ったバケット名"
    key    = "terraform.state"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"
}
  • bucketを作成したストレージを指定、コードの例ではS3
YuichiYuichi

tfファイルは名前はなんでもいいらいしい

$ cd 'terraformファイルがあるフォルダ'

# terraformの初期化を行うコマンド
# 必要なproviderやmoduleのダウンロードが実行される。
# terraform使うよって宣言
# この段階では"terraform.state"はストレレージに作成されない。リソースを作成した段階で作成される
$ terraform init

# 構文チェックができる。間違っている箇所を指摘してくれる
$ terraform validate

# 実行計画を表示するコマンド。dry run
# 追加や変更は実際には実行されない。実際に何を消すか作るかを表示してくれる
# 未設定はデフォルト値になる。たまにクラウドもしくはterrformのデフォルト値があわることがあるので影響調査が必要
$ terraform plan

# main.tfファイルで定義した内容を反映するコマンド
$ terraform apply
resource "aws_sns_topic" "terraform_test" { # terrformで管理している名前
  name = "terraform-test" # クラウドで管理している名前
}
YuichiYuichi

https://zenn.dev/oyasumipants/articles/8f0ac1a3395520

gcloud config configurations list

  • gcloud config configurations listでどの configuration が有効になっているかを確認できます
$ gcloud config configurations list
NAME    IS_ACTIVE  ACCOUNT        PROJECT  COMPUTE_DEFAULT_ZONE  COMPUTE_DEFAULT_REGION
default True       xxx@gmail.com  xxx
xxx     False      yyy@gmail.com  yyy
yyy     False      xxx@gmail.com  xxx

gcloud config configurations activate [NAME]

gcloud config configurations activate [NAME]

指定したアカウント名の gcloud CLI 構成をアクティブにします。
今回はすでにアクティブ済み

gcloud config set

gcloud config set

当該の configuration の project フィールドが更新
今回はすでに設定済み

gcloud auth login

gcloud auth login

コマンドはユーザーアカウントを認証し、その結果を gcloud CLI の設定に保存します

gcloud auth application-default login

gcloud auth application-default login

gcloud auth application-default login コマンドは、アプリケーションがGoogleCloudAPIを使用するための認証情報を取得します。

YuichiYuichi

既存のリソースを読み込みます。
tags にzukkie-terraform-stateバケットの情報を定義することが出来ます。

data "aws_s3_bucket" "zukkie_terraform_state" {
  bucket = "zukkie-terraform-state"
}

resource "aws_sns_topic" "terraform_test2" {
  name = "terraform-test2"
  tags = data.aws_s3_bucket.zukkie_terraform_state
}
YuichiYuichi

module

  • terraformコードを分割する事が出来ます。
  • 開発環境と本番環境で設定値が違うリソースを作りたい時など、resourceを1つだけ書いて異なる設定値を変数で渡すだけで済むので視認性がよくなります。
    terraform import
  • 手動で作ったリソースをterraformで管理したい場合などに使います。
    試しに 手動で作ったtfstateファイルを保存しているS3をimportします。
YuichiYuichi
terraform destroy

terraform destroyコマンドというリソースを削除するコマンドがありますが
削除したいリソースがある時は対象のリソースをコメントアウトしてterraform applyをすればOK

理由は terraform は tfstateファイルとtfファイルを比較してtfファイルの方に合わせる様にリソースを作成・削除するためです。

YuichiYuichi

https://zenn.dev/zozotech/articles/scheduler-pubsub-cloud-function-terraform

のPub/Subなしバージョン

moduleの中のフォルダでコネクタを分けていく
saas-a/variables.tf に該当のテーブルリストを記載しておくと、テーブルリストごとにCloud FunctionsやCloud Schedulerなどのセット作成

.
├── README.md                              # プロジェクト全体の説明や使用方法
├── projects
│   └── SaaS-A
│       └── README.md                      # SaaS-A プロジェクトの詳細説明
└── tf
    ├── backup.tf                          # バックアップ設定用 Terraform ファイル
    ├── saas-a.tf                          # SaaS-A 固有のリソース定義
    ├── module
    │   └── saas-a
    │       ├── README.md                  # SaaS-A モジュールの説明
    │       ├── cloudfunctions.tf          # Cloud Functions リソース定義
    │       ├── http_body
    │       │   └── body.json              # HTTP リクエストボディのテンプレート(必要な場合)
    │       ├── iam.tf                     # IAM 設定用 Terraform ファイル
    │       ├── monitoring.tf              # モニタリング設定用 Terraform ファイル
    │       ├── scheduler.tf               # Cloud Scheduler 設定用 Terraform ファイル
    │       ├── service_account.tf         # サービスアカウント設定用 Terraform ファイル
    │       ├── source
    │       │   ├── conf
    │       │   │   ├── schema.json        # データスキーマ設定(必要な場合)
    │       │   ├── main.py                # Cloud Functions のメインコード
    │       │   ├── requirements.txt       # Python 依存関係リスト
    │       │   └── util
    │       │       ├── bq.py              # BigQuery 関連ユーティリティ
    │       │       ├── cmc_api_client.py  # CMC API クライアント
    │       │       └── log.py             # ロギングユーティリティ
    │       ├── storage.tf                 # Cloud Storage 設定用 Terraform ファイル
    │       └── variables.tf               # モジュール変数定義
    ├── provider.tf                        # Terraform プロバイダー設定
    ├── variables.tf                       # ルートモジュール変数定義
    └── workload_identity.tf               # Workload Identity 設定用 Terraform ファイル
YuichiYuichi

スモールで

事前に、"test-data-terraform-001"を作成しておく

.
├── backup.tf
├── module
│   └── saas-a
│       ├── cloudfunctions.tf
│       ├── source
│       │   ├── main.py
│       │   └── requirements.txt
│       ├── storage.tf
│       └── variables.tf
├── provider.tf
├── saas-a.tf
└── variables.tf
  1. backup.tf:
# ローカル変数の定義
# (この部分は元のコードには含まれていませんでしたが、通常はここにlocals{}ブロックが来ます)

# プロバイダーの設定
terraform {
  # 必要なプロバイダーの指定
  required_providers {
    google = {
      source  = "hashicorp/google"  # Google Cloud Platformのプロバイダー
      version = ">= 5.23.0"         # バージョン5.23.0以上を使用
    }
  }

  # Terraformのバージョン指定
  required_version = ">= 1.7.0"  # Terraform 1.7.0以上が必要

  # バックエンドの設定(状態ファイルの保存先)
  backend "gcs" {
    bucket = "test-data-terraform-001"  # Google Cloud Storageのバケット名
    prefix = "terraform/state"         # バケット内のプレフィックス(パス)
  }
}

  1. module/saas-a/cloudfunctions.tf:
resource "google_cloudfunctions_function" "function" {
  for_each    = toset(var.target_apis) # toset()関数は、入力をセットに変換します。これにより、各要素に対して1つのCloud Functionが作成されます。
  name        = each.key # each.keyはfor_eachループの現在の要素を参照
  # name        = "hello-world-00001"
  description = "My Cloud Function"
  runtime     = "python312"

  available_memory_mb   = 128   # メモリの割り当て(MB)
  timeout               = 540  # タイムアウト(秒)
  source_archive_bucket = google_storage_bucket.cf-source.name  # ソースコードが格納されているバケット
  source_archive_object = google_storage_bucket_object.archive.name  # ソースコードのアーカイブオブジェクト
  trigger_http          = true  # HTTPトリガーを有効にする
  entry_point           = "hello_world"

}

  1. module/saas-a/source/main.py:
# cloudfunctions/main.py
def hello_world(request):
    return "Hello, World!"
  1. module/saas-a/source/requirements.txt:
# 必要なPythonパッケージを列挙
requests
  1. module/saas-a/storage.tf:
# Cloud Functionsのソースファイル格納用バケット
resource "google_storage_bucket" "cf-source" {
  name     = "${var.project_id}-cf-saas-a"
  location = "asia-northeast1"
}

# Cloud Functionsのソースファイルをzip化
data "archive_file" "function_archive" {
  type        = "zip"
  source_dir  = "${path.module}/source"
  output_path = "${path.module}/source/cloud_function.zip"
}

# Cloud Functionsのソースファイルをバケットにアップロード
resource "google_storage_bucket_object" "archive" {
  name   = "functions/src_${data.archive_file.function_archive.output_md5}.zip"
  bucket = google_storage_bucket.cf-source.name
  source = data.archive_file.function_archive.output_path
}
  1. module/saas-a/variables.tf:
variable "project_id" {}

variable "target_apis" {
  type    = list(string)
  default = ["func-table-001"] # テーブルに応じて追加していく
}
  1. provider.tf:
# Google Cloud Platformのプロバイダー設定
provider "google" {
  project = var.project_id  # プロジェクトIDを変数から取得
  region  = "asia-northeast1"  # デフォルトのリージョンを東京に設定
}
  1. saas-a.tf:
# saas-a連携用のモジュールを呼び出す
module "saas-a" {
  # モジュールのソースパスを指定
  # 現在のディレクトリの下の"module/saas-a"ディレクトリにあるモジュールを使用
  source     = "./module/saas-a"

  # プロジェクトIDを変数から渡す
  # この変数は親モジュールで定義されていると想定
  project_id = var.project_id
}
  1. variables.tf (ルートモジュール):
# プロジェクトIDを定義する変数
variable "project_id" {
  type    = string  # 変数の型を文字列に指定
  default = "cookbiz-data"  # デフォルト値として "cookbiz-data" を設定
}
YuichiYuichi

scheduler.tf

resource "google_cloud_scheduler_job" "~~~~" {
      schedule         = "30 0 * * *"
      time_zone        = "Asia/Tokyo" # 今度からこれいれとこ、毎回cronの時間の脳内変換がめんどくさいので
      # HTTP呼び出しの設定
      http_target {
        http_method = "POST"
        uri         = google_cloudfunctions_function.transfer_porters_to_gcs[each.key].https_trigger_url
        # ボディーの設定
        body = base64encode(file("${path.module}/http_body/body.json")) # bodyなかったらエラーになった無知でした
        # リクエストヘッダーの設定
        headers = {
          "Content-Type" = "application/json"
        }
        # OIDCトークンを使用した認証
        oidc_token {
          service_account_email = google_service_account.data-transfer-porters.email
        }
      }
# /http_body/body.json
{

}
YuichiYuichi

iam.tf

# ローカル変数の定義
locals {
  # ローカルからCloud Functionsを呼び出すユーザを定義。
  invoke_users = toset([
    "~@~"
  ])
}

# Cloud Functions呼び出し権限(ユーザ)
resource "google_cloudfunctions_function_iam_member" "invoker" {
  # 各ユーザとAPIの組み合わせで権限を付与、一つのユーザーに必要な権限つけるfor文
  for_each = {
    for item in setproduct(
      [for invork_user in local.invoke_users : invork_user],
      [for target_api in var.target_apis : target_api]
    ) : join("-", item) => item
  }
~
YuichiYuichi

workload_identity.tf

https://blog.g-gen.co.jp/entry/using-terraform-via-github-actions#Workload-Identity-連携による認証

https://zenn.dev/cloud_ace/articles/7fe428ac4f25c8
https://techblog.recruit.co.jp/article-1125/

Workload Identity 連携とは
外部のワークロードに対して Google Cloud のサービスアカウントの権限を利用させることができる機能です。

サービスアカウントキーを利用する方法よりセキュアな認証方式となっておりますので、GitHub Actions や AWS をはじめ、他の外部サービスから Google Cloud を操作させたい場合に

Workload Identity 連携を設定する際の注意点について解説します。

Workload Identity プロバイダ設定時に属性条件を指定しない場合

Workload Identity プロバイダを設定する際に属性条件を指定しないと、任意の外部IDがそのプロバイダを通じてGoogle Cloudのサービスアカウントを利用できるリスクがあります。これは、サービスアカウントとプロバイダ名を知っているだけで、誰でもそのサービスアカウントの権限を利用できる状態を招く可能性があるためです。このため、特定の属性条件を設定し、特定のユーザーやリソースのみがアクセスできるように制限をかけることが推奨されます。

IAM 設定時に Workload Identity プール配下の外部ID全てに対して権限を付与する場合

Workload Identity プール配下のすべての外部IDに対して権限を付与すると、プール内の任意の外部IDがその権限を利用できる状態になります。これにより、意図しないユーザーやリソースがサービスアカウントの権限を行使できるリスクが生じます。特にGitHubのようなマルチテナントのIDプロバイダと連携する場合、特定の組織やリポジトリに限定して権限を付与することが重要です。

推奨される対策

  1. 属性条件の設定:Workload Identity プロバイダ設定時に属性条件を指定し、特定のユーザーやリソースのみがアクセスできるようにします。例えば、GitHubの特定の組織やリポジトリに限定することで、セキュリティを強化できます。
  2. IAM設定の細分化:IAM設定時に、Workload Identity プール配下のすべての外部IDに対して権限を付与するのではなく、特定の属性に基づいて権限を付与します。これにより、特定のリポジトリやチームに対してのみサービスアカウントの権限を付与することが可能になります。
  3. ベストプラクティスの遵守:Workload Identity プールとプロバイダを専用のプロジェクトで管理し、組織ポリシーの制約を使用して他のプロジェクトでのプロバイダの作成を無効にするなどのベストプラクティスを遵守します。

これらの対策を講じることで、Workload Identity 連携のセキュリティを強化し、不正なアクセスや権限の濫用を防ぐことができます。

YuichiYuichi

API データ連携(テンプレート:README.md)

仕様

処理概要

API を実行し、Storage へ連携する。

  1. [処理ステップを記述]

システム構成

  • Runtime: Python 3.12
  • Cloud Scheduler → Cloud Functions → Cloud Storage[追加の構成情報]

環境変数

変数名 設定値 備考
API_KEY API キー Secret Manager に登録された値を取得する。
TZ Asia/Tokyo

API キーとデータベースURLはSecret Managerへ手動で登録する。

input

起動時の HTTP パラメータ

キー 必須 備考
target_date データ取得基準日(default: 起動日前日) リカバリ実行時などに指定

output

各テーブルはTARGET_DATEでパーティション化されている(BigQuery 上では_PARTITIONTIMEパーティショニング列が設定されている)

{project}.{dataset}.{table}

カラム名 カラム型 モード 説明
user_id STRING REQUIRED ユーザー識別子
event_type STRING REQUIRED イベントタイプ
timestamp TIMESTAMP REQUIRED イベント発生時刻
metadata JSON NULLABLE イベント関連メタデータ

運用

  • ジョブの失敗時・過去データのリカバリ時には、Cloud Functions 起動用の HTTP パラメータにtarget_dateを指定して実行する
  • TARGET_DATEを同じ日付で繰り返し実行した場合、BigQuery 上のデータは重複することなく、パーティションごとに洗い替えられる

アラート

monitoring.tfで定義。ALERTレベルのログが出力された際にSlackへ通知する。
通知先: #{slack_channel}

ローカルからの Cloud Functions 呼び出し

roles/cloudfunctions.invokerを持つユーザーで実行する。
iam.tfにて権限を設定する。

日付指定実行

text
TARGET_API={function_name}

curl -X POST \
$(gcloud --project={project_id} functions describe ${TARGET_API} --region {region} --format="value(httpsTrigger.url)") \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"target_date": "2024-03-31"}'

全件取得

text
TARGET_API={function_name}

curl -X POST \
$(gcloud --project={project_id} functions describe ${TARGET_API} --region {region} --format="value(httpsTrigger.url)") \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"mode": "full_refresh"}'

参考

[参考リンクや文書]