この記事は、 HashiCorp Japan Advent Calendar 2025 の 5 日目の投稿です。
はじめに:その apt-get update、いつまで待ちます?
ところで皆様、あなたのそのデプロイフロー、 「お祈り」 が含まれていませんか?(ドーン)
Auto Scaling でインスタンスが立ち上がるたび、あるいはデプロイのたびに走る Ansible の Playbook。
「外部リポジトリが落ちてたらどうしよう」
「依存パッケージのバージョンが勝手に上がってたらどうしよう」
そんな不安を抱えながら、黒い画面に流れる CHANGED のログを眺める時間は、人生の無駄遣いです。
「Ansible のそのコード、Packer で焼いちゃえばよくない?」
今日は、構成管理のタイミングを「デプロイ時」から「ビルド時」にずらすだけで、人類がいかに幸せになれるかについて語ります。
1. Ansible と Packer:役割は「何」かではなく「いつ」か
よくある誤解ですが、Ansible と Packer は敵対するものではありません。むしろ「最高のパートナー」です。
まず強調しておきたいのは、Ansible というツールの素晴らしさです。サーバーの中身を定義する「構成管理(Configuration Management)」において、Ansible は依然として最強のツールの一つです。
-
人間が読める「宣言的」なコード:
複雑な設定も YAML で書けるため、泥臭いシェルスクリプト(sedやawkの嵐)と違って、誰が見ても「何をしているか」が一目瞭然です。 -
巨人の肩に乗れる「モジュール群」:
user,yum,service,templateなど、OS の差異を吸収してくれるモジュールが豊富にあり、車輪の再発明を防げます。 -
資産の再利用性:
Role という単位で機能がモジュール化されており、他のプロジェクトでも使い回せます。
「Ansible は最高です。 ただし、それを『いつ』実行するかが問題なのです。 」
現代のクラウド運用において、「本番環境でサーバーが起動してから ansible-playbook を流す」 という運用(Runtime Configuration)は、アンチパターンになりつつあります。
Runtime Configuration (従来の Ansible 運用):
- サーバー起動後に
yum installやgit cloneをする。 - リスク: 起動が遅い。外部要因(リポジトリダウン等)でコケる。サーバーごとに微妙に状態がズレる (Configuration Drift)。
Golden Image (Packer 運用):
- Ansible の記述力でサーバーの中身を完璧に作り込み、それを「マシンイメージ(AMI など)」として固めておく。
- 起動時はイメージを展開するだけ。
- メリット: 爆速起動。100% の再現性。イミュータブル(不変)。
つまり、 「Ansible を捨てる」のではなく、「Ansible の強力な表現力を Packer の中で活かし、成果物だけを本番に持っていく」 のが正解です。
2. 実践:その「汚れちまった起動スクリプト」を供養する
「Packer 化する」といっても、Ansible の Playbook を全部捨てて HCL (HashiCorp Configuration Language) に書き直すわけではありません。
「Ansible を起動するためだけの、メンテナンス不可能な User Data (シェルスクリプト)」を捨てる のです。
具体的なビフォーアフターを見てみましょう。
Before:本番で震えながら走る User Data
EC2 の User Data や、起動テンプレートにこんなシェルスクリプト書いてませんか? これが「アンチパターン」の正体です。
#!/bin/bash
# ----------------------------------------------
# 恐怖のランタイムセットアップ
# ----------------------------------------------
# 1. そもそも Ansible を入れるところから始まる(遅い)
yum update -y
amazon-linux-extras install ansible2 -y
# 2. Playbook をどこかから落としてくる(権限周りでよく止まる)
# S3 や Git の認証情報が通るか、毎回ドキドキする瞬間
aws s3 cp s3://my-infra-bucket/playbooks/site.yml /tmp/site.yml
aws s3 cp s3://my-infra-bucket/roles /tmp/roles --recursive
# 3. 実行(ここでコケたら、このインスタンスは無用の長物に・・・)
ansible-playbook /tmp/site.yml -e "env=production" >> /var/log/ansible.log
# 4. ログ監視エージェントなどの再起動(Ansible でやったのに念のため再起動とか・・・)
systemctl restart amazon-cloudwatch-agent
このコードの罪深い点は、「インスタンスが起動するたびに、インターネットへの接続、リポジトリの生存、パッケージのバージョン整合性、全てが成功しないといけない」 という点です。悔い改める必要がありそうですね。
After:平穏をもたらす Packer 定義
これを Packer に書き換えると、こうなります。Ansible Playbook そのものは そのまま 使えます。
# build.pkr.hcl
build {
sources = ["source.amazon-ebs.app"]
# ステップ 1: Ansible 自体のインストール
# 「Ansible を実行するための環境」はシェルスクリプトでサクッと作る
provisioner "shell" {
inline = [
"sudo amazon-linux-extras install ansible2 -y"
]
}
# ステップ 2: Ansible の実行
# ローカルにある Playbook をインスタンスに転送して実行してくれる
# S3 から落とす必要も、Git clone する必要もない!
provisioner "ansible-local" {
playbook_file = "./ansible/site.yml"
role_paths = ["./ansible/roles"]
# 変数もここで注入。ビルド時に確定させる。
extra_arguments = [
"--extra-vars", "env=production build_id=${var.build_id}"
]
}
}
どうですか? 「ファイルをダウンロードする処理」が消えました。
Packer がローカル( CI 環境など )にある Playbook を一時的にインスタンスへアップロードし、実行してくれるからです。便利ですね!
Tips:Ansible の「過剰品質」を Shell で削ぎ落とす
さらに、Packer 化のついでに 「たったこれだけのために Ansible 書いてたの?」 という部分を、Packer の機能に書き換えて断捨離しましょう。
例 1:ファイル配置だけの Task のために Ansible を呼び出す必要はない
-
Ansible (Before):
copyモジュールを使うためだけに Task 定義...
- name: Config file copy
copy:
src: files/config.toml
dest: /etc/myapp/config.toml
-
Packer (After):
fileプロビジョナーとして組み込み統一感を出す
provisioner "file" {
source = "./files/config.toml"
destination = "/tmp/config.toml" # 一旦 tmp に置いて
}
provisioner "shell" {
inline = ["sudo mv /tmp/config.toml /etc/myapp/"] # sudo で移動
}
例 2:単純なコマンド実行で冪等性を考慮してあれこれ考える必要はない
-
Ansible (Before):
shellモジュールで実行。冪等性担保のためにcreatesとか書き出すと面倒。 -
Packer (After): Packer は「一度ビルドしたら終わり(次回実行時はまた更地から)」なので、冪等性を気にする必要がありません。 ガンガン
shellプロビジョナーで書いて OK です。
provisioner "shell" {
inline = [
"curl -sL https://rpm.nodesource.com/setup_16.x | sudo bash -",
"sudo yum install -y nodejs"
]
}
「Ansible の冪等性」は神機能ですが、Golden Image ビルドにおいては 「毎回まっさらな OS で 1 回だけ実行される」 ので、実はシェルスクリプトで十分なケースが多いのです。これに気づくと、Ansible コードはもっとシンプルになります。
3. シフトレフトの実装:エラーは「今」見つける
「シフトレフト」とは、せっかくなのでかっこいい言葉を使いたいだけ難しい概念ではありません。 「絶望するタイミングを前倒しする」 ということです。
具体的な恩恵:外部リポジトリダウンの悲劇
Ansible 実行型:
- 深夜 2 時のオートスケール時、
npm install先のリポジトリが応答なし。 - 結果: 新規インスタンスが起動失敗。サービスダウン。あなたは電話で叩き起こされる。[例の音声]:"Hello, this is P***y. You have ONE triggered incident on service 'Production-API'. Press 4 to acknowledge..."
Packer ビルド型 (シフトレフト):
- 昼間の CI パイプライン(GitHub Actions 等)での AMI ビルド中、リポジトリが応答なし。
- 結果: ビルドが失敗して赤くなるだけ。本番環境は**「最後に成功した正常な AMI」**で稼働し続ける。
- あなたの対応: 「あ、ビルドこけたわ。後で見よ」と言って優雅にコーヒーを飲むひととき。
CI への組み込みでシフトレフトを実現する
それこそ単純な話ですが、GitHub Actions で Packer を回しましょう。
- Pull Request 作成
-
packer validate&ansible-lint実行 - マージ後、
packer build実行 → AMI 作成 - Terraform などが新しい AMI ID を参照してデプロイ
これで、インフラのバグはデプロイ前(ビルドフェーズ)に検知されます。これが本当のシフトレフトです。
リポジトリ内の、 .git/workflows にいれる packer.yml のサンプルはこちらです。
ポイントは、PR の段階では重たい build (AMI 作成) を走らせないこと です。これにより、開発者は安心してコードをプッシュでき、CI の完了を待つ時間も短縮されます。
Sample の yml はこちら
name: Packer Build Pipeline
on:
push:
branches:
- main # マージされたら焼く
pull_request:
branches:
- main # PR 中は構文チェックのみ
jobs:
# ---------------------------------------------------
# 1. 検査フェーズ (Shift Left)
# PR のたびに走り、構文エラーやベストプラクティス違反を即検知する
# ---------------------------------------------------
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Ansible (for linting)
run: pip install ansible ansible-lint
- name: Setup Packer
uses: hashicorp/setup-packer@main
with:
version: "latest"
# Ansible の構文チェック
- name: Run ansible-lint
run: ansible-lint ./ansible/site.yml
# Packer の構文チェック
- name: Run packer init & validate
run: |
packer init .
packer validate .
# ---------------------------------------------------
# 2. ビルドフェーズ (Golden Image 生成)
# main ブランチにマージされた時だけ実行される
# ---------------------------------------------------
build:
needs: validate
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
id-token: write # AWS OIDC 認証用
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
# AWS 認証 (OIDC 推奨だが、Secrets でも可)
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-1
role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}
# または単純に Access Key を使う場合:
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Setup Packer
uses: hashicorp/setup-packer@main
with:
version: "latest"
- name: Run packer init
run: packer init .
# いざ、焼く!
- name: Build AMI
run: packer build .
Sample コードのポイントをいくつかご紹介します。
1.ansible-lint の導入:
- 「Packer は通ったけど、中で動く Ansible がインデントミスで死んだ」という悲劇を防ぐために必須です。
2.needs: validate:
- 構文チェックが通らない限り、ビルドフェーズには進ませません。無駄な AWS リソースの浪費を防ぎます。
3.if: github.event_name == 'push':
- ここがキモです。PR の段階で毎回 AMI を作っていたら、ビルド待ち時間で開発が止まってしまいます。「検査は早く、ビルドは慎重に」のメリハリをつけます。
4. Golden Image 運用へ切り替える時の「アプリ実装」の注意点
さて、ここまではインフラの話ですが、アプリアーキテクティングにも釘を刺しておく必要があります。
Golden Image 運用にするということは、「サーバーは使い捨て (Immutable)」 になるということです。
以下のコードが含まれているアプリケーションは、Packer 化と同時にアーキテクチャ(というか、考え方自体)の修正が必要です。
❌ 1. ログをローカルファイルに出力して満足している
# ダメな例
logging.basicConfig(filename='/var/log/myapp/app.log')
サーバーが入れ替わるとログも消えます。
対策: CloudWatch Logs, Datadog, Fluentd などを使い、ログは標準出力に吐くか、即座に外部へ転送してください。
❌ 2. アップロードファイルをローカルディスクに保存している
// ダメな例
move_uploaded_file($tmp, "/var/www/uploads/" . $filename);
次のデプロイでそのインスタンスが破棄された瞬間、ユーザーの画像は消滅します。
対策: S3 や GCS などのオブジェクトストレージに直接保存してください。
❌ 3. IP アドレスやホスト名に依存している
「DB の許可リストに Web サーバーの IP 書いたから OK」
対策: Auto Scaling で IP はコロコロ変わります。Security Group や IAM Role ベースの認証に切り替えてください。
まとめ:安眠のために焼こう
Ansible は素晴らしいツールです。記述性が高く、冪等性も担保してくれます。
しかし、それを 「いつ実行するか」 で、運用の質は天と地ほど変わります。
- 構成管理のコードはそのまま資産として使う。
- 実行場所を「本番サーバーの起動時」から「CI 上の Packer ビルド時」に移す。
- アプリをステートレスにする。
これだけで、デプロイ時間は短縮され、障害耐性は上がり、何より 「デプロイボタンを押す時の手の震え」 が止まります。
さあ、その user_data を削除して、.pkr.hcl を書き始めましょう。