[OCI]Terraform v0.13で冗長構成のWEBサーバを構築する

<a id="markdown-はじめに" name="はじめに"></a>
# はじめに
本記事では、Terraformを使用してOracle Cloudで下記図の環境を構築します。
構成が同じリソース(web01,web02など)は、v0.13から追加されたmoduleのループ(for_each, count)を使います。

DMZにロードバランサを配置し、別サブネットにWEBサーバ２台を配置します。
ロードバランサとWEBサーバの通信ポリシーは、OCIのネットワーク・セキュリティ・グループ(NSG)で定義します。

<img src=https://grimoire.tokyo/wp-content/uploads/2020/10/oci_terraform.png>

<a id="markdown-動作環境" name="動作環境"></a>
# 動作環境
- Terraform v0.13.2
- macOS Catalina v10.15.5 (Terraform実行環境)
- Oracle Cloud Infrastructure  (以降OCIと呼称)

<a id="markdown-目次" name="目次"></a>
# 目次
<!-- TOC -->

- [はじめに](#はじめに)
- [動作環境](#動作環境)
- [目次](#目次)
- [最終的なディレクトリ構成とファイル](#最終的なディレクトリ構成とファイル)
- [Terraformインストール](#terraformインストール)
- [APIキーの追加](#apiキーの追加)
- [OCI情報設定](#oci情報設定)
- [仮想クラウドネットワーク(VCN)作成](#仮想クラウドネットワークvcn作成)
- [サブネット作成](#サブネット作成)
- [インスタンス作成](#インスタンス作成)
- [ロードバランサ作成](#ロードバランサ作成)
- [ネットワーク・セキュリティ・グループ作成](#ネットワーク・セキュリティ・グループ作成)
- [Terraform実行(構築作業)](#terraform実行構築作業)
- [おわりに](#おわりに)
- [参考サイト](#参考サイト)

<!-- /TOC -->

<a id="markdown-最終的なディレクトリ構成とファイル" name="最終的なディレクトリ構成とファイル"></a>
# 最終的なディレクトリ構成とファイル
本記事で作成するファイルの一覧です。

```shell

./
├── 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](https://github.com/yuki9431/terraform_oci_web)にあります。
ご自身の情報をterraform.tfvarsに記載して、後述の"**TerraformにOCI認証情報設定**"の手順を実施いただくと、すぐに環境構築が完了します。

<a id="markdown-terraformインストール" name="terraformインストール"></a>
# Terraformインストール
[Download Terraform](https://www.terraform.io/downloads.html)から、ご自身のOSに対応して実行ファイルをダウンロードしてください。
あとはPATHの通っているディレクトリに配置するだけです。
"terraform -v"でバージョンが表示されたらOKです。

```shell
$ 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
```

<a id="markdown-apiキーの追加" name="apiキーの追加"></a>
# APIキーの追加
terraformでOCIにアクセスするためにAPIキーを追加します。
手順は[公式ドキュメント](https://docs.cloud.oracle.com/ja-jp/iaas/Content/API/Concepts/apisigningkey.htm)の通りです。

<a id="markdown-oci情報設定" name="oci情報設定"></a>
# OCI情報設定
必要な情報は下記の通りです。
- コンパートメントOCID
- ユーザOCID
- テナンシOCID
- フィンガープリント
- リージョン名

大切な情報なので変数だけ定義して、実際の値はterraform.tfvarsに別出しします。

```shell
$ vi oci_web/variables.tf
```
<details><summary>oci_web/variables.tfのコードを見る</summary><div>

```bash
# 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"
}

```
</div></details>

oci_web/variables.tfで定義した変数に値を代入します。
OCI管理画面からご自身のパラメータを確認して代入してください。
fingerprintは**APIキーの追加**で作成したものを使用します。
これらを代入しない場合はterraformコマンド実行時に求められます。

```shell
$ vi oci_web/terraform.tfvars
```

```bash
# 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"
```

<a id="markdown-仮想クラウドネットワークvcn作成" name="仮想クラウドネットワークvcn作成"></a>
# 仮想クラウドネットワーク(VCN)作成

VCNを作成するためのファイルを作成します。
Terraform[公式リポジトリ](https://github.com/terraform-providers/terraform-provider-oci/tree/master/examples/networking)のVCN作成サンプルを参考に、使いまわせるようにモジュール化しました。

```shell
$ vi modules/vcn/main.tf
```
<details><summary>modules/vcn/main.tfのコードを見る</summary><div>

```bash
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
  }
}
```
</div></details>

OCIではVCNを作成すると、自動的にセキュリティ・リストやDHCPオプションといったリソースが生成されます。
デフォルトのセキュリティ・リストは、22ポートを外部公開しており、セキュリティ的によろしくないので中身のルールを全て削除しています。
通信の制御は全て、後述のネットワーク・セキュリティ・グループで行います。

> *Note*
> デフォルトのセキュリティ・リスト本体は、OCIの仕様で削除できない

---
上記modules/vcn/main.tfで使用する変数を定義します。
ここでは変数名と型だけ宣言し、実際の値はモジュールを呼ぶ際に指定します。

```shell
$ vi modules/vcn/variables.tf
```

<details><summary>modules/vcn/variables.tfのコードを見る</summary><div>

```bash
# 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"
}
```
</div></details>

---
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

```shell
$ vi modules/vcn/outputs.tf
```

<details><summary>modules/vcn/outputs.tfのコードを見る</summary><div>

```bash
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
}
```

</div></details>
---
上記で作成したモジュールを呼びだして、VCNを作成するための設定を書きます。
今まで長々とモジュールのコードを書いてきましたが、モジュール化してあげると、次回以降は下記の記述のみで済みます。
localsで必要な値を代入したあとに、moduleに渡してます。
必要な値は、modules/vcn/variables.tfで宣言した変数ですね。
漏れがあるとエラーが発生するので、必要ないパラーメータならnullを代入しておきましょう。

```shell
$ vi oci_web/vcn.tf
```

```bash
# 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
}
```

<a id="markdown-サブネット作成" name="サブネット作成"></a>
# サブネット作成
次にサブネット用のファイルを書いていきます。
今回はpublicサブネットとprivateサブネットの2つのリソースを作ります。
publicサブネットは、直接インターネットからデータを受信できますが、privateは必ずprivateサブネットのLBを経由します。

VCN同様、モジュール化して他で使いまわせるようにします。

```shell
$ vi modules/subnet/main.tf 
```
<details><summary>modules/subnet/main.tfのコードを見る</summary><div>

```bash
# 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
  }
}
```

</div></details>

このモジュールの特徴は、for_eachでループさせていることです。
呼び出す時に、1つのmoduleで複数のリソースを作成するためにこのように書きました。
VCNモジュールはこのように作成しなかったので、複数のVCNを作成したい場合、その分だけmoduleを定義する必要があります。(=記述量が増える)

> *Note*
> Terraform v0.13では、module内で簡単にループができるので、実はそんなに記述量が増えない....
> 

---

モジュールで使用する変数を定義します。
前述した通り、このモジュールでは1つのmoduleで複数リソースを作るようにしたいで、変数はmap(object)型です。
このようにすることで、呼びだすときには1つの変数(この場合は"subnets")に複数リソースのパラメータを書くことが可能になります。

```shell
$ vi modules/subnet/variables.tf
```
```bash
# 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で定義します。

```shell
$ vi modules/subnet/outputs.tf
```

```bash
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の変更があっても、修正する必要がないですね。

```shell
$ vi oci_web/subnet.tf
```
<details><summary>oci_web/subnet.tfのコードを見る</summary><div>

```bash
# 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
}
```
</div></details>
<a id="markdown-インスタンス作成" name="インスタンス作成"></a>
# インスタンス作成
次にインスタンス作成用のモジュールを書いていきます。
作りはサブネットモジュールと同じで、呼び出し時に1つのmoduleで複数のリソースを作成可能です。

```
$ vi modules/compute/main.tf
```
<details><summary>modules/compute/main.tfのコードを見る</summary><div>

```bash
# 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
  }
}
```
</div></details>

```shell
$ vi modules/compute/locals.tf
```

<details><summary>modules/compute/locals.tfのコードを見る</summary><div>

```bash
# 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
  }
}
```
</div></details>
oci_core_instanceの一番下にprevent_destroyを書いています。
lifecycleのprevent_destroyをtrueにしてあげると、terraformコマンドから削除できなくなります。
何故これを入れているのかというと、本番運用を想定している場合、サーバを丸ごと削除することは少ないと思います。(もちろんシステムによりますが)
trueにするとオペレーションミスで、サーバを削除してしまうリスクを下げることができます。
当たり前ですが、OCI管理画面から手動でリソースを削除することは可能です。

> *Note*
> 今回はterraformnお動作検証が目的なのでprevent_destroy = falseにしてます。

---

モジュールで使用する変数を定義します。

```shell
$ vi modules/compute/variables.tf
```

<details><summary>modules/compute/variables.tfのコードを見る</summary><div>

```bash
# 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"
}

```
</div></details>
ここもサブネットモジュール同様map(object)型にすることで、まとめて複数のリソースを作成できるようにしました。
mapのキー名がそのままインスタンスの表示名になるようになってます。

---
モジュールを呼びだしてインスタンスリソースを作成します。

```shell
$ vi oci_web/compute.tf
```

<details><summary>oci_web/compute.tfのコードを見る</summary><div>

```bash
# 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
  }
}

```
</div></details>

ポイントは、mapのキー名をformat関数で動的に指定していることです。
これがそのままインスタンス名になるので、少し工夫しました。
注意点ですが、仮にこれをコピペして使う際には、ssh_authorized_keysの値を変更する必要があります。
opensslコマンドなどで秘密鍵と公開鍵を用意して、公開鍵のほうを指定してください。

> *Note*
> module1つで複数のリソースが作成できると書いておきながら、countでループさせてます。。。。
> モジュール側でmap型にする必要はなく、ここでループするだけで、いくつでもリソースが作成できます。
> 今回はキー名を動的に指定するために試行錯誤した結果、こうなりました。

<a id="markdown-ロードバランサ作成" name="ロードバランサ作成"></a>
# ロードバランサ作成
はい、ロードバランサのモジュールを作成して。。。。
実はOracleが[GitHub](https://github.com/oracle-terraform-modules/terraform-oci-tdf-lb)で公開してるモジュールがあるんですよね。
ここのモジュールを使って、ロードバランサのリソースを作成します。

> *Note*
> [Terraform Registry](https://registry.terraform.io)には各クラウドサービスのモジュールが公開されており、すぐにリソースを作成することができます。
> 自分でモジュールを作成する前にRegistryの存在を知りたかったです(´･ω･`)


```
$ vi oci_web/loadbalancer.tf
```

<details><summary>oci_web/loadbalancer.tfのコードを見る</summary><div>

```bash
# 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
}
```
</div></details>
実用性を考慮すると、LBにSSL証明書を持たせてHTTPSで通信させるべきですが、(面倒なので)今回はリスナーとバックエンドは、両方HTTPで通信させます。
shapeは無料で使用できる"10Mbps-Micro"を指定してます。
注意点はsourceの指定方法です。今までは、相対パスで指定してましたが、今回はGitHubから取ってくるのでこのように書いてます。


<a id="markdown-ネットワーク・セキュリティ・グループ作成" name="ネットワーク・セキュリティ・グループ作成"></a>
# ネットワーク・セキュリティ・グループ作成
セキュリティ・リストはサブネット単位で制御し、ネットワーク・セキュリティ・グループは各インスタンスごとに制御します。
公式では、ネットワーク・セキュリティ・グループ(NSG)を推奨しているので、本記事でもNSGで通信ポリシーを定義します。

> [Oracle Cloud Infrastructureドキュメント](https://docs.cloud.oracle.com/ja-jp/iaas/Content/Network/Concepts/securitylists.htm)
> - **セキュリティ・リスト**: このトピックで説明します。これは、ネットワーキング・サービスが提供する元のタイプの仮想ファイアウォールです。
> - **ネットワーク・セキュリティ・グループ**: セキュリティ・リストよりも推奨される別のタイプの仮想ファイアウォールです。

```shell
$ vi oci_web/nsg.tf
```

<details><summary>oci_web/nsg.tfのコードを見る</summary><div>

```bash
# 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
  }
}

```
</div></details>

LBと同じようにOracleが[GitHub](https://github.com/oracle-terraform-modules/terraform-oci-tdf-network-security)で公開しているモジュールを使いました。
注意点として、NSG内で別のNSGからのアクセスを許可する場合は、ルールより先にNSG本体を作成しないとエラーが発生する可能性があります。

<a id="markdown-terraform実行構築作業" name="terraform実行構築作業"></a>
# Terraform実行(構築作業)
長くなりましたが、あとはterraformコマンドを実行するだけです。

```shell
$ terraform init
$ terraform apply
```
OCI管理画面からリソースが正常に作成されたことを確認できると思います。
一通り確認が済みましたらリソースを削除しておきます。

```shell
$ terraform destroy
```

<a id="markdown-おわりに" name="おわりに"></a>
# おわりに
本来ならサーバ起動時にcloud-initスクリプトで、Apacheなどを起動させてブラウザからアクセス確認がしたかったのですが、それについては別の機会に書きます。
terraformはモジュール化するまで少々大変ですが、1度作成してしまうと何度も使い回せて非常に便利ですね。
Terraform Registryからモジュールを取得するなら、さらに工数を削減できそうです。
今回はOCIで実施しましたが、AWSをはじめとして様々なクラウドサービスに対応してるのもポイント高いです。

同じ記事を自分の[ブログ](https://grimoire.tokyo/cloud/185/)でも公開してます。

<a id="markdown-参考サイト" name="参考サイト"></a>
# 参考サイト
- [Terraform - Oracle Cloud Infrastructure Provider](https://registry.terraform.io/providers/hashicorp/oci/latest/docs)
- [github.com/terraform-providers/terraform-provider-oci](https://github.com/terraform-providers/terraform-provider-oci)
- [Terraform Registry](https://registry.terraform.io)
- [Terraformで複数台のEC2インスタンスを構築する場合のTIPS](https://dev.classmethod.jp/articles/tips-for-creating-multiple-aws_instance-resources/)
- [Download Terraform](https://www.terraform.io/downloads.html)