LoginSignup
15
12

More than 5 years have passed since last update.

cloudformation/samでsns通知をslackに流すgoのlambdaを作る

Last updated at Posted at 2018-03-04

作成するlambdaの概要

Blank Diagram - Page 1.png

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になるという認識なのですが、用語の使い方が間違っているかもしれません.
TypeAWS::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を指定します, RolePoliciesはどちらかひとつを選択します.Policiesを利用すると、指定されたPolicyがattachされたroleが自動的に作成されます.
EventsでLambdaのTriggerを指定します. 今回はSNSを設定するのでSNSのTopicのARNを指定します.
Transformを加えてもあくまでCloudformationであることはかわらないようで、今までできることはできるので、今回利用するSNSも同じtemplate内で作成しています.

lambdaのsourceを作成

main.go
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

FunctionCodeUriを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です)

Screen Shot 2018-03-04 at 12.57.16.png

成功すれば、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

15
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
12