この記事はMakuake Advent Calendar 2020の20日目の記事です。
TerraformとAnsibleを組み合わせてVPS上でサーバー構築をしてみたのでその手順をまとめておこうと思います。
趣味で開発しているアプリケーションのインフラ環境をIaCで整備したかったので、勉強を兼ねてTerraformを使ってみました。
ローカルでTerraformを実行してConoHa VPSにサーバーを建てたり、壊したりします。
OpenStackはIaaS環境を構築するためのOSSです。
ConoHaのVPSはOpenStackを採用しており、OpenStack準拠のAPIが用意されています。
cf. www.slideshare.net - "ConoHa" VPS-KVM; OpenStack Grizzly based service
TerraformでOpenStackのproviderを利用することでConoHa VPSにサーバーを構築することができます。
cf. conoha.jp - API
今回はTerraformでOpenStackのproviderを使いますが、AnsibleにもOpenStack Ansibleモジュールというのがあるので、同様のことはAnsibleだけでも実現可能だとは思います。試してはいないですが・・
cf. docs.ansible.com - OpenStack Ansible モジュール
今回作成したソースコードは、github.com - bmf-san/terraform-ansible-openstack-boilerplateに置いてあります。
Terraformでサーバー構築をして、Ansibleでサーバーの初期セットアップをします。
TerraformとAnsibleを両方使う場合は、TerraformからAnsibleを呼ぶのか、AnsibleからTerraformを呼び出すべきなのか迷う気がしますが、下記の記事ではどちらでも良い、正解不正解は特にないという見解でした。
cf. www.redhat.com - HASHICORP TERRAFORM AND RED HAT ANSIBLE AUTOMATION
Terraformはインフラリソースの設定管理、Ansibleはサーバー内の構成管理にそれぞれ強みがあるイメージなので、それぞれが得意な領域を担当できるようには意識しつつ、Terraform内でAnsibleを実行する構成にしてみました。
Terraform、Ansibleそれぞれの役割を意識した上で、コードをどう管理していきたいかという方針によっては逆のパターンが良いという場合もあるのではないかなと思います。
大まかな流れは以下の通りです。
ConoHaのAPI利用のためのAPIトークン取得
↓
利用したいイメージ、VMプランを決める
↓
Terraformのコードを書く
↓
Ansibleのコードを書く
まずはConoHaのAPIを利用するためのAPIトークンを取得します。
APIのエンドポイントはユーザーごとに異なるのでConoHaコントロールパネルのAPI情報にあるエンドポイントのリストを適宜参照してください。
curl -X POST \
-H "Accept: application/json" \
-d '{"auth":{"passwordCredentials":{"username":"USER_NAME","password":"PASSWORD"},"tenantId":"TENANT_ID"}}' \
https://identity.tyo2.conoha.io/v2.0/tokens \
| jq ".access.token.id"
取得したAPIトークンを使ってそれぞれの情報を取得して、利用したいイメージとVMプランを決めます。
利用可能なイメージ一覧を取得します。
curl -X GET \
-H 'Content-Type: application/json' \
-H "Accept: application/json" \
-H "X-Auth-Token: API_TOKEN" \
https://compute.tyo2.conoha.io/v2/TENANT_ID/images \
| jq ".images | sort_by(.name) | map(.name)"
今回はvmi-ubuntu-20.04-amd64-30gb
を使いました。
利用可能なVMプラン一覧を取得します。
curl -X GET \
-H 'Content-Type: application/json' \
-H "Accept: application/json" \
-H "X-Auth-Token: API_TOKEN" \
https://compute.tyo2.conoha.io/v2/TENANT_ID/flavors \
| jq ".flavors | sort_by(.name) | map(.name)"
今回はg-1gb
を選択しました。
1gb以下のプランだとディスクサイズが足りずに構築エラーになるようです。(g-512mb
で試しましたがダメでした。)
必要な情報が揃ったのでコードを書いていきます。
今回は以下のようなディレクトリ構成にしました。
.
├── ansible.cfg
├── main.tf
├── playbooks
├── templates
│ └── playbooks
│ ├── hosts.tpl
│ └── setup.tpl
├── terraform.tfvars
└── variable.tf
3 directories, 12 files
今回はやることが少ないのでtfファイルは特に細かく分割していません。
tfstateファイルの管理については、backendを使って外部ストレージで管理するのが良いかと思いますが、今回はローカルからの実行なので.gitignore
対象に含めるだけになっています。(ローカルとはいえちゃんとやっておきたい部分ではありますが..)
後述しますが、playbooks
にはterraformがtemplates
から生成するhosts
ファイルとsetup
ファイル(yml)が配置されます。
ゼロからインスタンスを構築するので、構築過程でIPアドレスの値を拾ってTerraformからAnsibleに値を渡してあげる必要があるため、hosts
ファイルについてはテンプレ化しておく意義があるかなと思うのですが、setup
ファイル(yml)についてはタスクと変数定義を分けて、変数定義をするファイルをテンプレ化したほうが良いかなと思います。今回は端折って分割していません。
Terraformに寄せすぎると後でAnsibleを切り出したいとなった時などに腰が重くなるような気がするので、この辺りは色んな事例を知りたいところです。
main.tf
の中身はこんな感じです。
terraform {
required_version = ">= 0.14"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "1.33.0"
}
}
}
provider "openstack" {
user_name = (var.user_name)
password = (var.password)
tenant_name = (var.tenant_name)
auth_url = (var.auth_url)
}
resource "openstack_compute_keypair_v2" "example_keypair" {
name = (var.keypair_name)
public_key = file(var.path_to_public_key_for_root)
}
resource "openstack_compute_instance_v2" "example_instance" {
name = (var.instance_name)
image_name = (var.image_name)
flavor_name = (var.flavor_name)
key_pair = (var.keypair_name)
security_groups = [
"gncs-ipv4-ssh",
"gncs-ipv4-web",
]
metadata = {
instance_name_tag = (var.instance_name_tag)
}
}
data "template_file" "hosts" {
template = file("./templates/playbooks/hosts.tpl")
vars = {
host = (var.host)
ip = (openstack_compute_instance_v2.example_instance.access_ip_v4)
}
depends_on = [openstack_compute_instance_v2.example_instance]
}
resource "local_file" "save_hosts" {
content = (data.template_file.hosts.rendered)
filename = "./playbooks/hosts"
depends_on = [openstack_compute_instance_v2.example_instance]
}
data "template_file" "setup" {
template = file("./templates/playbooks/setup.tpl")
vars = {
host = (var.host)
new_user_name = (var.new_user_name)
new_user_password = (var.new_user_password)
shell = (var.shell)
new_user_public_key = file(var.path_to_public_key)
port = (var.port)
}
depends_on = [openstack_compute_instance_v2.example_instance]
}
resource "local_file" "save_setup" {
content = (data.template_file.setup.rendered)
filename = "./playbooks/setup.yml"
depends_on = [openstack_compute_instance_v2.example_instance]
}
resource "null_resource" "example_provisoner" {
provisioner "local-exec" {
command = "ansible-playbook ./playbooks/setup.yml -i ./playbooks/hosts --private-key=${var.path_to_private_key_for_root}"
}
depends_on = [openstack_compute_instance_v2.example_instance]
}
公式でopenstackのproviderがあるのでそれを使っています。
provider "openstack" {
user_name = (var.user_name)
password = (var.password)
tenant_name = (var.tenant_name)
auth_url = (var.auth_url)
}
user_name
、password
はConoHaで作成したAPIユーザーの情報になります。tenant_name
は文字通りテナント名です。auth_url
はわかりづらいのですが、ここではConoHaのIdentity APIのエンドポイント(ex. https://identity.tyo2.conoha.io/v2.0
)になります。
インスタンス構築時にrootユーザーが利用する公開鍵・秘密鍵のキーペアのセットアップです。
cf. registry.terraform.io - openstack_compute_keypair_v2
resource "openstack_compute_keypair_v2" "example_keypair" {
name = (var.keypair_name)
public_key = file(var.path_to_public_key_for_root)
}
公開鍵を指定しない場合は公開鍵・秘密鍵のキーペアが自動で生成され仕組みになっています。
鍵情報はtfstateファイルに出力されるため、実環境で実行する場合はtfstateファイルを適切に管理する必要があります。
公開鍵認証が前提になっていますが、パスワード認証を可能にする方法も無いこともないみたいです。
cf. noaboutsnote.hatenablog.com - 【Openstack】インスタンスOSにパスワードログインできるようする
インスタンスのイメージやVMプラン、ネットワーク構成などインスタンスを構築するためのセットアップです。
cf. registry.terraform.io - openstack_compute_instance_v2
resource "openstack_compute_instance_v2" "example_instance" {
name = (var.instance_name)
image_name = (var.image_name)
flavor_name = (var.flavor_name)
key_pair = (var.keypair_name)
security_groups = [
"gncs-ipv4-ssh",
"gncs-ipv4-web",
]
metadata = {
instance_name_tag = (var.instance_name_tag)
}
}
instance_name
は任意の名前、image_name
は文字通りイメージ名です。flavor_name
は初見だと察しが付きづらいですが、ここではVMプラン名になります。
instance_name_tag
の部分は、ConoHaのコントロールパネルで表示されるネームタグになります。
今回は使用していませんが、user_dataを指定すればcloud-initを使うこともできます。
ex.
user_data = data.template_file.user_data.rendered
data "template_file" "user_data" {
template = file("user_data.sh")
}
null_resourceは他のresourceをトリガとしてプロビジョニングを行うresourceです。トリガはdepends_onで指定します。
構築したインスタンスにAnsibleでプロビジョニングを行いたいので、インスタンスの構築完了(Terraformの実行が完了というのが正確かもしれません。Terraformの実行が終了してもインスタンスの構築が完了しているわけではないので、後述しますがAnsibleでインスタンスの構築を待つ処理を用意しています。)をトリガとしています。
resource "null_resource" "example_provisoner" {
provisioner "local-exec" {
command = "ansible-playbook ./playbooks/setup.yml -i ./playbooks/hosts --private-key=${var.path_to_private_key_for_root}"
}
depends_on = [openstack_compute_instance_v2.example_instance]
}
今回はローカルで実行するのでlocal-exec
を使っています。
ちょうど良い感じのresourceがないかと調べたところ、github.com - jonmorehouse/terraform-provisioner-ansibleというのがありましたが、現在はメンテナンスされていないようでした。
今回はtemplates/playbooks
配下にテンプレートを用意して、Terraform実行時にテンプレを元に実ファイルを生成、生成したファイルを使ってAnsibleを実行する形を取りました。
プロビジョニングの内容は、実行ユーザーの作成、ssh周りの設定調整くらいです。
インスタンスの疎通を待たずしてプロビジョニングしようとしてハマりました...(wait_for_connection
を使って対応しました。)
terraformコマンドオンリーで完結です。
terraform init
terraform plan
terraform apply
terraform show
ssh username@ipaddress -i path_to_private_key
terraform destroy
初Terraformだったので良い勉強になりました。
TerraformはともかくOpenStackは面白い技術だなと思ったのでもう少し深堀りする機会を作りたいです。