はじめに
terraform Advent Calendar 2019 6日目です。
本記事は、Oracle Cloud環境におけるTerraform自動化についての記事になります。
使用するクラウドインフラストラクチャはOracle Cloudになりますが、tfファイルの基本的な書き方は変わらないため、汎用的な考え方は他のクラウドでも流用できるでしょう。
本記事では宣言的な構成管理にはじまり、Terraformの概要、基本的な使い方について解説し、以下の環境(※)をデプロイします。Terraformの威力をご賞味ください!
- インスタンスはWebサーバ2台、DBサーバ1台、運用管理サーバ1台を構築
- 業務LANと運用LANで分けるため、secondary_vnicを設定
- ロードバランサーに自前のSSL証明書をインポートし、ラウンドロビン及びバックエンドの設定
- WebサーバにはApache、DBサーバはPostgreSQLをインストール
(※)本記事で記載しているtfファイルはGitHubで公開しています。
宣言的な構成管理
Terraformを学ぶ前に、宣言的な構成管理と構成管理ツールの目的を知りましょう。
Terraformの価値を最大限に引き出すことができます!
宣言的な構成管理
クラウドネイティブ(※)は従来のインフラリソースを抽象化し、クラウド環境においてアプリケーションを開発するアプローチです。
また、クラウドネイティブはクラウドインフラストラクチャを軸としているため、コンテナ型仮装技術やマイクロサービスなど最小コンポーネントに分割したアプリケーションと親和性が高いのが特徴です。アジャイルの様な短いサイクルのシステムの開発手法に対して柔軟な対応ができるため、高速なリリースが期待できます。
例えば、従来のオンプレミス環境におけるウォーターフォールの開発では、インフラリソースに対して以下のフェーズがあります。
- 要件定義(非機能要件)の性能見積りでサーバリソースを決定
- 基本設計及び詳細設計でシステム構成及びパラメータを設計
- 構築及びテストを行いインフラ環境の完成
そして、リリース後の運用フェーズに入り、ITILに準じたサービスマネジメントを行う場合は構成管理と変更管理、リリース管理の各プロセスが発生します。
しかし、クラウドの民主化やコンテナ仮想化技術の浸透により、DevOpsやクラウドネイティブというバズワードが誕生し、構築から構成管理のライフサイクルを支える技術として、Puppet、Chefなどの構成管理ツールやKubernetes等のオーケストレーションツールが注目され、システムに対するアプローチは宣言的な手続きへと進化してきました。
今、クラウドネイティブにおけるインフラの概念は、宣言的な構成管理に考え方に基づくクラウドネイティブアーキテクチャと言えるでしょう。
宣言的な構成管理を前提とすることで、システムのアーキテクチャ設計時に運用設計も考慮することができます。かつては、運用が始まって発覚される顕在化された課題などについても、設計時に自動化の考慮ができます。また、スケーラブルを容易にします。
(※)クラウドネィティブという言葉は、人工知能と同じ様に言葉の意味として厳密に定義されていません。
構成管理ツールの目的
構成管理ツールの目的について考えます。
DevOpsの観点で大事なのは、構成管理ツールを導入することが目的ではなく、構成管理ツールという手段を活用にすることで開発プロセスをどれだけ最適化できるかが重要です。
以前、Ansibleとは何か 構成管理ツールの目的〜Ansible導入まで最速で理解するに書きましたが、構成管理ツールを導入する目的を明確にすることでやりたいことが見えてきます。
よって、構成管理ツールの目的を明確にし、メリットや運用後のフローを理解し予測できていないと、その恩恵を最大限に発揮できません。
従って、Terraformを導入することで、これまで手動で行っていたクラウドインフラストラクチャのリソース作成を自動化できます。また、Ansibleなどと組み合わせればOS及びMWセットアップを一気通貫で効率よく作業できます。
結果、Terraform等構成管理ツールを導入することで、これまで時間がかかっていた作業を劇的に短縮し、その時間を他の重要な課題などに当てることができます。
また、冪等性という観点で誰がやっても同じになるため、オペレーションミスを予防し品質を維持します。開発フェーズだけでなく、運用フェーズでも大いに機能します。
本記事では、クラウドインフラストラクチャのリソース作成と、OS及びMWの初期セットアップについて、Terraformとcloud-initで行います。
Terraform概要
Terraformは、クラウドインフラストラクチャのリソースを管理するための構成管理ツールの一つです。
DevOps及びInfrastructure as Code等現代のアプリケーション開発に必須なプロビジョニングを自動化するプロセスを提供します。
Terraformの使い方はとてもシンプルです。
tfファイルという定義ファイルに、インスタンスやネットワーク等リソース情報を記述し、terraformコマンドを実行するだけでクラウドインフラストラクチャが構築できます。
イメージとしては、アメリカではポピュラーなキッチン家電「スロークッカー」にとても似ています。食材をセットしてスイッチを押すと料理が出来上がるプロセスは、まさに同じです。
しかし、銀の弾丸ではありません。
Terraformで色々試していると、あれもこれもとやりたくなりますが、中にはクラウドプロパイダの仕様で、できないこともあります。
Terraformを導入する際は、たくさん検証を行い、できること及びできないことを明確にしておくとよいでしょう。
Terraform構築
Oracle Cloud環境におけるTerraformによる構築を行うためには、ユーザ作成を行い、必要なクレデンシャル情報を用意します。あとはTerraformをインストールし、Terraform実行に必要なtfファイルを用意します。
Oracle Cloud環境の前提条件を以下に記載します。
- Oracle Cloudのユーザ作成
- APIの公開キー作成及び設定(APIの公開キー作成手順は、Required Keys and OCIDsを参照)
- SSHの公開鍵の作成(SSHの公開鍵作成手順は、Managing Key Pairs on Linux Instancesを参照)
Terraformのインストール
本記事ではMacを例に解説します。
- はじめにDownload Terraformよりバイナリをダウンロードします。
- ダウンロード後、任意のディレクトリに展開します。
-
.bashrc
ファイル等にパスを通します。以下はアプリケーション配下にパスを通す例です。
export PATH=$PATH:/Applications/
パスを通した後は以下のコマンドを実行し、バージョン情報が出力されればパスは通っています。
# terraform -v
Terraform v0.12.7
Your version of Terraform is out of date! The latest version
is 0.12.13. You can update by downloading from www.terraform.io/downloads.html
(※)上記は、使用しているTerraformのバージョンが古いため出力されています。新しいバージョンのTerraformを使用している場合は表示されません。
Terraform Provider for Oracle Cloud Infrastructure のインストール
後述するterraform init
実行時に自動的にダウンロードされるため、個別のダウンロードは不要です。
tfファイルの作成
tfファイルについて解説します。
はじめに任意の作業ディレクトリに移動します。
本記事では以下のディレクトリ構成になり、カレントディレクトリはociディレクトリとします。
- ディレクトリ構成
.
|-- common
| |-- userdata
| |-- cloud-init1.tpl
| |-- cloud-init2.tpl
| |-- compute.tf
| |-- env-vars
| |-- lb.tf
| |-- network.tf
| |-- provider.tf
| |-- securitylist.tf
|-- oci_api_key.pem
|-- oci_api_key_public.pem
`-- ssh
|-- id_rsa
|-- id_rsa.pub
- 各種ファイル説明
ファイル名 | 役割 |
---|---|
cloud-init1.tpl | Webサーバ用の初期構築スクリプト |
cloud-init2.tpl | DBサーバ用の初期構築スクリプト |
compute.tf | インスタンスのtfファイル |
env-vars | プロパイダーで使用する変数のtfファイル |
lb.tf | ロードバランサーのtfファイル |
network.tf | ネットワークのtfファイル |
provider.tf | プロパイダーのtfファイル |
securitylist.tf | セキュリティリストのtfファイル |
oci_api_key.pem | API秘密鍵 |
oci_api_key_public.pem | API公開鍵 |
id_rsa | SSH秘密鍵 |
id_rsa.pub | SSH公開鍵 |
tfファイルを作成時のポイントについて以下に記載します。
- tfファイル名は分かりやすくし、リソース毎に分割する
- tfファイルの項目はデフォルトから変更がなければ記述しない
- tfファイルの項目の順番は関係ないが、ルールを決めて分かりやすい順番で記述する
- tfファイルの値を記述する際に、
=
の間は詰めなくてもいいので、揃えるなら揃えるなどルールを決めて分かりやすく記述する - リソース名にアンダーバーなど使えない記号がある
本記事で使用するtfファイルについて記載します。(※)
(※)一部の値はクレデンシャルな情報となるため、例としてxで記載
- env-vars
### Authentication details
export TF_VAR_tenancy_ocid=ocid1.tenancy.oc1..aaaaaaaaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export TF_VAR_user_ocid=ocid1.user.oc1..aaaaaaaaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export TF_VAR_fingerprint=12:34:56:78:90:ab:cd:ef:12:34:56:78:90:ab:cd:ef
export TF_VAR_private_key_path=/oci/oci_api_key.pem
export TF_VAR_private_key_password=xxxxxxxx
### Region
export TF_VAR_region=ap-tokyo-1
### Compartment
export TF_VAR_compartment_ocid=ocid1.compartment.oc1..aaaaaaaaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
### Public/private keys used on the instance
export TF_VAR_ssh_public_key=$(cat /oci/ssh/id_rsa.pub)
export TF_VAR_ssh_private_key=$(cat /oci/ssh/id_rsa)
(※)API秘密鍵にパスフレーズを設定しない場合は、private_key_password
は不要です。
- provider.tf
# Variable
variable "tenancy_ocid" {}
variable "user_ocid" {}
variable "fingerprint" {}
variable "private_key_path" {}
variable "private_key_password" {}
variable "region" {}
variable "compartment_ocid" {}
variable "ssh_private_key" {}
variable "ssh_public_key" {}
# Configure the Oracle Cloud Infrastructure provider with an API Key
provider "oci" {
tenancy_ocid = "${var.tenancy_ocid}"
user_ocid = "${var.user_ocid}"
fingerprint = "${var.fingerprint}"
private_key_path = "${var.private_key_path}"
private_key_password = "${var.private_key_password}"
region = "${var.region}"
}
- network.tf
# Virtual Cloud Network
## vcn1
resource "oci_core_virtual_network" "vcn1" {
display_name = "vcn1"
compartment_id = "${var.compartment_ocid}"
cidr_block = "10.0.0.0/16"
dns_label = "vcn1"
}
## vcn2
resource "oci_core_virtual_network" "vcn2" {
display_name = "vcn2"
compartment_id = "${var.compartment_ocid}"
cidr_block = "192.168.0.0/16"
dns_label = "vcn2"
}
# Subnet
## Subnet LB
resource "oci_core_subnet" "LB_Segment" {
display_name = "開発環境_LBセグメント"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
cidr_block = "10.0.0.0/24"
route_table_id = "${oci_core_default_route_table.default-route-table1.id}"
security_list_ids = ["${oci_core_security_list.LB_securitylist.id}"]
}
## Subnet Web
resource "oci_core_subnet" "Web_Segment" {
display_name = "開発環境_WEBセグメント"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
cidr_block = "10.0.1.0/24"
route_table_id = "${oci_core_default_route_table.default-route-table1.id}"
security_list_ids = ["${oci_core_security_list.Web_securitylist.id}"]
}
## Subnet DB
resource "oci_core_subnet" "DB_Segment" {
display_name = "開発環境_DBセグメント"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
cidr_block = "10.0.2.0/24"
route_table_id = "${oci_core_route_table.nat-route-table.id}"
prohibit_public_ip_on_vnic = "true"
security_list_ids = ["${oci_core_security_list.DB_securitylist.id}"]
}
## Subnet Operation
resource "oci_core_subnet" "Ope_Segment" {
display_name = "開発環境_運用セグメント"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn2.id}"
cidr_block = "192.168.1.0/24"
route_table_id = "${oci_core_default_route_table.default-route-table2.id}"
security_list_ids = ["${oci_core_security_list.Ope_securitylist.id}"]
}
# Route Table
## default-route-table1
resource "oci_core_default_route_table" "default-route-table1" {
manage_default_resource_id = "${oci_core_virtual_network.vcn1.default_route_table_id}"
route_rules {
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
network_entity_id = "${oci_core_internet_gateway.internet-gateway1.id}"
}
}
## nat-route-table
resource "oci_core_route_table" "nat-route-table" {
display_name = "nat-route-table"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
route_rules {
destination = "0.0.0.0/0"
network_entity_id = "${oci_core_nat_gateway.nat-gateway.id}"
}
}
## default-route-table2
resource "oci_core_default_route_table" "default-route-table2" {
manage_default_resource_id = "${oci_core_virtual_network.vcn2.default_route_table_id}"
route_rules {
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
network_entity_id = "${oci_core_internet_gateway.internet-gateway2.id}"
}
}
# Internet Gateway
## internet-gateway1
resource "oci_core_internet_gateway" "internet-gateway1" {
display_name = "internet-gateway1"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
}
## internet-gateway2
resource "oci_core_internet_gateway" "internet-gateway2" {
display_name = "internet-gateway2"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn2.id}"
}
# Nat-Gateway
resource "oci_core_nat_gateway" "nat-gateway" {
display_name = "nat-gateway"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
}
(※)サブネットに対してパブリックIPアドレスを許可したくない場合は、prohibit_public_ip_on_vnic = "true"
を記述します。
- lb.tf
/* Load Balancer */
resource "oci_load_balancer" "load-balancer" {
shape = "100Mbps"
compartment_id = "${var.compartment_ocid}"
subnet_ids = [
"${oci_core_subnet.LB_Segment.id}",
]
display_name = "load-balancer"
}
resource "oci_load_balancer_backend_set" "lb-bes1" {
name = "lb-bes1"
load_balancer_id = "${oci_load_balancer.load-balancer.id}"
policy = "ROUND_ROBIN"
health_checker {
port = "80"
protocol = "HTTP"
response_body_regex = ".*"
url_path = "/"
}
}
resource "oci_load_balancer_certificate" "lb-cert1" {
load_balancer_id = "${oci_load_balancer.load-balancer.id}"
ca_certificate = "-----BEGIN CERTIFICATE-----\nMIIC9jCCAd4CCQD2rPUVJETHGzANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCV0ExEDAOBgNVBAcMB1NlYXR0bGUxDzANBgNVBAoMBk9yYWNs\nZTAeFw0xOTAxMTcyMjU4MDVaFw0yMTAxMTYyMjU4MDVaMD0xCzAJBgNVBAYTAlVT\nMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwGT3JhY2xl\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA30+wt7OlUB/YpmWbTRkx\nnLG0lKWiV+oupNKj8luXmC5jvOFTUejt1pQhpA47nCqywlOAfk2N8hJWTyJZUmKU\n+DWVV2So2B/obYxpiiyWF2tcF/cYi1kBYeAIu5JkVFwDe4ITK/oQUFEhIn3Qg/oC\nMQ2985/MTdCXONgnbmePU64GrJwfvOeJcQB3VIL1BBfISj4pPw5708qTRv5MJBOO\njLKRM68KXC5us4879IrSA77NQr1KwjGnQlykyCgGvvgwgrUTd5c/dH8EKrZVcFi6\nytM66P/1CTpk1YpbI4gqiG0HBbuXG4JRIjyzW4GT4JXeSjgvrkIYL8k/M4Az1WEc\n2wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAuI53m8Va6EafDi6GQdQrzNNQFCAVQ\nxIABAB0uaSYCs3H+pqTktHzOrOluSUEogXRl0UU5/OuvxAz4idA4cfBdId4i7AcY\nqZsBjA/xqH/rxR3pcgfaGyxQzrUsJFf0ZwnzqYJs7fUvuatHJYi/cRBxrKR2+4Oj\nlUbb9TSmezlzHK5CaD5XzN+lZqbsSvN3OQbOryJCbtjZVQFGZ1SmL6OLrwpbBKuP\nn2ob+gaP57YSzO3zk1NDXMlQPHRsdSOqocyKx8y+7J0g6MqPvBzIe+wI3QW85MQY\nj1/IHmj84LNGp7pHCyiYx/oI+00gRch04H2pJv0TP3sAQ37gplBwDrUo\n-----END CERTIFICATE-----"
certificate_name = "certificate1"
private_key = "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA30+wt7OlUB/YpmWbTRkxnLG0lKWiV+oupNKj8luXmC5jvOFT\nUejt1pQhpA47nCqywlOAfk2N8hJWTyJZUmKU+DWVV2So2B/obYxpiiyWF2tcF/cY\n\ni1kBYeAIu5JkVFwDe4ITK/oQUFEhIn3Qg/oCMQ2985/MTdCXONgnbmePU64GrJwf\nvOeJcQB3VIL1BBfISj4pPw5708qTRv5MJBOOjLKRM68KXC5us4879IrSA77NQr1K\nwjGnQlykyCgGvvgwgrUTd5c/dH8EKrZVcFi6ytM66P/1CTpk1YpbI4gqiG0HBbuX\nG4JRIjyzW4GT4JXeSjgvrkIYL8k/M4Az1WEc2wIDAQABAoIBAGQznukfG/uS/qTT\njNcQifl0p8HXfLwUIa/lsJkMTj6D+k8DkF59tVMGjv3NQSQ26JVX4J1L8XiAj+fc\nUtYr1Ap4CLX5PeYUkzesvKK6lPKXQvCh+Ip2eq9PVrvL2WcdDpb5695cy7suXD7c\n05aUtS0LrINH3eXAxkpEe5UHtQFni5YLrCLEXd+SSA3OKdCB+23HRELc1iCTvqjK\n5AtR916uHTBhtREHRMvWIdu4InRUsedlJhaJOLJ8G8r64JUtfm3wLUK1U8HFOsd0\nLAx9ZURU6cXl4osTWiy1vigGaM8Xuish2HkOLNYZADDUiDBB3SshmW5IDAJ5XTn5\nqVrszRECgYEA79j1y+WLTyV7yz7XkWk3OqoQXG4b2JfKItJI1M95UwllzQ8U/krM\n+QZjP3NTtB9i1YoHyaEfic103hV9Fkgz8jvKS5ocLGJulpN4CgqbHN6v9EJ3dqTk\no6X8mpx2eP2E0ngRekFyC/OCp0Zhe2KR9PXhijMa5eB2LTeCMIS/tzkCgYEA7lmk\nIdVjcpfqY7UFJ2R8zqPJHOne2+llrl9vzo6N5kx4DzAg7MP6XO9MekOvfmD1X1Lm\nFckXWFEF+0TlN5YvCTR/+OmVufYM3xp4GBT8RZdLFbyI4+xpAAeSC4SeM0ZkC9Jt\nrKqCS24+Kqy/+qSqtkxiPLQrXSdCSfCUlmn0ALMCgYBB7SLy3q+CG82BOk7Km18g\n8un4XhOtX1uiYqa+SCETH/wpd0HP/AOHV6gkIrEZS59BDuXBGFaw7BZ5jPKLE2Gj\n7adXTI797Dh1jydpqyyjrNo0i6iGpiBqkw9x+Bvged7ucy5qql6MxmxdSk01Owzf\nhk5uTEnScfZJy34vk+2WkQKBgBXx5uy+iuN4HTqE5i6UT/FunwusdLpmqNf/LXol\nIed8TumHEuD5wklgNvhi1vuZzb2zEkAbPa0B+L0DwN73UulUDhxK1WBDyTeZZklB\nVWDK5zzfGPNzRs+b4tRwp2gtKPT1sOde45QyWELxmNNo6dbS/ZB9Pijbfnz0S5n1\ns2OFAoGBAJUohI1+d2hKlkSUzpCorQarDe8lFVEbGMu0kX0JNDI7QU+H8vDp9NOl\nGqLm3sCVBYypT8sWfchgZpcVaLRLQCQtWy4+CbMN6DT3j/uBWeDpayU5Gvqt0/no\nvwqbG6b0NEYLRPLEdsS/c8TV9mMlvb0EW+GXfmkpTrTNt3hyXniu\n-----END RSA PRIVATE KEY-----"
public_certificate = "-----BEGIN CERTIFICATE-----\nMIIC9jCCAd4CCQD2rPUVJETHGzANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCV0ExEDAOBgNVBAcMB1NlYXR0bGUxDzANBgNVBAoMBk9yYWNs\nZTAeFw0xOTAxMTcyMjU4MDVaFw0yMTAxMTYyMjU4MDVaMD0xCzAJBgNVBAYTAlVT\nMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwGT3JhY2xl\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA30+wt7OlUB/YpmWbTRkx\nnLG0lKWiV+oupNKj8luXmC5jvOFTUejt1pQhpA47nCqywlOAfk2N8hJWTyJZUmKU\n+DWVV2So2B/obYxpiiyWF2tcF/cYi1kBYeAIu5JkVFwDe4ITK/oQUFEhIn3Qg/oC\nMQ2985/MTdCXONgnbmePU64GrJwfvOeJcQB3VIL1BBfISj4pPw5708qTRv5MJBOO\njLKRM68KXC5us4879IrSA77NQr1KwjGnQlykyCgGvvgwgrUTd5c/dH8EKrZVcFi6\nytM66P/1CTpk1YpbI4gqiG0HBbuXG4JRIjyzW4GT4JXeSjgvrkIYL8k/M4Az1WEc\n2wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAuI53m8Va6EafDi6GQdQrzNNQFCAVQ\nxIABAB0uaSYCs3H+pqTktHzOrOluSUEogXRl0UU5/OuvxAz4idA4cfBdId4i7AcY\nqZsBjA/xqH/rxR3pcgfaGyxQzrUsJFf0ZwnzqYJs7fUvuatHJYi/cRBxrKR2+4Oj\nlUbb9TSmezlzHK5CaD5XzN+lZqbsSvN3OQbOryJCbtjZVQFGZ1SmL6OLrwpbBKuP\nn2ob+gaP57YSzO3zk1NDXMlQPHRsdSOqocyKx8y+7J0g6MqPvBzIe+wI3QW85MQY\nj1/IHmj84LNGp7pHCyiYx/oI+00gRch04H2pJv0TP3sAQ37gplBwDrUo\n-----END CERTIFICATE-----"
lifecycle {
create_before_destroy = true
}
}
resource "oci_load_balancer_path_route_set" "test_path_route_set" {
#Required
load_balancer_id = "${oci_load_balancer.load-balancer.id}"
name = "pr-set1"
path_routes {
#Required
backend_set_name = "${oci_load_balancer_backend_set.lb-bes1.name}"
path = "/test"
path_match_type {
#Required
match_type = "EXACT_MATCH"
}
}
}
resource "oci_load_balancer_hostname" "test_hostname1" {
#Required
hostname = "app.example.com"
load_balancer_id = "${oci_load_balancer.load-balancer.id}"
name = "hostname1"
}
resource "oci_load_balancer_listener" "lb-listener1" {
load_balancer_id = "${oci_load_balancer.load-balancer.id}"
name = "http"
default_backend_set_name = "${oci_load_balancer_backend_set.lb-bes1.name}"
hostname_names = ["${oci_load_balancer_hostname.test_hostname1.name}"]
port = 80
protocol = "HTTP"
connection_configuration {
idle_timeout_in_seconds = "2"
}
}
resource "oci_load_balancer_listener" "lb-listener2" {
load_balancer_id = "${oci_load_balancer.load-balancer.id}"
name = "https"
default_backend_set_name = "${oci_load_balancer_backend_set.lb-bes1.name}"
port = 443
protocol = "HTTP"
ssl_configuration {
certificate_name = "${oci_load_balancer_certificate.lb-cert1.certificate_name}"
verify_peer_certificate = false
}
}
resource "oci_load_balancer_backend" "lb-be1" {
load_balancer_id = "${oci_load_balancer.load-balancer.id}"
backendset_name = "${oci_load_balancer_backend_set.lb-bes1.name}"
ip_address = "${var.ip_address3}"
port = 80
backup = false
drain = false
offline = false
weight = 1
}
resource "oci_load_balancer_backend" "lb-be2" {
load_balancer_id = "${oci_load_balancer.load-balancer.id}"
backendset_name = "${oci_load_balancer_backend_set.lb-bes1.name}"
ip_address = "${var.ip_address4}"
port = 80
backup = false
drain = false
offline = false
weight = 1
}
output "lb_public_ip" {
value = ["${oci_load_balancer.load-balancer.ip_address_details}"]
}
(※)outputを記述すると、リソース作成後にoutputで指定した値を出力することができます。
- securitylist.tf
# Security list
## LB
resource "oci_core_security_list" "LB_securitylist" {
display_name = "開発環境_LBセグメント"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
ingress_security_rules {
source = "0.0.0.0/0"
protocol = "6"
tcp_options {
min = 443
max = 443
}
}
egress_security_rules {
destination = "0.0.0.0/0"
protocol = "ALL"
}
}
## Web
resource "oci_core_security_list" "Web_securitylist" {
display_name = "開発環境_Webセグメント"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
ingress_security_rules {
source = "10.0.0.0/24"
protocol = "6"
tcp_options {
min = 80
max = 80
}
}
egress_security_rules {
destination = "0.0.0.0/0"
protocol = "ALL"
}
}
## DB
resource "oci_core_security_list" "DB_securitylist" {
display_name = "開発環境_DBセグメント"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn1.id}"
ingress_security_rules {
source = "10.0.1.0/24"
protocol = "6"
tcp_options {
min = 5432
max = 5432
}
}
egress_security_rules {
destination = "0.0.0.0/0"
protocol = "ALL"
}
}
## Security list Ope
resource "oci_core_security_list" "Ope_securitylist" {
display_name = "開発環境_運用セグメント"
compartment_id = "${var.compartment_ocid}"
vcn_id = "${oci_core_virtual_network.vcn2.id}"
ingress_security_rules {
source = "192.168.1.0/24"
protocol = "1"
}
ingress_security_rules {
source = "x.x.x.x/32"
protocol = "6"
tcp_options {
min = 22
max = 22
}
}
ingress_security_rules {
source = "192.168.1.0/24"
protocol = "6"
tcp_options {
min = 22
max = 22
}
}
egress_security_rules {
destination = "0.0.0.0/0"
protocol = "ALL"
}
}
(※)パブリックIPアドレスに対するSSHの制限として、source = "x.x.x.x/32"
を記述しています。
- compute.tf
# Variable
variable "ImageOS" {
default = "Oracle Linux"
}
variable "ImageOSVersion" {
default = "7.7"
}
variable "instance_shape" {
default = "VM.Standard.E2.1"
}
variable "fault_domain" {
default = "FAULT-DOMAIN-1"
}
variable "ip_address1" {
default = "192.168.1.2"
}
variable "ip_address2" {
default = "192.168.1.3"
}
variable "ip_address3" {
default = "10.0.1.2"
}
variable "ip_address4" {
default = "10.0.1.3"
}
variable "ip_address5" {
default = "192.168.1.4"
}
variable "ip_address6" {
default = "10.0.2.2"
}
variable "ip_address7" {
default = "192.168.1.5"
}
# Gets a list of Availability Domains
data "oci_identity_availability_domains" "ADs" {
compartment_id = "${var.tenancy_ocid}"
}
# Gets a list of all Oracle Linux 7.7 images that support a given Instance shape
data "oci_core_images" "instance" {
compartment_id = "${var.tenancy_ocid}"
operating_system = "${var.ImageOS}"
operating_system_version = "${var.ImageOSVersion}"
shape = "${var.instance_shape}"
}
# Instance
## Compute Web-Server#1
resource "oci_core_instance" "instance1" {
source_details {
source_type = "image"
source_id = "${lookup(data.oci_core_images.instance.images[0], "id")}"
}
display_name = "Web-Server#1"
availability_domain = "${lookup(data.oci_identity_availability_domains.ADs.availability_domains[0], "name")}"
shape = "${var.instance_shape}"
compartment_id = "${var.compartment_ocid}"
create_vnic_details {
subnet_id = "${oci_core_subnet.Ope_Segment.id}"
assign_public_ip = "true"
private_ip = "${var.ip_address1}"
}
metadata = {
ssh_authorized_keys = "${var.ssh_public_key}"
user_data = "${base64encode(file("./userdata/cloud-init1.tpl"))}"
}
fault_domain = "${var.fault_domain}"
provisioner "remote-exec" {
connection {
host = "${oci_core_instance.instance1.public_ip}"
type = "ssh"
user = "opc"
agent = "true"
timeout = "3m"
}
inline = [
"crontab -l | { cat; echo \"@reboot sudo /usr/local/bin/secondary_vnic_all_configure.sh -c\"; } | crontab -"
]
}
}
### SecondaryVNIC Web-Server#1
resource "oci_core_vnic_attachment" "Web1_secondary_vnic_attachment" {
create_vnic_details {
display_name = "SecondaryVNIC"
subnet_id = "${oci_core_subnet.Web_Segment.id}"
assign_public_ip = "true"
private_ip = "${var.ip_address3}"
skip_source_dest_check = "false"
}
instance_id = "${oci_core_instance.instance1.id}"
}
## Compute Web-Server#2
resource "oci_core_instance" "instance2" {
source_details {
source_type = "image"
source_id = "${lookup(data.oci_core_images.instance.images[0], "id")}"
}
display_name = "Web-Server#2"
availability_domain = "${lookup(data.oci_identity_availability_domains.ADs.availability_domains[0], "name")}"
shape = "${var.instance_shape}"
compartment_id = "${var.compartment_ocid}"
create_vnic_details {
subnet_id = "${oci_core_subnet.Ope_Segment.id}"
assign_public_ip = "true"
private_ip = "${var.ip_address2}"
}
metadata = {
ssh_authorized_keys = "${var.ssh_public_key}"
user_data = "${base64encode(file("./userdata/cloud-init1.tpl"))}"
}
fault_domain = "${var.fault_domain}"
provisioner "remote-exec" {
connection {
host = "${oci_core_instance.instance2.public_ip}"
type = "ssh"
user = "opc"
agent = "true"
timeout = "3m"
}
inline = [
"crontab -l | { cat; echo \"@reboot sudo /usr/local/bin/secondary_vnic_all_configure.sh -c\"; } | crontab -"
]
}
}
### SecondaryVNIC Web-Server#2
resource "oci_core_vnic_attachment" "Web2_secondary_vnic_attachment" {
create_vnic_details {
display_name = "SecondaryVNIC"
subnet_id = "${oci_core_subnet.Web_Segment.id}"
assign_public_ip = "true"
private_ip = "${var.ip_address4}"
skip_source_dest_check = "false"
}
instance_id = "${oci_core_instance.instance2.id}"
}
## Compute DB-Server
resource "oci_core_instance" "instance3" {
source_details {
source_type = "image"
source_id = "${lookup(data.oci_core_images.instance.images[0], "id")}"
}
display_name = "DB-Server"
availability_domain = "${lookup(data.oci_identity_availability_domains.ADs.availability_domains[0], "name")}"
shape = "${var.instance_shape}"
compartment_id = "${var.compartment_ocid}"
create_vnic_details {
subnet_id = "${oci_core_subnet.Ope_Segment.id}"
private_ip = "${var.ip_address5}"
}
metadata = {
ssh_authorized_keys = "${var.ssh_public_key}"
user_data = "${base64encode(file("./userdata/cloud-init2.tpl"))}"
}
fault_domain = "${var.fault_domain}"
provisioner "remote-exec" {
connection {
host = "${oci_core_instance.instance3.public_ip}"
type = "ssh"
user = "opc"
agent = "true"
timeout = "3m"
}
inline = [
"crontab -l | { cat; echo \"@reboot sudo /usr/local/bin/secondary_vnic_all_configure.sh -c\"; } | crontab -"
]
}
}
### SecondaryVNIC DB-Server
resource "oci_core_vnic_attachment" "DB_secondary_vnic_attachment" {
create_vnic_details {
display_name = "SecondaryVNIC"
subnet_id = "${oci_core_subnet.DB_Segment.id}"
assign_public_ip = false
private_ip = "${var.ip_address6}"
skip_source_dest_check = "false"
}
instance_id = "${oci_core_instance.instance3.id}"
}
## Compute Operation-Server
resource "oci_core_instance" "instance4" {
source_details {
source_type = "image"
source_id = "${lookup(data.oci_core_images.instance.images[0], "id")}"
}
display_name = " Operation-Server"
availability_domain = "${lookup(data.oci_identity_availability_domains.ADs.availability_domains[0], "name")}"
shape = "${var.instance_shape}"
compartment_id = "${var.compartment_ocid}"
create_vnic_details {
subnet_id = "${oci_core_subnet.Ope_Segment.id}"
private_ip = "${var.ip_address7}"
}
metadata = {
ssh_authorized_keys = "${var.ssh_public_key}"
user_data = "${base64encode(file("./userdata/cloud-init1.tpl"))}"
}
fault_domain = "${var.fault_domain}"
provisioner "remote-exec" {
connection {
host = "${oci_core_instance.instance3.public_ip}"
type = "ssh"
user = "opc"
agent = "true"
timeout = "3m"
}
inline = [
"crontab -l | { cat; echo \"@reboot sudo /usr/local/bin/secondary_vnic_all_configure.sh -c\"; } | crontab -"
]
}
}
- cloud-init1.tpl
#cloud-config
runcmd:
# download the secondary vnic script
- wget -O /usr/local/bin/secondary_vnic_all_configure.sh https://docs.cloud.oracle.com/iaas/Content/Resources/Assets/secondary_vnic_all_configure.sh
- chmod +x /usr/local/bin/secondary_vnic_all_configure.sh
- sleep 60
- /usr/local/bin/secondary_vnic_all_configure.sh -c
- yum update -y
- echo "Hello World. The time is now $(date -R)!" | tee /root/output.txt
- echo '################### webserver userdata begins #####################'
- touch ~opc/userdata.`date +%s`.start
# echo '########## yum update all ###############'
# yum update -y
- echo '########## basic webserver ##############'
- yum install -y httpd
- systemctl enable httpd.service
- systemctl start httpd.service
- echo '<html><head></head><body><pre><code>' > /var/www/html/index.html
- hostname >> /var/www/html/index.html
- echo '' >> /var/www/html/index.html
- cat /etc/os-release >> /var/www/html/index.html
- echo '</code></pre></body></html>' >> /var/www/html/index.html
- firewall-offline-cmd --add-service=http
- systemctl enable firewalld
- systemctl restart firewalld
- touch ~opc/userdata.`date +%s`.finish
- echo '################### webserver userdata ends #######################'
- cloud-init2.tpl
#cloud-config
runcmd:
# download the secondary vnic script
- wget -O /usr/local/bin/secondary_vnic_all_configure.sh https://docs.cloud.oracle.com/iaas/Content/Resources/Assets/secondary_vnic_all_configure.sh
- chmod +x /usr/local/bin/secondary_vnic_all_configure.sh
- sleep 60
- /usr/local/bin/secondary_vnic_all_configure.sh -c
- echo 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin' > /var/spool/cron/root
- echo '@reboot /usr/local/bin/secondary_vnic_all_configure.sh -c' >> /var/spool/cron/root
# Postgresqlで使用するポート設定
- setenforce 0
- firewall-cmd --permanent --add-port=5432/tcp
- firewall-cmd --permanent --add-port=5432/udp
- firewall-cmd --reload
# 必要パッケージのインストール
- yum install -y gcc
- yum install -y readline-devel
- yum install -y zlib-devel
# PostgreSQLのインストール
- cd /usr/local/src/
- wget https://ftp.postgresql.org/pub/source/v11.3/postgresql-11.3.tar.gz
- tar xvfz postgresql-11.3.tar.gz
- cd postgresql-11.3/
# コンパイル
- ./configure
- make
- make install
# 起動スクリプト作成
- cp /usr/local/src/postgresql-11.3/contrib/start-scripts/linux /etc/init.d/postgres
- chmod 755 /etc/init.d/postgres
- chkconfig --add postgres
- chkconfig --list | grep postgres
# Postgresユーザ作成
- adduser postgres
Terraform構築
はじめに、以下の準備作業を行います。
- 環境変数の有効化
$ source env-vars
- 環境変数の確認
$ env
- SSH秘密鍵の登録(※)
$ ssh-add /oci/ssh/id_rsa
(※)Terraformではパスフレーズで保護されたSSHキーをサポートしていません。SSHエージェントにsshキーを登録することで回避しています。SSHの秘密鍵にパスフレーズを設定していない場合は不要です。
準備作業完了後、いよいよTerraform構築です。
Terraform構築作業は次の3Stepです!
-
terraform init
で初期化 -
terraform plan
で確認 -
terraform apply
で適用
では、順番に見ていきましょう。
terraform init
terraform init
は、Terraform構成ファイルを含む作業ディレクトリを初期化します。引数が指定されていない場合、現在の作業ディレクトリの構成が初期化されます。初期化中に、Terraformはプロバイダーへの直接および間接参照の構成を検索し、必要なプラグインをロードします。
# terraform init
Initializing the backend...
Initializing provider plugins...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.oci: version = "~> 3.40"
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan
terraform plan
は、実行計画を作成するために使用されます。本コマンドを実行しただけでは、実際に反映されません。実際のリソースや状態を変更せずに、期待通りに動くかテストのために使用されます。また、オプションの-out
引数を使用すると、生成されたプランを後で実行するためにファイルに保存されます。なお、tfファイルにエラーがあった場合は検知されますが、terraform plan
が成功してもterraform apply
で失敗する場合もあるので注意が必要です。
# terraform plan
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
data.oci_identity_availability_domains.ADs: Refreshing state...
data.oci_core_images.instance: Refreshing state...
------------------------------------------------------------------------
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.default-route-table1 will be created
+ resource "oci_core_default_route_table" "default-route-table1" {
+ defined_tags = (known after apply)
+ display_name = (known after apply)
+ freeform_tags = (known after apply)
+ id = (known after apply)
+ manage_default_resource_id = (known after apply)
+ state = (known after apply)
+ time_created = (known after apply)
+ route_rules {
+ cidr_block = (known after apply)
+ destination = "0.0.0.0/0"
+ destination_type = "CIDR_BLOCK"
+ network_entity_id = (known after apply)
}
}
/*中略*/
Plan: 32 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
terraform apply
Terraform apply
は、実行計画によってリソースの構成変更が適用されます。
本コマンドを実行すると、terraform.tfstateファイルが生成されます。
# terraform apply
data.oci_identity_availability_domains.ADs: Refreshing state...
data.oci_core_images.instance: Refreshing state...
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.default-route-table1 will be created
+ resource "oci_core_default_route_table" "default-route-table1" {
+ defined_tags = (known after apply)
+ display_name = (known after apply)
+ freeform_tags = (known after apply)
+ id = (known after apply)
+ manage_default_resource_id = (known after apply)
+ state = (known after apply)
+ time_created = (known after apply)
+ route_rules {
+ cidr_block = (known after apply)
+ destination = "0.0.0.0/0"
+ destination_type = "CIDR_BLOCK"
+ network_entity_id = (known after apply)
}
}
/*中略*/
Plan: 32 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
oci_core_virtual_network.vcn2: Creating...
oci_core_virtual_network.vcn1: Creating...
/*中略*/
Apply complete! Resources: 32 added, 0 changed, 0 destroyed.
Outputs:
lb_public_ip = [
[
{
"ip_address" = "X.X.X.X"
"is_public" = true
},
],
]
注意事項
ディレクトリ毎にtfファイルを分けている場合に、コンパートメントを変えて作業するときは、ディレクトリ変更時にsource env-vars
の実行を忘れいない様にしましょう。
ヒヤリハットとして、A環境で作業しました。次にB環境のディクトりに移動して、terraform apply
を実行したとしましょう。A環境で使用した変数が残っていると、意図しないコンパートメントにリソースの作成や削除を起こしてしまう恐れがあります。
リソース確認
terraform apply
実行後、Oracle Cloudのコンソール画面にアクセスして、作成したリソースを確認してみましょう。
- 仮想クラウドネットワーク
- インスタンス
cloud-initも完了後、Outputsで出力されたロードバランサーのIPアドレスにアクセスすると、Webサーバのindex.html
が表示されます。
terraform destroy
terraform destroy
は、構築した環境を全て削除します。
# terraform destroy
data.oci_identity_availability_domains.ADs: Refreshing state...
/*中略*/
Plan: 0 to add, 0 to change, 32 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
/*中略*/
oci_core_virtual_network.vcn1: Destruction complete after 0s
Destroy complete! Resources: 32 destroyed.
ナレッジ
Oracle Cloudでサポートされている「オペレーティングシステム」
Terraformで利用できるOracle Cloudのイメージは以下になります。
$ oci compute image list -c <コンパートメントのOCID> --all | jq -r '.data[] | ."operating-system"' | sort | uniq
Canonical Ubuntu
CentOS
Custom
Oracle Linux
Windows
よって、「Oracle Database」などのカスタムイメージを使用することはできません。
Terraformでデータベースサービスを起動したい場合は、マネージドデータベースサービス-DBaaSを使用することで実現できます。
複数台のインスタンス構築
複数台のインスタンスを構築する場合は、台数分記述するとコード量が増えます。tfファイルで変数の配列を利用することで、繰り返しの実行を行うことができます。本記事では、インスタンス数は多くないので台数分定義しています。
cloud-init
tfファイルでuser_dataの値に、カスタムスクリプトを指定できます。
ユーザーデータを活用する方法については、User-Data Formatsを参照。
cloud-initを使用するときのポイントは、カスタムスクリプトの中身まではチェックしないため、terraform apply
が成功しても、スクリプトが失敗する場合があります。
例として、以下の様に改行が入っている場合は失敗します。
$ sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common
また、terraform apply
が成功してもCloud-Initの処理はまだ完了していません。
意図した通りに動作しているかはインスタンスにログイン後以下のログファイルで確認できます。
# cat /var/log/cloud-init-output.log
Secondary VNIC
Oracle CloudでSecondary VNICを設定する場合は考慮が必要です。
本記事では、secondary_vnic_all_configure.shを実行して、IP構成を行っています。
例えば、terraform apply
実行時に、secondary_vnic_all_configure.shの呼び出すタイミングが早いとIP構成が失敗する場合があります。そのため、本記事では、cloud-Initのuserdataで指定したcloud-init.tplの中で、sleep処理を入れて確実に実行しています。
また、OS再起動時にsecondary VNICを有効にするため、tfファイルのremote-exec
のinline
でcronの設定を行っています。仮に、インスタンスにパブリックIPアドレスを設定しない場合は、remote-exec
が使用できません。その場合は、cloud-initを使用するといいでしょう。
あとは、本記事の様に業務LANと運用LANを分ける場合は、運用LANをプライマリのVNICを指定するのがベストプラクティスです。
既存環境のコード化
Terraformを導入する前に作成したリソースについても、terraform import
を使用すれば既存環境もコード化することができます。
おわりに
Terraformは、検証環境の払い出しから本番環境構築及び運用フェーズの構成管理等どのシーンでも真価を発揮します。
オンプレ育ちの私の様なレガシー環境でインフラエンジニアをやってきたものからすると、terraform apply
を初めて実行し、リソースを作成したときはとても感銘を受けました。
今はアプリケーションエンジニアの方もDockerなどを使用して開発する時代ですが、Terraformはインフラ領域を得意とするSREの役割だと思います。インフラをやってきた人間じゃないと考慮できないところがあると率直に感じました。
これからもTerraformを活用し、開発者が開発のみに集中できる環境を作りたいと思います。