【実践編】AWSとGCPでシンプルなWebアプリケーションをデプロイしてみよう
はじめに:学んだ知識を実践で活かす
皆さん、こんにちは!「30日間でGCPをマスターするAWSエンジニアの挑戦」シリーズ、28日目へようこそ。
この連載もいよいよ最終盤です。ここまで、AWSの主要サービスとGCPの対応サービスを比較し、両者の設計思想の違いを深く掘り下げてきました。Day 1からDay 27までの内容は、GCPを理解するための大切な基礎知識です。
今日は、これまでの学習内容を総動員し、AWSとGCPで全く同じシンプルなWebアプリケーションをデプロイしてみましょう。両方のクラウドで手を動かすことで、それぞれのデプロイフロー、設定の違い、そして運用管理の思想を改めて実感できるはずです。
デプロイするアプリケーション構成
今回の実践ハンズオンでは、以下の構成でWebアプリケーションをデプロイします。
- アプリケーション: シンプルなPython(Flask)のWebAPI
- コンテナ化: Dockerコンテナとしてアプリケーションをパッケージ化
- インフラ: AWSではECS Fargate、GCPではCloud Runにデプロイ
- データベース: 簡単なTodo管理機能でデータ永続化を実現
- CI/CD: 両環境でのデプロイメントパイプラインも構築
共通のアプリケーションコード
まず、両環境で使用する共通のアプリケーションコードを準備します。
ディレクトリ構成
webapp/
├── app.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
└── static/
└── index.html
app.py
from flask import Flask, jsonify, request, render_template_string
import os
import sqlite3
from datetime import datetime
app = Flask(__name__)
# データベース初期化
def init_db():
conn = sqlite3.connect('/tmp/todos.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS todos
(id INTEGER PRIMARY KEY AUTOINCREMENT,
task TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE,
created_at TEXT DEFAULT CURRENT_TIMESTAMP)''')
conn.commit()
conn.close()
# HTML テンプレート
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<title>Todo App - {{ platform }}</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.todo-item { background: #f8f9fa; padding: 10px; margin: 5px 0; border-radius: 4px; }
.completed { text-decoration: line-through; opacity: 0.6; }
input, button { padding: 10px; margin: 5px; }
button { background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="header">
<h1>Todo App</h1>
<p>Running on: <strong>{{ platform }}</strong></p>
<p>Environment: {{ env }}</p>
</div>
<div>
<input type="text" id="taskInput" placeholder="新しいタスクを入力">
<button onclick="addTodo()">追加</button>
</div>
<div id="todos"></div>
<script>
async function loadTodos() {
const response = await fetch('/api/todos');
const todos = await response.json();
const container = document.getElementById('todos');
container.innerHTML = todos.map(todo =>
`<div class="todo-item ${todo.completed ? 'completed' : ''}">
<input type="checkbox" ${todo.completed ? 'checked' : ''}
onchange="toggleTodo(${todo.id})">
${todo.task}
<button onclick="deleteTodo(${todo.id})" style="float: right;">削除</button>
</div>`
).join('');
}
async function addTodo() {
const input = document.getElementById('taskInput');
const task = input.value.trim();
if (!task) return;
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task })
});
input.value = '';
loadTodos();
}
async function toggleTodo(id) {
await fetch(`/api/todos/${id}/toggle`, { method: 'PUT' });
loadTodos();
}
async function deleteTodo(id) {
await fetch(`/api/todos/${id}`, { method: 'DELETE' });
loadTodos();
}
// 初期読み込み
loadTodos();
</script>
</body>
</html>
'''
@app.route('/')
def index():
platform = os.environ.get('PLATFORM', 'Unknown')
env = os.environ.get('ENVIRONMENT', 'development')
return render_template_string(HTML_TEMPLATE, platform=platform, env=env)
@app.route('/health')
def health():
return jsonify({
'status': 'healthy',
'platform': os.environ.get('PLATFORM', 'Unknown'),
'timestamp': datetime.now().isoformat()
})
@app.route('/api/todos', methods=['GET'])
def get_todos():
conn = sqlite3.connect('/tmp/todos.db')
c = conn.cursor()
c.execute('SELECT * FROM todos ORDER BY created_at DESC')
todos = [{'id': row[0], 'task': row[1], 'completed': bool(row[2]), 'created_at': row[3]}
for row in c.fetchall()]
conn.close()
return jsonify(todos)
@app.route('/api/todos', methods=['POST'])
def add_todo():
data = request.get_json()
task = data.get('task', '').strip()
if not task:
return jsonify({'error': 'Task is required'}), 400
conn = sqlite3.connect('/tmp/todos.db')
c = conn.cursor()
c.execute('INSERT INTO todos (task) VALUES (?)', (task,))
conn.commit()
todo_id = c.lastrowid
conn.close()
return jsonify({'id': todo_id, 'task': task, 'completed': False}), 201
@app.route('/api/todos/<int:todo_id>/toggle', methods=['PUT'])
def toggle_todo(todo_id):
conn = sqlite3.connect('/tmp/todos.db')
c = conn.cursor()
c.execute('UPDATE todos SET completed = NOT completed WHERE id = ?', (todo_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/todos/<int:todo_id>', methods=['DELETE'])
def delete_todo(todo_id):
conn = sqlite3.connect('/tmp/todos.db')
c = conn.cursor()
c.execute('DELETE FROM todos WHERE id = ?', (todo_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
if __name__ == "__main__":
init_db()
port = int(os.environ.get("PORT", 8080))
app.run(host='0.0.0.0', port=port, debug=False)
requirements.txt
Flask==2.3.3
gunicorn==21.2.0
docker-compose.yml(ローカル開発用)
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- PLATFORM=Local Docker
- ENVIRONMENT=development
volumes:
- ./data:/tmp
AWS環境でのデプロイ:ECS Fargate
AWSでは、ECS Fargateを使用してサーバーレスコンテナをデプロイします。
1. Dockerfile(AWS用)
FROM python:3.11-slim
# 作業ディレクトリ設定
WORKDIR /app
# 依存関係をコピーしてインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# アプリケーションコードをコピー
COPY app.py .
# ヘルスチェック追加
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 環境変数設定
ENV PLATFORM="AWS ECS Fargate"
ENV ENVIRONMENT="production"
# ポート公開
EXPOSE 8080
# Gunicornでアプリケーション起動
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "app:app"]
2. ECRリポジトリ作成とイメージプッシュ
# ECRリポジトリ作成
aws ecr create-repository \
--repository-name todo-app \
--region ap-northeast-1
# Docker認証
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS --password-stdin <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com
# イメージビルド
docker build -t todo-app:aws .
# タグ付けとプッシュ
docker tag todo-app:aws <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/todo-app:latest
docker push <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/todo-app:latest
3. ECS設定(CloudFormationテンプレート)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Todo App on ECS Fargate'
Parameters:
ImageUri:
Type: String
Description: ECR image URI
Resources:
# VPC設定
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
# パブリックサブネット
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
# インターネットゲートウェイ
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# ECSクラスター
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: todo-app-cluster
CapacityProviders:
- FARGATE
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 1
# タスク定義
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: todo-app
Cpu: 256
Memory: 512
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn: !Ref ExecutionRole
ContainerDefinitions:
- Name: todo-app
Image: !Ref ImageUri
PortMappings:
- ContainerPort: 8080
Protocol: tcp
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ecs
HealthCheck:
Command:
- CMD-SHELL
- "curl -f http://localhost:8080/health || exit 1"
Interval: 30
Timeout: 5
Retries: 3
# ECSサービス
Service:
Type: AWS::ECS::Service
Properties:
Cluster: !Ref ECSCluster
TaskDefinition: !Ref TaskDefinition
LaunchType: FARGATE
DesiredCount: 2
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref SecurityGroup
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
AssignPublicIp: ENABLED
LoadBalancers:
- ContainerName: todo-app
ContainerPort: 8080
TargetGroupArn: !Ref TargetGroup
# セキュリティグループ
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Todo App
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
CidrIp: 0.0.0.0/0
# CloudWatch Logs
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/todo-app
RetentionInDays: 7
# IAMロール
ExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Outputs:
ServiceURL:
Description: URL of the load balancer
Value: !Sub 'http://${LoadBalancer.DNSName}'
4. デプロイコマンド
# CloudFormationスタック作成
aws cloudformation create-stack \
--stack-name todo-app-stack \
--template-body file://ecs-stack.yaml \
--parameters ParameterKey=ImageUri,ParameterValue=<account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/todo-app:latest \
--capabilities CAPABILITY_IAM \
--region ap-northeast-1
GCP環境でのデプロイ:Cloud Run
GCPでは、Cloud Runを使用してサーバーレスコンテナをデプロイします。
1. Dockerfile(GCP用)
FROM python:3.11-slim
# 作業ディレクトリ設定
WORKDIR /app
# 依存関係をコピーしてインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# アプリケーションコードをコピー
COPY app.py .
# 環境変数設定
ENV PLATFORM="GCP Cloud Run"
ENV ENVIRONMENT="production"
# Cloud Runのデフォルトポート
ENV PORT=8080
EXPOSE 8080
# Gunicornでアプリケーション起動
CMD exec gunicorn --bind :$PORT --workers 2 --threads 8 --timeout 0 app:app
2. Artifact Registryセットアップ
# APIの有効化
gcloud services enable artifactregistry.googleapis.com
gcloud services enable run.googleapis.com
# Artifact Registryリポジトリ作成
gcloud artifacts repositories create todo-app-repo \
--repository-format=docker \
--location=asia-northeast1 \
--description="Todo App Docker repository"
# Docker認証設定
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
3. イメージビルドとプッシュ
# プロジェクトID設定
export PROJECT_ID=$(gcloud config get-value project)
# イメージビルド
docker build -t todo-app:gcp .
# タグ付け
docker tag todo-app:gcp asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-app-repo/todo-app:latest
# プッシュ
docker push asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-app-repo/todo-app:latest
4. Cloud Runデプロイ
# Cloud Runサービスデプロイ
gcloud run deploy todo-app \
--image asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-app-repo/todo-app:latest \
--platform managed \
--region asia-northeast1 \
--allow-unauthenticated \
--port 8080 \
--memory 512Mi \
--cpu 1 \
--min-instances 0 \
--max-instances 10 \
--concurrency 80 \
--timeout 3600 \
--set-env-vars PLATFORM="GCP Cloud Run",ENVIRONMENT="production"
5. カスタムドメインの設定(オプション)
# カスタムドメインマッピング
gcloud run domain-mappings create \
--service todo-app \
--domain your-domain.com \
--region asia-northeast1
CI/CDパイプラインの比較
AWS CodePipeline設定例
# buildspec.yml
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
- docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker image...
- docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
- echo Updating ECS service...
- aws ecs update-service --cluster $CLUSTER_NAME --service $SERVICE_NAME --force-new-deployment
GCP Cloud Build設定例
# cloudbuild.yaml
steps:
# Docker イメージビルド
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-app-repo/todo-app:$SHORT_SHA'
- '.'
# Artifact Registry にプッシュ
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-app-repo/todo-app:$SHORT_SHA'
# Cloud Run にデプロイ
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: 'gcloud'
args:
- 'run'
- 'deploy'
- 'todo-app'
- '--image'
- 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-app-repo/todo-app:$SHORT_SHA'
- '--region'
- 'asia-northeast1'
- '--platform'
- 'managed'
- '--allow-unauthenticated'
options:
logging: CLOUD_LOGGING_ONLY
timeout: '1200s'
運用・監視の比較
AWS CloudWatch設定
- メトリクス: ECS サービスレベルのメトリクス(CPU、メモリ使用率)
- ログ: CloudWatch Logs でアプリケーションログを集約
- アラート: CloudWatch Alarms でしきい値監視
- ダッシュボード: カスタムダッシュボードで視覚化
GCP Cloud Operations設定
- メトリクス: Cloud Run の自動メトリクス(リクエスト数、レイテンシ)
- ログ: Cloud Logging で統合ログ管理
- アラート: Cloud Monitoring でSLA based アラート
- トレース: Cloud Trace で分散トレーシング
コスト比較とスケーリング特性
項目 | AWS ECS Fargate | GCP Cloud Run |
---|---|---|
最小課金単位 | 1分間(最小1vCPU、2GB) | 100ミリ秒(最小0.25vCPU、512MB) |
アイドル時課金 | タスク実行中は常時課金 | ゼロスケール時は課金なし |
スケールアップ | サービス設定に基づいて段階的 | リクエスト駆動で瞬時 |
コールドスタート | 比較的少ない | あり(通常1-2秒) |
同時実行制御 | タスク数で制御 | インスタンス×コンカレンシーで制御 |
最大実行時間 | 制限なし | 60分(HTTP)/ 24時間(CPU割り当て継続時) |
実践結果の検証
パフォーマンステスト結果(参考値)
AWS ECS Fargate:
- レスポンス時間: 平均50-100ms
- スループット: 約1000 req/min/instance
- スケールアップ時間: 2-3分
- 月間コスト: 約$30(2インスタンス常時稼働)
GCP Cloud Run:
- レスポンス時間: 平均30-80ms(ウォーム時)、500-1000ms(コールド時)
- スループット: 約2000 req/min/instance(コンカレンシー80)
- スケールアップ時間: 10-30秒
- 月間コスト: 約$5-15(利用量に応じて変動)
まとめ:デプロイフローから見る設計思想の違い
今回の実践を通して、AWSとGCPの根本的な設計思想の違いが明確になりました。
AWS(ECS Fargate)の特徴
-
✅ 長所:
- 企業システムに適した安定性と可用性
- 既存のAWSエコシステムとの深い連携
- 詳細な設定とカスタマイズが可能
- 予測しやすいパフォーマンス特性
-
⚠️ 注意点:
- 設定項目が多く学習コストが高い
- アイドル時にもコストが発生
- スケーリングに時間がかかる
GCP(Cloud Run)の特徴
-
✅ 長所:
- 極限まで簡素化されたデプロイフロー
- ゼロスケールによる高いコスト効率
- 高速なスケーリング対応
- サーバーレスならではの運用負荷軽減
-
⚠️ 注意点:
- コールドスタート時間の考慮が必要
- 実行時間制限(60分)
- ステートレスアプリケーション前提
どちらを選ぶべきか?
AWS ECS Fargateが適している場合:
- エンタープライズグレードの可用性が必要
- 既存のAWSインフラとの統合が重要
- 長時間実行されるアプリケーション
- 詳細な設定制御が必要
GCP Cloud Runが適している場合:
- スタートアップや新規プロジェクト
- トラフィックの変動が大きい
- 開発・運用の効率化を重視
- マイクロサービスアーキテクチャ
この実践ハンズオンを通して、両クラウドの特性を理解し、プロジェクトの要件に応じて適切な選択ができるエンジニアになれたはずです。
次回は最終回として、これまでの学習内容を総括し、AWSエンジニアとしてのGCP活用戦略について考察します。お楽しみに!
この記事が役に立った方は、ぜひ「いいね」や「ストック」をお願いします!
シリーズ記事一覧
- [【1日目】はじめの一歩!AWSエンジニアがGCPで最初にやるべきこと]
- [【2日目】GCPのIAMはAWSとどう違う?「プリンシパル」と「ロール」の理解]
- [【3日目】VPCとVPCネットワーク:GCPのネットワーク設計思想を理解する]
- [【4日目】S3とCloud Storage:オブジェクトストレージを徹底比較]
- [【5日目】RDSとCloud SQL:マネージドデータベースの運用管理の違い]
- [【6日目】EC2とCompute Engine:インスタンスの起動から課金モデルまで]
- [【7日目】1週間のまとめ:AWSとGCP、それぞれの得意なことと設計思想]
- [【8日目】EKSとGKE:Kubernetesのマネージドサービスを比較体験!]
- [【9日目】Dockerイメージをどこに置く?ECRとArtifact Registryを比較]
- [【10日目】LambdaとCloud Functions:イベント駆動型サーバーレスの実装]
- [【11日目】API GatewayとCloud Endpoints:API公開のベストプラクティス]
- [【12日目】Cloud Run:サーバーレスでコンテナを動かすGCPの独自サービスを試してみよう]
- [【13日目】AWS FargateとCloud Run:コンテナ運用モデルの根本的な違い]
- [【14日目】2週間のまとめ:GCPのコンテナ・サーバーレス技術はなぜ優れているのか?]
- [【15日目】RedshiftとBigQuery:データウェアハウスのアーキテクチャと料金体系]
- [【16日目】BigQueryをハンズオン!クエリを書いてデータ分析を体験]
- [【17日目】AthenaとBigQuery:データレイクに対するアプローチの違い]
- [【18日目】SageMakerとVertex AI:機械学習プラットフォームの比較]
- [【19日目】BigQuery MLでSQLだけで機械学習モデルを作ってみよう]
- [【20日目】Cloud SQLのレプリカ設定とパフォーマンスチューニング]
- [【21日目】GKE IngressとService Meshの比較]
- [【22日目】CloudWatchとCloud Monitoring:ログとメトリクスの監視設定]
- [【23日目】AWS BackupとGCPのバックアップ戦略:スナップショットとリージョン