AMIの役割と構築タイミングを整理する
はじめに
EC2インスタンスを起動するとき、AMIを選ぶ必要があります。Amazon LinuxやUbuntuのAMIを選んで起動すれば、OSが入った状態のインスタンスが手に入る。
ただ、AMIにはOSだけでなく、ミドルウェアやアプリケーションまで含めることもできます。たとえば次のようなAMIが考えられます。
- OSだけを含むAMI
- OS+監視エージェント+ランタイムを含むAMI
- OS+ミドルウェア+アプリケーションまで含むAMI
しかも、ソフトウェアをEC2に入れる手段はAMIだけではありません。起動時にスクリプトで入れることもできるし、起動後に外部から配布することもできる。手段もタイミングも複数あるからこそ、「どこで何を入れるか」を整理する必要が出てきます。
この記事では、まずコンピューターが起動して動く仕組みを押さえたうえで、AMIが何を担っているのかを整理します。そのうえで、EC2の構成がいつ・何によって決まるのかを4つのタイミングに分類し、厚いAMIと薄いAMIの違いを見ていきます。具体的にどこまで焼き込むべきかという判断基準は、次の記事で扱う予定です。
なお、この記事ではLinuxのEBS-backed AMIと、Nitroベースの仮想インスタンスを前提に説明します。
コンピューターを起動してアプリケーションを動かすには
AMIの話に入る前に、そもそもコンピューターを起動してアプリケーションを動かすまでに何が必要なのかを整理しておきます。EC2も基本的には同じ仕組みの上に成り立っているので、ここを押さえておくとAMIの役割がわかりやすくなります。
ディスクがないとコンピューターは起動できない
PCの電源を入れてWebサーバーを動かすまでには、ストレージ上にいくつかのデータが揃っている必要があります。ストレージはハードディスクやSSDが一般的ですが、USBメモリやネットワーク上のストレージから起動することもできます。ここでは話をわかりやすくするために「ディスク」と呼ぶことにします。
まず起動の流れから見てみます。物理的なコンピューターの起動は、おおまかに以下のステップで進みます。
- ファームウェア(BIOS/UEFI)が起動する — マザーボードに組み込まれたファームウェアがまず動き、CPU・メモリ・ディスクなどのハードウェアを初期化する
- ファームウェアがブートローダーを読み込む — 設定された起動デバイスから、OSを起動するためのプログラム(GRUBなどのブートローダー)を読み込む
- ブートローダーがカーネルを読み込む — ブートローダーが、ディスク上にあるOSのカーネルをメモリにロードする
- カーネルがOSを起動する — カーネルがハードウェアを認識し、ディスク上のファイルシステムをマウントし、init/systemdを起動する。ここから各種サービスが立ち上がる
つまりディスクには、ブートローダー、カーネル、OS本体が保存されている必要があります。さらに、Webサーバーとしてアプリケーションを動かすなら、Nginxのような実行ファイルや設定ファイルもディスク上に必要です。
ディスク上のデータを整理する
開発中に作ったファイルやDBのデータがディスクに保存されるのは経験的にわかると思います。でも実は、ブートローダーやカーネルといった起動に必要なプログラムも、同じディスク上に置かれている。整理すると、こうなります。
| データ | 説明 | 例 |
|---|---|---|
| ブートローダー | OSの起動を開始するプログラム | GRUB |
| カーネル | OSの核。ハードウェアの制御やプロセス管理を行う | /boot/vmlinuz-* |
| OS本体 | コマンドや共有ライブラリ | /bin, /usr |
| OS設定 | 設定ファイル群 | /etc |
| インストールしたソフトウェア | ミドルウェアやエージェント | Nginx, Java, CloudWatch Agent |
| ログ | 動作中に出力される記録 | /var/log |
| DBのデータファイル | アプリケーションが管理するデータ | /var/lib/mysql |
| キャッシュ・一時ファイル | 一時的に使われるデータ | /tmp, /var/cache |
| ユーザーが作ったファイル | ユーザーごとのデータ | /home |
EC2を起動してアプリケーションを動かすには
ここまでは物理的なコンピューターの話でしたが、EC2でも基本的な仕組みは変わりません。
EC2でも同じ仕組みが動いている
現在の多くのEC2インスタンスは、AWSが管理する物理サーバー上でNitro Systemという基盤の上で動いているようです。公式ドキュメントによると、通常の仮想インスタンスではNitro HypervisorがCPUやメモリの分離を担い、1台の物理サーバーに複数のインスタンスが載る形になっているとのこと。
物理サーバー(AWS管理)
↓
Nitro System / Nitro Hypervisor
↓
仮想EC2インスタンスA 仮想EC2インスタンスB ...
(それぞれ独自のOS・カーネルを持つ)
仮想マシンではありますが、起動の仕組みは物理マシンと基本的に同じです。AWSの公式ドキュメントには、EC2のHVM(Hardware Virtual Machine)について「完全に仮想化されたハードウェア一式が提示され、ルートブロックデバイスのマスターブートレコードを実行してブートする」と記載されています。
つまりEC2でも、ディスク(EBSボリューム)上にブートローダー、カーネル、OS本体が必要であり、ファームウェア → ブートローダー → カーネルという順で起動する点は変わりません。
物理マシンの場合 EC2の場合
────────── ──────────
物理ハードウェア Nitroが提供する仮想ハードウェア
↓ ↓
BIOS/UEFI 仮想ファームウェア(BIOS/UEFI)
↓ ↓
物理ディスクを読む EBSボリュームを読む
↓ ↓
ブートローダー → カーネル ブートローダー → カーネル
↓ ↓
OS起動 OS起動
違いは、ハードウェアもディスクも仮想化されているという点です。EBS-backed AMIから起動したインスタンスでは、ルートボリュームとしてEBSボリュームが作成されます。EBSはネットワーク越しのブロックストレージですが、ゲストOSからはローカルディスクのように見えています。
AMIは「起動前のディスクの中身」を定義するもの
ここまでの整理を踏まえると、EC2を起動してアプリケーションを動かすためには、ディスク(EBSボリューム)にブートローダー、カーネル、OS、そして必要なソフトウェアが入っている必要があります。
AMIが担うのは、この「起動前のディスクの中身」をあらかじめ定義しておく役割です。AMIからインスタンスを起動すると、AMIに記録されたディスクの内容がEBSボリュームとして復元され、仮想マシンがそのボリュームからOSを起動します。
どこまでをAMIに含めておくか、つまりOSだけにするのか、ミドルウェアやアプリケーションまで入れるのか。これが「焼き込みの範囲」の問題であり、次の記事で掘り下げるテーマです。
AMIの構造をもう少し詳しく見る
前節で、AMIは「起動前のディスクの中身を定義するもの」だと整理しました。ここではAMIが内部的にどういう構造になっているかを見ていきます。
AMI(Amazon Machine Image)そのものがディスクの中身を丸ごと抱えているわけではありません。EBS-backed AMIの場合、その実体はEBSスナップショットへの参照と、それをどう使うかを定義するメタデータの組み合わせです。
- EBSスナップショットへの参照 — ルートボリュームや追加ボリュームの中身(OS、ブートローダー、パッケージ等)は、EBSスナップショットとして保存されている
- Block Device Mapping — どのスナップショットをどのデバイス名(
/dev/xvda等)にマッピングするか、ボリュームのサイズやタイプ、削除ポリシーなどの定義 - 起動許可 — どのAWSアカウントがこのAMIを使ってインスタンスを起動できるか
- アーキテクチャ・ブートモード — x86_64かarm64か、UEFIかLegacy BIOSか
AMIからインスタンスを起動するとき、裏では以下のことが起きています。
- AMIが参照するEBSスナップショットから、新しいEBSボリュームが作られる
- Nitroが仮想マシンを割り当て、そのEBSボリュームをルートディスクとしてアタッチする
- 仮想マシンの仮想ファームウェアが、EBSボリューム上のブートローダーを読み込み、OSが起動する
同じAMIから何台起動しても、各インスタンスには独立したEBSボリュームが割り当てられます。
AMI
├─ メタデータ(Block Device Mapping、起動許可、ブートモード)
└─ EBSスナップショットへの参照
↓ インスタンス起動時
1. スナップショットから新しいEBSボリュームを作成
↓
2. 仮想マシンにルートディスクとしてアタッチ
↓
3. 仮想ファームウェア → ブートローダー → カーネル → OS起動
AMIはあくまで「起動前の状態」を定義するテンプレートです。たとえば、AMIから起動したインスタンスにSSHでログインしてNginxをインストールしても、元のAMIにNginxが追加されるわけではありません。その変更はそのインスタンスのEBSボリューム上にだけ存在していて、同じAMIから別のインスタンスを起動しても、Nginxは入っていない状態で起動します。
「AMIに焼き込む」とはどういうプロセスか
この記事では、AMIの作成前にソフトウェアや設定をイメージに含めることを「焼き込む」と呼びます。
具体的なプロセスはこうです。
- 既存のAMI(Amazon Linux等)からEC2インスタンスを起動する
- SSHでログインし、必要なパッケージをインストール・設定する
- そのインスタンスからAMIを作成する
3のステップでは、AWSマネジメントコンソールから「イメージを作成」を選ぶか、CLIで aws ec2 create-image コマンドを実行します。
aws ec2 create-image \
--instance-id i-1234567890abcdef0 \
--name "my-web-server" \
--description "Nginx + CloudWatch Agent installed"
このコマンドを実行すると、裏では以下のことが起きます。
- デフォルトでは、整合性のあるスナップショットを取得するためにインスタンスが再起動される(
--no-rebootオプションで省略もできるが、ファイルシステムの整合性は保証されなくなる) - インスタンスにアタッチされている全EBSボリュームのスナップショットが作成される
- そのスナップショットを参照するAMIが登録される
カスタマイズ済みEC2
↓ create-image
インスタンスを再起動(デフォルト)
↓
EBSボリュームのスナップショットを作成
↓
スナップショットを参照するAMIを登録
手動で1台ずつSSHして構築する方法でもAMIは作れますが、手順が増えるほど再現性は下がります。EC2 Image BuilderやPackerといったツールを使えば、「ベースAMIに何をインストールして、どうテストするか」をコードとして定義し、AMIの作成を自動化できます。
焼き込む対象になりうるものには、たとえば以下があります。
- OSパッケージ
- 言語ランタイム
- Webサーバー
- 監視エージェント
- アプリケーション
- 設定ファイル
ただし、AMI作成時にはEBSボリューム上のすべてのデータがスナップショットに含まれます。SSH鍵やシェル履歴、ログ、一時ファイルなど、意図しないデータがAMIに残らないよう、作成前にクリーンアップが必要です。
EC2の構成はいつ決まるのか
ここでいう「構成」とは、EC2インスタンス上にどのソフトウェアがインストールされていて、どのような設定が適用されているか、という状態のことです。たとえば「OSはAmazon Linux 2023、Nginxが入っていて、CloudWatch Agentが動いている」というのが1つの構成。
この構成は、一度に決まるわけではありません。複数のタイミングで、異なる手段によって積み重なっていきます。ここでは4つのタイミングに分けて整理します。
Bake時:AMIを作るときに決める
AMIの作成段階で、OSやパッケージをインストールしておく方法です。
ベースAMI
↓
パッケージのインストール
↓
設定・テスト
↓
新しいAMI
ここで決めた構成は、このAMIから起動するすべてのインスタンスに共通して適用されます。環境(本番・ステージング)に関係なく同じ状態からスタートできる、というのがポイントです。
Boot時:EC2の起動時に決める
EC2の起動時に、User Dataを使って環境ごとの差分を反映する方法です。
AMI
↓ EC2を起動
User Dataを実行
↓
環境固有の設定を反映
User Dataは、インスタンスの起動時にスクリプトや設定情報を渡す仕組みです。シェルスクリプトやcloud-initディレクティブを渡すことができ、デフォルトでは初回起動時に1回だけ実行されます。
たとえば、汎用的なAMIを使いつつ、起動時に「この環境では本番用の設定ファイルを取得する」といった処理を走らせるケースがあります。同じAMIから起動しても、渡すUser Dataを変えれば環境ごとに構成を変えられます。
ただし、User DataにはパスワードやAPIキーなどの秘密情報を直接書かないようにします。必要な場合は、IAMロールを使ってParameter StoreやSecrets Managerから取得する方法が推奨されています。
Runtime時:起動後も望ましい状態を維持する
EC2が起動したあとも、稼働中に構成を変更したり、望ましい状態を維持したりする必要があります。ここで使われるのがAWS Systems Manager(SSM)です。Systems Managerは、EC2をはじめとするAWSリソースの運用管理を行うためのサービスで、コマンドの実行、設定の管理、パッチの適用などをまとめて扱えます。
起動済みEC2
↑
Systems Manager
├─ 設定を反映
├─ コマンドを実行
└─ 望ましい状態を維持
Systems Managerの機能のひとつにState Managerがあります。これは、Systems Managerの管理対象として登録されたEC2インスタンス(マネージドノードと呼ばれます)を、あらかじめ定義した状態に保つための構成管理機能です。
たとえば、CloudWatch Agentが起動していることを確認し、停止していれば起動するという処理をAssociationとして定期実行できます。State Managerは常時監視ではなく、作成時や指定したスケジュールに従って構成を再適用する仕組みです。こうした「定義した状態から外れてしまうこと」を構成ドリフトと呼び、State Managerは定期的にこのドリフトを修正する役割を担っています。
Deploy時:アプリケーションをリリースする
サーバーの構成変更と、アプリケーションのリリースは、別々のパイプラインで行えます。
AMI側のパイプラインでは、EC2 Image BuilderやPackerを使ってOSパッチの適用やエージェントの更新を行い、新しいAMIを作成します。一方、アプリケーション側のパイプラインでは、CodeDeployのようなデプロイツールやCI/CDパイプラインを使って、ビルドしたアプリケーションを稼働中のインスタンスに配布します。
AMIの更新(Image Builder等) アプリの更新(CodeDeploy等)
────────────────── ──────────────────
OS更新 ソース変更
Agent更新 ビルド
脆弱性対応 デプロイ
AMIの更新は月に1回、アプリのリリースは週に数回、というように更新頻度が異なることは珍しくありません。これらを同じリリース単位にまとめるか、分離するかは設計上の判断ポイントです。
4つのタイミングを並べてみる
ここまでの4分類をまとめます。
| タイミング | いつ | 主な手段 | 何を決めるか |
|---|---|---|---|
| Bake | AMI作成時 | Image Builder等 | OS、共通パッケージ、エージェント |
| Boot | EC2起動時 | User Data | 環境固有の設定、初期化処理 |
| Runtime | 起動後 | Systems Manager | 状態維持、設定変更、運用操作 |
| Deploy | リリース時 | デプロイツール | アプリケーションの更新 |
同じEC2でも、この4つのタイミングで異なるレイヤーの構成が決まっています。「全部AMIに入れる」というのは、この4つのうちBakeにすべてを寄せるということ。逆に「AMIは最小限にする」というのは、Boot・Runtime・Deployに責務を分散させるということです。
AMI・User Data・SSM・アプリデプロイの責務を整理する
4つのタイミングで使われる手段には、それぞれ得意な領域があります。
AMIが得意なこと
- OSの初期状態をそろえる
- 大きなパッケージを事前にインストールする
- 起動時の処理を減らして起動を速くする
- 検証済みの構成を固定する
AMIは「全インスタンスに共通で、変更頻度が低いもの」を含めるのに向いています。
User Dataが得意なこと
- 起動した環境に応じて設定を変える
- インスタンスごとの差分を埋める
- 最初の一度だけ必要な初期化を行う
- SSM Agentやデプロイエージェントを起動する
User Dataは「AMIは同じだけど、環境やインスタンスごとに変えたい部分」の処理が得意です。
Systems Managerが得意なこと
Systems Managerにはいくつかの機能があり、ここでは役割を分けて整理します。
Parameter Storeは、設定値を外部に保存する仕組みです。AMIに環境固有の値を埋め込まず、起動時やRuntime時にParameter Storeから取得する、という使い方ができます。AMI IDをパラメータ経由で参照するケースもある。
State Managerは、稼働中のEC2を望ましい状態に保つための機能です。エージェントのバージョンを常に最新にする、特定の設定ファイルが存在する状態を維持する、といった構成管理に使われます。
Run Command・Automationは、複数のインスタンスに対して操作を実行したり、手順化された運用作業を自動化したりする機能です。
SSM Agentは、これらの機能からのリクエストを受け取り、対象のインスタンス上で処理を実行します。AMIにSSM Agentを含めたうえで、IAMロールの付与やSystems Managerエンドポイントへのネットワーク疎通を用意しておけば、起動後早い段階からマネージドノードとして管理できます。
アプリケーションデプロイが得意なこと
- アプリケーションだけを頻繁に更新する
- リリース単位を管理する(バージョン管理、タグ付け)
- ローリングやBlue/Greenなどの方式で段階的に展開する
- アプリケーション単位でロールバックする
アプリケーションのライフサイクルは、OSやミドルウェアとは異なります。更新頻度が高く、すばやいロールバックが求められることが多い。こうした特性から、AMIの更新とは独立したパイプラインで管理されるケースがあります。
厚いAMIと薄いAMI
ここまでの整理を踏まえると、AMIに含める範囲は大きく2つの方向に分かれます。
厚いAMI
OSだけでなく、ミドルウェアやアプリケーションまで含めたAMIです。
厚いAMI
├─ OS
├─ セキュリティ設定
├─ 監視エージェント
├─ ランタイム
├─ Webサーバー
└─ アプリケーション
メリット
- EC2の起動後に行う処理が少なく、起動が速い
- インスタンスごとの差が生まれにくい
- 外部リポジトリの障害に影響されにくい
- 同じAMIから同じ状態を再現しやすい
- Auto Scalingでインスタンスを追加するとき、サービス提供可能になるまでの時間を短縮しやすい
デメリット
- アプリケーションを変更するたびにAMIを作り直す必要がある
- AMIのビルドとテストに時間がかかる
- AMIの世代数が増え、管理コストが上がる
- OS更新とアプリ更新が同じリリース単位になりやすい
- 環境ごとにAMIが増殖しやすい
薄いAMI
OSや最低限のエージェントだけを含め、残りを起動時や起動後に構築するAMIです。
薄いAMI
├─ OS
└─ 最低限のAgent
EC2起動後
├─ User Data
├─ Systems Manager
└─ アプリデプロイ
メリット
- 使い回せる範囲が広く、AMIの種類を減らせる
- アプリケーションを独立してリリースできる
- 環境ごとの差分を外部から注入しやすい
デメリット
- インスタンスがサービス提供可能になるまで時間がかかりやすい
- 起動中に外部からのパッケージ取得やスクリプト実行で失敗する要因が増える
- パッケージリポジトリやS3などへの依存が増す
- 同じスクリプトでも実行時期によって結果が変わる可能性がある
- 起動途中の中間状態を観測しにくい
厚い・薄いは二者択一ではない
厚いAMIと薄いAMIは、どちらかが常に正解というわけではありません。実際には、この2つの間のどこかに落とし所を見つけることになります。
「OSと共通エージェントまでは焼き込むが、アプリケーションは別途デプロイする」という構成は、厚いAMIと薄いAMIの中間にあたります。どこに線を引くかは、更新頻度、起動速度の要件、チームの運用体制によって変わります。
まとめ
この記事では、AMIの役割と、EC2の構成が決まるタイミングを整理しました。
- AMIはEC2の初期状態を定義するイメージであり、「焼き込む」とはAMI作成前にソフトウェアを含めること
- EC2の構成はBake(AMI作成時)、Boot(起動時)、Runtime(起動後)、Deploy(リリース時)の4つのタイミングで決まる
- AMI・User Data・Systems Manager・アプリデプロイには、それぞれ得意な領域がある
- AMIに多くを含める「厚いAMI」と最小限に抑える「薄いAMI」には、それぞれメリット・デメリットがある
具体的に何をAMIに焼き込み、何を外に出すべきなのか。その判断基準やトレードオフ、実務での構成パターンについては、また別の機会に整理します。