コラム

[Lambda]Dockerコンテナ上でLambdaを動作(PDFファイル作成、ダウンロード機能)

今回はLambda関数をコンテナイメージ化して、AWS上にデプロイして正常に動作するか確認してみたいと思います。

LambdaをDockerコンテナ化するメリット

Lambdaをコンテナ化出来るようになり、コンテナイメージをAWSにデプロイ可能になったので、非常に便利です。
zipファイルでアップロードでLambda関数を作成した時は外部ライブラリを使用した際などに、インストールしたライブラリのヴァージョンが合わない等の実行時にエラーが起きる問題が起きる事もあったので、ローカルでテストしたものが、そのままAWS上でも動くというのはメリットだと思います。

複数関数でライブラリを共有して使用する場合はLayerを使用するのも良いと思います。
Layerに関しては後に記事にしたいと思います。

機能内容

今回作成するLambdaの機能は、PDFを作成しPDFのデータを返すという機能です。
PDF作成するのにライブラリを使用するので、コンテナ内にあらかじめインストールしておきます。

下記のテンプレートファイルに会社や案件情報、金額などを入力しPDFファイルを作成しそのデータを返します。

テンプレート

作成ファイル

環境

Windows11
Docker20.10.20
Python3.8
reportlab
PyPDF2

ファイル一覧

ファイル名説明
app.pyLambda関数として起動するメインファイル
createpdf.pyPDFファイル作成処理を実装するファイル
app.pyから呼ばれる
Dockerfiledockerコンテナイメージ作成手順ファイル
template.pdfPDF作成の際のテンプレートとして利用する
このファイルの指定位置に特定の値を埋め込んで新規PDFを作成する

ファイル詳細

app.py

import json
import base64
import createpdf
import os

def handler(event, context):
    # PDF作成クラスインスタンス取得
    ss = createpdf.ReportlabView()

    # PDF作成処理依頼
    ss._create_pdf( '/tmp/sample.pdf', 'template.pdf' )
    
    # 作成されたファイルを読み込み、Base64に変換して返す
    with open("/tmp/sample.pdf", "rb") as file_data:
        text = file_data.read()
    
    text = base64.b64encode( text ).decode('utf-8')

    # 返す値はJSON形式の文字列で返す。ファイルダウンロードする際は以下の様な記述にする。
    return {
        "statusCode": 200,
        "headers": {
            'Content-Length': len(text),
            'Content-Type': 'application/pdf',
            'Content-disposition': 'attachment;filename=output.pdf'
        },
        "isBase64Encoded": True,
        "body": text
    }

ファイルをダウンロードする際にreturnで返す値は上記の様な値が必要です。

  • statusCode :ステータスコード
  • headers :各ヘッダー
    • Content-Length :ファイル長
    • Content-Type :コンテンツのタイプ
    • Content-disposition :ファイル名などを指定
  • isBase64Encoded  :Base64でエンコードされているか指定
  • body :ファイルの内容はBase64でエンコードした値

createpdf.py

from PyPDF2 import PdfFileWriter, PdfFileReader
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4, portrait
from reportlab.lib.units import inch, mm, cm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.platypus import Table, TableStyle
from reportlab.lib import colors

class ReportlabView():

    def _create_pdf(self, output_file, template_file):
        # ファイルの指定
        tmp_file = '/tmp/tmp.pdf' # 一時ファイル

        # Canvasを作成 
        w, h = portrait(A4)
        cv = canvas.Canvas(tmp_file, pagesize=(w, h))

        # フォントを登録しCanvasに設定
        font_size = 10
        font_name = 'HeiseiKakuGo-W5'
        pdfmetrics.registerFont(UnicodeCIDFont(font_name))
        
        cv.setFont(font_name, font_size)

        # 文字列を描画
        cv.drawString(
            20 * mm,
            264 * mm,
            "AAA株式会社御中"
        )
        
        cv.drawString(
            80 * mm,
            257 * mm,
            "AA案件"
        )
        
        data = [
            ["8800円"], #合計金額
        ]

        table = Table(data, colWidths=(60 * mm), rowHeights=(7 * mm))
        table.setStyle(
            TableStyle(
                [
                    ("FONT", (0, 0), (-1, -1), font_name, 10),
                    # ("BOX", (0, 0), (-1, -1), 1, colors.red),
                    # ("INNERGRID", (0, 0), (-1, -1), 1, colors.black),
                    ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
                    ("ALIGN", (0, 0), (-1, -1), "RIGHT"),
                ]
            )
        )
        table.wrapOn(
            cv,
            70 * mm,
            218 * mm,
        )
        table.drawOn(
            cv,
            70 * mm,
            218 * mm,
        )
        
        data = []

        data.append(["A案件", "1000", "2", "2000", "", ""])
        data.append(["B案件", "2000", "3", "6000", "", ""])

        
        table = Table(
            data,
            colWidths=(70 * mm, 25 * mm, 25 * mm, 20 * mm, 20 * mm, 20 * mm),
            rowHeights=6 * mm,
        )
        table.setStyle(
            TableStyle(
                [
                    ("FONT", (0, 0), (-1, -1), font_name, 8),
                    ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
                    ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
                ]
            )
        )
        table.wrapOn(cv, 17 * mm, 184 * mm)
        table.drawOn(cv, 17 * mm, 184 * mm)

        # 一時ファイルに保存
        cv.showPage()
        cv.save()

        # テンプレートとなるPDFを読む
        template_pdf = PdfFileReader(template_file)
        template_page = template_pdf.getPage(0)

        # 一時ファイルを読んでマージする
        tmp_pdf = PdfFileReader(tmp_file)
        template_page.mergePage(tmp_pdf.getPage(0))

        # 出力用PDFを用意し書き込む
        output = PdfFileWriter()
        output.addPage(template_page)
        with open(output_file, "wb") as fp:
            output.write(fp)
            

Dockerfile

FROM public.ecr.aws/lambda/python:3.8

RUN pip install pypdf2
RUN pip install reportlab

COPY template.pdf   ./
COPY app.py   ./
COPY createpdf.py   ./

CMD ["app.handler"]

ローカルPCで起動・テスト

起動

1.Dockerイメージ作成

docker build -t イメージ名 . 

2.作成イメージ起動

ポート番号は空いている番号を適当に使用しています。

docker run --rm -p 9000:8080 イメージ名:latest 

3.起動確認

docker psコマンドを実行すると起動しているコンテナを確認できます。

docker ps

CONTAINER ID   IMAGE          COMMAND                  CREATED      STATUS      PORTS                    NAMES
a9218fac6c25   イメージ名:latest   "/lambda-entrypoint.…"   2 days ago   Up 2 days   0.0.0.0:9000->8080/tcp   hardcore_feistel

動作確認

1.コマンドから確認

Windows環境での確認コマンドは「Invoke-WebRequest」を使用して行います。
URLの2015-03-31/functions/function/invocationsの部分はDockerであらかじめ決まっている部分みたいです。

Invoke-WebRequest http://localhost:9000/2015-03-31/functions/function/invocations -Method POST -Body '{}'

Linux環境での確認コマンドは「curl」を使用して行います。

curl -X POST http://localhost:9000/2015-03-31/functions/function/invocations -d {}

結果

以下の様な値が返ってくれば成功です。しかし、これではbodyの値が期待通りのPDFファイルが分からないので
テスト用プログラムを作成し、確認してみます。

StatusCode        : 200
StatusDescription : OK
Content           : {"statusCode": 200, "headers": {"Content-Length": 18448, "Content-Type": "application/pdf", "Conten
                    t-disposition": "attachment;filename=output.pdf"}, "isBase64Encoded": true, "body": "JVBERi0xLjQKJe
                    Lj...
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Content-Type: text/plain; charset=utf-8
                    Date: Thu, 27 Oct 2022 07:42:15 GMT

                    {"statusCode": 200, "headers": {"Content-Length": 18448, "Content-Type": "a...
Forms             : {}
Headers           : {[Transfer-Encoding, chunked], [Content-Type, text/plain; charset=utf-8], [Date, Thu, 27 Oct 2022 0
                    7:42:15 GMT]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 18634

2.テストプログラムから確認

次の様なテストプログラムをPythonで作成し、取得した値からファイルを作成してみます。

import requests
import json
import base64

response = requests.post('http://localhost:9000/2015-03-31/functions/function/invocations', '{}')

json_str = json.loads(response.text)

#取得したJSONデータのbody部分はBase64デコードをしてファイルに保存します。
data = base64.b64decode( json_str['body'].encode() )
f = open('sample.pdf', 'wb')
f.write( data )

結果、作成されたファイルが想定通りのものなら正常に動作してる事が確認できます。
次は実際にAWSデプロイして、Api Gateway経由でアクセスしてみましょう。

AWSにデプロイ

手順

  1. ECR( Elastic Container Registry )にリポジトリ作成
  2. ECRへ登録する用のdockerイメージ作成
  3. ECRへ作成イメージを登録
  4. Lambda関数をdockerイメージから作成
  5. テスト用にApiGatewayの設定

1.ECR( Elastic Container Registry )にリポジトリ作成

dockerイメージを登録する為にECRにリポジトリを作成します。

赤枠の部分はECR登録に使用します。

2.ECRへ登録する用のdockerイメージ作成

ローカル環境で作成したイメージにタグつけをします。
イメージIDはdocker imagesで見れます。
XXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-docker-sampleの部分は上記の赤枠部分を指定します。

docker tag イメージID XXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-docker-sample:tag

3.ECRリポジトリへ作成イメージを登録

登録する前のECRへのログイン処理を行います。
このコマンドはprofileを指定しない場合は、.awsファイルのデフォルトの認証情報を使用します。

aws ecr get-login-password --region  ap-northeast-1 | docker login --username AWS --password-stdin  XXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com

登録処理は次のコマンドを実行します。

docker push XXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-docker-sample:tag

4.Lambda関数をdockerイメージから作成

コンテナイメージで関数作成を選択します。

イメージ参照ボタンを押下し、リポジトリとイメージ選択します。

イメージ選択後、関数名やロールを選択し関数を作成すれば完成です。
今回は「CreatePDFSample」という関数名で作成します。

5.テスト用にApiGatewayの設定

ファイルをダウンロードのテストの為にApiGatewayを設定します。
ApiGatewayを使用すると、Lambdaから返値をファイル形式にしてダウンロードしてくれます。

Lambda関数画面でトリガー追加を押下します。

API Gatewayを選択します。
IntentはCreate a new APIを選択します。
API TypeはHTTP APIを選択します。

追加ボタンを押下するとトリガーにAPI Gatewayが作成されます。

API Gatewayを押下すると下記の画面が出てきます。
API endpointに記述してあるURLにアクセスします。
output.pdfファイルがダウンロードされたら、正常に動作せてる事が確認できます。

以上で、Dockerコンテナを使用してのLambdaの実装、デプロイ方法を紹介しました。

この記事をシェアする
  • Facebookアイコン
  • Twitterアイコン
  • LINEアイコン

お問い合わせ ITに関するお悩み不安が少しでもありましたら、
ぜひお気軽にお問い合わせください

お客様のお悩みや不安、課題などを丁寧に、そして誠実にお伺いいたします。

お問い合わせはこちら
お電話でのお問い合わせ 03-5820-1777(平日10:00〜18:00)
よくあるご質問