AWS Lambdaカスタムランタイム C++でHTTP APIをつくってみた

AWS LambdaカスタムランタイムのC++を利用してHTTP APIをビルド・デプロイしてみました。
2021.03.06

こんにちは、CX事業本部のうらわです。

最近、C++の勉強の題材としてAWSが提供しているAWS LambdaカスタムランタイムのC++を触っています。今回はDockerを使用したビルド環境を用意して以下のAPI Gatewayのサンプルコードをビルド・デプロイしてみます。

作業環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15

$ docker --version
Docker version 20.10.2, build 2291f61

$ aws --version
aws-cli/2.1.4 Python/3.7.4 Darwin/19.6.0 exe/x86_64

ビルド準備

Lambda関数のコードをLinux環境でビルドする必要があるためDockerを使用します。aws-lambda-cpp-runtimeaws-sdk-cppがインストール済みのDockerfileを用意します。

aws-sdk-cppは全サービスのライブラリをビルドするとかなり時間がかかるため、-DBUILD_ONLY="core"で必要なライブラリを絞っています。

Dockerfile

FROM ubuntu:20.04
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y \
  git \
  cmake \
  make \
  g++ \
  zip \
  libcurl4-openssl-dev \
  libssl-dev \
  uuid-dev \
  zlib1g-dev \
  libpulse-dev

WORKDIR /tmp

RUN git clone https://github.com/awslabs/aws-lambda-cpp-runtime.git && \
  cd aws-lambda-cpp-runtime && \
  mkdir build && \
  cd build && \
  cmake .. -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=OFF \
    -DCMAKE_INSTALL_PREFIX=~/install && \
  make && make install

RUN git clone https://github.com/aws/aws-sdk-cpp.git && \
  cd aws-sdk-cpp && \
  mkdir build && \
  cd build && \
  cmake .. -DBUILD_ONLY="core" \
    -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=OFF \
    -DENABLE_UNITY_BUILD=ON \
    -DCUSTOM_MEMORY_MANAGEMENT=OFF \
    -DCMAKE_INSTALL_PREFIX=~/install \
    -DENABLE_UNITY_BUILD=ON && \
  make && make install

COPY ./run.sh /usr/local/bin/run.sh
RUN chmod +x /usr/local/bin/run.sh

WORKDIR /src
ENTRYPOINT ["run.sh"]

任意のタグを付けてイメージをビルドしておきます。

docker build -t cpp-lambda .

Dockerfile内でCOPYしているrun.shは以下となります。ENTRYPOINTに指定しているため、上記でビルドしたDockerイメージを使用してコンテナを起動するとrun.shの処理が実行されます。

run.sh

#! /usr/bin/env bash

if [ $# != 1 ]; then
  echo There is no argument.
  exit 1
fi

mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~/install
make
make $1

Lambda関数の実装

Lambda関数のコードmain.cppとビルドするためのCMakeLists.txtを作成します。とは言っても、GitHubのexamples/api-gatewayのコードそのままです。

mkdir apigw
cd apigw
touch main.cpp
touch CMakeLists.txt

main.cpp

#include <aws/lambda-runtime/runtime.h>
#include <aws/core/utils/json/JsonSerializer.h>
#include <aws/core/utils/memory/stl/SimpleStringStream.h>

using namespace aws::lambda_runtime;

invocation_response my_handler(invocation_request const& request)
{

    using namespace Aws::Utils::Json;

    JsonValue json(request.payload);
    if (!json.WasParseSuccessful()) {
        return invocation_response::failure("Failed to parse input JSON", "InvalidJSON");
    }

    auto v = json.View();
    Aws::SimpleStringStream ss;
    ss << "Good ";

    if (v.ValueExists("body") && v.GetObject("body").IsString()) {
        auto body = v.GetString("body");
        JsonValue body_json(body);

        if (body_json.WasParseSuccessful()) {
            auto body_v = body_json.View();
            ss << (body_v.ValueExists("time") && body_v.GetObject("time").IsString() ? body_v.GetString("time") : "");
        }
    }
    ss << ", ";

    if (v.ValueExists("queryStringParameters")) {
        auto query_params = v.GetObject("queryStringParameters");
        ss << (query_params.ValueExists("name") && query_params.GetObject("name").IsString()
                   ? query_params.GetString("name")
                   : "")
           << " of ";
        ss << (query_params.ValueExists("city") && query_params.GetObject("city").IsString()
                   ? query_params.GetString("city")
                   : "")
           << ". ";
    }

    if (v.ValueExists("headers")) {
        auto headers = v.GetObject("headers");
        ss << "Happy "
           << (headers.ValueExists("day") && headers.GetObject("day").IsString() ? headers.GetString("day") : "")
           << "!";
    }

    JsonValue resp;
    resp.WithString("message", ss.str());

    return invocation_response::success(resp.View().WriteCompact(), "application/json");
}

int main()
{
    run_handler(my_handler);
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
set(CMAKE_CXX_STANDARD 11)

project(api LANGUAGES CXX)

find_package(aws-lambda-runtime REQUIRED)
find_package(AWSSDK COMPONENTS core)

add_executable(${PROJECT_NAME} "main.cpp")
target_link_libraries(${PROJECT_NAME} PUBLIC AWS::aws-lambda-runtime ${AWSSDK_LINK_LIBRARIES})

aws_lambda_package_target(${PROJECT_NAME})

Lambda関数のビルド

現在のディレクトリ(main.cppCMakeLists.txtが存在するapigwディレクトリ)をコンテナの/srcディレクトリにマウントしてDockerコンテナを起動します。実行時の引数に指定しているaws-lambda-package-apirun.shに渡され、makeコマンドの引数に使用されます。

--rmオプションをつけているため、このコンテナはビルドが終わったら自動的に削除されます。

docker run --rm -v $(pwd):/src cpp-lambda aws-lambda-package-api

ビルドが成功すると、apigw/build内にapi.zipが作成されます。これをLambda関数としてアップロードします。AWS CLIでアップロードする際のコマンド例はGitHubのaws-lambda-cpp-runtimeリポジトリのREADMEに書いてあります。

aws lambda create-function --function-name api \
  --role <予めIAMロールを作成しArnを指定する> \
  --runtime provided --timeout 15 --memory-size 128 \
  --handler api --zip-file fileb://apigw/build/api.zip

API Gatewayの作成とテスト実行

READMEに記載されている通りの手順でAPI Gatewayのリソースを作成します(作成したLambda関数のトリガーからAPI Gatewayを作成します)。

作成が完了したらAPI Gatewayのエンドポイントを確認し、curlでリクエストを送ってみます。以下のようなメッセージが返ってくれば成功です。

$ curl -X POST \
  '<API Gatewayのエンドポイント>/api?name=Bradley&city=Chicago' \
  -H 'content-type: application/json' \
  -H 'day: Sunday' \
  -d '{ "time": "evening" }'

{"message":"Good evening, Bradley of Chicago. Happy Sunday!"}

おわりに

ビルドするための環境構築が少々手間ですが、ベースとなるDockerfileさえ用意できればあとは作成するLambda関数に応じて少しカスタマイズするだけになります。

今回はサンプルコードを使用してビルドする手順が中心でした。今後はローカルマシンにもaws-lambda-runtime-cppaws-sdk-cppをインストールして自作のLambda関数を実装する準備を整えようと思います。

なお、本記事のコードは以下のリポジトリに格納してあります。