ほげほげ

プログラミング、英会話、ヨガ、料理などの備忘録など。

3ヶ月ぶり転職

ちょっと前に5年ぶり転職 - ほげほげというエントリーを書いたばかりですが、オイシックスを8/31に退職しました。 9月からはフリーランスのエンジニアとしてプロジェクトに参画しています。

このエントリーでは将来また転職しようとした時に、会社と自分のミスマッチをしないために今の考えをメモします。 記事のわかりやすさの為に会社名を出していますが、それぞれの会社をディスる意図は全くありません。

簡単に大学卒業から今までの経歴を整理すると以下のようになります。

ポッケをやめた理由

ポッケでは幅広く開発することができ、今の自分のエンジニアとしての軸ができました。 その上でポッケを転職しようと思った理由は以下の様なものでした。

  • お金もっとちょーだい
  • もう少し大きなサービスに携わりたいよ(数人で1プロダクトのような)
  • 現職以外のマネタイズ方法も経験したいよ
  • 開発以外の組織づくりもしたいよ
  • 他の現場も経験したいよ(開発フローとか興味あった)

こういった理由で転職しオイシックスに入社することにしました。

オイシックスをやめた理由

何かが出来ない環境になって初めてそれが好きだったことに気づくことがあると思います。 まさに「なんでもないようなことが〜♪」という心境です。

  • 開発したい

これが一番大きな理由でした。

僕は開発によって便利や楽しいを増やすことが好きなので、たくさん機能の開発をしたいタイプのエンジニアです。 ただこの会社では開発案件が動くまでに時間がかかり、思うような量の開発ができなそうに感じました。

上場している企業ですし、社員として長く安心して働けそうな感じではありましたが、開発から離れることでエンジニアとして自分の市場価値を下げるのではないかと懸念しました。 自分の売りは開発力と案件の実現力なので、その力を更に伸ばせる開発の現場に戻ることにしてフリーのエンジニアに転職しました。

正社員じゃないのは、転職活動に時間がかかる点と、今回のようなミスマッチの再現を恐れたためです。

根底にある考え

そもそもなんで開発がしたいのか。 開発でサービスを消費者に届けるのが楽しいというのももちろんありますが、理想の働き方を追求したときに開発力がまず必要だと思ったからです。

僕の理想の働き方は以下の様な感じです。

  • そこそこ高収入(年収1500万くらい)
  • 時間に余裕がある(週30時間労働くらい)
  • そこそこ楽しい
  • ストレスフリー

こういった働き方をしたいのですが、やはり一般的な会社だと難しいと思っています。 となると自分でサービスを作るか、受託で開発するか、あたりが選択肢になってくるので、現場の力すなわち開発力を維持しておきたいと思っています。 開発については受託も視野に入れると幅広いスキルと、プロダクトを実際に作るまでの実現力を今後も伸ばしていきたいので、開発の仕事をしたいです。

これから

フリーのエンジニアも40代になると案件なくなるという事も耳にしてますし、ちょうど転職も難しくなる年が迫っているのはわかっています。 ただ今回の転職は理想の働き方を目指すためのはじめの一歩でもあり、プロのエンジニアとして長い期間働いていく覚悟を決めたような意味もあります。

先々なにが起こるかはわかりませんが、今はモンモンとした気持ちは無くなり、迷いない日々を過ごせています。

docker-composeで作成したcontainerにattach

docker-composeで作成したcontainerに docker attach ID_OR_NAME でアタッチしたところ、bashが起動せず ctrl+p ctrl+qでも抜けられなくなった。

どうやらdocker-composeで作成したコンテナにはアタッチできないようなのですが、ちょっと環境変数確認したいとかディレクトリ構成確認したいとかあると思います。 その場合は以下のようにすればbashで接続することができます。

docker exec -it ID_OR_NAME bash

参考 Docker Compose で複数のコンテナを管理する - Qiita

Docker Compose について

Docker Composeについて

Composeは複数コンテナのDockerアプリケーションを定義、実行するツール。 Docker fileで設定し、コマンド一つでアプリケーションを開始することができる。

開発、テスト、ステージング環境のようなCIワークフローに有用。

使い方

大きく3ステップ。

  1. Dockerfile でアプリの環境を定義する。これによりどこでも再構築できる。
  2. docker-compose.ymlにサービスを定義する。1つの独立した環境に必要なコンテナが同時に実行することができる。
  3. docker-compose up コマンドを実行する。環境が構築され使用できる。

docker-compose.yml例

# バージョン
version: '2'

# サービスの定義(コンテナ)
services:
  # webという名前のコンテナ
  web:
    build: . # カレントディレクトリの Dockerfile から生成
    ports: 
    - "5000:5000" # 5000ポートを5000ポートにフォワード
    volumes:
    - .:/code # カレントディレクトリをコンテナの/codeにマウント
    - logvolume01:/var/log 
    links:
    - redis # redisコンテナにリンク

  # redisという名前のコンテナ
  redis: 
    image: redis # redisイメージから生成

volumes:
  logvolume01: {}

services には 必要なコンテナ が定義される。

参考

https://docs.docker.com/compose/overview/

Dockerfileについて

DockerfileというDockerイメージを作るための設定ファイルについて調べたのでメモ。

Dockerfileについて

DockerはDockerfileからイメージをビルドできる。 Dockerfileはテキストファイルで、コマンドラインでイメージを構築するときに使用できるコマンドの全てを使用できる。

使い方

docker buildコマンドはDockerfileとからイメージをビルドする。 Dockerfileは指定することができ、PATHかURLである。 PATHはローカルファイル・システム、URLはGitリポジトリのURL.

次の例は、カレントディレクトリがコンテキストになる.

$ docker build .

コンテキストに/(ルートディレクトリ)は指定しない方がいい。多分コンテナから書き込まれるから?

$ docker build -f /path/to/a/Dockerfile .

これはカレントディレクトリをコンテキストにして、別の場所のDockerfileを参照する方法。

以下のように-tオプションをつけることでイメージを保存できる

$ docker build -t hoge .
$ docker images # hogeが登録されていることを確認できる

フォーマット

Dockerfileのフォーマットは以下です。

# Comment
INSTRUCTION arguments

INSTRUCTIONは大文字小文字関係なしだが、大文字の方がargumentsと区別しやすい。 はじめのINSTRUCTIONはFROMで、ビルドするイメージを指定します。 #で始まる行はコメント。

主要なINSTRUNCTION

  • FROM イメージを指定FROM hoge:tagname
  • CMD コンテナを実行した時に実行されるコマンド
  • EXPOSE コンテナがLISTENするポート
  • VOLUME ホストとのディレクトリ共有用のマウントポイント

参考

Dockerfile reference

初めてのDocker インストールからサンプル実行まで

dockerで開発環境の構築をしようと思い、試してみました。 docker run コマンドでDBやWebサーバーを立ち上げられるので、すぐに開発環境が構築できる。

概要

  • DockerはVM立ち上げるよりも低コスト(時間,マシンリソース)で開発環境の構築ができる。
  • ファイルシステムを含めたイメージの共有が可能。
  • Sandbox内に構築するのでホストの環境を汚さない。
  • コンテナ型

インストール

インストール

1.ダウンロードページからdmgをダウンロードし、インストールする 1.パスワードが求められたら入力 1.メニューバーにクジラアイコンが表示されたら、ターミナルを起動してdockerコマンドが使用できるか確認する

$ docker version
Client:
 Version:      1.12.0
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   8eab29e
 Built:        Thu Jul 28 21:15:28 2016
 OS/Arch:      darwin/amd64

Server:
 Version:      1.12.0
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   8eab29e
 Built:        Thu Jul 28 21:15:28 2016
 OS/Arch:      linux/amd64

Dockerエンジン、コンポーズ、マシーンのバージョンのチェック

$ docker --version
Docker version 1.12.0, build 8eab29e
$ docker-compose --version
docker-compose version 1.8.0, build f3628c7
$ docker-machine --version
docker-machine version 0.8.0, build b85aac1

実際のバージョンはこれと異なっても構わない。

サンプル実行

runしようとしているイメージがない場合は、Docker HUBからpullされる。

hello world

docker run hello-world

コンソールにHello from Docker!と表示されればOK

Nginxを起動

docker run -d -p 80:80 --name webserver nginx

http://localhost でNginxの初期ページが表示されていればOK. 80:80はlocalhostの80をコンテナの80にフォワードする設定. webserverが起動した状態でdocker psを実行すると、webserverコンテナの詳細を確認できる。

他にもサンプルを見たい場合はこちら

設定

メニューバーのクジラアイコン→preferencesから色々設定できる

bash completion対応

以下のコマンドでbash completionに対応できる

cd /usr/local/etc/bash_completion.d
ln -s /Applications/Docker.app/Contents/Resources/etc/docker.bash-completion
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-machine.bash-completion
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-compose.bash-completion

用語

作ろうとしているもの

最近新しい言語の記事を書いていますが、何を作ろうとしているかというと自分のプロフィールページのようなものです。

今までのプログラマとしての経歴や成果物、ポートフォリオや、実験ページなどなどを載せておこうと思っています。

ブログで事足りるのですが、新しい言語、DB、デザインの勉強の意味も含めて、作ってゆこうと思います。

使う技術は以下を予定 * Go(Gin) * PostgreSQL * AngularJS2 * HTML5 * CSS3

プラットフォーム * BlueMix

開発環境(検討中) * Docker ? * vagrant ?

Goフレームワーク Gin について

Ginを使うことにしました。 README.mdを通読したので、メモしておきます。

Ginを選んだ理由

困ったときにコード読みたかったので、リフレクションを使っているMartiniはコード追いにくいような気がしたからやめた。 お酒のマティーニが好きだったので、Martiniにしたかったけど。。 お酒つながりでGinにした。パフォーマンスもMartiniよりイイらしい。

Gin Web Framework(訳)

GinはGoで書かれたWebフレームワークです。Ginはマティーニと類似のAPIを提供し、httprouterのおかげでマティーニの40倍ほど高速です。もしあなたがパフォーマンスと生産性を必要としているのであれば、Ginを気に入るでしょう。

インストール&使い方

ダウンロードとインストール

$ go get github.com/gin-gonic/gin

プログラム(app.go)

package main
import (
    "github.com/gin-gonic/gin"
    "net/http"
)
func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello World")
    })
    r.Run() // listen and server on 0.0.0.0:8080
}

ビルド&起動

$ go build app.go
$ ./app

http://localhost:8080/ にアクセスするとHello Worldと表示される。

APIサンプル集

ginのgithubにありますが、簡単に訳してメモしておきます。

GET,POST,PUT,PATCH,DELETE,OPTIONSの使い方

func main() {
    // デフォルトのミドルウェアとともにginルーターを作成
    // ミドルウェアはロガーとリカバリー(クラッシュフリー)
    router := gin.Default()

    router.GET("/someGet", getting)
    router.POST("/somePost", posting)
    router.PUT("/somePut", putting)
    router.DELETE("/someDelete", deleting)
    router.PATCH("/somePatch", patching)
    router.HEAD("/someHead", head)
    router.OPTIONS("/someOptions", options)

    // PORT環境変数が指定されていない場合は8080ポートで待受
    router.Run()
    // router.Run(":3000") // ポートをハードコーディングしたい場合
}

パス内のパラメタ

*:の違いに注目。

func main() {
    router := gin.Default()

    // このハンドラは /user/john にマッチするが、 /user/ や /user にはマッチしない
    router.GET("/user/:name", func(c *gin.Context) {
        name := c.Param("name")
        c.String(http.StatusOK, "Hello %s", name)
    })

    // でもこれは /user/john/ や /user/john/send にマッチする
    //  /user/john にマッチするルーターがなければ、/user/john/ にリダイレクトされる
    router.GET("/user/:name/*action", func(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        message := name + " is " + action
        c.String(http.StatusOK, message)
    })

    router.Run(":8080")
}

クエリストリングパラメタ

GETの値の参照方法。

func main() {
    router := gin.Default()

    // クエリストリングパラメーターは既存のrequest objectの下でパースされる
    // このリクエストは /welcome?firstname=Jane&lastname=Doe へ返答する
    router.GET("/welcome", func(c *gin.Context) {
        firstname := c.DefaultQuery("firstname", "Guest") // Geustはデフォルト値?
        lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname") のショートカット

        c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    })
    router.Run(":8080")
}

Multipart/Urlencoded Form

POSTの値の参照方法。

func main() {
    router := gin.Default()

    router.POST("/form_post", func(c *gin.Context) {
        message := c.PostForm("message")
        nick := c.DefaultPostForm("nick", "anonymous") // anonymousはデフォルト値

        c.JSON(200, gin.H{
            "status":  "posted",
            "message": message,
            "nick":    nick,
        })
    })
    router.Run(":8080")
}

POST時にクエリストリングも参照する

func main() {
    router := gin.Default()

    router.POST("/post", func(c *gin.Context) {

        id := c.Query("id")
        page := c.DefaultQuery("page", "0")
        name := c.PostForm("name")
        message := c.PostForm("message")

        fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
    })
    router.Run(":8080")
}

ファイルのアップロード

func main() {
    router := gin.Default()

    router.POST("/upload", func(c *gin.Context) {

            file, header , err := c.Request.FormFile("upload")
            filename := header.Filename
            fmt.Println(header.Filename)
            out, err := os.Create("./tmp/"+filename+".png")
            if err != nil {
                log.Fatal(err)
            }
            defer out.Close()
            _, err = io.Copy(out, file)
            if err != nil {
                log.Fatal(err)
            }   
    })
    router.Run(":8080")
}

グルーピング ルーティング

APIのバージョンとかに使えそう?

func main() {
    router := gin.Default()

    // Simple group: v1
    v1 := router.Group("/v1")
    {
        v1.POST("/login", loginEndpoint)
        v1.POST("/submit", submitEndpoint)
        v1.POST("/read", readEndpoint)
    }

    // Simple group: v2
    v2 := router.Group("/v2")
    {
        v2.POST("/login", loginEndpoint)
        v2.POST("/submit", submitEndpoint)
        v2.POST("/read", readEndpoint)
    }

    router.Run(":8080")
}

ミドルウェアを使用する

func main() {
    // デフォルトのミドルウェアなしのGinルーターを作成
    r := gin.New()

    // グローバルミドルウェア
    r.Use(gin.Logger())
    r.Use(gin.Recovery())

    // ルーターごとのミドルウェア, 使いたいだけ追加できます。
    r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

    // 認証グループ
    // authorized := r.Group("/", AuthRequired())
    // これは以下のようにもかけます
    authorized := r.Group("/")
    // この場合authorizedグループだけにAuthRequired() ミドルウェアを使う。
    authorized.Use(AuthRequired())
    {
        authorized.POST("/login", loginEndpoint)
        authorized.POST("/submit", submitEndpoint)
        authorized.POST("/read", readEndpoint)

        // ネストされたグループ
        testing := authorized.Group("testing")
        testing.GET("/analytics", analyticsEndpoint)
    }

    // Listen and server on 0.0.0.0:8080
    r.Run(":8080")
}

モデルバインディングとバリデーション

リクエストボディを型にバインドするには、モデルバインディングを使用してください。 GinはJSON,XMLと標準的なフォームの値(foo=bar&hoge=fuga)をサポートしています。

// JSONからバインド
type Login struct {
    User     string `form:"user" json:"user" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}

func main() {
    router := gin.Default()


    // JSONをバインディングする例({"user": "manu", "password": "123"})
    router.POST("/loginJSON", func(c *gin.Context) {
        var json Login // Login型変数
        if c.BindJSON(&json) == nil { // c.BindJSON関数を使ってバインドする BindJSONの戻り値はerrorオブジェクトかな
            if json.User == "manu" && json.Password == "123" {
                c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
            } else {
                c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
            }
        }
    })

    // HTML formをバインドする例(user=manu&password=123)
    router.POST("/loginForm", func(c *gin.Context) {
        var form Login // Login型変数
    // content-typeにより元データの形式(JSON,XML,form)を予想してバインドする
        if c.Bind(&form) == nil {
            if form.User == "manu" && form.Password == "123" {
                c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
            } else {
                c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
            }
        }
    })

    // Listen and server on 0.0.0.0:8080
    router.Run(":8080")
}

Multipart/Urlencoded(ポスト) のバインディング

同様にできる

XML,JSON,YAMLの結果を返す

func main() {
    r := gin.Default()

    // gin.H は map[string]interface{} へのショートカット
    r.GET("/someJSON", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
    })

    r.GET("/moreJSON", func(c *gin.Context) {
        // 構造体も使える
        var msg struct {
            Name    string `json:"user"`
            Message string
            Number  int
        }
        msg.Name = "Lena"
        msg.Message = "hey"
        msg.Number = 123
    // この msg.Name は JSONないでは "user"となる
    // 出力{"user": "Lena", "Message": "hey", "Number": 123}
        c.JSON(http.StatusOK, msg)
    })

    r.GET("/someXML", func(c *gin.Context) {
        c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
    })

    r.GET("/someYAML", func(c *gin.Context) {
        c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
    })

    // Listen and server on 0.0.0.0:8080
    r.Run(":8080")
}

静的ファイル

静的コンテンツは以下のようにディレクトリやファイルごとにマッピングしておける。

func main() {
    router := gin.Default()
    router.Static("/assets", "./assets")
    router.StaticFS("/more_static", http.Dir("my_file_system"))
    router.StaticFile("/favicon.ico", "./resources/favicon.ico")

    // Listen and server on 0.0.0.0:8080
    router.Run(":8080")
}

HTMLを返却する

LoadHTMLTemplates()関数を使う。

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("templates/*") // 事前にテンプレートをロード
    //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") // ファイル指定でロード
    router.GET("/index", func(c *gin.Context) {
        // テンプレートを使って、値を置き換えてHTMLレスポンスを応答
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
            "title": "Main website",
        })
    })
    router.Run(":8080")
}

templates/index.html

<html>
    <h1>
        {{ .title }}
    </h1>
</html>

同じ名前でディレクトリが異なるテンプレートの使い方

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("templates/**/*")
    router.GET("/posts/index", func(c *gin.Context) {
        c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
            "title": "Posts",
        })
    })
    router.GET("/users/index", func(c *gin.Context) {
        c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
            "title": "Users",
        })
    })
    router.Run(":8080")
}

リダイレクト

r.GET("/test", func(c *gin.Context) {
    c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})

独自ミドルウェアの使い方

func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()

        // Set example variable
        c.Set("example", "12345")

        // リクエスト前

        c.Next()

        // リクエスト後
        latency := time.Since(t)
        log.Print(latency)

        // 送信したリクエストにアクセスできる
        status := c.Writer.Status()
        log.Println(status)
    }
}

func main() {
    r := gin.New()
    r.Use(Logger())

    r.GET("/test", func(c *gin.Context) {
        example := c.MustGet("example").(string)

        // it would print: "12345"
        log.Println(example)
    })

    // Listen and server on 0.0.0.0:8080
    r.Run(":8080")
}

ミドルウェア内でのゴルーチン

ミドルウェアやハンドラー内でゴルーチンを使う場合、オリジナルのcontextは使うべきでない。リードオンリーコピーを使うようにしなさい。

func main() {
    r := gin.Default()

    r.GET("/long_async", func(c *gin.Context) {
        // create copy to be used inside the goroutine
        cCp := c.Copy()
        go func() {
            // simulate a long task with time.Sleep(). 5 seconds
            time.Sleep(5 * time.Second)

            // note that you are using the copied context "cCp", IMPORTANT
            log.Println("Done! in path " + cCp.Request.URL.Path)
        }()
    })

    r.GET("/long_sync", func(c *gin.Context) {
        // simulate a long task with time.Sleep(). 5 seconds
        time.Sleep(5 * time.Second)

        // since we are NOT using a goroutine, we do not have to copy the context
        log.Println("Done! in path " + c.Request.URL.Path)
    })

    // Listen and server on 0.0.0.0:8080
    r.Run(":8080")
}

独自のHTTPの設定

http.ListenAndServe()を以下のように直接使う

func main() {
    router := gin.Default()
    http.ListenAndServe(":8080", router)
}

または

func main() {
    router := gin.Default()

    s := &http.Server{
        Addr:           ":8080",
        Handler:        router,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}

グレースフルリスタート

グレースフルリスタートしたいですか?いくつかの方法があります。 fvbock/endlessでデフォルトのListenAndServeを置換できます。

router := gin.Default()
router.GET("/", handler)
// [...]
endless.ListenAndServe(":4242", router)

endlessの代わりにmannersというものもあります。

Ginを使ったプロジェクトの例

  • drone Goで書かれたDockerで動くデリバリシステム
  • goruch Goで書かれたプッシュ通知サーバー