Phase 2:Compute + Ansible¶
目標¶
- Phase 1 のネットワークに Always Free の A1 VM を 1 台作り、nginx を動かす
- cloud-init と Ansible の役割分担を理解する(Phase 2.5)
構成¶
flowchart LR
PC[ローカル PC のブラウザ] -->|HTTP :80| VM
CS[Cloud Shell<br/>プライベート・ネットワーク] -->|SSH :22<br/>Ansible| VM
subgraph VCN["VCN 10.0.0.0/16"]
subgraph SN["パブリック・サブネット"]
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 まで無料)
- イメージ(Image)は data source
oci_core_imagesで条件(Oracle Linux 9 + シェイプ(Shape))を指定して最新のものを検索し、OCID は直書きしない - サブネット(Subnet)の OCID は
data "terraform_remote_state"で01-networkの state(状態ファイル)から output を読む → 2 つのディレクトリ間で OCID を手で書き写す必要がない
NSG(ネットワーク・セキュリティ・グループ)¶
- セキュリティ・リスト(Security List)はサブネット単位、NSG は VNIC 単位(≒ AWS の Security Group)
- 両者は和集合:どちらかで許可されていれば通る
- この段階では HTTP 80 を NSG で許可
cloud-init と Ansible の役割分担¶
最初は cloud-init で nginx をインストールしていましたが、のちに最小限の初期化だけにして、nginx は Ansible に任せるよう変更しました。
| cloud-init | Ansible | |
|---|---|---|
| 実行タイミング | 初回起動時に 1 回だけ実行 | いつでも何度でも実行できる |
| 変更したとき | 稼働中の VM には反映されない。user_data を変えると VM が再作成される(# forces replacement) |
もう一度実行すれば反映される |
| 向いている用途 | タイムゾーン、ユーザーなどの最小限の初期化 | ミドルウェアのインストール、設定、その後の変更管理 |
一般的な役割分担:Terraform = インフラ / cloud-init = 最小限の初期化 / Ansible = OS とミドルウェアの設定
もう 1 つの考え方:Immutable
Packer で設定済みのイメージを作っておき、変更するときはイメージを差し替えて VM を作り直します。稼働中の VM の設定は変更しません。Phase 6 の Kubernetes も同じ考え方です。
SSH:利用中のネットワークで 22 番ポートがブロックされている¶
ローカル PC から直接 SSH → Connection timed out(github.com:22 にもつながらず、443 はつながる)。
対策:Cloud Shell + エフェメラル・プライベート・ネットワーク(Ephemeral Private Network)
- コンソール(GUI)右上の
>_で Cloud Shell を開く - ツールバー左上の
Network: Public→Ephemeral private network setup→ VCN とサブネットを選択 →Use as active network - Cloud Shell に VCN 内のプライベート IP が割り当てられるので、VM のプライベート IP に SSH する
- セキュリティ・リストで「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 → 冪等性(idempotency) |
- handler:notify した task が changed のときだけ実行され、すべての task が終わったあとにまとめて 1 回実行される
- 確認:レスポンス・ヘッダーの
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)になるはず → これが冪等性(idempotency)
# ------------------------------------------------------------
- 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"
# 設定ファイルが変わったときだけ nginx を reload(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 は 1 ファイルずつしかアップロードできない
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)に Always Free VM を 1 台作成
# - data source で可用性ドメイン(Availability Domain)と最新の Oracle Linux 9 イメージ(Image)を取得
# - ネットワーク・セキュリティ・グループ(NSG)で HTTP を許可(ローカル PC の 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:既存情報の読み取りのみ。リソースは作成しない ----------
# 可用性ドメインの一覧(大阪は AD が 1 つだけ)
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)はサブネット全体に効き、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 のルールは独立したリソース(セキュリティ・リストとは書き方が異なる)
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 ブロック:shape_config は Flex シェイプのときだけ必要(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)も一緒に削除(削除しないと残り続け、無料枠を消費する)
preserve_boot_volume = false
freeform_tags = local.common_tags
}