ApexでGoのLambdaを作成しようとしたら少しハマった

はじめに

Lambdaを作成するご用事があったので、goで作ってみることにしました。デプロイにapexを使ってみたのですが、少しハマったので、原因と解決方法をまとめておきます。

TL;DR

  • apexでgoを使うには"go get -v -t -d ./... && GOOS=linux GOARCH=amd64 go build -o main main.go"をフックに追加が必要
  • function.jsonにフックがあるとproject.jsonのフックは呼ばれない
  • 別法として"runtime": "golang"を入れるとビルドしてくれるようになるが、フックがあると効果が無くなる

使用環境

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.12.6
BuildVersion:	16G1510

apex

Lambdaを作成するときにマネージメントコンソールで作成するのは少々面倒ですが、apexというツールを使うとコマンドラインで作業を進められるので幸せになれそうです。

まずは手始めに、apexのgithubリポジトリにある、apex/_examples/goを試してみました。

インストール

apexコマンドを下記のようにインストールします。

$ curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh

バージョンは下記でした。

$ apex version
Apex version 1.0.0-rc2

サンプルの取得

リポジトリを丸ごとcloneしておきます。

$ git clone https://github.com/apex/apex

goのサンプルは、apex/_examples/goにあります。

AWS-CLIとクレデンシャルの用意

aws-cliが使える環境を用意しておきます。

IAMポリシーの作成

サンプルを使うためには、project.jsonにroleを設定する必要がありまし。公式サイトに記載されているように、マネージメントコンソールからroleを作成したうえで、下記の内容のポリシーをアタッチしておきます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "iam:CreateRole",
        "iam:CreatePolicy",
        "iam:AttachRolePolicy",
        "iam:PassRole",
        "lambda:GetFunction",
        "lambda:ListFunctions",
        "lambda:CreateFunction",
        "lambda:DeleteFunction",
        "lambda:InvokeFunction",
        "lambda:GetFunctionConfiguration",
        "lambda:UpdateFunctionConfiguration",
        "lambda:UpdateFunctionCode",
        "lambda:CreateAlias",
        "lambda:UpdateAlias",
        "lambda:GetAlias",
        "lambda:ListAliases",
        "lambda:ListVersionsByFunction",
        "logs:FilterLogEvents",
        "cloudwatch:GetMetricStatistics"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

roleのarnをproject.jsonに設定します。

{
  "name": "go",
  "description": "Go example project",
  "role": "arn:aws:iam::XXXXXXXXXXXX:role/api-function"
}

デプロイ

あとは、コマンドラインからこんな感じでデプロイするだけです。

$ cd apex/_examples/go
$ apex deploy
   • creating function         env= function=simple
   • created alias current     env= function=simple version=1
   • function created          env= function=simple name=go_simple version=1

マネージメントコンソールで確認すると、lambdaがそれらしくできていました。

ところが...

デプロイしたlambdaは、apex invokeコマンドで実行可能となるはずです。ところが、いざ実行してみようとすると、こんなエラーが発生してしまいました。

$ apex invoke simple <event.json 
   ⨯ Error: function response: fork/exec /var/task/main: no such file or directory

最初は、apexが何をやっているのかわかっていなかったので、原因を調べるのに手こずってしまいました。apex buildコマンドでzipファイルを作成してみると、中身が.goのソースファイルしか入っていません。

$ apex build simple > main.zip
$ unzip -Lv main.zip
Archive:  main.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name ("^" ==> case
--------  ------  ------- ---- ---------- ----- --------  ----   conversion)
     280  Defl:N      197  30% 01-01-1970 09:00 c43fdcff  main.go
--------          -------  ---                            -------
     280              197  30%                            1 file

このzipがアップロードされたとしても、lambdaでは実行しようもありません。エラーの原因はapexがgoのコンパイルをしてくれておらず、バイナリが入っていないということでした。

対応:フックを追加する

これを修正するため、project.jsonに下記を追記します。これはちゃんと公式サイトに記載されています。しかしサンプルapex/_example/goには設定されていませんでした。

  "hooks": {
    "build": "GOOS=linux GOARCH=amd64 go build -o main main.go",
    "clean": "rm -f main"
  }

ところが、これをやっても正常にビルドが行われませんでした。というのも、このサンプルのfunction.jsonには外部モジュールを引っ張ってくるための別のフックが記述されており、それが優先された結果project.jsonのフックが無視されてしまっていたようです。

  "hooks":{
    "build": "go get -v -t -d ./..."
  }

なので正解は、上記二つを合わせた下記のフックをfunction.jsonもしくはproject.jsonのいずれかに設定することのようです。今回は、function.jsonのみにフックを作成することにしました。

  "hooks": {
      "build": "go get -v -t -d ./... && GOOS=linux GOARCH=amd64 go build -o main main.go",
      "clean": "rm -f main"
  }

.apexignore

アップロードするzipファイルに必要なのはバイナリだけで、ソースファイルは必要ありません。そのため、.apexignoreファイルを下記を内容で作成します。

*.go

以上の作業により、必要十分な内容のzipファイルがapex buildで作成されるようになりました。mainのバイナリサイズはおよそ860kByteとなっています。

$ apex build simple > main.zip && unzip -Lv main.zip 
Archive:  xxx.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name ("^" ==> case
--------  ------  ------- ---- ---------- ----- --------  ----   conversion)
 8610976  Defl:N  3052041  65% 01-01-1970 09:00 8eace7d4  main
--------          -------  ---                            -------
 8610976          3052041  65%                            1 file

実行

apex invokeコマンドで実行してみます。入力にあらかじめ用意されているevent.jsonを指定します。

$ apex invoke simple <event.json 
"Hello Tobi, you are a Ferret"

無事成功しました!

--logsオプションを付けると、ログも同時に確認することができます。

$ apex invoke simple --logs <event.json 
START RequestId: 4b19b319-9b04-11e8-bae2-df4f63b4fe20 Version: 15
END RequestId: 4b19b319-9b04-11e8-bae2-df4f63b4fe20
REPORT RequestId: 4b19b319-9b04-11e8-bae2-df4f63b4fe20	Duration: 0.42 ms	Billed Duration: 100 ms 	Memory Size: 128 MB	Max Memory Used: 22 MB	
"Hello Tobi, you are a Ferret"

実行時間やメモリ量など参考になる情報が得られます。

もう一つの方法

上の例ではフックを追加する方法を示しましたが、実は別にもう一つ方法があることに気づきました。その方法とは、function.jsonもしくはproject.jsonに、"runtime": "golang"を追加するのです。そうすると、ちゃんとApex組み込みのgo向けビルドルールが適用され、フックがなくてもちゃんとバイナリが生成されるようになります。

{
  "description": "Example",
  "runtime": "golang"
}

これで解決かと思いきや、実はこの方法では外部パッケージのダウンロードをしてくれないのです。元のように、フックで外部パッケージをダウンロードのコマンドを入れてしまうと"runtime": "golang"の効果が失われ、またしてもバイナリが作られないのです。外部パッケージのロードは一度行えば、以後は不要となるのですが、もやっとすることは否めません。

結論としては、フックで明示的に、ダウンロードとビルドを共に行うのが最も汎用性があるようです。

まとめ

最初は何が起きているのかわからず、かなり回り道をしてしまいました。おかげでapexの理解が進んだようにも思います。デプロイ時にエラーが生じなかったので、つい油断していた気もします。私のハマった数時間が、どなたかのお役に立てば幸いです。

参考

  • http://apex.run/
  • https://github.com/apex/apex/blob/master/docs/hooks.md
  • https://dev.classmethod.jp/cloud/aws/aws-lambda-supports-go/
  • https://dev.classmethod.jp/cloud/aws/deploy-serverless-applications-to-aws-with-apex-up/