lambda
Terraform
YARN
Lambda@Edge

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のエッジサーバ上で動作するものです。


Lambda@edgeをどこで管理するべきか問題

Lambdaでサーバレスアーキテクチャのサービスを組む場合はApexServerlessを用いることが多いのではないかと思います。しかし、Lambda@edgeはCloudfrontのエッジサーバ上で動作するものですから、Cloudfrontの裏に存在するoriginサーバまでサーバレスアーキテクチャである必要はありません。

ApexはそもそもLambda@edgeに対応していないようですし、Serverlessもプラグインは存在するものの、一緒にCloudfrontまでデプロイする必要があるようで2、Serverlessでそこまで頑張るのが適切かどうかというと疑問が残ります。

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


Lambda@edgeでnpmパッケージが使いたいことがある

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

やりたい内容がこのように単一のjsファイルだけで完結するレベルであれば、archive_fileデータソースpublishをtureにしたaws_lambda_functionリソースcloudfront_distributionリソースのlambda_function_associationブロックを使えば終わりです。

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



  • UAParser.jsでデバイスごとにリダイレクトしたい

  • ドキュメントルートにアクセスが来たとき、Moment.jsと組み合わせて最新月の記事一覧を自動的に表示させたい

とか。

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


Terraformでデプロイするときに必ずyarn installさせたい

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

かといって、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で。

#!/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そのものにはワーキングディレクトリを変更する機能がありません4。したがって、実行するbashスクリプト内でワーキングディレクトリを変更する必要があります。

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


External Data Sourceから呼び出す

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

Lamnda@edgeはCloudfrontと関連付けする関係上、バージニア北部(us-east-1)リージョンに存在する必要がありますので、東京などのリージョンを使っている場合はMultiple Providersを使ってLambda@edgeを作成するリージョンだけバージニア北部にしなければなりません。

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


まとめ

本記事ではTerraform管理下のLambda@edgeでapply前にyarn installを必ず実行する方法について書きました。

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

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





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



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



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



  4. 実はgithub上のmasterブランチではその機能が既にマージされています。が、そのマージ以来9ヶ月、バージョンの付いたリリースが行われていないため、普通には使用することができません。 



  5. どのシェルでも動くようにするのが意外と難しい ref: bash/zshでsourceされたスクリプト内で、ファイル自身の絶対パスをとるシンプルな記法