第4章:Docker・バックエンド(Node.js)
本章では、ECS 上で動作させるバックエンドアプリケーションの設計と Docker 化 について解説します。
①構成の説明
②S3(静的Webサイトホスティング)/フロントエンド
③VPC関連
④Docker/バックエンド
⑤ECS + ALB
⑥DynamoDB・S3連携
⑦CI/CD(アプリケーション)
⑧CI/CD(インフラ)
目次
1. 構成の説明
何をしたいか
- フロントエンド(S3)から呼び出される バックエンド API をECS(Fargate)で実装
- DynamoDB のデータを取得
- S3 上の画像に対して 署名付きURL を発行し、ブラウザから直接参照可能にする
どんな構成か・何ができるか
- Node.js(Express) を使用
- DynamoDB / S3 へは AWS SDK を利用してアクセス
- 認証情報はコードに埋め込まず、ECS タスクロール(IAM ロール) によって取得
- コンテナは ALB 経由で外部公開
2. 作成方法
前提条件
ローカル開発環境 に以下がインストールされていることを前提とします。
-
Node.js(18.x 系)
バックエンド API(Express)を実装・動作確認するために使用 -
Docker
作成したバックエンドアプリケーションを
ECS(Fargate)で実行可能な コンテナイメージとしてビルド するために使用
構築の流れ(概要)
1.下記ファイルを作成
- Dockerfile
- package.json
- server.js
2.コンテナイメージを作成
docker build -t コンテナ名 .
3.コンテナを起動
docker run -p 3000:3000 コンテナ名
- 左側の3000: 自分のPCが待ち受けるポート番号
- 右側の3000: Dockerコンテナの中でアプリ(server.js)が動いているポート番号
主な設定方針・注意点
- リージョン・テーブル名・バケット名はすべて環境変数
- AWS 認証情報は 明示的に設定しない
- CORSは全許可
今回はECSの学習に焦点を当てた最小限の構成であり、ドメイン取得を行っていないため「全許可(*)」としています。本来はセキュリティの観点から、信頼できる特定のドメインのみを許可する設定が推奨されます。
3. コードの解説
バックエンドアプリ(app.js)
セキュリティ・基本設定
app.use(helmet());
app.use(cors({ origin: '*' }));
DynamoDB 接続確認(起動時)
await docClient.send(new ScanCommand({
TableName: process.env.DYNAMODB_TABLE_NAME,
Limit: 1
}));
- 起動時に DynamoDB へ疎通確認
- 失敗した場合は コンテナを即終了
- ECS のヘルスチェックと組み合わせることで、異常タスクを自動的に排除可能
DynamoDB データ取得 API
app.get('/data', async (req, res) => {
const command = new ScanCommand({ TableName: ... }); // 「全件取得して」という命令
const result = await docClient.send(command); // 命令を実行
res.json(result.Items); // 結果をJSON形式で返却
});
- シンプルな
Scanによる全件取得
Scanは下記理由から実運用では非推奨。
- コスト削減: Scanは全データを読み込むため、データが増えるほど課金が増える。
- レスポンス速度: Scanはデータ量に比例して遅くなる。
改善方法
-
キー設計: Queryを使うには、テーブル作成時に「パーティションキー」を適切に設定する。
-
IAM権限の追加: IAMポリシーで dynamodb:Query を許可, dynamodb:Scanを削除。
-
疎通確認方法変更: 起動時の接続チェックに Scan や Query を使うと無駄な読み取りコストがかかるため、テーブル情報の取得(DescribeTable)を使う。
S3 署名付き URL 生成 API
app.get('/image/*', async (req, res) => {
const command = new GetObjectCommand({ Bucket: ..., Key: key });
// 署名付き URLを作成する
const url = await getSignedUrl(s3Client, command, { expiresIn });
res.json({ url });
});
- API が直接画像を返却しない構成
- フロントエンドは S3 から 直接画像を取得
- ECS 側の帯域・負荷を抑える設計
エラーが起きた時の処理, サーバーの待機開始
// どこにも当てはまらないURLに来た時(404エラー)
app.use((req, res) => { res.status(404).json({ error: 'Not Found' }); });
// サーバーを起動
app.listen(port, () => console.log(`Backend running on port ${port}`));
Dockerfile
# 1. ベースとなるOS(Node.jsがインストール済みのもの)を選ぶ
FROM node:18
# 2. コンテナの中での作業ディレクトリ(フォルダ)を決める
WORKDIR /app
# 3. パッケージのリスト(設定ファイル)だけを先にコピーする
COPY package*.json ./
# 4. 必要なライブラリをインストールする(npm install)
RUN npm install
# 5. 残りのプログラムコードをすべてコピーする
COPY . .
# 6. 3000番ポートを使うことを宣言する(メモ書きのようなもの)
EXPOSE 3000
# 7. コンテナが動き出した時に実行するコマンド(サーバー起動)
CMD ["npm", "start"]
4. 振り返り
改善できるところ
- DynamoDB の取得処理を
Queryベースに変更 - CORS 設定の厳格化
- Docker イメージサイズの削減(マルチステージ)
次にやること
次章では、
この Docker イメージを ECS(Fargate)上で実行し、ALB 経由で公開 する構成について解説します。
- ①構成の説明
- ②S3(静的Webサイトホスティング)/フロントエンド
- ③VPC関連
- ④Docker/バックエンド
- ⑤ECS + ALB
- ⑥DynamoDB・S3連携
- ⑦CI/CD(アプリケーション)
- ⑧CI/CD(インフラ)
5. 参照
6. 用語
AWS SDK
特定のAWSサービス(S3やDynamoDBなど)をプログラムから操作するためのツール集。
SDKが一時的な権限を取得し認証を自動化できる。
Node.js
本来Webブラウザ上でしか動作しないJavaScriptをパソコンやサーバー上でも動かせるようにした実行環境。
Express
Node.jsの上で動作する、WebアプリケーションやAPIを開発するための定番フレームワーク。
パッケージ
特定の機能を持つコードを、配布・導入しやすいように一つにまとめたもの。
Node.jsではnpmなどの管理ツールを使い管理できる。
ライブラリ
特定の機能を実行するために独立してパッケージ化された使い回しができるプログラムの集まり。
SDK
特定のプラットフォーム向けのソフトを作るために必要なツールやライブラリがセットになったもの。
フレームワーク
開発を効率的にするライブラリを集めたアプリケーションの雛形。
ライブラリ(部品)をどう組み合わせるかという「構造やルール」までを含んだ大きな仕組みを指す。
枠組みの中に必要なコードを書き込むため、コードの構造に一貫性を持たせられる。
helmet
Express アプリに対して
よくある Web 攻撃への耐性を高めるための HTTP セキュリティヘッダを自動的に付与するミドルウェア。
CORS
あるWebサイトから、別のWebサイト(ドメインなど)にあるデータにアクセスしても安全かどうかを判断し、許可する仕組み。
マルチステージ
「ビルド用のコンテナ」と「実行用のコンテナ」を分けること。
Stage 1 (ビルド用): npm install を行い、ビルドに必要な全てのツールを入れる。
Stage 2 (実行用): Stage 1で作られた成果物(node_modulesなど)だけをコピーし、軽量なOSで動かす。
メリット: 最終的なイメージにソースコードやビルドツールが含まれないため軽量。
7. コード全体
Dockerfile
FROM node:18
# 作業ディレクトリ設定
WORKDIR /app
# パッケージインストール
COPY package*.json ./
RUN npm install
# アプリケーションコードをコピー
COPY . .
# アプリが使用するポートを開放
EXPOSE 3000
# アプリ起動コマンド
CMD ["npm", "start"]
package.json
{
"name": "backend",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.0.0",
"@aws-sdk/lib-dynamodb": "^3.0.0",
"@aws-sdk/client-s3": "^3.0.0",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"helmet": "^7.0.0",
"@aws-sdk/s3-request-presigner": "^3.0.0"
}
}
server.js
const express = require('express');
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, ScanCommand } = require('@aws-sdk/lib-dynamodb');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const helmet = require('helmet');
const app = express();
const port = process.env.PORT || 3000;
app.use(helmet()); // セキュリティ向上
// DynamoDB設定
const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
const docClient = DynamoDBDocumentClient.from(dynamoClient);
// S3インスタンス作成
const s3Client = new S3Client({ region: process.env.AWS_REGION });
// 起動時にDynamoDB接続確認
(async () => {
try {
await docClient.send(new ScanCommand({
TableName: process.env.DYNAMODB_TABLE_NAME,
Limit: 1
}));
console.log('DynamoDB接続確認に成功しました');
} catch (err) {
console.error('DynamoDB接続エラー:', err.message);
process.exit(1); // 起動中止
}
})();
// ExpressでのCORS対応例
const cors = require('cors');
app.use(cors({
origin: '*', // 開発時のみ * を許可。本番はフロントドメインを指定。固定ドメインがないので開発環境では*で運用
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type']
}));
app.get('/', (req, res) => {
res.status(200).send('OK');
});
// データ取得エンドポイント
app.get('/data', async (req, res) => {
try {
const command = new ScanCommand({
TableName: process.env.DYNAMODB_TABLE_NAME
});
const result = await docClient.send(command);
res.json(result.Items);
} catch (err) {
console.error('DynamoDB取得エラー:', err.message);
res.status(500).json({ error: 'データ取得に失敗しました' });
}
});
// S3署名付きURL返却エンドポイント
app.get('/image/*', async (req, res) => {
const key = req.params[0];
console.log("バケット名:", process.env.S3_BUCKET);
console.log("画像キー:", key);
if (!key || typeof key !== 'string' || key.trim() === '') {
console.warn('不正なキーが渡されました:', key);
return res.status(400).json({ error: '無効な画像キーです' });
}
try {
const expiresIn = parseInt(process.env.S3_URL_EXPIRES || '300', 10); // デフォルト300秒
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
});
const url = await getSignedUrl(s3Client, command, { expiresIn });
console.log("生成された署名付きURL:", url);
res.json({ url });
} catch (err) {
console.error('S3署名URL生成エラー:', err.message);
res.status(500).json({ error: '画像URLの取得に失敗しました' });
}
});
// 未定義ルート
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
// エラーハンドラ
app.use((err, req, res, next) => {
console.error('予期しないエラー:', err);
res.status(500).json({ error: 'サーバーエラーが発生しました' });
});
app.listen(port, () => console.log(`Backend running on port ${port}`));
