TerraformとAnsibleの連携で決めることは、Terraformが作ったサーバーの情報をどうやってAnsibleのインベントリに渡すか、というインベントリへの情報の受け渡し方法の1点です。この記事では、EC2を2台作ってnginxを入れる構成を例に、ansible_hostリソースとcloud.terraformのインベントリプラグインでstateを読む方式、amazon.awsのaws_ec2プラグインでタグから引く方式、Terraform 1.14で入ったactionsでterraform applyの中からPlaybookを流す方式の3つを、コピーして動かせるHCLとYAMLで示します。SSH待ちやホスト鍵などの詰まりどころと、連携させない方がよい構成も扱います。版番号は2026年10月時点の公式情報で確かめた内容です。
まとめ:TerraformとAnsibleの連携はインベントリの渡し方を先に決める
役割分担は単純で、VPCやEC2のような箱をTerraformが作り、その中のOS設定やミドルウェアをAnsibleが入れます。迷うのは受け渡しの方式です。
- Terraformとサーバーが1対1で対応する小〜中規模の構成なら、
ansible_hostリソースでホストをstateに書き、cloud.terraformのterraform_providerプラグインで読む方式を既定にする - Terraform以外(Auto Scalingや手作業)でもサーバーが増減するなら、
aws_ec2プラグインでタグから引く方式に切り替える terraform apply1回で構成まで終わらせたいなら、Terraform 1.14以降のactionsでansible_playbook_runを呼ぶ。ただしPlaybookの失敗をapplyの失敗として扱う運用を受け入れられる場合に限るlocal-execでansible-playbookを呼ぶ構成は新しく作らない
どの方式でも、SSHが通るまで待つ処理とホスト鍵の扱いは必ず書きます。
連携の前提:Terraformが作りAnsibleが構成する分担と4つの受け渡し方式
AnsibleとTerraformのどちらを採用するかという比較や、ChefやPuppetを含めた選定の話は構成管理ツールの比較と選び方|Ansible・Terraform・Chef・Puppet・OpenTofuの選定条件で扱っています。ここでは両方を使うと決めたあとの、つなぎ方だけを書きます。
local-exec・state読取・タグ検索・actionsの4方式を並べた比較表
Terraformの出力をAnsibleへ渡す方法は、次の4つに整理できます。どれもTerraformとAnsibleの公式配布物だけで組めるもので、2018年頃によく紹介されたterraform-inventoryのような個人製ツールは含めていません。
| 方式 | 誰がPlaybookを起動するか | インベントリの出どころ | 本記事の扱い |
|---|---|---|---|
| local-exec | Terraform(apply中) | コマンド引数のIP | 新規では選ばない |
| ansible_host+state読取 | 人またはCI | Terraformのstate | 既定の方式 |
| aws_ec2のタグ検索 | 人またはCI | AWSのAPI | 台数が変わる構成 |
| actions | Terraform(apply中) | ansible_inventory | 1.14以降で選択肢 |
分かれ目は、Playbookを起動するのがTerraformか外か、インベントリの正がstateかクラウド側の実体か、の2軸です。
local-execでansible-playbookを呼ぶ構成を避ける2つの理由
EC2リソースの中にprovisioner "local-exec"を書き、作成直後のIPを渡してansible-playbookを呼ぶ例は今も検索上位に残っています。HashiCorpのprovisionerのドキュメントは、provisionerを使う前にあらゆる代替手段を尽くすよう求めており、マシンイメージや構成管理が使えない場合の最後の手段という位置づけです。
実務上の理由は2つあります。1つは失敗時の扱いで、作成時のprovisionerが失敗するとリソースはtaintedとされ、次のapplyで作り直されます。Playbookの1タスクが落ちただけでEC2が破棄・再作成されるのは過剰です。もう1つは、provisionerは作成時にしか動かないため、Playbookだけ流し直す手段が無いことです。
Terraform 1.16系・ansible-core 2.21系の検証環境
以降の例は、制御ノード1台(Linux・macOS・WSL)から東京リージョンにEC2を2台作り、nginxを入れる構成です。AWSの認証とS3バックエンドの設定はTerraformでAWSを構築する手順|aws 6系の認証とS3バックエンド・エラー対処のとおりに済んでいる前提で、Ansible側のインストールとansible.cfgはAnsibleの使い方入門の手順で用意します。
ディレクトリ構成とrequired_providersでのansibleの版固定
Terraformの最新は2026年10月18日時点で1.16.6です。actionsは1.14.0(2025年11月19日)のリリースノートで追加された機能なので、actionsを使う予定があればrequired_versionを1.14以上にします。AnsibleプロバイダはTerraform Registry上のansible/ansibleで、GitHubのリリースでは1.5.0(2026年6月)が最新です。
# ディレクトリ構成(TerraformとAnsibleを同じリポジトリに置く)
infra/
├── terraform/
│ ├── versions.tf
│ ├── main.tf
│ └── variables.tf
└── ansible/
├── ansible.cfg
├── inventory.yml
└── site.yml
# terraform/versions.tf
terraform {
required_version = ">= 1.14"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
ansible = {
source = "ansible/ansible"
version = "~> 1.5"
}
}
}
Ansibleプロバイダ自体はサーバーに何も作りません。インベントリ情報をstateに書くansible_host・ansible_group、Playbookを動かすansible_playbookとactionansible_playbook_run、データソースansible_inventoryなどを提供します。Playbookを実際に動かすのは手元のansible-playbookなので、Terraformを実行するマシンにAnsibleも入れておきます。
ansible_hostリソースでTerraformのstateからインベントリを作る手順
最初に勧めるのが、このstate読取の方式です。Terraformが作ったEC2と同じapplyでインベントリ情報もstateに書くため、サーバーとインベントリがずれません。
EC2の作成と同時にansible_hostでグループと接続変数を登録する
ansible_hostのスキーマで必須なのはホスト名のnameだけで、所属グループのgroupsと、任意のホスト変数を入れるvariablesを持てます。EC2の数に合わせてcountで並べます。
# terraform/main.tf(VPC・サブネット・セキュリティグループは省略)
resource "aws_instance" "web" {
count = 2
ami = var.ami_id
instance_type = "t3.micro"
subnet_id = var.subnet_id
vpc_security_group_ids = [var.web_sg_id]
key_name = var.key_name
tags = {
Name = "web-${count.index + 1}"
Role = "web"
}
}
resource "ansible_host" "web" {
count = length(aws_instance.web)
name = aws_instance.web[count.index].public_ip
groups = ["web"]
variables = {
ansible_user = "ec2-user"
ansible_ssh_private_key_file = var.private_key_path
ansible_ssh_common_args = "-o StrictHostKeyChecking=accept-new"
}
}
nameにはSSHで到達できるアドレスを入れ、踏み台やVPN経由ならprivate_ipに替えます。Roleタグは、タグ検索の方式へ移るときに同じ値でグループを作るために今のうちに付けておきます。
cloud.terraformのterraform_providerでstate読取設定
stateに書いたansible_hostを読むのは、Red Hatが配布するcloud.terraformコレクションのインベントリプラグインです。コレクションの要件はansible-core 2.15以上・Python 3.9以上で、Ansible Galaxyでの最新は4.0.0です。
# コレクションを入れる
ansible-galaxy collection install cloud.terraform
# ansible/inventory.yml
plugin: cloud.terraform.terraform_provider
project_path: ../terraform
プラグインのソースを読むと、stateファイルを直接開くのではなくterraform showを実行して中身を得ています。このためproject_pathのディレクトリでterraform initが済んでいる必要があり、S3バックエンドの場合はAnsibleを実行するユーザーにもstateを読むIAM権限が要ります。project_pathはリストで複数のディレクトリを並べられ、子モジュールの中のansible_hostまで読むにはsearch_child_modules: trueを足します(既定はfalse)。stateに何が入っているかはTerraform stateとは?tfstateの構造とS3バックエンド・移動削除の安全手順で確認できます。
ansible-inventory –graphでの所属確認とPlaybook実行
Playbookを流す前に、Terraformの出力がインベントリとして読めているかを必ず確かめます。
cd infra/terraform
terraform init
terraform apply
cd ../ansible
# webグループに2台のIPが並べば受け渡しは成功
ansible-inventory -i inventory.yml --graph --vars
# 疎通を確かめてからPlaybookを流す
ansible web -i inventory.yml -m ansible.builtin.ping
ansible-playbook -i inventory.yml site.yml
--graphの結果が@ungroupedだけになるときは、project_pathの指す先が違うか、search_child_modulesがfalseのままです。EC2をterraform destroyすればansible_hostも消えるので、削除済みのサーバーにPlaybookが向かうことはありません。
amazon.awsのaws_ec2プラグインでタグからインベントリを引く手順
Auto Scalingのようにapplyと無関係にサーバーが増減する構成では、AWSのAPIに直接問い合わせます。
Roleタグで絞るaws_ec2.ymlの書き方とkeyed_groupsのグループ化
amazon.awsコレクションのaws_ec2プラグイン(ドキュメントは11.4.0時点)は、設定ファイル名の末尾がaws_ec2.ymlかaws_ec2.yamlでないと読み込まれません。制御ノードにはboto3とbotocoreの1.35.0以上が要ります。
# ansible/inventory.aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- ap-northeast-1
filters:
tag:Role: web
instance-state-name: running
keyed_groups:
- key: tags.Role
prefix: role
hostnames:
- ip-address
compose:
ansible_user: "'ec2-user'"
keyed_groupsにより、Role=webのインスタンスはrole_webグループに入ります。Playbookのhostsに指定するのも、接続対象が所属するこのグループ名です。filtersでrunningに絞っておかないと、停止中や終了処理中のインスタンスにも接続しに行って失敗します。Terraform側の仕事は、Roleタグを正しく付けることだけになります。
state読取とタグ検索を選び分ける条件と両方式を混ぜない理由
選び分けは、サーバーの増減を誰が起こすかで決めます。
- EC2を
aws_instanceで1台ずつ書いていて、台数を変えるのもterraform applyだけなら、state読取の方式。AWSのAPI権限をAnsible側に渡さずに済む - Auto Scalingや別チームの手作業でもサーバーが変わるなら、タグ検索の方式。stateに載らないサーバーも拾える
同じグループに両方の方式でホストを足すのは避けます。同じサーバーがIPとホスト名の2通りで登録され、Playbookが2回流れる原因になるためです。
Terraform actionsでapply後にPlaybookを流す手順
Terraform 1.14で入ったactionsのansible_playbook_runを使うと、apply1回の中でPlaybookまで実行できます。
ansible_inventoryとaction_triggerをつなぐHCL設定例
ansible_playbook_runのドキュメントでは、必須の引数はplaybooksだけで、インベントリはJSONで渡すinventoriesかファイルを渡すinventory_filesで指定します。JSONはansible_inventoryデータソースで組み立てられます。起動のきっかけは、actionsのドキュメントにあるとおり、リソースのlifecycleの中に書くaction_triggerです。
# terraform/actions.tf
data "ansible_inventory" "web" {
group {
name = "web"
dynamic "host" {
for_each = aws_instance.web
content {
name = host.value.public_ip
ansible_user = "ec2-user"
ansible_private_key_file = var.private_key_path
ansible_ssh_extra_args = "-o StrictHostKeyChecking=accept-new"
}
}
}
}
action "ansible_playbook_run" "configure_web" {
config {
playbooks = ["${path.module}/../ansible/site.yml"]
inventories = [data.ansible_inventory.web.json]
}
}
# EC2の顔ぶれが変わったときだけPlaybookを流すための中継リソース
resource "terraform_data" "web_config" {
input = aws_instance.web[*].id
lifecycle {
action_trigger {
events = [after_create, after_update]
actions = [action.ansible_playbook_run.configure_web]
}
}
}
action_triggerをaws_instanceに直接書くと、count = 2の各インスタンスごとにPlaybookが起動します。インスタンスIDの一覧を入力に持つterraform_dataに書けば、台数や置き換えが起きたapplyで1回だけ流れます。ansible_inventoryのjson属性はsensitive扱いなので、planの出力に接続情報が出ることはありません。
-invokeでの手動再実行とapply中にPlaybookが失敗したときの扱い
Playbookだけを変えた場合、EC2に変化が無いのでaction_triggerは動きません。そのときはactionを名指しで呼びます。
# EC2に変更が無くても、Playbookだけを流し直す
terraform apply -invoke=action.ansible_playbook_run.configure_web
# 変更を加えずに差分だけ見たいときは、actionの設定でcheck_modeとdiff_modeを有効にする
# config { check_mode = true diff_mode = true ... }
actionsはstateに結果を残しません。Playbookが途中で落ちるとapplyが失敗として終わりますが、provisionerのようにEC2がtaintedになって作り直されることはありません。直して-invokeで流し直せば済みます。ただ、applyの成否にPlaybookの成否が混ざるため、インフラ変更の承認とミドルウェア設定の承認を別の人が行う組織では、この方式は合いません。その場合はstate読取の方式に戻して、2つのコマンドを別のジョブにします。
SSH待ち・ホスト鍵・CIでの実行順まで含めたPlaybook側の作り込み
Terraformが作成完了と返した時点では、EC2のOSはまだ起動途中です。
wait_for_connectionで起動直後のSSH失敗を防ぐPlaybook例
Playbookの冒頭でファクト収集を止め、ansible.builtin.wait_for_connectionでSSHが通るまで待ってから、改めてファクトを集めます。
# ansible/site.yml
- name: Webサーバーを構成する
hosts: web
gather_facts: false
become: true
tasks:
- name: SSHで接続できるまで最大300秒待つ
ansible.builtin.wait_for_connection:
timeout: 300
- name: 接続できてからファクトを集める
ansible.builtin.setup:
- name: nginxを入れる
ansible.builtin.dnf:
name: nginx
state: present
- name: nginxを起動して自動起動を有効にする
ansible.builtin.service:
name: nginx
state: started
enabled: true
gather_facts: trueのままだと、待つ処理より先にファクト収集が接続を試みて落ちます。タグ検索の方式でrole_webグループを使う場合は、hostsをそのグループ名に替えます。
新しいIPのホスト鍵をaccept-newで受け入れる設定と本番での注意点
Terraformで作り直すたびにIPとホスト鍵が変わるので、known_hostsに未登録の鍵で止まります。前の章の例で-o StrictHostKeyChecking=accept-newを入れたのはこのためです。OpenSSHのaccept-newは、未登録の鍵は登録して進み、登録済みの鍵が変わったときだけ拒否します。
host_key_checking = Falseで検査そのものを止める例も見かけますが、接続先のすり替えも素通しにするので使いません。同じIPを使い回すElastic IPの構成では、作り直したサーバーの鍵が「変わった鍵」として拒否されるので、terraform applyの後にssh-keygen -R <IP>で古い鍵を消す手順をジョブに入れます。
CIでterraform apply後にansible-playbookを流す構成
state読取の方式をCIで流す場合、1つのジョブの中で次の順に実行します。AWSの認証をOIDCで行う設定とS3バックエンドのロックはGitHub ActionsとTerraformでAWSのCI/CDを構築する手順と同じです。
# CIジョブのステップ(GitHub ActionsやGitLab CIのrunに書く内容)
terraform -chdir=infra/terraform init
terraform -chdir=infra/terraform apply -auto-approve
pipx install "ansible-core==2.21.*"
ansible-galaxy collection install cloud.terraform
cd infra/ansible
ansible-inventory -i inventory.yml --graph
ansible-playbook -i inventory.yml site.yml
applyとPlaybookを別のジョブに分ける場合は、後のジョブでもterraform initを実行して初期化を済ませる手順が必要です。インベントリプラグインがterraform showを呼ぶためで、ここを忘れると「初期化されていない」というエラーでインベントリが空になります。PlaybookにVaultで暗号化した変数があるときは、復号パスワードをCIのシークレットから渡す手順がAnsible Vaultの使い方にあります。
TerraformとAnsibleを連携させるべき構成と分けたまま運用すべき場面
採用と見送りの線を、条件付きで言い切ります。
連携を採用する条件:環境の作り直しとサーバー台数の変更が月1回以上
検証環境を案件ごとに作っては壊す、負荷試験のたびに台数を変える、といった使い方で、サーバーの作成から構成までを月に1回以上繰り返すなら連携させます。手作業でインベントリにIPを書き写す回数がそのまま事故の機会になるためです。この条件なら、state読取の方式で始め、Auto Scalingを入れた時点でタグ検索に移るのが手戻りの少ない順番です。
連携を見送る場面:コンテナ中心の構成とイメージ焼き込みで足りる構成
次の2つでは連携を作りません。1つはECSやKubernetesが中心の構成で、設定はコンテナイメージとタスク定義に入るためAnsibleの出番がありません。もう1つは、ミドルウェアの構成が月単位で変わらず、起動の速さを優先したい構成です。この場合はAnsibleで設定したイメージをPackerで事前に焼き、Terraformはそのイメージから起動するだけにします。起動のたびにPlaybookを流すより速く、起動時の失敗点も1つ減ります。
サーバーが3台以下で、年に数回しか作り直さないなら、連携の仕組みを作る工数の方が大きくなります。その規模では、terraform outputでIPを出して静的なインベントリに書く運用で十分です。
社内で連携を組みきれないときに外部の支援を入れる作業範囲の決め方
TerraformとAnsibleを単体で書ける人がいても、CIの権限設計やVaultの鍵の受け渡しまで含めて1本のパイプラインにまとめる段階で止まる例は多くあります。止まっている箇所がCIと権限の設計なら、DevOps・CI/CD導入支援のような外部の支援に、パイプラインの設計とレビューだけを任せる方法も選択肢です。Playbookの中身は社内に残すと、運用を引き取るときの負担が小さくなります。
よくある質問
TerraformとAnsibleを組み合わせるときによく出る疑問に、2026年10月時点の仕様で答えます。
TerraformだけでOSの設定までできるのに、Ansibleを併用する意味はありますか?
あります。user_dataで渡すcloud-initのスクリプトは起動時に1回動くだけで、稼働中のサーバーの設定変更には使えません。パッケージ更新や設定ファイルの差し替えを冪等に繰り返すのはAnsibleの役割です。初回起動の最小設定だけをuser_dataに書き、残りをAnsibleに寄せます。
AnsibleからTerraformを実行する構成でもよいですか?
使えます。cloud.terraformコレクションのcloud.terraform.terraformモジュールで、Playbookの中からterraform applyを実行できます。AWXなどのワークフローに作成から構成までを1ジョブで載せたい場合に向く構成です。planをプルリクエストでレビューする体制なら、本記事のようにTerraformを先に流す順番の方が素直です。
terraform-inventoryはまだ使えますか?
2018年頃の記事で紹介されているterraform-inventoryは個人が公開したツールで、新しいTerraformへの追従は保証されていません。同じことはcloud.terraformのterraform_providerプラグインとansible_hostで実現できるので、これから組むなら公式の組み合わせを選びます。
ansible_playbookリソースとansible_playbook_runアクションはどう使い分けますか?
ansible_playbookはリソースなので、実行結果の保存先はstateです。replayable = trueにするとapplyのたびに作り直されてPlaybookが流れ、falseなら最初のapplyで1回だけ流れます。一方のansible_playbook_runはstateに何も残さず、action_triggerか-invokeで呼んだときだけ動く仕組みです。Terraformが1.14以上ならactionの方を選びます。
OpenTofuでも同じ連携はできますか?
state読取とタグ検索の方式は、Terraform本体の機能に依存しないのでOpenTofuでも同じように組めます。cloud.terraformのプラグインはbinary_pathでtofuのバイナリを指定して試します。actionsはTerraform 1.14で入った機能なので、OpenTofuでの対応状況は使う版のリリースノートで確かめてから採用してください。
関連記事
- IaCとは?Infrastructure as Codeの仕組み・メリットと導入判断を解説:TerraformとAnsibleを含むIaC全体の考え方と導入判断。
- Ansibleとは?エージェントレスの仕組みとメリット・デメリット、Red Hat AAPの制約:Ansibleがエージェントを入れずに動く仕組み。
- Ansible Playbookの書き方と実行手順:–checkでのdry-runとhandlers【2.21系】:本記事のsite.ymlを本番向けに育てるときの書き方。
- Semaphore UIとは?Ansible・TerraformのWeb UIの導入手順と料金、AWXとの違い【v2.19】:TerraformとAnsibleの実行をWeb画面から管理する選択肢。
- 構成管理ツールの比較と選び方|Ansible・Terraform・Chef・Puppet・OpenTofuの選定条件:連携の前に、どのツールを使うかを決めるための比較。