目的
Google CloudからAWSのリソースを操作したい。
当然ながら、AWSのアクセスキーやシークレットを、環境変数に保存して扱うや、Google CloudのSecret Managerに保存して利用するなどは、よろしくない。
ので、Google Cloudのサービスアカウントに対して、許可を与えて操作できるようにする。
方法
まずは、Google Cloudでサービスアカウントを作成する。このサービスアカウントでCloud Functionsなどを動かすようにする。
サービスアカウントには、アカウントID(メールアドレスではない)がある。この値を使って、AWS側で許可設定を行う。
AWSでGCPのサービスアカウントに対して、リソース操作の許可を付与する。
locals {
role_name = "google-cloud-service-account" // Edit
service_account_id = "" // e.g. 123456789
policies = [
"arn:aws:iam::aws:policy/AmazonSESReadOnlyAccess",
]
}
resource "aws_iam_role" "this" {
name = local.role_name
assume_role_policy = data.aws_iam_policy_document.this.json
}
data "aws_iam_policy_document" "this" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
effect = "Allow"
principals {
identifiers = ["accounts.google.com"]
type = "federated"
}
condition {
test = "StringEquals"
variable = "accounts.google.com:sub"
values = [local.service_account_id]
}
}
}
resource "aws_iam_role_policy_attachment" "this" {
for_each = toset(local.policies)
role = aws_iam_role.this.name
policy_arn = each.value
}
次に、Cloud Functionsで以下を作成する。ここで使用するサービスアカウトは、最初で作成したサービスアカウント。
以下のサンプルでは、SESの一覧を取得している。
package handle
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"strings"
"github.com/GoogleCloudPlatform/functions-framework-go/functions"
"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/ses"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/samber/mo"
"google.golang.org/api/idtoken"
)
func init() {
functions.HTTP("handle", handle)
}
func handle(w http.ResponseWriter, _ *http.Request) {
if err := listSES(); err != nil {
slog.Error("ses check", slog.String("error", err.Error()))
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
// listSES SESの一覧を取得
func listSES() error {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"))
if err != nil {
return err
}
token, err := getToken(ctx).Get()
if err != nil {
return fmt.Errorf("get token from metadata: %w", err)
}
cred, err := assume(ctx, cfg, token).Get()
if err != nil {
return fmt.Errorf("assume role: %w", err)
}
cfg, err = config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"), config.WithCredentialsProvider(cred))
if err != nil {
return err
}
client := ses.NewFromConfig(cfg)
output, err := client.ListIdentities(ctx, &ses.ListIdentitiesInput{})
if err != nil {
return err
}
var result []string
for _, identity := range output.Identities {
result = append(result, identity)
}
slog.Info("check", slog.String("result", strings.Join(result, ",")))
return nil
}
// metadataからtokenを取得
func getToken(ctx context.Context) mo.Result[string] {
s, err := idtoken.NewTokenSource(ctx, "accounts.google.com")
if err != nil {
return mo.Err[string](err)
}
token, err := s.Token()
if err != nil {
return mo.Err[string](err)
}
return mo.Ok(token.AccessToken)
}
// tokenを使ってAWSに接続するためのcredentialsを取得
func assume(ctx context.Context, cfg aws.Config, token string) mo.Result[credentials.StaticCredentialsProvider] {
client := sts.NewFromConfig(cfg)
out, err := client.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityInput{
RoleArn: aws.String(os.Getenv("ARN")),
WebIdentityToken: aws.String(token),
RoleSessionName: aws.String("cloud-functions"),
})
if err != nil {
return mo.Err[credentials.StaticCredentialsProvider](err)
}
return mo.Ok(credentials.NewStaticCredentialsProvider(
*out.Credentials.AccessKeyId,
*out.Credentials.SecretAccessKey,
*out.Credentials.SessionToken,
))
}