😸

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