freeeの開発情報ポータルサイト

リソースベースポリシーをサポートしないAWSリソースのクロスアカウント設定と、Go による実装

こんにちは、サービス基盤のkumashunです。

freeeでは、ほとんどのサービスがAWSをインフラ基盤として利用しています。さまざまな目的や業務上の要件に応じて、複数のAWSアカウントを運用しており、時折、異なるアカウント間でのネットワーク通信やリソースへのアクセス、つまりクロスアカウントアクセスが必要となります。ここでは、リソースベースポリシーをサポートしていないAWSリソースにおける、クロスアカウントアクセスの実現方法についてご紹介します。

事例を "リソースベースポリシーをサポートしないAWSリソース" に限定しているのは、以下の背景からです。簡単に用語を説明します。

  • IAM Policy
    • AWSリソースへのアクセス定義
    • 誰がどのリソースに何をできる/できないかをJSON形式で定義できる
  • IAM Role
    • 1つ以上のIAM Policyをまとめたもの
    • 人間がマネジメントコンソールにログインする際に特定のIAM Roleを選択したり、EKS clusterのnode group等にアタッチすることで、紐づくIAM Policyに基づいたリソースへのアクセスを可能にする
  • リソースベースポリシー
    • リソースにアクセスする側ではなく、アクセスされるリソース側に設定するIAM Policy
    • 逆にアクセスする側にアタッチされるものはアイデンティティベースポリシーと呼ばれる

アクセスしたいリソースとアクセス元が同一アカウント上にある場合、アクセス元に対応したIAM Roleを作成し、そこに必要なアクセス定義をまとめたIAM Policyを紐付ければokです。

リソースとアクセス元が同一アカウント上にある場合。例えばEC2インスタンスからS3バケットにアクセスしたければ必要なIAM Policyを紐付けたIAM RoleをEC2にアタッチすれば良い。
リソースとアクセス元が同一アカウント上にある場合

クロスアカウントアクセスの場合は、上記に加え、アクセスしたいリソース側のリソースベースポリシーに、クロスアカウントのIAM Roleからのアクセスを許可する必要があります。

クロスアカウントアクセスの場合(リソースベースポリシー有)。EC2にIAM Roleをアタッチするのに加えて、S3バケットにもリソースベースポリシーを設定してアクセス許可を定義する。
クロスアカウントアクセスの場合(リソースベースポリシー有)

リソースベースポリシーの設定は同一アカウント上においても有効ですが、アクセス元が権限を満たしていればリソースベースポリシー側での許可は不要です。

同じアカウント内の ID ベースのポリシーまたはリソースベースのポリシーの一方がリクエストを許可し、他方が許可しない場合でも、リクエストは許可されます。

IAM Policyを追加で1つ足すだけで、簡単にクロスアカウントアクセスを行えます。ただつらい点として、下記でまとめられている通り、全てのAWSリソースがリソースベースポリシーをサポートしているわけではない、という事情があります。実際に自分が関わるプロジェクトではKinesis Data Streams(以下Kinesis)のクロスアカウントアクセスが発生したのですが、Kinesisはそのサポート外のリソースに含まれています。

IAM と連携する AWS のサービス - AWS Identity and Access Management

そのため、より古いやり方となる、クロスアカウントアクセスのためのAssumeRoleの設定が必要になりました。AssumeRoleとは、自分自身にアタッチされていないIAM Roleになり変わって、紐づくIAM Policyの権限を行使することです。誰にAssume Roleを許可するかはIAM Role側のTrusted Entitiesとして予め設定しておきます。

AssumeRoleによるリソースアクセスは以下の流れになります。

  1. Trusted Entitiesで信頼されたアクセス元が、対象のIAM Roleに対してAssumeRoleを行使する
  2. AssumeRoleによって成り変わったIAM Roleの権限を使ってリソースにアクセスする

クロスアカウントアクセスの場合(リソースベースポリシー無し)。リソースベースポリシーの代わりに、AssumeRoleのための権限設定を行う。
クロスアカウントアクセスの場合(リソースベースポリシー無し)

このAssumeRoleの設定及びアプリケーション上でのAssumeRoleの行使(Goによる実装)について、以下のケースを想定して説明します。

  • アカウントA(id: 111111111111)にあるEKS clusterhoge-clusterから、アカウントB(id: 999999999999)にあるKinesis Data Streams fuga-streamにアクセスしたい

なおfreeeでは、AWS上のインフラリソースはTerraformによるIaC管理を実施しているため、インフラに関する説明にはTerraformを用いています。IaC管理を実施していない場合は適宜読み替えてもらうようお願いします。

AssumeRoleのためのインフラ設定

アカウントBにKinesisは作成済みとします。arnは以下。
arn:aws:kinesis:ap-northeast-1:999999999999:stream/fuga-stream

またhoge-clusterのnode groupのに紐付くinstance-profileとIAM Roleは以下のように定義しているとします。

resource "aws_iam_role" "hoge_cluster_ng_role" {
  name = "hoge-cluster-ng-role"
}

resource "aws_iam_instance_profile" "hoge_cluster_ng_profile" {
  name = "hoge-cluster-ng-profile"
  role = aws_iam_role.hoge_cluster_ng_role.name
}

まずはアカウントBにAssumeRole用のIAM Roleを作成します。(Go実装でこのIAM Roleに対してAssumeRoleを実行します) 上にも書いた通り、誰に対してAssumeRoleを許可するのかを Trusted Entitiesに設定する必要があるので、TerraformではIAM Roleの assume_role_policy に許可する対象 すなわちhoge-clusterのIAM Roleを指定します。

resource "aws_iam_role" "read_fuga_from_hoge" {
  name               = "read-fuga-from-hoge"
  assume_role_policy = data.aws_iam_policy_document.read_fuga_from_hoge.json
}

data "aws_iam_policy_document" "read_fuga_from_hoge" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type = "AWS"
      identifiers = [
        "arn:aws:iam::111111111111:role/hoge-cluster-ng-role",
      ]
    }
  }
}

引き続きアカウントBfuga-streamへのアクセス制御のためのIAM Policyを作成し、先ほどのIAM Roleにアタッチします。ここは単一アカウントに閉じる場合と同じです。

resource "aws_iam_role_policy_attachment" "fuga_kinesis_read" {
  role       = aws_iam_role.read_fuga_from_hoge.name
  policy_arn = aws_iam_policy.kinesis_read_policy.arn
}

resource "aws_iam_policy" "kinesis_read_policy" {
  name   = "kinesis-read-policy"
  policy = data.aws_iam_policy_document.kinesis_read_policy_document.json
}

data "aws_iam_policy_document" "kinesis_read_policy_document" {
  statement {
    sid    = "allowFetchDataRecord"
    effect = "Allow"

    actions = [
      "kinesis:Describe*",
      "kinesis:List*",
      "kinesis:Get*",
    ]

    resources = [
      "arn:aws:kinesis:ap-northeast-1:999999999999:stream/fuga-stream",
    ]
  }
}

最後に、read_fuga_from_hoge (IAM Role)へのAssume Roleを許可するIAM Policyを アカウントA に作成し、hoge-cluster のIAM Roleにアタッチします。

resource "aws_iam_policy" "hoge_to_fuga_policy" {
  name        = "hoge-from-fuga-policy"
  policy      = data.aws_iam_policy_document.hoge_from_fuga_policy_document.json
}

data "aws_iam_policy_document" "hoge_from_fuga_policy_document" {
  statement {
    sid    = "allowFetchDataRecord"
    effect = "Allow"

    actions = ["sts:AssumeRole"]

    resources = ["arn:aws:iam::999999999999:role/read-fuga-from-hoge"]
  }
}

resource "aws_iam_role_policy_attachment" "assume_role_to_fuga" {
  role       = aws_iam_role.hoge_cluster_ng_role.name
  policy_arn = aws_iam_policy.hoge_to_fuga_policy.arn
}

インフラの設定は以上!

アプリケーション上でのAssumeRole行使(Go実装)

hoge-cluster上のPodにデプロイするGoのプログラムとして、fuga-stream のShard一覧を取得するサンプルが以下になります。

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
    "github.com/aws/aws-sdk-go-v2/service/kinesis"
    "github.com/aws/aws-sdk-go-v2/service/sts"
)

const (
    streamName    = "fuga-stream"
    assumeRoleArn = "arn:aws:iam::999999999999:role/read-fuga-from-hoge"
)

func main() {
    ctx := context.Background()
    awsCfg := aws.NewConfig()
    // AssumeRoleを使った認証情報でaws configを上書き
    stsClient := sts.NewFromConfig(*awsCfg)
    awsCfg.Credentials = stscreds.NewAssumeRoleProvider(stsClient, assumeRoleArn)

    kc := kinesis.NewFromConfig(*awsCfg)
    out, err := kc.ListShards(ctx, &kinesis.ListShardsInput{
        StreamName: aws.String(streamName),
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("shard count:", len(out.Shards))
    for _, s := range out.Shards {
        fmt.Println(*s.ShardId)
    }
}

アカウントBで作ったIAM Roleに対してAssumeRoleProviderを生成し、aws configの認証情報を上書きすることでインフラ上で設定したAssumeRoleを実際に行使します。

// AssumeRoleを使った認証情報でaws configを上書き
stsClient := sts.NewFromConfig(*awsCfg)
awsCfg.Credentials = stscreds.NewAssumeRoleProvider(stsClient, assumeRoleArn)

pkg.go.dev

またaws configを上書きせず、元々の自アカウント向けのaws configを保持することも可能です。これによって、同一プログラム上で特定のAWSリソースはクロスアカウント上、それ以外は自アカウント上のリソースを扱うことも可能になります。

func main() {
    cfg := aws.NewConfig()

    // AssumeRoleを使った認証情報でkinesis clientを作成
    assumed := assumedAWSConfig(*cfg, assumeRoleArn)
    kc := kinesis.NewFromConfig(*assumed)
    ...

    // 自アカウントの認証情報でsqs clientを作成
    sc = sqs.NewFromConfig(*cfg)
    ...
}

func assumedAWSConfig(awsCfg aws.Config, assumeRoleARN string) *aws.Config {
    stsc := sts.NewFromConfig(awsCfg)
    awsCfg.Credentials = stscreds.NewAssumeRoleProvider(stsc, assumeRoleARN)
    return &awsCfg
}


リソースベースポリシーをサポートしないAWSリソースへのクロスアカウント実装方法を、インフラのコード含め簡単に説明しました。クロスアカウントアクセスにおけるAssumeRoleはどちらのアカウントに何を作ればいいのかがとても分かりにくいので、備忘録として残しておきます。全AWSリソースがリソースペースポリシーに対応してくれるとありがたいのですが、何かAWSの設計思想や背景があるのかもしれません。冒頭のリンクを再掲しますが、"IAM と連携するサービス"にある表から、今回取り上げたKinesis以外にも多くのリソースがリソースベースポリシーをサポートしていないことが分かります。

IAM と連携する AWS のサービス - AWS Identity and Access Management