AWS Advent Calendar 12/13 -Go言語でのAWS、特にDynamoDBの利用-

AWSアドベントカレンダーの12月13日として、私が興味あるGo言語からAWSをどうやって利用するかを述べようかなと思います。
ここ最近Goは暇な時間を見つけては好きに書いているだけなので、大して書けるわけではないです。そこんとこご了承ください。

Goって?

Googleが開発しているオープンソースプログラミング言語で、非常にシンプルなのにも関わらず必要十分な機能があります。
詳しくは http://golang.org/ まで。はい、おしまい。

シンプルなコードで簡単なWebアプリ書くならこんな感じになります。これはGo言語のチュートリアルかなんかに従って作ったWebアプリです。

	
package main
import (
        "fmt"
        "io/ioutil"
        "net/http"
        "html/template"
        "regexp"
        "errors"
        "log"
        "os"
)

/*
 * Struct prestenting wiki page
 */
type Page struct {
        Title string
        Body []byte
}

/*
 * Template caching for some templates which heavily used.
 */
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

/*
 * valid path regex
 *  - m1: edit, view, save
 *  - m2: [a-zA-Z0-9]+
 */
var validPath = regexp.MustCompile("^/(edit|view|save)/([a-zA-Z0-9]+)$")

/*
 * Simple logger from golang logging feature.
 */
var logger = log.New(os.Stdout, "", log.Ldate|log.Lmicroseconds|log.Lshortfile)

/*
 * Make http.HandlerFunc for editing, viewing, saving by title given by URL
 */
func MakeHandler(fn func(w http.ResponseWriter, r *http.Request, title string)) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                 m := validPath.FindStringSubmatch(r.URL.Path)
                 if m == nil {
                         http.NotFound(w, r)
                         return
                 } else {
                         fn(w, r, m[2])
                 }
        }
}

/*
 * View handler
 */
 func ViewHandler(w http.ResponseWriter, r *http.Request, title string) {
         p, err := LoadPage(title)
         if err != nil {
                 http.Redirect(w, r, "/edit/" + title, http.StatusFound)
                 return
         } else {
                RenderTemplate(w, "view", p)
        }
 }

/*
 * Edit Handler
 */
 func EditHandler_ugly(w http.ResponseWriter, r *http.Request, title string) {
         p, err := LoadPage(title)
         if err != nil {
                 p = &Page{Title:title}
         }
         fmt.Println("ugly", p.Body)
 }

/*
 * Edit Handler
 */
 func EditHandler(w http.ResponseWriter, r *http.Request, title string) {
         p, err := LoadPage(title)
         if err != nil {
                 p = &Page{Title:title}
         }
         RenderTemplate(w, "edit", p)
 }


/*
 * Save Handler
 */
func SaveHandler(w http.ResponseWriter, r *http.Request, title string) {
        body := r.FormValue("body")
        page := &Page{Title:title, Body:[]byte(body)}
        err := page.Save()
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
        } else {
                http.Redirect(w, r, "/view/" + title, http.StatusFound)
        }
}

/*
 * save page 
 */
func (p *Page) Save() error {
        filename := p.Title + ".txt"
        return ioutil.WriteFile(filename, p.Body, 0600)
}

/*
 * load page as Page struct
 */
func LoadPage(title string) (*Page, error) {
        filename := title + ".txt"
        body, error := ioutil.ReadFile(filename)
        if error != nil {
                return nil, error
        }
        return &Page{Title:title, Body:body}, nil
}

/*
 * Render using template loaded from name.
 */
func RenderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
        err := templates.ExecuteTemplate(w, tmpl + ".html", p)
         if err != nil {
                 http.Error(w, err.Error(), http.StatusInternalServerError)
         }
}

/*
 * bootstrap
 */
func main() {
        logger.Println("start web server...")
        logger.Println("register http.HandlerFunc.")
        http.HandleFunc("/view/", MakeHandler(ViewHandler))
        http.HandleFunc("/edit/", MakeHandler(EditHandler))
        http.HandleFunc("/save/", MakeHandler(SaveHandler))

        logger.Println("bootstrap web server...")
        http.ListenAndServe(":8080", nil)
}

GoからのAWS利用方法

現状残念ながらAWSからの正式SDKは出ていませんが、比較的アクティブに動いているプロジェクトは幾つかあります。
例えばGoamz(https://launchpad.net/goamz)とかですね。その中で特にDynamoDBを利用するには、今回紹介するGodynamoがなんかよさげにみえます。

Godynamoの前に

今回のre:InventでAWSのヘビーユーザであるSmugmugはMySQLからDynamoDBへ移行した話をしてくれました。

Smugmugは大きく分けて3つ今回新しいオープンソースを出しています。

  1. Godynamo : DynamoDBをGoから使うライブラリ
    1. https://github.com/smugmug/godynamo
  2. bbpd : DynamoDBをHTTPプロキシしてくれるライブラリというかサーバ?
    1. https://github.com/smugmug/bbpd
  3. goawsroles : GoからIAM Roleをシームレスに使うためのライブラリ。
    1. https://github.com/smugmug/goawsroles

時間が出来ればbbpdは試してみたいですねー!bbpdがあると

curl -X POST -d '{"TableName":"mytable","Key":{"Date":{"N":"20131001"},"UserID":{"N":"1"}}}' "http://localhost:12333/GetItem?indent=1&compact=1"

こんな感じでDynamoのデータを参照できます。便利!

Godynamo

GodynamoはAWSのヘビーユーザであるSmugmugが実際に使っているGo言語用のDynamoDBライブラリになります。
実際にMySQLからの移行から使われているので実績は既にあるものになります。

Godynamoを使うには3つのことをする必要があります。

  1. Goのインストール
  2. 必要なライブラリのインストール
    1. go get github.com/smugmug/godynamo
    2. go get github.com/smugmug/goawsroles
    3. go get github.com/bradclawsie/slog
  3. クレデンシャル情報の設定

クレデンシャル情報の設定はjsonファイルにて行います。
大きく分けて、アクセスキーとシークレットキーを使う方法と、IAM Roleを使う方法があります。
設定ファイル自体は~/.aws-config.jsonとしておきます。


設定ファイルはこんな感じ。

{
    "extends":[],
    "services": {
        "default_settings":{
            "params":{
                "access_key_id":"your_access_key",
                "secret_access_key":"your_secret_key",
                "use_sys_log":true
            }
        },
        "dynamo_db": {
            "host":"dynamodb.ap-northeast-1.amazonaws.com",
            "zone":"ap-northeast-1",
            "iam": {
                "use_iam":false,
                "role_provider":"file",
                "access_key":"role_access_key",
                "secret_key":"role_secret_key",
                "token":"role_token",
                "base_dir":"/dir/where/you/update/role_files",
                "watch":true
            }
        }
    }
}

本来はIAM Roleを使うべきですが、今回は割愛します。本番ではIAM Roleを使いましょう!

コードは非常にシンプルに作ることが出来ます。私が気に入ってるのは、

  • 直感的でわかりやすい
  • レスポンスが生のJSONで帰ってくるので、どう扱うかは利用者側にゆだねられている
  • 早い

のあたりです。特に生のJSONファイルなのでとりまわしは正直楽で、かつパースは後からやればいいので高速です。このあたりのシンプルさが好きですね。


例えばQueryのコードはこんな感じになります。ハッシュキーを指定してレンジキーを1万件取得している感じです。

package main

import (
        "fmt"
        "encoding/json"
        ep "github.com/smugmug/godynamo/endpoint"
        query "github.com/smugmug/godynamo/endpoints/query"
        "github.com/smugmug/godynamo/conf"
        "github.com/smugmug/godynamo/conf_file"
)

func main() {
        conf_file.Read()
        if conf.Vals.Initialized == false {
                panic("the conf.Vals global conf struct has not been initialized")
        }

        tn := "revaluate"
        q := query.NewQuery()
        {
                q.TableName = tn
                q.Select = ep.SELECT_ALL
        }
        k_v1 := "209963"
        var kc query.KeyCondition
        {
                kc.AttributeValueList = make([]ep.AttributeValue,1)
                kc.AttributeValueList[0] = ep.AttributeValue{N:k_v1}
                kc.ComparisonOperator = query.OP_EQ
        }
        q.Limit = 10000
        q.KeyConditions["id"] = kc
        json,_ := json.MarshalIndent(q, "", "")
        fmt.Printf("JSON:%s\n",string(json))
        body,code,err := q.EndpointReq()
        fmt.Printf("%v\n%v\n%v\n",body,code,err)
}

レスポンスはJSONで帰ってくるので必要な部分だけをパースすればよいと思います。

2013/12/13 17:03:38 read conf from: /Users/shot6/.aws-config.json
JSON:{
"AttributesToGet": null,
"ConsistentRead": false,
"ExclusiveStartKey": null,
"IndexName": null,
"KeyConditions": {
"id": {
"AttributeValueList": [
{
"N": "209963"
}
],
"ComparisonOperator": "EQ"
}
},
"Limit": 10000,
"ReturnConsumedCapacity": "NONE",
"ScanIndexForward": true,
"TableName": "revaluate",
"Select": "ALL_ATTRIBUTES"
}
{"Count":17,
"Items":[{"id":{"N":"209963"},"date":{"N":"1366807742203"},"user":{"S":"209963:231131"}},......]}
200
<nil>

まとめ

いかがでしたでしょうか?非常に簡単に使える感覚がわかってもらえたら嬉しいです。
私自身も試し始めたばかりですが、そこそこ使えているので、ぜひ試しにやってみてください。

また、DynamoDB自身もいよいよ待望のグローバルセカンダリインデックスが対応されましたので、こちらも試してみてください!

http://aws.typepad.com/aws/2013/12/now-available-global-secondary-indexes-for-amazon-dynamodb.html