TerraformのWrapperツール、Terragrunt導入の検証を行いました。

April 22, 2023

Intro

  • Terraform の Wrapper ツール、Terragrunt 導入の検証を行いました。

導入の検証

  • 通常、私は Terraform を使用していますが、次のような不便さがありました:
    • 各デプロイの影響範囲を最小限に抑えるために、Git リポジトリをより小さなものに分割する必要がありました。Terraform のバージョンアップが面倒でした。
    • コードからリソースの依存関係を理解するのが難しかったです。
      • depends_on が明示的に使用できない場所でデプロイ順序に注意する必要がありました。

導入後の課題

  • 導入に関して、以下の 2 点について検証が保留されています。もし詳しい方がいたら相談に乗っていただけると幸いです。
    • Terraform の管理だけでなく、Terragrunt のバージョン管理も行う必要があります。バージョンアップのプロセスはどのように変わるのか
    • もし Terragrunt の開発が停止した場合、スムーズに Terraform のコードに戻せるか

技術要素

  • terraform
  • terragrunt
  • aws

Terragrunt とは

Terragrunt は、Terraform を使用してインフラストラクチャを管理するための作業を簡素化するための Terraform Wrapper として知られるオープンソースのツールです。Terragrunt は Terraform が提供する機能を拡張し、モジュールのコード再利用、柔軟な構成、再利用性の向上を可能にします。特に、Terraform を使用して複数の環境やアカウントを管理する際に Terragrunt は非常に便利です。

インストール

$ brew install tfenv # Installs Terraform (version specified in .terraform-version)
$ brew install tgenv # Installs Terragrunt (version specified in .terragrunt-version)

### フォルダ構成

このフォルダ構成は、他の記事を参考に作成されました。この構造の利点は、各環境ごとに異なる変数を指定できるため、コードの再利用性が向上することです。

$ tree .
.
├── README.md
├── docs
│   └── graph-dependencies.png
├── envs
│   ├── prod
│   │   ├── ResourceGroupA
│   │   │   └── terragrunt.hcl
│   │   ├── ResourceGroupB
│   │   │   └── terragrunt.hcl
│   │   └── env.hcl
│   └── terragrunt.hcl
└── modules
    ├── ResourceGroupA
    │   └── xx.tf
    └── ResourceGroupB
        └── xx.tf

共通ファイル(envs/terragrunt.hcl)

このファイルには、バックエンドとして AWS S3 バケットに*.tfstate ファイルを保存するための設定が含まれており、各環境に独自の*.tfstate ファイルを S3 バケット内に保存することができます。また、AWS リソースを作成するために Terraform が使用するプロバイダの定義も含まれています。設定では、各 AWS リージョンごとに異なるプロバイダ設定が指定されています。

*.tfstate ファイルを使用してリソースの状態を管理することで、複数の人がプロジェクトに取り組んでいても競合を回避することができます。

# Configuration for storing *.tfstate files for each environment
remote_state {
  backend = "s3"
  config = {
    bucket = "tfstate-xxxxxxxxxxxxxx"
    # Stored in `stg/modA.tfstate`
    key    = "${path_relative_to_include()}.tfstate"
    region = "ap-northeast-1"
    encrypt  = true
  }
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
terraform {
  required_version = ">= 1.3.7"
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.53.0"
    }
  }
}

# Tokyo region
provider "aws" {
  region   = "ap-northeast-1"
  alias  = "tokyo"
}
# Virginia region
provider "aws" {
  region  = "us-east-1"
  alias = "virginia"
}
# ... Other regions
EOF
}

環境固有のファイル(envs/prod/env.hcl)

このファイルは、Terragrunt の環境固有のファイルであり、prod 環境の設定が含まれています。これらの変数は、他の Terragrunt ファイルで使用され、環境固有の設定を定義するために使用されます。具体的には、envs/prod/ResourceGroupA の hcl ファイルで呼び出され、Terraform コードの module/ResourceGroupA にパラメータとして渡されます。

locals {
  ENV    = "prod"
  PROJECT_NAME = "test"
  ACCOUNT_ID   = "123456789"
  VPC_CIDE     = "10.0.0.0/24"
}

各リソースグループのための HCL ファイル

「ResourceGroupA」と「ResourceGroupB」の HCL ファイルの例を示しましょう。

最初は依存関係がないと仮定し、A というクリーンなスレートのシナリオを考えます。B は A に依存するリソースグループとします。

  • 「ResourceGroupA」の HCL ファイルの作成
    • vim envs/prod/ResourceGroupA/terragrunt.hcl
    • env.hcl から変数を受け取り、それらをモジュールに値として渡します。
locals {
  ENV = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}

# Include definition of all environments (envs/terragrunt.hcl)
include {
  path = find_in_parent_folders()
}

terraform {
  # Reference the module
  source = "../../../modules//ResourceGroupA"
}

# Specify the input values for the module
inputs = {
  ENV          = local.ENV.locals.ENV
  PROJECT_NAME = local.ENV.locals.PROJECT_NAME
  ACCOUNT_ID   = local.ENV.locals.ACCOUNT_ID
}
  • 「ResourceGroupB」のファイルの作成:
    • vim envs/prod/ResourceGroupB/terragrunt.hcl
    • env.hcl から変数を取得し、それらをモジュールに渡します。
    • この例では、ResourceGroupA で作成した VPC の ID を ResourceGroupB に渡すことを想定しています。
locals {
  ENV = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}

# Include definition of all environments (envs/terragrunt.hcl)
include {
  path = find_in_parent_folders()
}

terraform {
  # Reference the module
  source = "../../../modules//ResourceGroupB"
}

# Specify the input values for the module
inputs = {
  ENV          = local.ENV.locals.ENV
  PROJECT_NAME = local.ENV.locals.PROJECT_NAME
  ACCOUNT_ID   = local.ENV.locals.ACCOUNT_ID
  VPC_ID       = local.ENV.locals.VPC_ID
}

モジュールの tf ファイル

  • 一般的には、通常どおり Terraform のコードを記述できます。
  • 他のモジュールからパラメータを受け取りたい場合(依存関係がある場合や env.hcl からパラメータを渡す場合)、変数ブロックで空の変数を定義する必要があります。上記で言及した ResourceGroupB の例では、次のようになります。
variable "ENV" {
  description = "Environment"
  type = string
}
variable "PROJECT_NAME" {
  description = "Project Name"
  type = string
}
variable "vpc_id" {
  description = "The ID of the VPC"
  type = string
}
  • 1 つのリソースグループから別のリソースグループにパラメータを渡す場合は、それらを出力として定義する必要があります。例えば、Resource Group A の場合、以下のように定義する必要があります:
output "vpc_id" {
  value = aws_vpc.main.id
}

Deployment

Terragrunt Commands

  • Formatting
cd envs/prod
terragrunt run-all hclfmt
terragrunt run-all fmt
  • validate
cd envs/prod
terragrunt validate-all
  • plan
cd envs/prod
terragrunt run-all plan
  • apply
cd envs/prod
terragrunt run-all apply
  • Dependency Graph
    • tips
      • Installing dot command on M1 Mac:
        • Graphviz is a graph visualization tool that includes the dot command. You can install Graphviz by running the following command in the terminal:
        • brew install graphviz
        • Verify if the dot command is installed:
          • dot -V
cd envs/terragrunt-prod
terragrunt graph-dependencies | dot -Tpng > graph-dependencies.png

Deployment from git

GithubAction

name: Terragrunt Actions

on:
  push:
    branches:
      - master

env:
  TERRAGRUNT_CACHE_DIR: ${{ github.workspace }}/tool

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      TARGET_ENV: ''
    steps:
      - name: Set Production
        run: |
          mkdir -p ${GITHUB_WORKSPACE}/deploy
          echo 'export TARGET_ENV="prod"' >> ${GITHUB_WORKSPACE}/deploy/.env
        if: github.ref == 'refs/heads/master'
        env:
          GITHUB_WORKSPACE: ${{ github.workspace }}
      - name: Set Staging
        run: |
          mkdir -p ${GITHUB_WORKSPACE}/deploy
          echo 'export TARGET_ENV="stg"' >> ${GITHUB_WORKSPACE}/deploy/.env
        if: github.ref == 'refs/heads/staging'
        env:
          GITHUB_WORKSPACE: ${{ github.workspace }}
      - name: Terragrunt Plan
        env:
          TERRAGRUNT_DOWNLOAD: "https://github.com/gruntwork-io/terragrunt/releases/download/v0.34.0/terragrunt_linux_amd64"
        run: |
          source ${GITHUB_WORKSPACE}/deploy/.env
          sudo apt-get update && sudo apt-get install -y curl unzip git
          curl -Lo /tmp/terragrunt.zip ${TERRAGRUNT_DOWNLOAD}
          sudo unzip -d /usr/local/bin /tmp/terragrunt.zip
          cd ${GITHUB_WORKSPACE}/envs/${TARGET_ENV}
          terragrunt run-all plan
        if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/staging'
        env:
          GITHUB_WORKSPACE: ${{ github.workspace }}
          TERRAGRUNT_CACHE_DIR: ${{ env.TERRAGRUNT_CACHE_DIR }}
      - name: Terragrunt Apply
        if: github.ref == 'refs/heads/master'
        env:
          TERRAGRUNT_DOWNLOAD: "https://github.com/gruntwork-io/terragrunt/releases/download/v0.34.0/terragrunt_linux_amd64"
        run: |
          source ${GITHUB_WORKSPACE}/deploy/.env
          sudo apt-get update && sudo apt-get install -y curl unzip git
          curl -Lo /tmp/terragrunt.zip ${TERRAGRUNT_DOWNLOAD}
          sudo unzip -d /usr/local/bin /tmp/terragrunt.zip
          cd ${GITHUB_WORKSPACE}/envs/${TARGET_ENV}
          terragrunt run-all apply --terragrunt-non-interactive
        env:
          GITHUB_WORKSPACE: ${{ github.workspace }}
          TERRAGRUNT_CACHE_DIR: ${{ env.TERRAGRUNT_CACHE_DIR }}

Bitbucket-Pipelines

image: hashicorp/terraform:1.3.7

definitions:
  caches:
    terragrunt: ${BITBUCKET_CLONE_DIR}/tool
  steps:
    - step: &set-production
        name: Set Production
        script:
          - mkdir -p ${BITBUCKET_CLONE_DIR}/deploy
          - echo 'export TARGET_ENV="prod"' >> ${BITBUCKET_CLONE_DIR}/deploy/.env
        artifacts:
          - deploy/**
    - step: &set-staging
        name: Set Staging
        script:
          - mkdir -p ${BITBUCKET_CLONE_DIR}/deploy
          - echo 'export TARGET_ENV="stg"' >> ${BITBUCKET_CLONE_DIR}/deploy/.env
        artifacts:
          - deploy/**
    - step: &terragrunt-plan
        name: terragrunt Plan
        caches:
          - terragrunt
        script:
          - source ${BITBUCKET_CLONE_DIR}/deploy/.env
          - apk update && apk add bash curl groff jq less unzip git
          - if [ ! -d ${BITBUCKET_CLONE_DIR}/tool/tfenv ]; then git clone https://github.com/tfutils/tfenv.git ${BITBUCKET_CLONE_DIR}/tool/tfenv; fi
          - if [ ! -d ${BITBUCKET_CLONE_DIR}/tool/tgenv ]; then git clone https://github.com/cunymatthieu/tgenv.git ${BITBUCKET_CLONE_DIR}/tool/tgenv; fi
          - export PATH=${BITBUCKET_CLONE_DIR}/tool/tfenv/bin:$PATH
          - export PATH=${BITBUCKET_CLONE_DIR}/tool/tgenv/bin:$PATH
          - cd ${BITBUCKET_CLONE_DIR}/envs/${TARGET_ENV}
          - tfenv install && tgenv install
          - terragrunt run-all plan
    - step: &terragrunt-apply
        name: terragrunt Apply
        trigger: manual
        caches:
          - terragrunt
        script:
          - source ${BITBUCKET_CLONE_DIR}/deploy/.env
          - apk update && apk add bash curl groff jq less unzip git
          - if [ ! -d ${BITBUCKET_CLONE_DIR}/tool/tfenv ]; then git clone https://github.com/tfutils/tfenv.git ${BITBUCKET_CLONE_DIR}/tool/tfenv; fi
          - if [ ! -d ${BITBUCKET_CLONE_DIR}/tool/tgenv ]; then git clone https://github.com/cunymatthieu/tgenv.git ${BITBUCKET_CLONE_DIR}/tool/tgenv; fi
          - export PATH=${BITBUCKET_CLONE_DIR}/tool/tfenv/bin:$PATH
          - export PATH=${BITBUCKET_CLONE_DIR}/tool/tgenv/bin:$PATH
          - cd ${BITBUCKET_CLONE_DIR}/envs/${TARGET_ENV}
          - tfenv install && tgenv install
          - terragrunt run-all apply --terragrunt-non-interactive

pipelines:
  default:
    - step: *set-production
    - step: *terragrunt-plan
  branches:
    master:
      - step: *set-production
      - step: *terragrunt-plan
      - step:
          <<: *terragrunt-apply
          deployment: production
Nifty tech tag lists from Wouter Beeftink