😸
Python Flask + Terraform + AWS デプロイガイド
🏗️ アーキテクチャパターン比較
Flaskアプリケーションの展開方法は、要件に応じて複数のパターンがあります:
パターン | コスト | 複雑度 | スケーラビリティ | 適用場面 |
---|---|---|---|---|
EC2 単体 | 低 | 低 | 手動 | 開発・小規模 |
ALB + EC2 | 中 | 中 | 中 | 本格運用 |
ECS Fargate | 中 | 高 | 高 | マイクロサービス |
Lambda | 最低 | 中 | 自動 | イベント駆動 |
Elastic Beanstalk | 中 | 低 | 自動 | 迅速なプロトタイプ |
🚀 パターン1: ALB + Auto Scaling EC2(推奨)
本格的な本番運用に適したスケーラブルな構成です。
ディレクトリ構造
flask-terraform-aws/
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── vpc.tf
│ ├── security_groups.tf
│ ├── load_balancer.tf
│ ├── auto_scaling.tf
│ └── rds.tf
├── app/
│ ├── app.py
│ ├── requirements.txt
│ └── wsgi.py
└── scripts/
└── user_data.sh
Flaskアプリケーション
# app/app.py
from flask import Flask, jsonify, request
import os
import logging
from datetime import datetime
app = Flask(__name__)
# ログ設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.route('/')
def index():
return jsonify({
'message': 'Flask App on AWS!',
'timestamp': datetime.now().isoformat(),
'server': os.environ.get('HOSTNAME', 'unknown')
})
@app.route('/health')
def health_check():
return jsonify({'status': 'healthy'}), 200
@app.route('/api/data', methods=['GET', 'POST'])
def api_data():
if request.method == 'POST':
data = request.get_json()
logger.info(f"Received data: {data}")
return jsonify({'received': data, 'status': 'success'})
else:
return jsonify({
'data': ['item1', 'item2', 'item3'],
'count': 3
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
# app/wsgi.py
from app import app
if __name__ == "__main__":
app.run()
# app/requirements.txt
Flask==2.3.3
gunicorn==21.2.0
psycopg2-binary==2.9.7
boto3==1.34.0
Terraform設定
メイン設定
# terraform/main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "flask-app/terraform.tfstate"
region = "ap-northeast-1"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}
}
}
# データソース
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
VPC設定
# terraform/vpc.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
# パブリックサブネット
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-subnet-${count.index + 1}"
Type = "public"
}
}
# プライベートサブネット(データベース用)
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project_name}-private-subnet-${count.index + 1}"
Type = "private"
}
}
# ルートテーブル
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
セキュリティグループ
# terraform/security_groups.tf
resource "aws_security_group" "alb" {
name = "${var.project_name}-alb-sg"
description = "Security group for Application Load Balancer"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-alb-sg"
}
}
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg"
description = "Security group for Web servers"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from ALB"
from_port = 5000
to_port = 5000
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.ssh_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-web-sg"
}
}
resource "aws_security_group" "rds" {
name = "${var.project_name}-rds-sg"
description = "Security group for RDS"
vpc_id = aws_vpc.main.id
ingress {
description = "PostgreSQL from Web"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
tags = {
Name = "${var.project_name}-rds-sg"
}
}
Application Load Balancer
# terraform/load_balancer.tf
resource "aws_lb" "main" {
name = "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
enable_deletion_protection = var.environment == "production"
tags = {
Name = "${var.project_name}-alb"
}
}
resource "aws_lb_target_group" "web" {
name = "${var.project_name}-tg"
port = 5000
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
tags = {
Name = "${var.project_name}-tg"
}
}
resource "aws_lb_listener" "web" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
}
# HTTPS設定(SSL証明書がある場合)
resource "aws_lb_listener" "web_https" {
count = var.ssl_certificate_arn != "" ? 1 : 0
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = var.ssl_certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
}
Auto Scaling設定
# terraform/auto_scaling.tf
resource "aws_launch_template" "web" {
name_prefix = "${var.project_name}-"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.web.id]
iam_instance_profile {
name = aws_iam_instance_profile.web.name
}
user_data = base64encode(templatefile("${path.module}/../scripts/user_data.sh", {
db_host = aws_db_instance.main.endpoint
db_name = aws_db_instance.main.db_name
db_username = aws_db_instance.main.username
app_version = var.app_version
}))
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.project_name}-web-server"
}
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_autoscaling_group" "web" {
name = "${var.project_name}-asg"
vpc_zone_identifier = aws_subnet.public[*].id
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
health_check_grace_period = 300
min_size = var.min_size
max_size = var.max_size
desired_capacity = var.desired_capacity
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
tag {
key = "Name"
value = "${var.project_name}-asg"
propagate_at_launch = true
}
}
# オートスケーリングポリシー
resource "aws_autoscaling_policy" "scale_out" {
name = "${var.project_name}-scale-out"
scaling_adjustment = 1
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.web.name
}
resource "aws_autoscaling_policy" "scale_in" {
name = "${var.project_name}-scale-in"
scaling_adjustment = -1
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.web.name
}
# CloudWatch アラーム
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "${var.project_name}-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "300"
statistic = "Average"
threshold = "70"
alarm_description = "This metric monitors ec2 cpu utilization"
alarm_actions = [aws_autoscaling_policy.scale_out.arn]
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.web.name
}
}
resource "aws_cloudwatch_metric_alarm" "low_cpu" {
alarm_name = "${var.project_name}-low-cpu"
comparison_operator = "LessThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "300"
statistic = "Average"
threshold = "30"
alarm_description = "This metric monitors ec2 cpu utilization"
alarm_actions = [aws_autoscaling_policy.scale_in.arn]
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.web.name
}
}
# IAM Role for EC2
resource "aws_iam_role" "web" {
name = "${var.project_name}-web-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "web" {
name = "${var.project_name}-web-policy"
role = aws_iam_role.web.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:*:*"
},
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = aws_secretsmanager_secret.db_password.arn
}
]
})
}
resource "aws_iam_instance_profile" "web" {
name = "${var.project_name}-web-profile"
role = aws_iam_role.web.name
}
RDS設定
# terraform/rds.tf
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet-group"
subnet_ids = aws_subnet.private[*].id
tags = {
Name = "${var.project_name}-db-subnet-group"
}
}
resource "aws_secretsmanager_secret" "db_password" {
name = "${var.project_name}-db-password"
description = "Database password for ${var.project_name}"
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = var.db_password
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
allocated_storage = 20
max_allocated_storage = 100
storage_type = "gp2"
storage_encrypted = true
engine = "postgres"
engine_version = "14.9"
instance_class = var.db_instance_class
db_name = var.db_name
username = var.db_username
password = var.db_password
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = var.environment == "production" ? 7 : 1
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
skip_final_snapshot = var.environment != "production"
deletion_protection = var.environment == "production"
tags = {
Name = "${var.project_name}-database"
}
}
変数定義
# terraform/variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-1"
}
variable "project_name" {
description = "Project name"
type = string
default = "flask-app"
}
variable "environment" {
description = "Environment (dev, staging, production)"
type = string
default = "dev"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "availability_zones" {
description = "Availability zones"
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c"]
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "key_name" {
description = "EC2 Key Pair name"
type = string
}
variable "ssh_cidr" {
description = "CIDR block for SSH access"
type = string
default = "0.0.0.0/0"
}
variable "min_size" {
description = "Minimum number of instances"
type = number
default = 1
}
variable "max_size" {
description = "Maximum number of instances"
type = number
default = 3
}
variable "desired_capacity" {
description = "Desired number of instances"
type = number
default = 2
}
variable "db_instance_class" {
description = "RDS instance class"
type = string
default = "db.t3.micro"
}
variable "db_name" {
description = "Database name"
type = string
default = "flaskapp"
}
variable "db_username" {
description = "Database username"
type = string
default = "dbuser"
}
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
variable "ssl_certificate_arn" {
description = "SSL certificate ARN for HTTPS"
type = string
default = ""
}
variable "app_version" {
description = "Application version"
type = string
default = "latest"
}
出力値
# terraform/outputs.tf
output "load_balancer_dns" {
description = "DNS name of the load balancer"
value = aws_lb.main.dns_name
}
output "load_balancer_zone_id" {
description = "Zone ID of the load balancer"
value = aws_lb.main.zone_id
}
output "database_endpoint" {
description = "RDS instance endpoint"
value = aws_db_instance.main.endpoint
sensitive = true
}
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = aws_subnet.public[*].id
}
ユーザーデータスクリプト
#!/bin/bash
# scripts/user_data.sh
# ログファイル設定
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
# システム更新
yum update -y
# Python 3とpipのインストール
yum install -y python3 python3-pip git
# アプリケーションディレクトリ作成
mkdir -p /opt/flaskapp
cd /opt/flaskapp
# GitHubからアプリケーションをクローン(または直接配置)
cat > app.py << 'EOF'
from flask import Flask, jsonify, request
import os
import logging
from datetime import datetime
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.route('/')
def index():
return jsonify({
'message': 'Flask App on AWS!',
'timestamp': datetime.now().isoformat(),
'server': os.environ.get('HOSTNAME', 'unknown')
})
@app.route('/health')
def health_check():
return jsonify({'status': 'healthy'}), 200
@app.route('/api/data', methods=['GET', 'POST'])
def api_data():
if request.method == 'POST':
data = request.get_json()
logger.info(f"Received data: {data}")
return jsonify({'received': data, 'status': 'success'})
else:
return jsonify({
'data': ['item1', 'item2', 'item3'],
'count': 3
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
EOF
cat > requirements.txt << 'EOF'
Flask==2.3.3
gunicorn==21.2.0
psycopg2-binary==2.9.7
boto3==1.34.0
EOF
# 依存関係のインストール
pip3 install -r requirements.txt
# 環境変数設定
cat > /etc/environment << EOF
DB_HOST=${db_host}
DB_NAME=${db_name}
DB_USERNAME=${db_username}
APP_VERSION=${app_version}
EOF
# systemdサービスファイル作成
cat > /etc/systemd/system/flaskapp.service << 'EOF'
[Unit]
Description=Flask Application
After=network.target
[Service]
Type=exec
User=ec2-user
WorkingDirectory=/opt/flaskapp
Environment=PATH=/usr/local/bin
EnvironmentFile=/etc/environment
ExecStart=/usr/local/bin/gunicorn --bind 0.0.0.0:5000 --workers 3 app:app
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# 権限設定
chown -R ec2-user:ec2-user /opt/flaskapp
# サービス開始
systemctl daemon-reload
systemctl enable flaskapp
systemctl start flaskapp
# CloudWatch Logs エージェント設定
yum install -y awslogs
cat > /etc/awslogs/awslogs.conf << 'EOF'
[general]
state_file = /var/lib/awslogs/agent-state
[/var/log/messages]
file = /var/log/messages
log_group_name = /aws/ec2/flask-app
log_stream_name = {instance_id}/var/log/messages
datetime_format = %b %d %H:%M:%S
[/var/log/user-data.log]
file = /var/log/user-data.log
log_group_name = /aws/ec2/flask-app
log_stream_name = {instance_id}/var/log/user-data.log
datetime_format = %Y-%m-%d %H:%M:%S
EOF
systemctl enable awslogsd
systemctl start awslogsd
echo "Flask application setup completed!"
🐳 パターン2: ECS Fargate(コンテナ化)
よりモダンなコンテナベースの実装です。
Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "3", "app:app"]
ECS設定
# ECS Cluster
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Name = "${var.project_name}-cluster"
}
}
# ECR Repository
resource "aws_ecr_repository" "app" {
name = "${var.project_name}-app"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
tags = {
Name = "${var.project_name}-ecr"
}
}
# ECS Task Definition
resource "aws_ecs_task_definition" "app" {
family = "${var.project_name}-task"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = "${var.project_name}-container"
image = "${aws_ecr_repository.app.repository_url}:latest"
portMappings = [
{
containerPort = 5000
protocol = "tcp"
}
]
environment = [
{
name = "DB_HOST"
value = aws_db_instance.main.endpoint
},
{
name = "DB_NAME"
value = aws_db_instance.main.db_name
}
]
secrets = [
{
name = "DB_PASSWORD"
valueFrom = aws_secretsmanager_secret.db_password.arn
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.app.name
awslogs-region = var.aws_region
awslogs-stream-prefix = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:5000/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
tags = {
Name = "${var.project_name}-task"
}
}
# ECS Service
resource "aws_ecs_service" "app" {
name = "${var.project_name}-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.desired_capacity
launch_type = "FARGATE"
network_configuration {
security_groups = [aws_security_group.ecs.id]
subnets = aws_subnet.public[*].id
assign_public_ip = true
}
load_balancer {
target_group_arn = aws_lb_target_group.ecs.arn
container_name = "${var.project_name}-container"
container_port = 5000
}
depends_on = [aws_lb_listener.ecs]
tags = {
Name = "${var.project_name}-service"
}
}
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/${var.project_name}"
retention_in_days = 7
tags = {
Name = "${var.project_name}-logs"
}
}
⚡ パターン3: AWS Lambda(サーバーレス)
軽量なAPIやイベント駆動アプリケーション向け。
Lambda用Flaskアプリ
# lambda_app.py
from flask import Flask, jsonify, request
import serverless_wsgi
import os
import json
app = Flask(__name__)
@app.route('/')
def index():
return jsonify({
'message': 'Flask App on Lambda!',
'version': '1.0',
'aws_region': os.environ.get('AWS_REGION', 'unknown')
})
@app.route('/health')
def health_check():
return jsonify({'status': 'healthy'}), 200
@app.route('/api/data', methods=['GET', 'POST'])
def api_data():
if request.method == 'POST':
data = request.get_json()
return jsonify({'received': data, 'status': 'success'})
else:
return jsonify({
'data': ['lambda-item1', 'lambda-item2', 'lambda-item3'],
'count': 3
})
def lambda_handler(event, context):
return serverless_wsgi.handle_request(app, event, context)
if __name__ == '__main__':
app.run(debug=True)
Lambda用Terraform設定
# Lambda用のTerraform設定
# lambda.tf
# Lambda関数用のZIPファイル作成
data "archive_file" "lambda_zip" {
type = "zip"
source_dir = "${path.module}/../lambda"
output_path = "${path.module}/lambda_function.zip"
}
# Lambda関数
resource "aws_lambda_function" "flask_app" {
filename = data.archive_file.lambda_zip.output_path
function_name = "${var.project_name}-flask-lambda"
role = aws_iam_role.lambda.arn
handler = "lambda_app.lambda_handler"
runtime = "python3.11"
timeout = 30
memory_size = 256
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
environment {
variables = {
FLASK_ENV = var.environment
DB_HOST = var.db_endpoint
}
}
tags = {
Name = "${var.project_name}-lambda"
}
}
# API Gateway
resource "aws_apigatewayv2_api" "flask_api" {
name = "${var.project_name}-api"
protocol_type = "HTTP"
description = "Flask application API"
cors_configuration {
allow_credentials = false
allow_headers = ["content-type", "x-amz-date", "authorization", "x-api-key"]
allow_methods = ["*"]
allow_origins = ["*"]
max_age = 86400
}
tags = {
Name = "${var.project_name}-api"
}
}
# Lambda統合
resource "aws_apigatewayv2_integration" "flask_lambda" {
api_id = aws_apigatewayv2_api.flask_api.id
integration_type = "AWS_PROXY"
connection_type = "INTERNET"
description = "Flask Lambda integration"
integration_method = "POST"
integration_uri = aws_lambda_function.flask_app.invoke_arn
passthrough_behavior = "WHEN_NO_MATCH"
}
# ルート設定
resource "aws_apigatewayv2_route" "flask_route" {
api_id = aws_apigatewayv2_api.flask_api.id
route_key = "ANY /{proxy+}"
target = "integrations/${aws_apigatewayv2_integration.flask_lambda.id}"
}
resource "aws_apigatewayv2_route" "flask_root" {
api_id = aws_apigatewayv2_api.flask_api.id
route_key = "ANY /"
target = "integrations/${aws_apigatewayv2_integration.flask_lambda.id}"
}
# デプロイメント
resource "aws_apigatewayv2_stage" "flask_stage" {
api_id = aws_apigatewayv2_api.flask_api.id
name = var.environment
auto_deploy = true
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_gateway.arn
format = jsonencode({
requestId = "$context.requestId"
ip = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
httpMethod = "$context.httpMethod"
routeKey = "$context.routeKey"
status = "$context.status"
protocol = "$context.protocol"
responseLength = "$context.responseLength"
})
}
tags = {
Name = "${var.project_name}-stage"
}
}
# Lambda実行権限
resource "aws_lambda_permission" "api_gateway" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.flask_app.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.flask_api.execution_arn}/*/*"
}
# CloudWatch Logs
resource "aws_cloudwatch_log_group" "lambda" {
name = "/aws/lambda/${aws_lambda_function.flask_app.function_name}"
retention_in_days = 7
tags = {
Name = "${var.project_name}-lambda-logs"
}
}
resource "aws_cloudwatch_log_group" "api_gateway" {
name = "/aws/apigateway/${aws_apigatewayv2_api.flask_api.name}"
retention_in_days = 7
tags = {
Name = "${var.project_name}-api-logs"
}
}
# Lambda IAM Role
resource "aws_iam_role" "lambda" {
name = "${var.project_name}-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
tags = {
Name = "${var.project_name}-lambda-role"
}
}
resource "aws_iam_role_policy_attachment" "lambda_basic" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
role = aws_iam_role.lambda.name
}
# Lambda用の依存関係
# requirements.txt
serverless-wsgi==0.2.1
Flask==2.3.3
🚀 デプロイメント手順
1. 事前準備
# 1. Terraformのインストール確認
terraform --version
# 2. AWS CLIの設定
aws configure
aws sts get-caller-identity
# 3. SSH Key Pairの作成(EC2アクセス用)
aws ec2 create-key-pair --key-name flask-app-key --query 'KeyMaterial' --output text > flask-app-key.pem
chmod 400 flask-app-key.pem
2. 環境変数設定
# terraform.tfvars ファイル作成
cat > terraform.tfvars << EOF
project_name = "my-flask-app"
environment = "dev"
aws_region = "ap-northeast-1"
key_name = "flask-app-key"
db_password = "your-secure-password-here"
ssh_cidr = "YOUR_IP_ADDRESS/32"
# 本番環境用設定
# environment = "production"
# instance_type = "t3.small"
# min_size = 2
# max_size = 6
# desired_capacity = 3
# db_instance_class = "db.t3.small"
EOF
3. Terraform実行
# 1. 初期化
terraform init
# 2. プラン確認
terraform plan
# 3. 適用
terraform apply
# 4. 出力確認
terraform output load_balancer_dns
4. 動作確認
# ロードバランサーのDNS名を取得
ALB_DNS=$(terraform output -raw load_balancer_dns)
# ヘルスチェック
curl http://$ALB_DNS/health
# アプリケーション動作確認
curl http://$ALB_DNS/
curl -X POST http://$ALB_DNS/api/data -H "Content-Type: application/json" -d '{"test": "data"}'
📊 コスト分析
月間コスト試算(東京リージョン)
パターン1: ALB + Auto Scaling EC2
コンポーネント別コスト:
┌─────────────────────┬──────────┬─────────────┐
│ サービス │ 使用量 │ 月額($) │
├─────────────────────┼──────────┼─────────────┤
│ EC2 (t3.micro x2) │ 24/7 │ 16.32 │
│ Application LB │ 1台 │ 16.20 │
│ RDS (db.t3.micro) │ 24/7 │ 14.50 │
│ EBS (20GB x2) │ 2個 │ 4.00 │
│ Data Transfer │ 10GB │ 1.50 │
│ CloudWatch Logs │ 1GB │ 0.50 │
├─────────────────────┼──────────┼─────────────┤
│ 合計 │ │ 約53$ │
└─────────────────────┴──────────┴─────────────┘
パターン2: ECS Fargate
コンポーネント別コスト:
┌─────────────────────┬──────────┬─────────────┐
│ サービス │ 使用量 │ 月額($) │
├─────────────────────┼──────────┼─────────────┤
│ Fargate (0.25vCPU) │ 24/7 x2 │ 29.52 │
│ Application LB │ 1台 │ 16.20 │
│ RDS (db.t3.micro) │ 24/7 │ 14.50 │
│ ECR │ 0.5GB │ 0.05 │
│ CloudWatch Logs │ 2GB │ 1.00 │
├─────────────────────┼──────────┼─────────────┤
│ 合計 │ │ 約61$ │
└─────────────────────┴──────────┴─────────────┘
パターン3: Lambda
コンポーネント別コスト:
┌─────────────────────┬──────────┬─────────────┐
│ サービス │ 使用量 │ 月額($) │
├─────────────────────┼──────────┼─────────────┤
│ Lambda │ 100万req │ 2.00 │
│ API Gateway │ 100万req │ 3.50 │
│ RDS (db.t3.micro) │ 24/7 │ 14.50 │
│ CloudWatch Logs │ 0.5GB │ 0.25 │
├─────────────────────┼──────────┼─────────────┤
│ 合計 │ │ 約20$ │
└─────────────────────┴──────────┴─────────────┘
🔧 運用・監視設定
CloudWatchダッシュボード
# monitoring.tf
resource "aws_cloudwatch_dashboard" "flask_app" {
dashboard_name = "${var.project_name}-dashboard"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
x = 0
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/ApplicationELB", "RequestCount", "LoadBalancer", aws_lb.main.arn_suffix],
[".", "TargetResponseTime", ".", "."],
[".", "HTTPCode_Target_2XX_Count", ".", "."],
[".", "HTTPCode_Target_4XX_Count", ".", "."],
[".", "HTTPCode_Target_5XX_Count", ".", "."]
]
view = "timeSeries"
stacked = false
region = var.aws_region
title = "Application Load Balancer Metrics"
period = 300
}
},
{
type = "metric"
x = 0
y = 6
width = 12
height = 6
properties = {
metrics = [
["AWS/EC2", "CPUUtilization", "AutoScalingGroupName", aws_autoscaling_group.web.name],
["AWS/RDS", "CPUUtilization", "DBInstanceIdentifier", aws_db_instance.main.id],
[".", "DatabaseConnections", ".", "."]
]
view = "timeSeries"
stacked = false
region = var.aws_region
title = "System Resource Metrics"
period = 300
}
}
]
})
}
# SNS Topic for alerts
resource "aws_sns_topic" "alerts" {
name = "${var.project_name}-alerts"
tags = {
Name = "${var.project_name}-alerts"
}
}
resource "aws_sns_topic_subscription" "email_alerts" {
count = var.alert_email != "" ? 1 : 0
topic_arn = aws_sns_topic.alerts.arn
protocol = "email"
endpoint = var.alert_email
}
# CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "high_response_time" {
alarm_name = "${var.project_name}-high-response-time"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "TargetResponseTime"
namespace = "AWS/ApplicationELB"
period = "300"
statistic = "Average"
threshold = "2"
alarm_description = "This metric monitors ALB response time"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
LoadBalancer = aws_lb.main.arn_suffix
}
tags = {
Name = "${var.project_name}-high-response-time"
}
}
resource "aws_cloudwatch_metric_alarm" "high_4xx_errors" {
alarm_name = "${var.project_name}-high-4xx-errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "HTTPCode_Target_4XX_Count"
namespace = "AWS/ApplicationELB"
period = "300"
statistic = "Sum"
threshold = "10"
alarm_description = "This metric monitors 4XX errors"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
LoadBalancer = aws_lb.main.arn_suffix
}
tags = {
Name = "${var.project_name}-high-4xx-errors"
}
}
🛡️ セキュリティ強化
WAF設定
# waf.tf
resource "aws_wafv2_web_acl" "flask_app" {
name = "${var.project_name}-waf"
scope = "REGIONAL"
default_action {
allow {}
}
# Rate limiting rule
rule {
name = "RateLimitRule"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitRule"
sampled_requests_enabled = true
}
}
# AWS Managed Core Rule Set
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 10
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CommonRuleSetMetric"
sampled_requests_enabled = true
}
}
# SQL Injection protection
rule {
name = "AWSManagedRulesSQLiRuleSet"
priority = 20
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "SQLiRuleSetMetric"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${var.project_name}WAF"
sampled_requests_enabled = true
}
tags = {
Name = "${var.project_name}-waf"
}
}
# WAFをALBに関連付け
resource "aws_wafv2_web_acl_association" "flask_app" {
resource_arn = aws_lb.main.arn
web_acl_arn = aws_wafv2_web_acl.flask_app.arn
}
Secrets Manager統合
# app.py の改良版(Secrets Manager対応)
import boto3
import json
from flask import Flask, jsonify
from botocore.exceptions import ClientError
app = Flask(__name__)
def get_secret(secret_name, region_name="ap-northeast-1"):
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
secret = get_secret_value_response['SecretString']
return json.loads(secret)
except ClientError as e:
app.logger.error(f"Error retrieving secret: {e}")
return None
# データベース接続設定
def get_db_config():
secret_name = f"{os.environ.get('PROJECT_NAME', 'flask-app')}-db-password"
secret = get_secret(secret_name)
if secret:
return {
'host': os.environ.get('DB_HOST'),
'database': os.environ.get('DB_NAME'),
'user': os.environ.get('DB_USERNAME'),
'password': secret.get('password')
}
return None
@app.route('/api/secure-data')
def secure_data():
db_config = get_db_config()
if not db_config:
return jsonify({'error': 'Database configuration not available'}), 500
# データベース操作を実装
return jsonify({'message': 'Secure data retrieved successfully'})
🔄 CI/CDパイプライン設定
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy Flask App to AWS
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: ap-northeast-1
PROJECT_NAME: flask-app
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r app/requirements.txt
pip install pytest pytest-cov
- name: Run tests
run: |
cd app
python -m pytest tests/ -v --cov=.
terraform-plan:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
run: |
cd terraform
terraform init
- name: Terraform Plan
run: |
cd terraform
terraform plan -var="db_password=${{ secrets.DB_PASSWORD }}"
deploy:
runs-on: ubuntu-latest
needs: [test, terraform-plan]
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
run: |
cd terraform
terraform init
- name: Terraform Apply
run: |
cd terraform
terraform apply -auto-approve -var="db_password=${{ secrets.DB_PASSWORD }}"
- name: Health Check
run: |
ALB_DNS=$(cd terraform && terraform output -raw load_balancer_dns)
echo "Testing endpoint: http://$ALB_DNS/health"
# 最大5分間待機
for i in {1..30}; do
if curl -f "http://$ALB_DNS/health"; then
echo "Health check passed!"
break
fi
echo "Attempt $i failed, retrying in 10 seconds..."
sleep 10
done
📝 まとめ・推奨事項
アーキテクチャ選択の指針
開発・プロトタイプ段階:
- Lambda + API Gateway: 最小コスト、迅速な検証
- EC2単体: シンプルな構成、学習目的
本番運用:
- ALB + Auto Scaling EC2: バランス重視、一般的な用途
- ECS Fargate: コンテナ化、マイクロサービス
- Elastic Beanstalk: 運用負荷軽減、迅速な立ち上げ
セキュリティチェックリスト
- WAF設定
- Secrets Manager使用
- VPC設定(プライベートサブネット)
- セキュリティグループ最小権限
- CloudTrail有効化
- SSL/TLS証明書設定
運用チェックリスト
- CloudWatch監視設定
- ログ収集設定
- アラート通知設定
- バックアップ戦略
- 災害復旧計画
- CI/CDパイプライン
この設定で、本格的なFlaskアプリケーションをAWSで運用できます。要件に応じてアーキテクチャを選択し、段階的にスケールアップしていくことをお勧めします。
Discussion