ansibleからTerraformを使って、一歩進んだサーバー構築自動化!!

こんにちは!入社1年目、インフラの奥村です。

"immutable infrastructure" という言葉を初めて聞いてから半年がすぎました。

サーバー構築する際に冪等性などを意識してansibleのプレイブックを書いたりしています。

デジタルネイティブ世代ならぬ、サーバー構築自動化ネイティブ世代なのです。

今回は、ansibleからTerraformを使って、さらに便利なサーバー構築自動化をしたいと思います!

1.概要

まずはツールの紹介です。

terraformはHashiCorp社製のオーケストレーションツールです。 このterraformを使ってVMware vSphere上に仮想マシンを立ち上げます。

ansibleは構成管理ツールです。デプロイメントツールとして利用されることもありますが、この記事ではサーバーの設定などをするために使用します。

この二つのツールを使ったサーバー構築のイメージは

  1. terrafromで仮想マシンを立ち上げる
  2. ansibleでサーバーの設定をする

となります。

つまり、こんな感じです。

f:id:AdwaysEngineerBlog:20161111120307p:plain

めんどくさいですね。 今回はさらに便利にサーバーを自動構築するため、ansibleでterraformを実行するplaybookを作りました!

つまり、サーバーの構築は、以下のような流れになる予定です。

  1. ansibleでterraformを実行する
  2. ansibleでサーバーの設定をする

これを作った理由は「terraformで使うtfファイルを作るのが面倒くさい」 というのが一番大きかったです。

そんなときに思いついたのです。

ansibleでタスク書いたらtfファイルの作成とサーバーの構築まで一気にできるんちゃうか?

と安直な発想が。

ですが、ここで問題が発生しました。

terraformはサーバー作成コマンドを実行すると作成状況をリアルタイムで表示してくれます。
しかし、ansibleでterraformを実行するとokかfailedしか返ってきません。ですので現時点では

  • tfファイルの作成
  • inventoryの作成

しかしていません。ご了承ください。ですがこの二つを実行できてしまえばすぐにansibleを実行できますね! こんな感じです。

f:id:AdwaysEngineerBlog:20161111120447p:plain

※ 今回の例はサーバーの構築に使用するテンプレートがある程度できていることが前提です。

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_filestackipの内容を

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

最後までご覧くださいまして、ありがとうございました。