はじめに
本記事では、Terraformを使用してOracle Cloudで下記図の環境を構築します。
構成が同じリソース(web01,web02など)は、v0.13から追加されたmoduleのループ(for_each, count)を使います。
DMZにロードバランサを配置し、別サブネットにWEBサーバ2台を配置します。
ロードバランサとWEBサーバの通信ポリシーは、OCIのネットワーク・セキュリティ・グループ(NSG)で定義します。
動作環境
- Terraform v0.13.2
- macOS Catalina v10.15.5 (Terraform実行環境)
- Oracle Cloud Infrastructure (以降OCIと呼称)
目次
- はじめに
- 動作環境
- 目次
- 最終的なディレクトリ構成とファイル
- Terraformインストール
- APIキーの追加
- OCI情報設定
- 仮想クラウドネットワーク(VCN)作成
- サブネット作成
- インスタンス作成
- ロードバランサ作成
- ネットワーク・セキュリティ・グループ作成
- Terraform実行(構築作業)
- おわりに
- 参考サイト
最終的なディレクトリ構成とファイル
本記事で作成するファイルの一覧です。
./
├── modules
│ ├── compute
│ │ ├── locals.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── subnet
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── vcn
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
|
└── oci_web
├── compute.tf
├── loadbalancer.tf
├── network.tf
├── nsg.tf
├── provider.tf
├── terraform.tfvars
└── variables.tf
terraform.tfvars以外は、GitHubにあります。
ご自身の情報をterraform.tfvarsに記載して、後述の"TerraformにOCI認証情報設定"の手順を実施いただくと、すぐに環境構築が完了します。
Terraformインストール
Download Terraformから、ご自身のOSに対応して実行ファイルをダウンロードしてください。
あとはPATHの通っているディレクトリに配置するだけです。
"terraform -v"でバージョンが表示されたらOKです。
$ wget https://releases.hashicorp.com/terraform/0.13.3/terraform_0.13.3_linux_amd64.zip
$ unzip ./terraform_0.13.3_linux_amd64.zip
$ sudo mv ./terraform /usr/local/bin/
$ terraform -v
APIキーの追加
terraformでOCIにアクセスするためにAPIキーを追加します。
手順は公式ドキュメントの通りです。
OCI情報設定
必要な情報は下記の通りです。
- コンパートメントOCID
- ユーザOCID
- テナンシOCID
- フィンガープリント
- リージョン名
大切な情報なので変数だけ定義して、実際の値はterraform.tfvarsに別出しします。
$ vi oci_web/variables.tf
oci_web/variables.tfのコードを見る
# provider identity parameters
variable "region" {
# List of regions: https://docs.cloud.oracle.com/iaas/Content/General/Concepts/regions.htm#ServiceAvailabilityAcrossRegions
description = "the OCI region where resources will be created"
type = string
}
# general oci parameters
variable "compartment_id" {
type = string
description = "compartment id where to create all resources"
}
variable "tenancy_ocid" {
type = string
description = "tenancy id where to create all resources"
}
variable "user_ocid" {
type = string
description = "user id where to access OCI API"
}
variable "fingerprint" {
type = string
description = "fingerprint id where to access OCI API"
}
variable "private_key_path" {
type = string
description = "private key path to to access OCI API"
}
oci_web/variables.tfで定義した変数に値を代入します。
OCI管理画面からご自身のパラメータを確認して代入してください。
fingerprintはAPIキーの追加で作成したものを使用します。
これらを代入しない場合はterraformコマンド実行時に求められます。
$ vi oci_web/terraform.tfvars
# general oci parameters
region = "ap-tokyo-1"
compartment_id = "ocid1.compartment.oc1.."
tenancy_ocid = "ocid1.tenancy.oc1.."
user_ocid = "ocid1.user.oc1.."
fingerprint = "ff:ff:ff:ff:ff"
private_key_path = "~/private.pem"
仮想クラウドネットワーク(VCN)作成
VCNを作成するためのファイルを作成します。
Terraform公式リポジトリのVCN作成サンプルを参考に、使いまわせるようにモジュール化しました。
$ vi modules/vcn/main.tf
modules/vcn/main.tfのコードを見る
locals {
anywhere = "0.0.0.0/0"
}
# VCN
resource "oci_core_vcn" "this" {
cidr_block = var.vcn.cidr_block
compartment_id = var.vcn.compartment_id
display_name = var.vcn.display_name
dns_label = var.vcn.dns_label
# true: destroyコマンドで削除不可 false: destroyコマンドで削除可能
lifecycle {
prevent_destroy = false
}
}
resource "oci_core_default_security_list" "this" {
# Default Security List forの中身を削除する。
manage_default_resource_id = oci_core_vcn.this.default_security_list_id
}
# Internet Gateway
resource "oci_core_internet_gateway" "this" {
compartment_id = var.vcn.compartment_id
# use "this" id
vcn_id = oci_core_vcn.this.id
display_name = "${var.label}_igw"
lifecycle {
prevent_destroy = false
}
}
# NAT Gateway
resource "oci_core_nat_gateway" "this" {
compartment_id = var.vcn.compartment_id
vcn_id = oci_core_vcn.this.id
display_name = "${var.label}_ngw"
lifecycle {
prevent_destroy = false
}
}
# route table for internet gateway
resource "oci_core_route_table" "igw_route" {
compartment_id = var.vcn.compartment_id
route_rules {
network_entity_id = oci_core_internet_gateway.this.id
destination = local.anywhere
}
vcn_id = oci_core_vcn.this.id
display_name = "${var.label}_route_igw"
lifecycle {
prevent_destroy = false
}
}
# route table for nat gateway
resource "oci_core_route_table" "ngw_route" {
compartment_id = var.vcn.compartment_id
# add nat gateway
route_rules {
network_entity_id = oci_core_nat_gateway.this.id
destination = local.anywhere
}
vcn_id = oci_core_vcn.this.id
display_name = "${var.label}_route_ngw"
lifecycle {
prevent_destroy = false
}
}
OCIではVCNを作成すると、自動的にセキュリティ・リストやDHCPオプションといったリソースが生成されます。
デフォルトのセキュリティ・リストは、22ポートを外部公開しており、セキュリティ的によろしくないので中身のルールを全て削除しています。
通信の制御は全て、後述のネットワーク・セキュリティ・グループで行います。
Note
デフォルトのセキュリティ・リスト本体は、OCIの仕様で削除できない
上記modules/vcn/main.tfで使用する変数を定義します。
ここでは変数名と型だけ宣言し、実際の値はモジュールを呼ぶ際に指定します。
$ vi modules/vcn/variables.tf
modules/vcn/variables.tfのコードを見る
# VCN
variable "vcn" {
type = object({
compartment_id = string
cidr_block = string
display_name = string
dns_label = string
})
default = {
compartment_id = null
cidr_block = "192.168.0.0/16"
display_name = "oci_vcn"
dns_label = "ocivcn"
}
}
# prefix for resource name
variable "label" {
type = string
default = "oci"
}
outputs.tfはmoduleで作ったリソースの情報にアクセスするための定義です。
例えば、新たにサブネットを定義したいとします。
サブネットをどのVCNに作成するか指定する必要があります。
指定するには、VCNのIDを使いますが、outputで定義してあげないと参照できません。
今回はoutput "instance"で、VCNリソース(oci_core_vcn)をそのままvalueに渡しているので、oci_core_vcnの全ての情報にアクセス可能です。
Note
モジュール使用者がミスをしないように、あえて参照できる情報を限定することができます。(例) idのみ参照可能にする場合
value = oci_core_vcn.this.id
$ vi modules/vcn/outputs.tf
modules/vcn/outputs.tfのコードを見る
output "instance" {
description = "vcn that is created"
value = oci_core_vcn.this
}
output "nat_gateway_id" {
description = "id of nat gateway if it is created"
value = oci_core_nat_gateway.this.id
}
output "internet_gateway_id" {
description = "id of internet gateway if it is created"
value = oci_core_internet_gateway.this.id
}
output "igw_route_id" {
description = "id of internet gateway route table"
value = oci_core_route_table.igw_route.id
}
output "ngw_route_id" {
description = "id of VCN NAT gateway route table"
value = oci_core_route_table.ngw_route.id
}
$ vi oci_web/vcn.tf
# VCN
locals {
vcn = {
compartment_id = var.compartment_id
cidr_block = "192.100.0.0/16"
display_name = "terraform_vcn"
dns_label = "terraformvcn"
}
label = "terraform"
}
module "vcn" {
source = "../modules/vcn"
vcn = local.vcn
label = local.label
}
サブネット作成
次にサブネット用のファイルを書いていきます。
今回はpublicサブネットとprivateサブネットの2つのリソースを作ります。
publicサブネットは、直接インターネットからデータを受信できますが、privateは必ずprivateサブネットのLBを経由します。
VCN同様、モジュール化して他で使いまわせるようにします。
$ vi modules/subnet/main.tf
modules/subnet/main.tfのコードを見る
# subents
resource "oci_core_subnet" "this" {
for_each = var.subnets
compartment_id = each.value.compartment_id
display_name = each.key
dns_label = each.value.dns_label
vcn_id = each.value.vcn_id
cidr_block = each.value.cidr_block
prohibit_public_ip_on_vnic = each.value.prohibit_public_ip_on_vnic
route_table_id = each.value.route_table_id
dhcp_options_id = each.value.dhcp_options_id
# use Default Security List
security_list_ids = each.value.security_list_ids
lifecycle {
prevent_destroy = false
}
}
このモジュールの特徴は、for_eachでループさせていることです。
呼び出す時に、1つのmoduleで複数のリソースを作成するためにこのように書きました。
VCNモジュールはこのように作成しなかったので、複数のVCNを作成したい場合、その分だけmoduleを定義する必要があります。(=記述量が増える)
Note
Terraform v0.13では、module内で簡単にループができるので、実はそんなに記述量が増えない....
モジュールで使用する変数を定義します。
前述した通り、このモジュールでは1つのmoduleで複数リソースを作るようにしたいで、変数はmap(object)型です。
このようにすることで、呼びだすときには1つの変数(この場合は"subnets")に複数リソースのパラメータを書くことが可能になります。
$ vi modules/subnet/variables.tf
# subnets variables
variable "subnets" {
type = map(object({
compartment_id = string,
dns_label = string,
vcn_id = string,
cidr_block = string,
prohibit_public_ip_on_vnic = bool, # true: private subnet, false: pubilc subnet
route_table_id = string,
dhcp_options_id = string,
security_list_ids = list(string),
}))
}
参照できる情報をoutputで定義します。
$ vi modules/subnet/outputs.tf
output "instances" {
description = "The subnet(s) created/managed."
value = {
for i in oci_core_subnet.this:
i.display_name => i
}
}
上記のように書くことで、実際に取得できる値はこのようにmap型になります。
"key名 => 値"という意味なので、サブネット名とサブネット情報が紐づきます。
output取得イメージ
{
"private" {
id = hogehoge
cidr_block = hogehoge
....
}
"public" {
id = hogehoge
cidr_block = hogehoge
....
}
}
moduleを書いて、リソースの実際の値を設定します。
ここで注意するのは、"module.vcn.instance"や"module.vcn.igw_route_id"と書くことで、上で作ったVCNの情報を参照していることです。
あとは、cidrsubnet関数を利用して、動的にcidr_blockを作成しているのもポイントです。
このように書くことで、VCNでcidr_blockの変更があっても、修正する必要がないですね。
$ vi oci_web/subnet.tf
oci_web/subnet.tfのコードを見る
# subnets
locals {
public_cidr_block = cidrsubnet(module.vcn.instance.cidr_block, 8, 100) # = "192.100.100.0/24"
private_cidr_block = cidrsubnet(module.vcn.instance.cidr_block, 8, 200) # = "192.100.200.0/24"
subnets = {
public_subnet = {
compartment_id = var.compartment_id
dns_label = null
vcn_id = module.vcn.instance.id
cidr_block = local.public_cidr_block
prohibit_public_ip_on_vnic = false
route_table_id = module.vcn.igw_route_id
dhcp_options_id = module.vcn.instance.default_dhcp_options_id
security_list_ids = [module.vcn.instance.default_security_list_id]
}
private_subnet = {
compartment_id = var.compartment_id
dns_label = null
vcn_id = module.vcn.instance.id
cidr_block = local.private_cidr_block
prohibit_public_ip_on_vnic = true
route_table_id = module.vcn.ngw_route_id
dhcp_options_id = module.vcn.instance.default_dhcp_options_id
security_list_ids = [module.vcn.instance.default_security_list_id]
}
}
}
module "subnets" {
source = "../modules/subnet"
subnets = local.subnets
}
$ vi modules/compute/main.tf
modules/compute/main.tfのコードを見る
# availability domains
data "oci_identity_availability_domains" "this" {
compartment_id = var.compartment_id
}
# Instance
resource "oci_core_instance" "this" {
for_each = var.instances
compartment_id = each.value.compartment_id
availability_domain = each.value.ad != null ? data.oci_identity_availability_domains.this.availability_domains[each.value.ad].name : data.oci_identity_availability_domains.this.availability_domains[local.instances_default.ad].name
fault_domain = each.value.fault_domain != null ? local.fault_domain[each.value.fault_domain] : local.fault_domain[local.instances_default.fault_domain] # = FAULT-DOMAIN-x
display_name = each.key
shape = each.value.shape != null ? each.value.shape : local.instances_default.shape
create_vnic_details {
assign_public_ip = each.value.assign_public_ip
private_ip = each.value.private_ip
# nic名はインスタンス名と同じにする
display_name = each.key
nsg_ids = each.value.nsg_ids
subnet_id = each.value.subnet_id
}
metadata = {
ssh_authorized_keys = file(each.value.ssh_authorized_keys)
}
source_details {
source_id = each.value.source_id != null ? each.value.source_id : local.instances_default.source_id
source_type = each.value.source_type != null ? each.value.source_type : local.instances_default.source_type
boot_volume_size_in_gbs = each.value.boot_volume_size_in_gbs
}
# true: destroyコマンドで削除不可 false: destroyコマンドで削除可能
lifecycle {
prevent_destroy = false
}
}
$ vi modules/compute/locals.tf
modules/compute/locals.tfのコードを見る
# parameters for create instances
locals {
# Set the number of fault domain your reagion
number_fault_domain = 3
# If the number of FDs increases, increase the definition
fault_domain = [
"FAULT-DOMAIN-1",
"FAULT-DOMAIN-2",
"FAULT-DOMAIN-3",
]
}
# default parameters
locals {
instances_default = {
region = "ap-tokyo-1"
compartment_id = null
ad = 0
fault_domain = 0
private_ip = null
nsg_ids = null
shape = null
# see https://docs.cloud.oracle.com/en-us/iaas/images/
# this id is Oracle-provided image "Oracle-Linux-7.8-2020.08.26-0"
source_id = "ocid1.image.oc1.ap-tokyo-1.aaaaaaaadr3nqxb3xmunjeqvm5o5ywj7posqxwei6k3f7boytjfcpurb2a"
source_type = "image"
assign_public_ip = false
ssh_authorized_keys = null,
boot_volume_size_in_gbs = 50
}
}
Note
今回はterraformnお動作検証が目的なのでprevent_destroy = falseにしてます。
モジュールで使用する変数を定義します。
$ vi modules/compute/variables.tf
modules/compute/variables.tfのコードを見る
# Instance variables
variable "instances" {
type = map(object({
region = string,
compartment_id = string,
ad = number,
fault_domain = number,
private_ip = string,
shape = string,
nsg_ids = list(string),
subnet_id = string,
source_id = string,
source_type = string,
assign_public_ip = bool,
ssh_authorized_keys = string,
boot_volume_size_in_gbs = number,
}))
}
variable "compartment_id" {
type = string
description = "compartment id where to create all resources"
}
モジュールを呼びだしてインスタンスリソースを作成します。
$ vi oci_web/compute.tf
oci_web/compute.tfのコードを見る
# compute
locals {
# 参考URL https://docs.us-phoenix-1.oraclecloud.com/images/
# 下記はOracle-provided image "Oracle-Linux-7.8-2020.08.26-0"を選択している
image_oracle_linux7 = "ocid1.image.oc1.ap-tokyo-1.aaaaaaaadr3nqxb3xmunjeqvm5o5ywj7posqxwei6k3f7boytjfcpurb2a"
web_parameters = {
region = var.region
compartment_id = var.compartment_id
ad = null
fault_domain = null
private_ip = null
shape = "VM.Standard2.1"
nsg_ids = [module.network_security_groups.nsgs.nsg-web.id]
subnet_id = module.subnets.instances.private_subnet.id
source_id = local.image_oracle_linux7
source_type = "image"
assign_public_ip = false
ssh_authorized_keys = "~/.ssh/id_rsa.pub"
boot_volume_size_in_gbs = 50
}
}
module "computes" {
source = "../modules/compute"
count = 2
compartment_id = var.compartment_id
# mapのkeyがそのままインスタンス名になる
# format("web%02d", count.index + 1) = web01
instances = {
format("web%02d", count.index + 1) = local.web_parameters
}
}
ポイントは、mapのキー名をformat関数で動的に指定していることです。
これがそのままインスタンス名になるので、少し工夫しました。
注意点ですが、仮にこれをコピペして使う際には、ssh_authorized_keysの値を変更する必要があります。
opensslコマンドなどで秘密鍵と公開鍵を用意して、公開鍵のほうを指定してください。
Note
module1つで複数のリソースが作成できると書いておきながら、countでループさせてます。。。。
モジュール側でmap型にする必要はなく、ここでループするだけで、いくつでもリソースが作成できます。
今回はキー名を動的に指定するために試行錯誤した結果、こうなりました。
ロードバランサ作成
はい、ロードバランサのモジュールを作成して。。。。
実はOracleがGitHubで公開してるモジュールがあるんですよね。
ここのモジュールを使って、ロードバランサのリソースを作成します。
Note
Terraform Registryには各クラウドサービスのモジュールが公開されており、すぐにリソースを作成することができます。
自分でモジュールを作成する前にRegistryの存在を知りたかったです(´・ω・`)
$ vi oci_web/loadbalancer.tf
oci_web/loadbalancer.tfのコードを見る
# load balancer
locals {
lb_options = {
display_name = "terraform_lb"
compartment_id = null
shape = "10Mbps-Micro"
subnet_ids = [module.subnets.instances.public_subnet.id]
private = false
nsg_ids = [module.network_security_groups.nsgs.nsg-lb.id]
defined_tags = null
freeform_tags = null
}
health_checks = {
basic_http = {
protocol = "HTTP"
interval_ms = 1000
port = 80
response_body_regex = ".*"
retries = 3
return_code = 200
timeout_in_millis = 3000
url_path = "/"
}
}
backend_sets = {
web = {
policy = "ROUND_ROBIN"
health_check_name = "basic_http"
enable_persistency = false
enable_ssl = false
cookie_name = null
disable_fallback = null
certificate_name = null
verify_depth = null
verify_peer_certificate = null
backends = {
web01 = {
ip = cidrhost(local.private_cidr_block, 2)
port = 80
backup = false
drain = false
offline = false
weight = 1
},
web02 = {
ip = cidrhost(local.private_cidr_block, 3)
port = 80
backup = false
drain = false
offline = false
weight = 1
}
}
}
}
listeners = {
web = {
default_backend_set_name = "web"
port = 80
protocol = "HTTP"
idle_timeout = 180
hostnames = null
path_route_set_name = null
rule_set_names = null
enable_ssl = false
certificate_name = null
verify_depth = 5
verify_peer_certificate = true
}
}
}
module "load_balancer" {
source = "git@github.com:oracle-terraform-modules/terraform-oci-tdf-lb.git"
default_compartment_id = var.compartment_id
lb_options = local.lb_options
health_checks = local.health_checks
backend_sets = local.backend_sets
listeners = local.listeners
}
ネットワーク・セキュリティ・グループ作成
セキュリティ・リストはサブネット単位で制御し、ネットワーク・セキュリティ・グループは各インスタンスごとに制御します。
公式では、ネットワーク・セキュリティ・グループ(NSG)を推奨しているので、本記事でもNSGで通信ポリシーを定義します。
Oracle Cloud Infrastructureドキュメント
- セキュリティ・リスト: このトピックで説明します。これは、ネットワーキング・サービスが提供する元のタイプの仮想ファイアウォールです。
- ネットワーク・セキュリティ・グループ: セキュリティ・リストよりも推奨される別のタイプの仮想ファイアウォールです。
$ vi oci_web/nsg.tf
oci_web/nsg.tfのコードを見る
# general parameter
locals {
protocol = {
ALL = "all"
ICMP = "1"
TCP = "6"
UDP = "17"
ICMPv6 = "58"
}
ipaddr = {
anywhere = "0.0.0.0/0"
}
}
# NSG同士で通信を許可するため、先にNSGだけ先に作成する
locals {
empty_nsg = {
compartment_id = null
defined_tags = null
freeform_tags = null
ingress_rules = null
egress_rules = null
description = null
}
# 各サーバのネットワークセキュリティグループを定義
# 通信ポリシーでは、相互参照があるため先に作成する
nsgs = {
nsg-web = local.empty_nsg
nsg-lb = local.empty_nsg
}
}
#
# NOTE: 通信ポリシー(standalone_nsg_rules)で、相互参照があるため先に作成する
# 相互参照がない場合は、NSGと通信ルールはまとめて作成可能
#
# nsgsのリストを全て作成する
module "network_security_groups" {
source = "git@github.com:oracle-terraform-modules/terraform-oci-tdf-network-security.git"
default_compartment_id = var.compartment_id
vcn_id = module.vcn.instance.id
nsgs = local.nsgs
}
# NSG作成後にルールを追加する
locals {
rules = {
nsg-web = {
egress_rules = [{
nsg_id = module.network_security_groups.nsgs.nsg-web.id
description = "Egressは全て許可する。"
stateless = false
protocol = local.protocol.ALL
dst = local.ipaddr.anywhere
dst_type = "CIDR_BLOCK"
src_port = null
dst_port = null
icmp_code = null
icmp_type = null
}]
ingress_rules = [{
nsg_id = module.network_security_groups.nsgs.nsg-web.id
description = "LBからHTTP通信を許可する"
stateless = false
protocol = local.protocol.TCP
src = module.network_security_groups.nsgs.nsg-lb.id
src_type = "NETWORK_SECURITY_GROUP"
src_port = null
dst_port = {
min = 80
max = 80
}
icmp_code = null
icmp_type = null
}]
}
nsg-lb = {
egress_rules = [{
nsg_id = module.network_security_groups.nsgs.nsg-lb.id
description = "Egressは全て許可する。"
stateless = false
protocol = local.protocol.ALL
dst = local.ipaddr.anywhere
dst_type = "CIDR_BLOCK"
src_port = null
dst_port = null
icmp_code = null
icmp_type = null
}]
ingress_rules = [{
nsg_id = module.network_security_groups.nsgs.nsg-lb.id
description = "外部らHTTP通信を許可する"
stateless = false
protocol = local.protocol.TCP
src = local.ipaddr.anywhere
src_type = "CIDR_BLOCK"
src_port = null
dst_port = {
min = 80
max = 80
}
icmp_code = null
icmp_type = null
}]
}
}
}
module "standalone_nsg_rules" {
for_each = local.rules
source = "git@github.com:oracle-terraform-modules/terraform-oci-tdf-network-security.git"
default_compartment_id = var.compartment_id
vcn_id = module.vcn.instance.id
standalone_nsg_rules = {
ingress_rules = each.value.ingress_rules
egress_rules = each.value.egress_rules
}
}
LBと同じようにOracleがGitHubで公開しているモジュールを使いました。
注意点として、NSG内で別のNSGからのアクセスを許可する場合は、ルールより先にNSG本体を作成しないとエラーが発生する可能性があります。
Terraform実行(構築作業)
長くなりましたが、あとはterraformコマンドを実行するだけです。
$ terraform init
$ terraform apply
OCI管理画面からリソースが正常に作成されたことを確認できると思います。
一通り確認が済みましたらリソースを削除しておきます。
$ terraform destroy
おわりに
本来ならサーバ起動時にcloud-initスクリプトで、Apacheなどを起動させてブラウザからアクセス確認がしたかったのですが、それについては別の機会に書きます。
terraformはモジュール化するまで少々大変ですが、1度作成してしまうと何度も使い回せて非常に便利ですね。
Terraform Registryからモジュールを取得するなら、さらに工数を削減できそうです。
今回はOCIで実施しましたが、AWSをはじめとして様々なクラウドサービスに対応してるのもポイント高いです。
同じ記事を自分のブログでも公開してます。