Workload Identity使ってみた

  • #インフラ
  • #GCP
  • #Workload Identity

こんにちは。CURUCURU エンジニアの兄山です。

バッチ処理で、AWS Batch(Fargate)を用いて BigQuery へデータを流し込む処理を担当しました。

その際にキーを用いないWorkload Identityを使ってみたのでご紹介したいと思います。


Workload Identity とは

結論、

キーを使用せずにGCPリソースのアクセス権限をもてる仕組み

です。

なぜキーを使用しない仕組みを導入したのか

2 点あります ① キーの漏洩を避けるため ② キーの管理からの脱却

環境変数に、キーを設定し、リソースにアクセスする方法が一般的だと思います。 この方法だと、万が一アプリに脆弱性があり、キーが漏洩したりする可能性や、他サービスでも同じキーを用いたい場合にキーの複数管理を強いられることになります。

上記の理由からできる限りキーを用いた認証は避けたいと考え、導入をしました。

設定手順

設定の流れは以下です。 ① ECS タスクロール・ECS タスク実行ロールにアタッチするための IAM ロールを作成(AWS) ② Workload Identity Pool、Workload Identity Provider を設定する(GCP) ③ 認証のファイル(json)を取得する(GCP) ④ アプリ側で環境変数に設定してアプリケーション実行(AWS)

※Workload Identity Pool・・・外部 ID を管理できるエンティティ ※Workload Identity Provider・・・GCP と IdP(AWS, Azure etc...)の間の関係を記述するエンティティ

①ECS タスクロール・ECS タスク実行ロールにアタッチするための IAM ロールを作成(AWS)

下記 IAM ロールが作成済みでしたので以下のロールをアタッチしました

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

②Workload Identity Pool、Workload Identity Provider を設定する(GCP)

コンソール通りに設定していけば設定できます(特に注意点等もなかったはず)。

③ 認証のファイル(json)を取得する(GCP)

設定し終わるとクレデンシャルの代わりとなる、設定ファイルがダウンロードできるようになるのでダウンロードしておく。

下記のようなファイル

{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/111111111111/locations/global/workloadIdentityPools/bq-pool/providers/aws-provider",
  "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/qqqqqq@qqqqqq.iam.gserviceaccount.com:generateAccessToken",
  "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"
  }
}

④ アプリ側で環境変数に設定してアプリケーション実行(AWS)

以下コード(Golang)です。

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"time"

	"cloud.google.com/go/bigquery"
	"xxxxxxxx" // BigQueryを操作するためのアプリパッケージパス
	"yyyyyyyy" // BigQueryを操作するためのアプリパッケージパス
	...
	..
	.
)

type Credential struct {
	AccessKeyId   string    `json:"AccessKeyId"`
	SecretAccessKey string    `json:"SecretAccessKey"`
	Token       string `json:"Token"`
}

func main() {
	ctx := context.WithValue(context.Background(), ctxDerailleurTraceID, "rds-to-bigquery")
	logger.Log.Info(ctx, "start", logTags)

	// ①
	os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "./clientLibraryConfig-aws-provider.json")

	// ②
	resp, err := http.Get(fmt.Sprintf("http://169.254.170.2%s", os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")))
	if err != nil{
        logger.Log.Error(ctx, fmt.Sprintf("Error: Workload Identities %v", err), logTags)
		return
	}
	defer resp.Body.Close()
	if err != nil || resp.StatusCode != 200{
        logger.Log.Error(ctx, fmt.Sprintf("Error: status code %d", resp.StatusCode), logTags)
		return
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
        logger.Log.Error(ctx, fmt.Sprintf("Error: body ReadAll %v", err), logTags)
		return
	}
	var cred Credential
	if err = json.Unmarshal(body, &cred); err != nil {
        logger.Log.Error(ctx, fmt.Sprintf("Error: Credential Unmarshal %v", err), logTags)
		return
	}

	os.Setenv("AWS_ACCESS_KEY_ID", cred.AccessKeyId)
    os.Setenv("AWS_SECRET_ACCESS_KEY", cred.SecretAccessKey)
    os.Setenv("AWS_SESSION_TOKEN", cred.Token)

	// 以下BigQueryを操作するための処理
	logger.Log.Info(ctx, "START RDS TO BIGQUERY", logTags)
	bqClient, err := bigquery.NewClient(ctx, "curucuru-logs")
	if err != nil {
        logger.Log.Error(ctx, fmt.Sprintf("Failed to create bq bqClient:%v", err), logTags)
		return
    }

	....
	...
	..
	.


	logger.Log.Info(ctx, "FINISH RDS TO BIGQUERY BATCH", logTags)
}

ポイントは2点あります ① os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "./clientLibraryConfig-aws-provider.json") 環境変数に設定ファイルのファイルパスを指定しています。

この環境変数を設定していると、Google 側でこのクレデンシャル情報をチェックしてくれるようになります。

resp, err := http.Get(fmt.Sprintf("http://169.254.170.2%s", os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"))) 認証が通るためにはロールのクレデンシャル情報を取得し、環境変数に設定する必要があります。

アクセスするための URI はAWS_CONTAINER_CREDENTIALS_RELATIVE_URIになります。

この環境変数は、タスクにロールが付与されていると自動で設定されている環境変数になります。

タスク実行ロールに付与しても設定されないので注意してください(ここで沼ったので皆さんは気をつけてください。。)

まとめ

Fargate から BigQuery にアクセスするために Workload Identity を使用してみました。 権限周り大事ですね。 インフラ触るようになってからその思いが一層強くなりました(ハマるときは大体権限周り)。 以上、兄山でした。

メンバー募集

CURUCURU では開発メンバーを募集中です。 CURUCURU の開発に興味があったり、モダンな開発環境で挑戦してみたいという方がいましたら、ぜひこちらも覗いてみてください!