Phase 4:3 層アーキテクチャ¶
目標¶
本番環境に近い構成を作ります:VM にパブリック IP を持たせない、入口はロード・バランサ(Load Balancer、LB)だけ、DB はマネージド・サービスを使う。
構成¶
flowchart LR
PC[ローカル PC my_ip] -->|:80| LB
CS[Cloud Shell] -->|SSH| BAS[OCI 要塞]
subgraph VCN["VCN 10.4.0.0/16"]
subgraph PUB["public 10.4.1.0/24"]
LB["LB flexible 10Mbps<br/>lb-nsg"]
end
subgraph APP["app 10.4.2.0/24(パブリック IP なし)"]
VM1["app-1<br/>app-nsg"]
VM2["app-2<br/>app-nsg"]
BAS
end
NAT[NAT ゲートウェイ]
SGW[サービス・ゲートウェイ]
end
LB -->|:80| VM1 & VM2
BAS -->|:22| VM1 & VM2
VM1 & VM2 -->|0.0.0.0/0| NAT --> Internet((Internet))
VM1 & VM2 -->|Oracle Services| SGW --> ADB[(Autonomous DB<br/>Always Free)]
4 段階に分けて apply します:① ネットワーク → ② VM + 要塞(Bastion)→ ③ LB → ④ ADB。各段階で検証してから次に進みます。
ルーティングとアクセス制御の一覧¶
| 通信 | 経路 | 制御 |
|---|---|---|
| ローカル PC → Web | Internet → LB(public)→ VM:80 | lb-nsg(my_ip)→ app-nsg(ソース = lb-nsg) |
| 運用 SSH | Cloud Shell → 要塞 → VM:22 | app-nsg(ソース = 要塞のプライベート IP)+ セッションの公開鍵 |
| VM → Internet | rt-private 0.0.0.0/0 → NAT ゲートウェイ |
送信のみ、受信不可 |
| VM → ADB / OCI の dnf リポジトリ | rt-private Oracle Services → サービス・ゲートウェイ | ADB の ACL(VCN OCID) |
① ネットワーク:NAT ゲートウェイとサービス・ゲートウェイ¶
# プライベート・サブネット(Subnet)用のルート表(Route Table)
route_rules {
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
network_entity_id = oci_core_nat_gateway.this[0].id
}
route_rules {
destination = data.oci_core_services.all[0].services[0].cidr_block
destination_type = "SERVICE_CIDR_BLOCK" # 宛先は CIDR ではなく「サービス」
network_entity_id = oci_core_service_gateway.this[0].id
}
- NAT ゲートウェイ(NAT Gateway):プライベートな VM から Internet へ、内側から外側への接続だけができる(≒ AWS の NAT Gateway)
- サービス・ゲートウェイ(Service Gateway):Internet を経由せず、Oracle の内部ネットワーク経由で OCI のサービスにアクセスする(≒ AWS の VPC Endpoint)
- 宛先は
all-kix-services-in-oracle-services-networkのようなサービス名で、実際の IP 範囲は OCI が管理する(kix= 大阪のリージョン・キー) All ... Services In Oracle Services Networkを選ぶ(もう一方の選択肢はオブジェクト・ストレージ(Object Storage)だけで、ADB には使えない)
- 宛先は
- 2 つのルールの両方に一致する場合は、より具体的なほうが使われる(最長プレフィックス一致)→ Oracle サービス向けの通信はサービス・ゲートウェイを通る
② VM ×2 + 要塞¶
- NSG(ネットワーク・セキュリティ・グループ)は層ごとに作成:2 台の app VM で 1 つの
app-nsgを共用。module はnsg_idsを受け取るだけ -
SSH ルールのソースは IP を直書きせず、要塞の属性を参照する:
-
VM で要塞プラグイン(Oracle Cloud エージェント(Oracle Cloud Agent))を有効にする:
-
要塞の
client_cidr_block_allow_list = ["0.0.0.0/0"]:Cloud Shell の IP は固定されないため。セキュリティは IAM + セッションの公開鍵で担保する - セッションは Terraform で管理しない:最長 3 時間で期限切れになるので、管理下に置くと毎回 plan で差分が出る → 使うときに Cloud Shell で作成する
要塞経由でログインする(Cloud Shell)¶
ssh-keygen -t rsa -b 4096 -f ~/.ssh/bastion_key -N ""
oci bastion session create-managed-ssh \
--bastion-id <Bastion OCID> --target-resource-id <VM OCID> \
--target-os-username opc --ssh-public-key-file ~/.ssh/bastion_key.pub \
--session-ttl 3600 --wait-for-state SUCCEEDED \
--query 'data.resources[0].identifier' --raw-output # 1〜2 分かかる
ssh -i ~/.ssh/bastion_key \
-o ProxyCommand="ssh -i ~/.ssh/bastion_key -W %h:%p -p 22 <SESSION_OCID>@host.bastion.ap-osaka-1.oci.oraclecloud.com" \
-p 22 opc@<VM のプライベート IP>
検証:送信は本当に NAT ゲートウェイを通っているか?¶
| VM 上で実行 | 結果 | 説明 |
|---|---|---|
curl ifconfig.me |
NAT ゲートウェイのパブリック IP | Internet への送信は NAT ゲートウェイ経由 ✅ |
dnf install |
成功 | NAT ゲートウェイの証明にはならない:リポジトリは yum.ap-osaka-1.oci.oraclecloud.com で Oracle Services に属するため、サービス・ゲートウェイを通っている |
③ LB¶
shape = "flexible"、min = max = 10Mbps(variable の validation でほかの値を書けないようにする)-
バックエンドの IP は module の output から取得する:
for_each の key(app-1 / app-2)は plan の時点で確定しており、value(IP)は apply 後に判明する値でもかまわない
-
NSG の相互参照(
source_type = "NETWORK_SECURITY_GROUP"):app-nsg は「lb-nsg からの」80 だけを許可 → LB の IP が変わっても、VM が増減してもルールを変更する必要がない - 検証:6 回アクセスすると app-1 / app-2 が交互に返る → ROUND_ROBIN が効いている
- 同じ LB に対する変更(バックエンド・セット(Backend Set)/バックエンド/リスナー(Listener))は並列ではなく順番に実行され、それぞれ十数秒〜30 秒かかる
④ Autonomous DB¶
is_free_tier = trueを直書きし、output でもtrueであることを再確認-
Always Free の Autonomous Database(ADB)はプライベート・エンドポイント(Private Endpoint)に対応していない → パブリック・エンドポイント + ACL:
-
is_mtls_connection_required = false(ACL を設定した場合のみ無効化できる)→ wallet 不要で、TLS の接続文字列でそのまま接続できる - ADMIN パスワードは
random_passwordで生成 → state(状態ファイル)には平文で保存される。output にはsensitive = trueを設定
検証:通信は本当にサービス・ゲートウェイを通っているか?¶
VM 上で Python(pip install oracledb、thin モードなら Instant Client 不要)から ADB に接続:
結果は VM のプライベート IP(NAT ゲートウェイのパブリック IP ではない)→ 通信はサービス・ゲートウェイを経由しており、ACL は VCN OCID で許可されている ✅
Always Free ADB のライフサイクル
7 日間使われないと自動停止し、停止から 90 日後に削除されます。
ハマりどころ¶
Cloud Shell は FIPS モード
ssh-keygen -t ed25519 → ED25519 keys are not allowed in FIPS mode → RSA を使う。
リソース検索(Resource Search)の遅延
destroy 直後にリソース検索で調べると、要塞がまだ ACTIVE と表示されました。get_bastion を直接呼ぶとすでに DELETED でした。
検索インデックスには遅延があるので、API で直接取得した結果を正とします。
コード全体¶
04-3tier/env/main.tf
# ------------------------------------------------------------
# Phase 4:3 層構成(LB → Private VM ×2 → Autonomous DB)
# 段階的に apply:① ネットワーク → ② VM + Bastion → ③ LB → ④ ADB
# ------------------------------------------------------------
locals {
name_prefix = "tflab4"
vcn_cidr = "10.4.0.0/16"
common_tags = {
project = "tf-lab"
managed_by = "terraform"
phase = "4"
}
# app VM:1 行追加するごとに 1 台増える(Always Free の上限:A1 は合計 4 OCPU / 24GB、ブート・ボリューム(Boot Volume)は合計 200GB)
# 現在:2 台 × (1 OCPU / 6GB / 50GB) = 2 OCPU / 12GB / 100GB
app_instances = toset(["app-1", "app-2"])
}
# ---- ① ネットワーク ----
# public:ロード・バランサ(Load Balancer)を配置(IGW 経由)
# app :VM を配置(パブリック IP なし。外向き通信は NAT ゲートウェイ(NAT Gateway)、ADB へはサービス・ゲートウェイ(Service Gateway)経由)
module "network" {
source = "../modules/network"
compartment_ocid = var.compartment_ocid
name_prefix = local.name_prefix
vcn_cidr = local.vcn_cidr
vcn_dns_label = local.name_prefix
subnets = {
public = { cidr = "10.4.1.0/24", dns_label = "public", is_public = true }
app = { cidr = "10.4.2.0/24", dns_label = "app", is_public = false }
}
# SSH は VCN(仮想クラウド・ネットワーク)内部からのみ。要塞(Bastion)のプライベート・エンドポイント(Private Endpoint)が VCN 内にあるため
ssh_allowed_cidrs = [local.vcn_cidr]
freeform_tags = local.common_tags
}
# ---- ② app VM ×2(ネットワーク・セキュリティ・グループ(NSG)は security.tf、要塞は bastion.tf)----
module "app" {
source = "../modules/compute"
for_each = local.app_instances
compartment_ocid = var.compartment_ocid
name = "${local.name_prefix}-${each.key}" # tflab4-app-1 / tflab4-app-2
subnet_id = module.network.subnet_ids["app"]
nsg_ids = [oci_core_network_security_group.app.id]
assign_public_ip = false
enable_bastion_plugin = true
ssh_public_key = file(var.ssh_public_key_path)
user_data = file("${path.module}/cloud-init-app.yaml")
freeform_tags = local.common_tags
}
# ---- ③ LB:パブリック・サブネット(Public Subnet)に配置、バックエンド = app VM ×2 ----
module "lb" {
source = "../modules/lb"
compartment_ocid = var.compartment_ocid
name = "${local.name_prefix}-lb"
subnet_ids = [module.network.subnet_ids["public"]]
nsg_ids = [oci_core_network_security_group.lb.id]
# for 式:module.app(map)→ { app-1 = "10.4.2.139", app-2 = "10.4.2.11" }
backends = { for k, m in module.app : k => m.private_ip }
freeform_tags = local.common_tags
}
04-3tier/env/security.tf
# ------------------------------------------------------------
# ネットワーク・セキュリティ・グループ(NSG):「層」ごとに作成し、同じ層の VM で共用
# lb-nsg :ロード・バランサ(Load Balancer)
# app-nsg:app VM ×2
# ルールの「ソース/宛先」に別の NSG を指定可能(source_type = NETWORK_SECURITY_GROUP)
# → IP を書かずに済み、VM の増減や IP 変更があってもルールの修正は不要
#
# my_ip ──:80──→ [lb-nsg] LB ──:80──→ [app-nsg] VM ×2 ←──:22── 要塞(Bastion)
#
# checkov CKV_OCI_21(ingress ルールに stateless を要求)→ あえて対応せず、各ルールの skip コメントに理由を記載
# stateless = 接続状態を記録しないため、戻りトラフィック用のルールが別途必要(ルール数が倍、設定ミスしやすい)。利点は大量トラフィック時の性能
# ここはトラフィックが小さいため、デフォルトの stateful を使用
# ------------------------------------------------------------
# ---- lb-nsg ----
resource "oci_core_network_security_group" "lb" {
compartment_id = var.compartment_ocid
vcn_id = module.network.vcn_id
display_name = "${local.name_prefix}-lb-nsg"
freeform_tags = local.common_tags
}
# イングレス:ローカル PC の IP から LB の 80 へのアクセスのみ許可
resource "oci_core_network_security_group_security_rule" "lb_http_from_my_ip" {
#checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
network_security_group_id = oci_core_network_security_group.lb.id
direction = "INGRESS"
protocol = "6"
source = "${var.my_ip}/32"
source_type = "CIDR_BLOCK"
description = "HTTP from my IP"
tcp_options {
destination_port_range {
min = 80
max = 80
}
}
}
# エグレス:LB → app VM の 80(リクエスト転送 + ヘルスチェック)
# サブネット(Subnet)のセキュリティ・リスト(Security List)で全エグレスを許可済みのため、このルールがなくても通信は可能。NSG 単体で「LB は app にのみ送信」と示すために明記
resource "oci_core_network_security_group_security_rule" "lb_http_to_app" {
network_security_group_id = oci_core_network_security_group.lb.id
direction = "EGRESS"
protocol = "6"
destination = oci_core_network_security_group.app.id
destination_type = "NETWORK_SECURITY_GROUP"
description = "HTTP to app"
tcp_options {
destination_port_range {
min = 80
max = 80
}
}
}
# ---- app-nsg ----
resource "oci_core_network_security_group" "app" {
compartment_id = var.compartment_ocid
vcn_id = module.network.vcn_id
display_name = "${local.name_prefix}-app-nsg"
freeform_tags = local.common_tags
}
# SSH は要塞からの接続のみ受け付ける
# 要塞は app サブネット内にプライベート・エンドポイント(Private Endpoint、プライベート IP)を持ち、SSH はこの IP から入ってくる
resource "oci_core_network_security_group_security_rule" "app_ssh_from_bastion" {
#checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
network_security_group_id = oci_core_network_security_group.app.id
direction = "INGRESS"
protocol = "6"
source = "${oci_bastion_bastion.this.private_endpoint_ip_address}/32"
source_type = "CIDR_BLOCK"
description = "SSH from Bastion"
tcp_options {
destination_port_range {
min = 22
max = 22
}
}
}
# HTTP は LB からの接続のみ受け付ける(ソースは IP ではなく lb-nsg を指定)
# app VM 同士、要塞 → VM の 80 は引き続き通信不可
resource "oci_core_network_security_group_security_rule" "app_http_from_lb" {
#checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
network_security_group_id = oci_core_network_security_group.app.id
direction = "INGRESS"
protocol = "6"
source = oci_core_network_security_group.lb.id
source_type = "NETWORK_SECURITY_GROUP"
description = "HTTP from LB"
tcp_options {
destination_port_range {
min = 80
max = 80
}
}
}