はじめに
前回まではObject StorageやNotificationsなど、単体サービスをTerraformで作成してみました。
今回は複数サービスを作成する必要があるWebサーバ環境をTerraformでOCI上に構築します。
作成するリソースはVCN、Internet Gateway、Route Table、Public Subnet、Network Security Group、Compute Instanceです。
さらに、OCIコンソールから手動で設定を変更し、OCI Resource Managerのドリフト検出でTerraform管理状態との差分を確認します。
今回作るもの
構成は以下です。
VCN
├─ Internet Gateway
├─ Route Table
├─ Public Subnet
├─ Security List
│ └─ outbound許可
├─ Network Security Group
│ └─ TCP/80 inbound許可
└─ Compute Instance
└─ cloud-initでnginxを起動
なぜObject Storageより少し難しいのか
Object Storageバケットの作成は、基本的には1つのリソースを作るだけでした。
一方でWebサーバ環境では、Compute Instanceだけを作ってもブラウザからアクセスできません。
以下のような複数の要素が必要になります。
- VCN
- Public Subnet
- Internet Gateway
- Route Table
- Security List
- Network Security Group
- Compute Instance
- OS上のWebサーバ設定
今回の例ではSSH接続は行わず、cloud-initでnginxを自動起動して、ブラウザからHTTPアクセスできることを確認します。
ファイル構成
以下のファイルを作成します。
oci-webserver-rm-drift/
├── provider.tf
├── variables.tf
├── main.tf
├── outputs.tf
└── cloud-init.yaml
provider.tfを作成する
Resource ManagerでOCI Providerを使う場合、基本的には region を指定します。
terraform {
required_version = ">= 1.5.0"
required_providers {
oci = {
source = "oracle/oci"
version = ">= 6.0.0, < 9.0.0"
}
}
}
provider "oci" {
region = var.region
}
variables.tfを作成する
variable "region" {
description = "OCI region"
type = string
}
variable "compartment_ocid" {
description = "Compartment OCID"
type = string
}
variable "prefix" {
description = "Resource name prefix"
type = string
default = "tf-web"
}
variable "vcn_cidr" {
description = "VCN CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidr" {
description = "Public subnet CIDR block"
type = string
default = "10.0.1.0/24"
}
variable "instance_shape" {
description = "Compute instance shape"
type = string
default = "VM.Standard.E2.1.Micro"
}
main.tfを作成する
NSGのルールでは、TCPの80番ポートをインターネットから許可しています。OCI ProviderのNSGセキュリティルールでは、TCPの宛先ポート範囲を指定できます。
data "oci_identity_availability_domains" "ads" {
compartment_id = var.compartment_ocid
}
data "oci_core_images" "oracle_linux" {
compartment_id = var.compartment_ocid
operating_system = "Oracle Linux"
operating_system_version = "8"
shape = var.instance_shape
sort_by = "TIMECREATED"
sort_order = "DESC"
}
resource "oci_core_vcn" "web_vcn" {
compartment_id = var.compartment_ocid
cidr_block = var.vcn_cidr
display_name = "${var.prefix}-vcn"
dns_label = "tfwebvcn"
}
resource "oci_core_internet_gateway" "web_igw" {
compartment_id = var.compartment_ocid
vcn_id = oci_core_vcn.web_vcn.id
display_name = "${var.prefix}-igw"
enabled = true
}
resource "oci_core_route_table" "public_rt" {
compartment_id = var.compartment_ocid
vcn_id = oci_core_vcn.web_vcn.id
display_name = "${var.prefix}-public-rt"
route_rules {
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
network_entity_id = oci_core_internet_gateway.web_igw.id
}
}
resource "oci_core_security_list" "public_sl" {
compartment_id = var.compartment_ocid
vcn_id = oci_core_vcn.web_vcn.id
display_name = "${var.prefix}-public-sl"
egress_security_rules {
protocol = "all"
destination = "0.0.0.0/0"
}
}
resource "oci_core_subnet" "public_subnet" {
compartment_id = var.compartment_ocid
vcn_id = oci_core_vcn.web_vcn.id
cidr_block = var.public_subnet_cidr
display_name = "${var.prefix}-public-subnet"
dns_label = "public"
route_table_id = oci_core_route_table.public_rt.id
security_list_ids = [oci_core_security_list.public_sl.id]
prohibit_public_ip_on_vnic = false
}
resource "oci_core_network_security_group" "web_nsg" {
compartment_id = var.compartment_ocid
vcn_id = oci_core_vcn.web_vcn.id
display_name = "${var.prefix}-web-nsg"
}
resource "oci_core_network_security_group_security_rule" "allow_http" {
network_security_group_id = oci_core_network_security_group.web_nsg.id
direction = "INGRESS"
protocol = "6"
source = "0.0.0.0/0"
source_type = "CIDR_BLOCK"
stateless = false
description = "Allow HTTP from Internet"
tcp_options {
destination_port_range {
min = 80
max = 80
}
}
}
resource "oci_core_instance" "web_instance" {
compartment_id = var.compartment_ocid
availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
display_name = "${var.prefix}-instance"
shape = var.instance_shape
create_vnic_details {
subnet_id = oci_core_subnet.public_subnet.id
assign_public_ip = true
display_name = "${var.prefix}-vnic"
nsg_ids = [oci_core_network_security_group.web_nsg.id]
}
source_details {
source_type = "image"
source_id = data.oci_core_images.oracle_linux.images[0].id
}
metadata = {
user_data = base64encode(file("${path.module}/cloud-init.yaml"))
}
}
cloud-init.yamlを作成する
Compute Instance起動時にnginxをインストールして起動します。
OCIのCompute Instanceでは、metadataの user_data にbase64エンコードしたcloud-initデータを渡せます。(registry.terraform.io)
#cloud-config
package_update: true
packages:
- nginx
runcmd:
- systemctl enable --now nginx
- firewall-cmd --permanent --add-service=http
- firewall-cmd --reload
- echo '<html><body><h1>Hello from Terraform on OCI</h1><p>This web server was created by OCI Resource Manager.</p></body></html>' > /usr/share/nginx/html/index.html
outputs.tfを作成する
Apply後にブラウザでアクセスしやすいよう、パブリックIPとURLを出力します。
output "instance_public_ip" {
value = oci_core_instance.web_instance.public_ip
}
output "web_url" {
value = "http://${oci_core_instance.web_instance.public_ip}"
}
output "nsg_id" {
value = oci_core_network_security_group.web_nsg.id
}
Terraform構成をZIP化する
Resource Managerにアップロードするため、ファイルをZIP化します。
Resource ManagerでローカルのTerraform構成を使う場合、Terraformファイル一式をZIP化してStack作成時にアップロードできます。
terraform-oci-webserver.zipというファイルを作成しました。
Resource ManagerでStackを作成する
OCIコンソールで以下に移動します。
Terraform構成ファイルとして、先ほど作成した terraform-oci-webserver.zip をアップロードします。

VCNやComputeを作成するので、それにあたって値の確認が必要です。
必要でなければデフォルト値のまま進めます。

Planを実行する
Stack作成後、まずPlanを実行します。
今回の例では、以下のようなリソースが作成予定になります。
- VCN
- Internet Gateway
- Route Table
- Security List
- Public Subnet
- Network Security Group
- NSG Security Rule
- Compute Instance
ログを確認すると各コンポーネントが作成予定となっています。
8コンポーネントが作成予定になっていることがログの最後から確認できました。

Applyを実行する
Planの内容を確認したらApplyを実行します。
Applyが成功すると、Outputsに web_url が表示されます。
ブラウザでWebページを確認する
Outputsに表示されたURLへアクセスします。
http://<instance_public_ip>
以下のような画面が表示されたので成功です!
Hello from Terraform on OCI
This web server was created by OCI Resource Manager.
OCIコンソールから、作成されたリソースも確認することができました。

NSGルールを手動変更してドリフトを作る
ここから、あえてTerraform管理外の手動変更を行います。
今回はOCIコンソールから、Terraformで作成したNSGのHTTP許可ルールを削除します。

このルールをOCIコンソールから手動で削除します。
この時点で、Terraformの状態ファイル上は「TCP/80のNSGルールが存在する」ことになっています。
しかし、実際のOCI上では手動で削除されています。
この差分がドリフトです。

Resource Managerでドリフト検出を実行する
Resource ManagerのStack詳細画面に戻り、ドリフト検出を実行します。
Resource Managerのドリフト検出は、実際のインフラ状態とStackの最後に実行された構成との差分を確認できます。

ドリフト検出結果を確認する
手動で削除したNSG Security Ruleが、Terraformの状態と実際のOCI上の状態で一致していないことが分かります。
この結果から、Terraform管理しているリソースをOCIコンソールで手動変更すると、管理状態との差分が発生することが確認できました。
補足: 手動削除後のWebアクセス
NSGのHTTPルールを削除したため、ブラウザからWebサーバへアクセスできなくなります。

これは想定通りです。
TerraformとしてはHTTPルールがあるつもりですが、実際のOCI上では削除されているためです。
Destroyで削除する
検証が終わったら、Resource ManagerからDestroyを実行します。
Destroyが成功すると、Terraformで作成したリソースも削除されます。
まとめ
今回は、単体リソースではなく、ネットワークとComputeを組み合わせてWebサーバ環境を作成しました。
Terraformで複数リソースをつなげて作ることで、依存関係のイメージが少しつかめました。
作成後にNSGルールを手動変更するとTerraformの状態と実際のOCIリソースに差分が出て、そう程度通りに動かなくなるところも確認することができました。
ドリフト検出を活用することでもともとの構成を維持することもできることがわかり、クラウドでサーバー構築する大きなメリットを感じることができました。






