AWS Lambdaにblenderを載せてサーバーレスなレンダリングサーバーを作る
初めまして、株式会社Berryの齋藤です。
みなさまLambdaはやっておりますでしょうか。
Berryでも3Dデータの自動処理を行う上で数多くのLambda関数を作成、運用しています。
その中で3Dデータのプレビュー生成が必要になったため、blenderによるプレビュー生成を行うことにしました。
通常であればEC2を使い、レンダリングサーバーを立てることが一般的かと思いますが、費用面・運用面を考慮し、Lambdaによるサーバーレスなレンダリングサーバーを作成することにしました。
非常にニッチなユースケースですが、ざっと検索したところ日本語の情報が少なかったので、今回はblenderをLambda上で動かす方法を紹介したいと思います。
サンプルリポジトリ
前提条件
- AWS CLIとAWSアカウントが設定済み
- Dockerインストール済み
(x64のCPUで検証しています。armの場合はダウンロードするblenderやlambdaデプロイ時の設定をいじる必要があるかもしれません)
構成
AWS Lambdaでは、コンテナイメージを使った関数を作成することができます。
このコンテナイメージ、Lambdaのランタイムインターフェースクライアント(RIC)さえ入っていればAWS謹製のベースイメージ以外でも作成することができます。
つまり大体のことがLambda上でできるということです。
ということで使い慣れたubuntuベースのイメージにblenderを載せていきましょう。
処理の流れとしては下記の流れになります。
- Lambdaハンドラで受け取ったリクエストを展開し、blenderのスクリプトに渡す引数を取得する
- blenderで読み込ませるobjファイルをS3からダウンロードする
- blenderスクリプトをsubprocessで呼び出す
-
--background --python ${pythonスクリプト}
という形で実行することができます。
-
- 出力ファイルをS3にアップロードする
そのためにやることは下記の通りです
- 処理の流れを実装する
- Lambda-RICとblenderが入ったdockerイメージをビルドする
- dockerイメージをAWSのdockerリポジトリ(ECR)にプッシュする
- プッシュしたイメージを使ってLambdaを作成する
- テストデータをS3バケットにアップロードしてテストする
準備
blenderのダウンロード
wget https://mirror.freedif.org/blender/release/Blender4.2/blender-4.2.1-linux-x64.tar.xz
lambdaハンドラで使用されるpythonコードを作成
- srcLambda/app.py
import os
from uuid import uuid4
import shutil
from typing import Any
import boto3
from invoke_blender_rendering import invoke_blender_rendering
def download(bucket_name: str, key: str, file_path: str):
s3 = boto3.client("s3")
dst_dir = os.path.dirname(file_path)
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
s3.download_file(bucket_name, key, file_path)
def upload(bucket_name: str, key: str, file_path: str):
s3 = boto3.client("s3")
s3.upload_file(file_path, bucket_name, key)
def lambda_handler(event: dict[str, Any], context: Any) -> dict[str, str]:
# イベントの内容を取得
bucket_name = event.get('bucket_name')
assert isinstance(bucket_name, str)
obj_key = event.get('obj_key')
assert isinstance(obj_key, str)
mtl_key = event.get('mtl_key')
assert isinstance(mtl_key, str)
texture_key = event.get('texture_key')
assert isinstance(texture_key, str)
upload_dst_key = event.get('upload_dst_key')
assert isinstance(upload_dst_key, str)
img_size = event.get('img_size', 512)
assert isinstance(img_size, int) or isinstance(img_size, float)
img_size: int = int(img_size)
# 作業ディレクトリを作成
tmp_path = f'/tmp/{uuid4()}'
src_path = f'{tmp_path}/src'
dst_path = f'{tmp_path}/dst'
os.makedirs(tmp_path)
os.makedirs(src_path)
os.makedirs(dst_path)
# エラーが起きた場合もtmpディレクトリを削除したいのでtry-finallyで囲む
try:
local_obj_path = f'{src_path}/{os.path.basename(obj_key)}'
local_mtl_path = f'{src_path}/{os.path.basename(mtl_key)}'
local_texture_path = f'{src_path}/{os.path.basename(texture_key)}'
local_dst_path = f'{dst_path}/{os.path.basename(upload_dst_key)}'
# データをダウンロード
download(bucket_name, obj_key, local_obj_path)
download(bucket_name, mtl_key, local_mtl_path)
download(bucket_name, texture_key, local_texture_path)
# メインの処理
invoke_blender_rendering(
local_obj_path,
local_dst_path,
img_size
)
# アップロードとローカルファイルの削除
upload(bucket_name, upload_dst_key, local_dst_path)
finally:
# 保存したファイルをそのままにするとリクエストが重なった時にリクエストが失敗するので削除する
shutil.rmtree(tmp_path)
return {
'bucket_name': bucket_name,
'result_image_key': upload_dst_key,
}
- srcLambda/invoke_blender_rendering.py
import subprocess
import os
def invoke_blender_rendering(
obj_path: str,
dst_path: str,
img_size: int
):
cmd = get_cmd(
obj_path,
dst_path,
img_size
)
result = subprocess.run(cmd)
if result.returncode != 0:
print(result.stderr)
raise RuntimeError(f"Failed to render: {result.returncode}")
else:
print("Rendered successfully")
def get_cmd(
obj_path: str,
dst_path: str,
img_size: int
) -> list[str]:
blender_path = os.getenv("BLENDER_PATH", "/usr/local/blender/blender")
assert os.path.exists(blender_path), f"BLENDER_PATH does not exist: {blender_path}"
script_path = os.getenv("BLENDER_SCRIPT_PATH", "/app/srcBlender/render.py")
assert os.path.exists(script_path), f"BLENDER_SCRIPT_PATH does not exist: {script_path}"
template_blend_path = os.getenv("BLENDER_TEMPLATE_PATH", "/app/srcBlender/template.blend")
assert os.path.exists(template_blend_path), f"BLENDER_TEMPLATE_PATH does not exist: {template_blend_path}"
return [
blender_path,
"--background",
"--python", script_path,
"--",
"--obj_path", obj_path,
"--dst_path", dst_path,
"--template_blend_path", template_blend_path,
"--outputSize", f"{img_size}"
]
blenderで使用するスクリプトの作成
シーン構築を全てスクリプトで行うのはきついので、blendファイルを読み込むようにしています。
- srcBlender/render.py
import os
import bpy # type: ignore
from mathutils import Vector # type: ignore
from typing import NamedTuple
class RenderCfg(NamedTuple):
obj_path: str
dst_path: str
template_blend_path: str
outputSize: int
def import_geo_file(file_path: str): # type: ignore
ext = os.path.splitext(file_path)[1]
assert ext in [".obj", ".OBJ"], f"Unsupported file type: {ext}"
bpy.ops.wm.obj_import(filepath=file_path) # type: ignore
def render_main(cfg: RenderCfg):
bpy.ops.wm.open_mainfile(filepath=cfg.template_blend_path) # type: ignore
bpy.context.scene.render.resolution_x = cfg.outputSize # type: ignore
bpy.context.scene.render.resolution_y = cfg.outputSize # type: ignore
# シーンのカメラを設定
camera = bpy.data.objects["Camera"] # type: ignore
bpy.context.scene.camera = camera # type: ignore
bpy.context.scene.frame_start = 1 # type: ignore
bpy.context.scene.frame_end = 1 # type: ignore
# レンダリングサンプリングに設定
bpy.data.scenes["Scene"].eevee.taa_render_samples = 1 # type: ignore
# レンダリング出力先を設定
bpy.context.scene.render.filepath = cfg.dst_path # type: ignore
ext = os.path.splitext(cfg.dst_path)[1]
# 出力をマルチレイヤーEXRに設定
if ext.lower() == ".exr":
bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' # type: ignore
elif ext.lower() == ".png":
bpy.context.scene.render.image_settings.file_format = 'PNG' # type: ignore
elif ext.lower() == ".jpg":
bpy.context.scene.render.image_settings.file_format = 'JPEG' # type: ignore
else:
raise ValueError(f"Unsupported file extension: {ext}")
import_geo_file(cfg.obj_path)
# アニメーションをレンダリング
bpy.ops.render.render(animation=False, write_still=True) # type: ignore
if __name__ == '__main__':
import sys
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--obj_path', type=str, required=True)
parser.add_argument('--dst_path', type=str, required=True)
parser.add_argument('--template_blend_path', type=str, required=True)
parser.add_argument('--outputSize', type=int, required=True)
args = parser.parse_args(sys.argv[sys.argv.index('--') + 1:])
cfg = RenderCfg(
obj_path=args.obj_path,
dst_path=args.dst_path,
template_blend_path=args.template_blend_path,
outputSize=args.outputSize
)
render_main(cfg)
docker fileの作成
FROM ubuntu:22.04 AS blender-installer
RUN apt-get update
RUN apt-get install -y xz-utils
RUN mkdir /tmp/blender
WORKDIR /tmp/blender
# blenderのバイナリはいい感じに持ってきておいてください。
COPY ./cache/blender-4.2.1-linux-x64.tar.xz /tmp/blender/
RUN tar -xvf "./blender-4.2.1-linux-x64.tar.xz"
FROM ubuntu:22.04 as runtime
RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN apt-get update && apt-get upgrade -y
ENV LAMBDA_TASK_ROOT="/app"
ENV FUNCTION_DIR="${LAMBDA_TASK_ROOT}"
RUN mkdir -p ${LAMBDA_TASK_ROOT}
WORKDIR ${LAMBDA_TASK_ROOT}
ENV TZ=Asia/Tokyo
# blender依存関係のインストール
RUN apt-get install glibc-source -y
RUN apt-get install -y libsm6 libxext6
RUN apt-get install -y libx11-dev libxxf86vm-dev libxcursor-dev libxi-dev libxrandr-dev libxinerama-dev libegl-dev
RUN apt-get install -y libwayland-dev wayland-protocols libxkbcommon-dev libdbus-1-dev linux-libc-dev
# blender本体のインストール
ENV BLENDER_DIR="/usr/local/blender"
ENV BLENDER_PATH="${BLENDER_DIR}/blender"
ENV BLENDER_PYTHON_PATH="${BLENDER_DIR}/4.2/python/bin/python3.11"
COPY /tmp/blender/blender-4.2.1-linux-x64 ${BLENDER_DIR}
ENV PATH="${BLENDER_PATH}:${PATH}"
# pythonのセットアップ
RUN apt-get install -y python3.11
RUN apt-get install -y --fix-missing python3-pip
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1
RUN python -m pip install --upgrade pip setuptools wheel
RUN pip install --target ${LAMBDA_TASK_ROOT} awslambdaric
RUN python -m pip install boto3
RUN python -m pip install aws-lambda-powertools
# 実行ファイルのコピー
COPY srcPython/utils ${LAMBDA_TASK_ROOT}/utils
COPY srcPython/app.py ${LAMBDA_TASK_ROOT}/app.py
COPY srcBlender/render.py ${LAMBDA_TASK_ROOT}/render.py
COPY srcBlender/template.blend ${LAMBDA_TASK_ROOT}/template.blend
ENV BLENDER_TEMPLATE_PATH="${LAMBDA_TASK_ROOT}/template.blend"
ENV BLENDER_SCRIPT_PATH="${LAMBDA_TASK_ROOT}/render.py"
# 実行コマンドの設定
COPY srcPython/entry_script.sh ${LAMBDA_TASK_ROOT}/entry_script.sh
RUN chmod +x ${LAMBDA_TASK_ROOT}/entry_script.sh
ENTRYPOINT [ "./entry_script.sh" ]
CMD [ "app.lambda_handler" ]
entry_script.shも必要になってきます。サンプルリポジトリを参照してください。
デプロイ
イメージのビルドとプッシュ
# もしリポジトリがない場合は作成してください
REPO_NAME=blender-on-lambda-test
aws ecr describe-repositories --repository-names ${REPO_NAME} || aws ecr create-repository --repository-name ${REPO_NAME}
REGION=$(aws configure get region)
REPO_NAME=blender-on-lambda-test
ACCOUNTID=$(aws sts get-caller-identity --query Account --output text)
ECR_URL=${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com
TAG="test"
docker build -t ${REPO_NAME} . || exit 1
# イメージプッシュ
docker tag ${REPO_NAME} "${ECR_URL}/${REPO_NAME}:${TAG}"
aws ecr get-login-password --region "${REGION}" | docker login --username AWS --password-stdin "${ECR_URL}"
docker push "${ECR_URL}/${REPO_NAME}:${TAG}"
Lambda関数の作成
- レンダリングにそこそこスペックが必要なのでリソースは盛ってます。
- lambdaのCPUリソースはメモリによって決まるためメモリは最大値に設定しています。
aws lambda create-function \
--function-name blender-on-lambda-test \
--package-type Image \
--code ImageUri=${ECR_URL}/${REPO_NAME}:${TAG} \
--role <your_role_arn> \
--timeout 180 \
--memory-size 10240 \
--region ${REGION}
サンプルデータのアップロード
サンプルデータはこちらよりお借りした物を編集して使用しています。(CC0 Public Domain)
エリマキシギという鴫のモデルです。
AWS CLIでS3バケットにアップロードします。
aws s3 cp ./sampleData/ s3://<your_bucket_name>/ --recursive
Lambda関数の実行
aws lambda invoke \
--function-name blender-on-lambda-test \
--payload file://testEvent.json \
--cli-binary-format raw-in-base64-out \
output.json
結果のダウンロード
aws s3 cp s3://<your_bucket_name>/test_result.png .
できました。
雑にライティングしているのでだいぶ暗めですね...
時間は1フレーム512x512のレンダリングで80秒ほどかかっています。
まとめ
今回はblenderをLambda上で動かす方法を紹介しました。
コールドスタート時間は通常よりもかかりますし、レンダリング時間もかかり、Lambdaの中心となるユースケースからはだいぶ外れてしまう気がしますが、
サーバーレスの強みを活かしてスケールアウトすることができるので、大量のレンダリングを行う際には有用かと思います。
応募待ってます!
Berryでは他にもサーバーレスアーキテクチャを悪用活用した3Dデータ処理を数多く実装しています。様々な技術に気軽にトライできる環境は大変刺激的です。
エンジニア大募集中です。一緒に、このアーキテクチャを更に使い倒したい、発展させたい方、大歓迎です。
医療業界での経験や3Dの知見は問いません。Berryの考え方や製品に少しでも興味が持てた方はお気軽に応募下さい!
Discussion