Ubuntu ServerでCloud-initを使うガイド:初回起動時にシステム設定を自動化する

Ubuntu tutorial - IT technology blog
Ubuntu tutorial - IT technology blog

クラウドにインフラを構築し始めた最初の1ヶ月、新しいVPSを立ち上げるたびにチェックリストを開いて一つひとつ作業していました。パッケージの更新、ユーザー作成、SSH設定、基本ツールのインストール…毎回15〜20分かかり、それでも手順を抜かしてしまうこともありました。cloud-initをプロダクションで6ヶ月使ってからは、もう以前のやり方には戻れません。

新しいサーバーの設定方法3選:実践的な比較

試した3つの方法、どれにも存在する理由があります:

方法1:手動で一つひとつ設定する

新しいサーバーにSSHで入り、チェックリストに沿ってコマンドを一つずつ実行します。誰もが一度はやった方法です。柔軟性は抜群ですが、スケールしません。10台追加すれば午前中が丸々潰れます。しかもチェックリストはすぐに古くなります。

方法2:ログイン後にbashスクリプトを実行する

setup.shというファイルを作成し、SSHで接続して実行します。手動よりずっと優れています。再現性があり、バージョン管理もできます。ただし、誰かが実行する必要があります。idempotencyチェックがない状態でスクリプトが途中で失敗した場合、再実行するとエラーが発生しやすくなります。

方法3:cloud-init user-data

VM/VPS作成時に設定を渡します。サーバーは初回起動時に自動で設定されるため、誰かがSSHで入る必要がありません。主要なクラウドプロバイダー(AWS、GCP、Azure、DigitalOcean、Vultr、Hetzner)はほぼすべてサポートしています。

メリット・デメリットの分析

bashスクリプト:優れているが不十分

複雑なタスクではcloud-initと並行してbashスクリプトを使うこともあります。ただし、純粋なbashスクリプトにはいくつかの弱点があります:

  • 起動完了後に実行:ネットワークとSSHが必要で、手動ステップが増えるかパイプラインが複雑になります。
  • モジュールシステムがない:ユーザー作成、SSHキー設定、ファイル書き込みなど、すべて自分で実装する必要があります。
  • Idempotencyは自前で実装if [ ! -f /etc/myapp/configured ]; thenのようなチェックをあちこちに書くのでスクリプトが煩雑になります。

cloud-init:先に実行、より一貫性がある

cloud-initはSSHでログインできるよりも前の、早期ブート段階で実行されます。つまり、初めて接続したときにはすでにサーバーの設定が完了しています。インストールやアップグレードするパッケージ数によりますが、通常3〜8分で完了します。組み込みモジュールがユーザー、SSHキー、パッケージ、ファイル、コマンドを処理してくれるので、初期プロビジョニングの90%のニーズを満たせます。

実際のデメリット:bashスクリプトよりデバッグが難しいことです。エラーが発生したとき、どこのログを見ればよいかを知っておく必要があります。YAMLのインデントがほんの少しずれると設定全体が動かなくなりますが、明確なエラーメッセージが出ません。

bashスクリプトの代わりにcloud-initを選ぶべき場面

cloud-initを選ぶのは以下のような場面が多いです:

  • クラウドプロバイダーで新しいVPSを作成するとき。インスタンス作成時にほぼ必ずuser-dataフィールドがあります。
  • サーバーを何度も再構築する必要があるとき(テスト環境、ステージング環境など)。
  • 複数のサーバーを同時に一貫した設定にしたいとき。
  • Infrastructure as Code:user-dataをTerraform/AnsibleのconfigとともにGitで管理したいとき。

多層の条件分岐が必要な複雑なセットアップや、プロバイダーがuser-dataをサポートしていない場合は、純粋なbashスクリプトの方が適しています。

cloud-initの実践デプロイガイド

user-dataの基本構文

user-dataファイルはYAMLで、#cloud-configで始まることが必須です。この行がないと、cloud-initはエラーを出さずに何もしないまま処理をスキップします。

#cloud-config

# 起動時にパッケージを更新する
package_update: true
package_upgrade: true

# インストールするパッケージ
packages:
  - curl
  - git
  - htop
  - ufw
  - fail2ban
  - unattended-upgrades

ユーザー作成とSSHキーの設定

ここが最も手間を省けるポイントです。rootでSSHして手動でユーザーを作成したり、SSHキーを一つひとつコピーしたりする必要がなくなります。

#cloud-config

users:
  - name: deploy
    groups: sudo
    shell: /bin/bash
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your-public-key

# パスワードによるSSHログインを無効にし、キー認証のみ使用する
ssh_pwauth: false

# Disable root login
disable_root: true

ファイルの書き込みとコマンドの実行

write_filesruncmdを組み合わせると、bashスクリプトのsetupで行う処理の大部分を置き換えられます。しかも、ずっとシンプルに書けます:

#cloud-config

write_files:
  - path: /etc/sysctl.d/99-custom.conf
    content: |
      net.ipv4.tcp_syncookies = 1
      net.ipv4.conf.all.rp_filter = 1
      net.ipv6.conf.all.disable_ipv6 = 0
    permissions: '0644'

  - path: /etc/motd
    content: |
      ==========================================
       Production Server — Authorized access only
      ==========================================
    permissions: '0644'

runcmd:
  # ファイアウォールの設定
  - ufw default deny incoming
  - ufw default allow outgoing
  - ufw allow 22/tcp
  - ufw allow 80/tcp
  - ufw allow 443/tcp
  - ufw --force enable

  # sysctlを適用する
  - sysctl --system

  # Timezone
  - timedatectl set-timezone Asia/Tokyo

Webサーバー向け完全なuser-data例

新しいNginx VPSに使っている設定です。ここまでの内容をすべて組み合わせたもので、そのまま貼り付けてすぐ使えます:

#cloud-config

package_update: true
package_upgrade: true

packages:
  - curl
  - git
  - nginx
  - certbot
  - python3-certbot-nginx
  - ufw
  - fail2ban
  - htop
  - unattended-upgrades

users:
  - name: deploy
    groups: sudo, www-data
    shell: /bin/bash
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your-key-here

ssh_pwauth: false
disable_root: true

write_files:
  - path: /etc/fail2ban/jail.local
    content: |
      [DEFAULT]
      bantime = 3600
      findtime = 600
      maxretry = 3

      [sshd]
      enabled = true
    permissions: '0644'

runcmd:
  - timedatectl set-timezone Asia/Tokyo
  - ufw default deny incoming
  - ufw default allow outgoing
  - ufw allow 22/tcp
  - ufw allow 80/tcp
  - ufw allow 443/tcp
  - ufw --force enable
  - systemctl enable nginx
  - systemctl start nginx
  - systemctl enable fail2ban
  - systemctl start fail2ban

# アップグレード完了後に新しいカーネルを適用するために再起動する(ある場合)
power_state:
  mode: reboot
  condition: true

VPS作成時にuser-dataを渡す方法

プロバイダーによって方法が少し異なります:

Hetzner Cloud(hcloud CLI):

hcloud server create \
  --name my-webserver \
  --type cx21 \
  --image ubuntu-24.04 \
  --user-data-from-file cloud-init.yaml

DigitalOcean(doctl CLI):

doctl compute droplet create my-webserver \
  --image ubuntu-24-04-x64 \
  --size s-1vcpu-1gb \
  --region sgp1 \
  --user-data-file cloud-init.yaml

CLIがない場合は、Web UIの「User Data」フィールドに直接貼り付けてください。Hetzner、DigitalOcean、Vultrはどれも、インスタンス作成時にこの入力欄があります。

エラー時のログ確認方法

cloud-initが正常に動かない?順番に確認すべき3つの場所があります:

# 全体ログの確認
sudo cat /var/log/cloud-init.log

# runcmdコマンドの出力確認
sudo cat /var/log/cloud-init-output.log

# 全体ステータスの確認
sudo cloud-init status --long

# 設定ファイルのパースが正しいか確認
sudo cloud-init schema --config-file /path/to/cloud-init.yaml

schemaコマンドは実際のサーバーを作成する前に使うようにしています。一度YAMLのミスでVPSを削除して作り直す羽目になってから、事前にバリデーションする習慣がつきました。

実際の経験から得た注意点

CentOSからUbuntuに移行した当初、慣れるのに1週間ほどかかりました。CentOSにもcloud-initはありますが、モジュール名や動作が一部異なります。Ubuntuではaptモジュールがデフォルトで、非常にスムーズに動作します。

よくつまずくポイント:

  • 実行順序packagesruncmdより先に実行されます。packagesで宣言済みのパッケージはruncmd内でapt-get installする必要はありません。
  • YAMLのインデント:スペース2つを使い、タブは使わないこと。一度write_filesでタブを使ったら、ブロック全体が動かなくなりましたが明確なエラーが出ず、デバッグに1時間近くかかりました。
  • runcmdは一度だけ実行:cloud-initは再起動時に再実行されません(state filesを削除しない限り)。これはプロビジョニングとして正しい動作です。
  • 事前にローカルでテスト:Multipass(無料、macOSとLinux対応)を使えば、user-dataを指定してUbuntu VMを作れます。数分でテスト環境が整い、VPS代もかかりません。
# Multipassで素早くテストする
multipass launch --name test-vm --cloud-init cloud-init.yaml ubuntu:24.04

# SSH接続して確認
multipass shell test-vm

# テスト完了後に削除
multipass delete test-vm && multipass purge

cloud-initは万能ではありません。複雑な条件分岐が多いセットアップにはAnsibleの方が適しています。ただし、数十台のサーバーの初期プロビジョニングであれば、user-data YAMLは十分シンプルかつ強力です。現在はcloud-initファイルをTerraform configと一緒にGitで管理しています。環境を再構築するたびに、コマンド一つで完了します。

Share: