SAMとCodebuildでLambdaの自動deployをする with Julia on Layer

本記事は「[AWS LambdaとServerless Advent Calendar 2020](https://qiita.com/advent-calendar/2020/lambda)」の23日目の記事です。

LambdaとServerlessのadvent calendarなのでLambdaの事を書きます。
コンテナをそのままLambdaのランタイムに使えるようになってたりとかそのへんが気になってますが、個人的に好きな組み合わせを紹介いたします。

# これやります

**SAM(Serverless Application Model)**にて管理するLambdaを**CodeBuild**を使ってdeployをする話を書きます。
githubにpushすると、CodeBuildが実行されてLambdaがdeployされる仕組みです。
ついでにcustom runtimeを使って実行するJuliaのLayerもdeployします。

## 何がいいのか

SAMやChalise、Serverless FrameworkなどのツールはCLIからtestからdeployまで出来て便利です。S3やらSQSやらIAMロールやらいろんなリソースも同時に作る事もできます。
が、その反面、IAMユーザー(or ロール)の権限がめっちゃ強くないといけない。個人でやる分にはいいけど、会社でやるとなるとそんな強いユーザーを扱えない人もちらほらと。
でもgithubにpushなら開発者なら誰でもできる。なんでdeployだけは個人のIAMとは切り離して行いたいわけです。

# 目次

1. SAMのインストールとHello World
2. custom runtimeのLayerをsam build
3. Lambdaをsam build
4. Code Buildの設定

# 環境

- Amazon Linux 2 (on Cloud9)
- t3.micro
- docker 19.03.13-ce

# SAMのインストールとHello World

まずはインストール周りを軽く。
ここ見ればできるんですけどね。
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html

Linux環境でやってます。

## AWS アカウント作成方法
## IAM アクセス許可の設定
## Docker をインストール

すでにできてたので割愛

## インストール Homebrew.

macで使うパッケージマネージャかと思ってたんだけど、Linuxで使ってもいいんですね。
ちょっと意外でした。

```bash
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
$ test -d ~/.linuxbrew && eval $(~/.linuxbrew/bin/brew shellenv)
$ test -d /home/linuxbrew/.linuxbrew && eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv)
$ test -r ~/.bash_profile && echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.bash_profile
$ echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.profile
$ brew --version
$ Homebrew 2.6.2
Homebrew/linuxbrew-core (git revision ee302cd; last commit 2020-12-19)
```

## 1. AWS SAM CLI のインストール

そしてhomebrewにてインストールします。

``` bash
$ brew tap aws/tap
$ brew install aws-sam-cli
$ sam --version
SAM CLI, version 1.13.2
```

version 1.13.2 が入りました。

ついでにHelloWorldだけ。

## Hello World

チュートリアル: Hello Worldアプリケーションの導入
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html

```bash
 $ sam init

        SAM CLI now collects telemetry to better understand customer needs.

        You can OPT OUT and disable telemetry collection by setting the
        environment variable SAM_CLI_TELEMETRY=0 in your shell.
        Thanks for your help!

        Learn More: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-telemetry.html

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1
What package type would you like to use?
        1 - Zip (artifact is a zip uploaded to S3)
        2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Which runtime would you like to use?
        1 - nodejs12.x
        2 - python3.8
        3 - ruby2.7
        4 - go1.x
        5 - java11
        6 - dotnetcore3.1
        7 - nodejs10.x
        8 - python3.7
        9 - python3.6
        10 - python2.7
        11 - ruby2.5
        12 - java8.al2
        13 - java8
        14 - dotnetcore2.1
Runtime: 2

Project name [sam-app]: 

Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
        1 - Hello World Example
        2 - EventBridge Hello World
        3 - EventBridge App from scratch (100+ Event Schemas)
        4 - Step Functions Sample App (Stock Trader)
        5 - Elastic File System Sample App
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: python3.8
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    
    Next steps can be found in the README file at ./sam-app/README.md
```

お！なんか早速、`2 - Image (artifact is an image uploaded to an ECR image ` ってのが増えてる。ECRからLambda作れるようになりましたよね。
ちょっと試してみたいけど遠回りする余裕がないのでなくなくスルー。あとでやる。

```bash
$ cd sam-app
$ sam build
Building codeuri: hello_world/ runtime: python3.8 metadata: {} functions: ['HelloWorldFunction']
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

これであっさりとLambdaは完成。`sam deploy`ってやればオンラインで動いてしまう。
でもここはローカルで試すだけにしておきます。

```bash
$ sam local invoke
Invoking app.lambda_handler (python3.8)
Image was not found.

(略)

{"statusCode": 200, "body": "{\"message\": \"hello world\"}"
```

OK。動きました。
`sam local invoke` は`aws cli lambda invoke` をdeploy前のローカルのLambdaに実行する感じのコマンドですね。

# 2. custom runtimeのLayerをsam build

さて、お次です。
なんの脈略もないのですが、Juliaという言語を扱うためのcustom runtimeをbuildしてみます。
Juliaは使ったことないのですが、試しにという事で。もちろん、Lambdaがデフォルトで扱える言語には含まれていません。

カスタム ランタイムの作成
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/building-custom-runtimes.html

こちらを参考にさせていただきます。
https://github.com/FugroRoames/aws-lambda-julia-runtime
ちょっとversionが古いみたいですけどね。今回はまあいいです。

```bash
$ git clone https://github.com/FugroRoames/aws-lambda-julia-runtime
$ cd aws-lambda-julia-runtime
$ make build-runtime
```

ってやるとzipファイルが生成されます。これをupできれば良さそう。
途中でコンテナも作られてるし、`Type:Image`にした方が綺麗そうではあるけども。
ちょっと今回は手抜きで、このフォルダ(aws-lambda-julia-runtime)内で作業をしていきます。

なお、今回ですが、LayerとFunctionを別々にdeployしていきます。
Layerは本来共通化して使うのがいいと思いますし。

## Layerのdeploy

```yml:template-layer.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: A starter AWS Lambda function.
Resources:
  JuliaLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      Description: Layer description
      ContentUri: '.'
      CompatibleRuntimes:
        - provided
    Metadata:
      BuildMethod: makefile
```

Makefileですがsamのルールにしたがって、buildターゲット名を変更しています。
上の方の `Transform`はcloudformationで使う時に役に立つみたい。今回はこれが必要になります。
それと、もともとはzipファイルを生成して完了なbuildなのですが、ローカルに一度展開するという無駄な作業も。ちょっと強引ですけどね。
unzipの展開先をホームディレクトリ直下の`~/aws-lambda-julia-runtime`にしてしまったので、同じことやるかたは自分の環境に合わせてください。
（ `Type:Image` にしてDockerイメージのままupしたいところだけど、そこまでやる余裕がなかった。。）

```bash
21c21
< build-JuliaLayer:
---
> build-runtime:
36d35
<       @unzip $(CURDIR)/packaging/$(BUNDLE) -d ~/aws-lambda-julia-runtime
```

それともう一点。local moduleをincludeしないといけなかったのと、samとaws cliとで展開先が異なるみたいで、`packaging/bootstrap` も少しいじりました。

```bash
$ diff packaging/bootstrap{,.orig}
10d9
< CMD='using AWSLambdaJuliaRuntime; include("'$CLIENT_MODULE'.jl"); using '$CLIENT_MODULE'; AWSLambdaJuliaRuntime.main('$CLIENT_MODULE');'
12,18c11
< #/opt/bin/julia -e 'using AWSLambdaJuliaRuntime; include("hello.jl"); using hello; AWSLambdaJuliaRuntime.main(hello);'
< 
< if [ -f "/opt/bin/julia" ]; then
<           /opt/bin/julia -e "$CMD"
<   else
<         ${LAMBDA_TASK_ROOT}/bin/julia -e $"CMD"
< fi
---
> ${LAMBDA_TASK_ROOT}/bin/julia -e "using AWSLambdaJuliaRuntime; using ${CLIENT_MODULE}; AWSLambdaJuliaRuntime.main(${CLIENT_MODULE});"
```

まずはbuildです。作成したテンプレートを指定しつつおこないます。

```
$ sam build -t template-layer.yml
Building layer 'JuliaLayer'
Running CustomMakeBuilder:CopySource
Running CustomMakeBuilder:MakeBuild
Current Artifacts Directory : /home/user/aws-lambda-julia-runtime/.aws-sam/build/JuliaLayer

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

次はdeployします。ガイドを用いつつもおおむねはdefaultで。
無事完了するとAWSコンソール上でできてるのが確認できます。

```bash
$ sam deploy -t template-layer.yml --guided

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: julia-layer
        AWS Region [us-east-1]: ap-northeast-1
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: 
        Save arguments to configuration file [Y/n]: 
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 

....
```

![advn1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/34083fdc-4a72-e1ae-8eb0-2a27862e6611.png)

Layerはできました！

## 3. Lambdaをsam build

お次はFunctionです。
これについては別でDirectory作ります。
あと`aws-lambda-julia-runtime`にあるexamplesをコピっておきます。

```bash
$ mkdir -p advntLambda1223/lambda_functions/julia-lambda-function
$ cd advntLambda1223/lambda_functions/julia-lambda-function
$ cp -a (任意)/aws-lambda-julia-runtime/examples ./src
```

ここにtemplate.ymlを作ります。
Layersにarnが記載されてるのがポイントですね。AWSコンソールから確認して貼り付けると良いかと思います。

```yml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: An AWS Serverless Specification template describing your function.
Resources:
  HelloJuliaFunction:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: word_count.handler
      Runtime: provided
      CodeUri: ./src
      Description: ''
      MemorySize: 512
      Timeout: 30
      Layers:
        - 'arn:aws:lambda:ap-northeast-1:11111111:layer:JuliaLayer:2'
      Events:
        Hello:
          Type: Api 
          Properties:
            Path: /hello
            Method: get
```

あと、samの場合で`Runtime: provided`の場合、Makefileがないとエラーになっちゃったのでほとんど空ですが一応作っておきます。

```src/Makefile
build-HelloJuliaFunction:
        @echo '$(GREEN)Building runtime...$(RESET)'
```

これで準備は完了なのでbuildしつつ、試しにinvokeもしておきます。

```
$ sam build                                                                              
Building codeuri: ./src runtime: provided metadata: {} functions: ['HelloJuliaFunction']
Running CustomMakeBuilder:CopySource
Running CustomMakeBuilder:MakeBuild
Current Artifacts Directory : /home/user/advntLambda1223/lambda_functions/julia-lambda-function/.aws-sam/build/HelloJuliaFunction

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

$ sam local invoke                                                                       
Invoking word_count.handler (provided)
Downloading arn:aws:lambda:ap-northeast-1:11111111:layer:JuliaLayer  [####################################]  73945677/73945677
Image was not found.
Building image.......................
Skip pulling image and use local one: samcli/lambda:provided-aba26257d2bcf3655ab04c8a2.

Mounting /home/user/advntLambda1223/lambda_functions/julia-lambda-function/.aws-sam/build/HelloJuliaFunction as /var/task:ro,delegated inside runtime container
START RequestId: f7fab733-e3da-4d31-93b1-fbcbf04e15b7 Version: $LATEST
Hello World : {}
END RequestId: f7fab733-e3da-4d31-93b1-fbcbf04e15b7
REPORT RequestId: f7fab733-e3da-4d31-93b1-fbcbf04e15b7  Init Duration: 0.06 ms  Duration: 3597.43 ms    Billed Duration: 3600 ms        Memory Size: 512 MB        Max Memory Used: 512 MB
{"msg": "Me Rockz!"}
```

*ちゃんと動きましたね！*
内容はちょっとシンプルすぎてつまらないですが。。

じゃあこのまま一旦deployまでやっておきます。

```bash
$ sam build
Building codeuri: ./examples runtime: provided metadata: {} functions: ['HelloJuliaFunction']
Running CustomMakeBuilder:CopySource
Running CustomMakeBuilder:MakeBuild
Current Artifacts Directory : /home/user/advntLambda1223/aws-lambda-julia-runtime/.aws-sam/build/HelloJuliaFunction

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
    
ec2-user:~/aws-lambda-julia-runtime (master) $ sam deploy --guided -t template-2.yml                                                       

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Found
        Reading default arguments  :  Success

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [julia-layer]: julia-function
        AWS Region [ap-northeast-1]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [Y/n]: 
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: 
        Save arguments to configuration file [Y/n]: 
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 

        Looking for resources needed for deployment: Found!

                Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxx
                A different default S3 bucket can be set in samconfig.toml

        Saved arguments to config file
        Running 'sam deploy' for future deployments will use the parameters saved above.
        The above parameters can be changed by modifying samconfig.toml

.....

Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

2020-12-20 05:51:20 - Waiting for stack create/update to complete

......

Successfully created/updated stack - julia-function in ap-northeast-1
```

![advn2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/1e40c97d-06dd-0034-ab27-c685ff5b48ab.png)

できました！

# いったんまとめ

ここまでで、

- samを使ってJuliaを実行するcustom runtime Layerをdeploy
- 上記のcustom runtimeを使うfunctionをdeploy

ができました。

# 4. Code Buildの設定

次はCode Build上でdeployをする仕組みを構築していきます。
何をやるのかというと、

1. Gitにup
2. CodeBuildにてBuild&deploy

というシンプルな流れを作っていきます。
samで使ったtemplateを使いますが、ちょっとだけいじります。
Api Gatewayも使ってみる。スクリプトは単純なままですが。

```yml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: An AWS Serverless Specification template describing your function.
Resources:
  HelloJuliaFunction:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: hello.handler
      Runtime: provided
      CodeUri: ./src
      Description: ''
      MemorySize: 512
      Timeout: 30
      #Role: arn:aws:iam::1111111:role/LambdaExecRole
      Layers:
        - 'arn:aws:lambda:ap-northeast-1:11111111:layer:JuliaLayer:1'
      Events:
        Hello:
          Type: Api
          Properties:
            Path: /
            Method: get
````

## ディレクトリ構造

```
$ tree -la
.
└── lambda_functions
    ├── .build
    │   ├── build.sh #CodeBuildが実行するshell
    │   └── buildspec.yml #Build設定
    └── julia-lambda-function
        ├── build.sh
        ├── .gitignore
        ├── src
        │   ├── Makefile
        │   ├── REQUIRE
        │   └── hello.jl
        └── template-export.yml #template.ymlにrenameします
```

## gitにup

こちらのリポジトリを作りました。
https://github.com/ikegam1/advendLambda1223

各ファイルの中身については割愛しますが、リポジトリを直接見ていただければと思っています。

## CodeBuild設定

CodeBuildではリポジトリにtagを切ってpushされた動作をトリガーにbuildを実行していきます。
なお、複数のfunctionに対応したいため、タグ名にfunction名を含んでもらうようにしています。
タグ名のルールはこんな感じです。

`[function-name]-[stage].[version]`

例えば、`julia-lambda-function` の `develop` で　version: `1` であれば、
`julia-lambda-function.develop.1`という感じになります。

## Aws Console

ではコンソールでサクサクやっていきます。
まずはCodeBuildの画面を開きまして、*ビルドプロジェクトを作成*します。
プロジェクト名は任意。今回は`advendLambda1223`にしておきます。

[advn3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/d79eadcf-0fb1-9c36-e1ee-150d47bdf931.png)

ソースのところではGitHubを選択し、ソースのあるリポジトリを連携します。
WebHookの設定も必要で、タグが切られた時に走るようになります。

![advn9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/e28f9970-aaac-9d14-dbae-9b633ba300c7.png)

環境は以下。正直、適当です。
なお、buildspec.ymlのある場所はちゃんと指定しないといけないです。

![advn11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/986597b1-d516-a900-fff5-69488761da4b.png)

## iam Role設定

こちらで用いるiamロールですが、cloudformationとかバリバリ使うので強力な権限が必要です。samで使ってるiamと同じ程度の強さのやつ。
CloudFormationのFullとS3のFull、apiのAdmin・・とIam RoleのCreate権限があれば今回のFunctionは大丈夫かなと思います。
が、Iam Roleはtemplate.ymlに既存のやつを書いておいた方がいいですね。たぶん、samに自動で生成させてそれを使うと早いと思います。
`Resource>HelloJuliaFunction>Properties>Role`です。
ほんとはもっと絞った方がいいと思うのですが、お試しということで試した後にすぐ消す前提なら大丈夫かなって思います。

![advn12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/bf94aa9c-6844-a9cc-e52d-73bda78640be.png)


## deploy

試しにREADMEでも作ってpushしてみますね。

```bash
$ echo '# READ ME!' > README.md
$ git add README.md
$ git commit -m 'deploy test'
$ git push
$ git tag -a julia-lambda-function.develop.1 -m 'deploy start'
$ git push -u origin julia-lambda-function.develop.1 
```

![advn10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/ad355242-911b-c8aa-e580-a7568f7d0b65.png)

Buildが走りました！

![advn13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/f7a22036-e2ee-d59b-1ccd-297f4e9eba7b.png)

そして成功！

![advn16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308709/ba38154f-c3b9-2072-c1f8-4509b2d14701.png)

実行もOKでした！
URLの末尾に`?msg=hogehoge`みたいにするとechoしてくれます。

## lambda_functions/julia-lambda-function/build.sh

ちょっと工夫してるのはこのシェルです。
ローカルでは`sam`コマンドでLambda Functionをdeployしていますが、CodeBuildではこれを使っていません。
serverless frameworkとかもあるけど、samの好きなところはここですね。CodeBuild上でいちいちCLIをインストールする必要がない。

```bash:build.sh
#!/bin/bash
export BUCKET="sam-packages"

pwd

aws cloudformation package --force-upload --template-file template.yml --s3-bucket $BUCKET  --output-template template-export.yml

aws cloudformation deploy --force-upload --template-file template-export.yml --s3-bucket $BUCKET  \
  --s3-prefix $FUNCTION --stack-name "${TAG}-${STAGE}" --capabilities "CAPABILITY_IAM" --region ap-northeast-1 

aws cloudformation describe-stacks --stack-name "${TAG}-${STAGE}" --query 'Stacks[]'

exit 0
```
※だいぶ簡素にはしています。実際の運用ではこのshellからslack通知したりとかはしてます。

代わりにつかっているのは`aws cloudformation package`と`aws cloudformation deploy`。
samってcloudformationの拡張みたいなやつでして、cloudformationでも同じテンプレートが使えるのですよね。
ここではpackageでS3にuploadして、それをdeployでdeployしてる感じです。


# 最後に

advent-calendar、けっこうギリギリになってしまって余裕がなかったです。。
ECR周りも少し試してみた所便利そうでした。ただ、Layerとの絡みはsamの対応がまだ追いついてなさそう。すぐ追いつくとは思うけど。

あとは、Juliaが気になってたので試しに触れてみた。pythonに近い感じかと思ってたけど、コードはそんなに似てないかもしれない。