技術(tech)

TerraformでのAWSリソースIaC化のためのテンプレートを紹介

はじめに

インフラのリソースをコードで管理したい。
さて、Terraformを使おう… となった際に、どのようなディレクトリ構成で、どのようにワークスペースを区切るべきか、悩むことかと思います。

今回は小規模な組織がサクッとシンプルにTerraformを扱う際の個人的ベストな構成を紹介します。

前提

本記事では、以下のような方々をターゲットとしております。

  • AWSのリソースをTerraformで管理したい
  • 全てのインフラの変更を1, 2人で面倒見きれる程度の組織規模である(多少のコンフリクトは問題にならない)
  • なるべくお金をかけたくないので。なのでTerraform Cloudは利用しないで、AWS上のS3でTerraformのstateを管理する

逆に、大規模な組織においてTerraform導入を考えている場合、このケースは適さないと思います。

理由としては以下の通りです。

多人数の組織の場合は頻繁にTerraform Applyが発生し、コンフリクトや意図せぬリソース変更の原因となる

  • 現状、Terraform Apply時にS3上のTerraformのstateファイルにロックをかけることはできる
  • とはいえ、(CI/CDを構築した場合)mainブランチの状態を元にTerraform applyが適用されることになる
  • 複数人がmainにマージしてしまうと、意図しない事故が発生する可能性が高区なる
  • この場合には、素直にTerraform Cloudに移行して、state管理を任せましょう

概要

結論として、以下のような構成をお勧めします。
link: https://github.com/gonkunkun/terraform-template

本構成におけるポイントは以下の通りです。

  • TerraformのstateファイルはTerraformで管理せずに、Cloudformationを利用
    • これは言わずもがなです。
    • TerraformからTerraformのstateファイルを消せるのは論外
  • 環境はstgとprdの2環境を想定
  • 各環境毎にstateファイルを分けて管理

それぞれの環境毎にtfファイルを分けない

  • ここは一番結論が分かれるポイントだと思います
  • tfファイルを環境毎に分けないことで以下のメリットが生まれます
    • stgとprd環境の差異をなるべく発生させない
    • 仮に環境差異がある場合、コードをパッと見れば分かる
    • 2重メンテが発生しない
  • 反面、デメリットもあります
    • stgにのみ、prdにのみ適用したい変更がある場合には手間が増える
    • 具体的には、都度都度条件分岐やcountを利用して片面にのみ変更を当てる必要がある

詳細

ディレクトリ構成

ディレクトリ構成としては以下の通りです。

~/D/g/terraform-template ❯❯❯ tree .                                                                                                                                                                                               main
.
├── README.md
├── cloudformation
│   └── S3.yaml
└── workloads
    ├── data.tf
    ├── envs
    │   ├── prd
    │   │   ├── backend.hcl
    │   │   └── terraform.tfvars
    │   └── stg
    │       ├── backend.hcl
    │       └── terraform.tfvars
    ├── locals.tf
    ├── main.tf
    ├── providers.tf
    ├── terraform.tfvars
    ├── variables.tf
    ├── versions.tf
    └── vpc.tf

それでは、いくつかのポイント毎に見ていきましょう。

Stateファイルの配置先

本来はこの設定もCI/CDから変更したいですが、そこまで考えられていないです。
バケット名やTerraformの権限を持ったユーザを設定した上で、CloudformationでS3バケットを作成します。

Resources:
  StateBucketStg:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: "xxxxx-terraform-state-stg"
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  StateFileBucketPolicyForStg:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref StateBucketStg
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "AllowSpecificIAMUser"
            Effect: "Allow"
            Principal:
              AWS: "arn:aws:iam::xxxxxxxxx:user/terraform"
            Action:
              - "s3:GetObject"
              - "s3:PutObject"
              - "s3:ListBucket"
              - "s3:DeleteObject"
            Resource:
              - !Sub "arn:aws:s3:::${StateBucketStg}/*"
              - !Sub "arn:aws:s3:::${StateBucketStg}"

  TerraformPlanOnlyPolicyForStg:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: "TerraformPlanOnlyPolicy"
      Roles:
        - !Ref TerraformUserRole 
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "AllowPlanActions"
            Effect: "Allow"
            Action:
              - "s3:GetObject"
              - "s3:ListBucket"
            Resource:
              - !Sub "arn:aws:s3:::${StateBucket}"
              - !Sub "arn:aws:s3:::${StateBucket}/*"

          - Sid: "DenyApplyAndDestroy"
            Effect: "Deny"
            Action:
              - "s3:PutObject"
              - "s3:DeleteObject"
            Resource:
              - !Sub "arn:aws:s3:::${StateBucket}"
              - !Sub "arn:aws:s3:::${StateBucket}/*"

  StateBucketPrd:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: "xxxxx-terraform-state-prd"
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  StateFileBucketPolicyForPrd:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref StateBucketPrd
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "AllowSpecificIAMUser"
            Effect: "Allow"
            Principal:
              AWS: "arn:aws:iam::xxxxxxxxx:user/terraform"
            Action:
              - "s3:GetObject"
              - "s3:PutObject"
              - "s3:ListBucket"
              - "s3:DeleteObject"
            Resource:
              - !Sub "arn:aws:s3:::${StateBucketPrd}/*"
              - !Sub "arn:aws:s3:::${StateBucketPrd}"

ref: https://github.com/gonkunkun/terraform-template/blob/main/cloudformation/S3.yaml

backend (stateファイルの保存先)の設定

環境毎にbackendの設定を記載しています。

bucket         = "xxxxx-terraform-state-stg"
region         = "ap-northeast-1"
key            = "stg/terraform.tfstate"
use_lockfile   = true
encrypt        = true

stg: https://github.com/gonkunkun/terraform-template/blob/main/workloads/envs/stg/backend.hcl

bucket         = "xxxxx-terraform-state-prd"
region         = "ap-northeast-1"
key            = "prd/terraform.tfstate"
use_lockfile   = true
encrypt        = true

prd: https://github.com/gonkunkun/terraform-template/blob/main/workloads/envs/prd/backend.hcl

DynamoDBを用いたstate lockが不要になったのが嬉しいですね。
ref: https://github.com/hashicorp/terraform/blob/5279e432611752a9bcba27baaf657a3305c226f8/website/docs/language/backend/s3.mdx#state-locking

ちなみに、plan/applyを実行する際には以下のようなコマンドとなります。

# In the case of stg
## Specify the backend
terraform init -backend-config="envs/stg/backend.hcl"
## Plan
terraform plan -var="env=stg"

# In the case of prd
## Specify the backend
terraform init -backend-config="envs/prd/backend.hcl"
## Plan
terraform plan -var="env=prd"

stgとprd環境のリソース定義方法

環境毎に変数を定義し、terraformリソースに渡します。

locals {
  cidr_block = {
    stg : "10.10.0.0/16",
    prd : "10.20.0.0/16",
  }[var.env]
}

#trivy:ignore:AVD-AWS-0178
resource "aws_vpc" "sample" {
  cidr_block = local.cidr_block

  tags = {
    "Name" = "sample-${var.env}-vpc"
  }
  tags_all = {
    "Name" = "sample-${var.env}-vpc"
  }

  lifecycle {
    prevent_destroy = true
  }
}

ref: https://github.com/gonkunkun/terraform-template/blob/main/workloads/vpc.tf

セットアップ(Terraform planまで)

それでは、ローカル環境からplanを打てるようになるまでの設定を見ていきます。

事前準備

以下は事前に済ませておいてください。

  • 対象となるAWSアカウントの作成
  • state管理用のS3バケットの作成
  • Terraform実行用のIAM or Roleの作成 / ローカル環境上でのプロファイルの設定

設定

リポジトリをCloneしてきます。

# Install Terraform (using brew)
brew install terraform

# Clone the repository
git clone https://github.com/gonkunkun/terraform-template

バックエンドのS3バケット名を正しく設定します。
stg: https://github.com/gonkunkun/terraform-template/blob/main/workloads/envs/stg/backend.hcl
prd: https://github.com/gonkunkun/terraform-template/blob/main/workloads/envs/prd/backend.hcl

自分の環境に合わせてregionを設定します。
https://github.com/gonkunkun/terraform-template/blob/main/workloads/providers.tf

バックエンドを選択して、initしましょう。
(このタイミングでS3上にstateファイルが生成されるはず)

# Initialize the provider (for the staging environment)
cd ./terraform-template/workloads
terraform init -backend-config="envs/stg/backend.hcl"

あとはplanを打てることを確認します。

terraform plan -var="env=stg"

おわりに

本記事では、Terraformのディレクトリ構成と初期設定についてまとめました。

CI/CDの構築については以下で紹介しています。

TerraformのCI/CDをGitHub Actionsで実現するはじめに いざTerraformでインフラを管理しようとなった際に、どうすれば安全なパイプラインを構築できるものか、迷うことかと思います...
Terraform用のCI構築 - plan結果をPRのコメントに貼り付けるはじめに Terraformを利用している以上、planの実行結果を確認するフローをCIに組み込むことは避けては通れません。 &qu...

ここまで読んでいただきありがとうございます。