OracleCloudでResource Managerをつかって、Infrastructure as a Code をしてみた

# はじめに

Oracle Cloud Infrastrucrure には、Infrastructure as a Code を実現するために、Resource Manager というサービスが存在しています。
これを使うことで、OCI上のインフラ構成をCodeで定義して、テンプレートのように自動プロビジョニングを行うことができます。いわゆる、Infrastructure as a Code を実現するための1つのサービスとなります。

Resource Managerでは、Terraform が使われています。Terraformに馴染みのある方は扱いやすいと思います。
今回の記事では、以下の構成をResource Manager で作成するための構成を確認していきます。

なお、GitHubに公開しているので、適宜ご活用ください。

<https://github.com/Sugi275/oci-resourcemanager-sample>



# ResourceManagerで作成するOCIのリソース

以下の構成を Resource Manager で作成していきます。

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104247/18e3e185-9527-3722-a933-b00b76b6241f.png)


- Regional Public Subnet
- Compute x2
- FileStorage x1



# 用語

Resource Manager で出てくる用語を整理します。



## Stack

ResourceManager上で定義する、OCIリソースのまとまりを表しています。Stack単位でApply・Destroyを行うため、管理しやすい粒度で定義すると良いと思います。
他のPublicCloudの同様のサービスだと、定義したまとまりをApply後にStackと表現する場合がありますが、
ResourceManagerの場合は、Applyの有無にかかわらずStackと表現するため混乱しないように注意をする必要があります。



## Plan

定義したStackを使用して、OCIのリソースをどのように変更するのか、実行計画を立てます。
以前Stackを実行したときの状態と、Stackで定義したあるべき姿を比較して、何を変更するかを洗い出します。
Stackに定義されているリソースが存在しなければ新規作成を行い、以前作成したものがStackに存在しなければ削除を行います。



## Apply

Planで生成した実行計画をもとに、実際のOCI環境に適用を行います。
このタイミングで、OCIリソースの変更処理が発生します。

## Destroy

Stackで作成したOCIリソースのまとまりを削除します。



# 手順

## Terraform Install(Optional)

まず、Resource Manager そのものを利用する時にはTerraformは必要ではありません。
ただ、手元のローカルPCで、Terraform用の構成ファイルを作成する際に、terraformコマンドを使用できると便利なので、インストールを行います

Terraform用のバイナリファイルはzipファイルとして公開されています。Terraform用のディレクトリを作成し、バイナリファイルを格納します。

```
mkdir ~/terraform
cd ~/terraform
wget https://releases.hashicorp.com/terraform/0.11.13/terraform_0.11.13_linux_amd64.zip
```



zipを解凍します

```
unzip terraform_0.11.13_linux_amd64.zip
```



解凍後に生成さえたバイナリファイルを PATH に追加します
以下はfish shellの例

```
string trim '
set PATH ~/terraform $PATH
' >> ~/.config/fish/config.fish
```



これでshellを開きなおすと、terraformコマンドを実行可能になります

```
> terraform -h
Usage: terraform [-version] [-help] <command> [args]

The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you're just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.

...snip...
```





## 作業用ディレクトリの作成

適当に作業用ディレクトリを作成します

```
mkdir ~/resourcemanager/webserver/terraformfiles/
```



## Terraformのファイル群を作成

Terraformで使用するファイル群を作成します。
Linuxのfishでファイルを作成する方法で記載しています。
Bashやその他shellの場合は、適宜変更してファイルを作成してください。

また、「はじめに」で記載したGitHubに同様のファイルが存在するので、そちらをご利用していただいても問題ありません。



### OCI Provider

OCI Provider に必要な情報を定義します

```
string trim '
provider "oci" {
  region = "${var.region}"
}
' > ~/resourcemanager/webserver/terraformfiles/provider.tf
```



### Variable

variableを定義します。variableは後述のTerraform用定義ファイルで利用ができます。
また、Resource Manager(OCIコンソール、OCI CLI) でも同様のvariableを定義することができます。
適宜必要に応じて値を変更します。

```
string trim '
variable "region" {
    default = "us-phoenix-1"
}

variable "compartment_ocid" {
    default = "ocid1.compartment.oc1..todorename"
}

variable "instance_image_ocid" {
    type = "map"
    default = {
        // See https://docs.us-phoenix-1.oraclecloud.com/images/
        // Oracle-provided image "Oracle-Linux-7.6-2019.02.20-0"
        us-phoenix-1 = "ocid1.image.oc1.phx.aaaaaaaapxvrtwbpgy3lchk2usn462ekarljwg4zou2acmundxlkzdty4bjq"
        us-ashburn-1 = "ocid1.image.oc1.iad.aaaaaaaannaquxy7rrbrbngpaqp427mv426rlalgihxwdjrz3fr2iiaxah5a"
        eu-frankfurt-1 = "ocid1.image.oc1.eu-frankfurt-1.aaaaaaaa527xpybx2azyhcz2oyk6f4lsvokyujajo73zuxnnhcnp7p24pgva"
        uk-london-1 = "ocid1.image.oc1.uk-london-1.aaaaaaaarruepdlahln5fah4lvm7tsf4was3wdx75vfs6vljdke65imbqnhq"
        ca-toronto-1 = "ocid1.image.oc1.ca-toronto-1.aaaaaaaa7ac57wwwhputaufcbf633ojir6scqa4yv6iaqtn3u64wisqd3jjq"
    }
}

variable "mount_target_1_display_name" {
    default = "tf-mounttarget1"
}

variable "file_system_1_display_name" {
    default = "tf-fs1"
}

variable "export_path_fs1_mt1" {
    default = "/tf-fs1"
}

variable "ssh_public_key" {
    default = "overwrite by resourcemanager"
}

variable "ssh_private_key" {
    default = "overwrite by resourcemanager"
}

locals {
  mount_target_1_ip_address = "${lookup(data.oci_core_private_ips.ip_mount_target1.private_ips[0], "ip_address")}"
}

data "oci_core_private_ips" ip_mount_target1 {
  subnet_id = "${oci_file_storage_mount_target.my_mount_target_1.subnet_id}"

  filter {
    name   = "id"
    values = ["${oci_file_storage_mount_target.my_mount_target_1.private_ip_ids.0}"]
  }
}
' > ~/resourcemanager/webserver/terraformfiles/variable.tf
```



### Network

ネットワーク全般を定義します。
なお、Regional Subnet を作成したい場合は、Subnetを定義するときに、Availability Domain を指定しない方法となるため、注意が必要です。
基本的にはRegional Subnetで作成すべきです。

```
string trim '
resource "oci_core_virtual_network" "tfvcn" {
  cidr_block = "10.0.0.0/16"
  compartment_id = "${var.compartment_ocid}"
  display_name = "tfvcn"
  dns_label = "tfvcn"
}

resource "oci_core_internet_gateway" "tf-ig1" {
  compartment_id = "${var.compartment_ocid}"
  display_name   = "tf-ig1"
  vcn_id         = "${oci_core_virtual_network.tfvcn.id}"
}

resource "oci_core_default_route_table" "tf-default-route-table" {
  manage_default_resource_id = "${oci_core_virtual_network.tfvcn.default_route_table_id}"
  display_name               = "tf-default-route-table"

  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = "${oci_core_internet_gateway.tf-ig1.id}"
  }
}

resource "oci_core_security_list" "TFSecurityList" {
  compartment_id = "${var.compartment_ocid}"
  vcn_id         = "${oci_core_virtual_network.tfvcn.id}"
  display_name   = "TFSecurityList"

  // allow outbound tcp traffic on all ports
  egress_security_rules {
    destination = "0.0.0.0/0"
    protocol    = "6"
  }

  // allow outbound udp traffic on a port range
  egress_security_rules {
    destination = "0.0.0.0/0"
    protocol    = "17"        // udp
    stateless   = true

    udp_options {
      // These values correspond to the destination port range.
      "min" = 319
      "max" = 320
    }
  }

  // allow inbound ssh traffic from a specific port
  ingress_security_rules {
    protocol  = "6"         // tcp
    source    = "0.0.0.0/0"
    stateless = false

    tcp_options {
      // These values correspond to the destination port range.
      "min" = 22
      "max" = 22
    }
  }

  // allow inbound icmp traffic of a specific type
  ingress_security_rules {
    protocol  = 1
    source    = "0.0.0.0/0"
    stateless = true

    icmp_options {
      "type" = 3
      "code" = 4
    }
  }
  
  // allow inbound nfs traffic
  ingress_security_rules {
    protocol  = "6"         // tcp
    source    = "10.0.0.0/16"
    stateless = false

    tcp_options {
      "min" = 111
      "max" = 111
    }
  }
  
  // allow inbound nfs traffic
  ingress_security_rules {
    protocol  = "6"         // tcp
    source    = "10.0.0.0/16"
    stateless = false

    tcp_options {
      "min" = 2048
      "max" = 2050
    }
  }
  
  // allow inbound nfs traffic
  ingress_security_rules {
    protocol  = "17"        // udp
    source    = "10.0.0.0/16"
    stateless = false

    udp_options {
      "min" = 111
      "max" = 111
    }
  }
  
  // allow inbound nfs traffic
  ingress_security_rules {
    protocol  = "17"        // udp
    source    = "10.0.0.0/16"
    stateless = false

    udp_options {
      "min" = 2048
      "max" = 2048
    }
  }
  
  // allow inbound nfs traffic
  egress_security_rules {
    destination = "0.0.0.0/0"
    protocol    = "17"        // udp
    stateless   = true

    udp_options {
      // These values correspond to the destination port range.
      "min" = 111
      "max" = 111
    }
  }
}

resource "oci_core_subnet" "public01" {
  cidr_block = "10.0.1.0/24"
  display_name = "public01"
  dns_label = "public01"
  vcn_id = "${oci_core_virtual_network.tfvcn.id}"
  prohibit_public_ip_on_vnic = false
  security_list_ids = ["${oci_core_security_list.TFSecurityList.id}"]
  route_table_id = "${oci_core_virtual_network.tfvcn.default_route_table_id}"
  dhcp_options_id = "${oci_core_virtual_network.tfvcn.default_dhcp_options_id}"
  compartment_id = "${var.compartment_ocid}"
}

resource "oci_core_subnet" "private01" {
  cidr_block = "10.0.2.0/24"
  display_name = "private01"
  dns_label = "private01"
  vcn_id = "${oci_core_virtual_network.tfvcn.id}"
  prohibit_public_ip_on_vnic = true
  security_list_ids = ["${oci_core_virtual_network.tfvcn.default_security_list_id}"]
  route_table_id = "${oci_core_virtual_network.tfvcn.default_route_table_id}"
  dhcp_options_id = "${oci_core_virtual_network.tfvcn.default_dhcp_options_id}"
  compartment_id = "${var.compartment_ocid}"
}

' > ~/resourcemanager/webserver/terraformfiles/network.tf
```





### Compute

2個のCompute Instance を定義します。
ポイントは、"remote-exec" Provisioner を使用して Instance 内のコマンドを発行し、File Storage をマウントしている点です。
Resource Manager で、簡単なInstance内のコマンドを発行することができます。
ただ、複雑なものはResource Manager でやるのはつらいので、Ansible などのほかの構成管理ツールと一緒に使っていくと良いと思います。

```
string trim '
resource "oci_core_instance" "web01" {
  count               = "1"
  availability_domain = "TGjA:PHX-AD-1"
  compartment_id      = "${var.compartment_ocid}"
  display_name        = "web01"
  shape               = "VM.Standard2.2"

  create_vnic_details {
    subnet_id        = "${oci_core_subnet.public01.id}"
    display_name     = "primaryvnic"
    assign_public_ip = true
    hostname_label   = "web01"
  }

  source_details {
    source_type = "image"
    source_id   = "${var.instance_image_ocid[var.region]}"
  }

  metadata {
    ssh_authorized_keys = "${var.ssh_public_key}"
  }
  timeouts {
    create = "60m"
  }

  provisioner "remote-exec" {
    connection {
      agent       = false
      timeout     = "15m"
      host        = "${oci_core_instance.web01.public_ip}"
      user        = "opc"
      private_key = "${var.ssh_private_key}"
    }

    inline = [
      "sudo yum -y install nfs-utils > nfs-utils-install.log",
      "sudo mkdir -p /mnt/filestorage/fs1",
      "sudo mount ${local.mount_target_1_ip_address}:${var.export_path_fs1_mt1} /mnt/filestorage/fs1",
    ]
  }
}

resource "oci_core_instance" "web02" {
  count               = "1"
  availability_domain = "TGjA:PHX-AD-2"
  compartment_id      = "${var.compartment_ocid}"
  display_name        = "web02"
  shape               = "VM.Standard2.2"

  create_vnic_details {
    subnet_id        = "${oci_core_subnet.public01.id}"
    display_name     = "primaryvnic"
    assign_public_ip = true
    hostname_label   = "web02"
  }

  source_details {
    source_type = "image"
    source_id   = "${var.instance_image_ocid[var.region]}"
  }

  metadata {
    ssh_authorized_keys = "${var.ssh_public_key}"
  }
  timeouts {
    create = "60m"
  }
  
  provisioner "remote-exec" {
    connection {
      agent       = false
      timeout     = "15m"
      host        = "${oci_core_instance.web02.public_ip}"
      user        = "opc"
      private_key = "${var.ssh_private_key}"
    }

    inline = [
      "sudo yum -y install nfs-utils > nfs-utils-install.log",
      "sudo mkdir -p /mnt/filestorage/fs1",
      "sudo mount ${local.mount_target_1_ip_address}:${var.export_path_fs1_mt1} /mnt/filestorage/fs1",
    ]
  }
}
' > ~/resourcemanager/webserver/terraformfiles/compute.tf
```



### File Storage

File Storage を定義します

```
string trim '
resource "oci_file_storage_mount_target" "my_mount_target_1" {
  #Required
  availability_domain = "TGjA:PHX-AD-3"
  compartment_id      = "${var.compartment_ocid}"
  subnet_id           = "${oci_core_subnet.public01.id}"

  #Optional
  display_name = "${var.mount_target_1_display_name}"
}

resource "oci_file_storage_export_set" "my_export_set_1" {
  # Required
  mount_target_id = "${oci_file_storage_mount_target.my_mount_target_1.id}"

  # Optional
  display_name      = "my_export_set_1"
}

resource "oci_file_storage_export" "my_export_fs1_mt1" {
  #Required
  export_set_id  = "${oci_file_storage_export_set.my_export_set_1.id}"
  file_system_id = "${oci_file_storage_file_system.my_fs_1.id}"
  path           = "${var.export_path_fs1_mt1}"
  
    export_options = [
    {
      source                         = "0.0.0.0/0"
      access                         = "READ_WRITE"
      identity_squash                = "NONE"
      require_privileged_source_port = true
    },
  ]
}

resource "oci_file_storage_file_system" "my_fs_1" {
  #Required
  availability_domain = "TGjA:PHX-AD-3"
  compartment_id      = "${var.compartment_ocid}"

  #Optional
  display_name = "${var.file_system_1_display_name}"
}
' > ~/resourcemanager/webserver/terraformfiles/filestorage.tf
```



### StackのOutput

Resource Manager を実行したときに実行結果をカスタマイズすることができます。
Defaultで出力される実行結果の下に、インスタンスのGlobalIPを表示させています。

```
string trim '
output "InstancePublicIPs" {
  value = ["${oci_core_instance.web01.*.public_ip}",
           "${oci_core_instance.web02.*.public_ip}"]
}
' > ~/resourcemanager/webserver/terraformfiles/output.tf
```



## validate

上記で作成したTerraformのファイル群の、書式・内容に問題がないかを確認します。
確認しないでResource Manager へアップロードしてもよいのですが、Resource Manager 上の各種操作は5分ほど実行時間が必要です。
書式に問題があるのが発覚するまで若干の時間がかかるため、ローカルで確認するのが良いと思います。

まず、ローカルのterraformコマンドで書式を確認するために、init を実行します。

terraformのHCLとして正しい書式か、ローカル環境で検証

```
cd ~/resourcemanager/webserver/terraformfiles
terraform init
```



validate コマンドで書式に問題がないか確認します

```
cd ~/resourcemanager/webserver/terraformfiles
terraform validate
```



実行例。何もエラーが表示されなければOK

```
> terraform validate
> 
```



以下のようなエラーが表示された場合、対処を行います。

```
> terraform validate

Error: Variable 'ssh_public_key': duplicate found. Variable names must be unique.
```





## zip化

terraform用ファイルをzipで固めます。なお、`.terraform` ディレクトリを含めてzip化するとエラーになりますが、アスタリスクを指定しても`.terraform`ディレクトリは含まれないので問題ありません。

```
cd ~/resourcemanager/webserver/terraformfiles
zip webserver.zip *
```



## oci cliでstackを作成

zipファイルなどを指定してstackの新規作成を行います

```
cd ~/resourcemanager/webserver/terraformfiles
oci resource-manager stack create \
    --config-source "webserver.zip" \
    --display-name "Sample webserver" \
    --description "Example: Create a webserver Instance"
```



stackのocidを変数に格納します

```
set stackid (oci resource-manager stack list --display-name "Sample webserver" | jq -r ".data[0].id")
```



なお、zipファイルを更新する場合は、以下の update で更新が可能です

```
cd ~/resourcemanager/webserver/terraformfiles
oci resource-manager stack update \
    --stack-id $stackid \
    --config-source "webserver.zip"
```





## Variableを指定

公開鍵と秘密鍵を、ResourceManagerの機能で指定します。
ResourceManagerのvariablesで指定する理由は、Terraformのファイル群の中で記載すると、クレデンシャル情報をGitHubなどにアップロードすることになり、セキュリティに懸念があるためです。
秘密鍵の改行コードは \n に変換して指定すると良いでしょう。

なお、以下の鍵の情報は、中身を適当に書き換えているため使用することはできません。

```
oci resource-manager stack update \
    --stack-id $stackid \
    --variables '{"ssh_public_key":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAAQC8/ABrmRV/Vc2aGrUEsp15XpxoIelke72QZP1lgCPAxp2AO5EJ7bnER6pANwXPAcuQUv3ZaqtP/wNKH9xQUVUWUmOrvhJ6w9BHz8+zgR0N9WH2JoVTfvdEaQXEfHliRQwvT9dZWAC3GAw5VkwtyO3v87cwmNkDJeUjjQxVL8MRNTFB9MRkNKUFtV72gGtp4vbYEgCYgiBgSpJX5l6QjVNISVHCMbANHLhiGNGrmdKw177xK9POoasjdaopsjWHInskalNco2xqnOKNClqtL/7H7C/QJqbHkqbcj+q5+V63eQ57PdVxMQkR1tu790A8CaTWzTUvehKCO8DITRddeQr sugi@SUSUGIYA-JP",
    "ssh_private_key":"-----BEGIN RSA PRIVATE KEY-----\nsecret\nsecret\nsecret\nsecret\nsecret\nsecret\nsecret\nsecret\nsecret\n-----END RSA PRIVATE KEY-----"
}'
```



## Plan

作成したstackを使用して、実行計画を生成します。

```
oci resource-manager job create \
  --stack-id $stackid \
  --operation PLAN \
  --display-name "Plan Job"
```



抜粋した実行結果は以下のようになっており、目視で内容を読み解くことができます。

```
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create                                            <============= + マークの意味が記載されている。

Terraform will perform the following actions:

+ oci_core_default_route_table.tf-default-route-table <============= + マークがついているため、新規作成ということが読み取れる
      id:                                           <computed>
      display_name:                                 "tf-default-route-table"
      freeform_tags.%:                              <computed>
      manage_default_resource_id:                   "${oci_core_virtual_network.tfvcn.default_route_table_id}"
      route_rules.#:                                "1"
      route_rules.~4148037591.cidr_block:           <computed>
      route_rules.~4148037591.destination:          "0.0.0.0/0"
      route_rules.~4148037591.destination_type:     "CIDR_BLOCK"
      route_rules.~4148037591.network_entity_id:    "${oci_core_internet_gateway.tf-ig1.id}"
      state:                                        <computed>
      time_created:                                 <computed>
      time_modified:                                <computed>

  + oci_core_instance.web01                           <============= + マークがついているため、新規作成ということが読み取れる
      id:                                           <computed>
      agent_config.#:                               <computed>
      availability_domain:                          "TGjA:PHX-AD-1"
      boot_volume_id:                               <computed>
      省略
```





## Apply

最新の Plan で生成した実行計画を使用して、OCI上にリソースを生成します。
特に問題が発生しなければ、OCI上のリソースが作成されます。

```
oci resource-manager job create \
  --stack-id $stackid \
  --operation APPLY \
  --display-name "Apply Job" \
  --apply-job-plan-resolution '{"isUseLatestJobId":true}'
```



実行ログを確認し、CompleteやGlobalIPが表示されていることを確認します

```
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.

Outputs:

InstancePublicIPs = [
    129.146.48.77,
    129.146.114.140
]
```





## Instanceにログインして確認

Resource Manager で作成されたインスタンスにログインして、mount情報を確認します。
File Storage が 自動的にNFS マウントされていることを確認できます。

```
# df -hT
Filesystem       Type      Size  Used Avail Use% Mounted on
devtmpfs         devtmpfs   15G     0   15G   0% /dev
tmpfs            tmpfs      15G     0   15G   0% /dev/shm
tmpfs            tmpfs      15G  8.6M   15G   1% /run
tmpfs            tmpfs      15G     0   15G   0% /sys/fs/cgroup
/dev/sda3        xfs        39G  2.1G   37G   6% /
/dev/sda1        vfat      200M  9.7M  191M   5% /boot/efi
10.0.1.3:/tf-fs1 nfs       8.0E     0  8.0E   0% /mnt/filestorage/fs1     <===== File Storageがマウントされている
tmpfs            tmpfs     3.0G     0  3.0G   0% /run/user/1000
```



## fstab

注意点として、今回のResource Managerでは、インスタンスのfstabを更新していないので、インスタンスを再起動するとマウントが外れてしまいます。

Resource Manager で fstabを作成してもよいのですが、ファイルの作成は Resource Manager(Terraform)はあまり得意ではないと思うので、AnsibleといったほかのOSSを使うと良いのかなと思います。



# 操作の注意点

## StackをApplyした後、DestroyせずにStackのDeleteをすると

Stackで作成したものが、残ったままとなる。StackをDeleteする前に、かならずDestroyすること



## 実行時間

Stackに関する、Create, Plan, Apply, Destroy などの操作は、5分くらい実行時間がかかる。少々待つ必要がある。



# 参考URL

OCI Document
<https://docs.cloud.oracle.com/iaas/Content/ResourceManager/Concepts/resourcemanager.htm>

GitHub Provider
<https://github.com/terraform-providers/terraform-provider-oci>

GitHub Example
<https://github.com/terraform-providers/terraform-provider-oci/tree/master/examples>

Terraformの公式ページ 各種OCIリソースの定義方法が記載されている
<https://www.terraform.io/docs/providers/oci/index.html>

Terraform の Resource 定義
<https://www.terraform.io/docs/configuration/resources.html>

Terraform の remote-exec について (InstanceのShell上でコマンドを実行することができる)
<https://dev.classmethod.jp/cloud/how-to-provision-remote-servers-with-terraform-by-using-remote-exec/>