OCI で Terraform を始めてみる

インフラ構築を自動化できる Terraform。コレを Oracle Cloud Infrastructure (OCI) で動かしてみる。

目次

Terraform をインストールする

Terraform は、Vagrant などで知られる HashiCorp が提供している。terraform コマンドは以下の公式ページからダウンロードできる。

ダウンロードした Zip ファイルを解凍すると、MacOS 向けの場合は terraform という実行可能ファイル、Windows 向けの場合は terraform.exe というファイルが出てくる。コレを PATH の通っている場所に置くだけで良い。

Terraform 実行用の OCI ユーザを作成する

Terraform は、各種クラウドサービスが提供する API をコールする「ラッパー」でしかなく、OCI 環境を触るためには、その環境でリソースを作れる権限を持ったユーザアカウントを用意しないといけない。

細かな手順は省略するが、全体的な流れとしては以下のとおり。

  1. OCI 管理コンソールにログインする
    • この時、Tenancy 詳細を開き、Tenancy の OCID と、操作したいリージョンの名前を控えておく
  2. Identity → Users と移動し、Terraform 実行用の User を作成する
    • 作成した User の OCID は控えておく
  3. Identity → Groups と移動し、Terraform 実行用ユーザが所属する Group を作成する
  4. 作成した Group に Terraform 実行用 User を所属させる
  5. Identity → Policies と移動し、Terraform 実行用ユーザが所属する Group に対し、Tenancy 全体や Compartment 配下のリソースを操作できる権限を付与する
    • Terraform から OCI ユーザを作ったりもできるので、最初はテナンシー全体で何でもできる Allow group 【グループ名】 to manage all-resources in tenancy といったステートメントを付与してしまうのが手っ取り早いかと思う
  6. Terraform 実行用 User に登録するための API 鍵ペアを作成する
    • $ openssl genrsa -out ./api_key.pem 2048 コマンドで秘密鍵を作成する
    • $ openssl rsa -pubout -in ./api_key.pem -out ./api_key_public.pem コマンドで公開鍵を作成する
  7. 生成した公開鍵ファイルを Terraform 実行用 User の API Keys に登録する
    • 登録後に表示される Fingerprint を控えておく

以上の操作で、

  1. 操作対象の Tenancy の OCID
  2. 操作対象のリージョン名
  3. Terraform 実行用 User の OCID
  4. Terraform 実行用 User に登録した API Key の Fingerprint
  5. Terraform 実行用 User に登録した API Key の秘密鍵ファイル

の5つの情報が準備できたはずだ。この5つの情報は Terraform 定義ファイルの中で使用することになる。


先程、「Terraform はクラウドサービスの API をコールするラッパーだ」と書いたが、OCI については、OCI CLI という公式のコマンドラインツールを用いると、ほぼ全てのリソースが操作できる。Terraform が操作できる範囲は、この OCI CLI でできる範囲とほとんど変わりなく、OCI 環境を操作するための認証機構もほとんど同じだ。

OCI CLI の場合、$ oci setup config というコマンドを使うと、~/.oci/config というファイルにプロファイル情報が書き込まれる。以下はサンプル。

[DEFAULT]
user=ocid1.user.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
fingerprint=xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
key_file=/PATH/TO/.oci/oci_api_key.pem
tenancy=ocid1.tenancy.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
region=us-ashburn-1

先程準備するよう記載した、5つの情報が列挙されていることが分かるだろうか。つまり、この OCI CLI 向けのプロファイル情報を流用すれば、Terraform を使って同じ範囲を操作できるということだ。

Terraform 定義ファイルを書く

それではいよいよ、Terraform 定義ファイルを書いていく。適当な作業ディレクトリを作ったら、形式的に以下のようにファイルを作ろう。

$ mkdir terraform-oci-test
$ cd $_
$ touch main.tf
$ touch variables.tf
$ touch terraform.tfvars
$ touch outputs.tf

Terraform は、カレントディレクトリ内にある .tf 形式のファイルを全て拾い上げて結合して実行してくれる。拡張子さえ守っていれば、1ファイルに全ての情報を記載しても、細かくファイルを分けても、動作には変わりない。ただ、ベストプラクティスとして、

  1. main.tf … プロバイダの指定やリソースの定義
  2. variables.tf … 変数の定義
  3. outputs.tf … アウトプットの定義

という3つのファイルはお決まりで作ることが多い。

terraform.tfvars というファイルは、変数に対して値を設定するためのファイル。.env ファイル的なモノと思えばよいだろう。コチラは慣例的にこのようなファイル名、拡張子を取るが、コマンド実行時に引数でこのファイルを指定しないと参照されないので、例えば環境別に env_dev.tfvarsenv_prod.tfvars などと名付けて複数ファイルを用意しても良い。

この辺り、広く採用される「ベストプラクティス」はあるものの、構築する環境の規模や内容によっても変わってくるし、Terraform のバージョンアップによって新機能が追加されたりするとまた変わってくるので、「そのディレクトリを開いた時に全体的な構成が読み取りやすい状態であること」をとりあえず意識できれば良いかなと思う。

今回は、作成済のVM (Compute Instance) を1台作成するサンプルを作ってみた。以降は、各ファイルに記述する内容を記していく。

main.tf

main.tf には、「OCI を操作しますよー」という「プロバイダ宣言」や、リソース作成時に定数的に参照するようなリソース定義をしたりする。

/* 
 * OCI プロバイダ定義 : https://www.terraform.io/docs/providers/oci/index.html
 */
provider "oci" {
  region           = "${var.region}"
  tenancy_ocid     = "${var.tenancy_ocid}"
  user_ocid        = "${var.user_ocid}"
  fingerprint      = "${var.fingerprint}"
  private_key_path = "${var.private_key_path}"
}

/* 
 * 当該テナンシで利用可能な AD の一覧を取得する : https://www.terraform.io/docs/providers/oci/d/identity_availability_domains.html
 */
data "oci_identity_availability_domains" "my_ads" {
  compartment_id = "${var.tenancy_ocid}"
}

Terraform の構文はひたすらお勉強。全体的には、【リソース種別】 "【予め決まっているリソース名】" "【自分で任意に付ける名前】" { } というブロックで、一つのリソースを宣言する。

プロバイダの宣言である provider "oci" 部分は、任意の名前が付いていないが、コレで「OCI を操作するための Terraform 定義ファイル群である」という宣言がなされている。region だの tenancy_ocid だの、といったプロパティ名は、予め決められているプロパティ名だ。

プロパティに対する値は = "${var.region}" といった形で代入されている。"${文字列}" という記法で、環境変数の値や、よそのリソースの情報などを変数として利用できる。リージョン名や Tenancy の OCID などは、"${var.【環境変数名】}" という記法を使い、環境変数で値を設定するように定義している。環境変数への値の割当方法は後述。

次に記載している data "oci_identity_availability_domains" "my_ads" {} というブロックは、OCI 上の Availability Domain 情報を配列で取得するための Data Sources というモノ。コレを利用して、「AD1 にインスタンスを置こう」とか、「Load Balancer は AD1 と AD2 に割り当てよう」などと記述できるワケだ。

そうそう、コメントは /* */ で複数行コメントができる他、単一行コメントは //# のいずれかで記述できる。ココらへんはお好みで。

instance.tf

ココで、今回は VM (Compute Instance) を構築しようとしているので、そのリソースを定義するためのファイルを別に作っておこうと思う。名前は任意だが、分かりやすく instance.tf とした。中身は以下のとおり。

/* 
 * Instance 監視サーバ : https://www.terraform.io/docs/providers/oci/r/core_instance.html
 */
resource "oci_core_instance" "my_instance" {
  compartment_id      = "${var.compartment_ocid}"
  availability_domain = "${lookup(data.oci_identity_availability_domains.my_ads.availability_domains[0], "name")}"  // AD1
  shape               = "${var.instance_shape}"
  display_name        = "my-instance"
  source_details {
    source_type = "image"
    source_id   = "${var.instance_image_ocids[var.region]}"  // 当該リージョン用のイメージの OCID を指定する
  }
  create_vnic_details {
    subnet_id        = "${var.subnet_ocid}"  // Subnet の OCID
    assign_public_ip = true                  // Public IP を割り当てる
    hostname_label   = "myinstance"
  }
  metadata {
    ssh_authorized_keys = "${var.instance_ssh_public_key}"
  }
}

resource から始まるブロックで、「何らかのリソースを作成する定義」を行う。今回は oci_core_instance、つまり Compute Instance を作るためのリソース定義、ということだ。

availability_domain プロパティ内で、lookup() 関数を使い、AD 一覧の配列 (my_ads) から1つ目の要素を取得し、その中の name プロパティの値を拾っている。コレで kKLm:US-ASHBURN-AD-1 といったような Availability Domain 名を拾い上げ、プロパティに設定している。

今回は小さく作りたいので、VCN や Subnet は別途作成済であるものとして、この Compute Instance を配置する Subnet の指定は、変数 "${var.subnet_ocid}" から行うようにしている。勿論、Terraform で VCN を作り、その下に Subnet を作り、そうして生成された Subnet の OCID を拾い上げ、この Compute Instance を作成する、といった依存関係も実現できる。コード量が増えてしまうので今回は省略するが、記述は難しくないので、OCI Provider の API ドキュメントを見ながら作ってみてほしい。

variables.tf

main.tfinstance.tf の定義で、「何かを作る」ための定義は終わり。variables.tf では、他のファイルで "${var.【変数名】}" と書いて参照した変数の「宣言」のみを列挙しておく。以下のような要領だ。

/* 変数定義 */

variable "region" {
  type        = "string"
  description = "リージョン (ex. us-ashburn-1)"
  default     = "us-ashburn-1"
}

variable "tenancy_ocid"     { description = "テナンシの OCID" }
variable "user_ocid"        { description = "Terraform を実行するユーザの OCID" }
variable "fingerprint"      { description = "Terraform を実行するユーザの API Key のフィンガープリント" }
variable "private_key_path" { description = "Terraform を実行するユーザの API Key 秘密鍵のフルパス" }
variable "compartment_ocid" { description = "コンパートメントの OCID" }

// Compute Instance 関連

variable "instance_image_ocids" {
  description = "インスタンスイメージの OCID マップ"
  type        = "map"
  default     = {
    // - イメージ定義のベスト・プラクティス : https://www.terraform.io/docs/providers/oci/guides/best_practices.html#referencing-images
    // - イメージ情報は次の URL より確認できる : https://docs.us-phoenix-1.oraclecloud.com/images/
    // - 以下は Oracle-Linux-7.6-2019.05.14-0 イメージの OCID を指定している : https://docs.cloud.oracle.com/iaas/images/image/54f22559-7638-4da9-9e09-7fc78527608c/
    us-ashburn-1   = "ocid1.image.oc1.iad.aaaaaaaa4bfsnhv2cd766tiw5oraw2as7g27upxzvu7ynqwipnqfcfwqskla"
    ap-tokyo-1     = "ocid1.image.oc1.ap-tokyo-1.aaaaaaaamc2244t7h3gwrrci5z4ni2jsulwcg76gugupkb6epzrypawcz4hq"
    ap-seoul-1     = "ocid1.image.oc1.ap-seoul-1.aaaaaaaalhbuvdg453ddyhvnbk4jsrw546zslcfyl7vl4oxfgplss3ovlm4q"
    ca-toronto-1   = "ocid1.image.oc1.ca-toronto-1.aaaaaaaakjkxzw33dcocgu2oylpllue34tjtyngwru7pcpqn4qh2nwon7n7a"
    eu-frankfurt-1 = "ocid1.image.oc1.eu-frankfurt-1.aaaaaaaandqh4s7a3oe3on6osdbwysgddwqwyghbx4t4ryvtcwk5xikkpvhq"
    uk-london-1    = "ocid1.image.oc1.uk-london-1.aaaaaaaa2xe7cufdwkksdazshtmqaddgd72kdhiyoqurtoukjklktf4nxkbq"
    us-phoenix-1   = "ocid1.image.oc1.phx.aaaaaaaavtjpvg4njutkeu7rf7c5lay6wdbjhd4cxis774h7isqd6gktqzoa"
  }
}

variable "instance_shape" {
  description = "Compute Instance のシェイプ"
  default     = "VM.Standard1.1"
}

variable "subnet_ocid" {
  description = "Subnet の OCID"
}

variable "instance_ssh_public_key" {
  description = "管理サーバへの SSH 接続用の公開鍵"
}

variable "【変数名】" {} というブロックで、一つの変数の宣言を行う。コレをなくして "${var.【変数名】}" とだけ書くのは、「宣言がないから NG」という関係だ。

type プロパティは必須ではない。大抵は暗黙的に "string" で解釈されるが、中には map などを使うこともある。

description プロパティで説明文を書いておくと、terraform コマンド実行時に説明として表示されるので、単なるコメントとしてではなく、description プロパティで書いておくと良いだろう。

default プロパティも任意だが、コレを書いておくと、環境変数で値を渡さなかった時にその値がデフォルトとして使用される。

変数 instance_image_ocids の書き方は、OCI Provider が公式に推奨しているベスト・プラクティスに沿った書き方。このように変数を用意しておくと、instance.tf"${var.instance_image_ocids[var.region]}" という風に OCID を参照できる。

terraform.tfvars

変数の宣言は variables.tf で行った。変数の使用箇所は main.tfinstance.tf などに登場する。では、変数の値をどう設定するか、というと、簡単なやり方は、.tfvars という拡張子で変数の値を設定したファイルを作ることだ。

/* 変数値ファイル */

region           = "us-ashburn-1"
tenancy_ocid     = "ocid1.tenancy.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
user_ocid        = "ocid1.user.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
fingerprint      = "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx"
private_key_path = "/PATH/TO/.oci/oci_api_key.pem"
compartment_ocid = "ocid1.compartment.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

// Compute Instance 関連

// instance_image_ocids 変数の値は特に変更しなくて良い
// instance_shape 変数の値も、今回は default 値を使用するので定義しない

// Compute Instance を所属させる Subnet の OCID を設定しておく
subnet_ocid = "ocid1.subnet.oc1.iad.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
// Compute Instance に SSH 接続するための SSH 公開鍵を設定しておく
instance_ssh_public_key = "ssh-rsa xxxxx"

こんな感じ。default 値を使いまわしたいものはいちいち記述する必要はない。ただ、default がない状態で、.tfvars ファイルでも値を設定していない変数があった場合は、コマンド実行時に値を入力するよう問われる。

outputs.tf

最後に outputs.tf。このファイルは、一連のリソース作成が終わった後にコンソール出力したい情報を定義しておく。今回の場合であれば、作成した Compute Instance の Public IP がココで分かれば、そのあとすぐに SSH 接続できたりするだろう。

/* 結果出力 */

// 作成した Compute Instance に割り当てられた Public IP を出力する
output "my_instance_public_ip" {
  value = [ "${oci_core_instance.my_instance.public_ip}" ]
}

結果出力したい情報は output "【適当な名前】" { value = 【出力したい値】 } というブロックで記述する。value プロパティは固定。

instance.tf で、resource "oci_core_instance" "my_instance" { } というブロックでリソースを作るよう定義したが、コレで作ったリソースから情報を引っ張ってくるには、"${【予め決まっているリソース名】.【宣言した任意の名前】.【予め決まっているプロパティ名】}" という形で参照する。コレで、作成された Compute Instance の Public IP が拾えている。

ファイルの準備は以上

コレでファイルの準備は以上だ。どのようなリソースがあって、どんなプロパティがあるか、といった情報は、以下の公式ドキュメントを読み込んでいくしかない。

terraform init でプラグインを準備する

.tf ファイルや .tfvars ファイルが用意できたら、いよいよ実行していきたいワケだが、その前に、これらのファイルがあるディレクトリで $ terraform init というコマンドを実行する。

コレを実行すると、Terraform から OCI を操作するためのプラグインなどをダウンロードして、./.terraform/ ディレクトリ配下に自動的に置いてくれる。

terraform plan で Dry-Run してみる

terraform init で準備したらいよいよ実行…といきたいところだが、いきなりインフラ構築を始めてしまい、コードに間違いがあったりすると、復旧させるのが困難になる場合もある。

そこで、Terraform には実際にインフラ構築をする前にテストするための、$ terraform plan という Dry-Run コマンドがある。コレを使ってみよう。

ココからは、-var-file オプションを使って、用意していた .tfvars ファイルを参照するようにして実行する。

$ terraform plan -var-file='./terraform.tfvars'

このように実行すると、実際にはリソース作成などは行われないものの、コードに間違いがないか、コードを実行した結果、どのようなリソースが追加・変更・破棄されるのか、といった実行計画が確認できる。

output.tf で定義した output 情報は、terraform plan で実行した場合は出力されないので留意。

terraform apply でいよいよ実行する

実行計画を確認して問題なければ、いよいよ実行に移る。実行するには、$ terraform apply コマンドを使う。

$ terraform apply -var-file='./terraform.tfvars'

先程の plan 部分を apply に変更するだけで実行できる。

リソースの作成にかかる時間はまちまちだが、Compute Instance 1台くらいであれば、2・3分もあれば完了するだろう。

時々、terraform plan で異常が見つからなかったのに、terraform apply で実行して初めて問題が発生する場合があったりする。特に output の内容は terraform plan 実行時はあまり厳密にチェックされないので、よくよく注意して実行しよう。

上手く実行できると、OCI 管理画面上でも Compute Instance が作成できていることが確認できるはずだ。また、カレントディレクトリには terraform.tfstate といったファイルが生成されており、Terraform コマンドによってどのようなリソースが作成されたのか、といった履歴が全て記録されている。

terraform destroy で環境を破棄できる

terraform apply で作った環境は、$ terraform destroy というコマンドで丸ごと削除することもできる。

# -destroy オプションを付ければ削除時の実行計画が見られる
$ terraform plan -destroy -var-file='./terraform.tfvars'

# 削除する
$ terraform destroy -var-file='./terraform.tfvars'

今回は Compute Instance 一つの例だから効果が分かりづらいかもしれないが、VCN を丸ごと作るようなコードの場合、OCI 管理画面でポチポチ削除していったりするよりもはるかに容易だ。

今回はココまで

駆け足になったが、Terraform を使って、OCI 上に簡単なリソースを作成・破棄する基礎を紹介した。世間的にはまだマイナーな OCI を例に Terraform を紹介したが、GCP・AWS・Azure なども対応している。各クラウドサービスが提供する CLI ツールなどの使い方を個別に勉強するよりも、Terraform という単一の形式で管理した方が、言語やツールの仕様に関する学習コストは削減できるだろう。また、今回深くは紹介しなかったが、Terraform の機能によって「冪等性の担保」「差分管理」「変更・破棄の容易性」といったメリットを享受できるので、クラウドサービスを活用していく際はぜひとも Terraform を導入していきたい。