3. OCI核心网络的自助化配置

在OCI中,网络是所有计算、数据库、负载均衡与容器资源的“地基”。手工在控制台点选创建VCN、子网、网关和安全规则,短期看似直观,但一旦需要复制到测试、预生产、生产三套环境,或需要按合规要求审计“谁在什么时候改了哪条路由”,手工方式就会迅速失控。网络即代码(Network as Code)的核心,是把网络拓扑的每一个对象都写成可版本化、可评审、可重复执行的Terraform声明,让环境差异只体现在变量文件里,而不是散落在控制台的操作历史中。

一、先理解对象之间的依赖顺序

OCI的VCN不是“一个网络”,而是一组相互引用的资源集合。Terraform之所以适合它,正是因为Terraform会依据引用关系自动推导创建顺序。理解下面这条依赖链,是写出正确配置的前提:

  • VCN:定义整体地址空间,例如 10.0.0.0/16,是所有子网和网关的容器。
  • 网关:Internet Gateway(IGW)承载公网进出;NAT Gateway 让私有子网主动访问公网但不被外部主动连接;Service Gateway 让私有子网访问OCI对象存储等服务而不走公网。
  • 子网:从VCN CIDR中切分,分为公共子网(可关联IGW)和私有子网(关联NAT或Service Gateway)。
  • 路由表:决定子网流量“下一跳去哪”,必须引用已存在的网关。
  • 安全列表与NSG:安全列表作用于整个子网,NSG作用于具体VNIC,二者共同决定“谁能进来、谁能出去”。

因此,Terraform配置中网关和VCN必须先声明,路由表引用网关,子网再引用路由表和安全列表。下面这张图展示了本节要构建的典型双栈网络拓扑与依赖关系:

VCN: prod-vcn 10.0.0.0/16 Internet GW 公网进出 NAT GW 私有出公网 Service GW 访问OCI服务 Route Table 0.0.0.0/0 → 网关 公共子网 10.0.1.0/24 SL: 允许 22/80/443 私有应用子网 10.0.2.0/24 NSG: 仅来自LB 私有数据子网 10.0.3.0/24 NSG: 仅来自App 依赖顺序(Terraform 自动推导) VCN → 网关(IGW/NAT/Service) → 路由表(引用网关) → 子网(引用路由表+安全列表) → NSG(绑定VNIC) 安全列表作用于子网整体;NSG 作用于具体 VNIC,二者叠加生效,取并集允许。

二、用变量把“环境差异”抽离出来

直接写死CIDR和名称,等于把配置锁死在一个环境。推荐做法是把环境相关参数放进 terraform.tfvars,主配置只保留结构。例如:

# variables.tf
variable "compartment_id" { type = string }
variable "vcn_cidr"       { type = string  default = "10.0.0.0/16" }
variable "env"            { type = string  default = "prod" }

# terraform.tfvars(生产)
compartment_id = "ocid1.compartment.oc1..aaaa..."
vcn_cidr       = "10.0.0.0/16"
env            = "prod"

# terraform.tfvars(测试)
compartment_id = "ocid1.compartment.oc1..bbbb..."
vcn_cidr       = "10.1.0.0/16"
env            = "test"

这样同一份 main.tf 通过 -var-file 就能生成两套互不冲突的网络,CIDR不重叠,后续做对等连接或DRG互联时不会踩坑。

三、核心资源声明示例

下面给出VCN、Internet Gateway、NAT Gateway、路由表和两个子网的最小可用声明。注意 route_rules 中 network_entity_id 直接引用网关资源,Terraform会据此建立依赖:

resource "oci_core_vcn" "prod" {
  compartment_id = var.compartment_id
  cidr_blocks    = [var.vcn_cidr]
  display_name   = "${var.env}-vcn"
  dns_label      = "${var.env}vcn"
}

resource "oci_core_internet_gateway" "igw" {
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.prod.id
  display_name   = "${var.env}-igw"
  enabled        = true
}

resource "oci_core_nat_gateway" "nat" {
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.prod.id
  display_name   = "${var.env}-nat"
}

resource "oci_core_route_table" "public_rt" {
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.prod.id
  display_name   = "${var.env}-public-rt"
  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_internet_gateway.igw.id
  }
}

resource "oci_core_route_table" "private_rt" {
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.prod.id
  display_name   = "${var.env}-private-rt"
  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_nat_gateway.nat.id
  }
}

resource "oci_core_subnet" "public" {
  compartment_id    = var.compartment_id
  vcn_id            = oci_core_vcn.prod.id
  cidr_block        = cidrsubnet(var.vcn_cidr, 8, 1)   # 10.0.1.0/24
  display_name      = "${var.env}-public-subnet"
  route_table_id    = oci_core_route_table.public_rt.id
  security_list_ids = [oci_core_security_list.public_sl.id]
  dns_label         = "pub"
}

resource "oci_core_subnet" "private_app" {
  compartment_id    = var.compartment_id
  vcn_id            = oci_core_vcn.prod.id
  cidr_block        = cidrsubnet(var.vcn_cidr, 8, 2)   # 10.0.2.0/24
  display_name      = "${var.env}-app-subnet"
  route_table_id    = oci_core_route_table.private_rt.id
  security_list_ids = [oci_core_security_list.private_sl.id]
  dns_label         = "app"
}

这里用 cidrsubnet(var.vcn_cidr, 8, 1) 从VCN CIDR自动切出 /24 子网,比手写 10.0.1.0/24 更安全——当VCN换成 10.1.0.0/16 时,子网会自动变成 10.1.1.0/24,不会出现子网落在VCN之外的错误。

四、安全列表与NSG的分工

安全列表是子网级的有状态防火墙,适合定义“这个子网整体允许哪些端口”。例如公共子网只放行22、80、443:

resource "oci_core_security_list" "public_sl" {
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.prod.id
  display_name   = "${var.env}-public-sl"

  ingress_security_rules {
    protocol = "6"   # TCP
    source   = "0.0.0.0/0"
    tcp_options { min = 22  max = 22 }
  }
  ingress_security_rules {
    protocol = "6"
    source   = "0.0.0.0/0"
    tcp_options { min = 443 max = 443 }
  }
  egress_security_rules {
    protocol    = "all"
    destination = "0.0.0.0/0"
  }
}

而NSG更适合“按角色授权”。例如应用子网只接受来自负载均衡NSG的8080流量,数据子网只接受来自应用NSG的1521流量。NSG的规则引用的是另一个NSG的OCID,而不是IP段,这样即使实例IP变化,规则依然有效。实践中建议:子网级用安全列表做粗粒度兜底,实例级用NSG做细粒度角色授权,两者叠加,取允许的并集。

安全列表和NSG都是有状态的:允许了入站请求,返回流量会自动放行,不需要再写一条出站规则。但出站方向若未显式允许,主动发起的连接会被拒绝,因此私有子网通常需要一条 egress 0.0.0.0/0 或指向NAT的规则。

五、执行与验证

配置写完后,标准执行链是:

terraform init          # 下载 oci provider
terraform validate      # 语法与引用检查
terraform plan -var-file=prod.tfvars -out=prod.plan
terraform apply prod.plan

验证不能只看 apply 是否成功。建议按以下顺序确认:

  1. 在OCI控制台进入VCN详情,确认子网CIDR、路由表关联、安全列表关联与代码一致。
  2. 启动一台公共子网实例,SSH登录后 curl ifconfig.me,应返回公网IP,说明IGW和公共路由生效。
  3. 启动一台私有子网实例,执行 curl https://objectstorage.<region>.oraclecloud.com,若能返回响应,说明NAT或Service Gateway路径通。
  4. 从公网直接SSH私有子网实例应超时,说明私有子网没有公网入口,安全边界正确。
常见排障点:子网创建成功但实例无公网,先查路由表是否关联了IGW,再查安全列表出站是否放行;私有子网无法访问对象存储,检查是否配置了Service Gateway且路由表中存在指向它的规则;terraform plan 显示要重建子网,通常是因为修改了 cidr_block 或 dns_label,这两项在OCI中不可原地更新。

把上述配置纳入Git仓库后,网络变更就变成了Pull Request:评审人可以看到CIDR、路由和安全规则的每一行差异,CI可以自动跑 terraform plan 并把结果贴到PR评论中。这才是“网络即代码”真正带来的价值——不是少点几次鼠标,而是让网络拓扑具备可审计、可回滚、可复制的工程属性。