作成するlambdaの概要
SNSの通知をEventにSlackのIncoming Webhooksを利用してMessageをPOST
準備
コマンド
$ go version
go version go1.9.2 darwin/amd64
$ dep version
dep:
version : devel
build date :
git hash :
go version : go1.9.1
go compiler : gc
platform : darwin/amd64
$ aws --version
aws-cli/1.14.50 Python/2.7.14 Darwin/17.3.0 botocore/1.9.3
go, dep, awscliを利用します
AWS
- Cloudformation,SNS,Lamabda,S3,IAMの権限を付与されたIAM User
- Lambdaのcodeを格納するためのS3 Bucket
Slack
https://<your-team>.slack.com/apps/manage/custom-integrations
からIncoming WebHooksを設定しておきます. Webhook URLを取得できていればOKです、通知先のチャンネルはデフォルトなのでPOST時に上書きできます(Username, Iconemojiも同様)
作成
sampleのfetch
$ git clone git@github.com:ymgyt/awsctl.git
$ cd awsctl/sam/notify-sample
$ cp .envrc_template .envrc
# .envrcに値を追記
# source ./envrc でもよいです
$ direnv allow
$ dep ensure
sample https://github.com/ymgyt/awsctl/tree/master/sam/notify-sample
git clone後にsampleのdirに移動し、.envrc_template
を.envrc
にcopyします
以下の環境変数がSetされている状態なっていればいいのでenvrc
を利用しなくても特に問題ないです
その後、depで依存packageを取得します
利用する環境変数
-
AWS_DEFAULT_REGION
これから作成するリソースの作成先region -
FUNCTION_NAME
作成するLambdaの名前, 既存と重複があるとErrorになります -
S3_BUCKET
lambdaのcode格納先Bucket -
STACK
cloudformationのstack名 -
SNS_TOPIC
lambdaのevent sourceにするsnsのtopic名 -
WH_URL
Incoming WebhooksのURL -
WH_CHANNEL
通知先のchannel -
WH_USERNAME
messageの投稿者として利用 -
WH_ICONEMOJI
messageのIcon
Cloudromation/samのtemplate作成
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:
FunctionName:
Type: String
SNSTopic:
Type: String
WebHookURL:
Type: String
Channel:
Type: String
Username:
Type: String
IconEmoji:
Type: String
Resources:
Function:
Type: 'AWS::Serverless::Function'
Properties:
Runtime: 'go1.x'
Handler: 'main'
Tracing: 'PassThrough' # or Active
Environment:
Variables:
WEBHOOK_URL: !Ref WebHookURL
CHANNEL: !Ref Channel
USERNAME: !Ref Username
ICONEMOJI: !Ref IconEmoji
# main と指定すると fork/exec /var/task/main: permission denied: PathError
CodeUri: '.'
FunctionName: !Ref FunctionName
Description: 'post sns messages to slack incoming webhooks'
MemorySize: 128
Timeout: 5
# Role:
# if Role property is set, Policies property has no meaning
Policies:
- AWSLambdaBasicExecutionRole
Events:
SNS:
Type: SNS
Properties:
Topic: !Ref Topic
Tags:
Key: 'Value'
Topic:
Type: 'AWS::SNS::Topic'
Properties:
TopicName: !Ref SNSTopic
今回使用するtemplateは template.yml
です.
Transform: 'AWS::Serverless-2016-10-31'
を加えるとCloudformationからSAMになるという認識なのですが、用語の使い方が間違っているかもしれません.
Type
にAWS::Serverless::Function
を指定します, Runtime
は今のところgoを利用する場合, go1.x
しか選べないようです.Handlerにはmain
を指定します.
Tracing
はX-Ray関連の設定のようです(X-Rayについてわかっていない)
Enviroment
でLambdaに渡す環境変数を指定できます,現在のところEnviroment
で利用できるのはVariables
のみのようです.
CodeUri
にはカレントディレクトリを指定しています.ここでハマってしまう点があったので後述します.
FunctionName
は作成するLambdaの名前, MemorySize
,Timeout
はLambda起動時に割り当てるメモリ量、起動時間の制限です.
Role
, Policies
LambdaがassumeするIAMのPolicyを指定します, Role
とPolicies
はどちらかひとつを選択します.Policies
を利用すると、指定されたPolicyがattachされたroleが自動的に作成されます.
Events
でLambdaのTriggerを指定します. 今回はSNSを設定するのでSNSのTopicのARNを指定します.
Transform
を加えてもあくまでCloudformationであることはかわらないようで、今までできることはできるので、今回利用するSNSも同じtemplate内で作成しています.
lambdaのsourceを作成
package main
import (
"context"
"log"
"os"
"strings"
"time"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/ymgyt/slack/webhook"
)
var wh *webhook.Client
func init() {
var err error
wh, err = webhook.New(webhook.Config{
URL: os.Getenv("WEBHOOK_URL"),
Channel: os.Getenv("CHANNEL"),
Username: os.Getenv("USERNAME"),
IconEmoji: os.Getenv("ICONEMOJI"),
})
if err != nil {
panic(err)
}
}
// topic arn format
// arn:aws:sns:<region>:<account-id>:<topic-name>
func topicName(topicARN string) string {
name := strings.SplitN(topicARN, ":", 6)[5]
if name == "" {
return topicARN
}
return name
}
func handleRecord(record events.SNSEventRecord) error {
entity := record.SNS
payload := &webhook.Payload{
Text: "SNS Notification",
Attachments: []*webhook.Attachment{
{
Fallback: "SNS Notification",
Text: entity.Subject,
Pretext: topicName(entity.TopicArn),
Color: "good",
Fields: []*webhook.Field{
{
Title: "message",
Value: entity.Message,
},
{
Title: "timestamp",
Value: entity.Timestamp.Format(time.RFC3339),
},
},
},
},
}
return wh.SendPayload(payload)
}
func HandleEvent(ctx context.Context, event events.SNSEvent) error {
log.Println("start")
for _, record := range event.Records {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := handleRecord(record); err != nil {
return err
}
}
}
log.Println("end")
return nil
}
func main() {
lambda.Start(HandleEvent)
}
entry point
entry pointのmain
の中でgithub.com/aws/aws-lambda-go/lambdaより提供されているlambda.Start
を実行します.lambda.Start
に渡すのはfunctionで、そのfunctionのsignatureにはいくつか制限があります.
The handler must be a function.
The handler may take between 0 and 2 arguments. If there are two arguments, the first argument must implement context.Context.
The handler may return between 0 and 2 arguments. If there is a single return value, it must implement error. If there are two return values, the second value must implement error. For more information on implementing error-handling information, see Function Errors (Go) .
The following lists valid handler signatures. TIn and TOut represent types compatible with the encoding/json standard library. For more information, see func Unmarshal to learn how these types are deserialized.
func ()
func () error
func (TIn), error
func () (TOut, error)
func (context.Context) error
func (context.Context, TIn) error
func (context.Context) (TOut, error)
func (context.Context, TIn) (TOut, error)
ということで、contextを渡せ、レスポンス用のstructとerrorを返せるようです.
今回はfunc(context.Context,TIn) error
型のfuncを渡しています.
post
各種LambdaのEventSourceのstructはgithub.com/aws/aws-lambda-go/eventsで提供されています.今回はSNSを利用するので events.SNSEvent
を利用します.slack post用のstructはinit
の中で初期化しておきます.公式docにもそのような記載がありました。
Using Global State to Maximize Performance
To maximize performance, you should declare and modify global variables that are independent of your function's handler code. In addition, your handler may declare an init function that is executed when your handler is loaded. This behaves the same in AWS Lambda as it does in standard Go programs. A single instance of your Lambda function will never handle multiple events simultaneously. This means, for example, that you may safely change global state, assured that those changes will require a new Execution Context and will not introduce locking or unstable behavior from function invocations directed at the previous Execution Context. For more information, see Best Practices for Working with AWS Lambda Functions.
slackへのpostは自作のslack/webhook packageを利用しています.中ではjson.Marshal
してPOST requestを生成しているだけです.
すでに同じ用途のpackageはたくさんあると思います.
https://github.com/ashwanthkumar/slack-go-webhook 等
goのbuild
$ make build
GOOS=linux go build -o main main.go
linux用のbinaryをbuildします
lambdaのupload
$ make package
aws cloudformation package \
--template-file ./template.yml \
--s3-bucket <your-bucket> \
--output-template-file packaged-template.yml
aws cloudfromation package
コマンドでlambdaをs3にuploadします.コマンドが成功するとカレントにpackaged-template.yml
が出力されます.
packaged-template.yml
はさきほど作成したtemplate.yml
とほとんど同じですが以下の点が違います
Resources:
Function:
Properties:
CodeUri: s3://<bucket>/xxxxxxxxxxxxxxxxxxxxxxxxxx
Function
のCodeUri
をlocalのpathからupload後のS3のpathに書き換えてくれています.
cloudformation stack作成
$ make deploy
aws cloudformation deploy \
--template-file ./packaged-template.yml \
--stack-name <your-stackname> \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
"FunctionName=<your-function>" \
"SNSTopic="<your-topic>" \
"WebHookURL=<your-webhook-url>" \
"Channel=<your-channel>" \
"Username=<your-username>" \
"IconEmoji=<your-iconemoji>"
コマンドが成功すればStack,Lambda,SNS Topicが作成されています.
IAMのroleを生成するので --capabilities CAPABILITY_IAM
が必要です
通知test
$ make publish-to-topic
aws sns list-topics --query='Topics[].TopicArn' --output='text' | \
awk -v topic="${SNS_TOPIC}" 'BEGIN{RS="\t"} { \
sub("\n$",""); \
split($0,ary,":"); \
if (ary[6] == topic) {print $0} \
}' | \
xargs -I{} aws sns publish \
--topic-arn "{}" --message "test message"
aws sns publish
コマンドで作成したsnsに通知をだしています. jqに依存したくなかったのでawkを利用しています(awkは GNU Awkです)
成功すれば、Slackに通知がきます.
繰り返し
$ make
make
とだけ実行すると, build,package,deploy,publishまで走ります.
packageを実行するたびにS3にfileが作成されます.
はまったところ
lambda実行時にfork/exec /var/task/main: permission denied
CodeUri
で最初は./main
と指定していました。そうしたところ
lambda実行時にうまく起動できないerrorに遭遇しました。
https://github.com/awslabs/aws-sam-local/issues/274
https://github.com/aws/aws-cli/pull/3112
templateのvalidationができない
$ aws cloudformation validate-template --template-body=./template.yml
An error occurred (ValidationError) when calling the ValidateTemplate operation: Template format error: unsupported structure.
Transformしたtemplateはvalidate対象外のようです.
まとめ
Lambdaのruntimeにgoが追加されたので、goのlambdaを作成してみました.
Lambdaを運用すると様々なruntimeのlambdaが混在し、codeがconsole上で見えるもの/見えないものあったりしていかに管理していくかが自分の課題です.
Lambdaまわりのflowを管理するFlamework(serverless, apex等...)もあるのですが、他のAWSはcloudformationで管理してlambdaだけ別というのが気になるのでcloudformationとしてlambdaを管理できるsamを利用していこうかなと考えております.
参考
SAMのtemplate, package, deployについて
https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-handler-types.html
Goのlambdaについて
https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-handler-types.html