计算和负载均衡¶
一句话
把 VM 当成可以随时替换的零件:镜像不写死、配置交给 Ansible(或打进镜像)、前面放 Load Balancer(LB),一台重启或替换时服务不中断。 另外要记住:Terraform 的 plan 只告诉你"OCID 变不变",不告诉你"会不会重启"。
本页整理自 Phase 2、Phase 3、Phase 4、Phase 5a、Phase 9 的实验。网络和登录方式(Bastion)见 网络设计。
1. 先认识这些组件¶
| OCI | AWS | 一句话 |
|---|---|---|
| Shape | Instance Type | VM 的规格。名字带 .Flex 的可以自己指定 OCPU 数和内存 |
| OCPU | (约 2 个 vCPU) | OCI 的 CPU 单位 = 1 个物理核。x86 上 1 OCPU = 2 vCPU;Arm(A1)没有超线程,1 OCPU = 1 vCPU |
| Image | AMI | 操作系统镜像。Oracle Linux、Ubuntu 等,按 shape(x86 / Arm)区分 |
| Boot Volume | 根 EBS 卷 | 系统盘。最小 50GB |
| Availability Domain(AD) | Availability Zone | region 里独立的数据中心。大阪只有 1 个 AD |
| Fault Domain(FD) | (近似)Placement Group 的分散 | AD 里的机架分组,每个 AD 有 3 个。分到不同 FD = 不会同时因为同一台物理机坏掉 |
| Oracle Cloud Agent | SSM Agent | VM 里的代理程序,负责监控、Bastion 连接等(按 plugin 开关) |
| Instance Metadata Service(IMDS) | 同左 | VM 内部访问 169.254.169.254 拿到自己的信息(OCID、SSH 公钥、cloud-init 数据…) |
| Flexible Load Balancer | ALB / NLB | 负载均衡器。带宽可以指定(10Mbps 是免费的) |
| Listener / Backend Set / Backend | Listener / Target Group / Target | LB 的三层结构,见第 5 节 |
2. 原理:VM 的属性分三类¶
Terraform 改 VM 的属性时,结果有三种。plan 里的符号只能区分其中两种:
| 类别 | plan 的符号 | 例子(本仓库实际碰到的) |
|---|---|---|
| 改了要重建(OCID 变) | -/+(# forces replacement) |
镜像(source_id)、cloud-init(user_data) |
| 原地修改,但 VM 会重启 | ~ |
传输中加密(is_pv_encryption_in_transit_enabled,文档写明会重启) |
| 原地修改,不影响运行 | ~ |
标签(freeform_tags)、显示名 |
in-place(~)≠ 不停机
Phase 5a 给两台 app VM 同时改了 IMDS 和传输中加密(一次 apply 改了两项,分不出是哪一项导致重启),plan 是 0 to add, 2 to change(in-place)。
apply 后两台 VM 同时重启,LB 的健康检查全部失败,访问返回 502 Bad Gateway,几十秒后才恢复。
→ 会不会重启要看 provider 文档,或先在测试环境试。正式环境一台一台改(-target 分两次 apply,或先从 LB 摘掉)。
3. 初始化和配置:谁负责什么¶
一台 VM 从"建好"到"能提供服务",可以由不同的工具负责:
| Terraform | cloud-init | Ansible | |
|---|---|---|---|
| 负责 | VM、网络等云上的资源 | 第一次启动时的最小初始化(时区、用户) | OS 里的中间件安装、配置、之后的变更 |
| 什么时候执行 | apply 时 | 只在第一次启动时执行一次 | 什么时候都能反复执行 |
| 改了之后 | — | 已经在跑的 VM 不会反映;而且改 user_data 会让 VM 重建 |
再执行一次就反映 |
本仓库的做法:一开始用 cloud-init 装 nginx,后来改成 cloud-init 只做最小初始化,nginx 交给 Ansible。
Ansible 的关键性质是幂等:同一个 playbook 执行多次,结果一样。实际验证:第 1 次 changed=3,第 2 次以后 changed=0。
另一种思路:Immutable(不可变)
不在运行中的 VM 上改配置,而是把配置好的环境打包成镜像(Packer)或容器,要改就换新镜像、重建 VM。 Kubernetes 就是这个思路(见 Kubernetes 页)。
4. 镜像:不写死 OCID,但也不自动换¶
# 按条件查最新的镜像(Oracle Linux 9 + 这个 shape 能用的)
data "oci_core_images" "ol9" {
compartment_id = var.compartment_ocid
operating_system = "Oracle Linux"
operating_system_version = "9"
shape = var.shape
sort_by = "TIMECREATED"
sort_order = "DESC"
}
resource "oci_core_instance" "this" {
source_details {
source_type = "image"
source_id = data.oci_core_images.ol9.images[0].id # 最新的那个
}
lifecycle {
ignore_changes = [source_details[0].source_id] # 已经建好的 VM 不跟着换
}
}
- 不写死 OCID:镜像 OCID 每个 region 不一样,而且旧镜像会下架
ignore_changes:没有这一行的话,Oracle 每次发布新镜像,下次 plan 就会要求重建所有 VM- 结果:新建的 VM 用最新镜像,已有的 VM 不动。想换镜像时,有意识地对那台 VM 执行
terraform apply -replace=...
5. Load Balancer:三层结构¶
flowchart TB
C["客户端"] -->|":80"| L["Listener<br/>对外的端口和协议"]
L --> BS["Backend Set<br/>ROUND_ROBIN<br/>健康检查 GET / = 200"]
BS --> B1["Backend<br/>app-1 :80"]
BS --> B2["Backend<br/>app-2 :80"]
- 健康检查:LB 每 10 秒访问一次每台 VM 的
GET /,不是 200 的 VM 就不再分配流量。全部都不健康时返回 502(就是第 2 节那次) - LB 放 public subnet,VM 放 private subnet:VM 不需要公网 IP(见 网络设计)
-
Backend 的 IP 从 module 的输出取,不手写:
backends = { for k, m in module.app : k => m.private_ip } # { app-1 = "10.4.2.x", app-2 = "10.4.2.y" }for_each的 key(app-1 / app-2)在 plan 时就确定了,value(IP)可以是 apply 后才知道的值 -
Always Free 的条件写进代码:带宽 10Mbps 以外就收费,所以用 validation 锁住
这里的 LB 是 L7(HTTP),OKE 的是 L4(TCP)
本页的 Listener 协议是 HTTP:LB 看得懂 HTTP,健康检查也按 HTTP 判断。
Kubernetes 的 Service type=LoadBalancer 建出来的同一种 LB,Listener 是 TCP(只转发连接)。区别见 Kubernetes 页。
6. VM 本身的安全设置¶
详细的安全设计在"安全"页,这里只列和 VM 直接相关的三条:
| 设置 | 做法 | 为什么 |
|---|---|---|
| 不给公网 IP | assign_public_ip = false,放 private subnet |
公网 IP + 开着的端口 = 攻击面 |
| 只开 IMDS v2 | are_legacy_imds_endpoints_disabled = true |
v1 不需要 token 就能读元数据。应用有 SSRF 漏洞时,攻击者能借它读到 VM 的信息(和 AWS 的 IMDSv2 是同一个问题) |
| boot volume 传输中加密 | is_pv_encryption_in_transit_enabled = true |
存储本身默认加密;这个是 VM ↔ boot volume 之间的传输也加密 |
传输中加密:建 VM 时设了,实际却没生效
Phase 9 代码里写了 true,apply 成功,但再 plan 出现了差异 false -> true;用 API 查,VM 和 boot volume attachment 的实际值都是 false。
文档说建好之后也能打开(会重启)→ 第 2 次 apply(in-place update)后才变成 true,再 plan No changes。
apply 成功不代表设置生效:apply 后一定要再 plan,并用 API 确认实际状态。
7. Always Free 的计算资源¶
| 资源 | 免费额度 | 本仓库的用法 |
|---|---|---|
| A1(Arm)Flex | 合计 4 OCPU / 24GB | 每台 1 OCPU / 6GB,最多 4 台 |
| E2.1.Micro(x86) | 最多 2 台(1/8 OCPU、1GB) | 没用(太小) |
| Block Volume | boot volume 也算在内,合计 200GB | boot volume 最小 50GB → 实际最多 4 台 VM |
| Flexible LB | 1 个,10Mbps | Phase 4 用了 |
- 只能建在 Home Region(本仓库是大阪)
- A1 经常容量不足:Phase 11 建 OKE 的 node 时报
Out of host capacity。第一次和立刻重试都失败;之后每 10 分钟重试一次,第 3 次(约 25 分钟后)才成功。免费的 Arm 资源抢的人多,要有"等一等再试"的准备 - 几个 Phase 同时跑会超额度 → 下一个 Phase 要用之前,先 destroy 上一个
8. 设计决策一览¶
| 决策 | 选了 | 没选 | 理由 |
|---|---|---|---|
| shape | A1.Flex 1 OCPU / 6GB | E2.1.Micro | 免费额度内性能最好;Micro 只有 1GB 内存 |
| 镜像 | data source 查最新 + ignore_changes |
写死 OCID / 自动跟随最新 | 新 VM 用新镜像,已有 VM 不被意外重建 |
| 初始化 | cloud-init 只做最小初始化 | cloud-init 装所有东西 | 改 user_data 会重建 VM;已有 VM 改不了 |
| 配置管理 | Ansible(幂等) | 手动 SSH 进去改 | 可重复、可预览(--check --diff)、留在代码里 |
| 台数 | module + for_each,加一条就多一台 |
复制粘贴 resource | 台数变化只改一处 |
| 入口 | LB(HTTP、健康检查、ROUND_ROBIN) | 直接访问 VM | 一台坏了 / 重启时还有另一台 |
| 免费条件 | validation、写死 | 只靠记忆 | plan 阶段就报错,不会建出收费资源 |
9. 验证¶
| 要证明的事 | 怎么做 | 结果 |
|---|---|---|
| LB 真的在分流 | 连续访问 6 次 | app-1 / app-2 交替返回 → ROUND_ROBIN 生效 |
| Ansible 是幂等的 | 执行两次 playbook | 第 2 次 changed=0 |
| nginx 的配置生效了 | 看响应头 | Server: nginx 不带版本号(server_tokens off) |
| 安全设置真的生效了 | 再 plan + 用 API 查 VM 和 boot volume attachment 的实际值 | 第 1 次 apply 后再 plan 有差异、API 是 false;第 2 次 apply 后才是 true |
10. 坑¶
| 现象 | 原因 | 对策 |
|---|---|---|
in-place(~)apply 后 LB 返回 502 |
改的属性会让 VM 重启,两台同时重启 | 一台一台改 |
| 改了 cloud-init,VM 被重建 | user_data 是 # forces replacement |
配置交给 Ansible |
| 新镜像发布后 plan 要求重建 VM | data source 查到了新镜像 | ignore_changes = [source_details[0].source_id] |
传输中加密在新建时没生效(再 plan 出现 false -> true) |
建 VM 时的请求被忽略 | 第 2 次 apply;用 API 确认 |
If LaunchOptions is provided, NetworkType must be specified |
写了 launch_options 就必须写 network_type |
network_type = "PARAVIRTUALIZED"(和镜像默认值一致) |
| checkov 说没开传输中加密,但其实开了 | checkov 只认 launch_options 里的写法 |
按 checkov 的写法改 |
Out of host capacity |
免费的 A1 资源暂时不够 | 隔一段时间重试 |
| 同一个 LB 的多个变更 apply 很慢 | LB 的变更排队执行,每个十几到三十秒 | 正常现象 |
11. 自测问题¶
1. plan 显示 ~(in-place),可以放心在白天 apply 吗?
不一定。~ 只表示 OCID 不变,有些属性改了 VM 会重启(例:传输中加密)。要查文档或先在测试环境试;多台的话一台一台改。
2. 想给已经在运行的 VM 加一个软件,改 cloud-init 可以吗?
不好。cloud-init 只在第一次启动时执行,已有的 VM 不会反映;而且改 user_data 会让 Terraform 重建 VM。应该用 Ansible。
3. 为什么镜像用 data source 查,又要 ignore_changes?
用 data source 是为了不写死 OCID、新 VM 用最新镜像;ignore_changes 是为了新镜像发布时,已经在跑的 VM 不被重建。换镜像要有意识地做。
4. LB 返回 502,最可能是什么原因?
所有 backend 的健康检查都失败了:VM 停了 / 重启中、nginx 没起来、OS 防火墙没开端口、NSG 没放行 LB 来的流量。
5. Always Free 最多能建几台 VM?瓶颈是什么?
A1 合计 4 OCPU / 24GB,boot volume 合计 200GB 且每台最小 50GB → 最多 4 台(每台 1 OCPU)。瓶颈往往是 Block Volume。
6. IMDS v1 有什么风险?
不需要 token 就能读元数据。应用有 SSRF 漏洞时,攻击者可以让服务器替他请求 169.254.169.254,读到 VM 的信息。只开 v2(要带 token 头)就能挡住这类攻击。
7. 代码里写了传输中加密 = true,apply 成功了,可以认为已经加密了吗?
不能。本仓库实际碰到过:建 VM 时这个设置被忽略,API 查到的是 false。要用 API 确认实际值。
完整代码¶
04-3tier/modules/compute/main.tf(VM)
# ------------------------------------------------------------
# compute module:VM 1 台(Phase 4 版)
# 要建几台由调用方决定(env 里用 for_each 调用这个 module)
# 和 03 的区别:
# - NSG 不在 module 里建了 → 调用方按"层"建 NSG(app-nsg 等),用 nsg_ids 传进来
# (同一层的 VM 共用一个 NSG,规则只写一次)
# - 可以打开 Oracle Cloud Agent 的 Bastion plugin(Bastion 的 Managed SSH session 要用)
# ------------------------------------------------------------
locals {
is_flex = endswith(var.shape, ".Flex")
}
data "oci_identity_availability_domains" "ads" {
compartment_id = var.compartment_ocid
}
data "oci_core_images" "ol9" {
compartment_id = var.compartment_ocid
operating_system = "Oracle Linux"
operating_system_version = "9"
shape = var.shape
sort_by = "TIMECREATED"
sort_order = "DESC"
}
resource "oci_core_instance" "this" {
compartment_id = var.compartment_ocid
availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
display_name = var.name
shape = var.shape
dynamic "shape_config" {
for_each = local.is_flex ? [1] : []
content {
ocpus = var.ocpus
memory_in_gbs = var.memory_in_gbs
}
}
source_details {
source_type = "image"
source_id = data.oci_core_images.ol9.images[0].id
boot_volume_size_in_gbs = 50 # 最小值。Block Volume 合计 200GB 免费
}
create_vnic_details {
subnet_id = var.subnet_id
assign_public_ip = var.assign_public_ip
hostname_label = var.name
nsg_ids = var.nsg_ids
}
# boot volume 和 VM 之间的数据传输也加密(存储本身默认就加密)(checkov CKV_OCI_4)
# 顶层也有同名属性,但 checkov 只看 launch_options 里的这个
launch_options {
is_pv_encryption_in_transit_enabled = true
}
# 关掉旧版 Metadata 服务(IMDS v1):v1 不要 token 就能读元数据,SSRF 攻击时容易被利用(checkov CKV_OCI_5)
# 只留 v2(/opc/v2/,请求要带 Authorization: Bearer Oracle 头)。Oracle Cloud Agent / cloud-init 都支持 v2
instance_options {
are_legacy_imds_endpoints_disabled = true
}
# Oracle Cloud Agent 的 plugin 设定:这里只写要改的 plugin,没写的保持默认
agent_config {
plugins_config {
name = "Bastion"
desired_state = var.enable_bastion_plugin ? "ENABLED" : "DISABLED"
}
}
# user_data 是 null 时不写这个 key(merge 一个空 map)
metadata = merge(
{ ssh_authorized_keys = var.ssh_public_key },
var.user_data == null ? {} : { user_data = base64encode(var.user_data) }
)
preserve_boot_volume = false
freeform_tags = var.freeform_tags
# 新镜像发布后 data source 会拿到新的 OCID → 不加这行的话 plan 会要求重建 VM
lifecycle {
ignore_changes = [source_details[0].source_id]
}
}
04-3tier/modules/lb/main.tf(LB)
# ------------------------------------------------------------
# lb module:Flexible Load Balancer 一套(HTTP)
# LB ─ Listener(:80)─ Backend Set(ROUND_ROBIN + Health Check)─ Backend ×N
# Always Free:Flexible LB 1 台、带宽 10Mbps(min = max = 10)
# ------------------------------------------------------------
resource "oci_load_balancer_load_balancer" "this" {
compartment_id = var.compartment_ocid
display_name = var.name
# "flexible" + 带宽固定 10Mbps = Always Free 的条件(variables.tf 里有 validation)
shape = "flexible"
shape_details {
minimum_bandwidth_in_mbps = var.bandwidth_mbps
maximum_bandwidth_in_mbps = var.bandwidth_mbps
}
subnet_ids = var.subnet_ids
is_private = false # 公网 LB(分配公网 IP)
network_security_group_ids = var.nsg_ids
freeform_tags = var.freeform_tags
}
# Backend Set = 后端 VM 的组 + 分配方式 + 健康检查
resource "oci_load_balancer_backend_set" "this" {
load_balancer_id = oci_load_balancer_load_balancer.this.id
name = "${var.name}-bs"
policy = "ROUND_ROBIN" # 轮流分配
# 健康检查:LB 定期访问每台 VM 的 GET / ,不是 200 的 VM 就不再分配流量
health_checker {
protocol = "HTTP"
port = var.backend_port
url_path = "/"
return_code = 200
interval_ms = 10000
timeout_in_millis = 3000
retries = 3
}
}
# Backend = 组里的每台 VM(用私有 IP + 端口登记)
# for_each 的 key(app-1 等)在 plan 时就要确定;value(IP)可以是 apply 后才知道的值
resource "oci_load_balancer_backend" "this" {
for_each = var.backends
load_balancer_id = oci_load_balancer_load_balancer.this.id
backendset_name = oci_load_balancer_backend_set.this.name
ip_address = each.value
port = var.backend_port
}
# Listener = LB 对外接受连接的端口,收到的请求交给哪个 Backend Set
resource "oci_load_balancer_listener" "http" {
load_balancer_id = oci_load_balancer_load_balancer.this.id
name = "${var.name}-http"
default_backend_set_name = oci_load_balancer_backend_set.this.name
port = var.listener_port
protocol = "HTTP"
}
02-compute/ansible/site.yml(nginx 的配置)
# ------------------------------------------------------------
# Phase 2.5:用 Ansible 管理 nginx(在 Cloud Shell 里执行)
# ansible-playbook site.yml --check --diff # 先预览(相当于 terraform plan)
# ansible-playbook site.yml # 执行
# 再执行一次,所有 task 都应该是 ok(changed=0)→ 这就是幂等
# ------------------------------------------------------------
- name: Configure nginx web server
hosts: web
become: true # 用 sudo 执行
vars:
page_message: "Hello from Ansible on OCI"
tasks:
- name: Install nginx
ansible.builtin.dnf:
name: nginx
state: present
- name: Deploy index.html
ansible.builtin.template:
src: templates/index.html.j2
dest: /usr/share/nginx/html/index.html
mode: "0644"
# 配置文件有变化时才 reload nginx(handler)
- name: Hide nginx version in response headers
ansible.builtin.copy:
dest: /etc/nginx/conf.d/security.conf
content: "server_tokens off;\n"
mode: "0644"
notify: Reload nginx
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
# Oracle Linux 默认开着 firewalld,OS 内部也要放行 80
# Cloud Shell 的 Ansible 是 2.9,不认识 ansible.posix.firewalld(FQCN),所以用短名
- name: Allow HTTP in firewalld
firewalld:
service: http
permanent: true
immediate: true
state: enabled
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded