RのWeb APIをLambdaで作る

# はじめに

前回、LambdaをRで動かす`lambdr`を紹介しました。

https://qiita.com/a_b_/items/aec53d51d40781c18b89

以前から、ちょくちょくRをREST API化するplumberをコンテナサービスで動かしてきました。
ですが、Lambdaで同じコンテナは動かせないため、`lambdr`を使って、Function URLで似たようなものを作ってみました。

# 概要

以下の事をやっています。

- lambdrを使って、RをLambdaで動かします
- LambdaのFunction URLを使って、Web APIにします
    - API Gatewayでも同じようにできるはずです
- 動かす処理はPOSTで指定します
    - plumberのように、URLパスでの指定はできませんでした…

# 参考

https://cran.r-project.org/web/packages/lambdr/lambdr.pdf

https://rdrr.io/github/mdneuzerling/lambdr/man/html_response.html

https://mdneuzerling.com/post/serverless-on-demand-parametrised-r-markdown-reports-with-aws-lambda/


# やってみた

## 環境

いつものCloud9です。t2.microでやりました。デフォルトのストレージ10GBではギリギリなので、Rなど他のものをインストールしたインスタンスであれば、容量を大きくしてください。

## コンテナ作成とPUSH

ディレクトリとファイルを作っていきます。

```bash
# プロジェクトディレクトリ作成
mkdir test-lambdr-api && cd test-lambdr-api

# 対象をPULL
docker pull public.ecr.aws/lambda/provided

# サンプル用のファイルを作成。コードは以下に
touch Dockerfile
touch runtime.R
```

Rのコードはplumberと同じレスポンスになるように記述します。

https://qiita.com/a_b_/items/32cbc21bd140321719c2

Rはわからなかったので、稚拙なコードですがご容赦ください。

```R:runtime.R
sampleFunc <- function(funcid,...) {
  arglist = list(...)
  switch(funcid,
    hello = {
      if(length(arglist) == 0 ){
        return("Hello, world!")
      } else if( "name" %in% names(arglist) && "age" %in% names(arglist) ) {
        return(paste("Hello", arglist["name"], "You're", arglist["age"], "years old", seq=" "))
      } else {
        stop("ERROR\n")
      }
    }
    ,fn = {
      if( "x" %in% names(arglist) ) {
        x <- as.numeric(arglist["x"])
        y <- 2 * x + 1
        return(y)
      } else {
        stop("ERROR\n")
      }
    }
    ,plot ={
      #irisを読み込み
      df <- iris
      png("/tmp/iris_plot.png")
      plot(df$Sepal.Length, df$Sepal.Width, main="Sample plot", xlab="Sepal.Length", ylab="Sepal.Width")
      dev.off()
      
      library(base64enc)
      image_data <- readBin("/tmp/iris_plot.png", "raw", file.info("/tmp/iris_plot.png")$size)
      image_base64 <- base64enc::base64encode(image_data)
      
      # https://cran.r-project.org/web/packages/lambdr/lambdr.pdf
      lambdr::html_response(
        image_base64
        , is_base64 = TRUE
        , content_type = "image/png"
      )

    }
    ,stop("ERROR\n")
  )
}

lambdr::start_lambda()
```

Dokcerfileも公式サンプルと少し変わります。`base64enc`のインストールを追加しています。

```Dockerfile:Dockerfile
FROM public.ecr.aws/lambda/provided

ENV R_VERSION=4.0.3

RUN yum -y install wget git tar

RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
  && wget https://cdn.rstudio.com/r/centos-7/pkgs/R-${R_VERSION}-1-1.x86_64.rpm \
  && yum -y install R-${R_VERSION}-1-1.x86_64.rpm \
  && rm R-${R_VERSION}-1-1.x86_64.rpm

ENV PATH="${PATH}:/opt/R/${R_VERSION}/bin/"

# System requirements for R packages
RUN yum -y install openssl-devel

RUN Rscript -e "install.packages(c('httr', 'jsonlite', 'logger', 'remotes', 'base64enc'), repos = 'https://packagemanager.rstudio.com/all/__linux__/centos7/latest')"
RUN Rscript -e "remotes::install_github('mdneuzerling/lambdr')"

RUN mkdir /lambda
COPY runtime.R /lambda
RUN chmod 755 -R /lambda

RUN printf '#!/bin/sh\ncd /lambda\nRscript runtime.R' > /var/runtime/bootstrap \
  && chmod +x /var/runtime/bootstrap

CMD ["sampleFunc"]
```

作ったファイルからコンテナをBuildします。

```bash
# よく使う文字列を環境変数にセット
REGION="ap-northeast-1"
ACCOUNTID=$(aws sts get-caller-identity --output text --query Account)
IMAGENAME="test-lambdr-api"

# build 3分強。Cloud9デフォルト10GBだとカツカツ
docker build -t ${IMAGENAME} .
```

次にECRを作ります。
```bash
# ECRリポジトリ作成CFn
touch createECRRepository.yaml
```
ECRを作るCFnは以下になります。
```yaml:createECRRepository.yaml
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  RepositoryName:
    Type: String

Resources:
  TestEcrPoc:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref RepositoryName

Outputs:    
  RepositoryUri:
    Value: !GetAtt TestEcrPoc.RepositoryUri
```
ECRを作成して、作成したイメージをPUSHします。
```bash
# ECRにレポジトリ作成
STACKNAME="create-ecrrepo-lambdr-api"
REPOSITORYNAME="test-lambdr-api-ecs"
aws cloudformation create-stack --stack-name ${STACKNAME} \
  --template-body file://createECRRepository.yaml \
  --region ${REGION} \
  --parameters \
    ParameterKey=RepositoryName,ParameterValue=${REPOSITORYNAME}

# イメージにタグ付与
TAGNAME=`aws cloudformation describe-stacks --stack-name ${STACKNAME} --query "Stacks[].Outputs[?OutputKey=='RepositoryUri'].[OutputValue]"  --output text`:latest
docker tag ${IMAGENAME}:latest ${TAGNAME}

# 認証
aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com

# 作ったイメージをPUSH
docker push ${TAGNAME}
```

## Lambdaを作る
Function URLが使えるLambdaを作ります。以前の記事を使っていきます。

https://qiita.com/a_b_/items/4d605af2cbb64c3d8ba5#cloudformation

```bash
# Lambda作るCFn
touch createLambdaFunctionUrl.yaml
```

```yaml:createLambdaFunctionUrl.yaml
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  LambdaFunctionName:
    Type: String
  ImageUri:
    Type: String
    
Resources:
  ########################################################
  ### Log Group
  ########################################################
  FunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaFunctionName}"
      RetentionInDays: 3653

  ########################################################
  ### IAM Role
  ########################################################
  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "for-lambdafunction-${LambdaFunctionName}"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: '/service-role/'
      Policies:
        # CloudWatch
        - PolicyName: write-cloudwatchlogs
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunctionName}:*"    

  ########################################################
  ### Lambda Function
  ########################################################
  TargetFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Ref LambdaFunctionName
      Role: !GetAtt FunctionRole.Arn
      PackageType: Image
      Code:
        ImageUri: !Ref ImageUri

  ##################################################
  # 以下、URLと許可
  ##################################################
  TargetFunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      TargetFunctionArn: !GetAtt TargetFunction.Arn

  TargetFunctionUrlPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunctionUrl
      FunctionName: !GetAtt TargetFunction.Arn
      FunctionUrlAuthType: NONE
      Principal: "*"

Outputs:
  FunctionUrl:
    Value: !GetAtt TargetFunctionUrl.FunctionUrl
    Export:
      Name: TargetFunctionUrl
```
上記のCFnで、Lambdaを作ります。
```bash
DIGEST=$(aws ecr list-images --repository-name ${REPOSITORYNAME} --region ${REGION} --out text --query 'imageIds[?imageTag==`latest`].imageDigest')
IMAGEURI=`aws cloudformation describe-stacks --stack-name ${STACKNAME} --query "Stacks[].Outputs[?OutputKey=='RepositoryUri'].[OutputValue]"  --output text`@${DIGEST}
FUNCTIONNAME="func-lambdr-api"
STACKNAMEFUNCTION="create-func-lambdr-api"

aws cloudformation create-stack --stack-name ${STACKNAMEFUNCTION} \
  --template-body file://createLambdaFunctionUrl.yaml \
  --region ${REGION}  \
  --parameters \
    ParameterKey=LambdaFunctionName,ParameterValue=${FUNCTIONNAME} \
    ParameterKey=ImageUri,ParameterValue=${IMAGEURI} \
  --capabilities CAPABILITY_NAMED_IAM
```

## 動かす

Function URLで生成されたURLを変数にいったん格納します。
CURLコマンドで、実装した各処理を実行するように引数を渡します。
```bash
LambdrUrl=`aws lambda get-function-url-config --function-name ${FUNCTIONNAME} --query "FunctionUrl" --output text`

curl ${LambdrUrl} -d '{"funcid": "hello"}'

curl ${LambdrUrl} -d '{"funcid": "hello","name":"jiro","age":21}'

curl ${LambdrUrl} -d '{"funcid": "fn","x":2}'

curl -o img.png ${LambdrUrl} -d '{"funcid": "plot"}'
```

# おわりに

今回はRで、Lambdaを使って、Web APIを作ってみました。
"できます"というだけですので、1つのLambdaの中で色々な処理を入れ込まず、シンプルに作った方がいいと思います。そもそもLambdaで実装すべきか、ということもあるかと思いますが。

この記事がどなたかのお役に立てれば幸いです。