kakakakakku blog

Weekly Tech Blog: Keep Learning!

Terraform と SOPS (Secrets OPerationS) を組み合わせてシークレット値を扱おう

はじめに

SOPS (Secrets OPerationS) は YAML / JSON / ENV / INI などに記載されているシークレット値を暗号化するツールで,現在は CNCF Sandbox のプロジェクトになっている.暗号化のバックエンドとしては PGP / age / AWS KMS / Google Cloud KMS / Azure Key Vault / HashiCorp Vault など幅広くサポートしている.シークレット値が暗号化されるため GitHub でそのまま管理することもできる.

getsops.io

さらに terraform-provider-sops(carlpett/sops プロバイダー)を使うと Terraform と SOPS を組み合わせてシークレット値を扱える.ようするに SOPS で暗号化したファイルを Terraform 側で復号して安全にデプロイできる.また terraform-provider-sops は Terraform の Ephemeral resources(エフェメラルリソース)にも対応していてシークレット値を tfstate に保存しないようにもできる❗️

github.com

さっそく試していく \( 'ω')/

SOPS のドキュメントに以下のように書いてあって,今回は暗号化のバックエンドとして「age」を使う🔐

age is a simple, modern, and secure tool for encrypting files. It's recommended to use age over PGP, if possible.

github.com

環境

  • SOPS 3.13.1
  • age v1.3.1
  • Terraform v1.15.6
  • hashicorp/aws プロバイダー v6.50.0
  • carlpett/sops プロバイダー v1.4.1

ちなみに Terraform の Ephemeral resources(エフェメラルリソース)は Terraform v1.10 以降で使える.

developer.hashicorp.com

セットアップ

まずは SOPS と age をセットアップしておく.

$ brew install sops
$ sops --version
sops 3.13.1 (latest)

$ brew install age
$ age --version
v1.3.1

age で鍵を作る

まずは age-keygen コマンドで鍵を作る.すると公開鍵が出力される.さらに keys.txt には秘密鍵が出力される.誤ってコミットしないように keys.txt は別のディレクトリに退避しておくと良さそう.

$ age-keygen -o keys.txt
Public key: age1udwhv8n287suzrvuhgrqm2efcjfm446ax2uw2zn29d7umt5j539sq8wvyu

さらに環境変数 SOPS_AGE_KEY_FILEkeys.txt のパスを設定しておく.

$ export SOPS_AGE_KEY_FILE="~/sops/xxxxx/keys.txt"

👾 .sops.yaml

今度は .sops.yaml に age で作った公開鍵を指定する.

creation_rules:
  - age: age1udwhv8n287suzrvuhgrqm2efcjfm446ax2uw2zn29d7umt5j539sq8wvyu

👾 sandbox.enc.yaml

ここで sops コマンドの引数にシークレット値を含めるファイル名を指定して実行すると自動的にエディタが立ち上がる.

$ sops sandbox.enc.yaml

今回は sops コマンドにデフォルトで入っているサンプルのシークレット値をそのまま使う.

example_key: example_value
example_array:
    - example_value1
    - example_value2
example_number: 1234.56789
example_booleans:
    - true
    - false

エディタを保存すると sandbox.enc.yaml はシークレット値が ENC[AES256_GCM,data:...] というフォーマットで暗号化された状態になっている👌

example_key: ENC[AES256_GCM,data:3JoNQXeN9ArDANGsow==,iv:fXdh6tGbXwZLlW6eYHGFmeKMmck7fUxBZt2Cxbz2hfY=,tag:MdzfSPGLr+8JfV3kes+GYA==,type:str]
example_array:
    - ENC[AES256_GCM,data:eXicRcKAQZZlcfKovEA=,iv:XJQPlqB1sIU2LQao5v51QPJQJYs2FLLOkM416t1bwe4=,tag:5l+HCkVA/cHqRgWcNVJZHQ==,type:str]
    - ENC[AES256_GCM,data:hi1/Cq2RdUwqMgZy+cg=,iv:J1h2WaiDKlH0u7h5UohmjuEmsp8KXxFtWBH/r6yfkWM=,tag:7s8SW08w6p4invTem+Xe6A==,type:str]
example_number: ENC[AES256_GCM,data:ojoifALrZETniA==,iv:1KURyR0b7DGnnj7TdvBnl1FoRdq5sCkyipBz9P4Qpa0=,tag:H1w/Gs+y1o7fvq5BKh6Sig==,type:float]
example_booleans:
    - ENC[AES256_GCM,data:H48Eag==,iv:D3tycWiSPkH8zYUDu94wNVt5d/vbv3hdY3a/LE/GNeM=,tag:KJ/eeaItBuau4e+IXtk/vw==,type:bool]
    - ENC[AES256_GCM,data:tavixiM=,iv:L0CkLwKUJZfmDP7DewlBBmeMhEYHrNECGbSJ4thBnC4=,tag:dH47EaqUBLd5x/Cxbxq72Q==,type:bool]
sops:
    age:
        - enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKSXlsY1YvWXc4QkcvVDJ6
            eTE2TnF0Z1lTaUZuV2hNdGZkdWZxaDR0SGlFCnFrMWl1RWUwdHk4cGZ5ZTc0YU8x
            dStPWnJ4ZXBwM2N0S3JvSFRVRStza3MKLS0tIDlOem5GZGxma2xFazkzcWlvZUFT
            eE9FRW8vaml1UUFmOTNTRnJYcTBwTG8KkOzcfjQUawtEzubxwNPcIXzO28pULY89
            yv9CHMFLP/WWv2IfR3R0PLQkCjp7hqbbgQoP3Hb/PGVztcsJQlop5Q==
            -----END AGE ENCRYPTED FILE-----
          recipient: age1udwhv8n287suzrvuhgrqm2efcjfm446ax2uw2zn29d7umt5j539sq8wvyu
    lastmodified: "2026-06-16T16:15:46Z"
    mac: ENC[AES256_GCM,data:RN3oUZ6vR5ebkUhLC0S8km8oEQAGd6AysoG5OiQxk/dHE7ltEl1QOo8Ox9t0wcL6ogOoWRmwLVy3R95f+miJ/KISOPCP20sBXQywJgVlHJOeE8BHml5Empd17jI/z5lqOfi0bOrHT1ZKmx8jWBgYj7Jy1RrbSGh2KJXNb+a2WYo=,iv:Tcd4OAKiMHaUV98SnCl/bq1/jQ0ZhXlsX04cnQY+s3g=,tag:MAFLjewUiVW42AB+FstoHQ==,type:str]
    unencrypted_suffix: _unencrypted
    version: 3.13.1

sops decrypt コマンドでシークレット値を復号することもできる.

$ sops decrypt sandbox.enc.yaml
example_key: example_value
example_array:
    - example_value1
    - example_value2
example_number: 1234.56789
example_booleans:
    - true
    - false

Terraform と SOPS を組み合わせる

今回は AWS Systems Manager Parameter Store (SecureString) の値に SOPS 経由でシークレット値をデプロイしてみようと思う❗️

👾 terraform.tf

まずは terraform-provider-sops(carlpett/sops プロバイダー)を使えるようにしておく.

terraform {
  required_version = "~> 1.15.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.50.0"
    }
    sops = {
      source  = "carlpett/sops"
      version = "~> 1.4.1"
    }
  }
}

👾 ssm-data.tf(data source を使うパターン)

sops_file data source で sandbox.enc.yaml ファイルを参照しつつ,aws_ssm_parameter リソースの value にシークレット値(今回は example_key)を登録する.

data "sops_file" "secrets_data" {
  source_file = "sandbox.enc.yaml"
}

resource "aws_ssm_parameter" "sandbox_sops_data" {
  name  = "/sandbox-sops-data/example_key"
  type  = "SecureString"
  value = data.sops_file.secrets_data.data["example_key"]
}

terraform plan コマンドを実行すると value(sensitive value) になる.

$ terraform plan

(中略)

  # aws_ssm_parameter.sandbox_sops_data will be created
  + resource "aws_ssm_parameter" "sandbox_sops_data" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + has_value_wo   = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/sandbox-sops-data/example_key"
      + region         = "ap-northeast-1"
      + tags_all       = (known after apply)
      + tier           = (known after apply)
      + type           = "SecureString"
      + value          = (sensitive value)
      + value_wo       = (write-only attribute)
      + version        = (known after apply)
    }

terraform apply コマンドを実行すると期待通りに AWS Systems Manager Parameter Store をデプロイできるけど,tfstate(今回は S3 Backend)にはシークレット値が残ってしまう.

$ terraform show -json | jq -r '.values.root_module.resources[] | select(.address=="aws_ssm_parameter.sandbox_sops_data") | .values.value'
example_value

$ terraform show -json | jq '.values.root_module.resources[] | select(.address=="aws_ssm_parameter.sandbox_sops_data") | .values | {value, value_wo, value_wo_version}'
{
  "value": "example_value",
  "value_wo": null,
  "value_wo_version": null
}

👾 ssm-ephemeral.tf(Ephemeral resources を使うパターン)

今度は sops_file Ephemeral resources で sandbox.enc.yaml ファイルを参照しつつ,aws_ssm_parameter リソースの value_wo(書き込み専用)にシークレット値(今回は example_key)を登録する.

ephemeral "sops_file" "secrets_ephemeral" {
  source_file = "sandbox.enc.yaml"
}

resource "aws_ssm_parameter" "sandbox_sops_ephemeral" {
  name             = "/sandbox-sops-ephemeral/example_key"
  type             = "SecureString"
  value_wo         = ephemeral.sops_file.secrets_ephemeral.data["example_key"]
  value_wo_version = 1
}

terraform plan コマンドを実行すると value(sensitive value)value_wo(write-only attribute) になる.

$ terraform plan

(中略)

  # aws_ssm_parameter.sandbox_sops_ephemeral will be created
  + resource "aws_ssm_parameter" "sandbox_sops_ephemeral" {
      + arn              = (known after apply)
      + data_type        = (known after apply)
      + has_value_wo     = (known after apply)
      + id               = (known after apply)
      + insecure_value   = (known after apply)
      + key_id           = (known after apply)
      + name             = "/sandbox-sops-ephemeral/example_key"
      + region           = "ap-northeast-1"
      + tags_all         = (known after apply)
      + tier             = (known after apply)
      + type             = "SecureString"
      + value            = (sensitive value)
      + value_wo         = (write-only attribute)
      + value_wo_version = 1
      + version          = (known after apply)
    }

terraform apply コマンドを実行すると期待通りに AWS Systems Manager Parameter Store をデプロイできて,tfstate(今回は S3 Backend)にもシークレット値が残らなくなった.

$ terraform show -json | jq '.values.root_module.resources[] | select(.address=="aws_ssm_parameter.sandbox_sops_ephemeral") | .values | {value, value_wo, value_wo_version}'
{
  "value": "",
  "value_wo": null,
  "value_wo_version": 1
}

まとめ

Terraform と SOPS (Secrets OPerationS) を組み合わせてシークレット値を扱うパターンを試してみた❗️SOPS を使えばシークレット値が暗号化されるため GitHub でそのまま管理することもできるし,Ephemeral resources(エフェメラルリソース)を使えばシークレット値を tfstate に保存しないようにもできる.

Terraform を使うときにシークレット値の扱いは必ず話題に出るため SOPS を使うという選択肢も覚えておこう \( 'ω')/

Terraform の backend ブロックに環境別の設定を指定できる Partial Configuration

はじめに

Terraform で複数環境(たとえば stg 環境と prd 環境)にデプロイするときに backend 設定を切り替えたいことがある.しかしドキュメントに A backend block cannot refer to named values (like input variables, locals, or data source attributes). と書いてあって,Terraform では backend ブロックに変数が使えないという仕様がある(ちなみに OpenTofu だと Dynamic Backend Blocks はサポートされている).

github.com

Terraform には Partial Configuration という実装パターンがあって,backend ブロックの一部を空文字 "" で設定しておいて,terraform init コマンドを実行するときに -backend-config オプションで環境別の設定を指定することができる.今回は Terraform の Partial Configuration を AWS の S3 Backend で試してみた.

developer.hashicorp.com

環境

  • Terraform 1.15.6

ディレクトリ構成

今回は s3.tfbackendterraform.tfvarsenvironments ディレクトリで管理する.ちなみに Terraform のドキュメントだと *.backendname.tfbackend というファイル名が推奨されている📝

.
├── backend.tf
├── environments
│   ├── prd
│   │   ├── s3.tfbackend
│   │   └── terraform.tfvars
│   └── stg
│       ├── s3.tfbackend
│       └── terraform.tfvars
├── main.tf
├── providers.tf
├── terraform.tf
└── variables.tf

👾 backend.tf

backend.tf では regionuse_lockfile を固定で設定しつつ,bucketkey は空文字 "" で設定しておく.空文字にしておくところがポイントで Partial Configuration(部分的な設定)という感じ❗️

terraform {
  backend "s3" {
    region       = "ap-northeast-1"
    bucket       = ""
    key          = ""
    use_lockfile = true
  }
}

👾 environments/stg/s3.tfbackend と environments/prd/s3.tfbackend

そして environments/stg/s3.tfbackendenvironments/prd/s3.tfbackend には backend.tf で空文字にしておいた bucketkey の値を設定する.ちなみに今回 Amazon S3 バケット kakakakakku-tfstate-partial-configuration-stgkakakakakku-tfstate-partial-configuration-prd は作ってある前提とする.

bucket = "kakakakakku-tfstate-partial-configuration-stg"
key    = "terraform.tfstate"
bucket = "kakakakakku-tfstate-partial-configuration-prd"
key    = "terraform.tfstate"

👾 environments/stg/terraform.tfvars と environments/prd/terraform.tfvars

環境別の設定値は environments/stg/terraform.tfvarsenvironments/prd/terraform.tfvars に設定する.今回は環境名と Amazon SQS キューのメッセージ保持期間を設定しておくことにした.

env                       = "stg"
message_retention_seconds = 604800
env                       = "prd"
message_retention_seconds = 1209600

👾 main.tf

最後に main.tf で今回は Amazon SQS キューを1つデプロイする簡単なサンプルにしておいた😀

resource "aws_sqs_queue" "sandbox" {
  name                      = "sandbox-partial-configuration-${var.env}"
  receive_wait_time_seconds = 20
  message_retention_seconds = var.message_retention_seconds
}

動作確認: stg 環境

terraform init コマンドを実行するときに -backend-config=environments/stg/s3.tfbackend を指定すれば OK👌

$ terraform init -backend-config=environments/stg/s3.tfbackend
Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
(中略)
Terraform has been successfully initialized!

$ terraform plan -var-file=environments/stg/terraform.tfvars
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_sqs_queue.sandbox will be created
  + resource "aws_sqs_queue" "sandbox" {
      + arn                               = (known after apply)
      + content_based_deduplication       = false
      + deduplication_scope               = (known after apply)
      + delay_seconds                     = 0
      + fifo_queue                        = false
      + fifo_throughput_limit             = (known after apply)
      + id                                = (known after apply)
      + kms_data_key_reuse_period_seconds = (known after apply)
      + max_message_size                  = 262144
      + message_retention_seconds         = 604800
      + name                              = "sandbox-partial-configuration-stg"
      + name_prefix                       = (known after apply)
      + policy                            = (known after apply)
      + receive_wait_time_seconds         = 20
      + redrive_allow_policy              = (known after apply)
      + redrive_policy                    = (known after apply)
      + region                            = "ap-northeast-1"
      + sqs_managed_sse_enabled           = (known after apply)
      + tags_all                          = {
          + "Environment" = "stg"
          + "Project"     = "sandbox-terraform-aws-partial-configuration"
        }
      + url                               = (known after apply)
      + visibility_timeout_seconds        = 30
    }

Plan: 1 to add, 0 to change, 0 to destroy.

$ terraform apply -var-file=environments/stg/terraform.tfvars

動作確認: prd 環境

同じく terraform init コマンドを実行するときに -backend-config=environments/prd/s3.tfbackend を指定すれば OK👌

ちなみに Partial Configuration では .terraform ディレクトリを共有するため環境を変える場合は terraform init コマンドに -reconfigure オプションを付ける必要がある点に注意する.

$ terraform init -reconfigure -backend-config=environments/prd/s3.tfbackend
Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
(中略)
Terraform has been successfully initialized!

$ terraform plan  -var-file=environments/prd/terraform.tfvars
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_sqs_queue.sandbox will be created
  + resource "aws_sqs_queue" "sandbox" {
      + arn                               = (known after apply)
      + content_based_deduplication       = false
      + deduplication_scope               = (known after apply)
      + delay_seconds                     = 0
      + fifo_queue                        = false
      + fifo_throughput_limit             = (known after apply)
      + id                                = (known after apply)
      + kms_data_key_reuse_period_seconds = (known after apply)
      + max_message_size                  = 262144
      + message_retention_seconds         = 1209600
      + name                              = "sandbox-partial-configuration-prd"
      + name_prefix                       = (known after apply)
      + policy                            = (known after apply)
      + receive_wait_time_seconds         = 20
      + redrive_allow_policy              = (known after apply)
      + redrive_policy                    = (known after apply)
      + region                            = "ap-northeast-1"
      + sqs_managed_sse_enabled           = (known after apply)
      + tags_all                          = {
          + "Environment" = "prd"
          + "Project"     = "sandbox-terraform-aws-partial-configuration"
        }
      + url                               = (known after apply)
      + visibility_timeout_seconds        = 30
    }

Plan: 1 to add, 0 to change, 0 to destroy.

$ terraform apply -var-file=environments/prd/terraform.tfvars

まとめ

Terraform で複数環境にデプロイするときに使える Partial Configuration パターンを試してみた.Terraform では backend ブロックに変数が使えないため -backend-config オプションで環境別の設定を指定するイメージになる.環境差分が少ないときに使うと良さそう.逆に境差分が多いとリソース個別の count で条件分岐が増えすぎてしまいそうな気がする.

Partial Configuration 以外だと Google Cloud のドキュメントに載っている environments ディレクトリと modules ディレクトリを組み合わせるパターンもよく見るかな〜とは思う😀

docs.cloud.google.com

関連記事

kakakakakku.hatenablog.com

kakakakakku.hatenablog.com

ALB の HTTPS リスナーで Google Workspace ユーザーに限定した OIDC 認証を設定する

はじめに

Application Load Balancer (ALB) を使うアプリケーションを社内限定で公開したいときに「Google Workspace の OIDC 認証を組み合わせる」という選択肢がある.ALB の HTTPS リスナーは authenticate-oidc というアクションをサポートしていて,ALB と OIDC IdP を直接連携できる.今回は Terraform で試してみた❗️

docs.aws.amazon.com

準備

まず Google Workspace に紐付く Google Cloud プロジェクトで Google Auth Platform の設定をしておく.ポイントは対象ユーザーで「内部」を選択することで,そうすると Google Workspace 組織内のアカウントに限定して認証できるようになる.

<内部>

このモードでは、組織内の Google Workspace ユーザーのみがアプリを使用できます。 データの扱いについて、内部のユーザーと直接やりとりできます。

<外部>

アプリを使用できるのは、テストユーザーのリストに追加されたユーザーのみです。アプリを公開する準備ができたら、アプリの確認 が必要になる場合があります。

次に OAuth 2.0 クライアントを発行する.「アプリケーションの種類」ウェブアプリケーション にして,「承認済みのリダイレクト URI」には ALB の /oauth2/idpresponse エンドポイントを登録しておく.今回だと検証用のドメインを使うため https://sandbox.xxxxx.com/oauth2/idpresponse のような感じになる👌

Terraform を実装する

今回は Terraform で検証環境を実装する.

👾 terraform.tfvars

Google Cloud プロジェクトで発行した OAuth 2.0 クライアントから クライアント IDクライアントシークレット を取得して terraform.tfvars に設定する.

google_oauth_client_id = ""
google_oauth_client_secret = ""

👾 variables.tf

そして OAuth 2.0 クライアントを variable で受け取る.

variable "google_oauth_client_id" {
  type      = string
  sensitive = true
}

variable "google_oauth_client_secret" {
  type      = string
  sensitive = true
}

👾 sg.tf

ALB に設定するセキュリティグループを作る.今回は検証環境なので自宅 IP のみ許可している😀

resource "aws_security_group" "sandbox" {
  name   = "sandbox"
  vpc_id = "vpc-xxxxxxxx"
}

resource "aws_vpc_security_group_ingress_rule" "ingress_https" {
  security_group_id = aws_security_group.sandbox.id
  cidr_ipv4         = "xxx.xxx.xxx.xxx/32"
  from_port         = 443
  to_port           = 443
  ip_protocol       = "tcp"
}

resource "aws_vpc_security_group_egress_rule" "egress" {
  security_group_id = aws_security_group.sandbox.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

👾 alb.tf

ALB では aws_lb_listener リソースを使って HTTPS リスナーに authenticate_oidc を設定する.設定する Google 側のエンドポイントはドキュメントや .well-known/openid-configuration 参照で!

developers.google.com

ポイントは ALB リスナーの default_action を2段階にしているところ.order = 1authenticate-oidc アクションを実行して,認証できたら order = 2fixed-response を使って "Hello from ALB! Authentication successful." を返している.今回は簡易的に固定レスポンスを使っているけど,実際のアプリケーションであればターゲットグループに対して forward を設定することになる.

resource "aws_lb" "sandbox" {
  name               = "sandbox"
  load_balancer_type = "application"
  subnets            = ["subnet-xxxxxxxx", "subnet-xxxxxxxx"]
  security_groups    = [aws_security_group.sandbox.id]
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.sandbox.arn
  port              = 443
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate.sandbox.arn

  default_action {
    type  = "authenticate-oidc"
    order = 1

    authenticate_oidc {
      authorization_endpoint = "https://accounts.google.com/o/oauth2/v2/auth"
      token_endpoint         = "https://oauth2.googleapis.com/token"
      user_info_endpoint     = "https://openidconnect.googleapis.com/v1/userinfo"
      issuer                 = "https://accounts.google.com"
      client_id              = var.google_oauth_client_id
      client_secret          = var.google_oauth_client_secret
      session_timeout        = 300
    }
  }

  default_action {
    type  = "fixed-response"
    order = 2

    fixed_response {
      content_type = "text/plain"
      message_body = "Hello from ALB! Authentication successful."
      status_code  = "200"
    }
  }
}

👾 acm.tf

HTTPS リスナーを使うので AWS Certificate Manager で証明書を作っておく.

resource "aws_acm_certificate" "sandbox" {
  domain_name       = "sandbox.xxxxx.com"
  validation_method = "DNS"
}

resource "aws_acm_certificate_validation" "sandbox" {
  certificate_arn         = aws_acm_certificate.sandbox.arn
  validation_record_fqdns = [for record in aws_route53_record.acm_validation : record.fqdn]
}

👾 dns.tf

さらに検証用のドメイン https://sandbox.xxxxx.com でアクセスできるようにレコードも作っておく.

resource "aws_route53_record" "acm_validation" {
  for_each = {
    for dvo in aws_acm_certificate.sandbox.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  zone_id = "XXXXXXXXXXXXXXXXXXXX"
  name    = each.value.name
  type    = each.value.type
  records = [each.value.record]
  ttl     = 60
}

resource "aws_route53_record" "alb" {
  zone_id = "XXXXXXXXXXXXXXXXXXXX"
  name    = "sandbox.xxxxx.com"
  type    = "A"

  alias {
    name                   = aws_lb.sandbox.dns_name
    zone_id                = aws_lb.sandbox.zone_id
    evaluate_target_health = true
  }
}

動作確認

さっそく検証用のドメインにアクセスすると Google アカウントの認証画面にリダイレクトされた👌

まず個人の Google アカウントでログインすると アクセスをブロック: sandbox は組織内でのみ利用可能です というエラーになった.

そして Google Workspace の Google アカウントでログインすると期待通りに固定レスポンスの画面を表示できた \( 'ω')/

まとめ

Application Load Balancer (ALB) の HTTPS リスナーで authenticate-oidc アクションと Google Workspace の OAuth 2.0 クライアントを連携して Google 認証の仕組みを試してみた.比較的シンプルな構成で実現できるので,選択肢の1つとして覚えておこう.

研修で使える名札のテンプレートをダウンロードできる「折りたたみ名札メーカー」を公開した

はじめに

研修やワークショップなどのイベントを開催するときに A4 サイズの紙に名前を書いて三角に折ってもらうことがある.

硬めの厚紙だと半分に折れば立てることができるけど,普通の紙だとペラペラだから立てにくい.でも4等分して三角に折ればしっかり立つ❗️という Tips を昔に教えてもらって今でも使っている.

実際に折るとこんな感じ \( 'ω')/

折りたたみ名札メーカー

前からイイ感じにカスタマイズした名札のテンプレートを作りたいな〜と思っていて,最近「折りたたみ名札メーカー」というウェブサービスを公開してみた👏 個人的にはすごく便利でさっそく使っている!!!

name-tent-maker.kakakakakku.workers.dev

現状は「3種類」のテンプレートを用意している.他にもニーズがあったら追加しようかなと.

  • 名前
  • ニックネーム
  • 会社名 + 名前

さらにオプションで裏面に「アイスブレイク質問」を入れられるようにしていて,プリセットとカスタムから選べる.自己紹介やフリートークが苦手な人もいて(まさに僕😇)決まった質問があると話しやすかったりもするのでそういうときに使える.コミュニケーションが得な人なら名札なんてなくても全然大丈夫だと思うけども❗️

  • 好きな色は?
  • 最近ハマっていることは?
  • 出身はどこ?
  • 休日は何をして過ごしてる?
  • 行ってみたい場所は?
  • 好きな寿司ネタは?
  • 好きな AWS サービスは?
  • カスタム(30文字以内)

Nano Banana 2 で生成したイメージ画像

現状はテンプレートのみをダウンロードできる

普段から名前やニックネームは参加者に書いてもらっていて,現状ではテンプレートのみをダウンロードできるようにしている.もし CSV ファイルなどをアップロードすると自動的に参加者全員の名前がテンプレートに反映されるような仕組みにすると嬉しいんだけどな〜というフィードバックがあれば検討してみたいなと思う.

技術面

技術的には特別なことはしていないけど以下のような感じにした😀

  • Hono
  • Cloudflare Workers
  • TypeScript (JSX)
  • pdf-lib
  • Biome

PDF 生成は pdf-lib を使ってクライアント側で実行している.また今回は i18n で日本語と英語をサポートしていて,i18n ライブラリは使わずに messages.ts にオブジェクトとして定義して ?lang=en で切り替えられるようにした.

実装は Claude Code でイイ感じに作っていて,「実は皆さんに書いてもらった名札は Vibe Coding で実装したウェブサービスを使っていて〜」みたいな雑談にも使える✌

まとめ

「折りたたみ名札メーカー」というウェブサービスを公開してみた❗️

ぜひ使ってみてね〜 \( 'ω')/ 僕自身も使いながら改善してみようと思う.

name-tent-maker.kakakakakku.workers.dev

Google Cloud の Privileged Access Manager (PAM) で一時的な権限昇格を試す

はじめに

Google Cloud の Privileged Access Manager (PAM) を使うとプリンシパルに対する一時的な権限昇格を実現できる.たとえば通常は読み取り権限を付与しつつ,本番障害対応時に一時的な権限昇格を許可できたりする.Privileged Access Manager (PAM) を試してみる❗️

cloud.google.com

検証シナリオ

今回は検証用の Google Cloud Storage (GCS) バケットを作っておいて

  • 通常は「読み取り権限」
  • 一時的な権限昇格時は「書き込み権限」

を付与する.

Google Cloud Storage (GCS) バケットをデプロイする

まずは Terraform で Google Cloud Storage (GCS) バケットと README.md ファイルをデプロイしておく.

resource "google_storage_bucket" "assets" {
  name     = "xxxxx-sandbox-assets"
  location = "asia-northeast1"

  uniform_bucket_level_access = true
}

resource "google_storage_bucket_object" "readme" {
  name    = "README.md"
  bucket  = google_storage_bucket.assets.name
  content = "# xxxxx-sandbox-assets"
}

読み取り権限を付与する

そしてプリンシパルに roles/browser 権限と Google Cloud Storage (GCS) バケットに対する roles/storage.objectViewer 権限を付与しておく.

resource "google_project_iam_member" "browser" {
  project = "xxxxx"
  role    = "roles/browser"
  member  = "user:xxxxx"
}

resource "google_storage_bucket_iam_member" "object_viewer" {
  bucket = google_storage_bucket.assets.name
  role   = "roles/storage.objectViewer"
  member = "user:xxxxx"
}

Privileged Access Manager (PAM) の Service Agent に権限を付与する

さらにドキュメントを参考にして Privileged Access Manager (PAM) の Service Agent に roles/privilegedaccessmanager.serviceAgent 権限を付与しておく.

docs.cloud.google.com

resource "google_project_iam_member" "pam_service_agent" {
  project = "xxxxx"
  role    = "roles/privilegedaccessmanager.serviceAgent"
  member  = "serviceAccount:service-org-000000000000@gcp-sa-pam.iam.gserviceaccount.com"
}

Privileged Access Manager (PAM) をデプロイする

そして最後は google_privileged_access_manager_entitlement リソースを使って Privileged Access Manager (PAM) をデプロイする.一時的な権限昇格時は Google Cloud Storage (GCS) バケットに対する roles/storage.objectUser 権限を付与する.

今回は一時的に権限昇格できる時間として max_request_duration1800s(30分間) を設定した.最初は 600s(10分間) を設定したけど The requested duration should be between 30m0s and 168h0m0s. というエラーが出て,少なくとも 1800s(30分間) にする必要があった.

resource "google_privileged_access_manager_entitlement" "gcs_object_user" {
  parent         = "projects/xxxxx"
  location       = "global"
  entitlement_id = "gcs-object-user-jit"

  max_request_duration = "1800s"

  eligible_users {
    principals = ["user:xxxxx"]
  }

  privileged_access {
    gcp_iam_access {
      resource      = "//cloudresourcemanager.googleapis.com/projects/xxxxx"
      resource_type = "cloudresourcemanager.googleapis.com/Project"

      role_bindings {
        role                 = "roles/storage.objectUser"
        condition_expression = "resource.name.startsWith(\"projects/_/buckets/xxxxx-sandbox-assets\")"
      }
    }
  }

  requester_justification_config {
    unstructured {}
  }

  depends_on = [google_project_iam_member.pam_service_agent]
}

動作確認

Google Cloud の Cloud Shell 上でコマンドを実行して動作確認をする.

1. 通常時

読み取り権限を付与しているため gcloud storage ls コマンドは実行できるけど gcloud storage cp コマンドはエラーになった🛑

$ gcloud config set project xxxxx
Updated property [core/project].

$ gcloud storage ls gs://xxxxx-sandbox-assets/
gs://xxxxx-sandbox-assets/README.md

$ echo 'hello!' > hello.txt

$ gcloud storage cp hello.txt gs://xxxxx-sandbox-assets/
Copying file://hello.txt to gs://xxxxx-sandbox-assets/hello.txt
ERROR: Task 'gs://xxxxx-sandbox-assets/hello.txt' failed: GcsApiError('')
  Completed files 0/1 | 0B/7.0B

2. 一時的な権限昇格時

まずは gcloud pam grants create コマンドを使って一時的に権限昇格する.

$ gcloud pam grants create \
  --entitlement=gcs-object-user-jit \
  --requested-duration=1800s \
  --justification="Temporarily elevate to write to assets bucket" \
  --location=global \
  --project=xxxxx
Created [fd76848c-27bf-474b-89ee-877f03ebf32a].
createTime: '2026-06-05T01:24:11.402509877Z'
justification:
  unstructuredJustification: 'Temporarily elevate to write to assets bucket'
name: projects/xxxxx/locations/global/entitlements/gcs-object-user-jit/grants/fd76848c-27bf-474b-89ee-877f03ebf32a
privilegedAccess:
  gcpIamAccess:
    role: roles/storage.objectUser
requestedDuration: 1800s
requester: xxxxx
state: SCHEDULED
timeline:
  events:
  - eventTime: '2026-06-05T01:24:11.417076113Z'
    requested: {}
  - eventTime: '2026-06-05T01:24:11.417076113Z'
    scheduled:
      scheduledActivationTime: '2026-06-05T01:24:11.417076113Z'

すると gcloud storage cp コマンドで Google Cloud Storage (GCS) バケットにファイルをアップロードできた👌

$ gcloud storage cp hello.txt gs://xxxxx-sandbox-assets/
Copying file://hello.txt to gs://xxxxx-sandbox-assets/hello.txt
  Completed files 1/1 | 7.0B/7.0B

$ gcloud storage ls gs://xxxxx-sandbox-assets/
gs://xxxxx-sandbox-assets/README.md
gs://xxxxx-sandbox-assets/hello.txt

3. 通常時(一時的な権限昇格の失効)

一時的な権限昇格をしてから時間を置いて(30分間以上),改めて gcloud storage cp コマンドを実行すると最初と同じエラーになった🛑

$ gcloud storage cp hello.txt gs://xxxxx-sandbox-assets/
Copying file://hello.txt to gs://xxxxx-sandbox-assets/hello.txt
ERROR: Task 'gs://xxxxx-sandbox-assets/hello.txt' failed: GcsApiError('')
  Completed files 0/1 | 0B/7.0B

まとめ

Google Cloud の Privileged Access Manager (PAM) を Terraform でデプロイして試してみた.通常は必要最低限の権限に抑えつつ,いざ必要なときに一時的な権限昇格ができるのは便利だった❗️

あと pam-noreply@google.com というメールアドレスから一時的な権限昇格時と失効時に以下のタイトルでメールも送信されているようだった.覚えておこう.

You have been granted access to the resource projects/xxxxx
Your grant for the resource projects/xxxxx has ended