AWS LambdaのCustom RuntimesでC言語のLambda Functionを動かしてみた! #reinvent

本記事はAWS Lambda Custom Runtimes芸人 Advent Calendar 2018の15日目の記事です。C言語を使ってLambda Functionを実装してみました。
2018.12.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

福岡オフィスのyoshihitohです。 本記事はAWS Lambda Custom Runtimes芸人 Advent Calendar 2018の15日目の記事です。

はじめに

Advent Calendarを見るとわかるように、 Custom Runtimesを活用すると色々な言語でLambda Functionを実装できます。 今回はC言語を使ってLambda Functionを実装してみました。

やること

Custom Runtimesのチュートリアルを参考に、bootstrap のシェルスクリプトからC言語のプログラムを実行します。C言語のプログラムは、受け取ったイベントをシーザー暗号にするだけの単純なものにしてみます。

プログラムとCMakeプロジェクトを作る

まず、C言語のプログラムを作成します。

main.c

#include <stdio.h>
#include <ctype.h>

static const char* const ORIGINAL_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
static const char* const CAESAR_CHARS =   "XYZABCDEFGHIJKLMNOPQRSTUVW";

static int encode_caesar_cipher(int ch)
{
    if (isupper(c)) return CAESAR_CHARS[ch - 'A'];
    if (islower(c)) return tolower(CAESAR_CHARS[ch - 'a']);
    return c;
}

int main() {
    int c;
    while ((c = getchar()) != EOF) {
        if (!isprint(c)) continue;

        putchar(encode_caesar_cipher(c));
    }

    return 0;
}

次に、CMakeのプロジェクトを作成します。

CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(custom_runtime_c C)

set(CMAKE_C_STANDARD 99)

add_executable(custom_runtime_c main.c)

ビルド環境を作る

次にビルド環境を構築します。まず、ビルド用のシェルを作ります。

build.sh

#!/bin/sh

set -e
set -o pipefail

mkdir -p build
cd build
CC=gcc cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release ..
make

mkdir -p /artifacts/bin
cp custom_runtime_c /artifacts/bin
mkdir -p /artifacts/lib
cp /lib/ld-musl-x86_64.so.1 /artifacts/lib

次に、Dockerコンテナを利用してビルド環境を構築します。C++で試したときと同様に、Alpine Linuxを使用します。

Dockerfile

FROM alpine:latest

RUN apk update && apk add cmake make gcc bash zip musl-dev

WORKDIR /opt/src
COPY ./CMakeLists.txt .
COPY ./main.c .
COPY ./bootstrap .
COPY ./build.sh .

VOLUME /artifacts

また、簡単にビルドできるようにdocker-composeを用意します。

docker-compose.yaml

version: "3"
services:
  custom_runtime_c:
    build:
      context: .
    image: custom_runtime_c
    command: bash ./build.sh
    volumes:
      - ./artifacts:/artifacts

ここまでで準備完了です。ビルドしてみます。

$ mkdir -p ./artifacts
$ docker-compose up -d --build
$ ls -lah ./artifacts/bin
-rwxr-xr-x  1 yoshihitoh  staff    11K 12 15 18:17 custom_runtime_c
$ ls -lah ./artifacts/lib
-rwxr-xr-x  1 yoshihitoh  staff   550K 12 15 18:17 ld-musl-x86_64.so.1

ちゃんとビルドできていますね!

Lambda Functionを作る

最後にLambda Functionを作ります。チュートリアルとC++用のカスタムランタイムを参考に実装します。

artifacts/bootstrap

#!/bin/sh

set -euo pipefail

EXEC="$LAMBDA_TASK_ROOT/bin/$_HANDLER"
MUSL="$LAMBDA_TASK_ROOT/lib/ld-musl-x86_64.so.1"

# Processing
while true
do
  HEADERS="$(mktemp)"
  # Get an event
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # Execute the handler function from the script
  RESPONSE=$(echo "$EVENT_DATA" | exec $MUSL --library-path $LAMBDA_TASK_ROOT/lib $EXEC)

  # Send the response
  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
done

基本的にはサンプルと同様ですが、実行ファイルの呼び出し方を変更しています。

ZIPファイルに固めてLambda Functionを作れば完了です。

$ cd artifacts
$ zip -r custome-runtime-c.zip bootstrap bin lib
$ aws lambda create-function \
  --function-name "custom-runtime-c" \
  --zip-file "fileb://custome-runtime-c.zip" \
  --handler "custom_runtime_c" \
  --runtime provided \
  --role arn:aws:iam::xxxxxxxxxxxx:role/lambda_basic_execution

動かしてみる

例えば、暗号結果を以下のデータにしたい場合を考えます。

{
  "hello": "c-runtime!",
  "method": "caesar"
}

アルファベットを左に3個ずつずらした結果を上記の文字列にしたいので、右側に3個ずつずらしてみます。

{
  "khoor": "f-uxqwlph!",
  "phwkrg": "fdhvdu"
}

上記のテストデータを使ってLambda Functionを動かしてみます。

ちゃんと動いていますね!

おわりに

今回はC言語でLambda Functionを実装してみました。本当はbootstrapで行っているLambdaのAPIを叩く部分もC言語のプログラムに寄せたいなーと思っていたんですが、結構面倒くさそうな感じでした。 C言語でハンドラーを実装する場合、C++ or Rustのカスタムランタイムからハンドラ関数を呼び出すのがよさそうです。

C言語は長いこと使われている言語なこともあって、C言語実装のライブラリは数多く存在しています。それらを活用して面白いことができるんじゃないかなーと思うので、引き続き色々試していきたいと思います。