NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    Terraformのtemplatefile関数活用方法

    本記事は  AWSアワード記念!夏のアドベントカレンダー  14日目の記事です。
    🎆🏆  13日目  â–¶â–¶ æœ¬è¨˜äº‹ â–¶â–¶  15日目  ðŸ†ðŸŽ†

    カラスに襲われたためしばらくその付近には近づいていません。中村です。

    Terraformのライセンス変更があったためこれからどうなるのか心配していましたが、IBMがHashiCorpを買収することが発表されたため落ち着きそうで安心しています。今後も動向を注視していきます。

    本記事ではTerraformの組み込み関数templatefileについてご紹介します。

    templatefile関数とは

    指定されたパスにあるテンプレートファイルを読み取り、その内容をテンプレートとしてレンダリングすることでテキストファイルや設定ファイルを動的に生成することができます。
    そのため環境毎に少し異なるファイルを使用する必要がある場合などに非常に便利な関数です。 developer.hashicorp.com

    AWS環境におけるtemplatefile関数の活用方法をいくつかご紹介します。

    1. ユーザーデータ

    ユーザーデータはEC2インスタンス起動時の初期設定や、スクリプトを自動実行するために使用されます。
    このユーザーデータをtemplatefile関数で生成することが可能です。

    ディレクトリ構造、ファイル内容は以下のようにしています。

    • ディレクトリ構造
    project/
    ├── modules/
    │   └── ec2/
    │        ├── main.tf
    │        └── init.tpl
    └── env/
        ├── prod/
        │     └── ec2/
        │          └── main.tf
        └── stage/
              └── ec2/
                   └── main.tf
    • modules/ec2/main.tf
      • user_dataでtemplatefile関数を使用します。
        • 同ディレクトリ上のテンプレートファイルinit.tplを指定し、packagesとfile_contentを変数にしています。
    provider "aws" {
      region = "ap-northeast-1"
    }
    
    variable "packages" {
      type = list(string)
    }
    
    variable "file_content" {
      type = any
    }
    
    resource "aws_instance" "example" {
      ami                  = "ami-061a125c7c02edb39"
      instance_type        = "t2.micro"
      iam_instance_profile = "MySessionManagerRole"
      user_data = templatefile("${path.module}/init.tpl", {
        packages     = join(" ", var.packages)
        file_content = var.file_content
      })
    }
    • modules/ec2/init.tpl
      • user_dataで使用するテンプレートファイルです。cloud-config形式にしています。
        • 今回の例ではwrite_files:、runcmd:を使用していますが、users:でユーザー作成することも可能です。
          • init.tplにcloud_final_modules:を設定することで、ユーザーデータを利用して作成したファイルを削除したり、パッケージをアンインストールしたとしても再起動時には再作成・再インストールすることができます。
            ※既存のEC2インスタンスの構成を変更するために、このテンプレートファイルを修正してterraform applyを再実行した場合、ユーザーデータが修正されますが実行はされません。 新しいユーザーデータを実行したい場合には、EC2インスタンス上でcloud-init cleanを実行してcloud-initのキャッシュをリセットしたうえで、再起動する必要があります。
    #cloud-config
    cloud_final_modules:
     - [scripts-user, always]
     - [write-files, always]
    
    write_files:
      - path: /etc/welcome.txt
        permissions: '0644'
        owner: root:root
        content: |
          ${file_content}
    
    runcmd:
      - yum install -y ${packages}
    • env/prod/ec2/main.tf
      • prod環境のEC2インスタンスで作成するファイルの内容、インストールパッケージを記述しています。
    module "ec2-instance" {
      source = "../../../modules/ec2"
    
      packages = ["amazon-cloudwatch-agent", "git"]
    
      file_content = <<EOF
        Welcome to prod server!
    EOF
    }
    • env/stage/ec2/main.tf
      • packages, file_content以外はenv/prod/main.tfと同様の内容です。
    module "ec2-instance" {
      source = "../../../modules/ec2"
    
      packages = ["git"]
    
      file_content = <<EOF
        Welcome to stage server!
    EOF
    }

    env/prod/ec2/でterrafom applyを実行してEC2インスタンスを作成します。
    以下はprod環境のEC2インスタンスのユーザーデータ・ファイル・パッケージの確認結果です。

    prod環境EC2インスタンス

    同様にenv/stage/ec2/でterrafom applyを実行します。
    以下はstage環境EC2インスタンスのユーザーデータ・ファイル・パッケージの確認結果です。

    stage環境EC2インスタンス

    2. IAMポリシー、ロール

    main.tfにIAMポリシーやロールを記述すると追加するたびにファイルが肥大化していき、目当てのポリシー・ロールを探すことが手間になります。
    これを避けるためにtemplatefile関数を使用します。

    • ディレクトリ構造
    project/
    ├── modules/
    │   └── iam/
    │        ├── main.tf
    │        ├── iam_policy.json.tpl
    │        └── iam_role.json.tpl
    └── env/
        ├── prod/
        │     └── iam/
        │          └── main.tf
        └── stage/
              └── iam/
                   └── main.tf
    • modules/ec2/main.tf
      • aws_iam_policyリソースのpolicyと、aws_iam_roleリソースのassume_role_policyでtemplatefile関数を使用します。
        • 同ディレクトリ上のテンプレートファイルiam_policy.json.tplとiam_role.json.tplを指定し、iam_policy.json.tplはactionsとresourcesを変数に、iam_role.json.tplはserviceを変数にしています。
    variable "policies" {
      description = "List of IAM policies"
      type = list(object({
        name      = string
        actions   = list(string)
        resources = list(string)
      }))
    }
    
    variable "roles" {
      description = "List of IAM roles"
      type = list(object({
        name    = string
        service = string
        policies = list(string)
      }))
    }
    
    resource "aws_iam_policy" "example" {
      for_each = { for p in var.policies : p.name => p }
      name     = each.value.name
      policy   = templatefile("${path.module}/iam_policy.json.tpl", {
        actions   = jsonencode(each.value.actions)
        resources = jsonencode(each.value.resources)
      })
    }
    
    resource "aws_iam_role" "example" {
      for_each = { for r in var.roles : r.name => r }
      name               = each.value.name
      assume_role_policy = templatefile("${path.module}/iam_role.json.tpl", {
        service = each.value.service
      })
    }
    
    resource "aws_iam_role_policy_attachment" "example" {
      for_each = { for r in var.roles : r.name => r }
    
      role       = aws_iam_role.example[each.key].name
      policy_arn = lookup({ for p in var.policies : p.name => aws_iam_policy.example[p.name].arn }, each.value.policies[0])
    }
    • modules/iam/iam_policy.json.tpl
      • policyで使用するテンプレートファイルです。
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ${actions},
          "Resource": ${resources}
        }
      ]
    }
    • modules/iam/iam_role.json.tpl
      • assume_role_policyで使用するテンプレートファイルです。
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Service": "${service}"
          },
          "Action": "sts:AssumeRole"
        }
      ]
    }
    • env/prod/iam/main.tf
      • prod環境で作成するIAMポリシー、ロールを記述しています。
    module "iam" {
      source = "../../../modules/iam"
      policies = [
        {
          name      = "prod_policy_1"
          actions   = ["s3:ListBucket", "s3:GetObject"]
          resources = ["arn:aws:s3:::prod-bucket-name", "arn:aws:s3:::prod-bucket-name/*"]
        },
        {
          name      = "prod_policy_2"
          actions   = ["ec2:DescribeInstances"]
          resources = ["*"]
        }
      ]
      roles = [
        {
          name    = "prod_role_1"
          service = "ec2.amazonaws.com"
          policies = ["prod_policy_1"]
        },
        {
          name    = "prod_role_2"
          service = "lambda.amazonaws.com"
          policies = ["prod_policy_2"]
        }
      ]
    }
    • env/stage/iam/main.tf
      • stage環境で作成するIAMポリシー、ロールを記述しています。
    module "iam" {
      source = "../../../modules/iam"
      policies = [
        {
          name      = "stage_policy_1"
          actions   = ["s3:ListBucket", "s3:GetObject"]
          resources = ["arn:aws:s3:::stage-bucket-name", "arn:aws:s3:::stage-bucket-name/*"]
        },
        {
          name      = "stage_policy_2"
          actions   = ["ec2:DescribeInstances"]
          resources = ["*"]
        }
      ]
      roles = [
        {
          name    = "stage_role_1"
          service = "ec2.amazonaws.com"
          policies = ["stage_policy_1"]
        },
        {
          name    = "stage_role_2"
          service = "lambda.amazonaws.com"
          policies = ["stage_policy_2"]
        }
      ]
    }

    env/prod/iam/でterrafom applyを実行してIAMポリシー、ロールを作成します。
    以下はprod環境IAMポリシー、ロールの確認結果です。

    prod環境IAM-1
    prod環境IAM-2

    同様にenv/stage/iam/でterrafom applyを実行します。
    以下はstage環境IAMポリシー、ロールの確認結果です。

    stage環境IAM-1
    stage環境IAM-2

    3. Terraform設定ファイル

    Terraform設定ファイルの拡張子は.tfであり、インフラリソースを定義するmain.tf、変数宣言を行うvariables.tf、出力値を定義するoutputs.tfなどがあります。
    templatefile関数を使用することでTerraform設定ファイル(tfファイル)自体を動的に作成することが可能です。
    ※Terraformはディレクトリ内すべてのtfファイルを読み取るため、拡張子が.tfであればファイル名は自由に変更可能です。

    • ディレクトリ構造
    project/
    ├── modules/
    │   └── s3/
    │        ├── main.tf
    │        └── s3.tf.tpl
    └── env/
        ├── prod/
        │     └── s3/
        │          └── main.tf
        └── stage/
              └── s3/
                   └── main.tf
    • modules/s3/main.tf
      • local_fileリソースのcontentでtemplatefile関数を使用します。
        • 同ディレクトリ上のテンプレートファイルs3.tf.tplを指定し、bucket_nameを変数にしています。
    provider "aws" {
      region = "ap-northeast-1"
    }
    
    variable "bucket_names" {
      type = list(string)
    }
    
    variable "file_path" {
      type = string
    }
    
    resource "local_file" "s3_buckets" {
      count    = length(var.bucket_names)
      filename = "${var.file_path}/s3_${var.bucket_names[count.index]}.tf"
      content  = templatefile("${path.module}/s3.tf.tpl", {
        bucket_name = var.bucket_names[count.index]
      })
    }
    • modules/s3/s3.tf.tpl
      • contentで使用するテンプレートファイルです。
    resource "aws_s3_bucket" "${bucket_name}" {
      bucket = "${bucket_name}"
      acl    = "private"
    }
    • env/prod/s3/main.tf
      • prod環境で作成するs3バケットを記述しています。
    module "s3" {
      source = "../../../modules/s3"
      
      bucket_names = ["prod-bucket-example-1", "prod-bucket-example-2", "prod-bucket-example-3"]
      file_path = "."
    }
    • env/stage/s3/main.tf
      • stage環境で作成するs3バケットを記述しています。
    module "s3" {
      source = "../../../modules/s3"
      
      bucket_names = ["stage-bucket-example-1", "stage-bucket-example-2", "stage-bucket-example-3"]
      file_path = "."
    }

    env/prod/s3/でterrafom applyを実行するとTerraform設定ファイル(prod-bucket-example-1.tf、prod-bucket-example-2.tf、prod-bucket-example-3.tf)が作成されます。
    そして再度terraform applyを実行すると作成されたファイルを読み取り、S3バケットを作成します。
    以下はprod環境s3バケットの確認結果です。

    prod環境s3

    同様にenv/stage/s3/でterrafom applyを2回実行します。
    以下はstage環境s3バケットの確認結果です。

    stage環境S3

    最後に

    templatefile関数の活用方法を3つご紹介しましたが、ほかにも様々な活用方法があるかと思います。
    本記事がTerraformを使用する皆様の参考となり、templatefile関数の新しい活用方法を見つけるための助けとなりましたら幸いです。

    執筆者:中村 勇介 インフラエンジニア。最近GCPも勉強しています。