Slide 1

Slide 1 text

Line Bot on GKE Cloud Runbeta: 使⽤用 line/line-bot-sdk-go Cheng-Lung Sung (clsung@)

Slide 2

Slide 2 text

Outline • My five years of go • How-To: line/line-bot-sdk-go • Simple echo & face recognition chatbot • Develop/test with ngrok • Develop with docker-compose • Deploy to Google Cloud Runbeta • DevOps 三寶: Logging/Tracing/Metrics

Slide 3

Slide 3 text

Docker for binary on Android

Slide 4

Slide 4 text

Go 1.3 vendor

Slide 5

Slide 5 text

Go 1.3 vendor

Slide 6

Slide 6 text

line/line-bot-sdk-go

Slide 7

Slide 7 text

⾺馬偕醫院⾸首推LINE聊天機器⼈人技術

Slide 8

Slide 8 text

萬⼩小芳: 全台⾸首位⼈人⼯工智慧醫療服務聊天機器⼈人

Slide 9

Slide 9 text

Test
 @jak4370y GCP Test
 @309mxwju

Slide 10

Slide 10 text

Project Overview 這是⼀一個回應你講的話,以及跑雲端演 算法 (AWS, GCP) 偵測相片有多少⼈人臉 的 LINE Chat Bot • https://aws.amazon.com/tw/ rekognition/ • https://cloud.google.com/vision/

Slide 11

Slide 11 text

// FaceDetect detects face and return json data func FaceDetect(reader io.Reader) (ret []byte, err error) { sess := session.New(&aws.Config{ Region: aws.String("us-west-2"), }) svc := rekognition.New(sess) // Read buf to byte[] b, err := ioutil.ReadAll(reader) if err != nil { return b, err } // Send the request to Rekognition input := &rekognition.DetectFacesInput{ Image: &rekognition.Image{ Bytes: b, }, Attributes: []*string{aws.String("ALL")}, } result, err := svc.DetectFaces(input) if err != nil { log.Print(err) return ret, err } return []byte(fmt.Sprintf("AWS Faces: %d", len(result.FaceDetails))), nil }

Slide 12

Slide 12 text

// FaceDetect detects face and return json data func FaceDetect(reader io.Reader) (ret []byte, err error) { ctx := context.Background() // client, err := vision.NewImageAnnotatorClient(ctx, option.WithCredentialsFile(jsonPath)) client, err := vision.NewImageAnnotatorClient(ctx) if err != nil { log.Printf("Failed to create client: %v", err) return ret, err } defer client.Close() image, err := vision.NewImageFromReader(reader) if err != nil { log.Printf("Failed to create image: %v", err) return ret, err } faces, err := client.DetectFaces(ctx, image, nil, 10) if err != nil { log.Printf("Failed to detect labels: %v", err) return ret, err } return []byte(fmt.Sprintf("GCP Faces: %d", len(faces))), nil }

Slide 13

Slide 13 text

func main() { app := cvbot.NewCVApp() // Setup HTTP Server for receiving requests from LINE platform http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) { events, err := app.ParseRequest(req.Context(), req) if err != nil { if err == linebot.ErrInvalidSignature { w.WriteHeader(400) } else { w.WriteHeader(500) } return } v, err := app.Webhook(req.Context(), events) log.Printf("events: %d, err: %v", v, err) }) http.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) { _, err := w.Write([]byte(`{"health": "OK"}`)) if err != nil { log.Fatal(err) } }) port := os.Getenv("PORT") if port == "" { port = "8080" } log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) }

Slide 14

Slide 14 text

// CVApp app type CVApp struct { bot *linebot.Client } // NewCVApp returns a service func NewCVApp() Service { bot, err := linebot.New( os.Getenv("LINE_CHANNEL_SECRET"), os.Getenv("LINE_CHANNEL_TOKEN"), ) if err != nil { log.Fatal(err) } return &CVApp{ bot: bot, } } // ParseRequest parse http.Request into events func (app *CVApp) ParseRequest(ctx context.Context, r *http.Request) return app.bot.ParseRequest(r) } // Webhook handles parsed events func (app *CVApp) Webhook(ctx context.Context, r interface{}) (int, e events := r.([]*linebot.Event) for _, event := range events { replyMsgs, err := app.HandleEvent(ctx, event) if err != nil { return 0, errors.Wrap(err, "handle event failed") } if _, err := app.bot.ReplyMessage( event.ReplyToken, replyMsgs..., ).Do(); err != nil { return 0, errors.Wrap(err, "reply message failed") } } return len(events), nil }

Slide 15

Slide 15 text

// HandleEvent implements event handler with provider-based logic func (app *CVApp) HandleEvent(ctx context.Context, event *linebot.Event) (replyMsgs []linebot.SendingMessage, err error) { if event.Type == linebot.EventTypeMessage { switch message := event.Message.(type) { case *linebot.TextMessage: replyMsgs = append(replyMsgs, linebot.NewTextMessage(message.Text)) case *linebot.ImageMessage: var retMsgs []linebot.SendingMessage if retMsgs, err = app.handleImage(ctx, message); err != nil { return replyMsgs, errors.Wrap(err, "handle image failed") } replyMsgs = append(replyMsgs, retMsgs...) case *linebot.StickerMessage: return replyMsgs, errors.New("STICKER ERROR") default: replyMsgs = append(replyMsgs, linebot.NewTextMessage("not supported yet")) } } return replyMsgs, nil }

Slide 16

Slide 16 text

func (app *CVApp) handleImage(ctx context.Context, message *linebot.ImageMessage) (replyMsgs []linebot.SendingMessage, err error) { _, err = app.handleFaceRecognition(ctx, message.ID, func(gcpBuf io.Reader, awsBuf io.Reader) error { awsFaces, err := aws.FaceDetect(awsBuf) if err != nil { return errors.Wrap(err, "aws face detect error") } replyMsgs = append(replyMsgs, linebot.NewTextMessage(string(awsFaces))) gcpFaces, err := gcp.FaceDetect(gcpBuf) if err != nil { return errors.Wrap(err, "gcp face detect error") } replyMsgs = append(replyMsgs, linebot.NewTextMessage(string(gcpFaces))) return nil }) return replyMsgs, err } func (app *CVApp) handleFaceRecognition(ctx context.Context, messageID string, callback func(io.Reader, io.Reader) error) ([]linebot.SendingMessage, error) { content, err := app.bot.GetMessageContent(messageID).Do() if err != nil { return nil, errors.Wrap(err, "get image from line error") } defer content.Content.Close() var gcpBuf, awsBuf bytes.Buffer w := io.MultiWriter(&gcpBuf, &awsBuf) if _, err := io.Copy(w, content.Content); err != nil { return nil, errors.Wrap(err, "multiwriter copy error") } return nil, callback(io.Reader(&gcpBuf), io.Reader(&awsBuf))

Slide 17

Slide 17 text

Develop/test with ngrok

Slide 18

Slide 18 text

Develop with docker-compose

Slide 19

Slide 19 text

version: '3' services: chatbot: build: context: ../ dockerfile: deploy/Dockerfile command: simple #env_file: # - .env ports: - "8080:8080" environment: - PORT=8080 - LINE_CHANNEL_SECRET=${LINE_CHANNEL_SECRET} - LINE_CHANNEL_TOKEN=${LINE_CHANNEL_TOKEN} - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - GOOGLE_APPLICATION_CREDENTIALS=/google-cred.json volumes: - "${GOOGLE_APPLICATION_CREDENTIALS}:/google-cred.json:ro" docker-compose.yml

Slide 20

Slide 20 text

FROM golang:1.12-alpine as builder RUN apk update && apk upgrade && \ apk add --no-cache bash \ bind-tools \ git # Install openfortivpn RUN apk --update upgrade \ && apk add ca-certificates wget \ && update-ca-certificates ENV GO111MODULE off RUN go get -u "github.com/aws/aws-sdk-go/aws" \ "github.com/aws/aws-sdk-go/aws/session" \ "github.com/aws/aws-sdk-go/service/rekognition" \ "cloud.google.com/go/vision/apiv1" \ "github.com/sirupsen/logrus" \ "go.opencensus.io/trace" \ "contrib.go.opencensus.io/exporter/stackdriver" \ "github.com/clsung/logger" \ "github.com/pkg/errors" \ "github.com/line/line-bot-sdk-go/linebot" COPY . /go/src/github.com/clsung/cvbot/ WORKDIR /go/src/github.com/clsung/cvbot/cmd/tracesim/ RUN go build -o /go/bin/cvbot FROM alpine:3.9 RUN apk --no-cache add ca-certificates openssl ppp curl su-exec bash && rm -rf /var/cache/apk/*; COPY --from=builder /go/bin/cvbot /usr/bin/cvbot CMD ["/usr/bin/cvbot"]

Slide 21

Slide 21 text

Deploy to Google Cloud Runbeta

Slide 22

Slide 22 text

–Google Cloud Run “Bringing serverless to containers”

Slide 23

Slide 23 text

https://twitter.com/ahmetb/status/1116041166359654400/

Slide 24

Slide 24 text

Container runtime contract • The container is compiled for 64-bit Linux. • The container listens for HTTP requests on the port defined by the PORT environment variable, which is always set to 8080. • Can fit in up to 2 GB of memory (initially 256MB). • Container instances must start an HTTP server within 4 minutes after receiving a request. • Default timeout: 5mins • Revision should work as containers are auto-scaled from 0 to multiple running instances. • When a revision does not receive any traffic, it is scaled down to zero instances • All computation is stateless and scoped to a request.

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

#!/usr/bin/env bash # gcloud config set project {PROJECTID} DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" HOSTNAME=asia.gcr.io PROJECTID=${LINE_PROJECT} IMAGE=cvbot TAG=`git rev-parse --short=7 HEAD` cd "${DIR}/.." docker build -t ${HOSTNAME}/${PROJECTID}/${IMAGE}:${TAG} -f deploy/Dockerfile . if [[ ${?} != 0 ]]; then # build error exit $? fi docker push ${HOSTNAME}/${PROJECTID}/${IMAGE}:${TAG} gcloud beta run deploy ${IMAGE} --project ${PROJECTID} --image ${HOSTNAME}/${PROJECTID}/${IMAGE}:${TAG} --region us-central1 -- platform managed

Slide 27

Slide 27 text

–Gene Kim, The Phoenix Project: A Novel About IT, DevOps, and Helping Your Business Win “Any improvements made anywhere besides the bottleneck are an illusion”

Slide 28

Slide 28 text

–有⽤用的 DevOps 「DevOps 三寶:Logging、Tracing、 Metrics」

Slide 29

Slide 29 text

Stackdriver Logging

Slide 30

Slide 30 text

func main() { app := cvbot.LoggingMiddleware(logger.New())(cvbot.NewCVApp()) // Setup HTTP Server for receiving requests from LINE platform http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) { events, err := app.ParseRequest(req.Context(), req) log.Printf("events: %d, err: %v", events, err) if err != nil { if err == linebot.ErrInvalidSignature { w.WriteHeader(400) } else { w.WriteHeader(500) } return } app.Webhook(req.Context(), events) }) http.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) { _, err := w.Write([]byte(`{"health": "OK"}`)) if err != nil { log.Fatal(err) } }) port := os.Getenv("PORT") if port == "" { port = "8080" } log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) }

Slide 31

Slide 31 text

// Service describes a service that adds things together. type Service interface { Webhook(context.Context, interface{}) (int, error) ParseRequest(context.Context, *http.Request) (interface{}, error) } // Middleware describes a service (as opposed to endpoint) middleware. type Middleware func(Service) Service // LoggingMiddleware returns a service middleware that logs the // parameters and result of each method invocation. func LoggingMiddleware(logger Logger) Middleware { return func(next Service) Service { return loggingMiddleware{ logger: logger, next: next, } } }

Slide 32

Slide 32 text

func (mw loggingMiddleware) ParseRequest(ctx context.Context, r *http.Request) (v interface{}, err error) { defer func(begin time.Time) { mw.logger.Printf("events: %v, time: %v, error: %v", v, time.Since(begin), err) }(time.Now()) return mw.next.ParseRequest(ctx, r) } func (mw loggingMiddleware) Webhook(ctx context.Context, r interface{}) (v int, err error) { defer func(begin time.Time) { if log, ok := mw.logger.(*teltech.Log); ok { if err == nil { log.With(log.Fields{"result": v, "error": err, "elapsed": time.Since(begin)}).Info("Processed") } else { log.With(log.Fields{“result": v, "error": err, "elapsed": time.Since(begin)}).Error("Failed") } } else { mw.logger.Printf("result: %v, time: %v, error: %v", v, time.Since(begin), err) } }(time.Now()) return mw.next.Webhook(ctx, r) }

Slide 33

Slide 33 text

Stackdriver Logging

Slide 34

Slide 34 text

Stackdriver Trace

Slide 35

Slide 35 text

Stackdriver Monitoring 因為網域是 app.google.stackdriver.com 所以不想講
 時間不夠,下⼀一場再講

Slide 36

Slide 36 text

func main() { trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) sd, err := stackdriver.NewExporter(stackdriver.Options{ ProjectID: "alansandbox", // MetricPrefix helps uniquely identify your metrics. MetricPrefix: "tracesim", }) if err != nil { log.Fatalf("Failed to create the Stackdriver exporter: %v", err) } // It is imperative to invoke flush before your main function exits defer sd.Flush() // Register it as a trace exporter trace.RegisterExporter(sd) app := cvbot.LoggingMiddleware(logger.New())(cvbot.NewCVApp()) app = cvbot.TraceMiddleware()(app) // setup trace config, production please use trace.ProbabilitySampler // Setup HTTP Server for receiving requests from LINE platform http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) { ctx, span := trace.StartSpan(req.Context(), "webhook") defer span.End() events, err := app.ParseRequest(ctx, req)

Slide 37

Slide 37 text

func (app *CVApp) handleImageAsync(ctx context.Context, message *linebot.ImageMessage) (replyMsgs []linebot.SendingMessage, err error) { ctx, span := trace.StartSpan(ctx, "handle_image_async") defer span.End() _, err = app.handleFaceRecognition(ctx, message.ID, func(gcpBuf io.Reader, awsBuf io.Reader) error { dataCh := make(chan string, 1) go func() { _, span := trace.StartSpan(ctx, "aws") awsFaces, _ := aws.FaceDetect(awsBuf) span.End() dataCh <- string(awsFaces) return }() go func() { _, span := trace.StartSpan(ctx, "gcp") gcpFaces, _ := gcp.FaceDetect(gcpBuf) span.End() dataCh <- string(gcpFaces) return }() ret := <-dataCh replyMsgs = append(replyMsgs, linebot.NewTextMessage(string(ret))) return nil }) return replyMsgs, err }

Slide 38

Slide 38 text

Enhanced version

Slide 39

Slide 39 text

Thank you!