Terraform 実践 #7 IAM のコード化 — インスタンスロール、最小権限、GitHub Actions OIDC

読了 4分

これまでの 6 本で先送りにしてきた権限の問題を、ここで一気に解決します。インスタンスは SSM パラメータを読む必要があり(#6)、Session Manager で接続を受ける必要があり(#3)、いずれ CI が AWS にデプロイする必要も出てきます。すべて IAM の仕事です。IAM はコンソールで扱うと JSON の塊と格闘する領域ですが、Terraform に移すとポリシーがレビュー可能なコードになり、リソース参照のおかげで ARN のタイプミスが消えるので、IaC の恩恵をもっとも強く体感できる領域でもあります。

信頼ポリシー — 誰がこのロールを引き受けられるのか #

IAM ロールは 2 つのポリシーの組み合わせです。すなわち誰が引き受けられるのか(信頼ポリシー)と、引き受けたら何ができるのか(権限ポリシー)です。基礎 #4で jsonencode を使って JSON を作る方法を見ましたが、IAM にはもっと良い道具があります。aws_iam_policy_document データソースです。HCL の文法でポリシーを書くと JSON に変換してくれます。validate の段階で構造の誤りが検出され、参照も自然に書けるので、IAM コードの標準的な道具として定着しています。

iam.tf
# iam.tf (新しいファイル)
data "aws_iam_policy_document" "ec2_assume" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "web" {
  name_prefix        = "myapp-${var.env}-web-"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume.json
}

権限ポリシー — 必要なものだけ、リソース単位で #

インスタンスに必要な権限は 2 つです。Session Manager の動作(AWS マネージドポリシーのアタッチ)と、自前のシークレットの読み取り(手書き)です。

iam.tf
resource "aws_iam_role_policy_attachment" "ssm_core" {
  role       = aws_iam_role.web.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

data "aws_iam_policy_document" "web_permissions" {
  statement {
    actions   = ["ssm:GetParameter"]
    resources = [aws_ssm_parameter.db_password.arn]
  }

  statement {
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.assets.arn}/*"]
  }
}

resource "aws_iam_role_policy" "web" {
  name_prefix = "web-"
  role        = aws_iam_role.web.id
  policy      = data.aws_iam_policy_document.web_permissions.json
}

最小権限の核心は resources の行にあります。ssm:GetParameter を "*" に開放せず、前回作ったパラメータリソースへの ARN 参照に絞りました。パラメータ名が変わってもポリシーが追従し、このロールでは他のシークレットを読めません。コンソールから ARN をコピーして貼っていた時代のタイプミスや漏れが、参照 1 行で消える。IAM コード化を代表する場面です。

インスタンスプロファイル — ロールを EC2 に付けるアダプター #

EC2 はロールを直接受け取れず、インスタンスプロファイルという包みを要求します。歴史的な事情による一枚の層なので、慣用句として覚えてしまえば十分です。

iam.tf
resource "aws_iam_instance_profile" "web" {
  name_prefix = "myapp-${var.env}-web-"
  role        = aws_iam_role.web.name
}
compute.tf
# compute.tf の launch template に追加
resource "aws_launch_template" "web" {
  # ... 既存の内容 ...

  iam_instance_profile {
    name = aws_iam_instance_profile.web.name
  }
}

apply 後、ASG がインスタンスを入れ替えると(launch template の新バージョン)、コンソールの Session Manager で接続ボタンが有効になります。#3で約束した「SSH ポートなしの接続」が完成しました。接続して前回の aws ssm get-parameter コマンドを実行すれば、シークレット読み取りの権限まで動いていることを確認できます。

GitHub Actions OIDC — アクセスキーのない CI #

最後に、人間ではないものの認証を 1 つ先に準備しておきます。CI(GitHub Actions)が AWS にデプロイするには認証情報が必要ですが、アクセスキーを発行して GitHub のシークレットに入れる方式は、漏えいとローテーションの問題を抱え込みます。現在の標準は OIDC 連携です。GitHub がワークフローごとに発行する短命トークンを AWS が信頼するようにして、保存されたキーなしでロールを引き受ける方式です。

iam.tf
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
}

data "aws_iam_policy_document" "github_assume" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:my-org/myapp:*"]
    }
  }
}

resource "aws_iam_role" "github_actions" {
  name_prefix        = "myapp-${var.env}-gha-"
  assume_role_policy = data.aws_iam_policy_document.github_assume.json
}

核心は 2 つ目の condition です。sub クレームを repo:my-org/myapp:* に制限することで、自リポジトリのワークフローだけがこのロールを引き受けられます。この制限がないと、どの GitHub リポジトリからでもロールを引き受けられる穴になってしまうため、OIDC 構成でもっとも重要な 1 行です。ロールにどんなデプロイ権限を与えるか、そしてワークフロー側の設定は、CI で plan と apply を回す運用シリーズ #1 で完成させます。

まとめ #

今回扱った内容です。

  • IAM ロールは信頼ポリシーと権限ポリシーの組み合わせで、aws_iam_policy_document データソースが IAM コードの標準的な道具です
  • 権限はリソース ARN の参照で絞ります。最小権限がタイプミスなしで維持されることが、IAM コード化の最大の恩恵です
  • EC2 にはインスタンスプロファイルというアダプターでロールを付け、launch template に接続します。これで Session Manager 接続とシークレット読み取りが完成しました
  • CI はアクセスキーの代わりに OIDC でロールを引き受けます。sub クレームをリポジトリに制限する condition が、この構成の安全ピンです
  • CI ロールのデプロイ権限とワークフローの作成は、運用シリーズ #1 に続きます

次回(#8 ECS Fargate デプロイ)では、コンピューティング層を世代交代させます。AMI と user_data の限界を整理し、同じ ALB の後ろで EC2 とコンテナを並べて動かしながら乗り換えます。

X