📝

terraformでterraform実行ユーザや環境をセットアップする for AWS

2024/12/16に公開

最近初めてterraformを使ってAWSの環境構築を初めてみました。
今後も引き続き個人開発システムのデプロイのためにterraformingしていく予定なのですが、作業ログ的に内容を残していこうと思います。

前提

terraform実行用の権限などを作るための設定ため、この設定自体は恐れ多くもAdmin権限などの強い権限を持つユーザで実行する必要があります。

記事内容

  • 筆者はAWS, terraformの設定について知見がありません
  • 初心者を対象にしつつも、terraformそのものの説明については詳細には行いません
  • 本記事内に記載したものは動作します
  • スマートに解決できていない部分もあります

terraformの実行手順

今回は以下のようにterraformを実行していく想定で勧めていきます。

$ terraform init
$ terraform plan
$ terraform apply
# main.tfを微調整
$ terraform init -migrate-state

applyしたあとに一度main.tfを微調整してmigrateしているのが、少し変なところなのですが察しの通り苦肉の策です。
内容については後述します。

terraformを書いていく

それではさっそく設定を書いていきたいと思います。

terraformの基本構成を埋める

基本構成という呼び方が既に正しくなさそうですが、仮にそう呼びます。

terraform/setup/main.tf
provider "aws" {
  region = "ap-northeast-1"
}
terraform {
  required_version = ">= 1.10"

#  backend "s3" {
#    bucket = "your-bucket-name"
#    key    = "setup/terraform.tfstate"
#    encrypt = true
#    region = "ap-northeast-1"
#  }
}

まずはここまでです。
さっそく怪しいコメントアウトが発生していますね。

backendという設定はterraform実行結果などを管理する設定ファイル的なものの保存先を指定する構文です。
複数端末からterraformで対象プロジェクトの管理を行う場合にはs3Terraform Cloud(remote)などを指定するのが好ましいです。

しかしterraformでの初回セットアップ時はまだterraformの管理用S3 Bucketが存在しない状態なので、最初のこのタイミングではコメントアウトしています。

apply後のプロジェクトでbackendを変更すると、再度initが必要になり、そのときにinit -migrate-stateで変更後のbackendにstate群をmigrateする必要があったのですね。

terraform実行用のRoleを作成する

続いて今後terraformを実行するときに利用するRoleを作成していきます。

# assumerole用policy
resource "aws_iam_role" "terraform" {
  name = "terraform"
  assume_role_policy = data.aws_iam_policy_document.terraform_assumerole.json
}
data "aws_iam_policy_document" "terraform_assumerole" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
        type    = "AWS"
        identifiers =["your-local-user-arn"]
    }
  }
}

# terraform実行用policyのSample
resource "aws_iam_policy" "terraform" {
  name = "terraform"
  path = "/"

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:PutObject"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
  })
}
resource "aws_iam_role_policy_attachment" "terraform" {
  role = aws_iam_role.terraform.name
  policy_arn = aws_iam_policy.terraform.arn
}

最初の部分でterraformのAssumeRole用のRoleと、そのRoleに対してAssumeRoleを行えるユーザを設定しています(そのつもりです)

次のブロックでは今後実行したいterraform applyで必要そうな権限を管理して付与しています。
これはあくまで例なので、実際には各々が必要なポリシーを設定しましょう。

必要なPolicyなんてわからない

さてAWSの玄人ならともかく、そうでない人はPolicyが細かすぎてそんなものわからない、となりませんか?
僕はなりました。

なので、僕はpikeというツールを使うことにしました。
https://github.com/JamesWoolfenden/pike

これを利用して実行したいtfファイルを指定すると、その実行に必要なポリシーを列挙してterraform用のresource設定を出力してくれます。
なので、そんなに詳しくて自分ではよくわからないときにはとても便利ですので、慣れてくるまでオススメです。

ちなみにこれだけだと設定を変更してapply再実行するときのバージョニングなどに必要な権限は出力されないので、そういうときは自分で追記する必要があるので注意です。

terraform stateを管理するbucketを作成する

最後にbackendに利用するbucketを作成していきます。

# terraformのデータ保存用
resource "aws_s3_bucket" "terraform" {
  bucket = "your-project-terraformstate"
}
# パブリックアクセスをブロックする設定
resource "aws_s3_bucket_public_access_block" "terraform" {
  bucket                  = aws_s3_bucket.terraform.bucket
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
resource "aws_s3_account_public_access_block" "terraform" {
  block_public_acls = true
  ignore_public_acls = true
  block_public_policy = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "terraform" {
  bucket = aws_s3_bucket.terraform.id
  policy = data.aws_iam_policy_document.s3_terraform.json
}
data "aws_iam_policy_document" "s3_terraform" {
  statement {
    effect    = "Allow"
    actions   = [
      "s3:GetObject",
      "s3:ListBucket",
      "s3:PutObject",
      "s3:DeleteObject",
      "s3:ListMultipartUploadParts"
    ]
    resources = [
      aws_s3_bucket.terraform.arn,
      "${aws_s3_bucket.terraform.arn}/*",
    ]

    principals {
      type = "AWS"
      identifiers = [aws_iam_role.terraform.arn]
    }
  }
}

最初のブロックでprivateなbucketを作成しています。(そのつもり)

次のブロックでは作成したbucketに対して、terraform実行Roleに操作権限を付与しています。
これでterraformの実行ユーザがstateなどを対象バケットで管理できるようになります。

実行する

ここで作成したものを実行していきます。

main.tf全体
terraform/setup/main.tf
provider "aws" {
  region = "ap-northeast-1"
}
terraform {
  required_version = ">= 1.10"

#  backend "s3" {
#    bucket = "your-bucket-name"
#    key    = "setup/terraform.tfstate"
#    encrypt = true
#    region = "ap-northeast-1"
#  }
}

resource "aws_iam_role" "terraform" {
  name = "terraform"
  assume_role_policy = data.aws_iam_policy_document.terraform_assumerole.json
}
# assumerole用policy
data "aws_iam_policy_document" "terraform_assumerole" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
        type    = "AWS"
        identifiers =["your-local-user-arn"]
    }
  }
}

# terraform実行用policy
resource "aws_iam_policy" "terraform" {
  name = "terraform"
  path = "/"

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "dynamodb:DeleteItem",
                "dynamodb:DescribeTable",
                "dynamodb:GetItem",
                "dynamodb:PutItem"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeAccountAttributes"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": [
                "ecr:CreateRepository",
                "ecr:DeleteRepository",
                "ecr:DescribeRepositories",
                "ecr:ListTagsForResource"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "VisualEditor3",
            "Effect": "Allow",
            "Action": [
                "iam:AttachRolePolicy",
                "iam:CreateOpenIDConnectProvider",
                "iam:CreatePolicy",
                "iam:CreateRole",
                "iam:CreatePolicyVersion", # 個別に追加した
                "iam:DeleteOpenIDConnectProvider",
                "iam:DeletePolicy",
                "iam:DeleteRole",
                "iam:DetachRolePolicy",
                "iam:GetOpenIDConnectProvider",
                "iam:GetPolicy",
                "iam:GetPolicyVersion",
                "iam:GetRole",
                "iam:ListAttachedRolePolicies",
                "iam:ListInstanceProfilesForRole",
                "iam:ListPolicyVersions",
                "iam:ListRolePolicies",
                "iam:UpdateOpenIDConnectProviderThumbprint"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "VisualEditor4",
            "Effect": "Allow",
            "Action": [
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:PutObject"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "terraform" {
  role = aws_iam_role.terraform.name
  policy_arn = aws_iam_policy.terraform.arn
}

# terraformのデータ保存用
resource "aws_s3_bucket" "terraform" {
  bucket = "your-project-terraformstate"
}

# パブリックアクセスをブロックする設定
resource "aws_s3_bucket_public_access_block" "terraform" {
  bucket                  = aws_s3_bucket.terraform.bucket
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_account_public_access_block" "terraform" {
  block_public_acls = true
  ignore_public_acls = true
  block_public_policy = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "terraform" {
  bucket = aws_s3_bucket.terraform.id
  policy = data.aws_iam_policy_document.s3_terraform.json
}

data "aws_iam_policy_document" "s3_terraform" {
  statement {
    effect    = "Allow"
    actions   = [
      "s3:GetObject",
      "s3:ListBucket",
      "s3:PutObject",
      "s3:DeleteObject",
      "s3:ListMultipartUploadParts"
    ]
    resources = [
      aws_s3_bucket.terraform.arn,
      "${aws_s3_bucket.terraform.arn}/*",
    ]

    principals {
      type = "AWS"
      identifiers = [aws_iam_role.terraform.arn]
    }
  }
}
$ terraform init
$ terraform plan
$ terraform apply

上から順にやっていけば実行できます。

そして実行が完了したらmain.tfのbackend部分のコメントアウトを解除して以下を実行します。

$ terraform init -migrate-state

これで今回作成したbucketにterraformstateファイルがmigrateされます。

これにセットアップは完了です。

思ったこと

実行用ユーザは果たして必要なのだろうか

この設定を書くときに色々調べているとterraform実行用のユーザを作るというのを見かけることが多かったのですが、ユーザって作る必要あるんですかね?
ユーザを作るとそのユーザで実行するためにcredentialsを発行しないといけなくなるため、ユーザではなくRoleを作ってassume roleするほうがスマートなんじゃないかなと思いました。

terraform実行用のユーザ・ロール作成は管理画面から作ってるのをよく見かける

terraform実行用の構成が存在しないときはterraformで実行すると実行ユーザの権限が強すぎて危険だから、管理画面で自分が明確に作る、みたいな発想なのかもしれないなぁと思ったりしました。
ただ結局それはadminユーザとかでやることになるだろうから、セットアップもterraformでやっちゃってもいいんじゃないかなと思って自分はterraformで書いてみました。

backend bucketは手で作ったほうが良さそう

この記事を書きながら正気に戻ったところ、前述の通りbucket管理はこのmain.tfの中から外したほうがいいなと思いました。

bucketの名前などを変更したくなった場合を考えてみます。
このmain.tfを変更します。
それに合わせてbackendも修正します。

applyしたいのですが、backend変更後は一度initをしないとapplyできません。
なので、backendはそのままにapplyしようとしますが、そうするとbackendに指定されているbucketはもうないのでおかしなことになり、どうしようもなくなります。

これでは本末転倒ですね。
やめましょう。

まとめ

bucketやめたほうがいいと思うなら、書くなよとは思いましたが、このほうがリアル感があるので残してみました。
最近やっとAWSを真面目に一人で扱い始めたので、引き続き頑張りたいです。

Discussion