Terraform を使って OCI 上に予約済 Public IP を付与した VM を作成する

Pocket

こんにちは、mkrydik です。

今回は Terraform を使って、 OCI (Oracle Cloud Infrastructure) 上に 予約済 Public IP を付与した VM (インスタンス) を作成してみます。

前提条件

この記事は以下を前提とします。

  1. OCI に登録済で、Terraform を操作するためのユーザや API キーを取得済であること
  2. Terraform の言語仕様・基本操作について習得していること (今回 Terraform の初歩的な使い方などは説明しません)

今回作成する環境の構成

今回は次のようなリソースを作成します。

  • VCN : 1つ … 仮想ネットワーク
    • Subnet : 1つ … Availability Domain をまたいでリソースを配置できる「リージョナル・サブネット」にします
      • Compute Instance : 1つ … Subnet 内に VM を作成し、インターネットから SSH 接続できるようにします
        • Reserved Public IP : 「予約済 Public IP」のことです

予約済 Public IP とは

冒頭から単語が登場している 「予約済 Public IP (Reserved Public IP)」 とは何か、おさらいします。他のクラウドサービスでは「Elastic Public IP」「静的外部 IP アドレス」などとも呼ばれています。

通常、クラウド上で仮想マシンを作成した時に割り当てられる Public IP は、仮想マシンの停止や破棄に応じて割り当てが解除されてしまいます。

これに対して、「予約済 Public IP」は、割り当てた仮想マシンを破棄しても、その Public IP を持ち続けられるサービスです。同じ Public IP を他の仮想マシンに割り当て直せば、接続元の情報を変更することなく仮想マシンを入れ替えたりすることも可能なので、うまく活用したいサービスです。

OCI + Terraform での注意点が多数…

他のクラウドサービスと同じように、OCI にも「予約済 Public IP」サービスがあり、任意の Compute Instance に割り当てて使えます。しかし、こうした環境を管理コンソールからではなく、Terraform を使って構築しようとすると、いくつか注意点がありますので、今回はその点を紹介していきます。

先にコード全量掲載します

注意点を解説していくにあたって、コードを見た方が話が早いという方もいらっしゃると思いますので、はじめに今回のサンプルコードを全量掲載致します。以下のファイルのコードを掲載していきます。

  • main.tf
  • variables.tf
  • terraform.tfvars
  • vcn.tf
  • instance.tf
  • output.tf

main.tf

メインファイルではプロバイダの定義や、定数的に利用する locals および data の宣言をしています。

/* OCI プロバイダ定義 */
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}"
}

/* VCN・Security List の設定に使用する CIDR ブロックの定義 */
locals {
  cidr_public_internet = "0.0.0.0/0"  // インターネットとの通信
}

/* Security List の設定に使用するプロトコル番号の定義 */
locals {
  security_list_protocol_icmp = "1"    // ICMP
  security_list_protocol_tcp  = "6"    // TCP
  security_list_protocol_udp  = "17"   // UDP
  security_list_protocol_all  = "all"  // 全て
}

/* 当該テナンシで利用可能な AD の一覧を取得する */
data "oci_identity_availability_domains" "availability_domains" {
  compartment_id = "${var.tenancy_ocid}"
}

variables.tf

変数定義ファイルです。

// OCI 全般・API Key 設定
variable "region"           { description = "リージョン (ex. ap-tokyo-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" }

// VCN
variable "vcn_dns_label"  { description = "VCN の DNS Label 名・ハイフンを許容しないので注意"  default = "vcn" }
variable "vcn_cidr_block" { description = "VCN の CIDR Block"                                  default = "10.0.0.0/16" }

// Subnet
variable "subnet_instance_dns_label"  { description = "サーバの Subnet の DNS ラベル名・ハイフンを許容しないので注意 (ex. instance)"  default = "instance" }
variable "subnet_instance_cidr_block" { description = "サーバの Subnet の CIDR Block"                                                 default = "10.0.1.0/24" }

// サーバ
variable "instance_image_ocids" {
  description = "サーバの OS イメージの OCID マップ"
  type        = "map"
  default     = {
    // Oracle-Linux-7.7-2019.09.25-0 : https://docs.cloud.oracle.com/iaas/images/image/554b84a7-9b76-424d-89ef-15ea591c5eef/
    ap-mumbai-1    = "ocid1.image.oc1.ap-mumbai-1.aaaaaaaaefcax7pqzhgcpiaxomtzvwj67cssuxhazggbhoxjv4adcvsfkfga"
    ap-seoul-1     = "ocid1.image.oc1.ap-seoul-1.aaaaaaaaxabo4p5asejscj4ba4weg62owtivojokmtcjyr6eqrdeq4dgfzvq"
    ap-sydney-1    = "ocid1.image.oc1.ap-sydney-1.aaaaaaaahggevnzn2hs3abhwacvv5jxnguoxdej3bnuy5t4cy3jrslubgqoa"
    ap-tokyo-1     = "ocid1.image.oc1.ap-tokyo-1.aaaaaaaatbwoj3ee5sbaa6u5ptpy74bukkqmj75bptvn7dpezovpdvr6ds2q"
    ca-toronto-1   = "ocid1.image.oc1.ca-toronto-1.aaaaaaaanljihk7bncal55wmgk5yrt23kpongv733zx5w5k4h46qs4srgqua"
    eu-frankfurt-1 = "ocid1.image.oc1.eu-frankfurt-1.aaaaaaaajqghpxnszpnghz3um66jywaw5q3pudfw5qwwkyu24ef7lcsyjhsq"
    eu-zurich-1    = "ocid1.image.oc1.eu-zurich-1.aaaaaaaakla6mguktwqu7hmv75p7haiharf4usbpvjeogl7pnk3tbyqmawbq"
    sa-saopaulo-1  = "ocid1.image.oc1.sa-saopaulo-1.aaaaaaaaj6eqq3vky7mwktyewgylrvn7dhaqno6ypd6lt3yj7qigrfe7a4ca"
    uk-london-1    = "ocid1.image.oc1.uk-london-1.aaaaaaaaf4nj5yoqo7gv6ht6t7gtcr5de5slhy52alv3nqyjvmmh25knbama"
    us-ashburn-1   = "ocid1.image.oc1.iad.aaaaaaaa3onyerinivkpiqektrd3idoeo72fuz56cpz6rihkvqulmoux5qkq"
    us-langley-1   = "ocid1.image.oc2.us-langley-1.aaaaaaaawq36twtmxcxklpcqczltp7ckuy3aj7e6wkfkweq4xacfdelytgfa"
    us-luke-1      = "ocid1.image.oc2.us-luke-1.aaaaaaaabrxehwss7yrocavvaa5qcximxr2vkyrsuloq5ctnzxdhcvmdo2ga"
    us-phoenix-1   = "ocid1.image.oc1.phx.aaaaaaaalza4xew5okvv42djc3bphidkf7pa7xy435uieguz4aau735flbmq"
  }
}
variable "instance_shape"          { description = "サーバのシェイプ"                 default = "VM.Standard1.1" }
variable "instance_ssh_public_key" { description = "サーバへの SSH 接続用の公開鍵" }
variable "instance_hostname_label" { description = "サーバのホスト名 (ex. instance)"  default = "instance" }

terraform.tfvars

変数の値を渡すファイルです。 instance_image_ocids は、設定が面倒なのでコメントアウトして値を設定しないでおきます。こうすると variables.tf に記載の default 値を利用するようになります。

// OCI 全般・API Key 設定
region           = "ap-tokyo-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/API-KEY.pem"
compartment_ocid = "ocid1.compartment.oc1..XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

// VCN
vcn_dns_label  = "vcn"
vcn_cidr_block = "10.0.0.0/16"

// Subnet
subnet_instance_dns_label   = "instance"
subnet_instance_cidr_block  = "10.0.1.0/24"

// サーバ
// instance_image_ocids = ""
instance_shape          = "VM.Standard1.1"
instance_ssh_public_key = "ssh-rsa XXXXXXXXXX"
instance_hostname_label = "instance"

vcn.tf

VCN をはじめとするネットワーク系のリソースを作成するファイルです。

/* VCN */
resource "oci_core_vcn" "vcn" {
  compartment_id = "${var.compartment_ocid}"
  display_name   = "vcn"
  dns_label      = "${var.vcn_dns_label}"
  cidr_block     = "${var.vcn_cidr_block}"
}

/* Default Route Table */
resource "oci_core_default_route_table" "default_route_table" {
  manage_default_resource_id = "${oci_core_vcn.vcn.default_route_table_id}"
  display_name               = "default-route-table"
  route_rules {
    network_entity_id = "${oci_core_internet_gateway.internet_gateway.id}"
    destination       = "${local.cidr_public_internet}"
  }
}

/* Internet Gateway */
resource "oci_core_internet_gateway" "internet_gateway" {
  compartment_id = "${var.compartment_ocid}"
  vcn_id         = "${oci_core_vcn.vcn.id}"
  display_name   = "internet-gateway"
}

/* Subnet */
resource "oci_core_subnet" "instance" {
  // availability_domain プロパティを指定しないことでリージョナル・サブネットにする
  compartment_id    = "${var.compartment_ocid}"
  vcn_id            = "${oci_core_vcn.vcn.id}"
  display_name      = "instance-subnet"
  dns_label         = "${var.subnet_instance_dns_label}"
  cidr_block        = "${var.subnet_instance_cidr_block}"
  security_list_ids = [
    "${oci_core_security_list.instance_from_ssh.id}",
    "${oci_core_security_list.instance_to_externals.id}"
  ]
}

/* Security List : Inbound */
resource "oci_core_security_list" "instance_from_ssh" {
  compartment_id = "${var.compartment_ocid}"
  vcn_id         = "${oci_core_vcn.vcn.id}"
  display_name   = "instance-from-ssh"
  ingress_security_rules = [
    // インターネットからの SSH 通信
    {
      stateless = false
      source    = "${local.cidr_public_internet}"
      protocol  = "${local.security_list_protocol_tcp}"
      tcp_options { min = "22"  max = "22" }
    }
  ]
}

/* Security List : Outbound */
resource "oci_core_security_list" "instance_to_externals" {
  compartment_id = "${var.compartment_ocid}"
  vcn_id         = "${oci_core_vcn.vcn.id}"
  display_name   = "instance-to-externals"
  egress_security_rules = [
    // 同一 VCN 内を含めた全インターネットへの通信
    {
      stateless   = false
      destination = "${local.cidr_public_internet}"
      protocol    = "${local.security_list_protocol_all}"
    }
  ]
}

instance.tf

Compute Instance を作成し、予約済 Public IP を割り当てる、今回のキモとなるファイルです。

/* サーバ */
resource "oci_core_instance" "instance" {
  compartment_id      = "${var.compartment_ocid}"
  availability_domain = "${lookup(data.oci_identity_availability_domains.availability_domains.availability_domains[0], "name")}"
  shape               = "${var.instance_shape}"
  display_name        = "instance"
  source_details {
    source_type = "image"
    source_id   = "${var.instance_image_ocids[var.region]}"  // 当該リージョン用のイメージの OCID を指定する
  }
  create_vnic_details {
    subnet_id        = "${oci_core_subnet.instance.id}"
    assign_public_ip = false  // 後で予約済 Public IP を割り当てるのでインスタンス作成時は Public IP を割り当てない
    hostname_label   = "${var.instance_hostname_label}"
  }
  metadata {
    ssh_authorized_keys = "${var.instance_ssh_public_key}"
  }
}

/* サーバ の VNIC 一覧を取得する */
data "oci_core_vnic_attachments" "instance_vnics" {
  compartment_id      = "${var.compartment_ocid}"
  availability_domain = "${lookup(data.oci_identity_availability_domains.availability_domains.availability_domains[0], "name")}"
  instance_id         = "${oci_core_instance.instance.id}"
}

/* サーバ の VNIC 一覧からプライマリ VNIC を取得する */
data "oci_core_vnic" "instance_primary_vnic" {
  vnic_id = "${lookup(data.oci_core_vnic_attachments.instance_vnics.vnic_attachments[0], "vnic_id")}"
}

/* サーバ のプライマリ VNIC に割り当てられている Private IP を取得する */
data "oci_core_private_ips" "instance_private_ips" {
  vnic_id = "${data.oci_core_vnic.instance_primary_vnic.id}"
}

/* サーバ の Private IP を指定して予約済 Public IP を割り当てる */
resource "oci_core_public_ip" "instance_reserved_public_ip" {
  compartment_id = "${var.compartment_ocid}"
  lifetime       = "RESERVED"
  display_name   = "instance-public-ip"
  private_ip_id  = "${lookup(data.oci_core_private_ips.instance_private_ips.private_ips[0], "id")}"
}

output.tf

実行結果として出力する内容を定義したファイルです。Compute Instance に割り当てた予約済 Public IP の値を出力しています。

/* サーバに割り当てた予約済 Public IP */
output "instance_public_ip" {
  value = [ "${oci_core_public_ip.instance_reserved_public_ip.ip_address}" ]
}

注意点・ノウハウまとめ

それでは、OCI + Terraform における注意点やノウハウを順に説明していきます。

locals で CIDR Block や Protocol 番号を定義しておくと楽

Terraform の locals を使うと、変数を定義できます。 localsvariable よりも定数ちっくな使い方が想定されますので、よく使う CIDR Block の記述だったり、Security List で設定するプロトコル番号を定義しておくと、ベタ書きによるミスが少なくなります。

利用可能な Availability Domain を取得する

data.oci_identity_availability_domains を取得すると、指定のテナンシで利用可能な Availability Domain の一覧が配列で渡されます。

今回はこの結果を基に、Compute Instance を配置する Availability Domain を指定しています。

単一の Availability Domain を取得できる data.oci_identity_availability_domain という Data Source もありますが、こちらは取得結果に ad_number プロパティがなく、Availability Domain の番号を確認できないので注意が必要です。

Compute Instance の OS イメージは OCID のマップを定義しておく

Compute Instance に適用する OS イメージは、Oracle が発表している OCID を確認して、 map 型の変数を定義しておくと、異なるリージョンでも対応しやすいです。 variables.tfvar.instance_image_ocids 部分で定義しているのがそれです。

OS イメージは、同じ一つの OS イメージでも、リージョンごとに異なる OCID が付与されています。Terraform で操作するリージョンは var.region で定義していますので、 instance.tf ではこの値を組み合わせて、

resource "oci_core_instance" "instance" {
  // ……中略……
  source_details {
    source_type = "image"
    source_id   = "${var.instance_image_ocids[var.region]}"  // 当該リージョン用のイメージの OCID を指定する
  }

このように OS イメージの OCID を取得・指定しています。

VCN と同時に作成されるデフォルトのリソースを変更する

vcn.tf にて oci_core_vcn リソースを作成していますが、このとき

  • デフォルトの Security List
  • デフォルトの DHCP オプション
  • デフォルトのルートテーブル

などが自動的に作成されています。

今回はその内のデフォルト・ルートテーブルについて、Internet Gateway を割り当てるよう変更を加えています。

oci_core_default_route_table リソースで manage_default_resource_id プロパティを指定すると、デフォルトのルートテーブルの定義を変更できます。

リージョナル・サブネットを作成する

Availability Domain をまたいで存在できる「リージョナル・サブネット」を作るには、 oci_core_subnet リソースを作成する際に、 availability_domain プロパティを指定しないようにします。

Load Balancer などのリソースを配置する際はリージョナル・サブネットを使えない場合がありますので、その場合は availability_domain プロパティを指定してあげます。

// Availability Domain を指定して Subnet を作成する例
resource "oci_core_subnet" "ad1_subnet" {
  availability_domain = "${lookup(data.oci_identity_availability_domains.availability_domains.availability_domains[0], "name")}"
  // 以下略……

インスタンスに予約済 Public IP を割り当てる

さて、今回の本題です。ここが一番厄介でした。

インスタンスの作成定義は instance.tfoci_core_instance リソース部分です。この中に create_vnic_details.assign_public_ip というプロパティがありますが、これを true にすると、インスタンスに Public IP が割り当てられます。

しかしこれで割り当てられる Public IP は、インスタンスを破棄すると失われてしまう、動的 Public IP というもので、静的な予約済 Public IP はすんなりと割り当てられませんでした。

それではと、予約済 Public IP を作成する oci_core_public_ip リソースを見てみると、Public IP の割り当て先は private_ip_id 、つまり Private IP の OCID で指定しないといけないようです。

ということは、操作の流れは次のようになります。

  1. Public IP を割り当てずに Compute Instance を作成する
  2. 作成した Compute Instance から Private IP の OCID を取得する
  3. 取得した Private IP の OCID を指定して予約済 Public IP を作成する

実際に処理が実行される順序は、Terraform がよしなに解釈してやってくれるので気にすることはありませんが、このように複数のリソースが絡む際は、実装時に順序を気にしないと組み立てられませんね。

ここでもう一度 oci_core_instance リソースの仕様を確認します。リソースの Argument に create_vnic_details.private_ip という項目があり、ここに Private IP 文字列をしていすると、その Private IP を狙い打ちで使用して Instance を作成できます。もちろん、Instance が属する Subnet の CIDR Block の範囲に収まっている必要があり、既存の Instance と重複したりはできません。また、CIDR Block の 0 番目 (10.0.1.0 など) は予約されているので使えません。

このように、Private IP を指定して Instance を作成することは、可能ではありますがいくつか制約があるので、今回は Private IP はお任せで作らせようとしています。

続いて oci_core_instance リソースの Attributes を見ると private_ip という項目があり、Instance に割り当てられた Private IP が分かりそうに見えるのですが、どうもこれは create_vnic_details.private_ip プロパティを指定したときのみ値が返ってくるようです。今回のように Private IP をお任せで作ろうとしている場合は oci_core_instance リソースから直接 Private IP を知る術がないと分かりました。

では、Instance に割り当てられた Private IP をどうやって知るかというと、Instance にアタッチされる VNIC の情報を Data Source から拾ってくることになります。それぞれの Data Source が取得できる Attribute の制約があり、数珠繋ぎ的に複数の Data Source を辿っていくことになります。 instance.tf のコードも合わせてご確認ください。

  1. oci_core_instance.instance リソース (Instance) を作成する
  2. oci_core_instance.instance リソース (Instance) の id (OCID) を指定して → data.oci_core_vnic_attachments (Instance の VNIC 一覧) を取得する
  3. data.oci_core_vnic_attachments (Instance の VNIC 一覧) の配列の0番目にある VNIC の OCID を指定して → data.oci_core_vnic (プライマリ VNIC) を取得する
  4. data.oci_core_vnic (プライマリ VNIC) の OCID を指定して → data.oci_core_private_ips (当該 VNIC に割り当てられている Private IP 一覧) を取得する
  5. data.oci_core_private_ips (当該 VNIC に割り当てられている Private IP 一覧) の配列の0番目にある Private IP の OCID を取り出す → これを使う

このような流れで、 "${lookup(data.oci_core_private_ips.instance_private_ips.private_ips[0], "id")}" というコードが組み立てられたら、これでようやく Instance に割り当てられた Private IP の OCID が取得できました。

あとはこれを指定して、 oci_core_public_ip リソースを作成すれば、予約済 Public IP を生成して指定の Private IP (→ 作成した Instance) と紐付けられる、という流れになります。

まとめ

非常に長くなりましたが、以上で、Compute Instance に予約済 Public IP を割り当てる Terraform コードが実装できました。

OCI はリソースが非常に細かく抽象化されている印象があり、それは Terraform のコードからも伺えます。「VM に割り当てたい」という表面的な捉え方ではなく、VNIC などインフラ的な知識を前提にとられるので、ともすれば「直感的でない」と見られるかもしれません。

しかしその分、大規模な構成になっても一つひとつの要素は小さくまとまってくれるので、大規模化したときの管理は思ったより容易に済むかもしれません。

他のクラウドサービスの利用経験がある方は、Oracle Cloud の特徴を理解して、そのレールに乗って使ってあげると、混乱せずに済むかと思います。

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です