跳转至

Phase 1:网络(VCN)

目标

  • 用 Terraform 建一套最小的网络,熟悉 init → plan → apply → destroy 的流程
  • 理解 state 是什么:Terraform 怎么知道"哪些资源是我建的"

网络资源(VCN、Gateway、Route Table 等)本身全部免费,适合用来练手。

架构

flowchart LR
  Internet((Internet)) <--> IGW[Internet Gateway]
  subgraph VCN["VCN 10.0.0.0/16"]
    IGW --- RT["Route Table<br/>0.0.0.0/0 → IGW"]
    RT --- SN["Public Subnet 10.0.1.0/24"]
    SL["Security List<br/>SSH: my_ip / VCN 内部<br/>ICMP 3/4"] --- SN
  end

和 AWS 的对应

OCI AWS 说明
VCN VPC
Internet Gateway Internet Gateway
Route Table Route Table
Security List NACL(但默认是 stateful) Subnet 级别的防火墙
NSG(Network Security Group) Security Group VNIC 级别的防火墙,Phase 4 开始用
Compartment (近似)Resource Group / 账号划分 权限和费用的边界

要点

引用 = 依赖关系

resource "oci_core_internet_gateway" "igw" {
  vcn_id = oci_core_vcn.main.id   # 引用 VCN 的属性
}

引用了别的资源的属性,Terraform 就知道要先建 VCN 再建 IGW,删的时候反过来。不用自己写顺序。

Security List 的写法

  • protocol 用协议号:"6" = TCP、"1" = ICMP、"all" = 全部
  • SSH 只放行本机 IP("${var.my_ip}/32")
  • ICMP type 3 code 4(Path MTU Discovery)要放行,否则大包会出问题。OCI 默认的 Security List 里也有这一条

标签

所有资源都打上同样的 freeform_tags,在 Console 上一眼就能看出是 Terraform 管理的:

locals {
  common_tags = {
    project    = "tf-lab"
    managed_by = "terraform"
  }
}

state 是什么

  • terraform apply 后生成 terraform.tfstate:记录"代码里的资源 ↔ OCI 上实际的 OCID"
  • plan 时 Terraform 做三方比较:代码 ↔ state ↔ 实际资源
  • state 丢了,Terraform 就不认识已经建好的资源了,会想再建一套
  • 一个目录(root module)一份 state → 01-network 和 02-compute 要分别 apply,建的时候 01 → 02,删的时候 02 → 01
plan 里的符号 意思
+ 新建
~ 原地修改(OCID 不变)
-/+ 删掉重建(# forces replacement,OCID 会变)
- 删除

Note

资源还不存在时,不管怎么改代码,plan 都只显示 +。~ 和 -/+ 要等资源建好之后才会出现。

这一阶段的 state 放在本机。多人操作、防丢失的问题在 Phase 3 用 remote state 解决。

实际操作

1. 先 plan,再 apply

cd 01-network
terraform init
terraform plan      # → Plan: 5 to add, 0 to change, 0 to destroy.
terraform apply     # 再显示一次 plan,输入 yes 才执行

看 plan 的要点:

  • 先看最后一行 Plan: X to add, Y to change, Z to destroy。出现了没想到的 change 或 destroy,就先停下来
  • (known after apply):OCID 这类值要等资源建好才知道
  • 输出太长时:terraform plan -no-color > plan.txt 保存下来慢慢看

apply 的结果是 5 added,创建顺序是:

VCN → IGW 和 Security List(同时建,它们都只依赖 VCN)→ Route Table → Subnet

顺序是 Terraform 根据引用关系自动算出来的。删除时正好相反。

2. 资源还不存在时改代码

apply 之前把 project_name 改掉再 plan,符号还是 +,只有 display_name 的值变了。

→ state 是空的,比较对象不存在,所以不管怎么改都是"新建"。~ 和 -/+ 只有在资源建好之后才会出现。

3. drift:在 GUI 里手动改资源

  1. 在 Console 上给 Security List 手动加一条 Ingress Rule(0.0.0.0/0、TCP 80)
  2. terraform plan:

    ~ resource "oci_core_security_list" "public" {
        - ingress_security_rules {
            - protocol = "6" -> null
            - source   = "0.0.0.0/0" -> null
            ...
          }
          # (3 unchanged blocks hidden)
      }
    Plan: 0 to add, 1 to change, 0 to destroy.
    
    • ~ = 原地修改,- ingress_security_rules = 要删掉 GUI 里加的那条规则
    • 旧值 -> null = 这个值要被删掉
    • Terraform 在 plan 时先读一遍实际状态(refresh),再和代码比较。代码里没有这条规则,所以要把实际环境改回代码的样子
  3. 处理方法二选一:

    方法 做法 这次
    撤销手动改动 terraform apply ✅ 选这个 → 再 plan 是 No changes
    把改动收进代码 把规则写进 main.tf → plan 变成 No changes(不用 apply)

IaC 的基本原则

变更都通过代码来做,不在 GUI 里手动改。手动改了,下次 apply 时会被代码覆盖回去。

4. destroy 和重建

terraform destroy       # 先确认是 5 to destroy,再输入 yes
terraform state list    # → 空
terraform apply         # 建回来(Phase 2 的 VM 要放在这个 Subnet 里)

重建后 OCID 全部变了:Terraform 管理的是"代码里的地址 ↔ OCID"的对应关系,OCID 本身每次都是新的。

完整代码

01-network/main.tf
# ------------------------------------------------------------
# Phase 1:只建网络(VCN 及相关网络资源都免费)
#   VCN ─ Internet Gateway
#    └─ Public Subnet ─ Route Table(0.0.0.0/0 → IGW)
#                     └─ Security List(SSH 只放行本机 IP)
# ------------------------------------------------------------

locals {
  # 所有资源共用的标签,方便在 GUI 里筛选、确认是 Terraform 管理的资源
  common_tags = {
    project    = "tf-lab"
    managed_by = "terraform"
  }
}

# VCN:相当于 AWS 的 VPC
resource "oci_core_vcn" "main" {
  compartment_id = var.compartment_ocid
  cidr_blocks    = [var.vcn_cidr]
  display_name   = "${var.project_name}-vcn"
  dns_label      = var.project_name # 只能用字母和数字,以字母开头,15 个字符以内

  freeform_tags = local.common_tags
}

# Internet Gateway:Public Subnet 访问互联网的出入口
resource "oci_core_internet_gateway" "igw" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id # 引用其他资源的属性 → Terraform 会自动判断创建顺序
  display_name   = "${var.project_name}-igw"
  enabled        = true

  freeform_tags = local.common_tags
}

# Route Table:默认路由指向 IGW
resource "oci_core_route_table" "public" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "${var.project_name}-rt-public"

  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_internet_gateway.igw.id
  }

  freeform_tags = local.common_tags
}

# Security List:Subnet 级别的防火墙
resource "oci_core_security_list" "public" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "${var.project_name}-sl-public"

  # 出站:全部放行
  egress_security_rules {
    destination = "0.0.0.0/0"
    protocol    = "all"
  }

  # 入站:SSH 只允许本机 IP(protocol 用协议号表示:6=TCP, 1=ICMP)
  ingress_security_rules {
    source   = "${var.my_ip}/32"
    protocol = "6"
    tcp_options {
      min = 22
      max = 22
    }
  }

  # 入站:SSH 允许 VCN 内部(Cloud Shell 的 Private Network 会拿到这个 VCN 里的私有 IP)
  ingress_security_rules {
    source      = var.vcn_cidr
    protocol    = "6"
    description = "SSH from inside VCN (Cloud Shell private network)"
    tcp_options {
      min = 22
      max = 22
    }
  }

  # 入站:ICMP type 3 code 4(Path MTU Discovery 需要,OCI 默认 Security List 里也有)
  ingress_security_rules {
    source   = "0.0.0.0/0"
    protocol = "1"
    icmp_options {
      type = 3
      code = 4
    }
  }

  freeform_tags = local.common_tags
}

# Public Subnet:Regional Subnet(不指定 AD,可以跨 AD 使用)
resource "oci_core_subnet" "public" {
  compartment_id             = var.compartment_ocid
  vcn_id                     = oci_core_vcn.main.id
  cidr_block                 = var.public_subnet_cidr
  display_name               = "${var.project_name}-subnet-public"
  dns_label                  = "public"
  route_table_id             = oci_core_route_table.public.id
  security_list_ids          = [oci_core_security_list.public.id]
  prohibit_public_ip_on_vnic = false # false = 允许分配公网 IP(Public Subnet)

  freeform_tags = local.common_tags
}