1月15日、ついに待ちに待った AWS Lambda Go がリリースされました!🎉 今年最初のブログ記事はその活用例のご紹介をしたいと思います。
AWS Lambda は言わずと知れたサーバーレスアーキテクチャのプラットフォームですが、今まで Node.js, Java, Python, C# (.NET Core 1.0) の 4 種類のランタイムしか使えませんでした。それが今回、Go [参考文献1] と C# .NET Core 2.0 [参考文献2] に対応し、ますます強力なプラットフォームに進化しました。特に Go は、そのエコシステムがもたらすプロセスのフットプリントの小ささから、実行時間とメモリ消費でスケールする Lambda の課金体系にダイレクトに効いてくる事が大いに期待できます。そしてもちろん、AWS 公式ライブラリを始めとした膨大な Go コード資産を容易に Lambda 化出来るようになったという点も大きな魅力です。
今回は、AOSN 読書会のウェブサイト (https://aosn.github.io) の課題本ごとのページに設置した参加記録のチャートを動的に生成する仕組みを AWS Lambda Go と Chart.js を用いて実現することができたので紹介します。
こちらの元データは同じページにある参加記録の表となっています。このサイトは GitHub Pages 標準の Jekyll で生成されたもので、元データは Markdown です。読書会が終わると、議事録担当者 (だいたい私😇) が今日の参加者と進捗をここに記していく運用をかれこれ 3 年以上しています。
私は以前この Markdown をパースして Elasticsearch に送りデータビジュアライゼーションする Go 言語のコードを書いており、今回はそのコードを流用して Lambda をインプリします。Markdown の表部分に参加記録の行を足してからグラフに反映されるまでの仕組みは、次のような流れになります。
今回作るグラフは、冒頭にも示したように各回の参加者数の推移と参加者毎の参加回数のランキングの 2 つのグラフです。この流れの最終成果物 (Lambda から S3 に投入する JSON ファイル群) は次のような内容になります (課題本ごとに JSON ファイルを生成したうちの 1 つ)。
{
"times":{
"labels":[1,2,3,4,5,6],
"data":[4,3,3,3,3,3]
},
"attendees":{
"labels":["intptr-t","mikan","budougumi0617","MrBearing","kzt-ysmr"],
"data":[6,6,5,1,1]
}
}
対応する (パース結果を流し込む) Go の構造体はこんな感じです。
type DataSet struct {
ByTimes IntLabeledData `json:"times"`
ByAttendees StringLabeledData `json:"attendees"`
}
type IntLabeledData struct {
Labels []int `json:"labels"`
Data []int `json:"data"`
}
type StringLabeledData struct {
Labels []string `json:"labels"`
Data []int `json:"data"`
}
それでは本題の AWS Lambda Go の構築手順を説明したいと思います (前置きが長い)。
Handler Implementation
Go で AWS Lambda のハンドラーを実装するのは他の言語同様にとても簡単です。利用可能な関数シグニチャの一覧がドキュメントにあるので、好きなシグニチャを選んで定義するだけで、最も簡単なものは func ()
だけです [参考文献3,5]。関数名は何でも良いです。
package main
import "github.com/aws/aws-lambda-go/lambda"
func HandleLambdaEvent() {
// ...
}
func main() {
lambda.Start(HandleLambdaEvent)
}
github.com/aws/aws-lambda-go
ライブラリは、GitHub にあるので go get
して取ってきます [参考文献4]。
デプロイ可能な zip ファイルを作るには、GOOS=linux
でコンパイルした結果を zip
します。Linux 持ってない?心配ありません。Go ならクロスコンパイルは鼻血がでるほど簡単です。
GOOS=linux go build -o main
zip main.zip main
上記は macOS を想定したものですが、Windows で上記同様の zip を生成するために build-lambda-zip
というツールが提供されています (go get github.com/aws/aws-lambda-go/cmd/build-lambda-zip
)。私はこんなバッチファイルを作りました。
set GOOS=linux
go build -o main
build-lambda-zip -o main.zip main
del main
set GOOS=windows
⚠️ GOOS
を windows
に戻すのを忘れると悲惨なことになりますよ!
出来上がった zip はコンソールや awscli
でアップします。ハンドラ名 (zip の中に入ってるファイル名) は、この例では main
です。
Webhook Integration
アップロードできたら、GitHub の Webhook をトリガーにして走らせるため、API Gateway を仕込みます。Webhook なのでメソッドは POST
です。
今回は Lambda プロキシ統合は使用しません。GitHub の Webhook にはただステータス 200 を返してくれさえすれば良いのです。逆に、もし Lambda Proxy でデータを返す場合はレスポンス情報を組み立てる追加の実装が必要になります (そして Malformed Lambda proxy response と戦う😇)。
GitHub の Webhooks は以下のように設定します。Content type は application/json
を選びます。これで毎回 git push されるたびに API Gateway (+ Lambda) が叩かれる仕組みが出来上がりです。
Storing and Hosting
AWS Lambda Go で S3 を叩くには、すでにある aws-sdk-go
が便利です [参考文献6]。今回書いた S3 部分のソース全文を紹介しましょう。関数 Upload()
は文字通りファイル名と中のデータを受け取ってアップロードします。
package main
import (
"bytes"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
func Upload(name string, data []byte) {
bucket := "xxxxxxx"
service := s3.New(session.Must(session.NewSession(&aws.Config{
Region: aws.String(endpoints.ApNortheast1RegionID),
})))
_, err := service.PutObject(&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(name),
Body: bytes.NewReader(data),
ContentType: aws.String("application/json"),
ACL: aws.String(s3.BucketCannedACLPublicRead),
})
if err != nil {
panic(err)
}
}
コードのどこにも S3 のクレデンシャル情報が現れないことに不思議に思うかもしれません。でも、このコードは確かに動きます。お察しの通り、これは aws-sdk-go
と Lambda の Go ランタイムの組み合わせによって成し遂げられています。お見事ですね✨
お目当ての成果物が無事 S3 に上がったところで、もう一つだけ作業があります。バケットを公開することと、静的 Web サイトから直接 XHR で叩く (= ダイレクトホスティング) 用に CORS (Cross-Origin Resource Sharing) の設定を仕込むことです。
S3 のコンソール (https://s3.console.aws.amazon.com) で対象バケットを選び、「アクセス制限」を開くと CORS の設定が記述できるようになっています。AOSN 読書会のドメインから受け付ける設定を入れたものはこんな感じです。
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>http://aosn.github.io</AllowedOrigin>
<AllowedOrigin>https://aosn.github.io</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
これでインフラは全て整いました。
Chart.js
ここでようやく今回のもう一つのお目当ての一つ、Chart.js の登場です。HTML5 の canvas タグを用いたリッチなチャートを平易な API で描画できる素晴らしいライブラリです。公式サイトの Samples [参考文献7] を見ると、色々な図が描画できることがわかります。
今回、Chart.js 本体は CDN を利用することにしてみました。
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
チャートを表示したいページに canvas
を配置します。今回、同様のチャートを課題本ごとに描画するため、JS コードは関数にして共通化しています。
### 参加者推移
<canvas id="timesChart" width="400" height="200"></canvas>
### 参加回数
<canvas id="attendeesChart" width="400" height="200"></canvas>
<script>
handleEntryCharts("1-java8");
</script>
共通コードはこちらです。チャートの要素に充てるカラーパレットの元データは、SAP のスタイルガイドから拝借しました [参考文献9]。ページから呼び出された handleEntryCharts()
(一番下) で S3 に置いた JSON の XHR 取得およびパース、そしてそこから呼び出す drawEntryCharts()
(その上) が Chart.js を描画するところです。
<script>
colors = [
'rgba(92, 186, 230, 0.7)', 'rgba(182, 217, 87, 0.7)', 'rgba(250, 195, 100, 0.7)', 'rgba(140, 211, 255, 0.7)',
'rgba(217, 152, 203, 0.7)', 'rgba(242, 210, 73, 0.7)', 'rgba(147, 185, 198, 0.7)', 'rgba(204, 197, 168, 0.7)',
'rgba(82, 186, 204, 0.7)', 'rgba(219, 219, 70, 0.7)', 'rgba(152, 170, 251, 0.7)'];
lineChartOptions = {
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
};
horizontalBarChartOptions = {
scales: {
xAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
function drawEntryCharts(dataset) {
var timesChart = new Chart(document.getElementById("timesChart").getContext('2d'), {
type: 'line',
data: {
labels: dataset.times.labels,
datasets: [{
label: '参加者数',
data: dataset.times.data,
backgroundColor: colors
}]
},
options: lineChartOptions
});
var attendeesChart = new Chart(document.getElementById("attendeesChart").getContext('2d'), {
type: 'horizontalBar',
data: {
labels: dataset.attendees.labels,
datasets: [{
label: '参加回数',
data: dataset.attendees.data,
backgroundColor: colors
}]
},
options: horizontalBarChartOptions
});
}
function handleEntryCharts(key) {
const request = new XMLHttpRequest();
request.open("GET", "https://s3-ap-northeast-1.amazonaws.com/ws.aosn.chart/" + key);
request.addEventListener("load", (event) => {
if (event.target.status !== 200) {
console.log(`[chartgen] ${event.target.status}: ${event.target.statusText}`);
return;
}
drawEntryCharts(JSON.parse(event.target.responseText));
});
request.send();
}
</script>
描画結果 (再掲)
結果
今回実装した AWS Lambda Go は GitHub から Markdown 14 個とってきてパースして集計して 1 つ 1 つ S3 にぶち込むというデモにしてはやや大きな処理ですが、それがたったの 31 MB / 670.77 ms で動きました。Java で同じ機能を実現して走らせたらまず 128MB では足りないですし、時間ももっとかかるはずです。期待通りの優れたフットプリントでした。 今回はたまたま既に書いた Go のコードを手数をかけずに Lambda 化できるようになったことがモチベーションになって手を付けましたが、スクラッチで Lambda 作る場合も積極的に Go を選んで行きたくなりますね。
今回の Lambda のコードはこちらにあります。この記事を書いたあと構成を変えていなければ、cmd/aosn2lambda ディレクトリに今回の Lambda のコードがあります。
aosn/chartgen
Chart.js の関数は静的サイト、つまり AOSN 読書会ウェブサイトのリポジトリにあります。共通化した関数定義は _includes ディレクトリ内の head.html にあります。
aosn/aosn.github.io
参考にしてみてください。
Happy hacking!
参考文献
- AWS Lambda での Go サポート開始 - aws.amazon.com
- AWS Lambda での C# (.NET Core 2.0) サポート開始 - aws.amazon.com
- Programming Model for Authoring Lambda Functions in Go - AWS Lambda
- aws/aws-lambda-go: Libraries, samples and tools to help Go developers develop AWS Lambda functions.
- AWS Lambda Go早めぐり(LambdaContext, Logging, Error…) · My External Storage
- aws/aws-sdk-go: AWS SDK for the Go programming language.
- Chart.js | Open source HTML5 Charts for your website
- Chart.js samples
- Chart – Color Palette – Values and Names | SAP Fiori Design Guidelines