AWS ECSでnginx + php-fpmの簡単な構成を試してみる

はじめに

これまでの業務では、EC2 + 何かの構成が多く、ECSに触れる機会が資格試験の勉強くらいしかなかったので実際に触ってみる。
この記事では、ECSの基本的な構成を理解し、簡単なコンテナアプリケーションをデプロイするところまでを試してみる。

環境

Windows 11 Professional
aws-cli/2.24.24
Docker Desktop 4.49.0 (208700)

今回作成する構成図

今回作成する構成は以下とする。

aws-architecture-simple

記事の流れ

この記事では、以下の流れでECSの理解や実際のコンテナアプリケーションのデプロイを実施する

  1. ECSの基本概念を理解する
  2. ローカル環境でコンテナアプリケーションの作成
  3. ECRへイメージのプッシュ
  4. Fargateを使ったコンテナのデプロイ
  5. サービスの作成と実行

AWS ECSの基本概念

ECSとは

AWS ECS (Elastic Container Service) は、Dockerコンテナを簡単にデプロイ、管理、スケーリングできるサービスである。

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 list

ECSクラスターの作成

コンソールからクラスターを作成

1. AWSマネジメントコンソールにログインし、「Elastic Container Service」を選択する

create-cluster-00

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

create-cluster-01

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

create-cluster-02

4. クラスター設定を入力

全部デフォルトで作成。

create-cluster-03

サンプルアプリケーションの準備

Dockerイメージの準備

以下になっていればOK

├── Dockerfile
├── Dockerfile.nginx
├── index.php
└── nginx.conf
Dockerfile.nginx
FROM nginx:alpine

COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
Dockerfile
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"]
nginx.conf
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;
    }
}
index.php
<!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」で確認をするとリポジトリができている。

ecr-01

nginxの方を見るとイメージもちゃんとプッシュできている。
ecr-02

タスク定義の作成

タスク定義用のJSONがあれば、そのままタスク定義を作成できるが、今回はマネジメントコンソール上で作ってみる。 ※「Elastic Container Service」サービスに遷移しておく。

「新しいタスク定義の作成」を選択する

create-task-01

設定をして作成する

重要な部分は以下となる。

タスク定義ファミリー

項目設定値
ファミリー名nginx-php-fpm-test

インフラストラクチャの要件

項目設定値
起動タイプAWS Fargate

コンテナ - 1

項目設定値
コンテナ名nginx
必須コンテナはい
イメージURIECRのURIを選択
ポートマッピング80
依存関係の順序php-fpm (Start)

コンテナ - 2

項目設定値
コンテナ名php-fpm
必須コンテナはい
イメージURIECRの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": []
}

タスクの実行

作成したタスク定義について、サービスの実行をする。

作成したタスクを選択する

execute-task-01

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

execute-task-02

設定をして「サービスの作成」をする

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

execute-task-03

タスクの確認

# タスク一覧の取得
aws ecs list-tasks --cluster [クラスタ名] --profile=${PROFILE}

サービスの確認

aws ecs describe-services --cluster [クラスタ名] --services [サービス名] --profile=${PROFILE}

コンテナサービスの確認

作成したサービスが稼働していることを確認してみる。

作成したクラスターを選択

container-service-01

クラスター配下のサービスを確認

ステータス: アクティブとなっている。

container-service-02

サービス→タスクを確認

container-service-03

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

container-service-04

アクセス確認

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

container-service-05

ログの確認

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

log-01

発生した問題

1. コンテナがすぐに停止する

  • CloudWatch Logsでエラーログを確認 → 確認してみたら、ECS上でコンテナ名が名前解決できなかったためエラーになっていた部分があった。
    php-fpmという名前から → 127.0.0.1 に変更して解決。
  • ヘルスチェックの設定確認
  • リソース(CPU/メモリ)の割り当て確認

2. ネットワーク接続できない

  • セキュリティグループのインバウンドルール確認 → 普通にこれだった
  • ルートテーブルの設定確認
  • インターネットゲートウェイの設定確認

補足: ロードバランサーを使う

ALBの作成

  1. Application Load Balancerを作成
  2. ターゲットグループを作成(target type: IP)
  3. リスナーの設定

サービスと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との統合

参考

おわりに

AWS ECSを使うことで、コンテナアプリケーションを簡単にデプロイ・管理できることがわかった。
Fargateを使えばインフラ管理の手間を削減でき、アプリケーション開発に集中できる。
次回は、ALBを使って本番に近い環境の構築とマルチコンテナアプリケーションの構築に挑戦していきたい。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。