コンピュートとロード・バランサ¶
ひとことで
VM はいつでも入れ替えられる部品として扱います:イメージは固定しない、設定は Ansible に任せる(またはイメージに焼き込む)、前段にロード・バランサ(LB)を置く。こうすれば 1 台が再起動・入れ替えになってもサービスは止まりません。 もう 1 つ覚えておくこと:Terraform の plan が教えてくれるのは「OCID が変わるかどうか」だけで、「再起動するかどうか」は教えてくれません。
このページは Phase 2、Phase 3、Phase 4、Phase 5a、Phase 9 の実験をまとめたものです。ネットワークとログイン方法(要塞)は ネットワーク設計 を参照してください。
1. まず構成要素を知る¶
| OCI | AWS | ひとことで |
|---|---|---|
| シェイプ(Shape) | Instance Type | VM の規格。名前に .Flex が付くものは OCPU 数とメモリを自分で指定できる |
| OCPU | (vCPU 約 2 個) | OCI の CPU 単位 = 物理コア 1 つ。x86 では 1 OCPU = 2 vCPU、Arm(A1)はハイパースレッディングがないので 1 OCPU = 1 vCPU |
| イメージ(Image) | AMI | OS イメージ。Oracle Linux、Ubuntu など。シェイプ(x86 / Arm)ごとに分かれる |
| ブート・ボリューム(Boot Volume) | ルート EBS ボリューム | システム・ディスク。最小 50GB |
| 可用性ドメイン(AD) | Availability Zone | リージョン内の独立したデータセンター。大阪は AD が 1 つだけ |
| フォルト・ドメイン(FD) | (近い)Placement Group の分散 | AD 内のラックのグループで、AD ごとに 3 つ。別の FD に分ければ、同じ物理マシンの故障で同時に止まらない |
| Oracle Cloud エージェント(Oracle Cloud Agent) | SSM Agent | VM 内のエージェント。監視や要塞接続などを担当(プラグインごとにオン / オフ) |
| インスタンス・メタデータ・サービス(IMDS) | 同左 | VM の内部から 169.254.169.254 にアクセスして自分の情報(OCID、SSH 公開鍵、cloud-init のデータ…)を得る |
| フレキシブル・ロード・バランサ | ALB / NLB | 負荷分散装置。帯域を指定できる(10Mbps は無料) |
| リスナー / バックエンド・セット / バックエンド | Listener / Target Group / Target | LB の 3 層構造。5 節を参照 |
2. 原理:VM の属性は 3 種類に分かれる¶
Terraform で VM の属性を変えたときの結果は 3 通りあります。plan の記号で区別できるのはそのうち 2 通りだけです:
| 種類 | plan の記号 | 例(このリポジトリで実際に遭遇したもの) |
|---|---|---|
| 変更すると再作成(OCID が変わる) | -/+(# forces replacement) |
イメージ(source_id)、cloud-init(user_data) |
| インプレース変更だが、VM が再起動する | ~ |
転送中の暗号化(is_pv_encryption_in_transit_enabled、ドキュメントに再起動すると明記) |
| インプレース変更で、稼働に影響しない | ~ |
タグ(freeform_tags)、表示名 |
インプレース(~)≠ 無停止
Phase 5a で 2 台の app VM の IMDS と転送中の暗号化を同時に変更しました(1 回の apply で 2 項目を変えたので、どちらが再起動の原因かは区別できません)。plan は 0 to add, 2 to change(インプレース)でした。
apply 後、2 台の VM が同時に再起動し、LB のヘルス・チェックがすべて失敗して 502 Bad Gateway が返り、数十秒後にようやく復旧しました。
→ 再起動するかどうかはプロバイダのドキュメントで確認するか、テスト環境で先に試します。本番環境では 1 台ずつ変更します(-target で 2 回に分けて apply する、または先に LB から外す)。
3. 初期化と設定:だれが何を担当するか¶
VM が「作成された」状態から「サービスを提供できる」状態になるまでを、ツールごとに分担できます:
| Terraform | cloud-init | Ansible | |
|---|---|---|---|
| 担当 | VM やネットワークなどクラウド上のリソース | 初回起動時の最小限の初期化(タイムゾーン、ユーザー) | OS 内のミドルウェアのインストール、設定、その後の変更 |
| 実行タイミング | apply 時 | 初回起動時に 1 回だけ | いつでも何度でも |
| 変更した後 | — | 稼働中の 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 + このシェイプで使えるもの)
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 はリージョンごとに違い、古いイメージは提供終了になります
ignore_changes:この行がないと、Oracle が新しいイメージを公開するたびに、次の plan ですべての VM の再作成を求められます- 結果:新しく作る VM は最新イメージ、既存の VM はそのまま。イメージを替えたいときは、その VM に対して意識的に
terraform apply -replace=...を実行します
5. ロード・バランサ:3 層構造¶
flowchart TB
C["クライアント"] -->|":80"| L["リスナー<br/>外向きの<br/>ポートとプロトコル"]
L --> BS["バックエンド・セット<br/>ROUND_ROBIN<br/>ヘルス・チェック<br/>GET / = 200"]
BS --> B1["バックエンド<br/>app-1 :80"]
BS --> B2["バックエンド<br/>app-2 :80"]
- ヘルス・チェック:LB は 10 秒ごとに各 VM の
GET /にアクセスし、200 を返さない VM にはトラフィックを振り分けなくなります。すべてが異常なら 502 を返します(2 節のケース) - LB はパブリック・サブネット、VM はプライベート・サブネット:VM にパブリック IP は不要です(ネットワーク設計 を参照)
-
バックエンドの 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のキー(app-1 / app-2)は plan 時点で確定し、値(IP)は apply 後に分かる値でも構いません -
Always Free の条件をコードに書く:帯域が 10Mbps 以外だと課金されるので、validation で固定します
ここの LB は L7(HTTP)、OKE のものは L4(TCP)
このページのリスナーのプロトコルは HTTP です:LB は HTTP を理解し、ヘルス・チェックも HTTP で判定します。
Kubernetes の Service type=LoadBalancer で作られる同じ種類の LB は、リスナーが TCP(接続を転送するだけ)です。違いは Kubernetes のページを参照してください。
6. VM 自体のセキュリティ設定¶
セキュリティ設計の詳細は「セキュリティ」のページにあります。ここでは VM に直接関係する 3 つだけを挙げます:
| 設定 | やり方 | 理由 |
|---|---|---|
| パブリック IP を付けない | assign_public_ip = false、プライベート・サブネットに置く |
パブリック IP + 開いたポート = 攻撃対象 |
| IMDS v2 だけを有効にする | are_legacy_imds_endpoints_disabled = true |
v1 はトークンなしでメタデータを読める。アプリに SSRF の脆弱性があると、攻撃者がそれを使って VM の情報を読める(AWS の IMDSv2 と同じ問題) |
| ブート・ボリュームの転送中の暗号化 | is_pv_encryption_in_transit_enabled = true |
ストレージ自体はデフォルトで暗号化済み。これは VM ↔ ブート・ボリューム間の転送も暗号化する設定 |
転送中の暗号化:VM の作成時に指定したのに、実際には効いていなかった
Phase 9 ではコードに true と書き、apply も成功しましたが、再度 plan すると差分 false -> true が出ました。API で確認すると、VM とブート・ボリューム・アタッチメントの実際の値はどちらも false でした。
ドキュメントによると作成後でも有効化できる(再起動を伴う)→ 2 回目の apply(インプレース更新)でようやく true になり、再 plan は No changes になりました。
apply の成功は設定が効いたことを意味しません:apply 後は必ず再度 plan し、API で実際の状態を確認します。
7. Always Free のコンピュート・リソース¶
| リソース | 無料枠 | このリポジトリでの使い方 |
|---|---|---|
| A1(Arm)Flex | 合計 4 OCPU / 24GB | 1 台あたり 1 OCPU / 6GB、最大 4 台 |
| E2.1.Micro(x86) | 最大 2 台(1/8 OCPU、1GB) | 使っていない(小さすぎる) |
| ブロック・ボリューム | ブート・ボリュームも含めて合計 200GB | ブート・ボリュームは最小 50GB → 実質最大 4 台の VM |
| フレキシブル LB | 1 つ、10Mbps | Phase 4 で使用 |
- 作成できるのはホーム・リージョンだけです(このリポジトリでは大阪)
- A1 はよく容量不足になります:Phase 11 で OKE のノードを作るときに
Out of host capacityが出ました。1 回目もすぐの再試行も失敗し、その後 10 分ごとに再試行して、3 回目(約 25 分後)にようやく成功しました。無料の Arm リソースは取り合いなので、「少し待って再試行する」心づもりが必要です - 複数の Phase を同時に動かすと無料枠を超えます → 次の Phase で枠を使う前に、前の Phase を destroy します
8. 設計判断の一覧¶
| 判断 | 採用 | 不採用 | 理由 |
|---|---|---|---|
| シェイプ | A1.Flex 1 OCPU / 6GB | E2.1.Micro | 無料枠の中で性能が最もよい。Micro はメモリが 1GB しかない |
| イメージ | データ・ソースで最新を検索 + ignore_changes |
OCID を固定 / 常に最新に追従 | 新しい VM は新しいイメージ、既存の VM は意図せず再作成されない |
| 初期化 | cloud-init は最小限の初期化だけ | cloud-init ですべてを入れる | user_data を変えると VM が再作成される。既存の VM には反映できない |
| 構成管理 | Ansible(冪等) | 手作業で SSH して変更 | 繰り返せる、事前確認できる(--check --diff)、コードに残る |
| 台数 | module + for_each、1 行足せば 1 台増える |
resource をコピー&ペースト | 台数の変更は 1 か所だけ |
| 入口 | LB(HTTP、ヘルス・チェック、ROUND_ROBIN) | VM に直接アクセス | 1 台が故障 / 再起動してももう 1 台がある |
| 無料の条件 | validation、固定値 | 記憶だけに頼る | plan の段階でエラーになり、課金リソースが作られない |
9. 検証¶
| 証明したいこと | やり方 | 結果 |
|---|---|---|
| LB が本当に振り分けている | 6 回続けてアクセス | app-1 / app-2 が交互に返る → ROUND_ROBIN が有効 |
| Ansible が冪等 | playbook を 2 回実行 | 2 回目は changed=0 |
| nginx の設定が効いている | レスポンス・ヘッダーを見る | Server: nginx にバージョン番号がない(server_tokens off) |
| セキュリティ設定が本当に効いている | 再 plan + API で VM とブート・ボリューム・アタッチメントの実際の値を確認 | 1 回目の apply 後は再 plan で差分があり、API の値は false。2 回目の apply 後にようやく true |
10. ハマりどころ¶
| 現象 | 原因 | 対策 |
|---|---|---|
インプレース(~)の apply 後に LB が 502 を返す |
変更した属性で VM が再起動し、2 台が同時に再起動した | 1 台ずつ変更する |
| cloud-init を変えたら VM が再作成された | user_data は # forces replacement |
設定は Ansible に任せる |
| 新しいイメージの公開後、plan が VM の再作成を求める | データ・ソースが新しいイメージを見つけた | 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 の変更はキューで順番に実行され、1 つあたり十数秒〜30 秒 | 正常な動作 |
11. 確認問題¶
1. plan が ~(インプレース)を表示している。日中に安心して apply できる?
できるとは限りません。~ は OCID が変わらないことを示すだけで、変えると VM が再起動する属性もあります(例:転送中の暗号化)。ドキュメントを確認するかテスト環境で先に試し、複数台なら 1 台ずつ変更します。
2. 稼働中の VM にソフトウェアを 1 つ追加したい。cloud-init を変えればいい?
よくありません。cloud-init は初回起動時にしか実行されないので既存の VM には反映されず、しかも user_data を変えると Terraform が VM を再作成します。Ansible を使うべきです。
3. イメージをデータ・ソースで探しつつ、ignore_changes も付けるのはなぜ?
データ・ソースを使うのは OCID を固定せず、新しい VM で最新イメージを使うためです。ignore_changes は、新しいイメージが公開されたときに稼働中の VM が再作成されないようにするためです。イメージの入れ替えは意識して行います。
4. LB が 502 を返す。最も可能性が高い原因は?
すべてのバックエンドのヘルス・チェックが失敗しています:VM が停止 / 再起動中、nginx が起動していない、OS のファイアウォールでポートが開いていない、NSG が LB からの通信を許可していない。
5. Always Free で作れる VM は最大何台? ボトルネックは?
A1 は合計 4 OCPU / 24GB、ブート・ボリュームは合計 200GB で 1 台あたり最小 50GB → 最大 4 台(1 台 1 OCPU)。ボトルネックになりやすいのはブロック・ボリュームです。
6. IMDS v1 にはどんなリスクがある?
トークンなしでメタデータを読めます。アプリに SSRF の脆弱性があると、攻撃者がサーバーに 169.254.169.254 へのリクエストを代行させ、VM の情報を読めてしまいます。v2(トークン・ヘッダーが必要)だけを有効にすれば、この種の攻撃を防げます。
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 で 1 つの NSG を共用し、ルールは 1 回だけ書く)
# - Oracle Cloud エージェントの要塞プラグインを有効にできる(要塞のマネージド SSH セッションに必要)
# ------------------------------------------------------------
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 # 最小値。ブロック・ボリュームは合計 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
}
# ブート・ボリュームと VM の間のデータ転送も暗号化(ストレージ自体はデフォルトで暗号化済み)(checkov CKV_OCI_4)
# トップレベルにも同名の属性があるが、checkov が見るのは launch_options 内のこれだけ
launch_options {
is_pv_encryption_in_transit_enabled = true
}
# 旧メタデータ・サービス(IMDS v1)を無効化:v1 はトークンなしでメタデータを読めるため、SSRF 攻撃に悪用されやすい(checkov CKV_OCI_5)
# v2(/opc/v2/、リクエストに Authorization: Bearer Oracle ヘッダーが必要)だけを残す。Oracle Cloud エージェント / cloud-init はどちらも v2 に対応
instance_options {
are_legacy_imds_endpoints_disabled = true
}
# Oracle Cloud エージェントのプラグイン設定:変更したいプラグインだけを書く。書かないものはデフォルトのまま
agent_config {
plugins_config {
name = "Bastion"
desired_state = var.enable_bastion_plugin ? "ENABLED" : "DISABLED"
}
}
# user_data が null のときはこのキーを書かない(空の map を merge)
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
# 新しいイメージが公開されるとデータ・ソースが新しい OCID を返す → この行がないと plan が VM の再作成を求める
lifecycle {
ignore_changes = [source_details[0].source_id]
}
}
04-3tier/modules/lb/main.tf(LB)
# ------------------------------------------------------------
# lb module:フレキシブル・ロード・バランサ一式(HTTP)
# LB ─ リスナー(:80)─ バックエンド・セット(ROUND_ROBIN + ヘルス・チェック)─ バックエンド ×N
# Always Free:フレキシブル 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
}
# バックエンド・セット = バックエンド 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
}
}
# バックエンド = グループ内の各 VM(プライベート IP + ポートで登録)
# for_each のキー(app-1 など)は plan 時点で確定している必要がある。値(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
}
# リスナー = LB が外部からの接続を受け付けるポート。受けたリクエストをどのバックエンド・セットに渡すか
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)になるはず → これが冪等性(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