[TIL] 10. AWS ECS

コンテナはゲストOSを持たない
コンテナにはゲストOSは存在せず、ホストマシンとLinux Kernelを共有している。コンテナ型仮想化ソフトウェアであるDocker Engineにより、あたかもLinuxがコンテナ上で起動しているかのように見えている。
Dockerイメージには各ディストリビューションを構成する最小限のソフトウェアセットが含まれており、ホストマシンと共有しているカーネル + イメージファイルに含まれる最小限のソフトウェアセットの組み合わせにより、Ubuntuなど特定のディストリビューションを構成している。
これより分かるように、Docker Engineを起動するホストOSはLinuxである必要がある。ホストOSがLinuxではない場合にはLinux仮想マシンを構築し、その上でDocker Engineを起動する(Docker DesktopはLinux仮想マシンの構築+Docker Engineのインストールを同時におこなってくれる)。
Docker Engineは主に「Dockerクライアント+Dockerデーモン+デーモンが提供するRESTAPI」から構成されており、ユーザインターフェイスとなるDockerクライアントからdockerコマンドを叩くことで、REST APIを通じてDockerデーモンにHTTPリクエストを送信する。

コンテナの実体は「ホストマシン上の隔離されたプロセス」
コンテナはLinuxのNamespaceという機能により他と分離された1プロセスに過ぎない。
これを知ると、コンテナにゲストOSが存在しないということも、Docker Engineを起動するホストOSがLinuxである必要があるということもスッキリと理解できるのではないだろうか。

Dockerレイヤの実体はtarアーカイブファイル
Dockerイメージはレイヤの集合体であり、各レイヤは独自のファイルシステムをtarアーカイブファイルとして保有している。これを重ね合わせることで一つのファイルシステムとして構成していく。
このような複数のレイヤ化されたファイルシステムを統合して認識するためにLayerFSという技術が使用されている。ファイルの更新や削除の情報は全て書き込みレイヤ(UpperDir)に記録される。書き込みレイヤが読み込みレイヤ(LowerDir)よりも上段に位置するため、上から見たときに適切にファイルシステムを認識することができる。

Dockerイメージを深掘りする
1. Dockerイメージを展開
以下のようなDockerイメージをビルドする。
FROM amazonlinux:2023
COPY test.html /
CMD ["cat", "/test.html"]
下記コマンドにてビルドしたDockerイメージをtarアーカイブ形式で出力する。
docker image save IMAGE:TAG | tar -xC ./export(出力先フォルダ)
以下のようなディレクトリが展開される。
blobs/
└── sha256/
├── 8ef517cf0562e16c307e4af33413523eb9aeda43003ac4e6b9e6434ab6f29d33
├── 93b5cbbc86ee614f8432762e1f7f34b6cc9d6d4b95867cf25bca6ae179f49439
├── 910acca52c4a2a6c4a0325758f27b232d9c86bf512c3f83e1482c7e56249118d
├── 7178e9f0c7ec228dde6ba8b6b6f0ce73e2903afbbc7db8aa087a19c237a9f960
├── 7919e930d1a6dbd58e3cc6e5bb483800c157bbe02405c7f82354617f7e49f638
├── cc466a6053312e067e1281e9b75ed9883fc4c90355c12a8ac204e92d03e1b9da
├── cea9f0c5fc941907ee034e9e766287cce10d9eec662e2b641757f8d8ab0421ec
└── d09bdea1659d4c86a11e97092a545c075716406590dab4435891ace522e274c9
manifest.json
index.json
oci-layout
2. 各ファイルを確認
manifest.jsonとindex.jsonは役割としては同じであり、イメージを構成するレイヤを特定するためのjsonファイルである。docker image save
などでアーカイブファイルに展開する際に作成され、docker image load
などによりイメージを作成(復元)するための設計図として機能する。
両者の違いは、Docker専用のフォーマットかOCI標準フォーマットかという点である。
Docker単体で利用する際にはmanifest.jsonを参考にイメージを作成する一方で、podmanなどの異なるコンテナランタイムを利用する際にはindex.jsonを参考にイメージを作成できる。
ここではDockerを単体で利用する場合を想定し、manifest.jsonを確認していく。
manifest.jsonはConfig, RepoTags, Layersという3つの項目から構成される。
- Config:環境変数、実行コマンド、実行ディレクトリなどのイメージの設定情報メタデータやイメージビルド履歴などイメージの構成情報
- RepoTags:イメージタグ名
- Layers:イメージを構成するレイヤ
(93b5cb...がAL2023基盤ファイルシステムで、d09bde...がtest.htmlを追加)
[
{
"Config": "blobs/sha256/910acca52c4a2a6c4a0325758f27b232d9c86bf512c3f83e1482c7e56249118d",
"RepoTags": [
"amazonlinux:my2023"
],
"Layers": [
"blobs/sha256/93b5cbbc86ee614f8432762e1f7f34b6cc9d6d4b95867cf25bca6ae179f49439",
"blobs/sha256/d09bdea1659d4c86a11e97092a545c075716406590dab4435891ace522e274c9"
]
}
]
Configの値であるblobs配下のハッシュ値ファイルを確認する。
ここでは上記の通り、環境変数、実行コマンド、実行ディレクトリなどの設定情報メタデータやイメージビルド履歴、各レイヤのファイルシステムの情報が格納されている。
{
"architecture": "arm64",
"config": {
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"cat",
"/test.html"
],
"WorkingDir": "/",
"ArgsEscaped": true
},
"created": "2025-08-23T05:16:22.149041841Z",
"history": [
{
"created": "2025-08-09T03:45:10Z",
"created_by": "COPY /rootfs/ / # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2025-08-09T03:45:10Z",
"created_by": "CMD [\"/bin/bash\"]",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
},
{
"created": "2025-08-23T05:16:22.149041841Z",
"created_by": "COPY test.html / # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2025-08-23T05:16:22.149041841Z",
"created_by": "CMD [\"cat\" \"/test.html\"]",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:df46ada0ea49d2a0585e54f925f04348c2c48c6aaec7c4c624c31cfc0cd2da76",
"sha256:76a959cd248cade3d055ff705f9bbffcf54e49c7e3e45ef034c25c6eb831c5ba"
]
}
}
ここでConfigの値のrootfs/diff_ids
とLayersの値におけるハッシュ値が異なることに気づく。
これらには以下のような違いがある。実際にコマンドを実行して確かめてみると、期待通りの結果を得ることができる。
- rootfs/diff_ids:各レイヤの実際のファイルシステムをアーカイブ化した後のハッシュ値
- Layers:各レイヤの実際のファイルシステムをアーカイブ化+Zip化した後のハッシュ値
# gzip圧縮を展開してからsha256ハッシュ化
gunzip -c blobs/sha256/93b5cb... | shasum -a 256
> df46ada0ea49d2a0585e54f925f04348c2c48c6aaec7c4c624c31cfc0cd2da76
前者は該当レイヤの内容を示す「論理的識別子」であり、Dockerがレイヤを重ね合わせて統合ファイルシステムを構築する際の識別子として重要な役割を果たす。Dockerは同一のレイヤであれば異なるイメージ間でレイヤを共有するが、「同一か」を識別するのもdiff_idsである。
後者は実際にレイヤを保存する際の「物理的な保存形式」である。

Dockerイメージは特定のCPUアーキテクチャ向けにビルドされる
Dockerイメージは特定のCPUアーキテクチャ向けにビルドされる。ここで紐づくCPUアーキテクチャは基本的にビルドを実行したマシンのものとなる(Buildを実行するDocker Daemonは自身の上でコンテナを動かすことを想定しているため)。
M2 Macで採用されているCPUアーキテクチャはARMであるため、M2 MacでDockerイメージを作成すると、それはARMを搭載したマシンでのみ実行可能なものとなる。以下に示すイメージ展開後のConfigファイルからも確認できる。
{
"architecture": "arm64",
~~~~~~~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~~~~~
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:df46ada0ea49d2a0585e54f925f04348c2c48c6aaec7c4c624c31cfc0cd2da76",
"sha256:76a959cd248cade3d055ff705f9bbffcf54e49c7e3e45ef034c25c6eb831c5ba"
]
}
}
このイメージをECS Fargateで利用する場合には、タスク定義のプロパティであるRuntime platformを設定することで、アーキテクチャを合わせる必要がある(デフォルトはX86_64である)。
あるいは、Dockerイメージビルド時にdocker image build -t IMAGE:TAG --platform linux/x86_64
のようにして、イメージが対応するCPUアーキテクチャを変更することも可能である。

ECS Execの利用
Dockerコンテナ内部に接続したい場合には基本的にSSH接続は使用しない。SSHを待ち受けるためのsshdを起動(openssh-serverをインストール/起動)したりする必要があり、コンテナ構築が複雑になったり、脆弱性が生まれるためである。
Fargate上に作成されたコンテナに乗り込む際には、ECS Execを使用する。コンテナ基盤がEC2であればEC2に接続してdocker container exec
コマンドによりコンテナに乗り込むことができた(docker container execは自身のDocker Engine上に乗っているコンテナ操作のみが可能)。
コンテナ基盤の物理マシンに接続できないFargateではこの方法は不可であるため、ECS Execを使用することが必須となっている。

ECS Clusterの解釈
ECS Clusterは「タスクを配置するEC2インスタンス群」を指すが、基盤環境がFargateかEC2かで具体化されるタイミングが異なる。Fargateの場合には、Cluster作成時点では具体的な範囲は決定しておらず、サービスを作成 or タスクを配備する段階で初めてClusterの枠が定義される。EC2の場合には、Cluster作成時にEC2インスタンス群を定義する必要があるため、作成時点でClusterの範囲が決定する。

ECSにおけるロギング
ECSタスク定義においてログドライバをawslogs
に設定すると、標準出力(/dev/stdout
)および標準エラー出力(/dev/stderr
)に出力されるログをCloudWatch Logsに転送する。
awslogs ログドライバーは、Docker から CloudWatch Logs に STDOUT および STDERR I/O ストリーム> であるコンテナログを渡すだけです。そのため、アプリケーションが STDOUT および STDERR I/O スト> リームにログを送信していることを確認してください。

ここにFirelensの話も書く。割と壮大な話を描きたい。

ECRにおけるイメージ脆弱性スキャン
コンテナイメージは静的な断面であり、時間が経てば当然ライブラリなどの内部コンポーネントのバージョン等は古くなってしまう。作成時は最新の状態であっても、数ヶ月後には新たな脆弱性が発見され、対策が求められることとなる。そのため、イメージの継続的/自動的なスキャンが重要となる。
ECRには「基本スキャン」と「拡張スキャン」の2種類が存在する。
「拡張スキャン」を有効化することで、各種プログラミング言語のパッケージ(ライブラリ等)に関する脆弱性もスキャン可能+新たなCVEが追加されるたびに自動的に継続スキャンしてくれるため、基本的には「拡張スキャン」を採用する。

awsvpcモードではタスク毎にENIが生成される
ネットワークモードがawsvpc
の場合は、タスク毎にENIが割り当てられる。
SGはタスク毎に設定することで通信を制御可能。
ネットワークモードが awsvpc の場合は、タスクに Elastic Network Interface が割り当てられるため、タスク定義を使用したサービスの作成時またはタスクの実行時に NetworkConfiguration を指定する必要があります。

ECRからイメージをPullするためにはS3用ゲートウェイ型VPCeが必要
実際のコンテナイメージはS3に格納されているため、ECS起動時にS3からイメージを取得する。
AWSにQAした結果、インターフェイス型VPCeは利用不可とのこと。ゲートウェイ型VPCe or NAT GWを用意する必要がある。
Amazon ECS タスクで Amazon ECR からプライベートイメージをプルするには、Amazon S3 のゲートウェイエンドポイントを作成する必要があります。Amazon ECR からイメージをダウンロードするコンテナは、Amazon ECR にアクセスしてイメージマニフェストを取得してから Amazon S3 にアクセスして実際のイメージレイヤーをダウンロードする必要があります。

CloudWatch Container Insightsは基本的に有効化!
CloudWatch Container Insightsを有効化することで、ECSタスク/ECSコンテナ単位のCPU使用率やメモリ使用率などがメトリクスとして出力され、詳細なリソースモニタリングが可能になる。クラスタ単位に設定することが可能である。RunningTaskCount
やDesiredTaskCount
もCloudWatch Container Insightsを有効化しないと出力されないため注意が必要。
# Define ECS cluster
resource "aws_ecs_cluster" "frontend" {
name = "${var.common.env}-frontend-cluster"
setting {
name = "containerInsights"
value = "enhanced" # Container Insights with enhanced observability
}
}