T-CREATOR

Docker でマイクロサービスを構築するアーキテクチャ入門

Docker でマイクロサービスを構築するアーキテクチャ入門

近年、多くの企業がマイクロサービスアーキテクチャを採用し、システムの柔軟性と拡張性を追求しています。この変革の背景には、Dockerのようなコンテナ技術の成熟があります。

従来のモノリシックアプリケーションでは、すべての機能が単一のアプリケーション内に実装されていました。これに対してマイクロサービスは、機能ごとに独立したサービスとして分割し、それぞれが独立してデプロイ・スケーリング可能な設計です。

この記事では、Dockerを活用したマイクロサービスアーキテクチャの基本概念から実装まで、初心者の方にも分かりやすく解説いたします。具体的なコード例とサンプルアプリケーションを通じて、実際に手を動かしながら学習できる内容となっています。

背景

マイクロサービスアーキテクチャとは

マイクロサービスアーキテクチャは、アプリケーションを小さな独立したサービスの集合として構築するアプローチです。各サービスは特定のビジネス機能に特化し、独自のデータベースを持ちます。

モノリシックからマイクロサービスへの移行トレンド

従来のモノリシックアプリケーションでは、以下のような課題が顕在化していました。

#モノリシックの課題詳細説明
1部分的なスケーリングの困難さ全体をスケールする必要があり、リソースが無駄になる
2技術スタックの制約一度選択した技術から変更が困難
3開発チーム間の依存関係一つの変更が全体に影響を与える
4デプロイリスクの高さ小さな変更でも全体のデプロイが必要

これらの課題を解決するために、Netflix、Amazon、Uberなどの大手企業が率先してマイクロサービスアーキテクチャを採用し、その有効性が実証されています。

分散システムとしての特徴

マイクロサービスは本質的に分散システムです。以下の図で基本的な構造を理解しましょう。

mermaidflowchart TB
    client[クライアント] -->|API 呼び出し| gateway[API Gateway]
    gateway --> service1[ユーザー<br/>サービス]
    gateway --> service2[商品<br/>サービス]
    gateway --> service3[注文<br/>サービス]
    
    service1 --> db1[(ユーザー<br/>DB)]
    service2 --> db2[(商品<br/>DB)]
    service3 --> db3[(注文<br/>DB)]
    
    service1 -.->|データ取得| service2
    service3 -.->|ユーザー情報| service1
    service3 -.->|商品情報| service2

この図から分かるように、各サービスが独立しており、必要に応じてサービス間で通信を行います。API Gatewayが外部からの窓口となり、適切なサービスにリクエストをルーティングします。

ビジネス要件と技術要件の変化

現代のビジネス環境では、以下のような要件が重視されています。

  • 迅速な機能リリース: 市場の変化に素早く対応する必要性
  • 高可用性: システム停止によるビジネス損失の最小化
  • グローバル展開: 地域ごとの要件に柔軟に対応

これらの要件を満たすために、マイクロサービスアーキテクチャが注目されているのです。

Dockerがマイクロサービスに果たす役割

コンテナ技術の基本概念

Dockerはコンテナ技術を使用して、アプリケーションとその依存関係を軽量で移植可能なコンテナにパッケージ化します。

コンテナと仮想マシンの違いを表で比較してみましょう。

#項目コンテナ仮想マシン
1リソース使用量軽量(MB単位)重い(GB単位)
2起動時間秒単位分単位
3OSホストOSを共有独自のOS
4分離レベルプロセス分離ハードウェア分離

マイクロサービスとの親和性

Dockerとマイクロサービスは以下の点で非常に相性が良いです。

  1. サービスの独立性: 各コンテナが独立した実行環境を提供
  2. ポータビリティ: どの環境でも同じように動作
  3. スケーラビリティ: 必要なサービスだけをスケール

以下のDockerfileの基本例をご覧ください。

dockerfileFROM node:18-alpine

WORKDIR /app

# パッケージファイルをコピー
COPY package*.json ./

# 依存関係をインストール
RUN yarn install --frozen-lockfile

このように、アプリケーションの実行環境を明示的に定義することで、環境の違いによる問題を回避できます。

開発・運用の効率化

Dockerを使用することで、以下のような効率化が実現されます。

  • 環境の統一: 開発、テスト、本番環境で同じコンテナを使用
  • デプロイの簡素化: コンテナ単位でのデプロイが可能
  • ロールバックの容易性: 前のバージョンのコンテナに瞬時に切り戻し

課題

マイクロサービス構築における技術的課題

マイクロサービスアーキテクチャは多くの利点をもたらしますが、同時に新たな技術的課題も発生します。

サービス間通信の複雑性

モノリシックアプリケーションでは単純な関数呼び出しだったものが、マイクロサービスではネットワーク通信になります。

以下の図で通信の複雑さを理解しましょう。

mermaidsequenceDiagram
    participant Client as クライアント
    participant Gateway as API Gateway
    participant UserService as ユーザーサービス
    participant ProductService as 商品サービス
    participant OrderService as 注文サービス
    
    Client->>Gateway: 注文作成リクエスト
    Gateway->>OrderService: 注文処理開始
    OrderService->>UserService: ユーザー情報取得
    UserService-->>OrderService: ユーザー情報
    OrderService->>ProductService: 商品情報・在庫確認
    ProductService-->>OrderService: 商品情報
    OrderService-->>Gateway: 注文完了
    Gateway-->>Client: レスポンス

この図から分かるように、単一の操作でも複数のサービス間でやり取りが発生し、エラーハンドリングや遅延の管理が重要になります。

データの一貫性管理

分散システムでは、ACID特性の維持が困難です。特に以下の点が課題となります。

  • 結果的整合性: データの整合性が即座に保証されない
  • 分散トランザクション: 複数のサービスにまたがる処理の原子性確保
  • データ複製の管理: 同じデータが複数のサービスで必要な場合

運用監視の困難さ

マイクロサービスでは監視すべき対象が大幅に増加します。

#監視項目モノリシックマイクロサービス
1アプリケーション数1個N個
2ログの種類統一サービス毎に異なる
3障害の原因特定比較的容易サービス間の依存関係を考慮
4パフォーマンス分析単一指標複数サービスの総合評価

開発チーム間の連携

マイクロサービスでは、各サービスが異なるチームによって開発されることが多く、以下の課題があります。

  • API契約の管理: サービス間のインターフェース定義
  • バージョン互換性: サービスの更新タイミングの調整
  • 共通ライブラリ: 重複コードの管理

Dockerを使わない場合の問題点

環境依存の問題

従来の開発では「私の環境では動くのに...」という問題が頻繁に発生していました。

bash# 開発者Aの環境
node --version  # v16.14.0
npm --version   # 8.3.1

# 開発者Bの環境  
node --version  # v18.12.0
npm --version   # 9.1.0

このようなバージョンの違いが予期しない動作を引き起こすことがありました。

デプロイメントの複雑さ

従来のデプロイでは以下のような手順が必要でした。

  1. サーバーにアプリケーションをコピー
  2. 依存関係のインストール
  3. 設定ファイルの更新
  4. サービスの再起動
  5. 正常性の確認

これらの手順が複数のサービスで必要になると、デプロイの複雑さは指数関数的に増大します。

スケーリングの困難さ

マイクロサービスでは、需要に応じて特定のサービスのみをスケールしたいケースが多くあります。しかし、従来の環境では全体をスケールするか、手動で新しいインスタンスを設定する必要がありました。

解決策

Dockerを活用したマイクロサービス設計

コンテナ化戦略

効果的なコンテナ化戦略では、以下の原則に従います。

Single Responsibility原則 各コンテナは単一の責任を持つように設計します。

dockerfile# 良い例:Webアプリケーション専用
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN yarn install --frozen-lockfile

COPY src/ ./src/
COPY public/ ./public/

EXPOSE 3000
CMD ["yarn", "start"]

イミュータブルインフラストラクチャ コンテナイメージは変更不可能として扱い、設定変更時は新しいイメージをビルドします。

Docker Composeによるオーケストレーション

複数のマイクロサービスを連携させるために、Docker Composeを使用します。

yamlversion: '3.8'

services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "3000:3000"
    depends_on:
      - user-service
      - product-service
    environment:
      - NODE_ENV=production
yaml  user-service:
    build: ./user-service
    ports:
      - "3001:3000"
    depends_on:
      - user-db
    environment:
      - DB_HOST=user-db
      - DB_PORT=5432
yaml  user-db:
    image: postgres:14
    environment:
      POSTGRES_DB: users
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: password
    volumes:
      - user_data:/var/lib/postgresql/data

volumes:
  user_data:

ネットワーク設計

Docker Composeでは、サービス間の通信を効率的に管理できます。

mermaidflowchart LR
    subgraph "Docker Network"
        gateway[API Gateway<br/>Port: 3000]
        user[User Service<br/>Port: 3001]
        product[Product Service<br/>Port: 3002]
        order[Order Service<br/>Port: 3003]
        
        gateway --> user
        gateway --> product
        gateway --> order
        order --> user
        order --> product
    end
    
    external[外部クライアント] --> gateway

この設計により、外部からは API Gateway のみがアクセス可能で、内部のサービス間通信はDocker内部ネットワークで保護されます。

データ管理戦略

マイクロサービスでは、各サービスが独自のデータベースを持つのが基本原則です。

Database per Service パターン

yaml# docker-compose.yml での DB分割例
  user-db:
    image: postgres:14
    environment:
      POSTGRES_DB: users

  product-db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: products

  order-db:
    image: mongodb:5.0
    environment:
      MONGO_INITDB_DATABASE: orders

各サービスが最適なデータベースを選択できるのも、マイクロサービスの利点です。

開発・運用プロセスの改善

CI/CDパイプライン構築

GitHubActionsを使用したCI/CDパイプラインの例をご紹介します。

yaml# .github/workflows/deploy.yml
name: Deploy Microservices

on:
  push:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
yaml    - name: Build and test user service
      run: |
        cd user-service
        docker build -t user-service:latest .
        docker run --rm user-service:latest yarn test

    - name: Build and test product service  
      run: |
        cd product-service
        docker build -t product-service:latest .
        docker run --rm product-service:latest yarn test

監視・ログ管理

マイクロサービスでは集約ログ管理が重要です。

yaml# docker-compose.yml にログ収集を追加
  user-service:
    build: ./user-service
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    labels:
      - "service=user-service"
      - "environment=production"

構造化ログの実装例:

javascript// user-service/src/logger.js
const winston = require('winston');

const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console()
  ]
});

module.exports = logger;

セキュリティ対策

コンテナレベルでのセキュリティ対策も重要です。

dockerfile# セキュリティを考慮したDockerfile
FROM node:18-alpine

# 非rootユーザーを作成
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# 必要最小限のパッケージのみインストール
RUN apk add --no-cache libc6-compat

USER nextjs

WORKDIR /app

具体例

サンプルアプリケーション構築

実際にECサイトを想定したマイクロサービスシステムを構築してみましょう。

ECサイトを想定した3つのマイクロサービス

以下の図で全体のアーキテクチャを確認しましょう。

mermaidflowchart TB
    subgraph "External"
        web[Web Frontend]
        mobile[Mobile App]
    end
    
    subgraph "API Layer"
        gateway[API Gateway<br/>Express.js]
    end
    
    subgraph "Microservices"
        user[User Service<br/>Node.js + Express]
        product[Product Service<br/>Node.js + Express]
        order[Order Service<br/>Node.js + Express]
    end
    
    subgraph "Data Layer"
        userdb[(User Database<br/>PostgreSQL)]
        productdb[(Product Database<br/>MySQL)]
        orderdb[(Order Database<br/>MongoDB)]
    end
    
    web --> gateway
    mobile --> gateway
    gateway --> user
    gateway --> product
    gateway --> order
    user --> userdb
    product --> productdb
    order --> orderdb
    order -.-> user
    order -.-> product

このアーキテクチャにより、各サービスが独立して開発・デプロイ可能となり、適切なデータベースを選択できます。

ユーザー管理サービス

まず、ユーザー管理サービスのDockerfileから作成しましょう。

dockerfile# user-service/Dockerfile
FROM node:18-alpine

WORKDIR /app

# パッケージファイルをコピー
COPY package*.json ./
RUN yarn install --frozen-lockfile

# アプリケーションコードをコピー
COPY src/ ./src/
COPY .env.example .env

EXPOSE 3000

# ヘルスチェック機能を追加
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["yarn", "start"]

ユーザーサービスのAPIエンドポイント実装例:

javascript// user-service/src/routes/users.js
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const router = express.Router();

// ユーザー登録
router.post('/register', async (req, res) => {
  try {
    const { email, password, name } = req.body;
    
    // パスワードをハッシュ化
    const hashedPassword = await bcrypt.hash(password, 10);
    
    const user = await User.create({
      email,
      password: hashedPassword,
      name
    });
    
    res.status(201).json({ 
      message: 'ユーザーが正常に作成されました',
      userId: user.id 
    });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

module.exports = router;

商品管理サービス

商品サービスのDockerfile:

dockerfile# product-service/Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN yarn install --frozen-lockfile

COPY src/ ./src/

EXPOSE 3000

CMD ["yarn", "start"]

商品情報を管理するAPI実装:

javascript// product-service/src/routes/products.js
const express = require('express');
const Product = require('../models/Product');

const router = express.Router();

// 商品一覧取得
router.get('/', async (req, res) => {
  try {
    const { page = 1, limit = 10, category } = req.query;
    
    const filter = category ? { category } : {};
    const products = await Product.find(filter)
      .limit(limit * 1)
      .skip((page - 1) * limit)
      .exec();
    
    res.json({
      products,
      currentPage: page,
      totalPages: Math.ceil(await Product.countDocuments(filter) / limit)
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

module.exports = router;

注文管理サービス

注文サービスでは他のサービスとの連携が重要です。

javascript// order-service/src/services/orderService.js
const axios = require('axios');
const Order = require('../models/Order');

class OrderService {
  async createOrder(userId, items) {
    try {
      // ユーザー情報を確認
      const userResponse = await axios.get(`http://user-service:3000/users/${userId}`);
      if (!userResponse.data) {
        throw new Error('ユーザーが見つかりません');
      }
      
      // 商品情報と在庫を確認
      const productChecks = await Promise.all(
        items.map(item => 
          axios.get(`http://product-service:3000/products/${item.productId}`)
        )
      );
      
      // 在庫確認
      productChecks.forEach((response, index) => {
        const product = response.data;
        if (product.stock < items[index].quantity) {
          throw new Error(`商品 ${product.name} の在庫が不足しています`);
        }
      });
      
      // 注文を作成
      const order = await Order.create({
        userId,
        items,
        status: 'pending',
        createdAt: new Date()
      });
      
      return order;
    } catch (error) {
      throw error;
    }
  }
}

module.exports = OrderService;

API Gateway実装

API Gatewayで各サービスへのルーティングを管理します。

javascript// api-gateway/src/app.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');

const app = express();

// レート制限を設定
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100 // リクエスト数上限
});

app.use(limiter);
app.use(express.json());

// ユーザーサービスへのプロキシ
app.use('/api/users', createProxyMiddleware({
  target: 'http://user-service:3000',
  changeOrigin: true,
  pathRewrite: {
    '^/api/users': '/users'
  }
}));

// 商品サービスへのプロキシ
app.use('/api/products', createProxyMiddleware({
  target: 'http://product-service:3000',
  changeOrigin: true,
  pathRewrite: {
    '^/api/products': '/products'
  }
}));

module.exports = app;

実装手順とコード例

Dockerfile作成

各サービス用のDockerfileの作成方法を詳しく見ていきましょう。

マルチステージビルドの活用

dockerfile# user-service/Dockerfile(本番用最適化版)
# ビルドステージ
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile

COPY src/ ./src/
RUN yarn build

# 本番ステージ
FROM node:18-alpine AS production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S appuser -u 1001

WORKDIR /app

COPY package*.json ./
RUN yarn install --production --frozen-lockfile

COPY --from=builder /app/dist ./dist
COPY --chown=appuser:nodejs . .

USER appuser

EXPOSE 3000
CMD ["node", "dist/index.js"]

docker-compose.yml設定

開発環境用と本番環境用のCompose設定例:

開発環境用(docker-compose.dev.yml)

yamlversion: '3.8'

services:
  api-gateway:
    build: 
      context: ./api-gateway
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - ./api-gateway/src:/app/src
    environment:
      - NODE_ENV=development
      - USER_SERVICE_URL=http://user-service:3000
      - PRODUCT_SERVICE_URL=http://product-service:3000
    depends_on:
      - user-service
      - product-service
      - order-service
yaml  user-service:
    build:
      context: ./user-service
      dockerfile: Dockerfile.dev
    ports:
      - "3001:3000"
    volumes:
      - ./user-service/src:/app/src
    environment:
      - NODE_ENV=development
      - DB_HOST=user-db
      - DB_PORT=5432
      - DB_NAME=users_dev
    depends_on:
      - user-db

本番環境用(docker-compose.prod.yml)

yamlversion: '3.8'

services:
  api-gateway:
    build:
      context: ./api-gateway
      dockerfile: Dockerfile
    ports:
      - "80:3000"
    environment:
      - NODE_ENV=production
      - USER_SERVICE_URL=http://user-service:3000
    restart: always
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

サービス間通信の実装

サービス間の通信にはHTTP REST APIを使用し、エラーハンドリングとリトライ機能を実装します。

javascript// shared/src/serviceClient.js
const axios = require('axios');

class ServiceClient {
  constructor(baseURL, retries = 3) {
    this.client = axios.create({
      baseURL,
      timeout: 5000,
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    this.retries = retries;
    
    // リクエストインターセプター
    this.client.interceptors.request.use(
      config => {
        console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
        return config;
      },
      error => Promise.reject(error)
    );
    
    // レスポンスインターセプター
    this.client.interceptors.response.use(
      response => response,
      async error => {
        const config = error.config;
        
        if (!config || !config.retry || config.retry >= this.retries) {
          return Promise.reject(error);
        }
        
        config.retry += 1;
        
        // 指数バックオフでリトライ
        const delay = Math.pow(2, config.retry) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        
        return this.client(config);
      }
    );
  }
  
  async get(url, config = {}) {
    config.retry = 0;
    const response = await this.client.get(url, config);
    return response.data;
  }
  
  async post(url, data, config = {}) {
    config.retry = 0;
    const response = await this.client.post(url, data, config);
    return response.data;
  }
}

module.exports = ServiceClient;

データベース分割

各サービスのデータベース接続とマイグレーション設定:

javascript// user-service/src/database/connection.js
const { Pool } = require('pg');

const pool = new Pool({
  user: process.env.DB_USER || 'admin',
  host: process.env.DB_HOST || 'user-db',
  database: process.env.DB_NAME || 'users',
  password: process.env.DB_PASSWORD || 'password',
  port: process.env.DB_PORT || 5432,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// ヘルスチェック
pool.on('connect', () => {
  console.log('PostgreSQL に接続しました');
});

pool.on('error', (err) => {
  console.error('予期しないPostgreSQLエラー:', err);
  process.exit(-1);
});

module.exports = pool;

マイグレーションファイルの例:

sql-- user-service/migrations/001_create_users_table.sql
CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  password VARCHAR(255) NOT NULL,
  name VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);

図で理解できる要点

  • 各マイクロサービスが独立したコンテナとして実行される
  • API Gatewayがサービス間の通信を仲介する
  • データベースもサービスごとに分離されている
  • Docker Composeで全体のオーケストレーションを管理する

まとめ

Dockerを活用したマイクロサービスアーキテクチャは、現代のWebアプリケーション開発において非常に有効なアプローチです。

本記事では、基本概念から具体的な実装まで幅広くご紹介しました。重要なポイントをおさらいしますと、以下の通りです。

技術的な利点

  • サービスの独立性により、部分的なスケーリングとデプロイが可能
  • Dockerコンテナによる環境の標準化と移植性の確保
  • 各サービスに最適な技術スタックの選択が可能

開発・運用面での改善

  • チーム間の独立した開発サイクルの実現
  • CI/CDパイプラインによる自動化の促進
  • 障害の影響範囲を限定的にすることでシステム全体の可用性向上

実装のポイント

  • Docker Composeによるローカルでのオーケストレーション
  • API Gatewayパターンによるサービス間通信の管理
  • Database per Serviceパターンによるデータの分離

マイクロサービスアーキテクチャの導入は、初期の複雑性は増しますが、長期的にはシステムの柔軟性と拡張性に大きな価値をもたらします。本記事で紹介した実装例を参考に、まずは小規模なサンプルから始めて、段階的に理解を深めていくことをお勧めいたします。

関連リンク