以下の内容はhttps://mitomito.hatenablog.jp/entry/2025/02/21/074708より取得しました。


[Terraform x Ansible] 構築でTerraformとAnsibleを利用し、運用でそのままAnsibleを流用する

やりたいこと

TerraformとAnsibleを組み合わせて、良いとこ取りをします。
構築フェーズでは、TerraformとAnsibleでEC2を作成し、運用フェーズでそのままAnsibleが利用できるように、TerraformとAnsibleを書きます。

メリット

  1. 動的インベントリの活用でホスト管理が不要
    • Terraformで作成した terraform.tfstate をAnsibleのインベントリとして利用可能
    • 手動でインベントリファイルを更新する必要がなく、管理が楽
  2. 構成管理をコードベースで管理できる
    • user_data等のスクリプトよりも、柔軟な設定管理ができる
    • 変更適用が容易
  3. 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をいれます
    • sleepがないと、EC2の初期化中にansibleが実行され、SSH接続できずに失敗します
    • ほかにもdepends_onやuser_dataを試したのですが、ほぼほぼSSH接続できずに失敗しました
    • 確実に進めるなら、EC2の起動ステータスをチェックするスクリプトを用意するのがいいかも?
  • 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"
  }
}

# tfstateOutputに、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のファイルに名前がないので。




以上の内容はhttps://mitomito.hatenablog.jp/entry/2025/02/21/074708より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14