AWSのクレデンシャルをGCP側に渡さず、GCPからAWSのリソースにアクセス

## 目的
Google CloudからAWSのリソースを操作したい。
当然ながら、AWSのアクセスキーやシークレットを、環境変数に保存して扱うや、Google CloudのSecret Managerに保存して利用するなどは、よろしくない。
ので、Google Cloudのサービスアカウントに対して、許可を与えて操作できるようにする。

## 方法
まずは、Google Cloudでサービスアカウントを作成する。このサービスアカウントでCloud Functionsなどを動かすようにする。
サービスアカウントには、アカウントID（メールアドレスではない）がある。この値を使って、AWS側で許可設定を行う。

AWSでGCPのサービスアカウントに対して、リソース操作の許可を付与する。
```terraform
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の一覧を取得している。
```go
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,
	))
}
```