はじめに
前回、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はわからなかったので、稚拙なコードですがご容赦ください。
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
のインストールを追加しています。
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は以下になります。
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
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で実装すべきか、ということもあるかと思いますが。
この記事がどなたかのお役に立てれば幸いです。