第10章:ログ管理とトラブルシューティング
本章の意義と学習目標
なぜPodmanでCI/CDを構築する価値があるのか
従来のDockerベースのCI/CDには、セキュリティとスケーラビリティの課題がありました:
- セキュリティ向上: Docker-in-Docker不要、Rootless実行可能
- パイプライン効率化: デーモンレスによる並列実行の向上
- Kubernetes準備: 開発環境から本番まで一貫したワークフロー
- コスト削減: ライセンスコスト不要、リソース効率向上
本章では、セキュアで効率的なCI/CDパイプラインの構築方法を学びます。
10.1 CI/CD概要とPodman統合
なぜPodmanがCI/CDに適しているのか
CI/CD環境は特に攻撃対象となりやすく、以下の要件が重要です:
- 最小権限での実行: ビルドプロセスに不要な権限を与えない
- 環境の分離: ビルド間の完全な分離
- 再現性: 同じ結果を保証する決定論的なビルド
- 効率性: 高速なビルドとデプロイ
10.1.1 CI/CDパイプラインの構成要素
ソースコード → ビルド → テスト → イメージ作成 → レジストリ → デプロイ
↑ ↓
└──────────────── フィードバック ────────────────────┘
Podmanの各段階での利点
- ビルド段階
- Rootlessビルドによるセキュリティ
- BuildahによるDockerfile不要のビルド
- 並列ビルドの効率化
- テスト段階
- 分離された環境での安全なテスト実行
- システムテストでのPod活用
- リソース制限による安定性
- イメージ作成
- マルチアーキテクチャビルド
- イメージ署名による信頼性
- 最小権限でのプッシュ
- デプロイ段階
- Kubernetes YAMLの自動生成
- systemd統合による確実なデプロイ
- ロールバック機能
10.2 GitLab CI/CDとの統合
なぜGitLab Runnerの設定が重要なのか
RunnerはCI/CDの実行環境であり、セキュリティと効率の要です:
10.2.1 GitLab Runner設定
# Podman対応GitLab Runner設定
cat > /etc/gitlab-runner/config.toml << EOF
[[runners]]
name = "podman-runner"
url = "https://gitlab.example.com"
token = "REGISTRATION_TOKEN"
executor = "docker" # Dockerと互換性あり
[runners.docker]
tls_verify = false
image = "alpine:latest"
privileged = false # 特権不要!(Dockerとの違い)
disable_cache = false
volumes = ["/cache"]
runtime = "podman" # Podmanランタイム使用
helper_image = "gitlab/gitlab-runner-helper:x86_64-latest"
EOF
# なぜこの設定が優れているか:
# - privileged=false: セキュリティリスクの排除
# - Podmanランタイム: Rootless実行可能
# - キャッシュ有効: ビルド時間短縮
10.2.2 .gitlab-ci.yml例
実践的なパイプラインとその価値
# .gitlab-ci.yml
variables:
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
CONTAINER_RELEASE: $CI_REGISTRY_IMAGE:latest
stages:
- build
- test
- security
- publish
- deploy
before_script:
- podman version
- podman login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
# ビルドステージ - なぜ効率的か
build:
stage: build
script:
# キャッシュ活用で高速化
- podman build
--cache-from $CONTAINER_RELEASE # 前回のビルドを活用
--tag $CONTAINER_IMAGE
--tag $CONTAINER_RELEASE
--file Containerfile .
- podman push $CONTAINER_IMAGE
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
# テストステージ - 包括的な品質保証
test:unit:
stage: test
script:
- podman run --rm $CONTAINER_IMAGE npm test
coverage: '/Coverage: \d+\.\d+%/' # カバレッジ自動取得
test:integration:
stage: test
services:
- name: postgres:13
alias: db
variables:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
script:
# サービスコンテナとの統合テスト
- podman run --rm
--network host # CIでのサービス接続
-e DATABASE_URL=postgresql://test:test@db/test
$CONTAINER_IMAGE
npm run test:integration
# セキュリティステージ - なぜ必須か
security:scan:
stage: security
script:
# 脆弱性スキャン
- podman run --rm
-v /var/run/podman/podman.sock:/var/run/docker.sock
aquasec/trivy image
--severity HIGH,CRITICAL
--exit-code 1 # 重大な脆弱性で失敗
$CONTAINER_IMAGE
# この段階でのブロックにより:
# - 脆弱なイメージの本番投入を防止
# - コンプライアンス要件の充足
# - セキュリティインシデントの予防
security:secrets:
stage: security
script:
# シークレットスキャン
- podman run --rm
-v $(pwd):/src
trufflesecurity/trufflehog:latest
filesystem /src
--json
--fail # シークレット検出で失敗
# APIキーやパスワードの誤コミットを防止
# パブリッシュステージ - 信頼性の確保
publish:
stage: publish
only:
- main # mainブランチのみ
script:
- podman pull $CONTAINER_IMAGE
- podman tag $CONTAINER_IMAGE $CONTAINER_RELEASE
- podman push $CONTAINER_RELEASE
# 署名付きプッシュ - なぜ重要か
- podman push --sign-by $GPG_KEY_ID $CONTAINER_RELEASE
# イメージの改ざん防止
# 本番環境での信頼性保証
# デプロイステージ - 段階的なロールアウト
deploy:staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
only:
- main
script:
# ステージング環境へのデプロイ
- |
podman run --rm
-v $SSH_KEY:/root/.ssh/id_rsa:ro
-e HOST=$STAGING_HOST
alpine/ansible:latest
ansible-playbook -i $HOST, deploy.yml
deploy:production:
stage: deploy
environment:
name: production
url: https://example.com
when: manual # 手動承認
only:
- main
script:
# Blue-Greenデプロイメント
- kubectl set image deployment/app app=$CONTAINER_RELEASE
- kubectl rollout status deployment/app
# 自動ロールバック機能付き
このパイプラインが実現する価値
- 品質保証: 自動テストによる欠陥の早期発見
- セキュリティ: 脆弱性の自動検出とブロック
- 効率性: キャッシュとRootless実行による高速化
- 信頼性: 署名による改ざん防止
- 安全性: 段階的デプロイとロールバック
10.3 GitHub Actionsとの統合
なぜGitHub Actionsが人気なのか
GitHub Actionsは、以下の理由で多くの開発チームに選ばれています:
- 統合性: コードと同じリポジトリでCI/CD管理
- 無料枠: オープンソースプロジェクトは無料
- マーケットプレイス: 豊富な再利用可能アクション
- セキュリティ: GitHub のセキュリティ機能との統合
10.3.1 GitHub Actions設定
実践的なワークフローとその解説
# .github/workflows/ci-cd.yml
name: Build and Deploy
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: $
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write # セキュリティスキャン結果の書き込み
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Podman
run: |
# なぜPodmanを選ぶか:
# - Dockerデーモン不要(GitHub Actionsでより安全)
# - Rootless実行可能
# - 同じDockerfileが使える
sudo apt-get update
sudo apt-get -y install podman
podman version
- name: Log in to Container Registry
run: |
# GitHub Container Registry使用の利点:
# - GitHubとの完全統合
# - きめ細かいアクセス制御
# - パッケージとコードの紐付け
echo "$" | podman login $ -u $ --password-stdin
- name: Build image
run: |
# ビルドコンテキストの最適化
# GitHub Actions特有の環境変数を活用
podman build . \
--file Containerfile \
--tag $:$ \
--build-arg BUILD_SHA=$ \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- name: Run tests
run: |
# コンテナ内でのテスト実行
# なぜコンテナ内か:本番と同じ環境を保証
podman run --rm $:$ npm test
- name: Security scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: $:$
format: 'sarif' # GitHub Security タブ統合
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
# GitHub Security タブでの可視化
# プルリクエストへの自動コメント
- name: Push image
if: github.event_name != 'pull_request'
run: |
# プルリクエストではプッシュしない(セキュリティ)
podman tag $:$ $/$:$
podman tag $:$ $/$:latest
podman push $/$:$
podman push $/$:latest
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # mainブランチのみ
steps:
- name: Deploy to Kubernetes
env:
KUBE_CONFIG: $
run: |
# Kubernetesへの安全なデプロイ
echo "$KUBE_CONFIG" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
# デプロイメントの更新
kubectl set image deployment/app app=$/$:$
# ロールアウト待機(タイムアウト付き)
kubectl rollout status deployment/app --timeout=300s
このワークフローが実現する価値
- セキュリティファースト
- GITHUB_TOKENによる自動認証
- セキュリティスキャン結果の可視化
- プルリクエストでの自動チェック
- 開発効率
- プルリクエストごとの自動ビルド・テスト
- マージ前の問題検出
- レビュープロセスの効率化
- 透明性
- すべてのビルドログが公開(パブリックリポジトリ)
- セキュリティ問題の早期発見
- コミュニティからのフィードバック
10.4 Jenkinsとの統合
なぜ既存のJenkins環境でPodmanを使うのか
多くの企業では既にJenkinsが稼働しており、以下の理由でPodman統合が価値を生みます:
- 既存資産の活用: Jenkins の豊富なプラグインエコシステム
- セキュリティ強化: Docker から Podman への段階的移行
- スケーラビリティ: エージェントでの Rootless 実行
10.4.1 Jenkinsfile例
// Jenkinsfile
pipeline {
agent {
label 'podman' // Podman対応エージェント
}
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = "${env.JOB_NAME}".toLowerCase()
IMAGE_TAG = "${env.BUILD_NUMBER}"
PODMAN_ARGS = '--storage-driver=vfs' // Jenkins環境での安定性
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
script {
sh """
# なぜ storage-driver=vfs か:
# Jenkinsのワークスペース環境では
# overlayfsが使えない場合があるため
podman build ${PODMAN_ARGS} \
--tag ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
--tag ${REGISTRY}/${IMAGE_NAME}:latest \
--file Containerfile .
"""
}
}
}
stage('Test') {
parallel { // 並列実行で時間短縮
stage('Unit Tests') {
steps {
sh """
podman run ${PODMAN_ARGS} --rm \
${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
npm test
"""
}
}
stage('Integration Tests') {
steps {
sh """
# docker-compose互換のpodman-compose使用
podman-compose -f docker-compose.test.yml up \
--abort-on-container-exit \
--exit-code-from app
"""
}
}
stage('Security Scan') {
steps {
sh """
# Jenkinsでのセキュリティスキャン統合
podman run ${PODMAN_ARGS} --rm \
-v /var/run/podman/podman.sock:/var/run/docker.sock \
aquasec/trivy image \
--severity HIGH,CRITICAL \
${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
}
}
stage('Publish') {
when {
branch 'main'
}
steps {
withCredentials([usernamePassword(
credentialsId: 'registry-creds',
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PASS'
)]) {
sh """
echo \$REGISTRY_PASS | podman login \
-u \$REGISTRY_USER \
--password-stdin \
${REGISTRY}
podman push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
podman push ${REGISTRY}/${IMAGE_NAME}:latest
"""
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
script {
// 環境別デプロイメント
def environments = ['staging', 'production']
def deployments = environments.collectEntries { env ->
["${env}" : {
stage("Deploy to ${env}") {
if (env == 'production') {
input message: 'Deploy to production?'
}
sh """
ansible-playbook \
-i inventory/${env} \
-e image_tag=${IMAGE_TAG} \
deploy.yml
"""
}
}]
}
parallel deployments
}
}
}
}
post {
always {
// クリーンアップ - なぜ重要か
sh "podman system prune -f"
// ディスク容量の枯渴防止
// 次回ビルドの高速化
}
success {
slackSend(
color: 'good',
message: "Build Successful: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
color: 'danger',
message: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
10.5 自動テスト戦略
なぜコンテナベースのテストが優れているのか
従来のテスト環境の問題:
- 環境依存による「開発環境では動いた」問題
- テスト環境の準備・クリーンアップの複雑さ
- 並列実行時の干渉
コンテナベーステストの解決策:
- 完全に隔離された環境
- 使い捨て可能な環境
- 本番と同一の環境
10.5.1 テストコンテナパターン
# test_with_containers.py
import pytest
import podman
from testcontainers.postgres import PostgresContainer
from testcontainers.redis import RedisContainer
@pytest.fixture(scope="session")
def postgres():
"""PostgreSQL テストコンテナ
なぜテストコンテナを使うか:
- 各テストで新しいDBインスタンス
- スキーマ変更の影響を受けない
- 並列テストが可能
"""
with PostgresContainer("postgres:13") as postgres:
yield postgres
@pytest.fixture(scope="session")
def redis():
"""Redis テストコンテナ"""
with RedisContainer("redis:6-alpine") as redis:
yield redis
def test_database_connection(postgres):
"""データベース接続テスト
このアプローチの利点:
- 実際のPostgreSQLを使用(モックではない)
- 他のテストから完全に隔離
- CI環境でも同じように動作
"""
connection_url = postgres.get_connection_url()
# 実際のデータベース操作
assert connection_url is not None
def test_cache_operations(redis):
"""キャッシュ操作テスト"""
redis_client = redis.get_client()
redis_client.set("key", "value")
assert redis_client.get("key") == b"value"
def test_full_stack_integration(postgres, redis):
"""フルスタック統合テスト
複数のサービスを組み合わせたテスト
本番環境と同じ構成を再現
"""
# アプリケーションコンテナ起動
app_container = podman.from_env().containers.run(
"myapp:latest",
environment={
"DATABASE_URL": postgres.get_connection_url(),
"REDIS_URL": f"redis://{redis.get_container_host_ip()}:{redis.get_exposed_port(6379)}"
},
detach=True
)
try:
# テスト実行
response = requests.get("http://localhost:8080/health")
assert response.status_code == 200
finally:
app_container.stop()
app_container.remove()
10.5.2 E2Eテスト
なぜE2Eテストが重要なのか
単体テストでは検出できない、システム全体の統合問題を発見できます:
- API間の連携不具合
- パフォーマンス問題
- ユーザー視点での動作確認
// e2e-test.js
const { chromium } = require('playwright');
const { GenericContainer } = require('testcontainers');
describe('E2E Tests', () => {
let container;
let browser;
beforeAll(async () => {
// アプリケーションコンテナ起動
// なぜコンテナ化されたE2Eテストが優れているか:
// - 環境依存なし
// - 並列実行可能
// - CI/CD環境での再現性
container = await new GenericContainer('myapp:latest')
.withExposedPorts(3000)
.start();
browser = await chromium.launch();
});
afterAll(async () => {
await browser.close();
await container.stop();
});
test('Homepage loads correctly', async () => {
const page = await browser.newPage();
const url = `http://localhost:${container.getMappedPort(3000)}`;
await page.goto(url);
const title = await page.title();
expect(title).toBe('My Application');
});
test('User journey - complete purchase', async () => {
// 実際のユーザー行動をシミュレート
const page = await browser.newPage();
const baseUrl = `http://localhost:${container.getMappedPort(3000)}`;
// 1. 商品検索
await page.goto(`${baseUrl}/products`);
await page.fill('[data-testid="search"]', 'laptop');
await page.click('[data-testid="search-button"]');
// 2. 商品選択
await page.click('[data-testid="product-1"]');
await page.click('[data-testid="add-to-cart"]');
// 3. チェックアウト
await page.goto(`${baseUrl}/checkout`);
await page.fill('[data-testid="email"]', 'test@example.com');
await page.click('[data-testid="place-order"]');
// 4. 確認
await page.waitForSelector('[data-testid="order-confirmation"]');
const confirmationText = await page.textContent('[data-testid="order-number"]');
expect(confirmationText).toMatch(/ORDER-\d+/);
});
});
10.6 デプロイメント戦略
なぜ高度なデプロイメント戦略が必要なのか
単純な「停止→更新→起動」では、以下の問題があります:
- サービス停止によるユーザー影響
- 問題発生時の復旧困難
- 段階的な検証不可
高度な戦略により、リスクを最小化しながら迅速なデプロイを実現します。
10.6.1 Blue-Green Deployment
Blue-Greenの価値
- ゼロダウンタイムデプロイ
- 即座のロールバック可能
- 本番環境での検証
#!/bin/bash
# blue-green-deploy.sh
REGISTRY="registry.example.com"
APP_NAME="myapp"
NEW_VERSION=$1
# なぜ環境を色で識別するのか:
# - シンプルで理解しやすい
# - 人為的ミスの防止
# - 視覚的な状態管理
# 現在の環境を確認
CURRENT_ENV=$(podman ps --filter "label=app=$APP_NAME" --format "\{\{.Labels.environment\}\}")
if [ "$CURRENT_ENV" = "blue" ]; then
NEW_ENV="green"
else
NEW_ENV="blue"
fi
echo "Deploying to $NEW_ENV environment"
# 新環境にデプロイ
podman run -d \
--name ${APP_NAME}-${NEW_ENV} \
--label app=$APP_NAME \
--label environment=$NEW_ENV \
--label version=$NEW_VERSION \
--network production \
${REGISTRY}/${APP_NAME}:${NEW_VERSION}
# ヘルスチェック - なぜ重要か
echo "Running health checks..."
for i in {1..30}; do
if podman exec ${APP_NAME}-${NEW_ENV} curl -f http://localhost/health; then
echo "Health check passed"
break
fi
if [ $i -eq 30 ]; then
echo "Health check failed, aborting deployment"
podman stop ${APP_NAME}-${NEW_ENV}
podman rm ${APP_NAME}-${NEW_ENV}
exit 1
fi
sleep 2
done
# スモークテスト - 基本機能の確認
echo "Running smoke tests..."
CONTAINER_IP=$(podman inspect ${APP_NAME}-${NEW_ENV} --format '\{\{.NetworkSettings.IPAddress\}\}')
./smoke-tests.sh $CONTAINER_IP || {
echo "Smoke tests failed"
podman stop ${APP_NAME}-${NEW_ENV}
podman rm ${APP_NAME}-${NEW_ENV}
exit 1
}
# トラフィック切り替え
echo "Switching traffic to $NEW_ENV"
# nginxの設定を更新してトラフィックを切り替え
podman exec nginx sed -i "s/${CURRENT_ENV}/${NEW_ENV}/g" /etc/nginx/conf.d/upstream.conf
podman exec nginx nginx -s reload
# 監視期間 - 問題の早期発見
echo "Monitoring new deployment for 60 seconds..."
sleep 60
# エラー率チェック
ERROR_RATE=$(curl -s http://localhost:9090/api/v1/query?query=rate\(http_requests_total\{status=~\"5..\"\}\[1m\]\) | jq '.data.result[0].value[1]' | tr -d '"')
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "High error rate detected, rolling back"
# ロールバック
podman exec nginx sed -i "s/${NEW_ENV}/${CURRENT_ENV}/g" /etc/nginx/conf.d/upstream.conf
podman exec nginx nginx -s reload
podman stop ${APP_NAME}-${NEW_ENV}
podman rm ${APP_NAME}-${NEW_ENV}
exit 1
fi
# 旧環境を停止
echo "Stopping $CURRENT_ENV environment"
sleep 10 # 既存の接続が完了するまで待機
podman stop ${APP_NAME}-${CURRENT_ENV}
podman rm ${APP_NAME}-${CURRENT_ENV}
echo "Deployment completed successfully"
10.6.2 Canary Deployment
Canaryデプロイメントの価値
- 段階的なリスク検証
- 実ユーザーでの検証
- 問題の影響範囲限定
# canary-deployment.yml
apiVersion: v1
kind: Service
metadata:
name: myapp-canary
spec:
selector:
app: myapp
track: canary
ports:
- port: 80
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
spec:
replicas: 1 # 最初は1レプリカのみ
selector:
matchLabels:
app: myapp
track: canary
template:
metadata:
labels:
app: myapp
track: canary
spec:
containers:
- name: app
image: registry.example.com/myapp:canary
ports:
- containerPort: 8080
# カナリー特有の設定
env:
- name: CANARY_DEPLOYMENT
value: "true"
- name: METRICS_ENABLED
value: "true" # 詳細なメトリクス収集
---
# Nginx設定で重み付けルーティング
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
upstream.conf: |
upstream myapp {
# 90%のトラフィックは安定版へ
server myapp-stable max_fails=3 fail_timeout=30s weight=9;
# 10%のトラフィックはカナリー版へ
server myapp-canary max_fails=3 fail_timeout=30s weight=1;
}
カナリーデプロイメントの自動化
# canary-controller.py
import time
import requests
from prometheus_client.parser import text_string_to_metric_families
class CanaryController:
def __init__(self, prometheus_url, app_name):
self.prometheus_url = prometheus_url
self.app_name = app_name
self.canary_weight = 0.1 # 初期は10%
def get_error_rate(self, version):
"""エラー率を取得
なぜエラー率を監視するか:
- 最も直接的な品質指標
- ユーザー影響の定量化
- 自動判断の基準
"""
query = f'rate(http_requests_total\{\{app="{self.app_name}",version="{version}",status=~"5.."\}\}[5m])'
response = requests.get(f'{self.prometheus_url}/api/v1/query', params={'query': query})
# ... パース処理
def get_latency(self, version):
"""レイテンシを取得"""
query = f'histogram_quantile(0.95, rate(http_request_duration_seconds_bucket\{\{app="{self.app_name}",version="{version}"\}\}[5m]))'
# ...
def adjust_traffic(self, new_weight):
"""トラフィック配分を調整
段階的な増加により:
- 問題の早期発見
- 影響範囲の制限
- 安全な展開
"""
# Nginxの設定を更新
config = f"""
upstream myapp \{\{
server myapp-stable weight={int((1-new_weight)*10)};
server myapp-canary weight={int(new_weight*10)};
\}\}
"""
# ... 設定適用
def run_canary_deployment(self):
"""カナリーデプロイメントの実行"""
# Phase 1: 初期デプロイ (10%)
self.adjust_traffic(0.1)
time.sleep(300) # 5分間監視
canary_error_rate = self.get_error_rate('canary')
stable_error_rate = self.get_error_rate('stable')
if canary_error_rate > stable_error_rate * 1.5:
print("Canary has higher error rate, rolling back")
self.adjust_traffic(0)
return False
# Phase 2: 段階的増加
for weight in [0.25, 0.5, 0.75, 1.0]:
print(f"Increasing canary traffic to {weight*100}%")
self.adjust_traffic(weight)
time.sleep(300)
# 各段階でチェック
if self.get_error_rate('canary') > 0.01: # 1%以上のエラー
print("Error rate threshold exceeded, rolling back")
self.adjust_traffic(0)
return False
print("Canary deployment successful")
return True
演習問題
- GitLab CI/CDパイプラインを構築し、Podmanでのビルド・テスト・デプロイを自動化してください
- Blue-Greenデプロイメント戦略を実装し、ゼロダウンタイムを実現してください
- テストコンテナを使用した統合テストスイートを作成してください