サカトラ

自動化したり統計したり

Terraform ↔ Ansible

TerraformとAnsible

TerraformとAnsible、どちらもIaCツールでありインフラ構築の自動化を行うことができます。

TerraformとAnsible、この両者はどちらも一方から他方を呼ぶことができます。
ということでTerraform → AnsibleとAnsible → Terraformで両方のパターンの橋渡しの部分についてを書いていきます。

Terraform → Ansible

Terraformが主体となりAnsible Playbookを実行するやり方で大きくは2つのパターンがあるのでそれぞれ紹介します。

  • outputinventory.iniファイルを作成してlocal-execで実行
  • ansible/ansibleproviderを利用する方法

outputinventory.iniファイルを作成してlocal-execで実行

Terraformのoutputを利用し、Ansibleで利用するためのinventory.iniを作成し、local-execansible-playbookコマンドを実行する方法です。

Ansibleで使用するinventoryファイルはtemplatefileメソッドで作成します。
Jinja2っぽいテンプレートファイル(微妙にJinja2とは違う)を利用してあとは利用している変数のマッピングtemplatefileメソッドの引数で指定すればOKです。

output.tf

resource "local_file" "inventory" {
  depends_on = [aws_instance.myServer]
  content = templatefile("./inventory.tftpl",
    {
      server = aws_instance.myServer
    }
  )
  filename = "./inventory.ini"
}

inventory.tftpl

[server]
${server.tags.Name} ansible_host=${server.public_ip}

[server:vars]
ansible_user="ec2-user"

[all:vars]
ansible_ssh_private_key_file="./private_key.pem"

これでinventory.iniのファイルが生成されるのでそれを利用してansible-playbookコマンドを実行します。

Ansibleを実行するための.tfファイルは下記になります。

ansible.tf

resource "null_resource" "provisioning" {
  depends_on = [local_file.inventory]
  provisioner "remote-exec" {
    connection {
      host        = aws_instance.myServer.public_ip
      user        = "ec2-user"
      private_key = file("./ssh_key.pem")
    }
    inline = [ "echo 'ready to do ansible!'" ]
  }
  provisioner "local-exec" {
    command = "ansible-playbook -i inventory.ini setup_server.yaml"
  }

}

この時、対象のEC2が起動してくるのを待つためにremote-execでのコマンド実行を挟んでおくのがポイントです。
これがないと起動しきっていないインスタンスにPlaybookを実行してしまいます。

ansible/ansibleproviderを利用する

ansible/ansibleプロバイダーが公式より提供されています。 https://registry.terraform.io/providers/ansible/ansible/latest

このansible/ansibleプロバイダーではansible_playbookresourceansible-playbookコマンドを実行することができます。

また、ansible_hostリソースを使用すると、Ansibleのcloud.terraform.terraform_pluginで参照するためのインベントリ情報を作成することもできます。
これは後述のAnsible → Terraformにて使用します。

話を戻してTerraform → Ansibleについて。
このansible_playbookresourceですが、少々癖があり、リソースの設定ではinventoryファイルの指定ができない他、プロバイダーに同梱のansible_hostを全く参照してくれません。
そのため、リソースのnameか、extra_varsの部分でansible_hostansible_userを渡してあげる必要があります。

GitHubのIssueでは外部変数でinventory_fileを指定すると良いというような記述がありますが、inventory_fileはマジック変数でAnsibleの公式ドキュメントではマジック変数はユーザー側から指定できないとあるので、この解決方法はちょっと眉唾です。(ちなみに私はこれでは解決しませんでした。Issueにもそれでうまくいかなかった人のコメントがあります。)

ansible.tf

resource "ansible_playbook" "setup_server" {
  playbook   = "./setup_server.yaml"
  name       = "myServer"
  replayable              = false
  ignore_playbook_failure = true # ここをtrueにしないとproviderが呼べませんでした
  extra_vars = {
    ansible_host                 = aws_instance.myServer.public_ip
    ansible_user                 = "ec2-user"
    ansible_ssh_private_key_file = "./ssh_key.pem"
  }
}

output "playbook_stdout" {
  value = ansible_playbook.setup_server.ansible_playbook_stdout
}

outputansible_playbook_stdoutを指定してあげるとAnsibleの実行結果をoutputとして返してくれます。

ただ、ここでもEC2インスタンスが作成される前にAnsible Playbookが実行されてしまう問題があったので、最終的に下記の形になりました。

ansible.tf

resource "null_resource" "wait_instance" {
  depends_on = [aws_instance.myServer]
  provisioner "remote-exec" {
    connection {
      host        = aws_instance.myServer.public_ip
      user        = "ec2-user"
      private_key = file("./ssh_key.pem")
    }
    inline = ["echo 'ready to do ansible!'"]
  }
}

resource "ansible_playbook" "setup_server" {
  depends_on              = [null_resource.wait_instance]
  playbook                = "./setup_server.yaml"
  name                    = "myServer"
  replayable              = false
  ignore_playbook_failure = true
  extra_vars = {
    ansible_host                 = aws_instance.myServer.public_ip
    ansible_user                 = "ec2-user"
    ansible_ssh_private_key_file = "./ssh_key.pem"
  }
}
output "playbook_stdout" {
  value = ansible_playbook.setup_server.ansible_playbook_stdout
}
output "playbook_stderr" {
  value = ansible_playbook.setup_server.ansible_playbook_stderr
}

null_resourcewait_instanceで対象のインスタンスremote-execをし、それをansible_playbookからdepends_onで依存関係に設定することでインスタンスの立ち上がりを完全に待つことができました。

Ansible → Terraform

AnsibleからTerraformを呼ぶ方法は大きく下記の2つがあります。

  • ansible.builtin.shellモジュールでコマンドから実行する方法
  • cloud.terraformコレクションを利用する方法

ansible.builtin.shellモジュールでコマンドから実行

terraform initterraform applyansible.builtin.shellモジュールで実行します。

インベントリについてはプラットフォームのinventory pluginを用いてみます。
後述のcloud.terraform.terraform_pluginをつかっても良いのですが、差別化のためにここはあえて。

---
plugin: amazon.aws.aws_ec2
regions:
  - ap-northeast-1
filters:
  instance-state-name: running
keyed_groups:
  - key: tags.Name
hostnames:
  - ip-address

inventory pluginを使う場合はansible.cfgで有効化が必要です。

ansible.cfg

[inventory]
enable_pluginsf = amazon.aws.aws_ec2

これでPlaybook側で呼び出せばOKです。

---
# AnsibleからTerraformを呼ぶ
- name: Provisioning
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Call Terraform
      ansible.builtin.shell:
        cmd: terraform init && terraform apply -auto-approve

# Terraformで立ち上げたEC2に設定を行う
- name: Setting
  hosts: myServer
  gather_facts: true
  become: true
  tasks:
    - name: Install package
      ansible.builtin.yum:
        name:
          - httpd
...

cloud.terraformコレクションを利用する

cloud.terraformコレクションのcloud.terraform.terraformモジュールでTerraformのプロジェクトを呼び出します。
Ansibleによる設定投入を実行する際に必要となるインベントリについてはansible/ansibleprovidercloud.terraform.terraform_pluginを今回は使います。

inventory pluginを使う場合はTerraformとAnsibleのそれぞれでファイルに手を入れる必要があります。

Terraform側では、ansible_groupまたはansible_hostresourceを作成します。

inventory.tf

resource "ansible_host" "myServer" {
  depends_on = [aws_instance.myServer]
  name       = aws_instance.myServer.tags.Name
  groups     = ["server"]
  variables = {
    ansible_host                 = aws_instance.myServer.public_ip
    ansible_user                 = "ec2-user"
    ansible_ssh_private_key_file = "ssh_key.pem"
  }
}

Ansible側ではcloud.terraform.terraform_pluginを利用するinventoryファイルを作成します。

inventory.yaml

---
plugin: cloud.terraform.terraform_provider

ansible.cfgファイルにcloud.terraform.terraform_providerを有効化する設定を記述します。

[inventory]
enable_plugin = ini, cloud.terraform.terraform_provider

これでTerraformで作成したインスタンスをAnsibleのインベントリにつなげることができました。

call_terraform.yaml

---
# AnsibleからTerraformを呼ぶ
- name: Provisioning
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Call Terraform
      cloud.terraform.terraform:
        project_path: terraform/
        state: present
        force_init: true

# Terraformで立ち上げたEC2に設定を行う
- name: Setting
  hosts: myServer
  gather_facts: true
  become: true
  tasks:
    - name: Install package
      ansible.builtin.yum:
        name:
          - httpd
...

このcloud.terraform.terraformのドキュメントはこちら
community.general.terraformでもほとんど同じことができます。

ほとんど差がないですが、inventory pluginがある分cloud.terraformの方がお得な気がします。

参考

GitHub - ansible/terraform-provider-ansible: community terraform provider for ansible Inventory plugins — Ansible Documentation templatefile - Functions - Configuration Language | Terraform | HashiCorp Developer Ansible vs. Terraform, clarified Providing Terraform with that Ansible Magic

...

Ansible → Terraformの方が順序性がわかりやすくて個人的に好き。 ansible/ansibleansible_playbookのリソースが使い勝手悪いせいもあるので、今後アップデートがかかることに期待。