備忘録

なんとなく暇なときにでも....

APIGatewayとLambdaでSlackコマンドを作った話

タイトルの通りです。 ちょっとやってみました。

構成

f:id:tomAn:20170426091807p:plain

やったこと

  1. Lambda functionを作成
  2. API GatewayとLambdaと連携
  3. Slack(outgoing webhook, incomming webhook)の設定
  4. プルリクエストを出してみる

1. Lambda functionを作成

API GatewayからキックされたLambdaの役割は大きく2つです。

  • GithubAPIを利用して、指定のリポジトリのマスターマージのプルリクエストを送る。
  • SlackAPIを利用して、必要なレスポンスをする。

Lambda functionがサポートしている言語は、Node.jsやPythonなど複数存在しますが、今回はGo言語を用いて実装しています。

というのも、

などがあったため、あえてGoで実装しました。

前置きが長くなってしまったのですが、そろそろ初めます。

追加機能(プルリクエスト・Slackへのレスポンス)を実装し、ApexでLambda functionをdeployします。

実装

GithubAPIを利用して、指定のリポジトリのマスタマージのプルリクエストを送る。

先ほど紹介しましたgo-githubgo getして使っていきます。

SlackのAPIを利用して、チャンネルに必要なレスポンスを返す。

レスポンスを返すために、incomming webhookを使いました。

incoming webhookを用いて、slackにポストする外部パッケージは個人で練習用のものを作っていたので、そっちからgo getしてます。

はじめて自分で作ったものを外部パッケージとして利用したのですが、こういうことを簡単にできるのもGo言語のよいところだなと思いました。(他の言語と比較できませんが…)

実装したLambda functionをapex deployすればひとまずOK。

f:id:tomAn:20170426091829p:plain

2. API GatewayとLambdaと連携

ここで注意する点はoutgoingとlambdaの入手出力の形式が異なる点です。

  • outgoingの出力は、 application/x-www-form-urlencoded
  • lambdaの入力は、application/json

なのだそう。

ですので、API GatewayでContent-typeをそれらに合わせて変換しないといけません。 ( AWS Developer Forums: HOWTO: Mapping Template )

f:id:tomAn:20170426091906p:plain

API Gatewayの設定画面で、本文マーピングテンプレートのContent-typeapplication/x-www-form-urlencodedを指定し、テンプレートの生成します。

f:id:tomAn:20170426091918p:plain

3. Slackの設定 (Webhookの設定)

outgoing webhookだけでもレスポンスを返すことは可能なのですが、SlackのAPIを利用すれば、レスポンスする際の詳細な設定(レスポンス時のアイコンや名前など)が簡単にできるincoming webhookを使いました。

API Gatewayで作成したURLをoutgoingで実行するURLに設定すれば準備完了です。


4. プルリクエストを出してみる

実際にプルリクエストを出してみます。 outgoing webhookで設定したchannelでtrigger wordを入力すると…

f:id:tomAn:20170426091930p:plain

こんな感じです。

(モザイクがかかってますが)プルリクエストのURLやコミットタイトル、コミッターなどを表示しているので、誰がどのようなコミットをしているのかを一覧でさくっと見れてちょっといい気分になれます。


今のところChatOps的に社内の人に使ってもらえるレベルのものにはなっていませんが、 今回紹介した方法を利用すれば、Slackのintegrationにない機能でも実装できるかと思います。

たしかoutgoingには上限の個数があったはずなので、Lambda側でキーワードに応じてスイッチすれば、1つのoutgoingで、複数機能の実装もできるようになるのではないかと。(これはけっこういいなと勝手に思ってます。)

今後も時間を見つけてよりよいChatOps環境を作っていければなと思っている次第です。

Microservices Meetupに行ってきた話。

会社で最近話題のマイクロサービス化しようという流れがありまして、先週FiNCで開催されたMicroservices Meetupに行ってきました。

microservices-meetup.connpass.com
今回はAPI Gateway と BFFの話がメインテーマでした。
ざっくりまとめておきます。

APIのことはすべてシーマンが教えてくれた。

  • 何言ってるかよくわかんなかった…笑
  • (テーマとは関係ないですが)oracleAPI Gateway製品を出すらしい。

step by step BFF

  • BFFのユースケースについて話。(以下ユースケース)
    • APIコールをBFFでまとめてフロントに最適化した形で返却する
    • セッション管理
    • サーバーサイドレンダリング
    • ファイルアップロード
    • リアルタイム処理
  • BFFはクライアントとサーバーサイドのロジックのバランスを保つことで、クライアント・サーバーがそれぞれの役割に集中できる。

    • ドメインに特化したサービス(サーバサイド)とリッチ(フロントエンド)の調停役を担う。
  • BFFをNode.jsにすることで、

    • サーバサイドレンダリング
    • フロントの作業を共通化できる などのメリットも。
  • Netflixはバケモノ

    • 対応のデバイスが異常に多いのでBFFでそれぞれに最適化させた話
  • BFFを導入するときは

    • フロント、バックを分業したいとき
    • レガシーなシステムを少しずつリアーキテクトしたいとき
  • BFFを導入しないときは

    • フルスタックエンジニアが多い
    • (モノリシックにして)開発速度を優先したいとき
    • SEOいらないとき
      • 表示速度などにこだわる必要がなく、サーバサイドレンダリングなどが不要な時
  • step by step

    1. モノリシックな構成 f:id:tomAn:20170401172855p:plain
    2. モノリシックサービスからBFFを切り出す。 f:id:tomAn:20170401172852p:plain
    3. モノリシックサービスから少しづつサービスを切り出す。 f:id:tomAn:20170401172849p:plain

Webサービスの初期開発とマイクロサービス・BFF

  • できるだけAPIを単純化し、細かい粒度で作ることによって、設計・実装・テストが簡単になり、バグもほとんど起きなかった。

  • BFFの実装は難しくないけど、複雑で面倒なことが多いため、チームにはなかなか受け入れてもらうのに時間がかかった。

  • 個人的にはちょうどgolangをechoフレームワークで勉強中でした。。

    • BFFのサンプルコードをgithubにあげてくれててとても喜んでいます。 github.com

Railsで作るBFFの功罪

engineer.recruit-lifestyle.co.jp

  • 2番めと3番目のスライドではNode.js, golangでBFFを実装していて、個人的にもどっちかなんだろうなと思ってたところで、まさかのRailsでBFFを構築した話。

  • Node.js, golangと比べると、並列処理が苦手なので、パフォーマンスなどに問題が出る可能性があったそうなんですが、開発速度 > パフォーマンスという優先度として、Railsの使用を決めた。

  • 完全なマイクロサービス化は諦めて、BFFとモノリシックサービスを分離させることだけの集中した。

  • パフォーマンスの低下を少しでも防ぐには、BFFを極力薄く保つことが一番効いた。

パネルディスカッション

  • BFF の開発を誰が担うか。

    • フロントエンドの人が BFF の開発もする。WebSocket もやる。そこまでやらないといけないと思っている
  • API Gateway と BBF の違い。

    • BBF の機能の一つとして API Gateway があるという認識
  • bffはバックエンドのどの部分まで持つべきか?

  • ネイティブアプリだとリッチなマシンパワー使えるので、webと違ってBFF導入メリット薄い

感想

  • 今回の話だとBFFでのセッション・アグリゲータ・レンダリングの話が多かった。

  • BFF入れてAPIの並列化だけやってもパフォーマンス向上に繋がりそう。

  • 大規模になってくると、BFF自体がモノリシック化してしまいそうで怖い…

A Tour of Go を1週間かけてやりました。

最近golangの記事をちょちょく挙げさせていただいているように最近golangの勉強をしています。
それにも関わらず、実はかの有名な「A Tour of Go」をやっていないことに気づいたので、1週間ほど少しづつ進めて一通りなぞったので、完全に記録でしかありませんが、自分へのメモとして挙げておきます。

3.15

Defer (https://go-tour-jp.appspot.com/flowcontrol/12)

  • defer ステートメントは、 defer へ渡した関数の実行を、呼び出し元の関数の終わり(returnする)まで遅延させるものです。
  • defer へ渡した関数の引数は、すぐに評価されますが、その関数自体は呼び出し元の関数がreturnするまで実行されません。→ この呼び出しは関数スコープを離れる時(クエリ関数から戻る時)に実行されます。
  • defer へ渡した関数が複数ある場合、その呼び出しはスタック( stack )されます。 呼び出し元の関数がreturnするとき、 defer へ渡した関数は LIFO(last-in-first-out) の順番で実行されます。

3.16

Pointers to structs (https://go-tour-jp.appspot.com/moretypes/4)

  • structのフィールドは、structのポインタを通してアクセスすることできます。
  • スライスは配列への参照のようなものです (https://go-tour-jp.appspot.com/moretypes/8)

    スライスはどんなデータも格納しておらず、単に元の配列の部分列を指し示しています。 スライスの要素を変更すると、その元となる配列の対応する要素が変更されます。 同じ元となる配列を共有している他のスライスは、それらの変更が反映されます。

Slice literals (https://go-tour-jp.appspot.com/moretypes/9)

s := []struct {
    i int
    b bool
}{
    {2, true},
    {3, false},
    {5, true},
    {7, true},
    {11, false},
    {13, true},
}
fmt.Println(s)
//[{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]

3.17

Function values (https://go-tour-jp.appspot.com/moretypes/24)

関数も変数です。他の変数のように関数を渡すことができます。 関数値( function value )は、関数の引数に取ることもできますし、戻り値としても利用できます。

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12)) //13

    fmt.Println(compute(hypot)) //5
    fmt.Println(compute(math.Pow)) // 81
}

3.18

Methods (https://go-tour-jp.appspot.com/methods/1)

Goには、クラス( class )のしくみはありませんが、型にメソッド( method )を定義できます。 メソッドは、特別なレシーバ( receiver )引数を関数に取ります。 レシーバは、 func キーワードとメソッド名の間に自身の引数リストで表現します。 この例では、 Abs メソッドは v という名前の Vertex 型のレシーバを持つことを意味しています。

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 { // レシーバを(v Vertex)として設定できる
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
}

Pointer receivers (https://go-tour-jp.appspot.com/methods/4)

ポインタレシーバでメソッドを宣言できます。 ポインタレシーバを持つメソッド(ここでは Scale )は、レシーバが指す変数を変更できます。 レシーバ自身を更新することが多いため、変数レシーバよりもポインタレシーバの方が一般的です。

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}
  • メソッドがポインタレシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができます: go var v Vertex v.Scale(5) // OK p := &v p.Scale(10) // OK

    v.Scale(5) のステートメントでは、 v は変数であり、ポインタではありません。 メソッドでポインタレシーバが自動的に呼びだされます。 Scale メソッドはポインタレシーバを持つ場合、Goは利便性のため、 v.Scale(5) のステートメントを (&v).Scale(5) として解釈します。

  • メソッドが変数レシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができます: go var v Vertex fmt.Println(v.Abs()) // OK p := &v fmt.Println(p.Abs()) // OK

    この場合、 p.Abs() は (*p).Abs() として解釈されます。

  • ここまで!


3.19

Choosing a value or pointer receiver (https://go-tour-jp.appspot.com/methods/8)

  1. メソッドがレシーバが指す先の変数を変更するためです。
  2. メソッドの呼び出し毎に変数のコピーを避けるためです。 例えば、レシーバが大きな構造体である場合に効率的です。

一般的には、変数レシーバ、または、ポインタレシーバのどちらかですべてのメソッドを与え、混在させるべきではありません。 (この理由は数ページ後にわかります)


3.20

The empty interface (https://tour.golang.org/methods/14)

ゼロ個のメソッドを指定されたインターフェース型は、 空のインターフェース と呼ばれます: interface{} 空のインターフェースは、任意の型の値を保持できます。 (全ての型は、少なくともゼロ個のメソッドを実装しています。) 空のインターフェースは、未知の型の値を扱うコードで使用されます。

func main() {
    var i interface{}
    describe(i)
    // (<nil>, <nil>)

    i = 42
    describe(i)
    //(42, int)

    i = "hello"
    describe(i)
    //(hello, string)
}

Type assertions (https://go-tour-jp.appspot.com/methods/15)

アサーション は、インターフェースの値の基になる具体的な値を利用する手段を提供します。 t := i.(T)

ここまで!


3.21

ここまで!


3.22

ここまで!


3.24,25

Channels (https://go-tour-jp.appspot.com/concurrency/2)

チャネル( Channel )型は、チャネルオペレータの <- を用いて値の送受信ができる通り道です。

ch <- v    // v をチャネル ch へ送信する
v := <-ch  // ch から受信した変数を v へ割り当てる
           // (データは、矢印の方向に流れます)

マップとスライスのように、チャネルは使う前に以下のように生成します:

ch := make(chan int)

Buffered Channels (https://go-tour-jp.appspot.com/concurrency/3)

チャネルは、 バッファ ( buffer )として使えます。 バッファを持つチャネルを初期化するには、 make の2つ目の引数にバッファの長さを与えます ch := make(chan int, 100)

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    ch <- 3
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
/**
* fatal error: all goroutines are asleep - deadlock!
* 
* goroutine 1 [chan send]:
* main.main()
* /tmp/sandbox573738310/main.go:9 +0x100
**/

Select (https://go-tour-jp.appspot.com/concurrency/5)

select ステートメントは、goroutineを複数の通信操作で待たせます。

select は、複数ある case のいずれかが準備できるようになるまでブロックし、準備ができた case を実行します。 もし、複数の case の準備ができている場合、 case はランダムに選択されます。

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}
/**
0
1
1
2
3
5
8
13
21
34
quit
**/


本当にメモです….。


本日はここまで

" UNIXという考え方―その設計思想と哲学 "を読んで

名著とされている"unixという考え方を読んで"を1週間ほどで読みました。


以下紹介されていた9つの定理

  1. スモール・イズ・ビューティフル
  2. 1つのプログラムには1つのことをうまくやらせる
  3. できるだけ早く試作する
  4. 効率より移植性を優先する
  5. 数値データはASCIIフラットファイルに保存する
  6. ソフトウェアを梃子(てこ)として使う
  7. シェルスクリプトによって梃子(てこ)の効果と移植性を高める
  8. 過渡の対話的インターフェースを避ける
  9. すべてのプログラムをフィルタとして設計する


読んでしまえば、よく見聞きする思想で新鮮味があったというわけではないですが、
おそらく僕が今まで見てきたソース達もこれらのunix的な思想の影響を受けているのだなと思わされました。
(最近流行っているマイクロサービスもがっつり影響をうけてますよね。)

私のお気に入りは定理2の"1つのプログラムには1つのことをうまくやらせる"の章ですね。
プロダクトを3つのフェーズにわけていて、フェーズ3に向かうまでのプロダクトの障害の話が非常に気に入っています。


本日はここまで

golangのechoフレームワークを使ってRestfulAPIを作る ~GETクエリを取得する~

本日は"GETクエリを取得する"です。(なぜかまだやってみてなかったので。)
前回まではこちら

hchckeeer.hatenadiary.jp


以下のリクエストを送信します。

curl -v GET -H "Content-Type: application/json" "http://localhost:3000/users?id=3&name=hogehoge&age=25"


実際のソースはこんな感じに…

import (
    "github.com/labstack/echo"

    "strconv"
)

type (
    User struct {
                Id         int64   `db:"id"`
                Name   string  `db:"name"`
                Age      int    `db:"age"`
    }
)


func main() {
    e := echo.New()
  
    /**
    * Routing
    */
    e.GET("/users", GetUser)
 
    // Start server
    e.Logger.Fatal(e.Start(":3000"))
}


func GetUser(c echo.Context) error {

    // string -> int
    /**
     *  Package strconv
     *  see https://golang.org/pkg/strconv/
     */
    id,_ := strconv.Atoi(c.QueryParam("id"))
    var name string = c.QueryParam("name")
    age,_ := strconv.Atoi(c.QueryParam("age"))

    var user *User = newUser(id, name, age)

    return c.JSON(http.StatusCreated, user)
}

//構造体の初期化
func newUser(id int, name string, age int) *User{
    user := new(User)
    user.ID = id
    user.Name = name
    user.Age = age

    return user
}

QueryParam("id")でGETクエリ内のidで指定された値を取得しています。
この時点ではstring型ですので、golangの標準パッケージのstrconvを利用してstring -> intにキャストしています。

strconv - The Go Programming Language

string -> intのキャストにはstrconv.Atoi(string) intを用いればよさそう。


本日は以上です。

github.com


これ読みたい….

golangのechoフレームワークを使ってRestfulAPIを作る ~POSTデータをテーブルにINSERTする。~

最近は毎日会社が帰ってきてからgolangの勉強するかPS4バイオハザード7するかで毎日悩まされています。

前回の続きです。

前回↓
hchckeeer.hatenadiary.jp
本日はgolangのO/R Mapperのdbrを利用して"テーブルにPOSTしたデータをINSERT"します。

送信するリクエストは以下

curl -v POST -H "Content-Type: application/json" "http://localhost:3000/users" -d '{"id" : 1, "name" : "Taro Yamada", "age":20}'


import (
    "github.com/labstack/echo"
    _ "github.com/go-sql-driver/mysql"
    "github.com/gocraft/dbr"
)

type (
    Users struct {
                Id         int64   `db:"id"`
                Name   string  `db:"name"`
                Age      int    `db:"age"`
    }
)


func main() {
    e := echo.New()
  
    /**
    * Routing
    */
    e.POST("/users", PostUser)
 
    // Start server
    e.Logger.Fatal(e.Start(":3000"))
}


func PostUser(c echo.Context) error {
    conn, err := dbr.Open("mysql", "username:passwd@tcp(127.0.0.1:3306)/test_db", nil)
    if err != nil{
        return err
    }
    sess := conn.NewSession(nil)

    user := new(models.User)
    if err := c.Bind(user); err != nil {
        return err
    }
    c.JSON(http.StatusCreated, user)

    result, err := sess.InsertInto("users").
                    Columns("id", "name", "age").
                    Record(user).Exec()

    if err != nil {
        return err
    } else {
        return c.JSON(http.StatusCreated, result)
    }
}


struct を初期化しているデータを INSERT する場合は、Record() を用います。

その他方法としてはRecord()ではなく、Values()を用いる方法もあります。

// こんな感じ
result, err := sess.InsertInto("users").
                    Columns("id", "name", "agee").
                    Values(1, "Yamada", 20).
                    Exec()


本日は以上です。

github.com

golangのechoフレームワークを使ってRestfulAPIを作る ~DBへの接続。SELECTする。~

本日はサブタイトル通り”MySQLに接続して、テーブルからSELECTする。”です。

私は開発環境にDockerを使用しているのですが、初めて”別のコンテナを立てて、そのコンテナに現在動いているコンテナからアクセスする”といったことをやりました。
案外サクッとできてDockerのメリットを初めて個人の開発で実感したかもしれません。
これに関しては本日のテーマとは逸れるので別の機会に。


golangのORマッパーとしては、gormやgorpなどもありますが、dbrを使用しました。(dbrについては下記のURLを参考URLを載せておきます。)

使い方は以下の通り(例にもよって1ファイルにまとめていますが、実際のソースでは複数ファイルに分けています。)

import (
    _ "github.com/go-sql-driver/mysql"     //←これ!
    "github.com/gocraft/dbr"           //←これ!
)

type (
    Users struct {
                Id         int64   `db:"id"`
                Name   string  `db:"name"`
                Age      int    `db:"age"`
    }
)


func main() {
    e := echo.New()
 
    /**
    * Middleware
    */
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
 
    /**
    * Routing
    */
    e.GET("/users", ConnectDB)
 
    // Start server
    e.Logger.Fatal(e.Start(":3000"))
}


func ConnectDB(c echo.Context ) error{
    // mysqlへの接続設定
    // dbr.Open("mysql", "[mysql_username]:[mysql_passwd]@tcp( [接続先のmysqlコンテナのホストネーム] :3306)/[接続DB名]", nil)
    conn, err := dbr.Open("mysql", "username:passwd@tcp(127.0.0.1:3306)/test_db", nil)
    if err != nil{
        return err
    }

   //sess を利用して DB を操作する
    sess := conn.NewSession(nil)

    var user []Users
   // [{"Id": ,"Name":, "Age":}]


    // select * from users で取得した結果をuser構造体にバインドする。
    sess.Select("*").From("users").Load(&user)
 
    // [{"Id":1,"Name":"hogehoge","Age":24}]
    return c.JSON(http.StatusCreated, user)
}

usersテーブルにはid, name, ageカラムがそれぞれint, varchar, intで設定していました。

-- users table
CREATE TABLE `users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(16) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;

対応したカラム名をUsers構造体で定義することでクエリを実行した結果を、初期化された構造体にバインドしてくれてます。

いやー。少しづつですができてきましたね。(まだ序盤中の序盤ですが…….)

本日はここまで