0
0

RのWeb APIをLambdaで作る

Posted at

はじめに

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

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

概要

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

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

参考

やってみた

環境

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

コンテナ作成とPUSH

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

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

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

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

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

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
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します。

# よく使う文字列を環境変数にセット
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を作ります。

# ECRリポジトリ作成CFn
touch createECRRepository.yaml

ECRを作るCFnは以下になります。

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します。

# 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を作ります。以前の記事を使っていきます。

# Lambda作るCFn
touch createLambdaFunctionUrl.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を作ります。

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コマンドで、実装した各処理を実行するように引数を渡します。

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で実装すべきか、ということもあるかと思いますが。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0