GCP の Workload Identity Federation を Terraform で設定してサービスアカウントのキーを作成せずに AWS から API を呼ぶ
awsgcpgolangterraformGCP の Workload Identity Federation は GCP 外のワークロードにサービスアカウントのキーを共有することなく一時的な権限を与えられる機能。
なお、GCP から AWS は サービスアカウントの OIDC トークンによる AssumeRoleWithWebIdentity が行える。これによりインスタンスからメタデータサーバーにリクエストしてトークンを取得し AWS の API を呼んだり、Cloud Storage Transfer Service など一部のマネージドサービスは Role の Arn を渡すだけで AWS にアクセスできるようになっている。
/*
new iam.Role(this, 'GoogleWebIdentityRole', {
assumedBy: new iam.WebIdentityPrincipal("accounts.google.com", {
"StringEquals": {
"accounts.google.com:oaud": "sts.amazonaws.com",
"accounts.google.com:sub": "(service_account_id)"
}
}),
roleName: "*****",
})
*/
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sts"
"cloud.google.com/go/compute/metadata"
)
token, err := metadata.Get("instance/service-accounts/default/identity?audience=sts.amazonaws.com&format=full")
if err != nil {
return fmt.Errorf("failed to get identity token: %v", err)
}
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"))
if err != nil {
return fmt.Errorf("Failed to load AWS config: %v", err)
}
stsClient := sts.NewFromConfig(cfg)
assumeRoleInput := &sts.AssumeRoleWithWebIdentityInput{
RoleArn: aws.String(roleArn),
RoleSessionName: aws.String("gcp-assume-role-session"),
WebIdentityToken: aws.String(token),
DurationSeconds: aws.Int32(3600),
}
assumeRoleOutput, err := stsClient.AssumeRoleWithWebIdentity(ctx, assumeRoleInput)
if err != nil {
return fmt.Errorf("Failed to assume role: %v", err)
}
tempCfg := cfg.Copy()
tempCfg.Credentials = credentials.NewStaticCredentialsProvider(
*assumeRoleOutput.Credentials.AccessKeyId,
*assumeRoleOutput.Credentials.SecretAccessKey,
*assumeRoleOutput.Credentials.SessionToken,
)
tempStsClient := sts.NewFromConfig(tempCfg)
identity, err := tempStsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
if err != nil {
return fmt.Errorf("Failed to get caller identity: %v", err)
}
fmt.Println("success!")
Workload Identity Federation も OIDC や SAML に対応しているほか、署名付きの GetCallerIdentity リクエストによる認証を行うこともできる。
権限を与える AWS の Role を作成する。
new iam.Role(this, 'WorkloadIdentityRole', {
assumedBy: new iam.CompositePrincipal(
new iam.AccountPrincipal(this.account),
})),
roleName: "*****",
})
/*
eval $(aws sts assume-role \
--role-arn "arn:aws:iam::*****:role/*****" \
--role-session-name "GoogleWebIdentityTest" \
--duration-seconds 3600 \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text | \
awk '{printf "export AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s\n", $1, $2, $3}')
*/
google_iam_workload_identity_pool を作成し AWS 用の google_iam_workload_identity_pool_provider を紐付け、google_project_iam_member で attribute.aws_role が上の Role の Arn の場合にサービスアカウントの権限を与えるようにする。
provider "google" {
project = var.project_id
region = "asia-northeast1"
}
provider "google-beta" {
project = var.project_id
region = "asia-northeast1"
}
variable "project_id" {
type = string
}
data "google_project" "project" {
project_id = var.project_id
}
variable "aws_account_id" {
type = string
}
variable "aws_role_name" {
type = string
}
resource "google_iam_workload_identity_pool" "aws_pool" {
workload_identity_pool_id = "aws-pool"
display_name = "AWS Workload Identity Pool"
}
resource "google_iam_workload_identity_pool_provider" "aws_provider" {
workload_identity_pool_id = google_iam_workload_identity_pool.aws_pool.workload_identity_pool_id
workload_identity_pool_provider_id = "aws-provider"
display_name = "AWS Provider"
aws {
account_id = var.aws_account_id
}
attribute_mapping = {
"google.subject" = "assertion.arn"
"attribute.aws_role" = "assertion.arn.contains('assumed-role') ? 'arn:aws:iam::${var.aws_account_id}:role/' + assertion.arn.extract('assumed-role/{role_name}/') : assertion.arn"
}
}
resource "google_service_account" "gcs_service_account" {
account_id = "aws-gcs-service-account"
display_name = "AWS GCS Service Account"
}
resource "google_project_iam_member" "gcs_storage_admin" {
project = var.project_id
role = "roles/storage.admin"
member = "serviceAccount:${google_service_account.gcs_service_account.email}"
}
resource "google_service_account_iam_member" "workload_identity_user" {
service_account_id = google_service_account.gcs_service_account.name
role = "roles/iam.workloadIdentityUser"
member = "principalSet://iam.googleapis.com/projects/${data.google_project.project.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.aws_pool.workload_identity_pool_id}/attribute.aws_role/arn:aws:iam::${var.aws_account_id}:role/${var.aws_role_name}"
}
Provider名を x-goog-cloud-target-resource ヘッダーに入れて署名した GetCallerIdentity リクエストを subjectToken として Google の token API に送る設定ファイルを出力し、パスを環境変数 GOOGLE_APPLICATION_CREDENTIALS に入れる。
$ gcloud iam workload-identity-pools create-cred-config \
projects/*****/locations/global/workloadIdentityPools/aws-pool/providers/aws-provider \
--service-account=aws-gcs-service-account@*****.iam.gserviceaccount.com \
--aws \
--enable-imdsv2 \
--output-file=aws-cred.json
$ cat aws-cred.json
{
"universe_domain": "googleapis.com",
"type": "external_account",
"audience": "//iam.googleapis.com/projects/*****/locations/global/workloadIdentityPools/aws-pool/providers/aws-provider",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
},
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/aws-gcs-service-account@*****.iam.gserviceaccount.com:generateAccessToken"
}
$ export GOOGLE_APPLICATION_CREDENTIALS="./aws-cred.json"
あとはこのファイルを参照する golang.org/x/oauth2/google の DefaultClient をライブラリに渡すと、サービスアカウントの権限で API を呼ぶことができる。構成ファイルの url がインスタンスメタデータのものになっているが、先に環境変数 AWS_ACCESS_KEY_ID や AWS_SECRET_ACCESS_KEY が参照されるためローカルからも実行できる。
package main
import (
"context"
"fmt"
"log"
"os"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
"google.golang.org/api/storage/v1"
)
func main() {
ctx := context.Background()
projectID := os.Getenv("GCP_PROJECT_ID")
client, err := google.DefaultClient(ctx, storage.DevstorageReadOnlyScope)
if err != nil {
log.Fatalf("Failed to create DefaultClient: %v", err)
}
storageService, err := storage.NewService(ctx, option.WithHTTPClient(client))
if err != nil {
log.Fatalf("Failed to create storage service: %v", err)
}
buckets, err := storageService.Buckets.List(projectID).Do()
if err != nil {
log.Fatalf("Failed to list buckets: %v", err)
}
fmt.Printf("Buckets in project %s:\n", projectID)
for _, b := range buckets.Items {
fmt.Println(b.Name)
}
}