コンテンツにスキップ

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)

  1. コンソール(GUI)右上の >_ で Cloud Shell を開く
  2. ツールバー左上の Network: Public → Ephemeral private network setup → VCN とサブネットを選択 → Use as active network
  3. Cloud Shell に VCN 内のプライベート IP が割り当てられるので、VM のプライベート IP に SSH する
  4. セキュリティ・リストで「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
}