はじめに
これまでの業務では、EC2 + 何かの構成が多く、ECSに触れる機会が資格試験の勉強くらいしかなかったので実際に触ってみる。
この記事では、ECSの基本的な構成を理解し、簡単なコンテナアプリケーションをデプロイするところまでを試してみる。
環境
Windows 11 Professional
aws-cli/2.24.24
Docker Desktop 4.49.0 (208700)今回作成する構成図
今回作成する構成は以下とする。
記事の流れ
この記事では、以下の流れでECSの理解や実際のコンテナアプリケーションのデプロイを実施する
- ECSの基本概念を理解する
- ローカル環境でコンテナアプリケーションの作成
ECRへイメージのプッシュ- Fargateを使ったコンテナのデプロイ
- サービスの作成と実行
AWS ECSの基本概念
ECSとは
AWS ECS (Elastic Container Service) は、Dockerコンテナを簡単にデプロイ、管理、スケーリングできるサービスである。
Amazon Elastic Container Service とは
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.htmlフルマネージド: インフラの管理が不要
スケーラビリティ: 自動スケーリング対応
統合性: 他のAWSサービスとの連携が容易
セキュリティ: IAMロールによる細かい権限管理
ECSの主要コンポーネント
1. クラスター (Cluster)
コンテナインスタンスの論理的なグループ。
タスクやサービスを実行するための基盤となる。
2. タスク定義 (Task Definition)
コンテナの設定を定義するJSON形式のテンプレート。
以下の情報を含む
- 使用するDockerイメージ
- CPU/メモリの割り当て
- 環境変数
- ネットワーク設定
- ログ設定
3. タスク (Task)
タスク定義に基づいて実行されるコンテナインスタンス。
1つ以上のコンテナで構成される。
4. サービス (Service)
指定された数のタスクを常時実行し続けるための仕組み。
ロードバランサーとの統合も可能。
起動タイプ
ECSには2つの起動タイプがある
Fargate
- サーバーレス: EC2インスタンスの管理不要
- シンプル: インフラを意識せずにコンテナを実行
- 料金: 使用したリソース分のみ課金
EC2
- 柔軟性: EC2インスタンスを直接管理
- カスタマイズ: インスタンスタイプの選択が可能
- コスト最適化: リザーブドインスタンスなどが利用可能
※今回はFargateを使用する。
準備
- AWSアカウント
AWS CLIのインストールと設定Dockerのインストール
AWS CLIの設定確認
aws --version
aws configure listECSクラスターの作成
コンソールからクラスターを作成
1. AWSマネジメントコンソールにログインし、「Elastic Container Service」を選択する

2. 「クラスター」を選択する

3. 「クラスターの作成」をクリック

4. クラスター設定を入力
全部デフォルトで作成。

サンプルアプリケーションの準備
Dockerイメージの準備
以下になっていればOK
├── Dockerfile
├── Dockerfile.nginx
├── index.php
└── nginx.confFROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]FROM php:8.3-fpm
WORKDIR /var/www/html
COPY index.php /var/www/html/
RUN chown -R www-data:www-data /var/www/html
EXPOSE 9000
CMD ["php-fpm"]server {
listen 80;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PHP API Demo - AWS ECS</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.content {
padding: 40px;
}
.card {
background: #f8f9fa;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
border-left: 4px solid #667eea;
}
.card h2 {
color: #333;
margin-bottom: 16px;
font-size: 1.5em;
}
.info-row {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #e0e0e0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #666;
min-width: 140px;
}
.info-value {
color: #333;
flex: 1;
}
.status {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
}
.status.success {
background: #d4edda;
color: #155724;
}
.status.error {
background: #f8d7da;
color: #721c24;
}
.json-box {
background: #2d3748;
color: #68d391;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 0.9em;
line-height: 1.6;
}
.badge {
display: inline-block;
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85em;
margin-left: 8px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 PHP API Demo</h1>
<p>AWS ECS - PHP-FPM + Nginx Sidecar</p>
</div>
<div class="content">
<?php
$apiUrl = 'https://jsonplaceholder.typicode.com/users/1';
try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("HTTP Error: {$httpCode}");
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('JSON decode error');
}
echo '<div class="card">';
echo '<h2>📡 API Status <span class="status success">SUCCESS</span></h2>';
echo '<div class="info-row">';
echo '<div class="info-label">Endpoint:</div>';
echo '<div class="info-value">' . htmlspecialchars($apiUrl) . '</div>';
echo '</div>';
echo '<div class="info-row">';
echo '<div class="info-label">HTTP Status:</div>';
echo '<div class="info-value">' . $httpCode . ' OK</div>';
echo '</div>';
echo '</div>';
echo '<div class="card">';
echo '<h2>👤 User Information</h2>';
echo '<div class="info-row">';
echo '<div class="info-label">Name:</div>';
echo '<div class="info-value">' . htmlspecialchars($data['name']) . '</div>';
echo '</div>';
echo '<div class="info-row">';
echo '<div class="info-label">Username:</div>';
echo '<div class="info-value">' . htmlspecialchars($data['username']) . '</div>';
echo '</div>';
echo '<div class="info-row">';
echo '<div class="info-label">Email:</div>';
echo '<div class="info-value">' . htmlspecialchars($data['email']) . '</div>';
echo '</div>';
echo '<div class="info-row">';
echo '<div class="info-label">Phone:</div>';
echo '<div class="info-value">' . htmlspecialchars($data['phone']) . '</div>';
echo '</div>';
echo '<div class="info-row">';
echo '<div class="info-label">Website:</div>';
echo '<div class="info-value">' . htmlspecialchars($data['website']) . '</div>';
echo '</div>';
echo '<div class="info-row">';
echo '<div class="info-label">Company:</div>';
echo '<div class="info-value">' . htmlspecialchars($data['company']['name']) . '</div>';
echo '</div>';
echo '<div class="info-row">';
echo '<div class="info-label">City:</div>';
echo '<div class="info-value">' . htmlspecialchars($data['address']['city']) . '</div>';
echo '</div>';
echo '</div>';
echo '<div class="card">';
echo '<h2>📄 JSON Response</h2>';
echo '<div class="json-box">' . htmlspecialchars(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) . '</div>';
echo '</div>';
} catch (Exception $e) {
echo '<div class="card">';
echo '<h2>📡 API Status <span class="status error">ERROR</span></h2>';
echo '<div class="info-row">';
echo '<div class="info-label">Error:</div>';
echo '<div class="info-value">' . htmlspecialchars($e->getMessage()) . '</div>';
echo '</div>';
echo '</div>';
}
?>
<div class="card">
<h2>🖥️ System Information</h2>
<div class="info-row">
<div class="info-label">PHP Version:</div>
<div class="info-value"><?= phpversion() ?> <span class="badge">PHP-FPM</span></div>
</div>
<div class="info-row">
<div class="info-label">Server Time:</div>
<div class="info-value"><?= date('Y-m-d H:i:s T') ?></div>
</div>
<div class="info-row">
<div class="info-label">Hostname:</div>
<div class="info-value"><?= gethostname() ?></div>
</div>
<div class="info-row">
<div class="info-label">SAPI:</div>
<div class="info-value"><?= php_sapi_name() ?></div>
</div>
</div>
</div>
</div>
</body>
</html>
ECRへのプッシュ
REGION="ap-northeast-1"
PROJECT_NAME="php-app"
PROFILE="${AWS_PROFILE:-default}"
ACCOUNT_ID=$(aws sts get-caller-identity --profile $PROFILE --query Account --output text)
# ECRリポジトリURL
PHP_FPM_REPO="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${PROJECT_NAME}/php-fpm"
NGINX_REPO="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${PROJECT_NAME}/nginx"
# ECRリポジトリの作成 (存在しなければ作成)
aws ecr describe-repositories --repository-names "${PROJECT_NAME}/php-fpm" --region $REGION --profile $PROFILE 2>/dev/null || \
aws ecr create-repository --repository-name "${PROJECT_NAME}/php-fpm" --region $REGION --profile $PROFILE \
--image-scanning-configuration scanOnPush=true
aws ecr describe-repositories --repository-names "${PROJECT_NAME}/nginx" --region $REGION --profile $PROFILE 2>/dev/null || \
aws ecr create-repository --repository-name "${PROJECT_NAME}/nginx" --region $REGION --profile $PROFILE \
--image-scanning-configuration scanOnPush=true
# ECRにログイン
aws ecr get-login-password --region $REGION --profile $PROFILE | \
docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com
# Dockerイメージのビルド
## php-fpm
docker build -t php-fpm -f Dockerfile .
## nginx
docker build -t nginx -f Dockerfile.nginx .
# タグ付け
docker tag php-fpm:latest ${PHP_FPM_REPO}:latest
docker tag nginx:latest ${NGINX_REPO}:latest
# プッシュ
docker push ${PHP_FPM_REPO}:latest
docker push ${NGINX_REPO}:latest※ 生成AIにデプロイ用のスクリプト作ってもらったやつで必要なところだけ抜粋したもの
確認
AWS マネジメントコンソールの 「Elastic Container Registry」で確認をするとリポジトリができている。

nginxの方を見るとイメージもちゃんとプッシュできている。
タスク定義の作成
タスク定義用のJSONがあれば、そのままタスク定義を作成できるが、今回はマネジメントコンソール上で作ってみる。
※「Elastic Container Service」サービスに遷移しておく。
「新しいタスク定義の作成」を選択する

設定をして作成する
重要な部分は以下となる。
タスク定義ファミリー
| 項目 | 設定値 |
|---|---|
| ファミリー名 | nginx-php-fpm-test |
インフラストラクチャの要件
| 項目 | 設定値 |
|---|---|
| 起動タイプ | AWS Fargate |
コンテナ - 1
| 項目 | 設定値 |
|---|---|
| コンテナ名 | nginx |
| 必須コンテナ | はい |
| イメージURI | ECRのURIを選択 |
| ポートマッピング | 80 |
| 依存関係の順序 | php-fpm (Start) |
コンテナ - 2
| 項目 | 設定値 |
|---|---|
| コンテナ名 | php-fpm |
| 必須コンテナ | はい |
| イメージURI | ECRのURIを選択 |
補足
作成したJSONの定義
[ACCOUNT_ID] と ECRのURIは各自の環境に修正する必要がある
{
"compatibilities": [
"EC2",
"MANAGED_INSTANCES",
"FARGATE"
],
"containerDefinitions": [
{
"cpu": 0,
"dependsOn": [
{
"condition": "START",
"containerName": "php-fpm"
}
],
"environment": [],
"environmentFiles": [],
"essential": true,
"image": "[ACCOUNT_ID].dkr.ecr.ap-northeast-1.amazonaws.com/php-app/nginx@sha256:2615729c34088cdbf11452bef8ae435974f176ed2cb8127441868c3136629141",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/nginx-php-fpm-test",
"awslogs-create-group": "true",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs"
},
"secretOptions": []
},
"mountPoints": [],
"name": "nginx",
"portMappings": [
{
"appProtocol": "http",
"containerPort": 80,
"hostPort": 80,
"name": "80",
"protocol": "tcp"
}
],
"systemControls": [],
"ulimits": [],
"volumesFrom": []
},
{
"cpu": 0,
"environment": [],
"environmentFiles": [],
"essential": true,
"image": "[ACCOUNT_ID].dkr.ecr.ap-northeast-1.amazonaws.com/php-app/php-fpm@sha256:16b3b0289f8f196c701d5e57eb10305d73db066883617a203ceb3b95427e2219",
"mountPoints": [],
"name": "php-fpm",
"portMappings": [],
"systemControls": [],
"volumesFrom": []
}
],
"cpu": "1024",
"enableFaultInjection": false,
"executionRoleArn": "arn:aws:iam::[ACCOUNT_ID]:role/ecsTaskExecutionRole",
"family": "nginx-php-fpm-test",
"memory": "2048",
"networkMode": "awsvpc",
"placementConstraints": [],
"registeredAt": "2025-11-01T05:40:18.687Z",
"registeredBy": "arn:aws:iam::[ACCOUNT_ID]:root",
"requiresAttributes": [
{
"name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
},
{
"name": "ecs.capability.execution-role-awslogs"
},
{
"name": "com.amazonaws.ecs.capability.ecr-auth"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.21"
},
{
"name": "ecs.capability.container-ordering"
},
{
"name": "ecs.capability.execution-role-ecr-pull"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
},
{
"name": "ecs.capability.task-eni"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
}
],
"requiresCompatibilities": [],
"revision": 1,
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
},
"status": "ACTIVE",
"taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:[ACCOUNT_ID]:task-definition/nginx-php-fpm-test:1",
"volumes": [],
"tags": []
}タスクの実行
作成したタスク定義について、サービスの実行をする。
作成したタスクを選択する

デプロイ→「サービスの作成」を選択する

設定をして「サービスの作成」をする
基本はデフォルトとして、確認用にセキュリティグループについて自分のIPだけアクセスできるようなものを設定しておく。

タスクの確認
# タスク一覧の取得
aws ecs list-tasks --cluster [クラスタ名] --profile=${PROFILE}サービスの確認
aws ecs describe-services --cluster [クラスタ名] --services [サービス名] --profile=${PROFILE}コンテナサービスの確認
作成したサービスが稼働していることを確認してみる。
作成したクラスターを選択

クラスター配下のサービスを確認
ステータス: アクティブとなっている。

サービス→タスクを確認

パブリックIPを確認しアクセスをする

アクセス確認
今回作成したindex.phpが表示されていることを確認する。

ログの確認
タスクから「ログ」を確認するとコンテナのログが確認できる。

発生した問題
1. コンテナがすぐに停止する
- CloudWatch Logsでエラーログを確認
→ 確認してみたら、
ECS上でコンテナ名が名前解決できなかったためエラーになっていた部分があった。php-fpmという名前から →127.0.0.1に変更して解決。 - ヘルスチェックの設定確認
- リソース(CPU/メモリ)の割り当て確認
2. ネットワーク接続できない
- セキュリティグループのインバウンドルール確認 → 普通にこれだった
- ルートテーブルの設定確認
- インターネットゲートウェイの設定確認
補足: ロードバランサーを使う
ALBの作成
- Application Load Balancerを作成
- ターゲットグループを作成(target type: IP)
- リスナーの設定
サービスとALBの統合
コマンド例
aws ecs create-service \
--cluster my-ecs-cluster \
--service-name my-ecs-service-with-alb \
--task-definition my-ecs-task \
--desired-count 2 \
--launch-type FARGATE \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...,containerName=my-app,containerPort=80" \
--network-configuration "awsvpcConfiguration={subnets=[subnet-xxxxx,subnet-yyyyy],securityGroups=[sg-xxxxx]}"まとめ
この記事では、AWS ECSの基本的な概念と、Fargateを使った簡単なコンテナのデプロイ方法を学んだ。
学んだこと
- ECSの基本コンポーネント(クラスター、タスク、サービス)
- タスク定義の作成方法
- Fargateでのコンテナ実行
- ログの確認とトラブルシューティング
次やりたいこと
- Auto Scalingの設定
- CI/CDパイプラインの構築
- マルチコンテナアプリケーションの構築
- Secrets Managerとの統合
参考
Amazon ECS とは - Amazon Elastic Container Service
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.htmlAmazon ECS での Fargate の使用 - Amazon Elastic Container Service
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/what-is-fargate.htmlタスク定義パラメータ - Amazon Elastic Container Service
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.htmlAWSのECSってなんやねん
https://zenn.dev/mi_01_24fu/articles/aws-ecs-2024_03_18
おわりに
AWS ECSを使うことで、コンテナアプリケーションを簡単にデプロイ・管理できることがわかった。Fargateを使えばインフラ管理の手間を削減でき、アプリケーション開発に集中できる。
次回は、ALBを使って本番に近い環境の構築とマルチコンテナアプリケーションの構築に挑戦していきたい。