スクエニ ITエンジニア ブログ

[番外編] ハマグリ式! 運用しやすい GitHub と Terraform Cloud の設定例 ~Terraform コード付き~

はじめに

この記事を見つけたけど、後で見ようと思ったそこのあなた!

ぜひ下のボタンから、ハッシュタグ #ハマグリ式 でポストしておきましょう!

こんにちハマグリ。貝藤らんまだぞ。 今回は、ハマグリ式! 運用しやすい GitHub と Terraform Cloud の設定例 ~Terraform コード付き~ をお届けします!

番外編って?

ハマグリ式では、下記のようにレベルを設定しています。

  1. 初級者:初めてクラウドサービスを利用する人で、基本的な操作(例:ファイルの保存や、サーバーの起動)をインターフェースを通じて行うことができます。また、シンプルなセキュリティルールの設定や、一部の問題のトラブルシューティングに対応できます。
  2. 中級者:より深い知識を持ち、コードを用いて操作を自動化したり、より複雑なタスク(例:自動でサーバーの数を増減させる)を行います。また、より高度な監視や、全体のシステム設計と実装について理解があります。
  3. 上級者:幅広く深い知識を持ち、大規模で複雑なシステムを設計、実装、維持する能力があります。最先端のテクノロジーを活用し、安全性、耐障害性、効率性を最大化するためのソリューションを提供します。

今回は上記と関係の薄い GitHub、Terraform Cloud についての記事であるため、番外編に分類しています。

ハマグリ式って?

貝藤らんまが作成するブログ記事のブランド名です。あまり気にせず読み飛ばしてください。

何を書くの?

以下の通りです。

  • この記事で書くこと
    • Terraform Cloud 連携のための運用しやすい Terraform HCL 基本コード
    • Terraform Cloud 連携のための運用しやすい GitHub 基本設定
    • Terraform Cloud の運用しやすい基本設定
    • それらを実現する HCL コード
  • この記事で書かないこと
    • GitHub と Terraform Cloud の OAuth app 連携
    • Terraform HCL コードの応用、解説、正しいベストプラクティス
    • GitHub 設定の応用、解説、正しいベストプラクティス
    • Terraform Cloud 設定の応用、解説、正しいベストプラクティス

免責事項

  • この記事に書かれていることは弊社の意見を代表するものではありません。
  • この記事に書かれていることには一定の調査と検証を実施しておりますが、間違いが存在しうることはご承知おき下さい。
  • 筆者の専門外の内容については断定を避けておりますが、あらかじめ間違いが存在しうることはご承知おきください。
  • 記事の内容は、記事執筆時点 (2024/2) での情報です。ご承知おきください。

GitHub と Terraform Cloud 連携の基本

クラウドインフラの IaC 管理が一般的になりつつある昨今、コード管理・plan・レビュー・apply という一連の流れはクラウド上で実施したいですよね。

よくある例としては、GitHub と Terraform Cloud の連携でしょう。

ドキュメントを見れば設定はできるけど、どういう設定をするといいんだろう……

この記事では、実際利用したときに「こうすればよかった」とならないような運用しやすい基本設定を紹介します!

※Terraform のバージョンは 1.7.3 で検証しています。 ※クラウドサービスプロバイダとしては AWS を想定しています。

Terraform Working Directory

まずは Working Directory で適切な設定の Terraform コードを考えましょう。

結論からいうと、resource 以外は下記のようにするといいでしょう。

terraform {
    backend "remote" {
        hostname     = "app.terraform.io"
        organization = "hamaguri-corporation"
        workspaces {
          name = "hamaguri-terraform"
        }
    }
    required_providers {
        aws = {
            version = "5.36.0"
            source = "hashicorp/aws"
        }
    }
}

variable "AWS_ACCESS_KEY_ID" {}
variable "AWS_SECRET_ACCESS_KEY" {}

provider "aws" {
  region = "ap-northeast-1"
}

provider "aws" {
  alias  = "us-east-1"
  region = "us-east-1"
}

ポイントは以下です。

  1. backend は remote とし、Terraform Cloud を指定する。

  2. variable ディレクティブの記述は必要ない (無くても実行できる) のですが、Warning が発生するので provider で使用するものは書いておく。

  3. aws provider は主にメジャーバージョン間で大幅な変更がままあるのでバージョンは固定する。

ちょっと待って、Terraform Cloud 上で remote の backend ブロックが動作するの?

はい、動作します。

いやいや、にしても v1.1.0 から導入され、公式が推奨している cloud ブロックを使うべきなんじゃないですか?!

はい、使う"べき“です。

というのも cloud ブロックでは tag でのワークスペース検索など Terraform Cloud を使い回しやすい機能を使えるからです。

また公式が推奨していることもあるので今後も機能として残り、メンテナンスされていくはずでしょう。

じゃあ、なぜ backend ブロックを使っているのでしょうか?

手元で Terraform 実行するケース

結論から言うと、backend を local としてterraform initできないので、手元で Terraform 実行するケースで小回りがきかなくなってしまいます。

例えば、一度 Terraform Cloud で管理していたが多くのリソースを追加することになって apply を何度も実行する必要があり、local 実行じゃないとやってられないとき。

※Terraform Cloud 側でキューされるので、時間がかかる & 並列実行数を消費する。

例えば、terraform import する必要があるというシンプルな状況のとき。

cloud ブロックで init した Terraform Cloud から state を取得しようとしても、以下のようにうまくいきません。

$ head terraform.tf -n 2
terraform {
    backend "local" {}
$ terraform init -migrate-state

Initializing Terraform Cloud...
│ Error: Invalid command-line option
│ The -migrate-state option is for migration between state
│ backends only, and is not applicable when using
│ Terraform Cloud.
$ terraform init

Initializing the backend...
Migrating from Terraform Cloud to local state.
│ Terraform Cloud migration has additional steps,
│ configured by interactive prompts.
│ Error: Migrating state from Terraform Cloud to another backend is not yet implemented.
│ Please use the API to do this: https://www.terraform.io/docs/cloud/api/state-versions.html

not yet implementedなので、ゆくゆくは実装されるかもしれないぞ。そうしたら cloud ブロックを使おう!

Terraform Cloud の設定

Terraform Cloud の設定はドキュメント に沿って OAuth app 連携をすればこの記事では十分です。

variables は秘匿する必要に応じて sensitive 設定をしましょう。

また GitHub 側の Pull Request にトリガーするよう、一度Plan onlyで plan を流しておきましょう。

GitHub の設定

インフラ環境に変更を加える場合、Terraform の plan を確認して Terraform のコードレビューをしてから apply する、という流れが一般的でしょう。

そこで Github 側では、plan を確認する前に apply がトリガーするデフォルトブランチへ commit を取り込んでしまうことが無いような設定をしておくのが良いでしょう。

まずはデフォルトブランチ (ここでは master) に対して protection を設定しましょう。

Settings タブのメニューカラムから Branches を選択し、下図のAdd branch protection ruleボタンを押下します。

細かい入力を求められるので、以下のように設定します。

  1. Branch name patternsに制限する対象としてデフォルトブランチを入力する

  2. レビュワーを必須とするためRequire approvalsのチェックボックスにチェックを入れる

  3. Terraform Cloud のエラー時にマージできないようRequire status checks to pass before mergingのチェックボックスにチェックを入れる

  4. 他ブランチからマージした後のデフォルトブランチとの差分で、想定していないリソースに影響が出ないよう、Require branches to be up to date before mergingのチェックボックスにチェックを入れる

マージしたブランチが残ると増え続けてしまう運用をしている場合は、下図のように自動でブランチを削除するように設定するとよいでしょう。

コード

これらの GitHub・Terraform Cloud の設定は Terraform コードでも操作できます。

以下に Terraform のコードサンプルを記載します。

terraform {
  required_providers {
    github = {
      source  = "integrations/github"
      # required_status_checks ブロックでエラーが出るので古いバージョンを使用
      # Issue: https://github.com/integrations/terraform-provider-github/issues/1147
      version = "4.22.0"
    }
    tfe = {
      version = "0.52.0"
    }
  }
}

variable "github_token" {}
variable "oauth_token_id" {}

variable "AWS_ACCESS_KEY_ID" {}
variable "AWS_SECRET_ACCESS_KEY" {}
variable "DB_PASSWORD" {}

provider "github" {
  owner = "hamaguri-corporation"
  token = var.github_token
}

locals {
  project           = "hamaguri-project"
  terraform_version = "1.7.3"
  workspace = [
    "development",
    "production"
  ]
  common_files = [
    "ip_addresses.tf",
    "project.tf"
  ]
}

# GitHub

resource "github_repository" "main" {
  name       = local.project
  visibility = "private"
  # Pull Requset がマージされた後に head ブランチを自動で削除するかどうか
  delete_branch_on_merge = true
  # 最初の commit を自動でするかどうか
  auto_init = true
  /*
  auto_init を false にすると以下のようなエラーが出てしまう

  │ Error: PATCH https://api.github.com/repos/hamaguri-corporation/hamaguri-project: 422 Validation Failed [{Resource:Repository Field:default_branch Code:invalid Message:Cannot update default branch for an empty repository. Please init the repository and push first.}]
  │   with github_branch_default.main,
  │   on terraform.tf line 48, in resource "github_branch_default" "main":
  │   48: resource "github_branch_default" "main" {
  */
}

resource "github_branch_default" "main" {
  repository = github_repository.main.name
  branch     = "master"
}

resource "github_branch_protection_v3" "main" {
  repository     = github_repository.main.name
  branch         = "master"
  # わかりにくいが "Do not allow bypassing the above settings" の設定
  enforce_admins = true

  required_status_checks {
    strict = true
  }

  required_pull_request_reviews {
    dismiss_stale_reviews           = true
    required_approving_review_count = 1
  }

}

# Terraform Cloud

/*
$ tree workspace
workspace
├── development
└── production
*/

resource "tfe_project" "main" {
  organization = "hamaguri-corporation"
  name         = "hamaguri-project"
}

resource "tfe_workspace" "main" {
  for_each = toset(local.workspace)

  name         = "${local.project}-${each.key}"
  organization = "hamaguri-corporation"
  # 指定 branch への push にトリガーして自動で apply するかどうか
  # push が頻繁にあるなら true がよい
  # クリティカルな本番環境なら false がベターだがトリプルチェックともいえる
  auto_apply = false
  # トリガーするファイルを指定するかどうか。false だとあらゆるファイルの変更にトリガー
  file_triggers_enabled = true
  # terraform_remote_state を全ての workspace から受け付けるかどうか。
  global_remote_state = true
  # Pull Request に対して plan を提供するかどうか
  speculative_enabled = true
  # plan/apply に Terraform Cloud が提供する User Interface を使うかどうか
  structured_run_output_enabled = true

  terraform_version = local.terraform_version
  # Working Directory 以外にトリガーするファイル path
  trigger_prefixes = local.common_files

  working_directory = each.key

  project_id = tfe_project.main.id

  vcs_repo {
    branch         = "master"
    identifier     = "hamaguri-corporation/${local.project}"
    oauth_token_id = var.oauth_token_id
  }
}

resource "tfe_workspace_settings" "test-settings" {
  for_each       = toset(local.workspace)

  workspace_id   = tfe_workspace.main[each.key].id
  execution_mode = "remote"
}

resource "tfe_variable" "key-id" {
  for_each = toset(local.workspace)

  key          = "AWS_ACCESS_KEY_ID"
  value        = var.AWS_ACCESS_KEY_ID
  category     = "terraform"
  workspace_id = tfe_workspace.main[each.key].id
}

resource "tfe_variable" "secret-key" {
  for_each = toset(local.workspace)

  key          = "AWS_SECRET_ACCESS_KEY"
  value        = var.AWS_SECRET_ACCESS_KEY
  category     = "terraform"
  sensitive    = true
  workspace_id = tfe_workspace.main[each.key].id
}

resource "tfe_variable" "db-password" {
  for_each = toset(local.workspace)

  key          = "DB_PASSWORD"
  value        = var.DB_PASSWORD
  category     = "terraform"
  sensitive    = true
  workspace_id = tfe_workspace.main[each.key].id
}

これでマニュアルレス化と時短を同時に実現できるぞ。

まとめ

以上、ハマグリ式! 運用しやすい GitHub と Terraform Cloud の設定例 ~Terraform コード付き~ でした!

ぜひ下のボタンから、ハッシュタグ #ハマグリ式 で感想をポストしてください!

今後ともハマグリ式をどうぞよろしくお願いいたします!

この記事を書いた人

記事一覧
SQUARE ENIXでは一緒に働く仲間を募集しています!
興味をお持ちいただけたら、ぜひ採用情報ページもご覧下さい!