SSD Nodes Learn
ガイド Matt Connor著者 Matt Connor ・更新日 2026-07-24

Ansibleの使い方:Ubuntu 24.04でVPSを自動設定する

Ubuntu 24.04にpipxでAnsibleを導入し、VPSの要塞化を行うPlaybookを作成します。SSH設定やfirewall構築の手順に加え、Permission deniedやsudo関連のエラー解決策も解説します。手動設定を自動化して、サーバー管理のミスを防ぎましょう。

作成するもの

Ansibleをインストールしたコントロールマシン1台と、標準イメージのみがインストールされた新規のUbuntu 24.04 VPSを1台以上用意します。最終的に、サーバーを定義したinventory file、認証が正常であることを確認するad-hoc ping、そして新規VPS向けのチェックリストをコード化したplaybookを構築します。このplaybookは、SSH keyを持つdeploy userの作成、sshdの要塞化、fail2ban、unattended upgrades、およびOpenSSHを許可した後に他の通信を拒否するfirewallの設定を行います。対象は1台でも20台でも可能です。2回実行しても2回目は何も変更されません。これがこの手法の目的です。

15年間にわたりVPSのプロビジョニングを行ってきた経験から、ある共通のパターンを指摘できます。多くの人は最初の5台を手動で設定しますが、6台目の設定時に、最初の5台で行った作業を忘れてしまい、週末を無駄にします。このガイドは multiple Linux serversの管理 に関する調査を深めるものです。3つのターミナルで同じ apt install を入力していることに気づいたら、ぜひ読んでください。

Ansibleの本質的な定義

Ansibleはagentlessです。管理対象のサーバーにdaemonをインストールする必要はありません。control machineは通常のSSH経由で接続し、小さなPythonモジュールをtargetへコピーして実行します。実行後、出力されたJSONを読み取り、モジュールを削除します。targetに必要なのはpython3だけであり、これは標準のUbuntuイメージに既に含まれています。重要な概念はidempotent(冪等性)です。これは「タスクがアクションではなく、state(状態)を記述する」ことを意味します。パッケージに対するstate: presentは、「インストーラーを実行する」のではなく「インストールされている状態にする」ことを意味します。既にそのstateにある場合、Ansibleは何もしません。その際、changedではなくokと報告します。この特性こそが製品の本質です。この特性により、playbookの再実行が安全になり、安全な再実行によってshell scriptがinfrastructureへと進化します。

前提条件と注意点

  • コントロールマシン:ノートPCまたは小型のVPS。Ubuntu 24.04を想定しています。macOSの場合は、Homebrewでpipxをインストールすれば同様に動作します。
  • ターゲットVPS:KVM上で動作するUbuntu 24.04が1台以上必要です。rootユーザーでアクセス可能である必要があります。ターゲット側にソフトウェアのインストールは行いません。
  • SSH key 認証:すべてのターゲットに対して設定してください。Ansibleの認証方式は ssh コマンドに依存します。ssh root@host でパスワードを求められる場合、Ansibleは失敗します。
  • Ubuntu 24.04の仕様:Ubuntu 24.04では、pip install ansibleerror: externally-managed-environment によって動作しません。これは不具合ではなく、ディストリビューションの意図的な仕様です。pipxを使用してください。
  • YAMLの空白:インデントは構文の一部です。インデントが正しくないと mapping values are not allowed in this context が発生します。タブ文字が混入するとエラーになります。
  • SSHセッションの維持:Playbookでsshdを要塞化(hardening)している間は、各ターゲットでSSHセッションを維持したままにしてください。顧客の復旧作業において、テストのためにセッションを閉じてしまい、ロックアウトが発生するケースが多発しています。

Step 1: pipではなくpipxを使用して、control machineにAnsibleをインストールする

一般的な手法はpip3 install ansibleです。しかし、Command 'pip3' not found, but can be installed with: sudo apt install python3-pipにより、完全に新規の24.04イメージでは早い段階で失敗します。pipをインストールしても、以下の問題に直面するだけです:

pip3 install ansible
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

Ubuntu 24.04では、Pythonがexternally managed(PEP 668)としてマークされています。そのため、pipがaptと同じファイルに対して競合することはできません。--break-system-packagesは使用しないでください。そのflag名は正当な意味を持っています。推奨される方法はpipxです。pipxはAnsible専用のisolated virtualenvを作成し、バイナリをPATHに追加します:

sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible

PATHの変更を反映させるため、pipx ensurepathの後に新しいshellを開いてください。--include-depsは単なる装飾ではありません。ansibleパッケージ自体にはconsole scriptsは含まれていません。ansibleansible-playbookなどは、ansible-core依存関係のエントリポイントです。そのため、flagを指定しない場合、pipxはNo apps associated with package ansible or its dependenciesエラーでインストールを拒否します。また、ansible-coreではなくansibleパッケージをインストールしてください。フルパッケージにはcommunity collectionsが含まれています。このplaybookは、そのうちの2つ(ansible.posixcommunity.general)のモジュールを使用します。

ansible --version

正しい結果はansible [core 2.19.x]のような行で始まり、実行中のPythonの名前が表示されます。現在のコアリリースであれば、すべて問題なく動作します。ansible: command not foundと表示される場合は、~/.local/binがまだPATHに通っていないことを意味します。新しいshellを開くか、source ~/.bashrcを実行してください。

以上でインストールは完了です。targetには何もインストールされません。

Step 2: すべてのターゲットへの SSH key access

ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20

次に、各ホストに対して以下のコマンドを実行し、確認を行います。

ssh root@10.0.0.10 true && echo ok

この1行には2つの役割があります。パスワードなしで key auth が動作することの確認と、known_hosts への host key の記録です。すぐに実行してください。Ansible は未登録の host key がある場合、実行中にインタラクティブなプロンプトを表示します。これは、処理が停止(hang)しているように見えるためです。

Step 3: インベントリ — 最初は INI、規模が大きくなったら YAML

インベントリは、Ansibleが操作する対象のリストを記述したテキストファイルです。新しいプロジェクトディレクトリに inventory.ini を作成してください。

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 は任意のエイリアスです。これは出力に表示され、--limit web1 で指定する対象となります。ansible_host は実際の接続先アドレスです。[vps] はグループであり、[vps:vars] はそのグループ内のすべてのホストに対する変数を設定します。ansible_user は Ansible がログインするユーザーです。その隣にある ansible.cfg を設定しておけば、-i を入力する必要がなくなります。

[defaults]
inventory = inventory.ini

Ansible はカレントディレクトリから ansible.cfg を読み込みます。YAML形式のインベントリも同様です。inventory.yml という名前で保存し、ansible.cfg でそのファイル名を指定してください。ホストごとに多くの変数を管理する場合は、YAML形式が適しています。

vps:
  hosts:
    web1:
      ansible_host: 10.0.0.10
    web2:
      ansible_host: 10.0.0.20
  vars:
    ansible_user: root

これらは同等です。サーバーが2台程度なら INI の方が視認性に優れています。サーバーが20台程度になるなら YAML の方が拡張性に優れています。どちらか一方を選択し、それ以降は形式を固定してください。

Step 4: ad-hoc commands — the green pong that proves everything

ansible all -m ping

これは ICMP ではありません。ping モジュールは、SSH ログイン、モジュールのコピー、ターゲット上での Python 実行、クリーンアップを含む、完全なリハーサルです。正しい結果は、ホストごとに 1 ブロックの緑色になります。

web1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

緑色の SUCCESS は、認証、Python インタープリタ、およびトランスポートがすべて正常であることを意味します。この場合、playbook も正常に動作します。赤色の UNREACHABLE! は、モジュールが実行される前にトランスポートが失敗したことを意味します。詳細なエラー内容と修正方法は、以下の failure modes セクションを確認してください。知っておくべき ad-hoc コマンドがもう 2 つあります。

ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --become

ad-hoc は、単発の操作や確認用です。2 回以上実行する内容は、playbook に記述してください。

Step 5: 最初の playbook — new-VPS checklist as code

これは、新しいサーバーのセットアップ時に手動で行う作業をすべてまとめたものです。site.yml として保存してください。

---
- name: Baseline a fresh Ubuntu VPS
  hosts: vps
  become: true

  vars:
    deploy_user: deploy
    deploy_pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
    baseline_packages:
      - fail2ban
      - unattended-upgrades
      - ufw
    baseline_services:
      - fail2ban
      - unattended-upgrades

  tasks:
    - name: Create the deploy user
      ansible.builtin.user:
        name: "{{ deploy_user }}"
        groups: sudo
        append: true
        shell: /bin/bash

    - name: Install the deploy user's SSH key
      ansible.posix.authorized_key:
        user: "{{ deploy_user }}"
        key: "{{ deploy_pubkey }}"

    - name: Passwordless sudo for the deploy user
      ansible.builtin.copy:
        dest: /etc/sudoers.d/deploy
        content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
        mode: "0440"
        validate: /usr/sbin/visudo -cf %s

    - name: Install baseline packages
      ansible.builtin.apt:
        name: "{{ baseline_packages }}"
        state: present
        update_cache: true

    - name: Enable and start baseline services
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
        enabled: true
      loop: "{{ baseline_services }}"

    - name: Harden sshd with a drop-in
      ansible.builtin.copy:
        dest: /etc/ssh/sshd_config.d/00-hardening.conf
        content: |
          PasswordAuthentication no
          KbdInteractiveAuthentication no
          PermitRootLogin prohibit-password
          X11Forwarding no
        mode: "0644"
        validate: /usr/sbin/sshd -t -f %s
      notify: Restart ssh

    - name: Allow OpenSSH through ufw
      community.general.ufw:
        rule: allow
        name: OpenSSH

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

  handlers:
    - name: Restart ssh
      ansible.builtin.service:
        name: ssh
        state: restarted

コピーするだけでなく、理解しておくべき箇所:

Variablesvars: の下にあり、"{{ deploy_user }}" で参照されます。値が波括弧 { で始まる場合は、YAML parser の誤読を防ぐために式全体を引用符で囲んでください。lookup('file', ...) は実行時に control マシンから公開鍵を読み取るため、playbook 自体に鍵情報は含まれません。

The loop. loop: "{{ baseline_services }}" は各アイテムに対してサービスタスクを1回実行し、出力には各アイテムが1行ずつ表示されます。一方、apt タスクはパッケージリスト全体を一度に処理します。1回の apt トランザクションの方が高速であり、パッケージ管理における推奨パターンです。ループは、一度に1つの対象に対して動作するモジュールに使用します。

The handler の概念を理解してください。notify: Restart ssh は「今すぐ sshd を再起動する」という意味ではありません。handler をキューに入れ、play の最後に、かつ notifying タスクが実際に changed を報告した場合にのみ実行されます。明日、playbook を再実行してください。drop-in ファイルは既に正しく、copy タスクは ok を報告し、sshd は再起動されません。validate: の行は安全装置です。sshd は古いファイルを置き換える前にファイルをチェックするため、タイポがあってもデーモンが壊れるのではなく、タスクが失敗するだけで済みます。

PermitRootLogin prohibit-password ではなく no を意図的に使用。 この playbook は鍵を使用して root としてログインします。prohibit-password は、自身のアクセスを維持したまま、root へのパスワードログインを無効にします。デプロイユーザーの動作が確認できたら(ssh deploy@10.0.0.10 sudo trueweb1 は Ansible だけが知るエイリアスであるため、プレーンなアドレスを使用)、inventory 内の ansible_user=deploy を切り替え、後の実行で no に制限を強めます。自分が締め出されない順序で hardening を行ってください。

00- プレフィックスが重要です。 ほとんどのキーワードにおいて、sshd は最初に解析したものを優先します。Ubuntu の sshd_config は、自身の本体の前に辞書順で sshd_config.d/*.conf を含んでいます。Ubuntu 24.04 の cloud images には、既にそのディレクトリに 60-cloudimg-settings.conf が含まれています。また、cloud-init を通じてパスワードログインを有効にするプロバイダーは、PasswordAuthentication yes を持つ 50-cloud-init.conf を追加します。00-hardening.conf という名前を付けることで、それが最初にソートされ、両方よりも優先されます。

Task order はファイアウォールの安全策です。 Allow OpenSSH は、deny ポリシーを持つ Enable ufw の前に実行されます。Ansible はリストされた順序通りにタスクを実行するため、壁が構築される前に穴が存在することになります。fail2ban は、ここで役立てるために特別な設定を必要としません。Ubuntu のデフォルト設定で sshd を監視します。jail の実際の動作と調整方法については、fail2ban on Ubuntu 24.04 guide で説明しています。

Step 6: --check による dry run の実行、その後実際に実行

ansible-playbook site.yml --check

Check mode は接続を行い、実行予定の内容を計算しますが、変更は一切行いません。末尾にある PLAY RECAP 内の changed= を確認してください。これが、各ホストに対して実行される予定のタスク数です。注意点があります。後のタスクが前のタスクによる変更に依存している場合、check mode では構造上の制限により失敗します。Ubuntu の標準サーバーイメージには ufw がプリインストールされているため、この playbook の dry run は正常に終了します。しかし、ufw が入っていない最小構成のイメージでは、check mode で ufw タスクが失敗します。これは、check mode ではパッケージが実際にインストールされないため、モジュールが呼び出す対象が存在しないことが原因です。これは dry run の仕様であり、playbook のバグではありません。計画に問題がなければ、以下を実行してください。

ansible-playbook site.yml

各タスクはホストごとに 1 行ずつ結果を表示します(黄色は changed、緑は ok)。要約は以下のようになるはずです。

PLAY RECAP *********************************************************************
web1 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
web2 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

10 個の ok、8 つのタスク、および handler の合計です。changed は私の結果と 1、2 個異なる場合があります。Ubuntu の標準イメージには ufwunattended-upgrades がプリインストールされているためです。また、fail2ban は apt でインストールされた直後に起動するため、最初の実行時に「すでにその状態である」と報告し、ok となることがあります。unreachablefailed は必ず 0 でなければなりません。become: true について:これは root として接続している間の形式的なものですが、ansible_userdeploy に変更した瞬間、sudo が適用されます。この playbook がインストールする NOPASSWD 設定の sudoers ファイルにより、-K がコマンドラインに表示されなくなります。これがない場合、以下で説明する Missing sudo password が発生します。

Step 7: 2回実行する — 冪等性(idempotence)の動作

すぐに同じコマンドを再度実行してください:

web1 : ok=9  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0ok は、通知(notify)されたハンドラが実行されなかったため、値が1減少しました。再インストール、sshd の再起動、ufw への変更は一切行われていません。この性質により、Playbook はプロビジョナーであると同時に「監査(audit)」の役割も果たします。来月、inventory に web3 を追加して再実行してください。新しいサーバーは構築され、既存のサーバーは検証されます。未操作のサーバーで changed がゼロでない場合、それはドリフト(drift)を意味します。これは、Playbook で管理すべき設定が手動で変更されたことを示しています。

このパターンは積み重ねていくことができます。次に作成すべき Playbook は、同じ VPS 上に WireGuard VPN を構築し、SSH がトンネル経由でのみ応答するように ufw ルールを強化するものです。その次は、すべてのアプリサーバーに Docker と Compose をインストールするものです。site.yml の出力が3画面分を超えたら、role に分割してください。ただし、それまでは分割しないでください。

失敗パターンと表示される文字列

UNREACHABLE with Permission denied.

web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: root@10.0.0.10: Permission denied (publickey).",
    "unreachable": true
}

モジュールが実行される前にSSH接続に失敗しました。原因は ansible_user の設定ミス、鍵がホストにコピーされていない、または誤った鍵を提示していることです。まず plain ssh root@10.0.0.10 を実行し、次に ssh -v を実行して提示された鍵を確認してください。SSHでのパスワード認証は成功するが Ansible が失敗する場合は、ssh-copy-id をスキップしています。

Missing sudo password.

web1 | FAILED! => {
    "msg": "Missing sudo password"
}

become: true を設定し、非rootユーザーとして接続しています。そのユーザーに sudo のパスワードが必要です。コマンドラインに -K (--ask-become-pass) を追加するか、ユーザーに NOPASSWD の sudoers 設定を付与してください。Playbook が deploy に切り替える前に設定をインストールするのは、まさにこのためです。

error: externally-managed-environment. Ubuntu 24.04 のシステム Python に対して pip を実行しました。ステップ 1 の内容を参照してください。pipx を使用し、pip や --break-system-packages は使用しないでください。

mapping values are not allowed in this context.

ERROR! Syntax Error while loading YAML.
  mapping values are not allowed in this context

ほとんどの場合、インデントの問題です。キーの階層が正しくないか、コロンの後にスペースがありません。エラーに表示される行番号は、間違いの「付近」を指しています。上の行も確認してください。また、found character '\t' that cannot start any token はタブ文字が混入していることを意味します。YAML ではタブは禁止されています。実行前に必ず ansible-playbook site.yml --syntax-check を行い、エディタの YAML 設定をスペース2つ分のインデントにしてください。

/usr/bin/python3: not found. 標準的な Ubuntu 24.04 イメージでは稀ですが、minimal または netboot イメージでは一般的です。ターゲット側に Python がないため、モジュールの実行に失敗します。raw モジュールを使用して Python をインストールしてください。これはリモート側に何も必要としない唯一のモジュールです。ansible all -m raw -a "apt-get update && apt-get install -y python3" --become を実行した後、Playbook を再実行してください。

FAQ

管理対象のサーバーに Ansible をインストールする必要がありますか?

不要です。Ansible は agentless です。コントロールマシンが SSH 経由で小さな Python モジュールを転送・実行し、終了後に削除します。ターゲットには python3 と SSH アクセス権限のみが必要です。これらは Ubuntu の標準イメージに既に含まれています。このガイドでインストールが必要なのは、コントロールマシンのみです。

なぜ Ansible は "Permission denied (publickey)" と表示するのですか?

UNREACHABLE! ブロックに Permission denied (publickey) が含まれている場合、Ansible が実行される前に SSH 認証に失敗しています。inventory 内の ansible_user が設定済みのユーザーと一致しているか、そのホストに対して ssh-copy-id が実行できるか、そして通常の ssh user@host でパスワードなしでログインできるかを確認してください。通常の ssh コマンドが解決されれば、Ansible も解決されます。両者は同じ転送手段を使用しているためです。

Ansible における idempotent(冪等性)とは何を意味しますか?

タスクは「実行すべきアクション」ではなく、「あるべき状態(例:このパッケージが存在する、この行がファイルにある)」を宣言します。既にその状態である場合、Ansible は何もせず、changed の代わりに ok を報告します。そのため、プレイブックを2回実行すると、2回目は changed=0 と表示されます。再実行はリスクのある再インストールではなく、安全な監査として機能します。

Ubuntu 24.04 に Ansible をインストールする場合、pip と pipx のどちらを使うべきですか?

pipx を使用してください。Ubuntu 24.04 ではシステム Python が外部管理対象としてマークされているため、設計上 pip install ansibleerror: externally-managed-environment で失敗します。pipx install --include-deps ansible を使用すると、Ansible は隔離された virtualenv に配置され、ansibleansible-playbook、およびその他のコマンドが PATH に適切に登録されます。

ansible と ansible-core パッケージの違いは何ですか?

ansible-core はエンジンと ansible.builtin モジュールのみを含むパッケージです。ansible パッケージは、コア機能に厳選された community collections をまとめたものです。これには、本ガイドで使用する ansible.posix (authorized_key モジュール) と community.general (ufw モジュール) が含まれます。まずはフルパッケージから開始してください。コアと厳選したコレクションのみに絞り込むのは、明確な理由がある場合のみにしてください。