Terraform管理下のLambda@edgeでapply前にyarn installを必ず実行する方法

# はじめに

この記事はドワンゴアドベントカレンダー2019の12/4です。

名前をfusagikoと言います。
会社でもfusagikoというニックネームそのままで仕事をしています。
本名は異体字が入っているので素直に検索すると出ず、よく混乱されます。

会社ではTerraform(主にAWS)やAnsibleを使ったミドルウェア(nginx, PgBouncer, Dockerとかのその辺り)より下の設計、構築、運用を中心として、それらの上で駆動するRuby on RailsやNode.js、ついでにReactなどについてもコードを読んでミドルウェア以下の構造を合わせつつ、改修の必要があれば上下の事情を鑑みてうまく噛み合うように、自分がメインで見ているミドルウェアより下はもちろんアプリコード側にも同時にPRを送る、というような立ち位置で仕事をしています。

今回はAWSのLambda@edgeについて取り上げます。

# Lambda@edgeとは
サーバレスアーキテクチャ[^1]を駆動するプラットフォームの先駆けであるAWS Lambdaのうち、いくつかの制限(ネットワーク通信ができない、など)がある代わりにCDN(Content Delivery Network)サービスであるCloudfrontのエッジサーバ上で動作するものです。

[^1]: アプリケーションサーバを構築して利用者からの要求を待つという従来から存在するアーキテクチャと対照して、利用者からプラットフォームに対して要求が来たときに、その要求の種類に応じて対応するコードが都度ロードされ処理されるアーキテクチャ。Function as a Service(FaaS)とも。

# Lambda@edgeをどこで管理するべきか問題
Lambdaでサーバレスアーキテクチャのサービスを組む場合は[Apex](http://apex.run/)や[Serverless](https://serverless.com/)を用いることが多いのではないかと思います。しかし、Lambda@edgeはCloudfrontのエッジサーバ上で動作するものですから、Cloudfrontの裏に存在するoriginサーバまでサーバレスアーキテクチャである必要はありません。

[ApexはそもそもLambda@edgeに対応していない](https://github.com/apex/apex/issues/610)ようですし、[Serverlessもプラグインは存在する](https://github.com/silvermine/serverless-plugin-cloudfront-lambda-edge)ものの、一緒にCloudfrontまでデプロイする必要があるようで[^2]、Serverlessでそこまで頑張るのが適切かどうかというと疑問が残ります。

[^2]: Serverlessは裏側でCloudFormationを使っているので管理外のリソースを直接弄ることができない

Cloudfrontを管理するならばそういったインフラリソースを管理するために作られているAWS謹製のCloudFormationか、Terraformで管理したくなります。今回はTerraformを用いる場合についてです。

# Lambda@edgeでnpmパッケージが使いたいことがある
Terraformはインフラリソースを管理するために作られていますから、Lambda@edgeも当然のようにデプロイすることができます。

やりたい内容が[このように](https://aws.amazon.com/jp/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/)単一のjsファイルだけで完結するレベルであれば、[archive_fileデータソース](https://www.terraform.io/docs/providers/archive/d/archive_file.html)、[publishをtureにしたaws_lambda_functionリソース](https://www.terraform.io/docs/providers/aws/r/lambda_function.html)、[cloudfront_distributionリソースのlambda_function_associationブロック](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#lambda_function_association)を使えば終わりです。

が、npmライブラリを使いたくなる場合が稀によくあります。

- [UAParser.js](https://github.com/faisalman/ua-parser-js)でデバイスごとにリダイレクトしたい
- ドキュメントルートにアクセスが来たとき、[Moment.js](https://momentjs.com/)と組み合わせて最新月の記事一覧を自動的に表示させたい

とか。
裏に控えているアプリケーションサーバに負荷をかけずにこれらができるならまあ、それなりに嬉しい人もいるのではないかと思います。

# Terraformでデプロイするときに必ずyarn installさせたい
さて、今npmパッケージのパッケージ管理をするならばyarnを使うのが無難かなと思いますが[^5]、Terraformではarchive_fileデータソースのように自動でyarn installをしてくれるTerraform providerは存在しません。

[^5]: npmにもバージョン固定機能が実装されましたが、いずれにせよTerraformから外部コマンドを実行する必要があることに変わりはありません

かといって、README.mdに「デプロイ前にはこのディレクトリで必ずyarn installを実行してください」と書いたところで、実行し忘れたままのデプロイが発生して動作しなくなる事故がいつか発生するであろうことは明白です。

ではどうすればいいのでしょうか。

あるんですよ、外部コマンドを自由に呼び出せるproviderが。

# External provider
読んで字のごとく、Externalなproviderです。
このproviderはExternal Data Sourceというデータソースを唯一持っており、その動作は**「任意のコマンドを実行し、その標準出力をjsonとしてパースし、json内に書かれた値をAttributesとして出力する」**というものです。闇です。今、闇の深淵を覗いています。

そんなものなので凝ったことをしすぎると明らかに負債化しますので、用法用量を守って正しく使いましょう。

# External Data Sourceでyarn installを実行する
前節で書いたとおり、External Data Sourceは任意のコマンドを実行し、その結果をJSON文字列として受け取るものです。
つまり、標準出力へJSON文字列が出さえすればよい。

であれば、そのようなスクリプトを組んでしまいましょう、bashで。

```bash
#!/usr/bin/env bash
# 何らかのコマンドが失敗したり、未定義変数が使われたら即エラー終了
set -eu

# サブシェルのディレクトリを移動する
cd $(dirname ${BASH_SOURCE:-$0})

# yarn installの出力は/dev/nullに捨てる
yarn install > /dev/null 2>&1

# terraformに対する出力はjsonである必要があるのでechoで雑にjsonを出力する
echo '{"yarn": "OK"}'
```

はい、（書いてある内容そのものは）とても簡単ですね。
単なるシェルスクリプトなので、必要なら途中にwebpackやyarn testを挟むこともできます。

一つ注意点として、External Data Sourceそのものにはワーキングディレクトリを変更する機能がありません[^3]。したがって、実行するbashスクリプト内でワーキングディレクトリを変更する必要があります。

[^3]: 実はgithub上のmasterブランチでは[その機能](https://github.com/terraform-providers/terraform-provider-external/pull/12)が既にマージされています。が、そのマージ以来9ヶ月、バージョンの付いたリリースが行われていないため、普通には使用することができません。

その変更先ディレクトリのパスを取得する際にbash独自記法が入っている[^4]関係でShebangにenv bashを指定していますので、ashしかないAlpineなどでは動作しないケースがあるかもしれません。

[^4]: どのシェルでも動くようにするのが意外と難しい　ref: [bash/zshでsourceされたスクリプト内で、ファイル自身の絶対パスをとるシンプルな記法](https://qiita.com/yudoufu/items/48cb6fb71e5b498b2532)

# External Data Sourceから呼び出す
あとはexternalデータソースから呼び出すだけでコマンド、すなわち先ほど作成したbashスクリプトが実行され、Lambda@edgeのyarn installを実行された後にarchive_fileデータソースによってzip化が実行されます。

Lamnda@edgeはCloudfrontと関連付けする関係上、バージニア北部(us-east-1)リージョンに存在する必要がありますので、東京などのリージョンを使っている場合は[Multiple Providers](https://www.terraform.io/docs/configuration/providers.html#multiple-provider-instances)を使ってLambda@edgeを作成するリージョンだけバージニア北部にしなければなりません。

あとはもうterraform planし、terraform applyするだけです。

# まとめ
本記事ではTerraform管理下のLambda@edgeでapply前にyarn installを必ず実行する方法について書きました。
Lambda@edgeは制限こそあれど、User Agentを見てリダイレクトするといったようなアクセスごとに毎回実行する必要があり、なおかつ本質的でないコードをアプリケーションに書く必要がないという点で割と便利なのではないかと思います。

今回の内容のサンプルをgithubリポジトリとして公開する予定でしたが、サンプルとして公開できるもののみで組み直すのに時間が足りませんでしたので、これは後日追加します。