コンテンツにスキップ

Phase 3:Module / Remote State / import

目標

Phase 2 までで見えてきた 3 つの問題を解決します。

問題 対策
state(状態ファイル)が 1 台の PC にしかない(紛失のおそれ、複数人で操作できない、ロックがない) リモート state(remote state):オブジェクト・ストレージ(Object Storage)+ backend "oci"
環境を複数作るにはコードをコピー&ペーストするしかない モジュール(module) + for_each
コンソール(GUI)で手動作成したリソースを Terraform で管理できない import

構成

03-modules/
├── bootstrap/   ① state を保存するバケット(このディレクトリ自身は local state)
├── modules/     ② network / compute の部品
└── env/         ③ module を呼び出す。state はバケットに保存

① bootstrap:state を保存するバケット

  • state を保存するバケット(Bucket)は、自分自身の state を自分の中に保存できない → 別ディレクトリにして local state を使う(「鶏が先か卵が先か」問題)
  • バケットの設定:NoPublicAccess、versioning Enabled(state を壊しても旧バージョンを取り戻せる)、prevent_destroy = true(誤削除防止)
  • Always Free:オブジェクト・ストレージ 20GB + 月 5 万回の API リクエスト。state の使用量は無視できる程度

② module

modules/network:VCN / IGW / Route Table / Security List / Subnet(for_each)
modules/compute:NSG / NSG rule / VM
  • module の中には provider "oci" {} を書かず、required_providers だけを書く。認証とリージョン(Region)は呼び出し側(ルート・モジュール、root module)が決める
  • 同種のリソースが複数あるときは for_each、「0 個か 1 個か」は count(参照時は this[0])
  • ignore_changes = [source_details[0].source_id]:新しいイメージ(Image)が公開されても、data source が新イメージを返したせいで VM が再作成される、ということが起きない

for_each と count の違い

アドレス 途中の 1 つを削除したとき
for_each this["public"](key で指定) その 1 つだけが削除される
count this[0]、this[1](番号で指定) 後ろの番号がすべてずれる → 再作成される

→ 複数のリソースは原則 for_each を使います。

③ env:remote state

terraform {
  backend "oci" {}   # Terraform 1.12+ で使える OCI ネイティブの backend
}
# backend.hcl(Git には入れず、backend.hcl.example をコミット)
bucket    = "tflab-tfstate"
namespace = "<Object Storage namespace>"
key       = "03-modules/env/terraform.tfstate"
region    = "ap-osaka-1"
terraform init "-backend-config=backend.hcl"
  • backend ブロックでは variable が使えない → 値は別ファイルに書き、init 時に渡す(partial configuration)
  • 以前は S3 互換 API + 顧客秘密キー(Customer Secret Key)が必要だったが、1.12 以降は ~/.oci/config の認証をそのまま使える
  • 確認:terraform state list / terraform state pull

remote state の効果

2 人で同じ state を使う場合:A が先に apply し、そのあと B が apply すると結果は 0 changed になります。B が読むのは A が更新したあとの state だからです。 2 人が同時に apply した場合は、あとから実行したほうが state lock で止められます。

④ import:手動で作ったリソースを管理下に入れる

コンソールで NSG(ネットワーク・セキュリティ・グループ)+ ルールを手動で作成し、それを Terraform の管理下に移しました。

  1. import block を書く:

    import {
      to = oci_core_network_security_group.manual
      id = "ocid1.networksecuritygroup.oc1..xxxx"
    }
    
  2. terraform plan "-generate-config-out=generated.tf" → resource のコードが自動生成される

  3. 生成されたコードを整理する:OCID を変数や参照に置き換え、Oracle が自動で付ける defined_tags を削除し、common_tags を追加
  4. plan → 2 to import, 1 to change(タグをインプレース更新)→ apply → もう一度 plan すると No changes
  5. import block を削除する(import が終われば不要)

import は自動で検索しない

Terraform は OCI 上にどんなリソースがあるかを探しに行きません。「どの OCID がコード上のどのアドレスに対応するか」は人が指定する必要があります。 コンパートメント(Compartment)単位でまとめてエクスポートしたい場合は、OCI リソース・マネージャ(Resource Manager)のリソース検出(Resource Discovery)か Terraformer を使います。

ハマりどころ

NSG ルールの import ID がコンソールで見えない

形式は networkSecurityGroups/<NSG OCID>/securityRules/<ルール ID> ですが、ルール ID(例:675A86)はコンソールに表示されません。 → 一時ディレクトリで data source oci_core_network_security_group_security_rules を使って調べます。 import ID の形式はリソースの種類ごとに異なるので、provider ドキュメントの Import の節を確認します。

PowerShell 5.1 が引数を . の位置で分割する

-backend-config=backend.hcl が 2 つの引数に分割される → 引用符で囲んで "-backend-config=backend.hcl" にします。 -out=xxx.tfplan、-generate-config-out=generated.tf も同様です。

No configuration files

plan はルート・モジュール(env/、bootstrap/)で実行します。03-modules/ で実行するとこのエラーになります。

コード全体

03-modules/env/main.tf
# ------------------------------------------------------------
# Phase 3-③:モジュール(module)を呼び出して環境を 1 セット作成(state はオブジェクト・ストレージ(Object Storage))
#   01/02 の環境(tflab-*、10.0.0.0/16)はそのまま残し、ここでは別に tflab3-*(10.1.0.0/16)を作成
# ------------------------------------------------------------

locals {
  name_prefix = "tflab3"
  vcn_cidr    = "10.1.0.0/16"

  common_tags = {
    project    = "tf-lab"
    managed_by = "terraform"
    phase      = "3"
  }

  # 作成する VM:1 行追加するごとに 1 台増える(Always Free の上限に注意:A1 は合計 4 OCPU / 24GB、ブート・ボリューム(Boot Volume)は合計 200GB)
  instances = {
    web = { subnet = "public", assign_public_ip = true }
  }
}

module "network" {
  source = "../modules/network"

  compartment_ocid = var.compartment_ocid
  name_prefix      = local.name_prefix
  vcn_cidr         = local.vcn_cidr
  vcn_dns_label    = local.name_prefix

  subnets = {
    public  = { cidr = "10.1.1.0/24", dns_label = "public", is_public = true }
    private = { cidr = "10.1.2.0/24", dns_label = "private", is_public = false }
  }

  # 利用中のネットワークで 22 番ポートがブロックされている → VCN(仮想クラウド・ネットワーク)内部(Cloud Shell の Private Network)のみ許可
  ssh_allowed_cidrs = [local.vcn_cidr]

  freeform_tags = local.common_tags
}

# module でも for_each が使える:local.instances の件数分だけ呼び出される
#   state 上は module.compute["web"].oci_core_instance.this のようなアドレスになる
module "compute" {
  source   = "../modules/compute"
  for_each = local.instances

  compartment_ocid = var.compartment_ocid
  name             = "${local.name_prefix}-${each.key}"
  vcn_id           = module.network.vcn_id
  subnet_id        = module.network.subnet_ids[each.value.subnet]
  assign_public_ip = each.value.assign_public_ip

  ssh_public_key     = file(var.ssh_public_key_path)
  user_data          = file("${path.module}/cloud-init.yaml")
  http_allowed_cidrs = ["${var.my_ip}/32"]

  freeform_tags = local.common_tags
}