やりたいこと
TerraformとAnsibleを組み合わせて、良いとこ取りをします。
構築フェーズでは、TerraformとAnsibleでEC2を作成し、運用フェーズでそのままAnsibleが利用できるように、TerraformとAnsibleを書きます。
メリット
- 動的インベントリの活用でホスト管理が不要
- Terraformで作成した terraform.tfstate をAnsibleのインベントリとして利用可能
- 手動でインベントリファイルを更新する必要がなく、管理が楽
- 構成管理をコードベースで管理できる
- user_data等のスクリプトよりも、柔軟な設定管理ができる
- 変更適用が容易
- Ansibleはタスク単位でエラーをキャッチするので、適切なハンドリングがしやすい
- Ansibleは、スクリプトのようにエラー処理を作りこむ必要がない
環境
実行環境は、Cloud9を使用しています。
- Terraform: v1.10.5
- Ansible: 2.18.2
%%{init: {'theme': 'base', 'themeVariables': {'edgeStroke': '#000', 'fontColor': '#000', 'nodeBorder': '#000'}}}%%
graph TD;
User["ユーザ"] -->|"【構築1】Terraform実行"| Terraform["Terraform"]
Terraform -->|"【構築2】EC2インスタンス作成"| EC2["EC2"]
Terraform -->|"【構築3】Ansible実行"| Ansible["Ansible"]
Ansible -->|"【構築4】EC2に初期設定を適用"| EC2["EC2"]
User["ユーザ"] -->|"【運用1】Ansible実行"| Ansible["Ansible"]
Ansible -->|"【運用2】EC2の設定変更"| EC2["EC2"]
%% メインノードのスタイル
classDef mainNode fill:#ffffff,stroke:#000,stroke-width:1px,text-align:center,font-weight:bold,color:#000;
class User,Terraform,Ansible,EC2 mainNode;
%% 構築フェーズのエッジを青に
linkStyle 0 stroke:#007bff,stroke-width:2px;
linkStyle 1 stroke:#007bff,stroke-width:2px;
linkStyle 2 stroke:#007bff,stroke-width:2px;
linkStyle 3 stroke:#007bff,stroke-width:2px;
%% 運用フェーズのエッジを緑に
linkStyle 4 stroke:#28a745,stroke-width:2px;
linkStyle 5 stroke:#28a745,stroke-width:2px;
ディレクトリ構成
ansible.cfgとplaybook.ymlは、terraformディレクトリ内にシンボリックリンクを張ります。
そうすることで、共通のファイルが利用でき、管理するファイルが減ります。
├── ansible
│ ├── ansible.cfg
│ ├── inventory_provider.yml
│ └── playbook.yml
└── terraform
├── ansible.cfg -> ../ansible/ansible.cfg
├── main.tf
├── playbook.yml -> ../ansible/playbook.yml
└── terraform.tfstate
Terraformのコード
main.tf
コードのポイントは以下です。
- required_providersでansibleを記載します
- Terraform に ansible という公式プロバイダーは存在しないため
- resource "aws_instance"で、local-execでsleepをいれます
- resource "ansible_host"で、tfstateのOutputに、Ansibleのプラグインが読み込める形式でホスト情報を追加します
- resource "ansible_playbook"のreplayable = trueで、applyのたびにAnsibleを実行します
terraform { required_providers { ansible = { source = "ansible/ansible" # resource "ansible_***"を使用するため、必須 } } } provider "aws" { region = "ap-northeast-1" access_key = "アクセスキー" secret_key = "シークレットキー" } resource "aws_instance" "instance" { ami = "ami-02e5504ea463e3f34" # Amazonlinux2023 instance_type = "t3.medium" key_name = "sample" associate_public_ip_address = "true" vpc_security_group_ids = [aws_security_group.sg.id] provisioner "local-exec" { # ec2の初期化中に、後続のPlaybookが実行されるとエラーになるため、sleepを追加 command = "sleep 30" } tags = { Name = "mito_ec2" } } resource "aws_security_group" "sg" { name = "mito_sg" ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "mito_ec2" } } # tfstateのOutputに、Ansibleのプラグインが読み込める形式で、ホスト情報を追加します resource "ansible_host" "host" { name = aws_instance.instance.public_dns variables = { ansible_user = "ec2-user", # 必須 ansible_ssh_private_key_file = "~/.ssh/sample.pem", # 必須 ansible_python_interpreter = "/usr/bin/python3", } } # Ansibleで初期設定を投入する resource "ansible_playbook" "playbook" { name = aws_instance.instance.public_dns playbook = "./playbook.yml" replayable = true # terraform applyの度に実行する extra_vars = { ansible_user = "ec2-user" # 必須 ansible_ssh_private_key_file = "~/.ssh/sample.pem" # 必須 } }
Ansibleのコード
3ファイルを用意します。
ansible.cfg
まず、FingerPrintを無効にします。
次に、インベントリのプラグインでcloud.terraform.terraform_stateを追加します。
[defaults] host_key_checking = False [inventory] enable_plugin = ini, yml, cloud.terraform.terraform_state
playbook.yml
gitをインストールします。
--- - hosts: all gather_facts: no become: yes tasks: - name: install git ansible.builtin.yum: name: git state: present
inventory_provider.yml
プラグインにcloud.terraform.terraform_providerを指定します。
project_pathには、tfstateファイルがあるディレクトリを指定します。
--- plugin: cloud.terraform.terraform_provider project_path: ../terraform/
実行ログ
構築フェーズ
terraformを実行します。Terraform内で、さらにAnsibleを実行し、初期設定を行います。
user:~/environment/terraform $ terraform apply -auto-approve Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # ansible_host.host will be created + resource "ansible_host" "host" { + id = (known after apply) + name = (known after apply) + variables = { + "ansible_python_interpreter" = "/usr/bin/python3" + "ansible_ssh_private_key_file" = "~/.ssh/sample.pem" + "ansible_user" = "ec2-user" } } # ansible_playbook.playbook will be created + resource "ansible_playbook" "playbook" { + ansible_playbook_binary = "ansible-playbook" + ansible_playbook_stderr = (known after apply) + ansible_playbook_stdout = (known after apply) + args = (known after apply) + check_mode = false + diff_mode = false + extra_vars = { + "ansible_ssh_private_key_file" = "~/.ssh/sample.pem" + "ansible_user" = "ec2-user" } + force_handlers = false + id = (known after apply) + ignore_playbook_failure = false + name = (known after apply) + playbook = "./playbook.yml" + replayable = true + temp_inventory_file = (known after apply) + verbosity = 1 } # aws_instance.instance will be created + resource "aws_instance" "instance" { (略) # aws_security_group.sg will be created + resource "aws_security_group" "sg" { (略) Plan: 4 to add, 0 to change, 0 to destroy. aws_security_group.sg: Creating... aws_security_group.sg: Creation complete after 3s [id=sg-006c9695827c08cf8] aws_instance.instance: Creating... aws_instance.instance: Still creating... [10s elapsed] aws_instance.instance: Provisioning with 'local-exec'... aws_instance.instance (local-exec): Executing: ["/bin/sh" "-c" "sleep 30"] aws_instance.instance: Still creating... [20s elapsed] aws_instance.instance: Still creating... [30s elapsed] aws_instance.instance: Still creating... [40s elapsed] aws_instance.instance: Creation complete after 42s [id=i-077d3684d9e3ce66b] ansible_host.host: Creating... ansible_playbook.playbook: Creating... ansible_host.host: Creation complete after 0s [id=ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com] ansible_playbook.playbook: Still creating... [10s elapsed] ansible_playbook.playbook: Creation complete after 17s [id=2025-02-20 13:53:19.516832677 +0000 UTC m=+45.856521303] Apply complete! Resources: 4 added, 0 changed, 0 destroyed. user:~/environment/terraform $
tfstateファイルに記載されるansible_hostのOutput情報です。
ユーザと鍵も含まれています。
{ "version": 4, "terraform_version": "1.10.5", "serial": 182, "lineage": "6d9e16bb-7e85-b7a2-3686-03ebd14b5dc4", "outputs": {}, "resources": [ { "mode": "managed", "type": "ansible_host", "name": "host", "provider": "provider[\"registry.terraform.io/ansible/ansible\"]", "instances": [ { "schema_version": 0, "attributes": { "groups": null, "id": "ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com", "name": "ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com", "variables": { "ansible_python_interpreter": "/usr/bin/python3", "ansible_ssh_private_key_file": "~/.ssh/sample.pem", "ansible_user": "ec2-user" } }, "sensitive_attributes": [], "private": "bnVsbA==", "dependencies": [ "aws_instance.instance", "aws_security_group.sg" ] } ] },
運用フェーズ
terraform.tfstateのOutput情報を対象ホストとして、Ansibleを実行します。
user:~/environment/ansible $ ansible-inventory -i inventory_provider.yml --graph @all: |--@ungrouped: | |--ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com user:~/environment/ansible $ user:~/environment/ansible $ ansible-playbook -i inventory_provider.yml playbook.yml PLAY [all] ********************************************************************** TASK [install git] ************************************************************** ok: [ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com] PLAY RECAP ********************************************************************** ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 user:~/environment/ansible $
備考
クラスメソッドさんの SSM State Managerを使ってTerraformで作成したEC2インスタンスの初期化をしてみた | DevelopersIO もあるし、いろいろ方法はありますね!
もし対象ホストがプライベートサブネット内のEC2なら、AAPを使ってPlaybookの実行環境をパブリックサブネットに置くのがよさそう。買ってくれないかな。
最近、TerraformとAnsibleセットで語るとき、あまりPlaybookって言わなくなった。Terraformのファイルに名前がないので。