🎨

AWS Lambda + Docker + ImageMagickでPSD→PNG自動変換システムを構築する

に公開

はじめに

デザイナーから受け取ったPSDファイルをWeb用のPNGに変換する作業、手動でやっていませんか?

本記事では、S3にPSDファイルをアップロードするだけで自動的にPNGに変換されるサーバーレスシステムを構築します。

この記事で構築するもの:

  • S3へのPSDアップロードをトリガーにLambdaが起動
  • Docker + ImageMagick + WandでPSD→PNG変換
  • 変換後のPNGを同じS3バケットに保存
  • AWS CodeBuildによるCI/CDパイプライン

対象読者:

  • AWSでサーバーレスアプリケーションを構築したい方
  • Lambda + Dockerの実践例を知りたい方
  • ImageMagickをLambdaで使いたい方

アーキテクチャ

┌─────────────────┐     ┌────────────────────────────────────┐     ┌─────────────────┐
│   S3 (Input)    │     │     Lambda (Docker Container)      │     │   S3 (Output)   │
│                 │     │                                    │     │                 │
│  *.psd upload   │────▶│  Python 3.12 + ImageMagick + Wand  │────▶│  converted/*.png│
│                 │     │                                    │     │                 │
└─────────────────┘     └────────────────────────────────────┘     └─────────────────┘

S3イベント通知でLambdaをトリガーし、変換結果を同じバケットのconverted/プレフィックス配下に出力します。

技術選定

なぜDockerイメージ?

Lambda標準ランタイムにはImageMagickが含まれていません。Lambda Layerでバイナリを追加する方法もありますが、以下の理由からDockerイメージを選択しました。

方式 メリット デメリット
Lambda Layer デプロイが軽量 依存関係の管理が複雑、サイズ制限
Docker 環境の再現性、ローカルテスト容易 イメージサイズが大きい

特にImageMagickのような複雑な依存関係を持つライブラリは、Dockerで管理する方が圧倒的に楽です。

なぜWand?

PythonからImageMagickを使う方法は主に2つあります。

1. subprocess(コマンド実行)

import subprocess
subprocess.run(['convert', 'input.psd[0]', 'output.png'])

2. Wand(Pythonバインディング)

from wand.image import Image
with Image(filename='input.psd[0]') as img:
    img.save(filename='output.png')

Wandを選んだ理由は以下の通りです。

  • Pythonic: with文でリソース管理、例外処理が自然
  • メタデータアクセス: img.width, img.colorspaceなどに直接アクセス
  • エラーハンドリング: ImageMagickのエラーがPython例外として扱える

実装のポイント

ロジック分離(テスタビリティ)

S3とのI/O処理と変換ロジックを分離することで、S3なしでもユニットテストが可能になります。

src/
├── app.py         # Lambda ハンドラー(S3 I/O層)
└── psd_logic.py   # 変換ロジック(純粋関数)

psd_logic.py(抜粋)

@dataclass
class ConversionResult:
    success: bool
    input_path: str
    output_path: Optional[str]
    # ... その他のメタデータ

def convert_psd_to_png(
    input_path: str,
    output_path: str,
    options: ConversionOptions
) -> ConversionResult:
    """PSDをPNGに変換(S3に依存しない純粋関数)"""
    with Image(filename=f"{input_path}[0]") as img:
        # 変換処理
        img.format = 'png'
        img.save(filename=output_path)

app.py(抜粋)

def lambda_handler(event, context):
    # S3からダウンロード
    s3.download_file(bucket, key, local_input)
    
    # 変換(psd_logicを呼び出し)
    result = convert_psd_to_png(local_input, local_output, options)
    
    # S3にアップロード
    s3.upload_file(local_output, output_bucket, output_key)

この設計により、テストコードはS3モックなしで動作します。

def test_convert_basic(sample_psd, tmp_path):
    """変換ロジックのみをテスト(S3不要)"""
    output = tmp_path / "output.png"
    result = convert_psd_to_png(str(sample_psd), str(output), ConversionOptions())
    assert result.success
    assert output.exists()

PSDの[0]指定

PSDファイルはマルチレイヤー構造を持ちます。ImageMagickでは[0]を指定することで「統合イメージ(Composite)」を取得できます。

# [0]なし → 全レイヤーを個別に処理(意図しない結果に)
Image(filename='input.psd')

# [0]あり → 統合イメージのみ取得(通常はこちら)
Image(filename='input.psd[0]')

CMYK対応

印刷用に作成されたPSDファイルはCMYKカラースペースの場合があります。これをそのままPNGにすると色がおかしくなります。

if img.colorspace == 'cmyk':
    logger.warning(f"CMYK detected, converting to sRGB: {input_path}")
    img.transform_colorspace('srgb')

CMYKを検出したらsRGBに変換し、ログに警告を出力します。

無限ループ防止

S3イベントトリガーで注意すべきは「無限ループ」です。変換後のPNGをS3にアップロードすると、それがまたトリガーになってしまう可能性があります。

対策として、以下の2重チェックを行っています。

1. S3イベントフィルター(template.yaml)

Events:
  S3PsdUpload:
    Type: S3
    Properties:
      Bucket: !Ref InputBucket
      Events: s3:ObjectCreated:*
      Filter:
        S3Key:
          Rules:
            - Name: suffix
              Value: .psd  # PSDファイルのみ

2. Lambdaコード内チェック

if key.startswith(OUTPUT_PREFIX):
    logger.info(f"Skipping output file: {key}")
    return

マルチステージDockerビルド

本番イメージからテストコードを除外するため、マルチステージビルドを採用しています。

# Stage 1: base - 共通基盤
FROM public.ecr.aws/lambda/python:3.12 AS base
RUN dnf install -y ImageMagick ImageMagick-devel && dnf clean all
COPY requirements.txt ${LAMBDA_TASK_ROOT}/
RUN pip install --no-cache-dir -r requirements.txt

# Stage 2: test - テスト環境
FROM base AS test
COPY requirements-dev.txt ${LAMBDA_TASK_ROOT}/
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY src/ ${LAMBDA_TASK_ROOT}/
COPY tests/ ${LAMBDA_TASK_ROOT}/tests/
ENTRYPOINT ["python", "-m"]
CMD ["pytest", "tests/", "-v"]

# Stage 3: production - 本番環境
FROM base AS production
COPY src/ ${LAMBDA_TASK_ROOT}/
CMD ["app.lambda_handler"]

テスト実行は--target test、本番ビルドは--target productionで切り替えます。

デプロイ手順

ソースコードは以下からダウンロードできます。

https://github.com/toshiro3/lambda-psd-converter-demo

前提条件

  • AWS CLI v2(認証設定済み)
  • Docker
  • 約10分の時間

Step 1: ソースコードの準備

# ダウンロード&展開
unzip lambda-psd-converter-demo.zip
cd lambda-psd-converter-demo

Step 2: ローカルテスト(オプション)

# Dockerイメージビルド&テスト実行
make test

14件のテストがパスすればOKです。

Step 3: ソースコード用S3バケット作成

# 変数設定
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
SOURCE_BUCKET="psd-converter-source-${ACCOUNT_ID}"
INPUT_BUCKET="psd-input-${ACCOUNT_ID}"

# バケット作成
aws s3 mb s3://${SOURCE_BUCKET}

Step 4: ソースコードをアップロード

zip -r lambda-psd-converter-demo.zip . \
  -x ".git/*" -x "test-output/*" -x ".aws-sam/*" -x "*.zip"
aws s3 cp lambda-psd-converter-demo.zip s3://${SOURCE_BUCKET}/

Step 5: CodeBuildプロジェクト作成

aws cloudformation deploy \
  --template-file codebuild-stack.yaml \
  --stack-name psd-converter-codebuild \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    SourceBucketName=${SOURCE_BUCKET} \
    InputBucketName=${INPUT_BUCKET}

Step 6: ビルド実行

aws codebuild start-build --project-name psd-converter-build

ビルドには約3〜5分かかります。進捗はCloudWatchログで確認できます。

aws logs tail /aws/codebuild/psd-converter-build --follow

Step 7: 動作確認

テスト用のPSDファイルを生成してアップロードします。

# テスト用PSD生成(Dockerを利用)
docker build --target production -t psd-converter:latest .
docker run --rm \
  -v "$(pwd):/work" \
  --entrypoint convert \
  psd-converter:latest \
  -size 500x500 xc:red -fill blue -draw "circle 250,250 250,100" /work/sample.psd

# S3にアップロード
aws s3 cp sample.psd s3://${INPUT_BUCKET}/

# 変換結果を確認(数秒待つ)
sleep 5
aws s3 ls s3://${INPUT_BUCKET}/converted/

converted/sample.pngが表示されれば成功です!

クリーンアップ

検証が終わったら、以下のコマンドでリソースを削除します。

# S3バケットの中身を削除
aws s3api delete-objects \
  --bucket ${INPUT_BUCKET} \
  --delete "$(aws s3api list-object-versions \
    --bucket ${INPUT_BUCKET} \
    --query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}' \
    --output json)"

# スタック削除
aws cloudformation delete-stack --stack-name psd-converter-dev
aws cloudformation wait stack-delete-complete --stack-name psd-converter-dev

aws cloudformation delete-stack --stack-name psd-converter-codebuild
aws cloudformation wait stack-delete-complete --stack-name psd-converter-codebuild

# ECR削除
aws ecr delete-repository --repository-name psd-converter --force

# ソースバケット削除
aws s3 rm s3://${SOURCE_BUCKET} --recursive
aws s3 rb s3://${SOURCE_BUCKET}

パフォーマンスとコスト

実行結果

項目
入力サイズ 4MB (500x500 PSD)
出力サイズ 190KB (PNG)
変換時間 約800ms
コールドスタート 約4.5秒
メモリ使用量 120MB / 2048MB

コスト見積もり(月間1,000ファイル処理の場合)

リソース 計算 月額
Lambda 1000回 × 5秒 × 2GB 約$0.17
S3 1000 PUT + 1000 GET 約$0.01
ECR 500MB 約$0.05
合計 約$0.23

ほぼ無料で運用できます。

トラブルシューティング

ImageMagick policy エラー

wand.exceptions.PolicyError: not allowed by the security policy

ImageMagickはセキュリティ上の理由から、デフォルトでPSD/PSフォーマットの処理やリソース使用量を制限しています。Dockerfileでpolicy.xmlを修正して制限を解除しています。

# ImageMagick の policy.xml を修正
RUN POLICY_FILE=$(find /etc -name "policy.xml" 2>/dev/null | head -1) && \
    if [ -n "$POLICY_FILE" ]; then \
        # PSD/PS の読み書きを許可
        sed -i 's/<policy domain="coder" rights="none" pattern="PS" \/>/<policy domain="coder" rights="read|write" pattern="PS" \/>/g' "$POLICY_FILE" && \
        sed -i 's/<policy domain="coder" rights="none" pattern="PSD" \/>/<policy domain="coder" rights="read|write" pattern="PSD" \/>/g' "$POLICY_FILE" && \
        # メモリ制限を緩和(大きな PSD 対応)
        sed -i 's/<policy domain="resource" name="memory" value="[^"]*"\/>/<policy domain="resource" name="memory" value="2GiB"\/>/g' "$POLICY_FILE" && \
        sed -i 's/<policy domain="resource" name="map" value="[^"]*"\/>/<policy domain="resource" name="map" value="4GiB"\/>/g' "$POLICY_FILE" && \
        sed -i 's/<policy domain="resource" name="disk" value="[^"]*"\/>/<policy domain="resource" name="disk" value="8GiB"\/>/g' "$POLICY_FILE"; \
    fi

設定はmake check-imagemagickで確認できます。

メモリ不足 / タイムアウト / ストレージ不足

大きなPSDファイル(100MB以上)を処理する場合は、以下の点に注意してください。

エフェメラルストレージ(/tmp)について

Lambdaの/tmp領域はデフォルトで512MBです。最近の複雑なPSD(スマートオブジェクトや高解像度データを含むもの)は数GBに達することがあり、s3.download_fileの時点でディスク容量不足になる可能性があります。

template.yamlで以下を調整してください。

Globals:
  Function:
    Timeout: 300       # 最大900秒
    MemorySize: 4096   # 最大10240MB
    EphemeralStorage:
      Size: 5120       # 最大10240MB(デフォルト512MB)
PSDサイズ 推奨メモリ 推奨/tmp
~10MB 1024MB 512MB
~50MB 2048MB 1024MB
~100MB 3072MB 2048MB
100MB~ 4096MB+ 5120MB+

まとめ

本記事では、AWS Lambda + Docker + ImageMagick(Wand)を使ったPSD→PNG自動変換システムを構築しました。

ポイント:

  • Lambda + Dockerで複雑な依存関係を管理
  • Wandで Pythonic な画像処理
  • ロジック分離でテスタビリティ向上
  • CodeBuildでCI/CDパイプライン構築

このアーキテクチャは、PSD以外にも様々なフォーマット変換に応用できます。ぜひ試してみてください!

参考

GitHubで編集を提案

Discussion