こんにちは!入社1年目、インフラの奥村です。
"immutable infrastructure" という言葉を初めて聞いてから半年がすぎました。
サーバー構築する際に冪等性などを意識してansibleのプレイブックを書いたりしています。
デジタルネイティブ世代ならぬ、サーバー構築自動化ネイティブ世代なのです。
今回は、ansibleからTerraformを使って、さらに便利なサーバー構築自動化をしたいと思います!
1.概要
まずはツールの紹介です。
terraformはHashiCorp社製のオーケストレーションツールです。 このterraformを使ってVMware vSphere上に仮想マシンを立ち上げます。
ansibleは構成管理ツールです。デプロイメントツールとして利用されることもありますが、この記事ではサーバーの設定などをするために使用します。
この二つのツールを使ったサーバー構築のイメージは
- terrafromで仮想マシンを立ち上げる
- ansibleでサーバーの設定をする
となります。
つまり、こんな感じです。
めんどくさいですね。 今回はさらに便利にサーバーを自動構築するため、ansibleでterraformを実行するplaybookを作りました!
つまり、サーバーの構築は、以下のような流れになる予定です。
- ansibleでterraformを実行する
- ansibleでサーバーの設定をする
これを作った理由は「terraformで使うtfファイルを作るのが面倒くさい」
というのが一番大きかったです。
そんなときに思いついたのです。
ansibleでタスク書いたらtfファイルの作成とサーバーの構築まで一気にできるんちゃうか?
と安直な発想が。
ですが、ここで問題が発生しました。
terraformはサーバー作成コマンドを実行すると作成状況をリアルタイムで表示してくれます。
しかし、ansibleでterraformを実行するとokかfailedしか返ってきません。ですので現時点では
- tfファイルの作成
- inventoryの作成
しかしていません。ご了承ください。ですがこの二つを実行できてしまえばすぐにansibleを実行できますね! こんな感じです。
※ 今回の例はサーバーの構築に使用するテンプレートがある程度できていることが前提です。
2.方法
ansibleからterraformで使うtfファイルと、inventoryを作成するために、
単純にansibleのテンプレートモジュールをフル活用しているだけです。
playbook
ansibleのディレクトリ構成はこのようになっています。
. ├── ansible │ ├── build.yml │ ├── group_vars │ │ └── terraform │ ├── roles │ │ └── terraform │ │ ├── files │ │ │ └── provide.tf │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ ├── inventory.j2 │ │ └── tftemplate.j2 │ └── terraform.yml ├── inventory │ └── hosts └── terraform
build.yml
の内容は、terraform.yml
読み込むだけになっており、
terraform.yml
は
- terraform.yml
--- - hosts: terraform roles: - terraform
これだけです。
inventoryもこれに対応して作成しましょう。
- inventory/hosts
[terraform] 127.0.0.1
role/tasks/main.ymlの内容はこのようになっています。
- role/tasks/main.yml
--- - name: make tffile #ロールごとにtfファイルの作成 template: src=tftemplate.j2 dest="{{ playbook_dir }}/../terraform/{{ item.role }}.tf" with_items: "{{ tf_file }}" - name: copy provider #tfファイルのprovide.tfをコピー copy: src=provide.tf dest="{{ playbook_dir }}/../terraform/." - name: terraform plan #terraform applyした結果を変数に保存 expect: command: terraform plan responses: "The user password" : "{{ user.pass }}" "The user name" : "{{ user.name }}" register: result args: chdir: "{{ playbook_dir }}/../terraform/" - name: make result #結果をファイルに書き出す copy: content={{ result.stdout }} dest={{ playbook_dir }}/../result.txt - name: make inventory #インベントリファイルの作成 template: src=inventory.j2 dest="{{ playbook_dir }}/../inventory/hosts"
templateをガンガン使うのでタスクはそこまでたいしたことをしていません。
コピーしているprovide.tf
はvsphereの情報が入っています。
- provide.tf
provider "vsphere" { vsphere_server = "192.168.1.100" allow_unverified_ssl = "true" }
実行時に対話処理に対応するため、pythonのexpectモジュール
が必要です。
※ expectモジュールを使うには
* pythonのバージョン2.6以上
* pexpectのバージョン3.3以上
が接続先のマシンで必要になります。
今回はローカルホストに対してexpectモジュール
を使っているのでローカルにインストールします。
python --version Python 2.7.12 sudo apt-cache python | grep pexpect sudo apt-get install python-pexpect dpkg -l | grep pexpect 4.0.1-1
templateモジュール
次はrole/tasks/main.yml
で使用するtemplateを作成します。
今回はjinja2テンプレートを使用します。
terrafrom用のテンプレートファイルとinventory用のテンプレートファイルを用意し、
group_vars
に設定した値でtfファイルとinventoryを作成します。
そのフル活用jinja2ファイルがこちらの2つです。
- terraform.j2
{% for label in item.ip %} variable "{{ item.service_name }}-{{ item.role }}_{{ lebel }}-ips" { default = { {% for ip_address in item.ip[label] %} "{{ loop.index0 }}" = "{{ ip_address }}" {% endfor %} } } {% endfor %} resource "vsphere_virtual_machine" "{{ item.role }}" { count = {{ item.stack }} domain = "example.com." dns_servers = ["192.168.1.1"] dns_suffixes= ["dns.example.com"] time_zone = "Asia/Tokyo" name = "{{ item.service_name }}-{{ item.role }}-00${count.index+1}" folder = "{{ item.folder }}" datacenter = "ExampleDatacenter" cluster = "{{ item.cluster }}" vcpu = "{{ item.vcpu }}" memory = "{{ item.memory }}" disk { datastore = "{{ item.datastore }}" template = "TemplateDirectory/{{ item.template }}" size = "{{ item.disk_size }}" type = "{{ item.type }}" } {% for label in item.ip %} network_interface { label = "labels {{ label }}" ipv4_address = "${lookup(var.{{ item.service_name }}-{{ item.role }}_{{ label }}-ips, count.index)}" ipv4_prefix_length = "{{ label_network[label].prefix }}" ipv4_gateway = "{{ label_network[label].gateway }}" } {% endfor %} }
- inventory.j2
[terraform] 127.0.0.1 [terraform:vars] ansible_ssh_user={{ ansible_user }} {% set manage = manage_label %} [default] {% for machine in tf_file %} {% for label in machine.ip %} {% if label == manage %} {% for host in machine.ip[label] %} {{ host }} {% endfor %} {% endif %} {% endfor %} {% endfor %} [default:vars] ansible_ssh_user={{ ansible_user }}
続いてgroup_varsにテンプレートとplaybookが使うデータを設定しておきます。
- group_vars/terraform
--- tf_file: - service_name: okumura stack: 1 ip: label1: ['192.168.2.1'] label2: ['172.16.2.1'] role: web folder: ExampleFolder cluster: "ExampleCluster" vcpu: 1 memory: 1024 datastore: ExampleDatastore template: ExampleTemplate disk_size: 10 type: thin #それぞれの環境にあった設定をしましょう。 label_network: label1: gateway: 192.168.2.254 prefix: 24 label2: gateway: 172.16.2.248 prefix: 16 # vSphereにアクセスするユーザーを指定 user: name: vSphere_user pass: vSphere_user_password manage_label: label1 #inventoryに書き込まれるIPを指定。 ansible_user: example #ansibleを実行するユーザーを指定。
これはwebサーバーが一台構成の場合のvarsファイルです。
もし、webサーバを2台作りたい場合はtf_file
のstack
とip
の内容を
tf_file: - service_name: okumura stack: 2 ip: label1: ['192.168.2.1','192.168.2.2] label2: ['172.16.2.1','172.16.2.2] role: web folder: ExampleFolder cluster: "ExampleCluster" vcpu: 1 memory: 1024 datastore: ExampleDatastore template: ExampleTemplate disk_size: 10 type: thin
にして2台分の設定を用意します。
さらに、webサーバーとは別にdbサーバー用のtfファイルを作成したい場合は
tf_file: - service_name: okumura stack: 1 ip: label1: ['192.168.2.1'] label2: ['172.16.2.1'] role: web folder: ExampleFolder cluster: "ExampleCluster" vcpu: 1 memory: 1024 datastore: ExampleDatastore template: ExampleTemplate disk_size: 10 type: thin - service_name: okumura stack: 1 ip: label1: ['192.168.2.2'] label2: ['172.16.2.2'] role: db folder: ExampleFolder cluster: "ExampleCluster" vcpu: 1 memory: 1024 datastore: ExampleDatastore template: ExampleTemplate disk_size: 10 type: thin
このようにtf_file
の中のシーケンスの要素を増やせばwith_items
が自動で対応してくれます。
このようにvarsファイルを用意してansible-playbookを実行すると
- /inventory/ にinventory
- /terraform/ にtfファイル
- / にterraform planの結果
作成してくれます。
実行
webサーバー4台 dbサーバー2台でtf.ファイルとinventoryの作成を実行してみます!
group_vars/terraformのサーバー台数に対応している部分と役割に対応している部分を書き換えます
- service_name: okumura stack: 4 ip: label1: ['192.168.2.1','192.168.2.2','192.168.2.3','192.168.2.4'] #4台分のIP label2: ['172.16.2.1','172.16.2.2','172.16.2.3','172.16.2.4'] role: web
stack: 2 ip: label1: ['192.168.2.5''192.168.2.6'] #2台分のIP role: db
このようにします。 そして
ansible-playbook -i inventory/hosts ansible/terraform.yml
を実行! すると・・・ /terraform/に db.tfとweb.tfが作成されます! web.tfの中身はこんな感じ
variable "okumura-web_label1-ips" { default = { "0" = "192.168.2.1" "1" = "192.168.2.2" "2" = "192.168.2.3" "3" = "192.168.2.4" } } variable "okumura-web_label2-ips" { default = { "0" = "172.16.2.1" "1" = "172.16.2.2" "2" = "172.16.2.3" "3" = "172.16.2.4" } } resource "vsphere_virtual_machine" "web" { count = 4 domain = "example.com." dns_servers = ["192.168.1.1"] dns_suffixes= ["dns.example.com"] time_zone = "Asia/Tokyo" name = "okumura-web-00${count.index+1}" folder = "ExampleFolder" datacenter = "ExampleDatacenter" cluster = "ExampleClaster" vcpu = "1" memory = "1024" disk { datastore = "ExampleDatastore" template = "ExampleTemplate" size = "10" type = "thin" } network_interface { label = "labels label1" ipv4_address = "${lookup(var.okumura-web_label1-ips, count.index)}" ipv4_prefix_length = "24" ipv4_gateway = "192.168.2.254" } network_interface { label = "labels label2" ipv4_address = "${lookup(var.okumura-db_label2-ips, count.index)}" ipv4_prefix_length = "16" ipv4_gateway = "172.16.2.248" } }
おおすばらしい。terraformのループにも対応しているtfファイルが作成されました!
さてinventoryはどうだ~ /inventory/hosts
[terraform] 127.0.0.1 [terraform:vars] ansible_ssh_user=example [default] 192.168.2.1 192.168.2.2 192.168.2.3 192.168.2.4 192.168.2.5 192.168.2.6 [default:vars] ansible_ssh_user=example
あぁすばらしい。
これであとはterraform/
ディレクトリにはいって
terraform apply
するだけでvSphere上に仮想マシンが立ち上がるわけです!
inventoryも作っているのでサーバーに共通しているdefaultの設定もansibleで実行できるのです!
まとめ
- terraformのtfファイル自体にループを記述することができ、複数台の構築のときに役に立ちます。
- jinja2テンプレートにもループを記述することができ、同じような文字列を生成するときに役に立ちます。
- ansibleもwith_itemsでループを使うことができ、同じタスクを別の引数で実行する時に役に立ちます。
ループのループでループを作り出しているのです。
これが言いたかった。。。
templateモジュールを使うと色々便利なことができます。もっといろいろif文だとかできますので、是非試してください。
下記のようにansibleのplaybookを追加することによってすぐにansibleを実行することができますね!
# userを追加するplaybookを追加する例 . ├── ansible │ ├── build.yml │ ├── group_vars │ │ ├── default │ │ └── terraform │ ├── roles │ │ ├── terraform │ │ │ ├── files │ │ │ │ └── provide.tf │ │ │ ├── tasks │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ ├── inventory.j2 │ │ │ └── tftemplate.j2 │ │ └── users │ │ ├── files │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ └── main.yml │ ├── terraform.yml │ └── user_add.yml ├── inventory │ └── hosts └── terraform
最後までご覧くださいまして、ありがとうございました。