0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】ECSを用いた簡易web構築④(Docker・バックエンド-Node.js)

Last updated at Posted at 2025-12-21

第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.jsExpress) を使用
  • DynamoDB / S3 へは AWS SDK を利用してアクセス
  • 認証情報はコードに埋め込まず、ECS タスクロール(IAM ロール) によって取得
  • コンテナは ALB 経由で外部公開

ECS-DynamoDB-S3-CICD-arc.jpg


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: '*' }));
  • helmet
    最低限のセキュリティヘッダを設定しています。
  • CORS
    全許可 ※本来はフロントエンドのドメインに限定すべき

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 経由で公開 する構成について解説します。


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}`));

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?