0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DjangoアプリをAWS ALB+EC2+RDS構成で本番運用してみた【デプロイ編】

Posted at

はじめに

前回の【Djangoで再開発】では、DjangoでスキーリゾートAI積雪予測アプリ「Snow Deep Predict」を再開発しました。
今回は、そのアプリケーションを AWS ALB + EC2 + RDS の本格的な構成で本番環境にデプロイし、複数アプリケーション共存環境での運用まで解説します。

前回記事: [DjangoでスキーリゾートAI積雪予測アプリを作ってみた【Djangoで再開発編】]

🏗️ インフラ構成

構成図

Internet
    ↓
AWS ALB (Application Load Balancer)
    ↓ HTTPS/HTTP
EC2 Instance
├── Nginx (リバースプロキシ)
│   ├── /snow_deep/ → Gunicorn (ポート8000)
│   ├── /bikefinder/ → 他のDjangoアプリ
│   └── /aiapp/ → Streamlitアプリ
└── PostgreSQL RDS

技術スタック

  • ロードバランサー: AWS ALB
  • Webサーバー: Nginx 1.28
  • アプリケーションサーバー: Gunicorn
  • データベース: PostgreSQL (AWS RDS)
  • Frontend: Bootstrap 5.3 + Vanilla JavaScript (fetch API)
  • Ajax通信: fetch API(jQueryなしの生JavaScript)
  • プロセス管理: systemd
  • SSL/TLS: Let's Encrypt (Certbot)
  • ドメイン: 独自ドメイン + Route53

📋 前提条件

AWS環境

  • EC2インスタンス(Amazon Linux 2023)
  • RDS PostgreSQL インスタンス
  • ALB設定済み
  • Route53でドメイン設定済み

ローカル環境

  • 開発済みのDjangoアプリケーション
  • Git リポジトリ準備

🚀 デプロイ手順

1. EC2インスタンスの基本セットアップ

# システム更新
sudo yum update -y

# 必要パッケージインストール
sudo yum install -y python3 python3-pip nginx postgresql15 git

# Python3をpythonコマンドで使用
sudo alternatives --install /usr/bin/python python /usr/bin/python3 1

2. アプリケーションのデプロイ

# プロジェクトのクローン
cd /home/ec2-user
git clone https://github.com/your-repo/snow_deep_predict.git snow_deep
cd snow_deep

# 仮想環境作成
python3 -m venv venv313
source venv313/bin/activate

# 依存パッケージインストール
pip install --upgrade pip
pip install -r requirements_production.txt

requirements_production.txt の例:

Django==4.2.23
django-cors-headers==4.7.0
psycopg2-binary==2.9.9
python-dotenv>=1.0.0
gunicorn==21.2.0

3. 本番用設定ファイル作成

settings_production.py

# snow_predict/settings_production.py
import os
from pathlib import Path
from dotenv import load_dotenv

# .envファイル読み込み
load_dotenv()

BASE_DIR = Path(__file__).resolve().parent.parent

# セキュリティ設定
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
DEBUG = False

# ホスト設定(ALB対応)
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',') if os.environ.get('ALLOWED_HOSTS') else [
    'hungryclimber.com',
    'www.hungryclimber.com',
    'localhost',
    '127.0.0.1',
    '.amazonaws.com',  # ALB内部通信用
]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'prediction',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'snow_predict.urls'

# テンプレート設定
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# PostgreSQL RDS設定
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', 'snowpredictdb'),
        'USER': os.environ.get('DB_USER', 'postgres'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST'),
        'PORT': os.environ.get('DB_PORT', '5432'),
        'OPTIONS': {
            'connect_timeout': 60,
        }
    }
}

# 静的ファイル設定(重要:URLパスに注意)
STATIC_URL = '/snow_deep/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

# ALB対応のセキュリティ設定
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = False  # ALBでSSL終端するため
SECURE_REDIRECT_EXEMPT = [r'^health/$', r'^admin/$']

# CORS設定
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOWED_ORIGINS = [
    "https://hungryclimber.com",
    "http://hungryclimber.com",
]

# ログ設定
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': '/var/log/django/snow_deep.log',
            'formatter': 'verbose',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}

# 国際化
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
USE_I18N = True
USE_TZ = True

環境変数ファイル (.env)

# /home/ec2-user/snow_deep/.env

# Django基本設定
DJANGO_SECRET_KEY=your-50-character-secret-key-here
DJANGO_SETTINGS_MODULE=snow_predict.settings_production
DEBUG=False

# データベース設定
DB_NAME=snowpredictdb
DB_USER=postgres
DB_PASSWORD=your-rds-password
DB_HOST=your-rds-endpoint.ap-northeast-1.rds.amazonaws.com
DB_PORT=5432

# ドメイン設定
ALLOWED_HOSTS=hungryclimber.com,www.hungryclimber.com,.amazonaws.com,localhost,127.0.0.1

4. Gunicorn設定

# gunicorn_production.conf.py
import multiprocessing

# サーバーソケット
bind = "127.0.0.1:8000"
backlog = 2048

# ワーカー設定
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2

# プロセス名
proc_name = 'snow_deep_gunicorn'

# ログ設定
accesslog = '/var/log/gunicorn/snow_deep_access.log'
errorlog = '/var/log/gunicorn/snow_deep_error.log'
loglevel = 'info'

# プロセス設定
preload_app = True
daemon = False
pidfile = '/tmp/snow_deep_gunicorn.pid'

# セキュリティ
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190

5. systemdサービス設定

# /etc/systemd/system/snow_deep.service

[Unit]
Description=Snow Deep Predict Django Application
After=network.target

[Service]
User=ec2-user
Group=ec2-user
WorkingDirectory=/home/ec2-user/snow_deep
Environment="PATH=/home/ec2-user/snow_deep/venv313/bin"
EnvironmentFile=/home/ec2-user/snow_deep/.env
ExecStart=/home/ec2-user/snow_deep/venv313/bin/gunicorn snow_predict.wsgi:application -c /home/ec2-user/snow_deep/gunicorn_production.conf.py
ExecReload=/bin/kill -s HUP $MAINPID
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

6. Nginx設定(複数アプリ共存)

# /etc/nginx/conf.d/hungryclimber.conf

server {
    listen 80;
    server_name hungryclimber.com www.hungryclimber.com 127.0.0.1 localhost;

    # ルートはデフォルトのnginxページ
    root /usr/share/nginx/html;
    index index.html;

    # ALBヘルスチェック
    location = /health/ {
        add_header Content-Type text/plain;
        return 200 "OK\n";
    }

    # ===== Snow Deep Predict (Django) =====
    # 静的ファイル配信
    location ^~ /snow_deep/static/ {
        alias /home/ec2-user/snow_deep/staticfiles/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Access-Control-Allow-Origin "*";
    }
    
    # アプリケーション本体
    location ^~ /snow_deep/ {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_connect_timeout 5s;
        proxy_send_timeout    60s;
        proxy_read_timeout    65s;
        proxy_pass http://127.0.0.1:8000/;
    }

    # ===== 他のアプリケーション =====
    # bikefinder (Django)
    location ^~ /bikefinder/static/ {
        alias /home/ec2-user/bikefindAPP/motorcycle_finder/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    location ^~ /bikefinder/ {
        proxy_pass http://unix:/run/gunicorn-bikefinder/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # aiapp (Streamlit)
    location ^~ /aiapp/ {
        proxy_pass http://127.0.0.1:8501;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

7. フロントエンド調整(重要)

URLパスの修正が必要です:

// static/js/prediction.js

// 修正前:
fetch('/predict/', {

// 修正後:
fetch('/snow_deep/predict/', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': getCSRFToken()
    },
    body: JSON.stringify({
        resort: resortId,
        months: months
    })
})

8. データベースセットアップ

# ログディレクトリ作成
sudo mkdir -p /var/log/django /var/log/gunicorn
sudo chown ec2-user:ec2-user /var/log/django /var/log/gunicorn

# 仮想環境アクティベート
source venv313/bin/activate

# マイグレーション実行
python manage.py makemigrations --settings=snow_predict.settings_production
python manage.py migrate --settings=snow_predict.settings_production

# スーパーユーザー作成
python manage.py createsuperuser --settings=snow_predict.settings_production

# 静的ファイル収集
python manage.py collectstatic --noinput --settings=snow_predict.settings_production

9. サービス起動

# systemdサービス登録・開始
sudo systemctl daemon-reload
sudo systemctl enable snow_deep.service
sudo systemctl start snow_deep.service

# Nginx設定テスト・開始
sudo nginx -t
sudo systemctl restart nginx
sudo systemctl enable nginx

🔍 動作確認

1. サービス状態確認

# Djangoアプリ状態確認
sudo systemctl status snow_deep.service
sudo journalctl -u snow_deep.service -f

# Nginx状態確認
sudo systemctl status nginx
sudo nginx -t

# ポート確認
sudo netstat -tlnp | grep :8000
sudo netstat -tlnp | grep :80

2. ヘルスチェック

# ローカルヘルスチェック
curl -I http://localhost/snow_deep/health/
curl -I http://localhost:8000/health/

# 本番ヘルスチェック
curl -I https://hungryclimber.com/snow_deep/health/

3. アプリケーション動作確認

# メインページアクセス
curl -I https://hungryclimber.com/snow_deep/

# 静的ファイル確認
curl -I https://hungryclimber.com/snow_deep/static/css/style.css

🐛 よくあるトラブルと解決法

1. ModuleNotFoundError: No module named 'django'

原因: 仮想環境が正しく設定されていない

解決法:

# systemdサービスで仮想環境のパスを明示的に指定
# /etc/systemd/system/snow_deep.service の ExecStart を確認

ExecStart=/home/ec2-user/snow_deep/venv313/bin/gunicorn snow_predict.wsgi:application -c /home/ec2-user/snow_deep/gunicorn_production.conf.py

2. OperationalError: connection to server failed

原因: PostgreSQL接続設定の問題

解決法:

# settings_production.py の DATABASES 設定を確認
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST'),
        'PORT': os.environ.get('DB_PORT', '5432'),
        # OPTIONS は削除または適切に設定
    }
}

3. DisallowedHost at /

原因: ALLOWED_HOSTS設定が不適切

解決法:

# settings_production.py
ALLOWED_HOSTS = [
    'hungryclimber.com',
    'www.hungryclimber.com',
    '.amazonaws.com',  # ALB用
    'localhost',
    '127.0.0.1',
]

4. 静的ファイル404エラー

原因: Nginx設定または静的ファイル収集の問題

解決法:

# 静的ファイル再収集
python manage.py collectstatic --noinput --settings=snow_predict.settings_production

# Nginx設定確認
sudo nginx -t

# 権限確認
ls -la /home/ec2-user/snow_deep/staticfiles/

5. JavaScript SyntaxError(Ajax通信エラー)

原因: JSファイルのURLパスが間違っている、または fetch API の設定不備

解決法:

// prediction.js のURL修正とAjax設定
fetch('/snow_deep/predict/', {  // パスプレフィックス追加
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': getCSRFToken()  // CSRF対応
    },
    body: JSON.stringify(data)
})
.then(response => response.json())
.catch(error => console.error('Ajax error:', error));

📊 ログ管理

1. ログファイル場所

# Djangoアプリログ
tail -f /var/log/django/snow_deep.log

# Gunicornログ
tail -f /var/log/gunicorn/snow_deep_access.log
tail -f /var/log/gunicorn/snow_deep_error.log

# systemdログ
sudo journalctl -u snow_deep.service -f

# Nginxログ
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

2. ログローテーション設定

# /etc/logrotate.d/snow_deep
/var/log/django/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 644 ec2-user ec2-user
}

/var/log/gunicorn/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 644 ec2-user ec2-user
    postrotate
        systemctl reload snow_deep.service
    endscript
}

🔧 パフォーマンス最適化

1. Gunicorn最適化

# gunicorn_production.conf.py
workers = multiprocessing.cpu_count() * 2 + 1  # CPU数に応じて調整
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 50

2. Nginx最適化

# /etc/nginx/nginx.conf
worker_processes auto;
worker_connections 1024;

# gzip圧縮
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

# キープアライブ
keepalive_timeout 65;
keepalive_requests 100;

3. PostgreSQL最適化

-- RDSパラメータグループで設定
shared_preload_libraries = 'pg_stat_statements'
max_connections = 100
shared_buffers = 256MB
effective_cache_size = 1GB
work_mem = 4MB

🔒 セキュリティ設定

1. SSL/TLS証明書 (Let's Encrypt)

# Certbot インストール
sudo yum install -y certbot python3-certbot-nginx

# 証明書取得
sudo certbot --nginx -d hungryclimber.com -d www.hungryclimber.com

# 自動更新設定
sudo crontab -e
0 12 * * * /usr/bin/certbot renew --quiet

2. ファイアウォール設定

# Security Group設定 (AWS Console)
# HTTP: 80 - ALBからのみ
# HTTPS: 443 - ALBからのみ
# SSH: 22 - 管理者IPからのみ

3. Django セキュリティ設定

# settings_production.py
SECURE_SSL_REDIRECT = False  # ALBでSSL終端
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_FRAME_DENY = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

📈 監視・運用

1. ヘルスチェック実装

# prediction/views.py
def health_check(request):
    """ALB ヘルスチェック用エンドポイント"""
    try:
        # データベース接続確認
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1")
        
        return HttpResponse(
            "OK - Snow Deep Predict is running",
            status=200,
            content_type="text/plain"
        )
    except Exception as e:
        return HttpResponse(
            f"ERROR - {str(e)}",
            status=503,
            content_type="text/plain"
        )

2. 基本的な監視スクリプト

#!/bin/bash
# /home/ec2-user/scripts/monitor.sh

# サービス状態確認
if ! systemctl is-active --quiet snow_deep.service; then
    echo "$(date): Snow Deep service is down" >> /var/log/monitor.log
    sudo systemctl restart snow_deep.service
fi

# ディスク使用量確認
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ $DISK_USAGE -gt 80 ]; then
    echo "$(date): High disk usage: ${DISK_USAGE}%" >> /var/log/monitor.log
fi

# ログファイルサイズ確認
find /var/log -name "*.log" -size +100M -exec echo "$(date): Large log file: {}" \; >> /var/log/monitor.log

3. 定期メンテナンス

# crontab -e

# 毎日午前3時にログローテーション
0 3 * * * /usr/sbin/logrotate /etc/logrotate.conf

# 毎週日曜日午前2時にシステム更新確認
0 2 * * 0 /usr/bin/yum check-update

# 毎時監視スクリプト実行
0 * * * * /home/ec2-user/scripts/monitor.sh

🚀 デプロイ自動化

1. デプロイスクリプト

#!/bin/bash
# deploy.sh

set -e

echo "Starting deployment..."

# Git更新
git pull origin main

# 仮想環境アクティベート
source venv313/bin/activate

# 依存関係更新
pip install -r requirements_production.txt

# マイグレーション
python manage.py migrate --settings=snow_predict.settings_production

# 静的ファイル収集
python manage.py collectstatic --noinput --settings=snow_predict.settings_production

# サービス再起動
sudo systemctl restart snow_deep.service

echo "Deployment completed successfully!"

2. CI/CD連携 (GitHub Actions例)

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Deploy to EC2
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.HOST }}
        username: ec2-user
        key: ${{ secrets.KEY }}
        script: |
          cd /home/ec2-user/snow_deep
          ./deploy.sh

💰 コスト最適化

1. RDSコスト削減

  • Reserved Instance: 1年契約で30-60%削減
  • 適切なインスタンスサイズ: 過剰スペック避ける
  • 自動バックアップ期間: 必要最小限に設定

2. EC2コスト削減

  • Spot Instance: 開発環境で70-90%削減
  • Auto Scaling: 負荷に応じた自動調整
  • EBS最適化: gp3への移行でコスト削減

3. ALBコスト削減

  • Target Group最適化: 不要なヘルスチェック削減
  • リスナールール: 効率的なルーティング

🎯 まとめ

本記事では、DjangoアプリケーションをAWS ALB + EC2 + RDSの本格的な本番環境にデプロイする方法を詳しく解説しました。

学んだポイント

  1. 複数アプリ共存: Nginxでのパスベースルーティング
  2. 本番設定: セキュリティとパフォーマンスの両立
  3. Ajax通信: Vanilla JavaScript (fetch API) での非同期通信実装
  4. トラブルシューティング: よくある問題と対処法
  5. 運用・監視: 継続的な安定運用のための仕組み

ベストプラクティス

  • ✅ 環境変数で機密情報管理
  • ✅ 適切なログ設定とローテーション
  • ✅ ヘルスチェックとモニタリング
  • ✅ 自動デプロイとバックアップ
  • ✅ セキュリティ設定の徹底

次のステップ

  • Container化: Docker + ECS/EKSでの運用
  • CDN導入: CloudFrontでの高速化
  • monitoring強化: CloudWatch + Grafana
  • CI/CD: GitHub Actions/GitLab CI
  • Multi-AZ: 高可用性構成

🔗 関連リソース


質問やフィードバックがあれば、コメントでお気軽にお声かけください!

この記事が Django アプリの本番運用の参考になれば幸いです。

#Django #AWS #デプロイ #本番運用 #JavaScript #Ajax #ALB #EC2 #RDS #Nginx #Gunicorn #インフラ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?