RのパッケージplumberをFargateで動かす

# はじめに

Rには、スクリプトをREST APIにできる"plumber"というパッケージがあります。今回はイメージ化したplumberの環境を、ECSハンズオンの構成で動かしてみました。


# 参考

https://aws.amazon.com/jp/blogs/news/aws-hands-on-for-beginners-21/

https://blog.serverworks.co.jp/tech/2019/11/01/r-fargate/

https://stackoverflow.com/questions/65329940/running-plumber-api-on-docker-container

# やったこと

作業はCloud9で行いました。

## ディレクトリ・ファイル作成
まずは各種準備です。
```bash
# プロジェクトのディレクトリ作成
mkdir test-plumber && cd test-plumber

# イメージプル
docker pull r-base

# 必要なファイルを作成
touch Dockerfile
touch plumber.R
touch sample.R
```
Dockerfileは以下になります。参考にしたページとは、インストールするパッケージがあったり、ポート番号が異なったりしています。
```dockerfile:Dockerfile
FROM r-base

COPY  sample.R /usr/local/src/
COPY  plumber.R /usr/local/src/

WORKDIR /usr/local/src/

# install the linux libraries needed for plumber
RUN apt-get update -qq && apt-get install -y \
  libsodium-dev \
  libcurl4-gnutls-dev

RUN R -e 'install.packages("plumber")'

EXPOSE 80

ENTRYPOINT ["Rscript", "plumber.R"]
```
以下はRになります。
```r:plumber.R
library(plumber)
api <- plumber::plumb("sample.R")
api$run(host = "0.0.0.0", port=80)
```

```r:sample.R
#* @get /hello
hw <- function() {
    return("Hello, world!")
}

#* @get /hello/<name>/<age:int>
hw <- function(name, age) {
    return(paste("Hello", name, "You're", age, "years old", seq=" "))
}

#* @get /fn
hw <- function(x) {
    x <- as.numeric(x)
    y <- 2 * x + 1
    return(y)
}

#irisを読み込み
df <- iris

#* @get /plot
#* @png
hw <- function() {
    p <- plot(df$Sepal.Length, df$Sepal.Width, 
        main="Sample plot", xlab="Sepal.Length", ylab="Sepal.Width")
    print(p)
}
```
## イメージのBuild
ファイルに記述できたらbuildしていきます。
```bash
# よく使う文字列を環境変数にセット
REGION="ap-northeast-1"
ACCOUNTID=$(aws sts get-caller-identity --output text --query Account)
IMAGENAME="test-plumber"

# build 5分強
docker build -t ${IMAGENAME} .

# テストで起動
docker run --rm -p 8080:80 ${IMAGENAME}

# 別コンソールから
curl localhost:8080/hello
curl localhost:8080/hello/jiro/22
```
プレビューからでも確認可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/e0cc802a-e922-1df6-4256-1d906bb5b3a1.png)

## コンテナイメージを、ECR にアップロードする

### リポジトリ作成
CLIでやっていくため、CFnで作成します。
```
# ECRリポジトリ作成CFn
touch createECRRepository.yaml
```
```yaml:createECRRepository.yaml
AWSTemplateFormatVersion: "2010-09-09"

Resources:
  ########################################################
  ### ECR Repository
  ########################################################
  TestEcrPoc:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: test-plumber-ecs
Outputs:    
  RepositoryUri:
    Value: !GetAtt TestEcrPoc.RepositoryUri
```
ECRを作成して、PUSHします。
```bash
# ECRにレポジトリ作成
STACKNAME="create-ecrrepo"
aws cloudformation create-stack --stack-name ${STACKNAME} --template-body file://createECRRepository.yaml --region ${REGION}

# イメージにタグ付与
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}
```

## VPC作成

ハンズオンにあるように作ります。詳細はハンズオンを確認ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/1920b908-3027-ac30-6160-19ce1785b81f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/823d7ae1-730c-e592-6553-35bb06916e9d.png)

セキュリティグループを修正し、インバウンドルールにHTTPをすべてのIPから有効にする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/630cf0c9-cfdd-714e-4f62-b554eaab26b5.png)


## クラスターの作成
ECSにてクラスター名とVPCのみ指定して作成します。こちらも詳細はハンズオン資料にあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/aa6783b4-719d-1a22-1354-3c31dd0f622d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/fab4e340-6b85-e1aa-fe7f-9630f84da615.png)

## タスク定義
タスク定義ファイミリーと、コンテナ名、イメージURIを設定します。イメージURIは、以下のコマンドの結果を使うと楽かと思います。
```
echo `aws cloudformation describe-stacks --stack-name ${STACKNAME} --query "Stacks[].Outputs[?OutputKey=='RepositoryUri'].[OutputValue]"  --output text`:latest
```
作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/df8ab171-8321-b07b-708a-3c3a94f94f6e.png)



## サービス作成
作成したクラスターでサービスを作成します。ハンズオン資料は「デプロイ」とありますが、コンソールの表示が異なるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/8a229194-5d39-167f-3b2e-f6e8cdc60195.png)

デプロイ設定を以下のようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/5a3f4465-7105-5f72-7249-5879a0bdfc5f.png)

"ロードバランシング -オプション"を開いて、ロードバランサーも設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/6f400136-1870-6e68-a6a3-506d732c2b0c.png)
ヘルスチェックパスは`/hello`と設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/c85ac10e-e5fb-d29b-616c-4299b0e574ba.png)

作成されるまで待ちます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/40a61c39-b40b-35d2-961e-a92f038f9e0c.png)
正常に作成されると以下のようになりました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/c7e10fa1-03a7-7167-caf8-86e0b602caa3.png)

## 接続
ネットワーキングのタブのDNS名でアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/f1ed83a8-d27c-e9da-9bca-957ae72e080f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/805e9b6d-d73b-5f22-38e3-502bec5373f3.png)
URLの最後に`/hello`を付けて再アクセスです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/e2399c8f-76da-859e-c0b9-6eecbe6bf88a.png)
その他、`/hello/jiro/21`の場合。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/21ca9fb3-dcbc-3f2d-e7dd-e5eaf39ea99b.png)
`/fn?x=2`の場合。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/84cbab90-387e-d0c9-4961-161afa5079a7.png)
`/plot`の場合。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/c2653873-7f83-5f07-d105-2e05a2266def.png)

## 削除
ハンズオン資料にありますが、今はある程度まとめて消してくれるようです。

- ~~ALB~~(後でまとめて削除される)
- ~~TargetGroup~~(後でまとめて削除される)
- ~~ECS Service の Task 数を 0 に変更~~(後でまとめて削除される)
- ECS Service の削除
- クラスターの削除
- <font color="red">**タスク定義の削除**</font>
- VPC の削除
- ECR の削除
- Cloud9 の削除

ALBとTargetGroupは、ECS Service作成時に実行されるCloudformationのスタックにて定義されており、コンソールからの削除で、そのスタックで作成されたリソースはすべて消えます。
また削除時のメッセージを見る限り、0にスケールダウンしなくてもOKな模様です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/c6d960d1-2d58-0d85-9449-9471644fdf1b.png)

タスク定義は手動で削除する必要があります。対象のタスク定義の登録解除を行います。

https://dev.classmethod.jp/articles/amazon-ecs-task-definition-deletion/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/3b7be12d-8bd2-08e1-b9f7-8d1437104acc.png)
その後、"非アクティブなタスク"にして削除します。`DELETE_IN_PROGRESS`から消えるまで、30分程度かかる模様です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/5b70f15b-6a32-a52e-b1fd-fc9af573028e.png)

ECRはCloudformationで作成したので、スタックを削除すればOKです。削除前にイメージを削除しておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/b8c7d2ba-8437-c9b2-50e7-0013ad110111.png)


# おわりに
今回はECSのハンズオンを少し変えて、自作のイメージとしてR(plumber)を用いてみました。
簡単にスケーリングするRのスクリプト環境を作れるのが大変魅力的に感じました。