第9章:サーバーレスとコンテナサービス

9.1 サーバーレスコンピューティングの概念とメリット

サーバーレスという革命的パラダイム

サーバーレスコンピューティングは、その名前が示すような「サーバーがない」状態ではありません。サーバーの存在を開発者から完全に抽象化し、ビジネスロジックの実装に集中できる環境を提供する、インフラストラクチャ管理の革命的なアプローチです。

従来のアプローチからの解放

インフラストラクチャ管理の負担

従来のアプリケーション開発では、開発者は以下のような運用タスクに多大な時間を費やしていました。

従来の責任範囲:
  インフラ層:
    - サーバーのプロビジョニング
    - OSのパッチ適用
    - セキュリティアップデート
    - キャパシティプランニング
    
  スケーリング層:
    - 負荷分散の設計
    - オートスケーリングの設定
    - 可用性の確保
    - 障害時のフェイルオーバー
    
  運用層:
    - 監視システムの構築
    - ログ収集と分析
    - バックアップとリカバリ
    - コスト管理

サーバーレスは、これらすべての責任をクラウドプロバイダーに委譲し、開発者を運用の複雑性から解放します。

イベント駆動アーキテクチャの本質

リアクティブシステムの実現

サーバーレスの真の価値は、イベント駆動型の設計思想にあります。システムは外部イベントに反応し、必要な時にのみ処理を実行します。

# Lambda関数の例:S3イベントに反応する画像処理
import json
import boto3
from PIL import Image
import io

s3_client = boto3.client('s3')

def lambda_handler(event, context):
    """
    S3にアップロードされた画像を自動的にリサイズ
    """
    # イベントからバケット名とオブジェクトキーを取得
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']
        
        # 元画像をダウンロード
        response = s3_client.get_object(Bucket=bucket, Key=key)
        image_content = response['Body'].read()
        
        # 画像処理
        with Image.open(io.BytesIO(image_content)) as img:
            # サムネイル作成(150x150)
            thumbnail = img.copy()
            thumbnail.thumbnail((150, 150))
            
            # WebP形式で保存(高圧縮)
            output_buffer = io.BytesIO()
            thumbnail.save(output_buffer, format='WEBP', quality=85)
            output_buffer.seek(0)
            
            # 処理済み画像をS3に保存
            thumbnail_key = f"thumbnails/{key}"
            s3_client.put_object(
                Bucket=bucket,
                Key=thumbnail_key,
                Body=output_buffer,
                ContentType='image/webp'
            )
    
    return {
        'statusCode': 200,
        'body': json.dumps('Image processing completed')
    }

多様なイベントソース

イベントソースの分類:
  HTTPリクエスト:
    - API Gateway(REST/GraphQL)
    - Application Load Balancer
    - CloudFront(Lambda@Edge)
    
  メッセージングサービス:
    - SQS(Simple Queue Service)
    - SNS(Simple Notification Service)
    - EventBridge(カスタムイベント)
    - Kinesis(ストリーミングデータ)
    
  ストレージイベント:
    - S3(オブジェクト作成/削除)
    - DynamoDB Streams(データ変更)
    - EFS(ファイルシステムイベント)
    
  スケジュールイベント:
    - CloudWatch Events(cron式)
    - EventBridge Scheduler
    
  その他:
    - IoT Core(デバイスメッセージ)
    - Cognito(認証イベント)
    - Step Functions(ワークフロー)

Function as a Service (FaaS) の実行モデル

コールドスタートの理解と最適化

コールドスタートは、サーバーレスの特性上避けられない課題ですが、適切な対策により影響を最小化できます。

# コールドスタート最適化のテクニック

# 1. グローバル変数で接続を再利用
import os
import pymongo

# コールドスタート時のみ実行される
mongodb_client = None

def get_db_connection():
    global mongodb_client
    if mongodb_client is None:
        mongodb_client = pymongo.MongoClient(
            os.environ['MONGODB_URI'],
            maxPoolSize=1  # Lambda環境では接続プールを最小に
        )
    return mongodb_client

def lambda_handler(event, context):
    # ウォームスタート時は既存の接続を再利用
    db = get_db_connection()
    
    # ビジネスロジック
    result = db.mydb.mycollection.find_one({"_id": event['id']})
    
    return {
        'statusCode': 200,
        'body': json.dumps(result, default=str)
    }

# 2. 最小限の依存関係
# requirements.txt を最適化し、必要最小限のライブラリのみインストール

# 3. レイヤーの活用
# 共通ライブラリをLambda Layerとして分離

実行時間とメモリの最適化

Lambda設定の最適化戦略:
  メモリ割り当て:
    - 128MB〜10,240MB(10GB)の範囲
    - メモリに比例してCPU性能も向上
    - コスト vs パフォーマンスのバランス
    
  タイムアウト設定:
    - 最大15分(900秒)
    - 適切なタイムアウトで無駄なコストを防止
    - 非同期処理への分割を検討
    
  同時実行数:
    - デフォルト: 1,000
    - 予約同時実行数の設定
    - スロットリング対策

経済モデルの革新

真の従量課金の実現

# コスト計算の例
def calculate_lambda_cost(memory_mb, duration_ms, requests):
    """
    Lambda実行コストの計算(東京リージョン)
    """
    # 料金設定
    price_per_gb_second = 0.0000166667
    price_per_request = 0.0000002
    free_tier_gb_seconds = 400000
    free_tier_requests = 1000000
    
    # GB-秒の計算
    gb_seconds = (memory_mb / 1024) * (duration_ms / 1000) * requests
    billable_gb_seconds = max(0, gb_seconds - free_tier_gb_seconds)
    
    # リクエスト数の計算
    billable_requests = max(0, requests - free_tier_requests)
    
    # 総コスト
    compute_cost = billable_gb_seconds * price_per_gb_second
    request_cost = billable_requests * price_per_request
    total_cost = compute_cost + request_cost
    
    return {
        'compute_cost': compute_cost,
        'request_cost': request_cost,
        'total_cost': total_cost,
        'cost_per_request': total_cost / requests if requests > 0 else 0
    }

# 使用例:月間100万リクエスト、512MBメモリ、平均200ms
monthly_cost = calculate_lambda_cost(
    memory_mb=512,
    duration_ms=200,
    requests=1000000
)
print(f"月間コスト: ${monthly_cost['total_cost']:.2f}")

サーバーレスの適用パターン

理想的なユースケース

1. イベント処理パターン:
   画像/動画処理:
     trigger: S3アップロード
     process: リサイズ、フォーマット変換、メタデータ抽出
     output: 処理済みファイルをS3に保存
     
   データ変換ETL:
     trigger: 新規ファイル到着
     process: 検証、変換、集計
     output: データウェアハウスへロード
     
   通知システム:
     trigger: アプリケーションイベント
     process: 通知内容の生成
     output: Email/SMS/Push通知の送信

2. APIバックエンドパターン:
   RESTful API:
     - CRUD操作
     - 認証・認可
     - レート制限
     
   GraphQL:
     - スキーマ駆動開発
     - リゾルバーの実装
     - サブスクリプション

3. 定期処理パターン:
   バックアップ:
     schedule: "0 2 * * *"  # 毎日AM2:00
     process: DBスナップショット作成
     
   レポート生成:
     schedule: "0 9 * * MON"  # 毎週月曜AM9:00
     process: 週次レポート作成・配信

アンチパターンの認識

サーバーレスに適さないケース:
  長時間実行:
    - 15分を超える処理
    - 解決策: Step Functions、Batch、ECS
    
  高頻度・常時実行:
    - 秒間数千リクエスト以上
    - 解決策: コンテナ、EC2
    
  ステートフル処理:
    - WebSocketの長時間接続
    - 解決策: ECS、App Runner
    
  超低レイテンシ要求:
    - 10ms以下の応答時間
    - 解決策: EC2、Lambda@Edge

サーバーレスアーキテクチャの設計パターン

マイクロサービス分解

# API Gatewayと複数Lambda関数による構成
# serverless.yml (Serverless Framework)
service: ecommerce-api

provider:
  name: aws
  runtime: python3.9
  region: ap-northeast-1
  
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
    
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
          Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"

functions:
  # 商品管理API
  createProduct:
    handler: products.create
    events:
      - http:
          path: products
          method: post
          cors: true
          authorizer: aws_iam
          
  getProduct:
    handler: products.get
    events:
      - http:
          path: products/{id}
          method: get
          cors: true
          
  listProducts:
    handler: products.list
    events:
      - http:
          path: products
          method: get
          cors: true
          
  # 注文管理API
  createOrder:
    handler: orders.create
    events:
      - http:
          path: orders
          method: post
          cors: true
          authorizer: aws_iam
          
  processPayment:
    handler: payments.process
    events:
      - sqs:
          arn:
            Fn::GetAtt:
              - PaymentQueue
              - Arn
          batchSize: 10
          
resources:
  Resources:
    ProductsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5
          
    PaymentQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-payment-queue
        VisibilityTimeout: 60

9.2 コンテナ技術の基礎とDocker

コンテナ化がもたらす本質的価値

コンテナ技術は、「開発環境では動くのに本番では動かない」という長年の課題を根本的に解決しました。アプリケーションとその依存関係を一つのポータブルなユニットにパッケージ化することで、環境間の一貫性を保証します。

仮想化技術の進化と比較

ハードウェア仮想化からOS仮想化へ

仮想マシン(VM)のアーキテクチャ:
┌─────────────┬─────────────┬─────────────┐
│   App A     │   App B     │   App C     │
├─────────────┼─────────────┼─────────────┤
│  Guest OS   │  Guest OS   │  Guest OS   │
├─────────────┴─────────────┴─────────────┤
│            Hypervisor                    │
├─────────────────────────────────────────┤
│            Host OS                       │
├─────────────────────────────────────────┤
│            Hardware                      │
└─────────────────────────────────────────┘

コンテナのアーキテクチャ:
┌─────────────┬─────────────┬─────────────┐
│   App A     │   App B     │   App C     │
├─────────────┼─────────────┼─────────────┤
│ Container A │ Container B │ Container C │
├─────────────┴─────────────┴─────────────┤
│         Container Runtime                │
├─────────────────────────────────────────┤
│            Host OS                       │
├─────────────────────────────────────────┤
│            Hardware                      │
└─────────────────────────────────────────┘

性能とリソース効率の比較

# リソース使用量の比較
comparison = {
    "仮想マシン": {
        "起動時間": "30〜60秒",
        "メモリオーバーヘッド": "~1GB/インスタンス",
        "ディスク容量": "~10GB/インスタンス",
        "CPU効率": "80〜90%",
        "同時実行可能数": "10-20(標準的なホスト)"
    },
    "コンテナ": {
        "起動時間": "1〜2秒",
        "メモリオーバーヘッド": "~10MB/インスタンス",
        "ディスク容量": "~100MB/インスタンス",
        "CPU効率": "95〜98%",
        "同時実行可能数": "100-1000(標準的なホスト)"
    }
}

Dockerの基本概念と実装

レイヤー構造の深い理解

Dockerイメージのレイヤー構造は、効率的なストレージとネットワーク転送を実現します。

# 最適化されたDockerfile
# Stage 1: ビルド環境
FROM node:16-alpine AS builder

# 依存関係のキャッシュ効率を上げる
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# アプリケーションコードをコピー
COPY . .
RUN npm run build

# Stage 2: 実行環境(マルチステージビルド)
FROM node:16-alpine

# セキュリティ: 非rootユーザーの作成
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001

WORKDIR /app

# 必要最小限のファイルのみコピー
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./

# セキュリティ設定
USER nodejs
EXPOSE 3000

# ヘルスチェックの定義
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# PID 1問題を回避するためtiniを使用
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]

CMD ["node", "dist/server.js"]

ビルドキャッシュの最適化

# キャッシュを効果的に活用する順序
# 変更頻度: 低 → 高

# 1. ベースイメージ(めったに変更されない)
FROM python:3.9-slim

# 2. システムパッケージ(たまに変更)
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 3. Pythonパッケージ(時々変更)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 4. アプリケーションコード(頻繁に変更)
COPY . /app
WORKDIR /app

CMD ["python", "app.py"]

コンテナネットワーキングの詳細

ネットワークドライバーの選択と設計

# docker-compose.yml でのネットワーク設計
version: '3.8'

networks:
  frontend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/24
        
  backend:
    driver: bridge
    internal: true  # 外部アクセス不可
    ipam:
      config:
        - subnet: 172.21.0.0/24
        
  monitoring:
    driver: bridge
    ipam:
      config:
        - subnet: 172.22.0.0/24

services:
  nginx:
    image: nginx:alpine
    networks:
      - frontend
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - app
      
  app:
    build: ./app
    networks:
      - frontend
      - backend
    environment:
      - DB_HOST=postgres
      - REDIS_HOST=redis
    deploy:
      replicas: 3
      
  postgres:
    image: postgres:13
    networks:
      - backend
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    secrets:
      - db_password
      
  redis:
    image: redis:6-alpine
    networks:
      - backend
    command: redis-server --requirepass ${REDIS_PASSWORD}
    
  prometheus:
    image: prom/prometheus
    networks:
      - monitoring
      - frontend  # アプリケーションメトリクス収集用
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      
volumes:
  postgres_data:
    driver: local
    
secrets:
  db_password:
    file: ./secrets/db_password.txt

本番環境でのコンテナ運用

ログ管理戦略

# Fluentdを使用した統合ログ管理
# fluent.conf
<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>

# Dockerコンテナログの収集
<source>
  @type tail
  path /var/lib/docker/containers/*/*-json.log
  pos_file /var/log/fluentd-docker.pos
  tag docker.*
  format json
  time_key time
  time_format %Y-%m-%dT%H:%M:%S.%NZ
</source>

# ログの構造化と濃縮
<filter docker.**>
  @type parser
  key_name log
  <parse>
    @type json
  </parse>
</filter>

# メタデータの追加
<filter docker.**>
  @type record_transformer
  <record>
    hostname ${hostname}
    environment ${ENV['ENVIRONMENT']}
    service ${tag_parts[1]}
  </record>
</filter>

# 出力先の設定
<match docker.**>
  @type elasticsearch
  host elasticsearch
  port 9200
  logstash_format true
  logstash_prefix docker
  <buffer>
    @type file
    path /var/log/fluentd-buffers/docker.buffer
    flush_mode interval
    flush_interval 10s
  </buffer>
</match>

セキュリティスキャンの自動化

# CI/CDパイプラインでのセキュリティチェック
# .gitlab-ci.yml
stages:
  - build
  - scan
  - test
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

build:
  stage: build
  script:
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

security_scan:
  stage: scan
  image: aquasec/trivy:latest
  script:
    # 脆弱性スキャン
    - trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGE_TAG
    # ライセンススキャン
    - trivy image --license-scan --exit-code 1 $IMAGE_TAG
  allow_failure: false

dockerfile_lint:
  stage: scan
  image: hadolint/hadolint:latest
  script:
    - hadolint Dockerfile

secrets_scan:
  stage: scan
  image: trufflesecurity/trufflehog:latest
  script:
    - trufflehog docker --image=$IMAGE_TAG

9.3 コンテナオーケストレーション(ECS, EKS, GKE, AKS)

オーケストレーションの必然性

単一のコンテナ管理は簡単ですが、数百、数千のコンテナを本番環境で運用するには、高度な管理システムが必要です。

オーケストレーターが解決する課題

コンテナ管理の複雑性:
  配置とスケジューリング:
    - 適切なホストの選択
    - リソース制約の考慮
    - アフィニティ/アンチアフィニティ
    
  可用性の確保:
    - 自動的な再起動
    - ヘルスチェック
    - ローリングアップデート
    - ロールバック
    
  スケーリング:
    - 水平スケーリング
    - 垂直スケーリング
    - オートスケーリング
    
  ネットワーキング:
    - サービスディスカバリ
    - ロードバランシング
    - サービスメッシュ
    
  状態管理:
    - 永続ボリューム
    - ConfigMap/Secrets
    - StatefulSets

Kubernetes:デファクトスタンダード

Kubernetesのアーキテクチャ

# Kubernetes デプロイメント例
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: production
  labels:
    app: web-app
    version: v1.0.0
spec:
  replicas: 3
  revisionHistoryLimit: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
        version: v1.0.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/metrics"
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-app
            topologyKey: kubernetes.io/hostname
      containers:
      - name: web-app
        image: myregistry/web-app:v1.0.0
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: log.level
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          successThreshold: 1
        volumeMounts:
        - name: app-config
          mountPath: /etc/config
          readOnly: true
        - name: temp-storage
          mountPath: /tmp
        securityContext:
          runAsNonRoot: true
          runAsUser: 1000
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
      volumes:
      - name: app-config
        configMap:
          name: app-config
      - name: temp-storage
        emptyDir: {}
      serviceAccountName: web-app-sa
      securityContext:
        fsGroup: 2000
---
apiVersion: v1
kind: Service
metadata:
  name: web-app-service
  namespace: production
spec:
  type: ClusterIP
  selector:
    app: web-app
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
    name: http
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
      - type: Pods
        value: 4
        periodSeconds: 15
      selectPolicy: Max

マネージドKubernetesサービスの比較

Amazon EKS(Elastic Kubernetes Service)

# EKS特有の機能活用
# ALB Ingress Controller
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app-ingress
  namespace: production
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:123456789012:certificate/xxx
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
    alb.ingress.kubernetes.io/ssl-redirect: '443'
    alb.ingress.kubernetes.io/healthcheck-path: /health
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: '15'
    alb.ingress.kubernetes.io/healthcheck-timeout-seconds: '5'
    alb.ingress.kubernetes.io/healthy-threshold-count: '2'
    alb.ingress.kubernetes.io/unhealthy-threshold-count: '2'
spec:
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /*
        pathType: ImplementationSpecific
        backend:
          service:
            name: web-app-service
            port:
              number: 80

---
# IRSA (IAM Roles for Service Accounts)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: web-app-sa
  namespace: production
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/WebAppRole

Google GKE(Google Kubernetes Engine)

# GKE Autopilot モード設定
apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: production
spec:
  hard:
    requests.cpu: "1000"
    requests.memory: 1000Gi
    persistentvolumeclaims: "10"
    
---
# Workload Identity
apiVersion: v1
kind: ServiceAccount
metadata:
  name: web-app-sa
  namespace: production
  annotations:
    iam.gke.io/gcp-service-account: web-app@project-id.iam.gserviceaccount.com
    
---
# GKE Ingress with Cloud Load Balancer
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app-ingress
  namespace: production
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "web-app-ip"
    networking.gke.io/managed-certificates: "web-app-cert"
    kubernetes.io/ingress.class: "gce"
spec:
  defaultBackend:
    service:
      name: web-app-service
      port:
        number: 80

Amazon ECS:シンプルさを追求

タスク定義によるコンテナ管理

{
  "family": "web-app",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "taskRoleArn": "arn:aws:iam::123456789012:role/WebAppTaskRole",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "web-app",
      "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/web-app:latest",
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "environment": [
        {
          "name": "ENVIRONMENT",
          "value": "production"
        }
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:db-url-xxxxx"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/web-app",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      },
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      }
    }
  ]
}

ECSサービスの設定

# CDKによるECSサービス定義
from aws_cdk import (
    aws_ecs as ecs,
    aws_ec2 as ec2,
    aws_elasticloadbalancingv2 as elbv2,
    aws_logs as logs,
    aws_autoscaling as autoscaling,
    core
)

class EcsServiceStack(core.Stack):
    def __init__(self, scope: core.Construct, id: str, **kwargs):
        super().__init__(scope, id, **kwargs)
        
        # VPCの作成
        vpc = ec2.Vpc(
            self, "ServiceVpc",
            max_azs=2,
            nat_gateways=2
        )
        
        # ECSクラスター
        cluster = ecs.Cluster(
            self, "Cluster",
            vpc=vpc,
            container_insights=True
        )
        
        # Fargateサービス
        task_definition = ecs.FargateTaskDefinition(
            self, "TaskDef",
            memory_limit_mib=1024,
            cpu=512
        )
        
        container = task_definition.add_container(
            "WebContainer",
            image=ecs.ContainerImage.from_registry("myapp:latest"),
            logging=ecs.LogDrivers.aws_logs(
                stream_prefix="ecs",
                log_retention=logs.RetentionDays.ONE_WEEK
            ),
            environment={
                "ENVIRONMENT": "production"
            }
        )
        
        container.add_port_mappings(
            ecs.PortMapping(
                container_port=8080,
                protocol=ecs.Protocol.TCP
            )
        )
        
        # ALB
        lb = elbv2.ApplicationLoadBalancer(
            self, "ALB",
            vpc=vpc,
            internet_facing=True
        )
        
        # Fargateサービス
        service = ecs.FargateService(
            self, "Service",
            cluster=cluster,
            task_definition=task_definition,
            desired_count=3,
            assign_public_ip=False,
            service_name="web-app"
        )
        
        # オートスケーリング
        scaling = service.auto_scale_task_count(
            min_capacity=3,
            max_capacity=10
        )
        
        scaling.scale_on_cpu_utilization(
            "CpuScaling",
            target_utilization_percent=70
        )
        
        scaling.scale_on_request_count(
            "RequestScaling",
            requests_per_target=1000,
            target_group=target_group
        )

9.4 コンテナレジストリとCI/CD連携

コンテナレジストリの戦略的活用

コンテナレジストリは、コンテナイメージのライフサイクル管理において中心的な役割を果たします。

マルチステージレジストリ戦略

# レジストリ構成
registries:
  development:
    url: dev-registry.company.com
    retention: 7days
    scan: on-push
    
  staging:
    url: staging-registry.company.com
    retention: 30days
    scan: daily
    approval: automated
    
  production:
    url: prod-registry.company.com
    retention: 1year
    scan: continuous
    approval: manual
    signing: required

Amazon ECR(Elastic Container Registry)

ライフサイクルポリシーによる自動管理

{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Keep last 10 production images",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["prod-"],
        "countType": "imageCountMoreThan",
        "countNumber": 10
      },
      "action": {
        "type": "expire"
      }
    },
    {
      "rulePriority": 2,
      "description": "Expire untagged images after 1 day",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 1
      },
      "action": {
        "type": "expire"
      }
    },
    {
      "rulePriority": 3,
      "description": "Keep only 3 dev images",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["dev-"],
        "countType": "imageCountMoreThan",
        "countNumber": 3
      },
      "action": {
        "type": "expire"
      }
    }
  ]
}

脆弱性スキャンの自動化

# ECRスキャン結果の監視
import boto3
import json
from datetime import datetime

ecr = boto3.client('ecr')

def lambda_handler(event, context):
    """
    ECRスキャン完了時に呼び出され、脆弱性を評価
    """
    detail = event['detail']
    repository_name = detail['repository-name']
    image_digest = detail['image-digest']
    
    # スキャン結果を取得
    response = ecr.describe_image_scan_findings(
        repositoryName=repository_name,
        imageId={'imageDigest': image_digest}
    )
    
    findings = response['imageScanFindings']
    severity_counts = findings.get('findingSeverityCounts', {})
    
    # 重大度別の脆弱性数
    critical = severity_counts.get('CRITICAL', 0)
    high = severity_counts.get('HIGH', 0)
    medium = severity_counts.get('MEDIUM', 0)
    low = severity_counts.get('LOW', 0)
    
    # ポリシーチェック
    if critical > 0:
        # 本番環境へのデプロイをブロック
        send_alert(
            f"CRITICAL: {repository_name} has {critical} critical vulnerabilities",
            severity="critical"
        )
        tag_image_as_unsafe(repository_name, image_digest)
        
    elif high > 5:
        # 警告を発行
        send_alert(
            f"WARNING: {repository_name} has {high} high vulnerabilities",
            severity="warning"
        )
        
    # 詳細レポートの生成
    generate_vulnerability_report(repository_name, findings)
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'repository': repository_name,
            'critical': critical,
            'high': high,
            'medium': medium,
            'low': low
        })
    }

包括的なCI/CDパイプライン

GitHub Actions による完全自動化

# .github/workflows/container-pipeline.yml
name: Container CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com
  REPOSITORY: web-app

jobs:
  # コード品質チェック
  code-quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@master
        env:
          GITHUB_TOKEN: ``${{ secrets.GITHUB_TOKEN }}``
          SONAR_TOKEN: ``${{ secrets.SONAR_TOKEN }}``
          
      - name: Check Quality Gate
        uses: sonarsource/sonarqube-quality-gate-action@master
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ``${{ secrets.SONAR_TOKEN }}``

  # コンテナビルドとスキャン
  build-and-scan:
    needs: code-quality
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      
    steps:
      - uses: actions/checkout@v3
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ``${{ secrets.AWS_ROLE_ARN }}``
          aws-region: ap-northeast-1
          
      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
        
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
        
      - name: Build Image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: false
          tags: ``${{ env.REGISTRY }}``/``${{ env.REPOSITORY }}``:``${{ github.sha }}``
          cache-from: type=gha
          cache-to: type=gha,mode=max
          outputs: type=docker,dest=/tmp/image.tar
          
      - name: Run Trivy Scanner
        uses: aquasecurity/trivy-action@master
        with:
          input: /tmp/image.tar
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          
      - name: Upload Trivy Results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'
          
      - name: Run Snyk Container Test
        run: |
          docker load -i /tmp/image.tar
          snyk container test ``${{ env.REGISTRY }}``/``${{ env.REPOSITORY }}``:``${{ github.sha }}`` \
            --severity-threshold=high \
            --file=Dockerfile

  # パフォーマンステスト
  performance-test:
    needs: build-and-scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run Load Tests
        uses: grafana/k6-action@v0.3.0
        with:
          filename: tests/load-test.js
          
      - name: Upload Results
        uses: actions/upload-artifact@v3
        with:
          name: k6-results
          path: results.json

  # プロダクション展開
  deploy-production:
    needs: [build-and-scan, performance-test]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ``${{ secrets.AWS_PROD_ROLE_ARN }}``
          aws-region: ap-northeast-1
          
      - name: Push to ECR
        run: |
          docker load -i /tmp/image.tar
          docker tag ``${{ env.REGISTRY }}``/``${{ env.REPOSITORY }}``:``${{ github.sha }}`` \
                     ``${{ env.REGISTRY }}``/``${{ env.REPOSITORY }}``:production
          docker push ``${{ env.REGISTRY }}``/``${{ env.REPOSITORY }}``:production
          
      - name: Update ECS Service
        run: |
          aws ecs update-service \
            --cluster production \
            --service web-app \
            --force-new-deployment
            
      - name: Wait for Deployment
        run: |
          aws ecs wait services-stable \
            --cluster production \
            --services web-app
            
      - name: Run Smoke Tests
        run: |
          npm install
          npm run test:smoke -- --env=production

イメージ管理のベストプラクティス

セマンティックバージョニングとタグ戦略

# 自動タグ生成スクリプト
import re
import subprocess
from datetime import datetime

def generate_tags(branch, commit_sha, existing_tags):
    """
    ブランチとコミット情報から適切なタグを生成
    """
    tags = []
    
    # 基本タグ
    tags.append(commit_sha[:8])
    tags.append(f"{branch}-{commit_sha[:8]}")
    
    # ブランチ別タグ戦略
    if branch == "main":
        # セマンティックバージョン
        version = get_next_version(existing_tags)
        tags.extend([
            version,
            "latest",
            f"v{version}"
        ])
    elif branch == "develop":
        # 開発版タグ
        timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
        tags.extend([
            "develop",
            f"develop-{timestamp}"
        ])
    elif branch.startswith("release/"):
        # リリース候補
        version = branch.split("/")[1]
        tags.extend([
            f"rc-{version}",
            f"rc-{version}-{commit_sha[:8]}"
        ])
    
    return tags

def get_next_version(existing_tags):
    """
    既存のタグから次のバージョンを決定
    """
    versions = []
    for tag in existing_tags:
        match = re.match(r'^v?(\d+)\.(\d+)\.(\d+)$', tag)
        if match:
            versions.append((
                int(match.group(1)),
                int(match.group(2)),
                int(match.group(3))
            ))
    
    if not versions:
        return "1.0.0"
    
    # 最新バージョンを取得
    latest = max(versions)
    
    # パッチバージョンをインクリメント
    return f"{latest[0]}.{latest[1]}.{latest[2] + 1}"

コンテナのサプライチェーンセキュリティ

イメージ署名とポリシー

# Notary/DCTによるイメージ署名
# docker-compose.yml
version: '3.8'

services:
  notary-server:
    image: notary:server
    environment:
      - NOTARY_SERVER_DB_URL=mysql://notary@mysql:3306/notaryserver
    depends_on:
      - mysql
      
  notary-signer:
    image: notary:signer
    environment:
      - NOTARY_SIGNER_DB_URL=mysql://notary@mysql:3306/notarysigner
      
  mysql:
    image: mysql:5.7
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
    volumes:
      - notary-data:/var/lib/mysql

# 署名ポリシー
admission-policy:
  rules:
    - name: require-signature
      match:
        namespaces: ["production"]
      require:
        - signature:
            keys:
              - keyless:
                  issuer: https://token.actions.githubusercontent.com
                  subject: repo:myorg/myrepo:ref:refs/heads/main

サーバーレスとコンテナは、それぞれ異なる強みを持つクラウドネイティブ技術です。サーバーレスはイベント駆動で断続的なワークロードに最適であり、コンテナは複雑なアプリケーションや継続的な処理に適しています。重要なのは、それぞれの特性を理解し、適切なワークロードに適切な技術を選択することです。両者を組み合わせることで、スケーラブルで効率的、かつ管理しやすいクラウドネイティブアーキテクチャを実現できます。

第10章へ進む