ステップ3:Next.jsアプリケーションの作成とECSへのデプロイ
今回のステップの概要とこのカリキュラムとの関連について
このステップでは、実際のコンテナアプリケーション(Next.js)を作成し、前回構築したECS on Fargate環境にデプロイします。具体的には、シンプルなNext.jsアプリケーションの作成、Dockerfileの作成、Amazon ECRへのイメージプッシュ、ECSタスク定義とサービスのコード化を行います。
コンテナアプリケーションのインフラにとって、このステップは「実際の商品を棚に並べる作業」のような役割を果たします。これまでのステップで、土地(VPC)を整備し、棚(ECSクラスター)と受付カウンター(ALB)を用意しました。このステップでは、実際に販売する商品(Next.jsアプリ)を作り、箱詰め(Docker化)し、倉庫(ECR)に保管し、棚(ECS)に並べます。そして、お客様(ユーザー)がブラウザでアクセスすると、受付カウンター(ALB)が商品棚(ECSタスク)に案内し、商品(Webページ)を提供します。
従来、アプリケーションをデプロイするには、EC2インスタンスにSSHでログインし、Node.jsをインストールし、アプリケーションコードをコピーし、依存パッケージをインストールし、アプリケーションを起動する、という手作業が必要でした。コンテナ化とIaCにより、これらの作業をすべて自動化し、再現性の高いデプロイを実現できます。
このステップで学ぶこと
- Next.jsアプリケーションの作成とDockerfileの作成
- Amazon ECRへのコンテナイメージのプッシュ
- ECSタスク定義とサービスのコード化
- ECSセキュリティグループとトラフィック制御
リソースの関わりと構成説明
ステップ3で作成するリソースは、実際のコンテナアプリケーションを動かすための「タスク定義」「サービス」「コンテナイメージ」を構築するものです。それぞれのリソースがインフラにどのように関わるのかを説明します。
Next.jsアプリケーションとインフラの関わり
Next.js「ネクストジェイエス」は、React ベースのWebアプリケーションフレームワークで、サーバーサイドレンダリング(SSR)や静的サイト生成(SSG)が可能な「商品」のような役割を果たします。物理的な店舗で言えば、「実際に販売する商品そのもの」に相当します。このカリキュラムでは、トップページとヘルスチェックエンドポイントを持つシンプルなNext.jsアプリを作成します。このアプリをDockerコンテナ化することで、「どの環境でも同じように動作する商品パッケージ」にできます。これにより、開発環境、ステージング環境、本番環境で完全に同じアプリケーションを動かせます。
Amazon ECRとインフラの関わり
Amazon ECR(Elastic Container Registry)「コンテナイメージレジストリ」は、Dockerイメージを保管する「倉庫」のような役割を果たします。物理的な店舗で言えば、「商品を保管する倉庫」に相当します。開発者は、ローカルでDockerイメージをビルドし、ECRにプッシュ(アップロード)します。ECSは、ECRからイメージをプル(ダウンロード)して、コンテナを起動します。ECRは、イメージのバージョン管理、脆弱性スキャン、アクセス制御を提供し、セキュアにイメージを管理できます。
ECSタスク定義とインフラの関わり
ECSタスク定義「タスクデフィニション」は、コンテナの設定を記述した「レシピ」のような役割を果たします。物理的な店舗で言えば、「商品をどの棚にどのように並べるかを記載したマニュアル」に相当します。タスク定義には、使用するコンテナイメージ、CPU・メモリのリソース量、環境変数、ログ設定、IAMロールなどを記述します。ECSサービスは、このタスク定義を元に、実際のコンテナ(タスク)を起動します。タスク定義を変更すれば、コンテナの設定を即座に更新できます。
ECSサービスとインフラの関わり
ECSサービス「サービス」は、指定した数のタスクを常に稼働させる「店舗運営マネージャー」のような役割を果たします。物理的な店舗で言えば、「常に一定数の商品を棚に並べておく責任者」に相当します。サービスは、タスクが異常終了した場合に自動的に新しいタスクを起動し、指定した数(desiredCount)を維持します。また、ALBのターゲットグループにタスクを自動登録し、ヘルスチェックが失敗したタスクを自動的に置き換えます。これにより、高可用性を実現できます。
実際の手順
実際の手順では、たくさんの設定値を入力することになります。 本文中に設定値が指定されていない場合は、デフォルト値のまま作業を進めてください。
1. Next.jsアプリケーションの作成
シンプルなNext.jsアプリケーションを作成し、ヘルスチェックエンドポイントを実装します。
1-1. Next.jsプロジェクトの作成
- CDKプロジェクトのルートディレクトリで、Next.jsアプリ用のディレクトリを作成します
mkdir nextjs-app
cd nextjs-app- Next.jsプロジェクトを初期化します
npx create-next-app@latest . --typescript --tailwind --app --no-src-dir --import-alias "@/*"プロンプトが表示されたら、以下のように選択します:
- Which linter would you like to use? › ESLint
- Would you like to use React Compiler? … Yes
- Would you like to use Turbopack? (recommended) … Yes
【解説】ESLint、React Compiler、Turbopackとは何か
Next.jsのプロジェクト初期化時に選択する3つのツールについて、それぞれの役割と利点を理解しましょう。
ESLint(イーエスリント)- コード品質チェックツール
ESLintは、JavaScriptやTypeScriptのコードを自動的にチェックして、バグの原因になりやすいコードパターンや、コーディング規約に違反している箇所を警告してくれるツールです。これは「コードレビューを自動化するツール」と考えるとわかりやすいです。
例えば、「宣言したけど使っていない変数がある」「等価比較で==の代わりに===を使うべき」「非同期処理でawaitを忘れている」といった問題を、コードを書いた直後にエディタ上で教えてくれます。これにより、実行時エラーや予期しない動作を事前に防げます。
Next.jsのプロジェクトでは、React特有のルール(例:useEffectの依存配列の正しい指定、コンポーネント名の命名規則など)も自動的にチェックされるため、Reactのベストプラクティスに沿ったコードを書くことができます。チーム開発では、全員が同じコーディングスタイルを守ることで、コードの読みやすさと保守性が向上します。
ESLintは開発中のみ動作し、本番環境のパフォーマンスには影響しません。このカリキュラムでは、ESLintを有効にすることで、初心者が陥りやすいミスを早期に発見し、正しいコーディング習慣を身につけることを目指します。
React Compiler(リアクトコンパイラー)- パフォーマンス最適化ツール
React Compilerは、Reactの公式チームが開発した新しいコンパイラで、Reactコンポーネントのパフォーマンスを自動的に最適化してくれるツールです。従来、開発者はuseMemoやuseCallbackといったフックを手動で使って、コンポーネントの再レンダリングを最適化する必要がありました。React Compilerは、このような最適化を自動的に行ってくれるため、開発者がパフォーマンスチューニングを意識せずに、機能開発に集中できます。
具体的には、React Compilerは「このコンポーネントはpropsが変わらない限り再レンダリングする必要がない」「この計算結果は毎回計算し直す必要がない」といった判断を自動的に行い、必要最小限の再レンダリングだけを実行するようにコードを最適化します。これは、熟練したReact開発者が手動で行っていた最適化を、コンパイラが自動的に実施してくれるイメージです。
React Compilerはまだ比較的新しい機能ですが、Next.js 15以降では公式にサポートされており、将来的にはReact開発の標準になると期待されています。このカリキュラムでは、最新のReactのベストプラクティスを学ぶために、React Compilerを有効にします。ただし、もし互換性の問題が発生した場合は、無効にすることもできます。
Turbopack(ターボパック)- 高速ビルドツール
Turbopackは、Next.jsの開発チーム(Vercel)が開発した、次世代のJavaScriptバンドラー(ビルドツール)です。従来、Next.jsはWebpackというバンドラーを使用していましたが、プロジェクトが大きくなるにつれて、ビルド時間が長くなるという課題がありました。Turbopackは、Rust言語で実装されており、Webpackと比較して最大10倍高速にビルドできます。
バンドラーとは、複数のJavaScriptファイル、CSSファイル、画像ファイルなどを1つのファイルにまとめて、ブラウザで効率的に読み込めるようにするツールです。開発中は、コードを変更するたびにバンドラーが動作し、変更を画面に反映します。Turbopackを使うと、この「コード変更→画面反映」のサイクルが非常に高速になるため、開発効率が大幅に向上します。
例えば、大規模なNext.jsプロジェクトで、コンポーネントを1つ修正しただけなのに、画面の更新に数秒かかることがあります。Turbopackを使えば、この更新が数百ミリ秒で完了するため、ストレスなく開発できます。特に、このカリキュラムのように繰り返しコードを修正しながら学習する場合、高速なビルドは非常に重要です。
Turbopackは開発環境(next dev)で使用され、本番環境のビルド(next build)ではまだWebpackが使用されます(将来的にはTurbopackに移行予定)。このカリキュラムでは、Turbopackを有効にして、快適な開発体験を提供します。
3つのツールを有効にする理由
これらのツールは、すべて「開発者の生産性を向上させる」という共通の目的を持っています。ESLintはコードの品質を保ち、React Compilerはパフォーマンスを最適化し、Turbopackは開発速度を向上させます。初心者の方は、これらのツールの恩恵を受けながら、正しいコーディング習慣を身につけ、高速な開発環境で学習を進めることができます。
【解説】Next.js App Routerとは何か
Next.js 13以降では、「App Router」という新しいルーティングシステムが導入されました。従来の「Pages Router」では、pages/ ディレクトリにファイルを配置してルーティングを定義していましたが、App Routerでは、app/ ディレクトリにファイルを配置します。
App Routerの最大の特徴は、Reactの最新機能であるServer Components(サーバーコンポーネント)とStreaming(ストリーミングレンダリング)をネイティブにサポートしていることです。Server Componentsは、サーバー側でのみ実行されるコンポーネントで、クライアントに送信されるJavaScriptのサイズを削減できます。これにより、ページの初期ロードが高速化されます。
App Routerでは、app/page.tsx がトップページ、app/api/route.ts がAPIエンドポイント、app/layout.tsx が共通レイアウトという構造になります。このカリキュラムでは、App Routerを使用してシンプルなWebアプリケーションを作成します。
1-2. ヘルスチェックエンドポイントの実装
app/api/health/route.tsファイルを作成し、ヘルスチェックエンドポイントを実装します
mkdir -p app/api/healthapp/api/health/route.tsファイルを作成し、以下の内容を記述します
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json(
{
status: 'healthy',
timestamp: new Date().toISOString(),
},
{ status: 200 }
);
}【解説】ヘルスチェックエンドポイントはなぜ必要なのか
ヘルスチェックエンドポイントは、アプリケーションが正常に動作しているかを自動的に監視するための「安否確認システム」です。このエンドポイントがなぜ重要なのか、実際の問題を例に説明します。
問題:アプリケーションが壊れても気づけない
コンテナで動いているアプリケーションは、様々な理由で突然動かなくなることがあります。例えば、メモリ不足でクラッシュしたり、データベースへの接続が切れたり、コード内のバグで無限ループに陥ったりします。従来の方法では、このような障害が発生しても、「ユーザーからの問い合わせが来て初めて気づく」という状況になりがちでした。
物理的な店舗で例えると、店員が突然倒れてしまったのに、誰も気づかず、お客さんが店に入ってきて初めて「店員がいない!」と騒ぎ始めるようなものです。これでは、ビジネスに大きな損失が発生します。
解決策:定期的な安否確認で自動検知
ヘルスチェックエンドポイントは、この問題を解決します。ALB(ロードバランサー)は、数秒おきに /api/health というエンドポイントにアクセスして、「元気ですか?」と確認します。アプリケーションが正常であれば、「はい、元気です(ステータス200)」と返答します。もし応答がなかったり、エラーが返ってきたりすると、ALBは「このコンテナは異常だ」と判断し、そのコンテナへのトラフィック送信を停止します。
ヘルスチェックの具体的な動作フロー
-
正常時の動作 - ALBは、すべてのコンテナに対して定期的に
/api/healthにアクセスします。全てのコンテナが「200 OK」を返すため、ALBはユーザーからのリクエストを全てのコンテナに均等に振り分けます。 -
異常発生時の動作 - あるコンテナ(コンテナA)がメモリ不足でクラッシュしました。このコンテナは
/api/healthに応答できなくなります。ALBは2回連続でヘルスチェックに失敗したコンテナAを「Unhealthy(異常)」と判断し、トラフィックの送信を停止します。ユーザーからのリクエストは、正常なコンテナB・Cにのみ振り分けられるため、サービスは継続します。 -
自動復旧時の動作 - ECSは、コンテナAがクラッシュしたことを検知し、自動的に新しいコンテナA’を起動します。新しいコンテナA’が起動し、ヘルスチェックに連続で成功すると、ALBはコンテナA’を「Healthy(正常)」と判断し、再びトラフィックを振り分け始めます。
このように、ヘルスチェックエンドポイントがあることで、障害の自動検知→異常なコンテナの隔離→新しいコンテナの自動起動→サービス復旧という一連のプロセスが、人間の介入なしに自動的に実行されます。
シンプルな実装が重要な理由
ヘルスチェックエンドポイントは、できるだけシンプルに実装するのがベストプラクティスです。なぜなら、ヘルスチェック自体が複雑だと、「アプリケーションは正常だけど、ヘルスチェックだけが失敗する」という誤検知が発生するからです。
例えば、ヘルスチェックで「データベースに接続できるか」をチェックするとします。データベースが一時的に高負荷で応答が遅れた場合、アプリケーション自体は正常に動いているのに、ヘルスチェックがタイムアウトして失敗します。ALBはこのコンテナを異常と判断し、トラフィックを停止します。しかし実際には、数秒後にデータベースの負荷が下がり、アプリケーションは正常に動作可能だったとします。このような誤検知により、本来サービスを提供できるコンテナが無駄に停止されてしまいます。
基本的には、「アプリケーションが起動しているか」「HTTPリクエストに応答できるか」を確認するだけで十分です。このカリキュラムでは、/api/health エンドポイントがステータス200と { status: 'healthy' } というJSONを返すシンプルな実装にしています。
本番環境での応用
本番環境では、よりきめ細かいヘルスチェックを実装することもあります。例えば:
-
Liveness Probe(生存確認) - このカリキュラムで実装する基本的なヘルスチェック。「アプリケーションが起動しているか」だけを確認。
-
Readiness Probe(準備完了確認) - 「アプリケーションがリクエストを受け付ける準備ができているか」を確認。例えば、起動直後でまだデータベース接続プールを初期化中の場合は、「まだ準備できていない」と返答し、初期化が完了したら「準備完了」と返答します。
-
Startup Probe(起動確認) - 起動に時間がかかるアプリケーション(大きなファイルの読み込みが必要など)で、起動完了を確認します。
しかし、これらの高度なヘルスチェックを実装する場合でも、「ヘルスチェックの失敗がサービス停止につながる」ことを常に意識し、慎重に設計する必要があります。このカリキュラムでは、まず基本的なLiveness Probeを実装し、ヘルスチェックの仕組みを理解することに焦点を当てます。
1-3. トップページの更新
app/page.tsxファイルを開き、以下の内容に書き換えます
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<div className="z-10 max-w-5xl w-full items-center justify-center font-mono text-sm">
<h1 className="text-4xl font-bold mb-4">Welcome to IaC Learning!</h1>
<p className="text-xl">
This Next.js app is running on AWS ECS Fargate, deployed with AWS CDK.
</p>
<p className="mt-4">
Infrastructure as Code makes deployment easy and reproducible.
</p>
</div>
</main>
);
}- ローカルで動作確認します
npm run devブラウザで http://localhost:3000 にアクセスし、ページが表示されることを確認します。また、http://localhost:3000/api/health にアクセスし、{"status":"healthy","timestamp":"..."} というJSONが返ってくることを確認します。
- 確認が完了したら、開発サーバーを停止します(Ctrl+C)
2. Dockerfileの作成とECRへのプッシュ
Next.jsアプリをコンテナ化し、ECRにプッシュします。
2-1. Dockerfileの作成
nextjs-app/ディレクトリにDockerfileを作成します
FROM node:20-alpine AS base
# 依存関係のインストール
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ビルド
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# 本番環境
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]next.config.jsファイルを開き、standalone出力を有効化します
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
reactCompiler: true,
};
export default nextConfig;【解説】マルチステージビルドによるイメージサイズの最適化
このDockerfileは、「マルチステージビルド」という手法を使用しています。マルチステージビルドは、複数のFROMステートメントを使用して、ビルド環境と本番環境を分離する手法です。
従来のDockerfileでは、依存パッケージのインストール、アプリケーションのビルド、実行環境の準備をすべて1つのイメージで行っていました。その結果、最終的なDockerイメージには、ビルドツール、開発用パッケージ、ソースコードなど、実行時には不要なファイルが大量に含まれ、イメージサイズが数GBになることもありました。
マルチステージビルドでは、ビルドに必要なファイルを一時的なイメージ(deps、builder)で処理し、最終的な本番イメージ(runner)には、実行に必要な最小限のファイルのみをコピーします。このDockerfileでは、node_modules/ の一部、.next/standalone/(Next.jsのビルド成果物)、.next/static/(静的ファイル)のみをコピーしています。
さらに、node:18-alpine という軽量なベースイメージを使用しています。Alpineは、最小限のLinuxディストリビューションで、通常のDebianベースのイメージと比較して、サイズが約5分の1になります。これにより、最終的なDockerイメージは100MB〜200MB程度に抑えられ、ECRへのプッシュやECSでのプルが高速化されます。
Next.jsのoutput: 'standalone'設定は、ビルド時に実行に必要な最小限のファイルのみを.next/standalone/ディレクトリに出力する機能です。通常のビルドでは、node_modules/全体が必要ですが、standalone出力では、実際に使用されるパッケージのみが含まれます。
2-2. .dockerignoreファイルの作成
nextjs-app/ディレクトリに.dockerignoreファイルを作成します
node_modules
.next
.git
*.md
.env
.env.*2-3. ECRリポジトリの作成とイメージプッシュ
- CDKプロジェクトのルート(
cdk-iac-project/)に戻ります
cd ..lib/cdk-iac-project-stack.tsに、ECRリポジトリのコードを追加します
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ecr from 'aws-cdk-lib/aws-ecr';
export class CdkIacProjectStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// ... 省略(前述のVPC、ECS、ALB、IAMロールのコード) ...
// ECRリポジトリの作成
const repository = new ecr.Repository(this, 'MyRepository', {
repositoryName: 'nextjs-app',
removalPolicy: cdk.RemovalPolicy.DESTROY, // 学習目的でスタック削除時にリポジトリも削除
emptyOnDelete: true, // リポジトリ削除時にイメージも削除
imageScanOnPush: true, // プッシュ時に脆弱性スキャンを実行
});
// ECRリポジトリのURIを出力
new cdk.CfnOutput(this, 'RepositoryUri', {
value: repository.repositoryUri,
description: 'ECR Repository URI',
});
// ... 省略(以降のコード) ...
}
}- ECRリポジトリをデプロイします
cdk deploy --region us-east-1- 出力されたECRリポジトリのURIをメモします
Outputs:
CdkIacProjectStack.RepositoryUri = 123456789012.dkr.ecr.us-east-1.amazonaws.com/nextjs-app- ECRにログインします(リージョンとアカウントID(12ケタの数字)は自分の環境に合わせてください)
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com- Next.jsアプリのDockerイメージをビルドします
cd nextjs-app
docker build -t nextjs-app .- ローカルでDockerコンテナを起動し、アプリケーションが正常に動作することを確認します
# コンテナをバックグラウンドで起動(ポート3000をマッピング)
docker run -d -p 3000:3000 --name nextjs-app-test nextjs-app:latest- ブラウザでアプリケーションにアクセスして動作確認します
# コンテナのログを確認
docker logs nextjs-app-test
# ブラウザで http://localhost:3000 にアクセス
# Next.jsアプリのトップページが表示されることを確認- 動作確認が完了したら、テスト用のコンテナを停止・削除します
# コンテナを停止
docker stop nextjs-app-test
# コンテナを削除
docker rm nextjs-app-test【解説】ローカルでの動作確認の重要性と実践的なデバッグ手法
ECRにイメージをプッシュする前に、ローカルでDockerコンテナを起動して動作確認を行うことは、開発効率とコスト削減の観点から非常に重要です。実務では、この手順を省略せずに必ず実行することが推奨されます。
ビルドエラーの早期発見による時間とコストの節約
Dockerfileに問題がある場合、ローカルでビルド・実行することで、ECRにプッシュする前にエラーを発見できます。例えば、依存パッケージのインストールエラーが発生した場合、npm install が失敗する原因は、package.json や package-lock.json の不整合であることが多いです。ローカルでビルドを実行すると、このようなエラーが即座に表示され、package-lock.json を再生成するか、依存パッケージのバージョンを調整することで解決できます。同様に、マルチステージビルドでビルドステージとランタイムステージの間で必要なファイルがコピーされていない場合、COPY コマンドの記述を確認することで、ビルド時にエラーを発見できます。また、Dockerfileの EXPOSE で指定したポート番号と、アプリケーションが実際にリッスンするポートが一致していない場合、コンテナは起動しますが、外部からアクセスできないという問題が発生します。このような問題も、ローカルでコンテナを起動してブラウザからアクセスすることで、即座に発見できます。
アプリケーションの動作確認による本番環境での問題の予防
コンテナが正常に起動したとしても、アプリケーションが期待通りに動作するとは限りません。ローカルでコンテナを起動した状態で、実際にブラウザからアクセスして動作を確認することで、ECSにデプロイした後に発生する可能性のある問題を事前に防げます。まず、docker run -p 3000:3000 でポートマッピングが正しく設定されているか確認します。docker ps コマンドを実行すると、コンテナのポートマッピングが表示されるため、0.0.0.0:3000->3000/tcp のように表示されていれば、ポートマッピングは正しく設定されています。次に、本番環境と同じ設定で動作するか確認するため、docker run -e NODE_ENV=production のように環境変数を指定してコンテナを起動します。これにより、環境変数が正しく読み込まれているか、環境変数に依存する設定が正しく動作するかを確認できます。さらに、ヘルスチェックエンドポイントである /api/health が正常に応答するか確認します。ECSでは、このエンドポイントを使用してタスクの健全性を判断するため、ローカルで確認しておくことで、ECSでのヘルスチェック失敗を防げます。また、Next.jsの静的ファイル(CSS、JavaScript、画像など)が正しく配信されるか確認します。静的ファイルが404エラーになる場合、Next.jsのビルドが正しく実行されていない可能性があります。最後に、アプリケーションの主要なAPIエンドポイントが正常に動作するか確認します。これらの確認により、ECSにデプロイした後に「アプリケーションが起動しない」「APIが応答しない」などの問題を事前に防げます。
ローカル環境での詳細なデバッグ手法とその利点
ローカルで問題が発生した場合、Dockerが提供する豊富なデバッグコマンドを使用して、詳細に調査できます。docker logs -f nextjs-app-test コマンドを使用すると、コンテナのログをリアルタイムで確認できます。このコマンドは、アプリケーションが起動している間に実行すると、エラーメッセージや警告が即座に表示されるため、問題の原因を迅速に特定できます。docker exec nextjs-app-test ps aux コマンドを使用すると、コンテナ内で実行されているプロセスを確認できます。これにより、アプリケーションのプロセスが正常に起動しているか、予期しないプロセスが実行されていないかを確認できます。docker exec nextjs-app-test ls -la /app コマンドを使用すると、コンテナ内のファイル構造を確認できます。これにより、必要なファイルが正しくコピーされているか、ファイルの権限が正しく設定されているかを確認できます。docker exec nextjs-app-test env コマンドを使用すると、コンテナ内の環境変数を確認できます。これにより、環境変数が正しく設定されているか、期待する値が設定されているかを確認できます。docker exec -it nextjs-app-test /bin/sh コマンドを使用すると、コンテナ内に入って対話的にデバッグできます。これにより、ファイルの内容を確認したり、コマンドを実行したりして、問題の原因を詳細に調査できます。docker stats nextjs-app-test コマンドを使用すると、コンテナのリソース使用状況(CPU、メモリ、ネットワークI/O)を確認できます。これにより、ECSタスク定義で設定するCPUとメモリの値を適切に決定できます。例えば、アプリケーションが512MBのメモリを使用している場合、ECSタスク定義で1024MB(1GB)を設定することで、余裕を持たせることができます。
一方、ECSでデプロイしてから問題を調査する場合、CloudWatch Logsを確認したり、ECS Execでコンテナ内に入る必要がありますが、これには追加の設定(IAM権限、セキュリティグループ設定など)が必要です。また、CloudWatch Logsにログが表示されるまでに数秒から数分かかる場合があり、リアルタイムでのデバッグが困難です。ローカルでの確認は、これらの設定なしにすぐにデバッグできるため、迅速です。
トラブルシューティングの実践的なアプローチ
ローカルでコンテナを起動した際に、よくある問題とその解決方法を理解しておくことで、問題が発生した際に迅速に対応できます。コンテナがすぐに終了する場合、docker logs コマンドでエラーログを確認します。通常は、アプリケーションの起動エラーやポートのリッスン失敗が原因です。エラーログには、具体的なエラーメッセージが表示されるため、そのメッセージを基に原因を特定できます。ポート3000にアクセスできない場合、docker ps コマンドでポートマッピングを確認します。-p 3000:3000 が正しく設定されているか確認し、設定されていない場合は、コンテナを停止して再度起動します。環境変数が読み込まれない場合、docker exec nextjs-app-test env コマンドで環境変数を確認します。docker run 時に -e オプションで環境変数を指定しているか確認し、指定していない場合は、環境変数を追加してコンテナを再起動します。静的ファイルが404エラーになる場合、Next.jsのビルドが正しく実行されているか確認します。docker exec nextjs-app-test ls -la /app/.next コマンドでビルド成果物を確認し、.next ディレクトリが存在しない場合は、Dockerfileのビルドステップを確認します。
- イメージにタグを付けます(リージョンとアカウントID(12ケタの数字)は自分の環境に合わせてください)
docker tag nextjs-app:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/nextjs-app:latest- ECRにプッシュします(リージョンとアカウントID(12ケタの数字)は自分の環境に合わせてください)
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/nextjs-app:latest【解説】ECRの脆弱性スキャンとイメージタグ戦略
Amazon ECRは、プッシュされたDockerイメージの脆弱性スキャンを自動的に実行できます。imageScanOnPush: true を設定すると、イメージがプッシュされた際に、既知のセキュリティ脆弱性(CVE)をスキャンし、結果をECRコンソールで確認できます。重大な脆弱性が検出された場合、ベースイメージを更新したり、依存パッケージをアップデートしたりして対応します。
イメージタグ戦略も重要です。このカリキュラムでは、latest タグを使用していますが、本番環境では、GitのコミットSHAやビルド番号を使用した具体的なタグ(例:v1.2.3、sha-abc123)を付けることが推奨されます。具体的なタグを使用することで、「どのバージョンのコードがデプロイされているか」が明確になり、ロールバックも容易になります。
CI/CDパイプラインでは、GitHubのコミットSHAを自動的にタグとして使用できます。例えば、GitHub Actionsで docker build -t $ECR_URI:$GITHUB_SHA . のようにビルドすることで、コミットごとに一意のタグが付けられます。
3. ECSセキュリティグループへの追加設定
ECSタスク用のセキュリティグループに対して、ALBからのHTTPトラフィック(ポート3000)を許可する設定にします。
3-1. ALBからのHTTPトラフィック許可のCDKコード追加
lib/cdk-iac-project-stack.tsに、ALBからのHTTPトラフィック許可のコードを追加します(ECRリポジトリのコードの後に追加)
// ... 省略(前述のECRリポジトリのコード) ...
// ALBからのHTTPトラフィック(ポート3000)を許可
ecsSecurityGroup.addIngressRule(
albSecurityGroup,
ec2.Port.tcp(3000),
'Allow traffic from ALB on port 3000'
);
// ... 省略(以降のコード) ...【解説】ECSセキュリティグループとALBからのトラフィック制御
ECSタスク用のセキュリティグループは、「ALBからのトラフィックのみを許可する」という、非常に重要なセキュリティ設定です。この設定により、インターネットから直接ECSタスクにアクセスすることを防ぎ、すべてのトラフィックをALB経由にすることで、セキュリティとトラフィック管理を一元化できます。
なぜALBからのトラフィックのみを許可するのか
もしECSセキュリティグループが「すべてのIPアドレス(0.0.0.0/0)からのポート3000へのアクセスを許可」という設定だったらどうなるでしょうか。この場合、悪意のある攻撃者が、ALBをバイパスして、直接ECSタスクのIPアドレスにアクセスできてしまいます。ALBが提供するWAF(Web Application Firewall)による保護、アクセスログの記録、SSL/TLS終端などの機能が無効化され、セキュリティリスクが大幅に増加します。
しかし、ECSセキュリティグループで「ALBのセキュリティグループからのトラフィックのみ許可」と設定すると、ALBに所属するインスタンスからのトラフィックのみが許可されます。攻撃者が直接ECSタスクにアクセスしようとしても、セキュリティグループによって拒否されます。これは、物理的な店舗で例えると、「正面玄関(ALB)を通った人だけが店内(ECS)に入れる」という仕組みです。裏口や窓から侵入しようとしても、セキュリティシステムが働いて拒否されます。
ポート番号の違い:ALBは80、ECSは3000
ALBは外部からポート80(HTTP)でアクセスを受け付けますが、ECSタスクはポート3000で動作しています。これは正常な構成です。ALBがポート80でリクエストを受け取り、そのリクエストをバックエンドのECSタスクのポート3000に転送(プロキシ)します。
ユーザーは http://example.com/ のようにポート80でアクセスし、ALBが内部的にECSタスクの 10.0.1.5:3000(プライベートIPアドレスとポート3000)に転送します。ユーザーはECSタスクがポート3000で動いていることを知る必要はありません。このように、ALBがポート変換とプロキシの役割を果たします。
allowAllOutbound: true の意味
allowAllOutbound: true は、ECSタスクからインターネットへのアウトバウンドトラフィックを許可する設定です。これは、ECSタスクが外部のAPIを呼び出したり、パッケージをダウンロードしたりするために必要です。
例えば、Next.jsアプリが外部のAPIサービス(天気予報API、決済APIなど)にリクエストを送信する場合、アウトバウンドトラフィックが許可されていないと、通信が失敗します。また、Dockerイメージのビルド時に、npm installでパッケージをダウンロードする際にも、アウトバウンドトラフィックが必要です(ただし、イメージビルドはローカルやCI/CD環境で行うため、ECSタスク自体はパッケージダウンロードを行いません)。
セキュリティをさらに強化したい場合は、allowAllOutbound: false に設定し、特定のIPアドレスやポートのみを許可することもできます。例えば、「HTTPSポート443のみを許可」「特定のAPIサーバーのIPアドレスのみを許可」といった細かい制御が可能です。
セキュリティグループの依存関係
この設定では、ECSセキュリティグループが albSecurityGroup を参照しています。そのため、CDKコード内で、ALBセキュリティグループがECSセキュリティグループよりも先に定義されている必要があります。CDKは依存関係を自動的に解決し、正しい順序でリソースを作成します。
もしALBセキュリティグループが定義されていない場合、albSecurityGroup is not defined というエラーが発生します。このエラーが出た場合は、step2で作成したALBセキュリティグループのコードが正しく含まれているか確認してください。
4. ECSタスク定義とサービスの作成
ECSタスク定義とサービスをCDKコードで作成し、Next.jsアプリをデプロイします。
4-1. タスク定義のCDKコード追加
lib/cdk-iac-project-stack.tsに、ECSタスク定義のコードを追加します
// ... 省略(前述のECSセキュリティグループのコード) ...
// ECSタスク定義の作成
const taskDefinition = new ecs.FargateTaskDefinition(this, 'MyTaskDef', {
memoryLimitMiB: 512,
cpu: 256,
executionRole: taskExecutionRole,
taskRole: taskRole,
});
// コンテナの追加
const container = taskDefinition.addContainer('NextjsContainer', {
image: ecs.ContainerImage.fromEcrRepository(repository, 'latest'),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'nextjs-app',
logRetention: 7, // ログを7日間保持
}),
environment: {
NODE_ENV: 'production',
},
});
// コンテナのポートマッピング
container.addPortMappings({
containerPort: 3000,
protocol: ecs.Protocol.TCP,
});
// ... 省略(以降のコード) ...【解説】Fargateのタスクサイズとコスト最適化
Fargateのタスクサイズは、CPU(0.25 vCPU、0.5 vCPU、1 vCPU、2 vCPU、4 vCPU)とメモリ(512MB〜30GB)の組み合わせで指定します。このカリキュラムでは、最小サイズの256 CPU(0.25 vCPU)と512MBメモリを使用しています。
Fargateの料金は、タスクが実行されている時間に対して課金されます。料金は、CPUとメモリの量によって決まります。例えば、東京リージョンでは、0.25 vCPUで約$0.04/時間、512MBメモリで約$0.004/時間です。つまり、1タスクを1ヶ月(730時間)稼働させた場合、約$32の料金がかかります。
コスト最適化のポイントは、アプリケーションに必要な最小限のCPU・メモリを使用することです。CloudWatch Container Insightsで、CPU使用率とメモリ使用率を監視し、リソースが余っている場合は、タスクサイズを小さくすることでコストを削減できます。逆に、CPU使用率が常に80%以上の場合は、タスクサイズを大きくすることで、パフォーマンスが向上します。
本番環境では、Auto Scalingを設定し、トラフィック量に応じてタスク数を自動的に増減させることで、コストとパフォーマンスのバランスを取ります。
4-2. ECSサービスのCDKコード追加
lib/cdk-iac-project-stack.tsに、ECSサービスのコードを追加します
// ... 省略(前述のタスク定義のコード) ...
// ECSサービスの作成
const service = new ecs.FargateService(this, 'MyService', {
cluster: cluster,
taskDefinition: taskDefinition,
desiredCount: 2, // 2つのタスクを常に稼働
securityGroups: [ecsSecurityGroup],
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, // プライベートサブネットに配置
},
healthCheckGracePeriod: cdk.Duration.seconds(60), // ヘルスチェック開始までの猶予時間
});
// サービスをALBのターゲットグループに登録
service.attachToApplicationTargetGroup(targetGroup);
// サービス名を出力
new cdk.CfnOutput(this, 'ServiceName', {
value: service.serviceName,
description: 'ECS Service Name',
});【解説】desiredCountとヘルスチェック猶予時間の設定
desiredCount: 2 は、常に2つのタスクを稼働させる設定です。ECSサービスは、タスクが異常終了した場合や、ヘルスチェックが失敗した場合に、自動的に新しいタスクを起動して、desiredCountを維持します。これにより、1つのタスクに障害が発生しても、もう1つのタスクがトラフィックを処理し続けるため、サービスが継続します。
本番環境では、desiredCountを3以上に設定することが推奨されます。2つのタスクの場合、片方に障害が発生すると、残り1つのタスクにすべてのトラフィックが集中し、過負荷になる可能性があります。3つ以上のタスクを配置することで、障害時の影響を分散できます。
healthCheckGracePeriod: cdk.Duration.seconds(60) は、タスク起動後、ヘルスチェックを開始するまでの猶予時間です。Next.jsアプリは、起動に数秒〜数十秒かかるため、起動直後にヘルスチェックを実行すると、まだアプリが起動していないため失敗します。猶予時間を設定することで、アプリが完全に起動してからヘルスチェックを開始できます。
ヘルスチェック猶予時間は、アプリケーションの起動時間に合わせて調整します。起動に30秒かかるアプリなら、猶予時間を60秒に設定することで、余裕を持ってヘルスチェックを開始できます。逆に、猶予時間を短くしすぎると、起動中のタスクがヘルスチェックに失敗し、無限に再起動を繰り返す「クラッシュループ」に陥る可能性があります。
4-3. ECSサービスのデプロイと動作確認
- CDKプロジェクトのルートに戻ります(
nextjs-app/からcdk-iac-project/に移動)
cd ..- 変更内容を確認します
cdk diff --region us-east-1- デプロイします
cdk deploy --region us-east-1- デプロイが完了したら、ALBのDNS名にブラウザでアクセスします
Outputs:
CdkIacProjectStack.AlbDnsName = CdkIa-MyAlb-XXXXXXXXXXXX.us-east-1.elb.amazonaws.com-
ブラウザで
http://<ALB DNS名>/にアクセスし、Next.jsアプリのトップページが表示されることを確認します -
http://<ALB DNS名>/api/healthにアクセスし、ヘルスチェックエンドポイントが正常に応答することを確認します
【解説】デプロイ後のトラブルシューティング方法
デプロイが成功しても、ALBにアクセスしたときに「503 Service Unavailable」が表示される場合があります。これは、タスクがまだ起動中、またはヘルスチェックに失敗している可能性があります。以下の手順でトラブルシューティングを行いましょう。
1. ECSタスクの状態を確認: AWSコンソールのECSダッシュボードで、クラスター → サービス → タスクを選択し、タスクの状態を確認します。状態が「RUNNING」でない場合、タスク詳細を開いて、停止理由(Stopped reason)を確認します。
2. CloudWatch Logsを確認: タスク詳細の「Logs」タブから、CloudWatch Logsのログストリームを開き、コンテナのログを確認します。Next.jsの起動エラー、ポート番号の設定ミス、環境変数の不足などがログに記録されています。
3. ターゲットグループのヘルスチェック状態を確認:
EC2ダッシュボードの「Target Groups」で、ターゲットグループを選択し、「Targets」タブでタスクのヘルスチェック状態を確認します。状態が「unhealthy」の場合、ヘルスチェックパス(/api/health)が正しく実装されているか、ポート番号が一致しているかを確認します。
4. セキュリティグループのルールを確認: VPCダッシュボードの「Security Groups」で、ALB用とECS用のセキュリティグループのインバウンド・アウトバウンドルールを確認します。ALB → ECS(ポート80または3000)の通信が許可されているかを確認します。
このステップで何をしたのか
このステップでは、実際のコンテナアプリケーション(Next.js)を作成し、ECS on Fargate環境にデプロイしました。具体的には、Next.jsアプリの作成とヘルスチェックエンドポイントの実装、Dockerfileの作成とマルチステージビルド、Amazon ECRへのコンテナイメージのプッシュ、ECSセキュリティグループの作成、ECSタスク定義とサービスのコード化を行いました。
このインフラでどのような影響があるのか
この構成により、コンテナアプリケーションが実際にECS上で動作し、ブラウザからアクセスできるようになりました。ユーザーがALBのDNS名にアクセスすると、ALBがヘルスチェックを通過したECSタスクにトラフィックを転送し、Next.jsアプリのページが表示されます。
これは、物理的な店舗で言えば、「商品(Next.jsアプリ)を箱詰め(Docker化)し、倉庫(ECR)に保管し、棚(ECS)に並べ、お客様(ユーザー)が受付カウンター(ALB)から案内されて商品を購入する」という一連の流れが完成した状態です。
次のステップ4では、このシンプルなインフラに対して、環境分離(dev/stg/prod)、タグ付け、ネーミング規則などのベストプラクティスを適用し、さらにGitHubでのコード管理とCI/CD統合の準備を行います。
技術比較まとめ表
| 技術領域 | AWS | オンプレミス |
|---|---|---|
| コンテナレジストリ | Amazon ECR マネージドサービス、脆弱性スキャン、IAM統合、高可用性 | Docker Hub、Harbor 自社で構築・管理、ストレージ容量管理、バックアップ設定が必要 |
| コンテナデプロイ | ECSタスク定義 + サービス 宣言的な設定、自動復旧、Auto Scaling統合 | Kubernetes Deployment YAMLで設定、ReplicaSet管理、HPA設定が必要 |
| ログ管理 | CloudWatch Logs 自動収集、永続化、検索・分析機能、ログ保持期間設定 | ELK Stack、Fluentd 自社でログ収集基盤を構築、ストレージ管理、インデックス設計が必要 |
| デプロイ時間 | cdk deploy で5〜10分 タスク定義、サービス、コンテナイメージを自動デプロイ | 数十分〜数時間 Kubernetesマニフェスト適用、イメージプル、ヘルスチェック確認を手動実施 |
| コスト | Fargate + ECR タスク実行時間とストレージ容量に応じた従量課金 | EC2 + EBS + S3 インスタンス費用、ストレージ費用、ネットワーク転送料が固定費 |
学習において重要な技術的違い
1. コンテナイメージの管理とセキュリティ
- AWS:ECRはマネージドサービスで、イメージのバージョン管理、脆弱性スキャン、ライフサイクルポリシー(古いイメージを自動削除)を提供。IAMで細かいアクセス制御が可能
- オンプレミス:Docker HubやHarborを自社で構築・管理する必要があり、ストレージ容量の監視、バックアップ設定、脆弱性スキャンツールの導入を手動で実施
2. コンテナのログ管理とモニタリング
- AWS:CloudWatch Logsに自動的にログが送信され、ログ保持期間、検索、メトリクスフィルターを簡単に設定可能。Container Insightsでメトリクスも可視化
- オンプレミス:ELK Stack(Elasticsearch、Logstash、Kibana)やFluentdを使ったログ収集基盤を自社で構築し、ストレージ管理、インデックス設計、保持期間設定を手動で実施
3. デプロイの自動化とロールバック
- AWS:ECSサービスは、新しいタスク定義をデプロイする際、ローリングアップデート(古いタスクを徐々に新しいタスクに置き換える)を自動実行。デプロイに失敗した場合、自動ロールバックも可能
- オンプレミス:Kubernetesのローリングアップデートを手動で設定し、デプロイ状況を監視。ロールバックも
kubectl rollout undoコマンドで手動実施
実践チェック:CDKコードと実行結果で証明しよう
下記のチェック項目について、実際にCDKコードを記述し、cdk deploy で構築できていることを確認し、各項目ごとに該当画面のスクリーンショットを撮影して提出してください。
-
Next.jsアプリが正常に作成され、ローカル(
npm run dev)で動作することを確認 -
ヘルスチェックエンドポイント
/api/healthがステータス200とJSONを返すことを確認 -
Dockerfileが正しく記述され、
docker buildでイメージがビルドできることを確認 -
ECRリポジトリが作成され、
imageScanOnPush: trueで脆弱性スキャンが有効化されている -
Dockerイメージが正常にECRにプッシュされ、ECRコンソールでイメージが確認できる
-
ECSタスク定義が作成され、CPU 256、メモリ512MB、CloudWatch Logsのログドライバーが設定されている
-
ECSサービスが作成され、desiredCount 2、プライベートサブネット配置が設定されている
-
ブラウザでALBのDNS名にアクセスし、Next.jsアプリのトップページが表示される
-
ブラウザで
http://<ALB DNS名>/api/healthにアクセスし、ヘルスチェックエンドポイントが応答する -
EC2ダッシュボードのターゲットグループで、2つのECSタスクが「healthy」状態で登録されている
-
CloudWatch Logsで、Next.jsアプリのログ(起動ログ、アクセスログ)が確認できる
提出方法: 各項目ごとにスクリーンショットを撮影し、まとめて提出してください。 ファイル名やコメントで「どの項目か」が分かるようにしてください。 ブラウザでのアクセス確認では、URLとページ内容が見えるようにしてください。
構成図による理解度チェック
ステップ2の構成図に、このステップで作成したリソースを追記して、現在のインフラ構成を完成させましょう。
なぜ構成図を更新するのか?
ステップ3では、実際のコンテナアプリケーション(ECSタスク)がECS環境にデプロイされ、ALBを経由して外部からアクセスできるようになりました。構成図を更新することで、「ユーザーのリクエストがどのようにALBからECSタスクに転送され、Next.jsアプリがレスポンスを返すか」という一連のトラフィックフローを視覚的に理解できます。
また、ECR、CloudWatch Logs、タスク定義、サービスといった新しいリソースがどのように連携しているかを確認することで、ECSのデプロイメントモデルを深く理解できます。
- トラフィックフローの完全な理解: 外部 → ALB → ターゲットグループ → ECSタスク → Next.jsアプリ → レスポンス
- コンテナデプロイメントの理解: ECR → タスク定義 → サービス → タスク(コンテナ)
- ログとモニタリングの理解: ECSタスク → CloudWatch Logs、Container Insights
構成図の書き方
ステップ2で作成した構成図をベースに、以下のリソースを追記してみましょう。
- Amazon ECR: VPCの外部に配置し、Dockerイメージのアイコンで表現
- ECSタスク(コンテナ): プライベートサブネット内のECSクラスターに2つ配置
- CloudWatch Logs: VPCの外部に配置し、ECSタスクからログが送信される矢印を描く
- タスク定義: ECSクラスターの近くに配置し、ECRイメージを参照している関係を矢印で示す
- 通信経路の完全な流れ: ユーザー → インターネット → IGW → ALB → ターゲットグループ → ECSタスク(Next.jsアプリ)
💡 ヒント: ECSタスクは、コンテナのアイコンで表現し、タスク定義から参照されている関係を点線の矢印で示すと分かりやすいです。CloudWatch Logsへのログ送信も、点線の矢印で表現しましょう。
理解度チェック:なぜ?を考えてみよう
AWSの各リソースや設計には、必ず”理由”や”目的”があります。 下記の「なぜ?」という問いに自分なりの言葉で答えてみましょう。 仕組みや設計意図を自分で説明できることが、真の理解につながります。 ぜひ、単なる暗記ではなく「なぜそうなっているのか?」を意識して考えてみてください。
Q. Dockerfileでマルチステージビルドを使用した理由は何ですか?すべての処理を1つのステージで行うDockerfileと比較して、どのようなメリットがありますか?
Q. ECSタスク定義で、desiredCount: 2 を設定しました。なぜ1つではなく、2つ以上のタスクを稼働させることが推奨されるのですか?
Q. ECRで imageScanOnPush: true を設定しました。なぜコンテナイメージの脆弱性スキャンが重要なのですか?脆弱性が検出された場合、どのように対応すべきですか?
Q. ECSサービスで healthCheckGracePeriod: cdk.Duration.seconds(60) を設定しました。なぜヘルスチェック開始までの猶予時間が必要なのですか?この値を短くしすぎると、どのような問題が発生しますか?
今回のステップで利用したAWSサービス名一覧
- Next.js:Reactベースのサーバーサイドレンダリング対応Webアプリケーションフレームワーク
- Docker:アプリケーションをコンテナ化するためのプラットフォーム、マルチステージビルドで最適化
- Amazon ECR:Dockerイメージを保管するマネージドなコンテナレジストリ、脆弱性スキャン機能
- ECSセキュリティグループ:ECSタスクへのトラフィックを制御するファイアウォールルール
- ECSタスク定義:コンテナの設定(イメージ、CPU、メモリ、環境変数、ログ)を定義
- ECSサービス:指定した数のタスクを常に稼働させる仕組み、自動復旧・Auto Scaling
- CloudWatch Logs:コンテナのログを収集・保存・検索する監視サービス