本文へ移動
購読
GUIDE#devtools更新 2026.10.02

Ansibleでサーバー構築を自動化する:インベントリ・Playbook・ロールの基本と実践

SSH経由でエージェントなしにサーバー設定を自動化するAnsible入門。インベントリ、Playbook、ハンドラー、ロール、Vaultを押さえ、SSHハードニングを冪等に配る実例まで。

level
中級
time
約35分
stack
Ansible / Ubuntu 24.04
author
@ryoma_t
// 前提条件
SSH鍵認証とsudoの基礎 / YAMLが読めること / 管理対象のLinuxサーバー(VM可)が2台以上あること
ansible-playbookでweb01・web02・db01に堅牢化ロールを1台ずつ適用し、PLAY RECAPを確認したターミナル
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

Ansibleとは、管理対象のサーバーにエージェントを入れず、SSH経由で設定変更やパッケージ導入を実行するオープンソースの構成管理ツールです。あるべき状態をYAMLのPlaybookに書き、何度実行しても同じ結果に収束する(冪等性)ため、手順書をそのままコードに置き換えられます。

前提はUbuntu 24.04のコントロールノード1台と、SSH鍵でログインできてsudoが使える管理対象サーバー数台です。最後にSSHハードニングをロールとして配布し、--check --diffで安全に検証するところまで進めます。

この記事でわかること

  • エージェントレスの仕組み(SSH + Python)と、冪等性が成り立つ条件・成り立たない条件
  • インベントリ(INI)、group_vars / host_varsの優先順位、ansible.cfgの最小構成
  • Playbookのbecome、notifyとハンドラー、Jinja2テンプレート、loop / when
  • ロールの標準ディレクトリと、Ansible Vault(--vault-id)で秘密情報をGitに置く方法
  • SSHハードニングを自動化する実例と、--check --diff・--limitでの段階的な適用

Ansibleとは:エージェントレスと冪等性

Ansibleはコントロールノード(自分のPCや踏み台)からSSHで各サーバーに入り、小さなPythonスクリプト(モジュール)を一時ディレクトリへ転送して実行し、結果をJSONで受け取ります。管理対象側に必要なのはSSHとPython 3だけで、Ubuntu 24.04なら追加インストールは不要です。既存サーバーにも「入れるだけ」で適用できる反面、SSH鍵とsudo権限の管理がそのままAnsibleの安全性になります。

冪等性とは「同じPlaybookを何度流しても、結果が同じ状態に収束する」性質です。ansible.builtin.aptにstate: presentと書けば、未導入なら入れ、導入済みなら何もしない(changed=0)という判定をモジュール側が行います。ただしansible.builtin.shellやcommandは判定を持たないため毎回changedになり、creates:やchanged_when:を自分で書かない限り冪等になりません。シェルスクリプトとの差はこの「状態判定を誰が持つか」に集約されます。

観点シェルスクリプト + SSHループAnsible
状態判定自分でif文を書くモジュールが現在値と比較してchangedを返す
複数台の並列実行自作が必要forks(既定5)台ずつ並列
変更の事前確認できない--check --diffで差分を表示
秘密情報平文になりがちVaultで暗号化してGitに置ける

インストールとSSH接続の準備

Ubuntu 24.04のaptにもansibleパッケージはありますが、リリースから時間が経つほど古くなります。pipxで入れると、最新のansible(ansible-coreに主要コレクションを同梱したコミュニティパッケージ)を自分のユーザー領域に隔離して導入できます。

terminal(コントロールノード)
sudo apt install -y pipx
pipx install --include-deps ansible
pipx ensurepath        # ~/.local/bin をPATHに追加。シェルを開き直す
ansible --version

# 管理対象へ鍵を配布し、鍵だけで入れてsudoできることを確認
ssh-keygen -t ed25519 -f ~/.ssh/ansible -C "ansible control"
ssh-copy-id -i ~/.ssh/ansible.pub [email protected]
ssh -i ~/.ssh/ansible [email protected] 'sudo -n true && echo sudo OK'

sudo -n trueはパスワードなしでsudoできるかの確認です。NOPASSWDにしていない環境では、実行時に--ask-become-pass(-K)でパスワードを渡します。

インベントリの書き方

inventory.ini
[web]
web01 ansible_host=192.168.10.11
web02 ansible_host=192.168.10.12

[db]
db01 ansible_host=192.168.10.21

[homelab:children]
web
db

[homelab:vars]
ansible_user=admin
ansible_ssh_private_key_file=~/.ssh/ansible
ansible.cfg
[defaults]
inventory = ./inventory.ini
interpreter_python = auto_silent
forks = 10

[privilege_escalation]
become_method = sudo

グループは[web]、グループの入れ子は[homelab:children]、グループ共通の変数は[homelab:vars]です。変数が増えたらgroup_vars/web.ymlやhost_vars/db01.ymlにYAMLで分け、インベントリはホスト一覧だけにします。同名の変数はhost_varsがgroup_varsより優先されます。ansible-inventory --graphで構造を、ansible-inventory --host ホスト名(例ではweb01)で最終的に解決された変数を確認できます。

ansible.cfgにhost_key_checking = Falseを書く例をよく見ますが、SSHのホスト鍵検証を切ると接続先のなりすましに気づけません。初回だけ手動でsshして鍵を受け入れるか、ssh-keyscanの結果をknown_hostsに登録してください。

アドホックコマンドで接続を確認する

terminal
$ ansible all -m ansible.builtin.ping
web01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
(web02、db01も同様)

$ ansible all -a "uptime"
$ ansible web -m ansible.builtin.apt -a "name=nginx state=present" --become
$ ansible db01 -m ansible.builtin.setup -a "filter=ansible_memtotal_mb"

pingモジュールはICMPではなく「SSHで入ってPythonが動く」ことの確認です。-mを省くとcommandモジュールになります。setupが返す情報(facts)はansible_factsとしてPlaybook内で使え、ディストリビューション判定や物理メモリに応じた設定の分岐に利用します。

Playbookの基本:tasks・become・ハンドラー・テンプレート

site.yml
- name: Web servers
  hosts: web
  become: true
  vars:
    server_name: app.home.internal
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Deploy site config
      ansible.builtin.template:
        src: templates/site.conf.j2
        dest: /etc/nginx/sites-available/{{ server_name }}.conf
        mode: "0644"
      notify: Reload nginx

    - name: Enable site
      ansible.builtin.file:
        src: /etc/nginx/sites-available/{{ server_name }}.conf
        dest: /etc/nginx/sites-enabled/{{ server_name }}.conf
        state: link
      notify: Reload nginx

    - name: Ensure nginx is running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
templates/site.conf.j2
server {
    listen 80;
    server_name {{ server_name }};
    root /var/www/{{ server_name }};
{% if enable_gzip | default(true) %}
    gzip on;
{% endif %}
}

become: trueはsudoで実行する指定で、Play全体にもタスク単位にも付けられます。モジュール名はansible.builtin.aptのように完全修飾名(FQCN)で書くと、コレクションが増えても曖昧になりません。notifyは「このタスクがchangedになったら、対応するハンドラーをPlayの最後にまとめて1回だけ実行する」仕組みで、設定ファイルを3つ変えてもリロードは1回で済みます。mode: "0644"を引用符で囲むのは、YAMLの数値として解釈されると意図しないパーミッションになるためです。

テンプレートはJinja2で、{{ 変数 }}の展開と{% if %}の分岐が使えます。| default(true)フィルタは変数が未定義のときの既定値で、これを付けないと未定義変数でタスクが失敗します。cache_valid_time: 3600は「aptのキャッシュが1時間以内ならupdate_cacheを省く」設定で、これがないとupdate_cache: trueのたびにchangedにならないものの実行時間が伸びます。

ロールで整理する

terminal / ディレクトリ構成
$ ansible-galaxy role init roles/hardening
roles/hardening/
├── defaults/main.yml   # 利用側に上書きさせたい既定値(優先度が最も低い)
├── files/              # copy モジュール用の静的ファイル
├── handlers/main.yml
├── meta/main.yml       # 依存ロールとメタ情報
├── tasks/main.yml
├── templates/          # template モジュール用の .j2
└── vars/main.yml       # ロール内で固定したい値(優先度が高い)
site.yml
- name: Baseline for all hosts
  hosts: homelab
  become: true
  serial: 1            # 1台ずつ適用。締め出し事故を1台で止める
  roles:
    - role: hardening
      vars:
        admin_users: [ryoma]

site.ymlのadmin_users: [ryoma]は例で、実際には自分たちの管理ユーザー名を並べます。ロールは「1つの役割(nginx、postgresql、hardening)を1ディレクトリに閉じ込める」単位で、tasks/main.ymlから自動的に読み込まれます。defaultsとvarsの違いは優先度だけで、利用側に上書きさせたい値はdefaultsに置きます。モジュールの多くはコレクションとして配布されており、community.general(ufwモジュールなど)はansibleパッケージに同梱、追加したいものはansible-galaxy collection installで入れてrequirements.ymlに記録します。

Ansible Vaultで秘密情報を管理する

terminal
# 暗号化ファイルを作る(エディタが開く)
ansible-vault create --vault-id homelab@prompt group_vars/all/vault.yml

# 1つの値だけ暗号化して、平文のYAMLに貼り付ける
ansible-vault encrypt_string --vault-id homelab@prompt 'S3cret!' --name 'vault_db_password'

# パスワードファイルで実行(ファイルは chmod 600 にして .gitignore へ)
ansible-playbook site.yml --vault-id homelab@~/.vault_pass_homelab

Vaultはgroup_varsやhost_varsのファイルをAES256で暗号化し、そのままGitにコミットできるようにする仕組みです。慣例として暗号化した変数にはvault_接頭辞を付け、平文側のdb_password: "{{ vault_db_password }}"から参照します。こうするとgrepで「どこで使われているか」を追えます。--vault-id ラベル@ソースの形にしておくと、本番と検証で別のパスワードを持たせて同時に扱え、ansible.cfgのvault_identity_listに書けば毎回の指定も不要です。

実例:サーバーハードニングを自動化する

手作業ではLinuxサーバーの基本ハードニングで行う内容をロールhardeningにします。要点は、sshdの設定を/etc/ssh/sshd_config.d/のドロップインで管理し、validateで構文チェックしてからハンドラーで再起動すること、ufwを有効化する前にSSHの許可ルールを入れることです。

roles/hardening/tasks/main.yml
- name: Install packages
  ansible.builtin.apt:
    name: [ufw, fail2ban, unattended-upgrades]
    state: present
    update_cache: true

- name: Create admin users with sudo
  ansible.builtin.user:
    name: "{{ item }}"
    groups: sudo
    append: true
    shell: /bin/bash
  loop: "{{ admin_users }}"
  when: ansible_facts['distribution'] == 'Ubuntu'

- name: Harden sshd via drop-in
  ansible.builtin.copy:
    dest: /etc/ssh/sshd_config.d/50-hardening.conf
    content: |
      PasswordAuthentication no
      PermitRootLogin no
      KbdInteractiveAuthentication no
      MaxAuthTries 3
    mode: "0600"
    validate: sshd -t -f %s
  notify: Restart sshd

- name: Allow SSH (rate limited) before enabling ufw
  community.general.ufw:
    rule: limit
    port: "22"
    proto: tcp

- name: Enable ufw with default deny
  community.general.ufw:
    state: enabled
    policy: deny

- name: Enable fail2ban
  ansible.builtin.service:
    name: fail2ban
    state: started
    enabled: true
roles/hardening/handlers/main.yml
- name: Restart sshd
  ansible.builtin.service:
    name: ssh
    state: restarted

validate: sshd -t -f %sは、書き込む前に一時ファイル(%s)をsshd -tで検証し、構文エラーなら配置せずタスクを失敗させます。sshdの設定が壊れた状態で再起動すると全台に入れなくなるので、この1行は省かないでください。loopは配列の要素ごとにタスクを繰り返し、whenはfacts(ここではansible_facts['distribution'])で条件分岐します。Ubuntu 24.04のSSHサービス名はsshで、ハンドラーもそれに合わせます。

PasswordAuthentication noを配る前に、鍵でログインできることをアドホックコマンドで全台確認してください。順番を誤ると締め出されます。初回は--limit ホスト名(例ではweb01)で1台に流し、別ターミナルでそのサーバーへのSSHセッションを開いたまま実行すると、失敗しても復旧できます。Playのserial: 1は、この「1台ずつ」を本番でも強制する設定です。

動作確認

まず--check --diffで何が変わるかを見てから本実行し、2回目の実行でchanged=0になることを確かめます。ここでchangedが残るタスクは冪等に書けていない証拠です。

terminal
$ ansible-playbook site.yml --check --diff --limit web01
TASK [hardening : Harden sshd via drop-in] ****************************
--- before
+++ after: /home/me/.ansible/tmp/ansible-local-1234/tmpab12cd/source
@@ -0,0 +1,4 @@
+PasswordAuthentication no
+PermitRootLogin no
+KbdInteractiveAuthentication no
+MaxAuthTries 3
changed: [web01]
(中略)
PLAY RECAP ************************************************************
web01 : ok=8  changed=4  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

$ ansible-playbook site.yml --limit web01
$ ansible-playbook site.yml --limit web01     # 2回目
PLAY RECAP ************************************************************
web01 : ok=8  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

$ ssh -o PreferredAuthentications=password [email protected]
[email protected]: Permission denied (publickey).   # パスワード認証が閉じた

つまずきポイント

症状・エラーメッセージ原因対処
Failed to connect to the host via ssh: [email protected]: Permission denied (publickey).ansible_userか鍵のパスが違う、または鍵がまだ配布されていない同じユーザー・鍵でsshが通るか確認。-vvvで実際のsshコマンドを表示
Missing sudo passwordbecomeのタスクがあるのにNOPASSWDでなく、パスワードも渡していない-Kを付けるか、sudoersでNOPASSWDを設定
couldn't resolve module/action 'community.general.ufw'ansible-coreだけを入れていてコレクションがないansible-galaxy collection install community.general
Attempting to decrypt but no vault secrets foundVaultファイルがあるのに--vault-idを渡していない--vault-idを付けるか、ansible.cfgのvault_identity_listに登録

チェックモードとデバッグ

--checkは変更を加えずに「何がchangedになるか」を、--diffはファイルの差分を表示します。command/shellはcheckモードでスキップされるため、その結果をregisterして後続で使っているとundefined variableで止まります。読み取り専用のコマンドならcheck_mode: falseとchanged_when: falseを付けて、checkモードでも実行させます。

調査は-v(モジュールの戻り値)→-vvv(実際のSSHコマンドと転送)の順に上げ、ansible.builtin.debugのvar:で変数の中身を出します。--tagsや--start-at-taskで途中から再開でき、ansible-lintを入れるとFQCN漏れやmodeの引用符忘れを実行前に拾えます。

よくある質問

AnsibleとTerraformの違いは?

Terraformはクラウドのリソース(VM、ネットワーク、DNS)を作る・消す工程を担当し、stateファイルで実体を追跡します。AnsibleはそのVMの中身(パッケージ、設定ファイル、ユーザー)を整える工程が得意です。「TerraformでVMを作り、Ansibleで設定する」のが一般的な組み合わせで、詳細はTerraformで始めるIaCで扱っています。

Windowsも管理できますか?

できます。接続はWinRM(ansible_connection=winrm、コントロールノードにpywinrmが必要)か、OpenSSHを入れたWindowsならSSHです。モジュールはansible.windowsコレクションのwin_featureやwin_packageなどLinux用とは別のものを使い、Windows側で事前にWinRMリスナーかOpenSSHサーバーの設定が必要です。

冪等性とは?

同じ操作を何回繰り返しても結果が変わらない性質です。Ansibleでは、モジュールが「現在の状態」と「あるべき状態」を比べて必要なときだけ変更し、2回目の実行ではchanged=0になります。shellやcommandのように判定を持たないモジュールは、createsやchanged_whenで自分で判定を与える必要があります。

まとめ

Ansibleの導入で決めるべきことは、鍵とsudoの運用、インベントリの分け方、秘密情報の置き場所(Vault)の3つです。Playbookはbecome・notify・テンプレート・FQCNの4点を守って書き、役割ごとにロールへ分割します。適用は必ず--check --diff → --limitで1台 → serial: 1で全台の順に進め、2回目の実行でchanged=0になることを冪等性の合格ラインにしてください。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。