Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Line Bot on Cloud Run: 使用 line/line-bot-sdk-go

Line Bot on Cloud Run: 使用 line/line-bot-sdk-go

- 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

Cheng-Lung Sung

June 28, 2019
Tweet

More Decks by Cheng-Lung Sung

Other Decks in Programming

Transcript

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

    View Slide

  2. 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

    View Slide

  3. Docker for binary on Android

    View Slide

  4. Go 1.3 vendor

    View Slide

  5. Go 1.3 vendor

    View Slide

  6. line/line-bot-sdk-go

    View Slide

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

    View Slide

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

    View Slide

  9. Test

    @jak4370y
    GCP Test

    @309mxwju

    View Slide

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

    View Slide

  11. // 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
    }

    View Slide

  12. // 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
    }

    View Slide

  13. 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))
    }

    View Slide

  14. // 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
    }

    View Slide

  15. // 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
    }

    View Slide

  16. 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))

    View Slide

  17. Develop/test with ngrok

    View Slide

  18. Develop with docker-compose

    View Slide

  19. 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

    View Slide

  20. 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"]

    View Slide

  21. Deploy to
    Google Cloud Runbeta

    View Slide

  22. –Google Cloud Run
    “Bringing serverless to containers”

    View Slide

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

    View Slide

  24. 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.

    View Slide

  25. View Slide

  26. #!/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

    View Slide

  27. –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”

    View Slide

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

    View Slide

  29. Stackdriver Logging

    View Slide

  30. 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))
    }

    View Slide

  31. // 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,
    }
    }
    }

    View Slide

  32. 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)
    }

    View Slide

  33. Stackdriver Logging

    View Slide

  34. Stackdriver Trace

    View Slide

  35. Stackdriver Monitoring
    因為網域是 app.google.stackdriver.com 所以不想講

    時間不夠,下⼀一場再講

    View Slide

  36. 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)

    View Slide

  37. 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
    }

    View Slide

  38. Enhanced version

    View Slide

  39. Thank you!

    View Slide