
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に主要コレクションを同梱したコミュニティパッケージ)を自分のユーザー領域に隔離して導入できます。
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)でパスワードを渡します。
インベントリの書き方
[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[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に登録してください。
アドホックコマンドで接続を確認する
$ 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・ハンドラー・テンプレート
- 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: reloadedserver {
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にならないものの実行時間が伸びます。
ロールで整理する
$ 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 # ロール内で固定したい値(優先度が高い)- 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で秘密情報を管理する
# 暗号化ファイルを作る(エディタが開く)
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_homelabVaultは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の許可ルールを入れることです。
- 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- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restartedvalidate: 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が残るタスクは冪等に書けていない証拠です。
$ 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 password | becomeのタスクがあるのに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 found | Vaultファイルがあるのに--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になることを冪等性の合格ラインにしてください。
次に読む
- GitHub Actionsで始めるCI/CD:Playbookの
--checkをPRで自動実行し、mainマージで適用する - Proxmox VEで自宅サーバーを構築する:Ansibleの管理対象になるVM・LXCを用意する