sasaryo.dev  |  blog   about

AMIの役割と構築タイミングを整理する

#aws

はじめに

EC2インスタンスを起動するとき、AMIを選ぶ必要があります。Amazon LinuxやUbuntuのAMIを選んで起動すれば、OSが入った状態のインスタンスが手に入る。

ただ、AMIにはOSだけでなく、ミドルウェアやアプリケーションまで含めることもできます。たとえば次のようなAMIが考えられます。

しかも、ソフトウェアをEC2に入れる手段はAMIだけではありません。起動時にスクリプトで入れることもできるし、起動後に外部から配布することもできる。手段もタイミングも複数あるからこそ、「どこで何を入れるか」を整理する必要が出てきます。

この記事では、まずコンピューターが起動して動く仕組みを押さえたうえで、AMIが何を担っているのかを整理します。そのうえで、EC2の構成がいつ・何によって決まるのかを4つのタイミングに分類し、厚いAMIと薄いAMIの違いを見ていきます。具体的にどこまで焼き込むべきかという判断基準は、次の記事で扱う予定です。

なお、この記事ではLinuxのEBS-backed AMIと、Nitroベースの仮想インスタンスを前提に説明します。

コンピューターを起動してアプリケーションを動かすには

AMIの話に入る前に、そもそもコンピューターを起動してアプリケーションを動かすまでに何が必要なのかを整理しておきます。EC2も基本的には同じ仕組みの上に成り立っているので、ここを押さえておくとAMIの役割がわかりやすくなります。

ディスクがないとコンピューターは起動できない

PCの電源を入れてWebサーバーを動かすまでには、ストレージ上にいくつかのデータが揃っている必要があります。ストレージはハードディスクやSSDが一般的ですが、USBメモリやネットワーク上のストレージから起動することもできます。ここでは話をわかりやすくするために「ディスク」と呼ぶことにします。

まず起動の流れから見てみます。物理的なコンピューターの起動は、おおまかに以下のステップで進みます。

  1. ファームウェア(BIOS/UEFI)が起動する — マザーボードに組み込まれたファームウェアがまず動き、CPU・メモリ・ディスクなどのハードウェアを初期化する
  2. ファームウェアがブートローダーを読み込む — 設定された起動デバイスから、OSを起動するためのプログラム(GRUBなどのブートローダー)を読み込む
  3. ブートローダーがカーネルを読み込む — ブートローダーが、ディスク上にあるOSのカーネルをメモリにロードする
  4. カーネルが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スナップショットへの参照と、それをどう使うかを定義するメタデータの組み合わせです。

AMIからインスタンスを起動するとき、裏では以下のことが起きています。

  1. AMIが参照するEBSスナップショットから、新しいEBSボリュームが作られる
  2. Nitroが仮想マシンを割り当て、そのEBSボリュームをルートディスクとしてアタッチする
  3. 仮想マシンの仮想ファームウェアが、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の作成前にソフトウェアや設定をイメージに含めることを「焼き込む」と呼びます。

具体的なプロセスはこうです。

  1. 既存のAMI(Amazon Linux等)からEC2インスタンスを起動する
  2. SSHでログインし、必要なパッケージをインストール・設定する
  3. そのインスタンスから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"

このコマンドを実行すると、裏では以下のことが起きます。

  1. デフォルトでは、整合性のあるスナップショットを取得するためにインスタンスが再起動される(--no-reboot オプションで省略もできるが、ファイルシステムの整合性は保証されなくなる)
  2. インスタンスにアタッチされている全EBSボリュームのスナップショットが作成される
  3. そのスナップショットを参照するAMIが登録される
カスタマイズ済みEC2
   ↓ create-image
インスタンスを再起動(デフォルト)

EBSボリュームのスナップショットを作成

スナップショットを参照するAMIを登録

手動で1台ずつSSHして構築する方法でもAMIは作れますが、手順が増えるほど再現性は下がります。EC2 Image BuilderやPackerといったツールを使えば、「ベースAMIに何をインストールして、どうテストするか」をコードとして定義し、AMIの作成を自動化できます。

焼き込む対象になりうるものには、たとえば以下があります。

ただし、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分類をまとめます。

タイミングいつ主な手段何を決めるか
BakeAMI作成時Image Builder等OS、共通パッケージ、エージェント
BootEC2起動時User Data環境固有の設定、初期化処理
Runtime起動後Systems Manager状態維持、設定変更、運用操作
Deployリリース時デプロイツールアプリケーションの更新

同じEC2でも、この4つのタイミングで異なるレイヤーの構成が決まっています。「全部AMIに入れる」というのは、この4つのうちBakeにすべてを寄せるということ。逆に「AMIは最小限にする」というのは、Boot・Runtime・Deployに責務を分散させるということです。

AMI・User Data・SSM・アプリデプロイの責務を整理する

4つのタイミングで使われる手段には、それぞれ得意な領域があります。

AMIが得意なこと

AMIは「全インスタンスに共通で、変更頻度が低いもの」を含めるのに向いています。

User Dataが得意なこと

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エンドポイントへのネットワーク疎通を用意しておけば、起動後早い段階からマネージドノードとして管理できます。

アプリケーションデプロイが得意なこと

アプリケーションのライフサイクルは、OSやミドルウェアとは異なります。更新頻度が高く、すばやいロールバックが求められることが多い。こうした特性から、AMIの更新とは独立したパイプラインで管理されるケースがあります。

厚いAMIと薄いAMI

ここまでの整理を踏まえると、AMIに含める範囲は大きく2つの方向に分かれます。

厚いAMI

OSだけでなく、ミドルウェアやアプリケーションまで含めたAMIです。

厚いAMI
├─ OS
├─ セキュリティ設定
├─ 監視エージェント
├─ ランタイム
├─ Webサーバー
└─ アプリケーション

メリット

デメリット

薄いAMI

OSや最低限のエージェントだけを含め、残りを起動時や起動後に構築するAMIです。

薄いAMI
├─ OS
└─ 最低限のAgent

EC2起動後
├─ User Data
├─ Systems Manager
└─ アプリデプロイ

メリット

デメリット

厚い・薄いは二者択一ではない

厚いAMIと薄いAMIは、どちらかが常に正解というわけではありません。実際には、この2つの間のどこかに落とし所を見つけることになります。

「OSと共通エージェントまでは焼き込むが、アプリケーションは別途デプロイする」という構成は、厚いAMIと薄いAMIの中間にあたります。どこに線を引くかは、更新頻度、起動速度の要件、チームの運用体制によって変わります。

まとめ

この記事では、AMIの役割と、EC2の構成が決まるタイミングを整理しました。

具体的に何をAMIに焼き込み、何を外に出すべきなのか。その判断基準やトレードオフ、実務での構成パターンについては、また別の機会に整理します。