Phase 2:Compute + Ansible¶
目标¶
- 在 Phase 1 的网络里建一台 Always Free 的 A1 VM,跑 nginx
- 理解 cloud-init 和 Ansible 的分工(Phase 2.5)
架构¶
flowchart LR
PC[本机浏览器] -->|HTTP :80| VM
CS[Cloud Shell<br/>private network] -->|SSH :22<br/>Ansible| VM
subgraph VCN["VCN 10.0.0.0/16"]
subgraph SN["Public Subnet"]
VM["tflab-web<br/>A1.Flex 1 OCPU / 6GB<br/>Oracle Linux 9"]
end
end
要点¶
VM 的规格¶
VM.Standard.A1.Flex:ARM(aarch64),用shape_config指定 OCPU 和内存- boot volume 50GB(最小值,Block Volume 合计 200GB 免费)
- 镜像用 data source
oci_core_images按条件(Oracle Linux 9 + shape)查最新的,不写死 OCID - Subnet 的 OCID 用
data "terraform_remote_state"从01-network的 state 里读 output → 两个目录之间不用手抄 OCID
NSG(Network Security Group)¶
- Security List 是 Subnet 级别,NSG 是 VNIC 级别(≒ AWS 的 Security Group)
- 两者是并集:哪个放行了都能通
- 这个阶段 HTTP 80 用 NSG 放行
cloud-init 和 Ansible 的分工¶
一开始用 cloud-init 装 nginx,后来改成只做最小初始化,nginx 交给 Ansible:
| cloud-init | Ansible | |
|---|---|---|
| 执行时机 | 只在第一次启动时执行一次 | 什么时候都能反复执行 |
| 改了之后 | 已经在跑的 VM 不会反映;改 user_data 会导致 VM 重建(# forces replacement) |
再执行一次就反映 |
| 适合 | 时区、用户等最小初始化 | 中间件安装、配置、之后的变更管理 |
一般的分工:Terraform = 基础设施 / cloud-init = 最小初始化 / Ansible = OS 和中间件配置
另一种思路:Immutable
用 Packer 把配置好的镜像打包,要改就换镜像重建 VM,不在运行中的 VM 上改配置。Phase 6 的 Kubernetes 也是这个思路。
SSH:所在网络屏蔽了 22 端口¶
从本机直接 SSH → Connection timed out(连 github.com:22 也不通,443 可以)。
对策:Cloud Shell + Ephemeral Private Network
- Console 右上角
>_打开 Cloud Shell - 工具栏左上角
Network: Public→Ephemeral private network setup→ 选 VCN 和 Subnet →Use as active network - Cloud Shell 拿到 VCN 里的私有 IP,用私有 IP SSH 到 VM
- Security List 里放行"22 from VCN CIDR"
为什么不直接放行 Cloud Shell 的公网 IP?
Cloud Shell 的出口 IP 是 Oracle 的共用 IP,而且会变。放行它,等于对其他 OCI 用户也开放了 22 端口。
Phase 2.5:用 Ansible 配置 nginx¶
在 Cloud Shell 里执行(PC 的 22 端口不通)。
ansible web -m ping # 连通确认
ansible-playbook site.yml --check --diff # 预览(≒ terraform plan)
ansible-playbook site.yml # 执行
| 结果 | |
|---|---|
| 第 1 次 | changed=3(index.html、security.conf、handler reload) |
| 第 2 次以后 | changed=0 → 幂等 |
- handler:只有 notify 它的 task 是 changed 时才执行,在所有 task 结束后统一执行一次
- 确认:响应头
Server: nginx不带版本号(server_tokens off生效) - Oracle Linux 默认开着 firewalld,OS 内部也要放行 80(云上的 NSG 放行了也不够)
02-compute/ansible/site.yml
# ------------------------------------------------------------
# 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
坑¶
Cloud Shell 的 Ansible 是 2.9
ansible.posix.firewalld 报 couldn't resolve module → 用短名 firewalld。ansible.builtin.* 可以正常用。
cloud-init status --long 报 PermissionError
/run/cloud-init/cloud.cfg 只有 root 能读 → 加 sudo。
Cloud Shell 的 Upload 只能传单个文件
把 ansible 目录打成 tar.gz 再上传:tar.exe -czf ansible.tar.gz -C 02-compute ansible。
PS 5.1 的 Compress-Archive 打出来的 zip 路径分隔符是 \,Linux 上解不开,所以不用 zip。
完整代码¶
02-compute/main.tf
# ------------------------------------------------------------
# Phase 2:在 01-network 建好的 Public Subnet 里建 1 台 Always Free VM
# - 用 data source 获取 AD 和最新的 Oracle Linux 9 镜像
# - 用 NSG 放行 HTTP(只允许本机 IP)
# - 用 cloud-init 安装 nginx
# ------------------------------------------------------------
locals {
common_tags = {
project = "tf-lab"
managed_by = "terraform"
}
is_flex = endswith(var.instance_shape, ".Flex")
# 从 01-network 的 state 里取出 output 的值
vcn_id = data.terraform_remote_state.network.outputs.vcn_id
subnet_id = data.terraform_remote_state.network.outputs.public_subnet_id
}
# ---------- 跨目录引用:读取 01-network 的 state ----------
# 现在 state 是本地文件,所以用 backend = "local"
# (Phase 3 会把 state 搬到 Object Storage)
data "terraform_remote_state" "network" {
backend = "local"
config = {
path = "${path.module}/../01-network/terraform.tfstate"
}
}
# ---------- data source:读取已有信息,不会新建任何资源 ----------
# Availability Domain 列表(大阪只有 1 个 AD)
data "oci_identity_availability_domains" "ads" {
compartment_id = var.compartment_ocid
}
# 和 shape 匹配的 Oracle Linux 9 镜像,取最新的一个
# (A1 是 Arm,会自动筛选出 aarch64 的镜像)
data "oci_core_images" "ol9" {
compartment_id = var.compartment_ocid
operating_system = "Oracle Linux"
operating_system_version = "9"
shape = var.instance_shape
sort_by = "TIMECREATED"
sort_order = "DESC"
}
# ---------- NSG:绑定在 VNIC 上的防火墙 ----------
# Security List 作用于整个 Subnet,NSG 只作用于绑定了它的 VM,更细粒度
resource "oci_core_network_security_group" "web" {
compartment_id = var.compartment_ocid
vcn_id = local.vcn_id
display_name = "${var.project_name}-nsg-web"
freeform_tags = local.common_tags
}
# NSG 的规则是独立的资源(和 Security List 的写法不同)
resource "oci_core_network_security_group_security_rule" "web_http" {
network_security_group_id = oci_core_network_security_group.web.id
direction = "INGRESS"
protocol = "6" # TCP
source = "${var.my_ip}/32"
source_type = "CIDR_BLOCK"
description = "HTTP from my IP"
tcp_options {
destination_port_range {
min = 80
max = 80
}
}
}
# ---------- VM ----------
resource "oci_core_instance" "web" {
compartment_id = var.compartment_ocid
availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
display_name = "${var.project_name}-web"
shape = var.instance_shape
# dynamic block:只有 Flex shape 才需要 shape_config(E2.1.Micro 时不生成这个块)
dynamic "shape_config" {
for_each = local.is_flex ? [1] : []
content {
ocpus = var.a1_ocpus
memory_in_gbs = var.a1_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 = local.subnet_id
assign_public_ip = true
nsg_ids = [oci_core_network_security_group.web.id]
}
metadata = {
ssh_authorized_keys = file(var.ssh_public_key_path)
user_data = base64encode(file("${path.module}/cloud-init.yaml"))
}
# destroy 时连 boot volume 一起删掉(不删的话 boot volume 会留下来,占用免费额度)
preserve_boot_volume = false
freeform_tags = local.common_tags
}