🎉

Terraform cloud 使い出し(2) - モジュール化と再利用、その他(ローカルてplanする時はtokenを呼びこんでこよう)

2024/12/10に公開

まあzenn.devの記事を書き始めようと思ったのは初心者向けチュートリアルが書きたいのではなくて、こういうニッチな記事を放りこむ用途だったのを思い出しためり。

現在の/main.tf

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

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "main-vpc"
  }
}

このようになっている。もうちょっと追加してInternet Gatewayを置いてみると

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

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "main-vpc"
  }
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw"
  }
}

こんな風になる。ここでVPCの作成だけを関数にすると考えたとき、どのようにするかというと疑似的な関数でいうならば

function VPCの作成(cidr_block = "10.0.0.0/16") {
   vpc = VPC.create(cidr_block = "10.0.0.0/16")
   return vpc.id
}

ま、みたいな、こんな感じのイメージになるんじゃないすか?そうすると問題なのは引数と返り値ってことになる。ここでの引数はcidr_blockであり、返り値は作成済みvpcのidだ。このidは

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id # ここ

  tags = {
    Name = "main-igw"
  }
}

で使う必要があるので必ず返却しないといけない

モジュール化

ここでnetworkという名前のモジュールを作るとする。

modules/network にモジュールを作成するので、まずディレクトリを切ったのち

modules/network/variables.tf

variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "vpc_name" {
  description = "Name tag for the VPC"
  type        = string
  default     = "main-vpc"
}

というファイルで引数の定義を行っている。次に

modules/network/main.tf

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = var.vpc_name
  }
}

で実際のVPCを作成し、最後に
modules/network/outputs.tf

output "vpc_id" {
  value       = aws_vpc.main.id
  description = "ID of the created VPC"
}

として返却する。

main.tfを修正する

まず

resource "aws_vpc" "main" {

とかの処理はモジュールに任せているので必要ない。main.tfからはnetworkモジュールをコールする事を記述しておく。ここで引数も与えている

/main.tf

# ...
module "network" {
  source   = "./modules/network"
  vpc_cidr = "10.0.0.0/16" # ← 引数
  vpc_name = "example-vpc" # ← 引数
}
resource "aws_internet_gateway" "main" {
  vpc_id = module.network.vpc_id

  tags = {
    Name = "main-igw"
  }
}

この引数はvariableのキーと一致させないといけない

さらにIGWのvpc id参照がちょっと変わってくるのでこれも修正している。いずれにせよこの例は簡単なモジュール作成の例なのでここまでは難なくクリアできるはず、だ。


こんな感じで変更がかかる

なお

  • modules/network/main.tf
  • modules/network/outputs.tf
  • modules/network/variables.tf

のようなファイル構成を取らなくても1つのファイルにまとめても別にいいすよ。

モジュールのレジストリ化

organization、つまりワークスペースの上位の設定項目にRegistryというのがある。ここでは

このようにパブリッシュできるようになっている。これに関してはgitレポジトリをモジュールごと1つ作成し置いていくという、そういった機能になるため、ここでは紹介に留めておく。モジュールを汎用化して作成していったときに複数のワークスペースあるいはオーガニゼーションで利用したくなった時にこれを使う事になる。

vimで編集する場合

  Plug 'hashivim/vim-terraform'

みたいなプラグインを入れといて

" Enable Terraform filetype
autocmd BufRead,BufNewFile *.tf set filetype=terraform

" Enable automatic formatting with terraform fmt
let g:terraform_fmt_on_save=1

みたいにしておくとフォーマットに統一が取れてよいと思う。特に複数人でtfを弄るのを前提とする場合。vim以外はまあ頑張って、気になるならCI/CDとかするのがいいかもしれん。やったことはない。

TFCのユーザー情報を共有してローカルでplanしたい!

これはユーザーのtokenを得る事でできる。たとえば

data "aws_availability_zones" "available" {}
resource "aws_subnet" "public" {
  for_each = toset(slice(data.aws_availability_zones.available.names, 0, 2)) 

  vpc_id                  = aws_vpc.main.id
  availability_zone       = each.key
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, index(slice(data.aws_availability_zones.available.names, 0, 2), each.key))
}

みたいな

for_each = toset(slice(data.aws_availability_zones.available.names, 0, 2))

とかわかんねえよそもそもdata.aws_availability_zones.available.namesって何が入ってんだよみたいなとき、これをローカルで確認したいとしよう。この際

/main.tf

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

module "network" {
  source   = "./modules/network"
  vpc_cidr = "10.0.0.0/16"
  vpc_name = "example-vpc"
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "main-vpc"
  }
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "main" {
  vpc_id = module.network.vpc_id

  tags = {
    Name = "main-igw"
  }
}

# 探る
data "aws_availability_zones" "available" {}
output "availability_zone_slice" {
  value = slice(data.aws_availability_zones.available.names, 0, 2)
}

こんな感じで何やってんのかoutputで探りたいとする。これをコミット&プッシュして確認してもいいんだけど、このためにcommitしたくねえなと普通思うよね?そういう場合にこれを使うといいかも。

設定

organizationの設定でAPI Tokenをみつけてくる

Userのタブに移動するとアカウントからtokenを作れといわれるが、まあそうしよう

するとこんな感じの取扱厳重注意みたいな感じになっているがCreate an API tokenを押すとモーダルウインドウがあらわれて

まこんな感じのノリでセットする。Terraform Cloudを通じてできる事はほぼできるため取扱を厳重に注意しないといけない、が、

こんな感じでトークンができあがってくる。これをtfを編集しているホストの

$HOME/.terraformrc

credentials "app.terraform.io" {
  token = "****"
}

みたいな感じで配置する。その後tfをいじってるレポジトリでbackend.tfなんかを作成し、どこを弄ってるのか伝えてあげる。

backend.tf
terraform {
  backend "remote" {
    organization = "test_for_zenn_doc"

    workspaces {
      name = "prod"
    }
  }
}

そしたら

 terraform init

する

% terraform init
Initializing the backend...

Successfully configured the backend "remote"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing modules...
- network in modules/network
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v5.80.0...
- Installed hashicorp/aws v5.80.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

みたいな感じになる。

planしてみる

じゃあ、やってみよう

terraform plan

とすると

data.aws_availability_zones.available: Reading...
aws_vpc.main: Refreshing state... [id=vpc-05fa1f9ecec2a85ef]
module.network.aws_vpc.main: Refreshing state... [id=vpc-08465daaa526afe46]
data.aws_availability_zones.available: Read complete after 0s [id=ap-northeast-1]
aws_internet_gateway.main: Refreshing state... [id=igw-04955870e6491de92]

Changes to Outputs:
  + availability_zone_slice = [
      + "ap-northeast-1a",
      + "ap-northeast-1c",
    ]

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

みたいな感じなって

  • ap-northeast-1a
  • ap-northeast-1c

が入ってることがわかる。なお、通信しなくていいようなやつはterraform consoleでも確認できますからね

% terraform console
> cidrsubnet("10.0.0.0/16", 8, 0)
"10.0.0.0/24"
> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"
> exit

ちなみにここからapplyすると

% terraform apply
╷
│ Error: Apply not allowed for workspaces with a VCS connection
│
│ A workspace that is connected to a VCS requires the VCS-driven workflow to ensure that the VCS remains the single
│ source of truth.
╵

このようにワークスペースがVCS接続限定になってるのでエラーになる。plan専用と考えるならこれはこれで良好な事だと思う。ま、そういう風に思うならTokenの生存時間は1日とかでなくある程度伸ばしてもいいのかもしれないね。

どうしても弄りたいならこの辺。いい未来が待ってるかどうかは知らんけど...

事後: gitの状態

# Terraform関連の不要なファイル・ディレクトリ
.terraform/
.terraform.lock.hcl

# Terraformバックエンド設定ファイル
backend.tf

# その他の可能性のある一時ファイル
*.tfstate
*.tfstate.backup

chatgpt氏による .gitignore の提案

Discussion