TerraformでIAM Policyを書く方法5つ

タイトル修正しました。最初「TerraformでIAM Policyを書く方法4つと失敗する方法ひとつ」というものでしたが、失敗する方法は単に私の書き方が間違ってただけでした
2021.06.28

TerraformでIAM Policyを書く方法は色々とあるので、紹介していきます。

今回は例として、こちらの公式ドキュメントに出てくる以下ポリシーを使いたいと思います。

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "dynamodb:*",
    "Resource": "arn:aws:dynamodb:us-east-2:123456789012:table/Books"
  }
}

ファイル外だし

policyの中身はtfファイルに書かず、JSONファイルとして外だしします。それをfile関数を使って参照します。

すでに出来上がっているポリシーを流用する場合に便利だと思います。

ddb-allow-policy.json

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "dynamodb:*",
    "Resource": "arn:aws:dynamodb:us-east-2:123456789012:table/Books"
  }
}

iam.tf

resource "aws_iam_policy" "file" {
  name = "file"
  policy = file("./ddb-allow-policy.json")
}

この方法の良いところは、ポリシー部分は純粋なJSONファイルになるので、IDEでJSONのシンタックスチェックが働くことです。

しかし、ポリシー内の一部を動的に変更することができません。今回の例だとリージョンやアカウントIDはプロビジョニング先の環境のものに動的に変更したいですし、もしアクセス許可先リソースであるDynamoDBテーブルBooksもこのTerraform内で作成しているのであれば、テーブル名の部分もそのDynamoDBリソースのAttributeを参照したいですね。

そういう場合は次のパターンを使います。

ファイル外だし(動的な値が必要な場合)

policyの中身はtfファイルに書かず、JSONファイルとして外だしします。ここまでは先程の場合と同じです。参照する際にfile関数ではなくtemplatefile関数を使います。そうすると、参照先のファイル内の変数を指定した値で置換して使用することができます。

JSONに以下のように変数を埋め込みます。

ddb-allow-policy-with-templatefile

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "dynamodb:*",
    "Resource": "arn:aws:dynamodb:${region}:${account_id}:table/${table_name}"
  }
}

templatefile関数の第二引数で変数値を指定します。

iam.tf

resource "aws_iam_policy" "file" {
  name   = "file"
  policy = file("./ddb-allow-policy.json")
}

resource "aws_iam_policy" "templatefile" {
  name = "templatefile"
  policy = templatefile(
    "./ddb-allow-policy-with-templatefile.json",
    {
      region     = data.aws_region.current.id,
      account_id = data.aws_caller_identity.current.account_id,
      table_name = aws_dynamodb_table.book.name
    }
  )
}

data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
resource "aws_dynamodb_table" "book" {
  name     = "Books"
  hash_key = "id"
  attribute {
    name = "id"
    type = "N"
  }
  billing_mode = "PAY_PER_REQUEST"
}

変数を埋め込む以外にも、for文を使ってループさせるといったこともできます。非常に便利です。がその反面、書き方次第ではfile関数案のメリットだったIDEでのJSONシンタックスチェックでエラーになります。

蛇足: TerraformのJSONはコメントが書ける

一般的にJSONにはコメントを残すことができませんが、Terraformだと可能です。キー名を//とするとそのキーは無視されるので、値にコメントを残せます。

{
  "//": "コメントが書けます",
  "Version": "2012-10-17",
  "Statement": {
    "//": "ネストした所にもコメントが書けます",
    "Effect": "Allow",
    "Action": "dynamodb:*",
    "Resource": "arn:aws:dynamodb:${region}:${account_id}:table/${table_name}"
  }
}

ヒアドキュメントを使う

ヒアドキュメントを使ってpolicy引数部分にそのままポリシーを書いてしまいます。変数が使えます。ファイル分割も必要なくaws_iam_policyリソースの定義だけで済むのでシンプルなのが良い点かと思います。が、JSONシンタックスチェックは効きませんし、インデントが崩れて若干読みにくくなります。

resource "aws_iam_policy" "heredoc" {
  name   = "heredoc"
  policy = <<EOT
{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "dynamodb:*",
    "Resource": "${aws_dynamodb_table.book.arn}"
  },
  "//": "コメントは書けます"
}
EOT
}

追記: ヒアドキュメントでもインデントする方法

以下のようにヒアドキュメントの開始マーカー<<の後ろに-をつけるとヒアドキュメントでもインデント崩れを防ぐことができます。

resource "aws_iam_policy" "heredoc" {
  name   = "heredoc"
  policy = <<-EOT
    {
      "Version": "2012-10-17",
      "Statement": {
        "Effect": "Allow",
        "Action": "dynamodb:*",
        "Resource": "${aws_dynamodb_table.book.arn}"
      }
    }
  EOT
}

こちら、yyodaさんに教えていただきました。ありがとうございます。

jsonencode関数を使う

policy引数部分にそのままポリシーを書くのは先のヒアドキュメント案と同じです。が、JSONではなくHCLの形式で書き、jsonencode関数でJSONに変換します。

resource "aws_iam_policy" "jsonencode" {
  name = "jsonencode"
  policy = jsonencode({
    "Version" = "2012-10-17"
    "Statement" = {
      "Effect"   = "Allow"
      "Action"   = "dynamodb:*"
      "Resource" = "${aws_dynamodb_table.book.arn}"
    }
  })
}

この方法、エラーになりました。

error creating IAM policy jsonencode: MalformedPolicyDocument: The policy failed legacy parsing

というエラーを吐きました。

どうやら以下の理由のようです。

  • IAM PolicyのJSONは最初のキーとしてVersionが来ないといけない
  • jsonencode関数を通してHCLのオブジェクトからJSONに変換する際に、キーはその記述順は無視されアルファベット昇順で並び替えられてしまう
  • 今回、VersionStatementというキーが存在。アルファベット昇順だとStatementが先になるので、そういうJSONが生成され、エラーになる

2個目の「キーはその記述順は無視されアルファベット昇順で並び替えられてしまう」に関しては以下のようにterraform consoleで確認しました。

$ terraform console
> jsonencode({"Version"="a", "Statement"="b"})
"{\"Statement\":\"b\",\"Version\":\"a\"}"

追記: エラーになったのは書き方が悪かったから

以下のように、Statement句の値を配列にするとエラーは発生しませんでした。確かにStatementは複数書けますよね。

resource "aws_iam_policy" "jsonencode" {
  name = "jsonencode"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "dynamodb:*"
      Resource = aws_dynamodb_table.book.arn
    }]
  })
}

ですが、あえてこの方法で書くメリットは無いのではないかと思います。ベタ書きしたいなら前述のヒアドキュメントでできますし、HCLで書きたいなら次に紹介するData Sourceの方が便利です。

専用のData Sourceを使う

iam_policy_documentというData Sourceがあります。これを使うとHCLでポリシーが書けて、それをaws_iam_policypolicyで参照することができます。

data "aws_iam_policy_document" "allow_ddb" {
  statement {
    actions = [
      "dynamodb:*"
    ]
    resources = [
      aws_dynamodb_table.book.arn
    ]
  }
}

resource "aws_iam_policy" "datasource" {
  name   = "datasource"
  policy = data.aws_iam_policy_document.allow_ddb.json
}

IDEにTerraform関連のPluginやExtensionを入れておくと、補完が効くので捗ります。以下はPyCharmでHashiCorp Terraform / HCL language supportをインストールしている場合の例です。

ありもののポリシーを使う・たたき台にするのではなく、イチからTerraformでポリシーを作成する場合はこれが一番書きやすいんじゃないかなと思います。HCL言語を書いているだけなので、コメントもHCLの書き方で書けます。

以上、TerraformでIAM Policyを書く色々な方法をご紹介しました。個人的にはtemplatefile関数の案かiam_policy_document Data Sourceの案を使うことが多いです。