Phase 4:3 层架构¶
目标¶
做一个接近正式环境的结构:VM 不要公网 IP,入口只有 LB,DB 用托管服务。
架构¶
flowchart LR
PC[本机 my_ip] -->|:80| LB
CS[Cloud Shell] -->|SSH| BAS[OCI Bastion]
subgraph VCN["VCN 10.4.0.0/16"]
subgraph PUB["public 10.4.1.0/24"]
LB["LB flexible 10Mbps<br/>lb-nsg"]
end
subgraph APP["app 10.4.2.0/24(没有公网 IP)"]
VM1["app-1<br/>app-nsg"]
VM2["app-2<br/>app-nsg"]
BAS
end
NAT[NAT GW]
SGW[Service GW]
end
LB -->|:80| VM1 & VM2
BAS -->|:22| VM1 & VM2
VM1 & VM2 -->|0.0.0.0/0| NAT --> Internet((Internet))
VM1 & VM2 -->|Oracle Services| SGW --> ADB[(Autonomous DB<br/>Always Free)]
分 4 步 apply:① 网络 → ② VM + Bastion → ③ LB → ④ ADB。每一步验证完再进下一步。
路由和访问控制一览¶
| 流量 | 路径 | 控制 |
|---|---|---|
| 本机 → Web | Internet → LB(public)→ VM:80 | lb-nsg(my_ip)→ app-nsg(来源 = lb-nsg) |
| 运维 SSH | Cloud Shell → Bastion → VM:22 | app-nsg(来源 = Bastion 的私有 IP)+ session 公钥 |
| VM → Internet | rt-private 0.0.0.0/0 → NAT GW |
只出不进 |
| VM → ADB / OCI 的 dnf 仓库 | rt-private Oracle Services → Service GW | ADB 的 ACL(VCN OCID) |
① 网络:NAT GW 和 Service GW¶
# Private Subnet 用的 Route Table
route_rules {
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
network_entity_id = oci_core_nat_gateway.this[0].id
}
route_rules {
destination = data.oci_core_services.all[0].services[0].cidr_block
destination_type = "SERVICE_CIDR_BLOCK" # 目的地不是 CIDR,而是"服务"
network_entity_id = oci_core_service_gateway.this[0].id
}
- NAT GW:Private VM 只能从里往外连 Internet(≒ AWS 的 NAT Gateway)
- Service GW:不经过 Internet,走 Oracle 内部网络访问 OCI 的服务(≒ AWS 的 VPC Endpoint)
- 目的地是
all-kix-services-in-oracle-services-network这样的服务名,实际 IP 段由 OCI 维护(kix= 大阪的 region key) - 选
All ... Services In Oracle Services Network(另一个选项只有 Object Storage,ADB 用不了)
- 目的地是
- 两条规则都匹配时,走更具体的那条(最长前缀匹配)→ Oracle 服务的流量走 SGW
② VM ×2 + Bastion¶
- NSG 按层建:两台 app VM 共用一个
app-nsg。module 只接收nsg_ids -
SSH 规则的来源写成 Bastion 的属性,不写死 IP:
-
VM 要开启 Bastion plugin(Oracle Cloud Agent):
-
Bastion 的
client_cidr_block_allow_list = ["0.0.0.0/0"]:Cloud Shell 的 IP 不固定。安全性靠 IAM + session 的公钥 - session 不放进 Terraform:最长 3 小时就过期,放进去每次 plan 都会出差异 → 用的时候在 Cloud Shell 里建
用 Bastion 登录(Cloud Shell)¶
ssh-keygen -t rsa -b 4096 -f ~/.ssh/bastion_key -N ""
oci bastion session create-managed-ssh \
--bastion-id <Bastion OCID> --target-resource-id <VM OCID> \
--target-os-username opc --ssh-public-key-file ~/.ssh/bastion_key.pub \
--session-ttl 3600 --wait-for-state SUCCEEDED \
--query 'data.resources[0].identifier' --raw-output # 要等 1〜2 分钟
ssh -i ~/.ssh/bastion_key \
-o ProxyCommand="ssh -i ~/.ssh/bastion_key -W %h:%p -p 22 <SESSION_OCID>@host.bastion.ap-osaka-1.oci.oraclecloud.com" \
-p 22 opc@<VM 的私有 IP>
验证:出口真的走 NAT GW 吗?¶
| 在 VM 上执行 | 结果 | 说明 |
|---|---|---|
curl ifconfig.me |
NAT GW 的公网 IP | 出 Internet 走 NAT GW ✅ |
dnf install |
成功 | 证明不了 NAT GW:仓库是 yum.ap-osaka-1.oci.oraclecloud.com,属于 Oracle Services,走的是 Service GW |
③ LB¶
shape = "flexible"、min = max = 10Mbps(variable 的 validation 防止写成别的值)-
Backend 的 IP 从 module 的 output 取:
for_each 的 key(app-1 / app-2)在 plan 时就确定了,value(IP)可以是 apply 后才知道的值
-
NSG 互相引用(
source_type = "NETWORK_SECURITY_GROUP"):app-nsg 只放行"来自 lb-nsg"的 80 → LB 的 IP 变了、VM 增减了都不用改规则 - 验证:访问 6 次,app-1 / app-2 交替返回 → ROUND_ROBIN 生效
- 同一个 LB 的变更(Backend Set / Backend / Listener)是排队执行的,不是并行,每个十几到三十秒
④ Autonomous DB¶
is_free_tier = true写死,output 再确认一次是true-
Always Free ADB 不支持 private endpoint → public endpoint + ACL:
-
is_mtls_connection_required = false(设了 ACL 才能关)→ 不用 wallet,TLS 连接串直接连 - ADMIN 密码用
random_password生成 → state 里是明文;output 设sensitive = true
验证:流量真的走 Service GW 吗?¶
在 VM 上用 Python(pip install oracledb,thin mode 不需要 Instant Client)连 ADB:
结果是 VM 的私有 IP(不是 NAT GW 的公网 IP)→ 流量经过 Service GW,ACL 靠 VCN OCID 放行 ✅
Always Free ADB 的生命周期
7 天没人用会自动停止,停止 90 天后会被删除。
坑¶
Cloud Shell 是 FIPS 模式
ssh-keygen -t ed25519 → ED25519 keys are not allowed in FIPS mode → 用 RSA。
Resource Search 的延迟
destroy 后马上用 Resource Search 查,Bastion 还显示 ACTIVE。直接调用 get_bastion 已经是 DELETED。
搜索索引有延迟,以 API 直接查到的结果为准。
完整代码¶
04-3tier/env/main.tf
# ------------------------------------------------------------
# Phase 4:3 层结构(LB → Private VM ×2 → Autonomous DB)
# 分步骤 apply:① 网络 → ② VM + Bastion → ③ LB → ④ ADB
# ------------------------------------------------------------
locals {
name_prefix = "tflab4"
vcn_cidr = "10.4.0.0/16"
common_tags = {
project = "tf-lab"
managed_by = "terraform"
phase = "4"
}
# app VM:加一条就多建一台(Always Free 上限:A1 合计 4 OCPU / 24GB,boot volume 合计 200GB)
# 现在:2 台 × (1 OCPU / 6GB / 50GB) = 2 OCPU / 12GB / 100GB
app_instances = toset(["app-1", "app-2"])
}
# ---- ① 网络 ----
# public:放 LB(走 IGW)
# app :放 VM(没有公网 IP。出外网走 NAT GW,访问 ADB 走 Service GW)
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.4.1.0/24", dns_label = "public", is_public = true }
app = { cidr = "10.4.2.0/24", dns_label = "app", is_public = false }
}
# SSH 只从 VCN 内部来(Bastion 的 private endpoint 在 VCN 里)
ssh_allowed_cidrs = [local.vcn_cidr]
freeform_tags = local.common_tags
}
# ---- ② app VM ×2(NSG 在 security.tf,Bastion 在 bastion.tf)----
module "app" {
source = "../modules/compute"
for_each = local.app_instances
compartment_ocid = var.compartment_ocid
name = "${local.name_prefix}-${each.key}" # tflab4-app-1 / tflab4-app-2
subnet_id = module.network.subnet_ids["app"]
nsg_ids = [oci_core_network_security_group.app.id]
assign_public_ip = false
enable_bastion_plugin = true
ssh_public_key = file(var.ssh_public_key_path)
user_data = file("${path.module}/cloud-init-app.yaml")
freeform_tags = local.common_tags
}
# ---- ③ LB(Public Subnet,后端 = app VM ×2)----
module "lb" {
source = "../modules/lb"
compartment_ocid = var.compartment_ocid
name = "${local.name_prefix}-lb"
subnet_ids = [module.network.subnet_ids["public"]]
nsg_ids = [oci_core_network_security_group.lb.id]
# for 表达式:module.app(map)→ { app-1 = "10.4.2.139", app-2 = "10.4.2.11" }
backends = { for k, m in module.app : k => m.private_ip }
freeform_tags = local.common_tags
}
04-3tier/env/security.tf
# ------------------------------------------------------------
# NSG:按"层"建,同一层的 VM 共用
# lb-nsg :LB
# app-nsg:app VM ×2
# 规则里可以写"来源/目的地 = 另一个 NSG"(source_type = NETWORK_SECURITY_GROUP)
# → 不用写 IP,VM 增减、IP 变化都不用改规则
#
# my_ip ──:80──→ [lb-nsg] LB ──:80──→ [app-nsg] VM ×2 ←──:22── Bastion
#
# checkov CKV_OCI_21(要求 ingress 规则用 stateless)→ 有意不改,在各规则里用 skip 注释写明理由
# stateless = 不记录连接状态,返回流量要另写规则放行(规则翻倍、易配错);优点是大流量时性能好
# 这里流量很小,用默认的 stateful
# ------------------------------------------------------------
# ---- lb-nsg ----
resource "oci_core_network_security_group" "lb" {
compartment_id = var.compartment_ocid
vcn_id = module.network.vcn_id
display_name = "${local.name_prefix}-lb-nsg"
freeform_tags = local.common_tags
}
# 入:只允许本机的 IP 访问 LB 的 80
resource "oci_core_network_security_group_security_rule" "lb_http_from_my_ip" {
#checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
network_security_group_id = oci_core_network_security_group.lb.id
direction = "INGRESS"
protocol = "6"
source = "${var.my_ip}/32"
source_type = "CIDR_BLOCK"
description = "HTTP from my IP"
tcp_options {
destination_port_range {
min = 80
max = 80
}
}
}
# 出:LB → app VM 的 80(转发请求 + 健康检查)
# (Subnet 的 Security List 已经允许全部出站,这条不写也能通。写出来是为了让 NSG 自己就能说明"LB 只往 app 发")
resource "oci_core_network_security_group_security_rule" "lb_http_to_app" {
network_security_group_id = oci_core_network_security_group.lb.id
direction = "EGRESS"
protocol = "6"
destination = oci_core_network_security_group.app.id
destination_type = "NETWORK_SECURITY_GROUP"
description = "HTTP to app"
tcp_options {
destination_port_range {
min = 80
max = 80
}
}
}
# ---- app-nsg ----
resource "oci_core_network_security_group" "app" {
compartment_id = var.compartment_ocid
vcn_id = module.network.vcn_id
display_name = "${local.name_prefix}-app-nsg"
freeform_tags = local.common_tags
}
# SSH 只接受 Bastion 来的连接
# Bastion 在 app Subnet 里有一个 private endpoint(私有 IP),SSH 会从这个 IP 进来
resource "oci_core_network_security_group_security_rule" "app_ssh_from_bastion" {
#checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
network_security_group_id = oci_core_network_security_group.app.id
direction = "INGRESS"
protocol = "6"
source = "${oci_bastion_bastion.this.private_endpoint_ip_address}/32"
source_type = "CIDR_BLOCK"
description = "SSH from Bastion"
tcp_options {
destination_port_range {
min = 22
max = 22
}
}
}
# HTTP 只接受 LB 来的连接(来源写 lb-nsg,不写 IP)
# app VM 之间、Bastion → VM 的 80 仍然不通
resource "oci_core_network_security_group_security_rule" "app_http_from_lb" {
#checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
network_security_group_id = oci_core_network_security_group.app.id
direction = "INGRESS"
protocol = "6"
source = oci_core_network_security_group.lb.id
source_type = "NETWORK_SECURITY_GROUP"
description = "HTTP from LB"
tcp_options {
destination_port_range {
min = 80
max = 80
}
}
}