SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-07

AnsibleでUbuntu 24.04 VPSを初期設定する方法

pipxでAnsibleを導入し、inventoryと最初のplaybookを作成します。SSH強化、fail2ban、unattended upgrades、ファイアウォールを設定し、Permission deniedやsudoエラーの対処も解説します。

構築するもの

Ansible をインストールした 1 台の操作用マシンと、標準イメージ以外には何も入っていない 1 台以上の Ubuntu 24.04 VPS を用意します。最後には、サーバー名を記載したインベントリファイル、認証が最初から最後まで機能することを確認するアドホック ping、そして新しい VPS の初期設定チェックリスト全体をコードとして実行する playbook が完成します。playbook では、SSH key を設定した deploy ユーザー、強化した sshd、fail2ban、unattended upgrades、さらに OpenSSH を許可してから、それ以外の通信をすべて拒否するファイアウォールを構成します。1 台でも 20 台でも対象にできます。2 回実行しても、2 回目は何も変更されません。これが目的です。

15 年間 VPS をプロビジョニングしてきた経験から、率直なパターンを説明します。誰もが最初の 5 台は手作業で設定します。しかし 6 台目で週末を失います。最初の 5 台に何をしたのか、誰も覚えていないからです。このガイドでは、複数の Linux サーバーの管理についての説明をさらに深めます。同じ apt install を 3 つのターミナルに入力していることに気付いたときに、読み進めてください。

Ansible とは何かを 1 段落で説明します

Ansible はエージェントレスです。管理対象のサーバーにインストールするデーモンはありません。制御マシンが通常の SSH で接続し、小さな Python モジュールを対象ホストへコピーして実行し、モジュールが出力した JSON を読み取り、最後にそのモジュールを削除します。対象ホストに必要なのは python3 だけで、標準の Ubuntu イメージにはすでに含まれています。重要なのは 冪等性 です。これは単純な意味で、タスクがアクションではなく 状態 を記述するということです。パッケージに対する state: present は「インストーラーを実行する」ではなく、「これがインストール済みであることを保証する」という意味です。その状態がすでに満たされていれば、Ansible は何も変更せず、ok として報告します。changed とは報告しません。この性質こそが製品全体の中核であり、playbook を安全に再実行できる理由です。そして安全な再実行によって、シェルスクリプトがインフラストラクチャになります。

前提条件と、最初に把握しておくべき注意点

  • 管理用マシン: ノート PC または小規模な VPS を用意します。ここでは Ubuntu 24.04 を前提とします。macOS でも Homebrew から pipx をインストールすれば同じように動作します。
  • Ubuntu 24.04 を KVM 上で実行し、root で接続できる対象 VPS を 1 台以上用意します。対象 VPS には何もインストールしません。
  • すべての対象で SSH の暗号鍵認証を使用します。Ansible は ssh コマンドと同じ認証状態で実行されます。ssh root@host がパスワードを要求する場合、Ansible は失敗します。
  • Ubuntu 24.04 では、pip install ansibleerror: externally-managed-environment で終了します。これは意図されたディストリビューションの方針であり、故障ではありません。pipx を使用します。
  • YAML では空白が構文になります。インデントを誤ると mapping values are not allowed in this context が発生し、タブ文字が 1 文字でも含まれていると処理は失敗します。
  • playbook で sshd を強化している間は、各対象への SSH 接続を 1 つ維持してください。私が復旧を支援したロックアウトでは、すべて「クリーンな環境から確認する」ために最後のセッションを閉じていました。

ステップ 1: control machine に Ansible を pipx でインストールする(pip は使用しない)

よくある発想は pip3 install ansible です。完全に新しい 24.04 のイメージでは、1 つ前の手順で失敗します。Command 'pip3' not found, but can be installed with: sudo apt install python3-pip です。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 を外部管理対象(PEP 668)として扱います。そのため、pip は同じファイルをめぐって apt と競合できません。--break-system-packages に頼らないでください。このフラグの名前は文字どおりの意味です。適切な方法は pipx です。pipx は Ansible 用の分離された virtualenv を作成し、バイナリを PATH に追加します。

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

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

ansible --version

正しい結果は ansible [core 2.19.x] のような行で始まり、実行に使用する Python を示します。ここで使用する限り、現在の core release であれば問題ありません。ansible: command not found は、~/.local/bin がまだ PATH にないことを意味します。新しい shell を開くか、source ~/.bashrc を実行してください。

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

手順 2: すべての対象への SSH key アクセス

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

次に、ホストごとに 1 回ずつ実行して確認します。

ssh root@10.0.0.10 true && echo ok

この 1 行には 2 つの役割があります。パスワードなしで key 認証が機能することを確認し、ホスト key を known_hosts に記録します。今の段階で実行してください。Ansible は未記録のホスト key を実行途中の対話プロンプトとして表示するため、処理が停止したように見えるからです。

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 にそのファイル名を指定する方法は、各ホストに複数の変数を持たせるようになった場合に適しています。

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

両者は同等です。サーバーが 2 台なら INI のほうが目視で確認しやすく、20 台なら YAML のほうが拡張しやすくなります。どちらかを選び、以後は形式の選択に悩まないでください。

ステップ 4: ad-hoc コマンドで、すべてが正常であることを示す緑色の pong

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! は、モジュールが実行される前にトランスポートが失敗したことを示します。正確なエラー文字列と修正方法は、下記の「失敗モード」セクションに記載されています。知っておくと便利な ad-hoc コマンドがあと 2 つあります。

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

ad-hoc は、1 回限りの操作や確認に使用します。2 回実行する操作は、playbook に記述してください。

手順 5: 最初の playbook、新しい VPS のチェックリストをコード化する

これは、新しいサーバーで最初の 10 分間に手作業で行う内容です。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

コピーするだけでなく、理解しておくべき行を説明します。

変数vars: の下に定義し、"{{ deploy_user }}" で参照します。値が波括弧で始まる場合は式全体を引用符で囲んでください。囲まないと YAML パーサーが誤って解釈します。lookup('file', ...) は実行時に control マシンから公開鍵を読み取るため、playbook に鍵の内容は含まれません。

ループloop: "{{ baseline_services }}" は項目ごとにサービスのタスクを 1 回実行し、出力にも各項目が 1 行ずつ表示されます。一方、apt タスクはパッケージ一覧全体をまとめて処理します。パッケージでは 1 回の apt トランザクションの方が高速で、推奨される方法です。ループは、実際に 1 回に 1 つの対象を処理するモジュールに使用します。

handler は理解しておくべき概念です。notify: Restart ssh は「今すぐ ssh を再起動する」という意味ではありません。handler をキューに追加し、play の終了時に 1 回だけ実行します。また、通知元のタスクが実際に 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 true には Ansible だけが認識するエイリアスである web1 ではなく、通常のアドレスを指定します)、inventory の ansible_user=deploy を切り替え、後続の実行で no に厳格化します。接続不能にならない順序で強化してください。

00- プレフィックスが重要です。 sshd は多くのキーワードについて、解析時に最初に出現した設定を適用します。Ubuntu の sshd_config は、独自の本体より前に sshd_config.d/*.conf を辞書順で読み込みます。Ubuntu 24.04 の cloud image には、すでにそのディレクトリに 60-cloudimg-settings.conf が含まれています。また、cloud-init でパスワードログインを有効にするプロバイダーは 50-cloud-init.conf を追加し、そこに PasswordAuthentication yes を設定します。作成するファイルを 00-hardening.conf と命名すると、辞書順で最初に読み込まれ、両方の設定より優先されます。

タスクの順序がファイアウォールの安全性を左右します。 Allow OpenSSH は拒否ポリシーを設定する Enable ufw より前に実行されます。Ansible は記述された順序どおりにタスクを実行するため、壁が有効になる前に通信を許可する穴が存在します。ここでは fail2ban に追加設定は必要ありません。Ubuntu のデフォルト設定で sshd を直ちに監視します。jail の実際の動作と調整方法については、Ubuntu 24.04 での fail2ban ガイドで説明します。

手順 6: --check でドライランを実行し、その後に本番実行する

ansible-playbook site.yml --check

Check モードは接続して実行予定の内容を計算しますが、変更は行いません。下部の PLAY RECAP に表示される changed= の数を確認してください。これは、各ホストを変更するタスク数です。1 つ注意点があります。後続のタスクが先行タスクの変更に依存する場合、Check モードには構造上の制限があります。Ubuntu の標準サーバーイメージには ufw があらかじめ含まれているため、この playbook のドライランは正常に完了します。しかし、ufw がない最小構成のイメージでは、Check モードで ufw のタスクが失敗します。Check モードはパッケージを実際にはインストールしないため、モジュールが呼び出す対象が存在しないからです。これはドライランの制限であり、playbook のバグではありません。計画に問題がなければ、次を実行します。

ansible-playbook site.yml

各タスクはホストごとに 1 行を出力します。黄色が changed、緑色が ok です。最後の recap は次のようになります。

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 個のタスク、ハンドラーの合計です。changed は、私の結果と 1 または 2 異なっていても問題ありません。Ubuntu の標準イメージには ufwunattended-upgrades があらかじめ含まれており、fail2ban は apt によるインストール直後に自動起動するためです。そのため、初回実行でも、タスクが宣言した状態をすでに満たしているとして ok と報告することがあります。0 でなければならない数値は unreachablefailed です。become: true について 1 点補足します。root として接続している間は形式的な設定ですが、ansible_userdeploy に切り替えた時点で sudo が実際に必要になります。この playbook がインストールする NOPASSWD sudoers ファイルによって、-K をコマンドラインに入力せずに済みます。このファイルがない場合は Missing sudo password が発生します。詳細は以下で説明します。

手順 7: 2 回実行し、べき等性を確認する

同じコマンドをすぐにもう一度実行します。

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

changed=0ok が 1 つずつ減ったのは、通知されなかったハンドラーが実行されなかったためです。再インストールは行われず、sshd も再起動されず、ufw にも変更はありませんでした。これにより、この playbook はプロビジョニングだけでなく、監査 も行えます。来月インベントリに web3 を追加して再実行すれば、新しいホストは構築され、既存のホストは検証されます。変更を加えていないホストで changed が 0 以外になる場合、それはドリフトです。playbook で変更すべき内容を誰かが手作業で編集したことを示します。

ここから、このパターンを拡張していきます。次に作成する価値がある playbook は、同じ VPS に WireGuard VPN を構築し、SSH がトンネル経由でのみ応答するよう ufw のルールを厳格化するものです。その次は、すべてのアプリケーションサーバーに Docker と Compose をインストールする playbook です。site.yml が 3 画面を超えるようになったら、roles に分割します。ただし、それまでは分割しません。

エラー時に表示されるメッセージ

Permission denied による UNREACHABLE。

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 が誤っている、対象ホストに鍵をコピーしていない、または誤った鍵を提示している可能性があります。通常の ssh root@10.0.0.10 で再現し、ssh -v で提示された鍵を確認します。パスワードによる SSH 接続は成功するのに Ansible が失敗する場合は、ssh-copy-id を指定していません。

sudo パスワードがありません。

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

become: true を設定して非 root ユーザーで接続しており、そのユーザーが sudo にパスワードを要求されています。コマンドラインに -K--ask-become-pass)を追加するか、ユーザーに NOPASSWD の sudoers エントリを付与します。プレイブックが deploy に切り替える前に、そのエントリをインストールするのはこのためです。

error: externally-managed-environment。 Ubuntu 24.04 のシステム Python に対して pip を実行しています。手順 1 で説明したとおり、pip ではなく pipx を使用し、--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 イメージではまれですが、最小構成や netboot のイメージではよく発生します。対象ホストに Python がないため、モジュールの実行に失敗しています。raw モジュールでインストールします。リモート側で何も必要としない唯一のモジュールです。ansible all -m raw -a "apt-get update && apt-get install -y python3" --become を実行してから、プレイブックを再実行します。

FAQ

Ansible が管理するサーバーにも Ansible のインストールが必要ですか?

いいえ。Ansible はエージェントレスです。制御マシンが小さな Python モジュールを SSH 経由で転送して実行し、その後削除します。対象サーバーに必要なのは python3 と SSH 接続だけで、どちらも標準の Ubuntu イメージに含まれています。このガイド全体でインストールが必要なのは、制御マシンだけです。

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

UNREACHABLE! ブロックに Permission denied (publickey) が含まれている場合、Ansible が何も実行する前に SSH 認証が失敗しています。インベントリの ansible_user が実際に設定したアカウントと一致すること、そのホストに対して ssh-copy-id を実行したこと、通常の ssh user@host でパスワードなしにログインできることを確認してください。通常の ssh コマンドで解決すれば、Ansible も解決します。どちらも同じトランスポートを使用するためです。

Ansible で idempotent とは何を意味しますか?

タスクでは、「このパッケージが存在する」「このファイルにこの行がある」のように、実行する操作ではなく目的の状態を宣言します。その状態がすでに満たされていれば、Ansible は何もせず、changed ではなく ok と報告します。そのため、playbook を 2 回実行すると、2 回目には changed=0 と表示されます。再実行は、危険な再インストールではなく安全な監査になります。

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

pipx を使ってください。Ubuntu 24.04 ではシステム Python が外部管理対象として扱われるため、pip install ansible は仕様上 error: externally-managed-environment で失敗します。pipx install --include-deps ansible は Ansible を分離された virtualenv に配置し、ansibleansible-playbook などを PATH に適切に追加します。

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

ansible-core は、ansible.builtin モジュールだけを含む実行エンジンです。ansible パッケージには core に加えて、厳選された community collections が含まれます。この中には、このガイドで使用する ansible.posixauthorized_key モジュール)と community.generalufw モジュール)も含まれます。まず完全なパッケージから始め、必要な理由がある場合だけ、core と選択した collections の構成に縮小してください。