Terraform 実践 #7 IAM のコード化 — インスタンスロール、最小権限、GitHub Actions OIDC
これまでの 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 (新しいファイル)
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 マネージドポリシーのアタッチ)と、自前のシークレットの読み取り(手書き)です。
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 はロールを直接受け取れず、インスタンスプロファイルという包みを要求します。歴史的な事情による一枚の層なので、慣用句として覚えてしまえば十分です。
resource "aws_iam_instance_profile" "web" {
name_prefix = "myapp-${var.env}-web-"
role = aws_iam_role.web.name
}# 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 が信頼するようにして、保存されたキーなしでロールを引き受ける方式です。
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 とコンテナを並べて動かしながら乗り換えます。