跳转至

Phase 3:Module / Remote State / import

目标

解决到 Phase 2 为止暴露出来的 3 个问题:

问题 对策
state 只在一台 PC 上(会丢、不能多人操作、没有锁) remote state:Object Storage + backend "oci"
要建多套环境就得复制粘贴代码 module + for_each
Console 上手动建的资源 Terraform 管不了 import

结构

03-modules/
├── bootstrap/   ① 存 state 的 bucket(这个目录自己用 local state)
├── modules/     ② network / compute 零件
└── env/         ③ 调用 module,state 放在 bucket 里

① bootstrap:存 state 的 bucket

  • 存 state 的 bucket 没法把自己的 state 存进自己 → 单独一个目录,用 local state("先有鸡还是先有蛋"问题)
  • bucket 设置:NoPublicAccess、versioning Enabled(state 改坏了可以找回旧版本)、prevent_destroy = true(防止误删)
  • Always Free:Object Storage 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]:新镜像发布后,VM 不会因为 data source 查到新镜像而被重建

for_each 和 count 的区别

地址 删掉中间一个时
for_each this["public"](按 key) 只删那一个
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 的效果

两个人用同一份 state:A 先 apply 了,B 再 apply 时结果是 0 changed,因为 B 读到的是 A 更新后的 state。 两个人同时 apply 的话,后执行的会被 state lock 挡住。

④ import:把手动建的资源纳入管理

在 Console 手动建了一个 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 在 Console 上看不到

格式是 networkSecurityGroups/<NSG OCID>/securityRules/<规则 ID>,规则 ID(例:675A86)Console 不显示。 → 在临时目录里用 data source oci_core_network_security_group_security_rules 查。 import ID 的格式每种资源都不一样,要看 provider 文档的 Import 一节。

PowerShell 5.1 把参数在 . 处拆开

-backend-config=backend.hcl 会被拆成两个参数 → 要加引号 "-backend-config=backend.hcl"。 -out=xxx.tfplan、-generate-config-out=generated.tf 也一样。

No configuration files

plan 要在 root module(env/、bootstrap/)里执行,在 03-modules/ 里执行会报这个错。

完整代码

03-modules/env/main.tf
# ------------------------------------------------------------
# Phase 3-③:调用 module 建一套环境(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:加一条就多建一台(注意 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
}