Python DashアプリをAWS Lambdaにデプロイする完全ガイド

AWS CLIを使用したデプロイ方法

AWS CLIを使用したPython Dashアプリのデプロイ

AWS CLIによるデプロイの概要

AWS CLIを使用したデプロイは、最も柔軟性が高く、完全なカスタマイズが可能なアプローチです。このアプローチでは、AWS CLIコマンドを直接使用して、Lambda関数の作成、設定、デプロイを行います。

このアプローチの主な特徴は以下の通りです:

前提条件

AWS CLIのインストールと設定

# AWS CLIのインストール
pip install awscli

# AWS CLIの設定
aws configure

設定時に以下の情報を入力します:

IAMロールの作成

まず、Lambda関数が使用するIAMロールを作成します。このロールには、Lambda関数の実行に必要な権限を付与します。

1. 信頼ポリシーの作成

trust-policy.jsonというファイルを作成し、以下の内容を記述します:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

2. IAMロールの作成

aws iam create-role \
  --role-name DashLambdaRole \
  --assume-role-policy-document file://trust-policy.json

このコマンドの出力には、作成されたロールのARNが含まれています。このARNは後で使用するため、メモしておきます。

3. ポリシーのアタッチ

aws iam attach-role-policy \
  --role-name DashLambdaRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

必要に応じて、追加のポリシーをアタッチすることもできます。例えば、DynamoDBにアクセスする場合は、以下のコマンドを実行します:

aws iam attach-role-policy \
  --role-name DashLambdaRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess

Dashアプリケーションの作成

1. プロジェクトディレクトリの作成

mkdir -p dash-lambda-app

2. Dashアプリケーションの作成

dash-lambda-app/app.pyというファイルを作成し、以下の内容を記述します:

import plotly.express as px
import dash
from dash import html, dcc
from dash.dependencies import Input, Output
import json
import base64

# サンプルデータを読み込む
df = px.data.gapminder()

# アプリケーションを作成する
app = dash.Dash(__name__)

# グラフを作成する関数
def create_graph(data, x_col, y_col):
    fig = px.scatter(data, x=x_col, y=y_col, color='continent',
                     log_x=True, size_max=60, title=f"{y_col} vs {x_col}")
    return fig

# レイアウトを定義する
app.layout = html.Div([
    html.H1("Dash App on AWS Lambda"),
    html.Div([
        dcc.Graph(id='graph-1')
    ]),
    html.Div([
        dcc.Dropdown(
            id='x-column',
            options=[{'label': i, 'value': i} for i in df.columns],
            value='gdpPercap'
        )
    ], style={'width': '25%', 'display': 'inline-block'}),
    html.Div([
        dcc.Dropdown(
            id='y-column',
            options=[{'label': i, 'value': i} for i in df.columns],
            value='lifeExp'
        )
    ], style={'width': '25%', 'display': 'inline-block'}),
])

# コールバックを定義する
@app.callback(
    Output('graph-1', 'figure'),
    [Input('x-column', 'value'),
     Input('y-column', 'value')])
def update_graph(x_col, y_col):
    fig = create_graph(df, x_col, y_col)
    return fig

# Lambda関数のハンドラー
def lambda_handler(event, context):
    # API Gatewayからのイベントを処理
    path = event.get('path', '/')
    http_method = event.get('httpMethod', 'GET')
    headers = event.get('headers', {})
    query_string_parameters = event.get('queryStringParameters', {})
    
    # WSGIアプリケーションを実行
    from werkzeug.wrappers import Request
    from werkzeug.wrappers import Response
    from werkzeug.serving import run_simple
    
    # リクエストの作成
    environ = {
        'PATH_INFO': path,
        'REQUEST_METHOD': http_method,
        'QUERY_STRING': '',
        'SERVER_NAME': 'localhost',
        'SERVER_PORT': '80',
        'wsgi.url_scheme': 'http',
        'wsgi.input': '',
        'wsgi.errors': '',
        'wsgi.multithread': False,
        'wsgi.multiprocess': False,
        'wsgi.run_once': False,
    }
    
    # クエリパラメータの追加
    if query_string_parameters:
        query_string = '&'.join([f"{k}={v}" for k, v in query_string_parameters.items()])
        environ['QUERY_STRING'] = query_string
    
    # ヘッダーの追加
    for key, value in headers.items():
        key = key.upper().replace('-', '_')
        if key not in ('CONTENT_TYPE', 'CONTENT_LENGTH'):
            key = 'HTTP_' + key
        environ[key] = value
    
    # リクエストボディの追加
    if 'body' in event and event['body']:
        body = event['body']
        if event.get('isBase64Encoded', False):
            body = base64.b64decode(body)
        environ['wsgi.input'] = body
        environ['CONTENT_LENGTH'] = str(len(body))
    
    # レスポンスの取得
    response_data = {}
    
    def start_response(status, response_headers, exc_info=None):
        status_code = int(status.split(' ')[0])
        response_data['statusCode'] = status_code
        response_data['headers'] = dict(response_headers)
        return lambda x: None
    
    # アプリケーションの実行
    response_body = b''.join(app.server(environ, start_response))
    
    # レスポンスの作成
    response_data['body'] = response_body.decode('utf-8')
    response_data['isBase64Encoded'] = False
    
    return response_data

# ローカルテスト用
if __name__ == '__main__':
    app.run_server(debug=True, host="0.0.0.0")

重要なポイント:

  • lambda_handler関数は、API GatewayからのイベントをWSGIアプリケーションに変換し、レスポンスを返します。
  • このコードは簡略化されており、実際の環境ではaws-wsgiなどのライブラリを使用することをお勧めします。

3. 依存関係ファイルの作成

dash-lambda-app/requirements.txtというファイルを作成し、以下の内容を記述します:

dash==2.14.0
plotly==5.18.0
pandas==2.1.1
werkzeug==2.3.7
flask==2.2.5

デプロイメントパッケージの作成

1. 仮想環境の作成

cd dash-lambda-app
python3 -m venv venv
source venv/bin/activate

2. 依存関係のインストール

pip install -r requirements.txt

3. パッケージディレクトリの作成

mkdir -p package

4. 依存関係のコピー

pip install -t package -r requirements.txt

5. アプリケーションコードのコピー

cp app.py package/

6. ZIPファイルの作成

cd package
zip -r ../dash-lambda-package.zip .
cd ..

S3バケットへのアップロード

1. S3バケットの作成

aws s3 mb s3://dash-lambda-deployment-bucket

バケット名はグローバルに一意である必要があります。適切な名前に変更してください。

2. デプロイメントパッケージのアップロード

aws s3 cp dash-lambda-package.zip s3://dash-lambda-deployment-bucket/

Lambda関数の作成

aws lambda create-function \
  --function-name dash-lambda-app \
  --runtime python3.9 \
  --role arn:aws:iam::123456789012:role/DashLambdaRole \
  --handler app.lambda_handler \
  --timeout 30 \
  --memory-size 512 \
  --code S3Bucket=dash-lambda-deployment-bucket,S3Key=dash-lambda-package.zip

以下のパラメータを適切に設定してください:

  • --role: 先ほど作成したIAMロールのARN
  • --runtime: 使用するPythonのバージョン
  • --handler: Lambda関数のエントリーポイント
  • --timeout: 関数のタイムアウト時間(秒)
  • --memory-size: 関数のメモリサイズ(MB)
  • --code: デプロイメントパッケージのS3バケットとキー

API Gatewayの設定

1. REST APIの作成

aws apigateway create-rest-api \
  --name "Dash Lambda API" \
  --description "API for Dash Lambda application"

このコマンドの出力には、作成されたAPIのIDが含まれています。このIDは後で使用するため、メモしておきます。

2. ルートリソースIDの取得

aws apigateway get-resources \
  --rest-api-id abcdef1234

このコマンドの出力には、ルートリソースのIDが含まれています。このIDは後で使用するため、メモしておきます。

3. プロキシリソースの作成

aws apigateway create-resource \
  --rest-api-id abcdef1234 \
  --parent-id rootResourceId \
  --path-part "{proxy+}"

このコマンドの出力には、作成されたリソースのIDが含まれています。このIDは後で使用するため、メモしておきます。

4. ANY メソッドの作成(ルートリソース)

aws apigateway put-method \
  --rest-api-id abcdef1234 \
  --resource-id rootResourceId \
  --http-method ANY \
  --authorization-type NONE

5. Lambda統合の設定(ルートリソース)

aws apigateway put-integration \
  --rest-api-id abcdef1234 \
  --resource-id rootResourceId \
  --http-method ANY \
  --type AWS_PROXY \
  --integration-http-method POST \
  --uri arn:aws:apigateway:region:lambda:path/2015-03-31/functions/arn:aws:lambda:region:123456789012:function:dash-lambda-app/invocations

6. ANY メソッドの作成(プロキシリソース)

aws apigateway put-method \
  --rest-api-id abcdef1234 \
  --resource-id proxyResourceId \
  --http-method ANY \
  --authorization-type NONE

7. Lambda統合の設定(プロキシリソース)

aws apigateway put-integration \
  --rest-api-id abcdef1234 \
  --resource-id proxyResourceId \
  --http-method ANY \
  --type AWS_PROXY \
  --integration-http-method POST \
  --uri arn:aws:apigateway:region:lambda:path/2015-03-31/functions/arn:aws:lambda:region:123456789012:function:dash-lambda-app/invocations

8. Lambda関数の権限設定

aws lambda add-permission \
  --function-name dash-lambda-app \
  --statement-id apigateway-root \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:region:123456789012:abcdef1234/*/ANY/"

aws lambda add-permission \
  --function-name dash-lambda-app \
  --statement-id apigateway-proxy \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:region:123456789012:abcdef1234/*/ANY/{proxy+}"

9. APIのデプロイ

aws apigateway create-deployment \
  --rest-api-id abcdef1234 \
  --stage-name prod

APIがデプロイされると、以下のURLでアクセスできます:

https://abcdef1234.execute-api.region.amazonaws.com/prod/

Lambda関数の更新

アプリケーションを更新する場合は、以下の手順を実行します:

1. コードの変更

必要に応じてアプリケーションコードを変更します。

2. 新しいデプロイメントパッケージの作成

cd package
rm -rf *
pip install -t . -r ../requirements.txt
cp ../app.py .
zip -r ../dash-lambda-package-v2.zip .
cd ..

3. S3バケットへのアップロード

aws s3 cp dash-lambda-package-v2.zip s3://dash-lambda-deployment-bucket/

4. Lambda関数の更新

aws lambda update-function-code \
  --function-name dash-lambda-app \
  --s3-bucket dash-lambda-deployment-bucket \
  --s3-key dash-lambda-package-v2.zip

リソースの削除

不要になったリソースを削除する場合は、以下の手順を実行します:

1. API Gatewayの削除

aws apigateway delete-rest-api \
  --rest-api-id abcdef1234

2. Lambda関数の削除

aws lambda delete-function \
  --function-name dash-lambda-app

3. IAMロールの削除

aws iam detach-role-policy \
  --role-name DashLambdaRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

aws iam delete-role \
  --role-name DashLambdaRole

4. S3バケットの削除

aws s3 rm s3://dash-lambda-deployment-bucket --recursive
aws s3 rb s3://dash-lambda-deployment-bucket

デプロイの自動化

上記の手順をシェルスクリプトにまとめることで、デプロイを自動化することができます。以下は、基本的なデプロイスクリプトの例です:

#!/bin/bash

# 変数の設定
FUNCTION_NAME="dash-lambda-app"
BUCKET_NAME="dash-lambda-deployment-bucket"
ROLE_NAME="DashLambdaRole"
API_NAME="Dash Lambda API"
STAGE_NAME="prod"
REGION="ap-northeast-1"
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)

# S3バケットの作成(存在しない場合)
aws s3 mb s3://$BUCKET_NAME --region $REGION || true

# デプロイメントパッケージの作成
mkdir -p package
pip install -t package -r requirements.txt
cp app.py package/
cd package
zip -r ../dash-lambda-package.zip .
cd ..

# S3バケットへのアップロード
aws s3 cp dash-lambda-package.zip s3://$BUCKET_NAME/

# IAMロールの作成(存在しない場合)
ROLE_ARN=$(aws iam get-role --role-name $ROLE_NAME --query "Role.Arn" --output text 2>/dev/null || echo "")

if [ -z "$ROLE_ARN" ]; then
  echo "Creating IAM role..."
  cat > trust-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

  ROLE_ARN=$(aws iam create-role \
    --role-name $ROLE_NAME \
    --assume-role-policy-document file://trust-policy.json \
    --query "Role.Arn" \
    --output text)

  aws iam attach-role-policy \
    --role-name $ROLE_NAME \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  # IAMロールが有効になるまで待機
  echo "Waiting for IAM role to be ready..."
  sleep 10
fi

# Lambda関数の作成または更新
FUNCTION_EXISTS=$(aws lambda get-function --function-name $FUNCTION_NAME 2>/dev/null || echo "")

if [ -z "$FUNCTION_EXISTS" ]; then
  echo "Creating Lambda function..."
  aws lambda create-function \
    --function-name $FUNCTION_NAME \
    --runtime python3.9 \
    --role $ROLE_ARN \
    --handler app.lambda_handler \
    --timeout 30 \
    --memory-size 512 \
    --code S3Bucket=$BUCKET_NAME,S3Key=dash-lambda-package.zip
else
  echo "Updating Lambda function..."
  aws lambda update-function-code \
    --function-name $FUNCTION_NAME \
    --s3-bucket $BUCKET_NAME \
    --s3-key dash-lambda-package.zip
fi

# API Gatewayの作成
API_ID=$(aws apigateway get-rest-apis --query "items[?name=='$API_NAME'].id" --output text)

if [ -z "$API_ID" ]; then
  echo "Creating API Gateway..."
  API_ID=$(aws apigateway create-rest-api \
    --name "$API_NAME" \
    --description "API for Dash Lambda application" \
    --query "id" \
    --output text)

  # ルートリソースIDの取得
  ROOT_RESOURCE_ID=$(aws apigateway get-resources \
    --rest-api-id $API_ID \
    --query "items[?path=='/'].id" \
    --output text)

  # プロキシリソースの作成
  PROXY_RESOURCE_ID=$(aws apigateway create-resource \
    --rest-api-id $API_ID \
    --parent-id $ROOT_RESOURCE_ID \
    --path-part "{proxy+}" \
    --query "id" \
    --output text)

  # ANY メソッドの作成(ルートリソース)
  aws apigateway put-method \
    --rest-api-id $API_ID \
    --resource-id $ROOT_RESOURCE_ID \
    --http-method ANY \
    --authorization-type NONE

  # Lambda統合の設定(ルートリソース)
  aws apigateway put-integration \
    --rest-api-id $API_ID \
    --resource-id $ROOT_RESOURCE_ID \
    --http-method ANY \
    --type AWS_PROXY \
    --integration-http-method POST \
    --uri arn:aws:apigateway:$REGION:lambda:path/2015-03-31/functions/arn:aws:lambda:$REGION:$ACCOUNT_ID:function:$FUNCTION_NAME/invocations

  # ANY メソッドの作成(プロキシリソース)
  aws apigateway put-method \
    --rest-api-id $API_ID \
    --resource-id $PROXY_RESOURCE_ID \
    --http-method ANY \
    --authorization-type NONE

  # Lambda統合の設定(プロキシリソース)
  aws apigateway put-integration \
    --rest-api-id $API_ID \
    --resource-id $PROXY_RESOURCE_ID \
    --http-method ANY \
    --type AWS_PROXY \
    --integration-http-method POST \
    --uri arn:aws:apigateway:$REGION:lambda:path/2015-03-31/functions/arn:aws:lambda:$REGION:$ACCOUNT_ID:function:$FUNCTION_NAME/invocations

  # Lambda関数の権限設定
  aws lambda add-permission \
    --function-name $FUNCTION_NAME \
    --statement-id apigateway-root \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --source-arn "arn:aws:execute-api:$REGION:$ACCOUNT_ID:$API_ID/*/ANY/"

  aws lambda add-permission \
    --function-name $FUNCTION_NAME \
    --statement-id apigateway-proxy \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --source-arn "arn:aws:execute-api:$REGION:$ACCOUNT_ID:$API_ID/*/ANY/{proxy+}"
fi

# APIのデプロイ
aws apigateway create-deployment \
  --rest-api-id $API_ID \
  --stage-name $STAGE_NAME

# デプロイされたURLの表示
echo "Deployment completed. Your API is available at:"
echo "https://$API_ID.execute-api.$REGION.amazonaws.com/$STAGE_NAME/"

このスクリプトをdeploy.shとして保存し、実行権限を付与します:

chmod +x deploy.sh

デプロイを実行するには、以下のコマンドを実行します:

./deploy.sh