3ヶ月ぶり転職
ちょっと前に5年ぶり転職 - ほげほげというエントリーを書いたばかりですが、オイシックスを8/31に退職しました。 9月からはフリーランスのエンジニアとしてプロジェクトに参画しています。
このエントリーでは将来また転職しようとした時に、会社と自分のミスマッチをしないために今の考えをメモします。 記事のわかりやすさの為に会社名を出していますが、それぞれの会社をディスる意図は全くありません。
簡単に大学卒業から今までの経歴を整理すると以下のようになります。
- 2006/04-2008/03 ハイテクシステム株式会社(SIer)
- 2008/05-2010/05 派遣とフリーランス(Web系)
- 2011/01-2016/05 株式会社ポッケ(モバイルコンテンツ)
- 2016/05-2016/08 オイシックス株式会社(食品系EC)
- 2016/09- フリーランス(アプリ/Web)
ポッケをやめた理由
ポッケでは幅広く開発することができ、今の自分のエンジニアとしての軸ができました。 その上でポッケを転職しようと思った理由は以下の様なものでした。
- お金もっとちょーだい
- もう少し大きなサービスに携わりたいよ(数人で1プロダクトのような)
- 現職以外のマネタイズ方法も経験したいよ
- 開発以外の組織づくりもしたいよ
- 他の現場も経験したいよ(開発フローとか興味あった)
こういった理由で転職しオイシックスに入社することにしました。
オイシックスをやめた理由
何かが出来ない環境になって初めてそれが好きだったことに気づくことがあると思います。 まさに「なんでもないようなことが〜♪」という心境です。
- 開発したい
これが一番大きな理由でした。
僕は開発によって便利や楽しいを増やすことが好きなので、たくさん機能の開発をしたいタイプのエンジニアです。 ただこの会社では開発案件が動くまでに時間がかかり、思うような量の開発ができなそうに感じました。
上場している企業ですし、社員として長く安心して働けそうな感じではありましたが、開発から離れることでエンジニアとして自分の市場価値を下げるのではないかと懸念しました。 自分の売りは開発力と案件の実現力なので、その力を更に伸ばせる開発の現場に戻ることにしてフリーのエンジニアに転職しました。
正社員じゃないのは、転職活動に時間がかかる点と、今回のようなミスマッチの再現を恐れたためです。
根底にある考え
そもそもなんで開発がしたいのか。 開発でサービスを消費者に届けるのが楽しいというのももちろんありますが、理想の働き方を追求したときに開発力がまず必要だと思ったからです。
僕の理想の働き方は以下の様な感じです。
- そこそこ高収入(年収1500万くらい)
- 時間に余裕がある(週30時間労働くらい)
- そこそこ楽しい
- ストレスフリー
こういった働き方をしたいのですが、やはり一般的な会社だと難しいと思っています。 となると自分でサービスを作るか、受託で開発するか、あたりが選択肢になってくるので、現場の力すなわち開発力を維持しておきたいと思っています。 開発については受託も視野に入れると幅広いスキルと、プロダクトを実際に作るまでの実現力を今後も伸ばしていきたいので、開発の仕事をしたいです。
これから
フリーのエンジニアも40代になると案件なくなるという事も耳にしてますし、ちょうど転職も難しくなる年が迫っているのはわかっています。 ただ今回の転職は理想の働き方を目指すためのはじめの一歩でもあり、プロのエンジニアとして長い期間働いていく覚悟を決めたような意味もあります。
先々なにが起こるかはわかりませんが、今はモンモンとした気持ちは無くなり、迷いない日々を過ごせています。
docker-composeで作成したcontainerにattach
Docker Compose について
Docker Composeについて
Composeは複数コンテナのDockerアプリケーションを定義、実行するツール。 Docker fileで設定し、コマンド一つでアプリケーションを開始することができる。
開発、テスト、ステージング環境のようなCIワークフローに有用。
使い方
大きく3ステップ。
Dockerfile
でアプリの環境を定義する。これによりどこでも再構築できる。docker-compose.yml
にサービスを定義する。1つの独立した環境に必要なコンテナが同時に実行することができる。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 には 必要なコンテナ が定義される。
参考
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 ホストとのディレクトリ共有用のマウントポイント
参考
初めての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
用語
- Docker HUB imageのリポジトリのようなもの
作ろうとしているもの
最近新しい言語の記事を書いていますが、何を作ろうとしているかというと自分のプロフィールページのようなものです。
今までのプログラマとしての経歴や成果物、ポートフォリオや、実験ページなどなどを載せておこうと思っています。
ブログで事足りるのですが、新しい言語、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というものもあります。