tjun月1日記

なんでもいいので毎月書きたい

GoogleAppEngineのManagedSSLを使ってみた

ちょっと前にAppEngineのManagedSSLというのが発表されました。

今までも証明書設定してSSLで使っていたので、最初はどういうことなのかよく分からなかったんですが、使ってみたら便利でした。

やり方は Google Cloud Platform Blog: Introducing managed SSL for Google App Engine に書いてある通りに、AppEngineのSetteingから Enable ManagedSecurity を押すだけです。少し待つと適用されます。 サブドメイン切ってもそれぞれSSLを有効にできます。

見ればわかりますが、Let's Encrypt の証明書です。

ManagedSSLのいいところは、無料で、簡単に設定できて、自動で更新もしてくれるところです。証明書買って、設定して、更新するの、結構面倒です。 とりあえず暗号化したいだけなら、これで十分という感じがしました。

今からgoでwebサーバ書くならchiがいいかも

goでwebサーバを書く時、フレームワーク的なもののデファクトがいまいちない感じですが、chiを触ってみたらよさそうだったので紹介します。

これまでのgoでのWeb開発

去年くらいに調べたときの感じでは、

  • 標準のnet/httpでいいでしょ + routerに gorilla/muxみたいな薄いライブラリを入れる
  • 比較的軽めのframeworkで、 echo, Gin, goji など
  • Railsみたいなのが欲しい人はrevelとか beegoとか?

という感じでした。

個人的には、goで書くならあまり重いフレームワークは使いたくないけど net/httpはしんどそう、ということで今までは echo使ってました。結構よかったです。contextを引き回しておけば、そこから必要なものが取得できていい感じに書けました。

echoよかったけど・・・

echoよかったんですが、今から使おうと思うと自分の場合以下の点が気になりました。

  • contextの取り扱いが echo ver.2 から ver.3で変わっていて、AppEngine+go1.8で使おうと思うといまいちだった
  • echo作ってるチームがarmorというのを後から始めていて、echo今後もやっていくのか少し不安がある

という感じで、AppEngine+Go1.8で使うならあまりオススメできません。

chiよさそう

そこでechoの代わりに使えるものを探してみて、chiを知りました。

go-chi/chi: lightweight, idiomatic and composable router for building Go HTTP services

READMEにある説明によると

  • 軽量
  • 高速
  • 標準pkg以外の外部パッケージに依存しない
  • net/httpに100%準拠
  • APIをモジュール化できる仕組み(ミドルウェア、routeグループ、subrouter)

ということで、薄いpkgでルーティングをいい感じにしたい人にはぴったりです。

chiのレポジトリのREADMEにある以下のコードを見ると、できることがだいたい分かると思います。

import (
  //...
  "context"
  "github.com/go-chi/chi"
  "github.com/go-chi/chi/middleware"
)

func main() {
  r := chi.NewRouter()

  // A good base middleware stack
  r.Use(middleware.RequestID)
  r.Use(middleware.RealIP)
  r.Use(middleware.Logger)
  r.Use(middleware.Recoverer)

  // Set a timeout value on the request context (ctx), that will signal
  // through ctx.Done() that the request has timed out and further
  // processing should be stopped.
  r.Use(middleware.Timeout(60 * time.Second))

  r.Get("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hi"))
  })

  // RESTy routes for "articles" resource
  r.Route("/articles", func(r chi.Router) {
    r.With(paginate).Get("/", listArticles)                           // GET /articles
    r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017

    r.Post("/", createArticle)                                        // POST /articles
    r.Get("/search", searchArticles)                                  // GET /articles/search

    // Regexp url parameters:
    r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug)                // GET /articles/home-is-toronto
    
    // Subrouters:
    r.Route("/{articleID}", func(r chi.Router) {
      r.Use(ArticleCtx)
      r.Get("/", getArticle)                                          // GET /articles/123
      r.Put("/", updateArticle)                                       // PUT /articles/123
      r.Delete("/", deleteArticle)                                    // DELETE /articles/123
    })
  })

  // Mount the admin sub-router
  r.Mount("/admin", adminRouter())

  http.ListenAndServe(":3333", r)
}

ルーティングにmiddleware入れる仕組みがあるのがいいですね。

例えば、ベーシック認証を行うmiddlewareは次のように書けます。

var userPasswords = map[string]string{
    "user": "PassW0rd",
}

func basicAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        usr, pw, ok := r.BasicAuth()
        if !ok {
            w.Header().Set("WWW-Authenticate", "Basic")
            w.WriteHeader(http.StatusUnauthorized)
            http.Error(w, "auth required", http.StatusUnauthorized)
            return
        }

        if userPasswords[usr] != pw {
            http.Error(w, "incorrect auth info", http.StatusUnauthorized)
            return
        }

        next.ServeHTTP(w, r)
    })
}

で、ベーシック認証かけたいところで

    r.With(basicAuth).Get("/internal", secretPage) 

という感じで使うことが可能です。

gorilla/muxを使ったことはないけど、READMEを読む限り、書き方的にはchiが好きの方が好きです。

ということで、ドキュメントを読んでちょっと触ってみた限り、とてもいい感じがするのでオススメです。

GKEにstaticなegressのIPアドレスを割り当てる

タイトルのとおりですが、こんなことする人はあまりいないと思います。ingressをstaticにするのは簡単ですが、egressはやり方調べても情報がなくて苦労しました。

今回のケースでは、GKEである処理を行うworkerを作っていて、その処理の途中で外部のサーバへ接続してデータを取ってくる必要があるのですが、その外部のサーバがIPアドレスによるアクセス制限をかけていました。 そのため、アクセスするIPアドレスを申請する必要があり、どのnodeからリクエストしてもそのegressのIPアドレスを固定したいという状況です。

やり方を一言でいうと、NAT用のinstanceを立てる、です。

 johnlabarge/gke-nat-example を参考にしました。

IP, network, subnet, NAT用instanceなどいろいろと作らなくてはいけなくて大変なので、deployment-managerを使って設定を書いていきます。

メインのyaml myapp.yaml

imports:
- path: myapp-with-nat.jinja

resources:
- name: myapp-with-nat
  type: myapp-with-nat.jinja
  properties:
    region: asia-northeast1
    zone: asia-northeast1-a
    cluster_name: myapp
    num_nodes: 3

myapp-with-nat.jinja.scheme

info:
  title: MyApp GKE cluster with NAT  
  description: Creates a MyApp GKE Cluster with a nat route

required:
  - zone
  - cluster_name
  - num_nodes

properties:
  region:
    type: string
    description: GCP region
    default: asia-northeast1

  zone:
    type: string
    description: GCP zone
    default: asia-northeast1-a

  cluster_name:
    type: string
    description: Cluster Name
    default: "myapp"

  num_nodes:
    type: integer
    description: Number of nodes
    default: 3

myapp-with-nat.jinja

resources:
######## Static IP ########
- name: {{ properties["cluster_name"] }}-static-address
  type: compute.v1.address
  properties:
    region: {{ properties["region"] }}

######## Network ############
- name: {{ properties["cluster_name"] }}-nat-network
  type: compute.v1.network
  properties: 
    autoCreateSubnetworks: false
######### Subnets ##########
######### For Cluster #########
- name: {{ properties["cluster_name"] }}-cluster-subnet 
  type: compute.v1.subnetwork
  properties:
    network: $(ref.{{ properties["cluster_name"] }}-nat-network.selfLink)
    ipCidrRange: 172.16.0.0/12
    region: {{ properties["region"] }}
########## NAT Subnet ##########
- name: nat-subnet
  type: compute.v1.subnetwork
  properties: 
    network: $(ref.{{ properties["cluster_name"] }}-nat-network.selfLink)
    ipCidrRange: 10.1.1.0/24
    region: {{ properties["region"] }}
########## NAT VM ##########
- name: nat-vm
  type: compute.v1.instance 
  properties:
    zone: {{ properties["zone"] }}
    canIpForward: true
    tags:
      items:
      - nat-to-internet
    machineType: https://www.googleapis.com/compute/v1/projects/{{ env["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro
    disks:
      - deviceName: boot
        type: PERSISTENT
        boot: true
        autoDelete: true
        initializeParams:
          sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20150423
    networkInterfaces:
    - network: projects/{{ env["project"] }}/global/networks/{{ properties["cluster_name"] }}-nat-network
      subnetwork: $(ref.nat-subnet.selfLink)
      accessConfigs:
      - name: External NAT
        type: ONE_TO_ONE_NAT
        natIP: $(ref.{{ properties["cluster_name"] }}-static-address.address)
    metadata:
      items:
      - key: startup-script
        value: |
          #!/bin/sh
          # --
          # ---------------------------
          # Install TCP DUMP
          # Start nat; start dump
          # ---------------------------
          apt-get update
          apt-get install -y tcpdump
          apt-get install -y tcpick 
          iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
          nohup tcpdump -e -l -i eth0 -w /tmp/nat.pcap &
          nohup tcpdump -e -l -i eth0 > /tmp/nat.txt &
          echo 1 | tee /proc/sys/net/ipv4/ip_forward
########## FIREWALL RULES FOR NAT VM ##########
- name: nat-vm-firewall 
  type: compute.v1.firewall
  properties: 
    allowed:
    - IPProtocol : tcp
      ports: []
    sourceTags: 
    - route-through-nat
    network: $(ref.{{ properties["cluster_name"] }}-nat-network.selfLink)
- name: nat-vm-ssh
  type: compute.v1.firewall
  properties: 
    allowed:
    - IPProtocol : tcp
      ports: [22]
    sourceRanges: 
    - 0.0.0.0/0
    network: $(ref.{{ properties["cluster_name"] }}-nat-network.selfLink)
########## GKE CLUSTER CREATION ##########
- name: {{ properties["cluster_name"] }}
  type: container.v1.cluster
  metadata: 
   dependsOn:
   - {{ properties["cluster_name"] }}-nat-network 
   - {{ properties["cluster_name"] }}-cluster-subnet
  properties: 
    cluster: 
      name: {{ properties["cluster_name"] }}
      initialNodeCount: {{ properties["num_nodes"] }}
      network: {{ properties["cluster_name"] }}-nat-network
      subnetwork: {{ properties["cluster_name"] }}-cluster-subnet
      nodeConfig:
        oauthScopes:
        - https://www.googleapis.com/auth/compute
        - https://www.googleapis.com/auth/devstorage.read_write
        - https://www.googleapis.com/auth/logging.write
        - https://www.googleapis.com/auth/monitoring
        - https://www.googleapis.com/auth/bigquery
        tags:
        - route-through-nat
    zone: {{ properties["zone"] }}
########## GKE MASTER ROUTE ##########
- name: master-route
  type: compute.v1.route
  properties:
    destRange: $(ref.{{ properties["cluster_name"] }}.endpoint)
    network: $(ref.{{ properties["cluster_name"] }}-nat-network.selfLink)
    nextHopGateway: projects/{{ env["project"] }}/global/gateways/default-internet-gateway
    priority: 100
    tags:
    - route-through-nat
########## NAT ROUTE ##########
- name: {{ properties["cluster_name"] }}-route-through-nat
  metadata: 
    dependsOn:
    - {{ properties["cluster_name"] }}
    - {{ properties["cluster_name"] }}-nat-network
  type: compute.v1.route
  properties: 
    network: $(ref.{{ properties["cluster_name"] }}-nat-network.selfLink)
    destRange: 0.0.0.0/0
    description: "route all other traffic through nat"
    nextHopInstance: $(ref.nat-vm.selfLink)
    tags:
    - route-through-nat
    priority: 800

長いので説明は省きますが、読めばなんとなく分かると思います。

これで、

deployment-manager deployments create myapp --config myapp.yml

とすると、NAT経由でリクエストを投げられるGKEクラスタを作ることができます。

独自imageのdocker-machineをGCEで利用する

独自のdocker imageを作ってGoogleCloudPlatform(以下GCP)上のContainer Registryに登録して、GCE(Google Compute Engine)で動かすやり方です。

Dockerfileを元にimageを作ってContainer Registryにpushするまで

以下の例ではcontainer registryのサーバはアジアにしてます。

NAME=myapp
VERSION=1
APPID=<gcpのprojectID>

docker build -t ${NAME}:${VERSION} .
docker tag ${NAME}:${VERSION} asia.gcr.io/$(APPID)/${NAME}:${VERSION}
docker tag asia.gcr.io/$(APPID)/${NAME}:${VERSION} asia.gcr.io/$(APPID)/${NAME}:latest

gcloud --project=$(APPID) docker --server=asia.gcr.io -- push asia.gcr.io/$(APPID)/${NAME}

GCEをdocker-machineとして起動する

以下のように、docker-machineを作る際にdriverとしてgoogleを指定します。 その他のoptionは Google Compute Engine | Docker Documentation を参考に設定します。

docker-machine create \
    --driver google \
    --google-project $(APPID) \
    --google-preemptible \
    --google-zone asia-northeast1-a \
    --google-machine-type n1-highcpu-8 \
    --google-disk-size 300 \
    --google-disk-type pd-ssd \
    --google-machine-image https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-1604-xenial-v20170815a \
    --google-scopes https://www.googleapis.com/auth/devstorage.read_write \
    ${NAME} 

作ったdocker-machineを確認する

$ docker-machine ls
NAME             ACTIVE   DRIVER   STATE     URL   SWARM   DOCKER    ERRORS
myapp                 -        google   Stopped                 Unknown

activeなdocker-machineを切り替える

eval $$(docker-machine env ${NAME}) 

docker-machine上で、登録したimageを実行する

gcloud docker -- run -it -e PROJECT_ID=$(APPID) --name ${NAME} asia.gcr.io/$(APPID)/${NAME}:latest /bin/bash

goのtemplateのrangeで複数の配列を扱う

最初ちょっとやり方が分からなかったのでメモ。

やりたいことは、例えばgoで以下のような配列があったとき

type User struct {
 ID int
 Name string
}

type UserInfo struct {
 ID int
 Age int
}

users := []User{
    User{ID: 1, Name: "taro"},
    User{ID: 2, Name: "jiro"},
    User{ID: 3, Name: "hanako"},
}

infos := []UserInfo{
    UserInfo{ID: 1, Age: 10},
    UserInfo{ID: 2, Age: 20},
    UserInfo{ID: 3, Age: 30},
}

ちょっと例が微妙ですが、usersinfosを同じindexでループを回したいようなことがあるかと思います。

goのtemplateでは、単独のループであれば

{{range $index, $user := Users}}
    {{$user.Name}}
{{end}}

のように書けるのですが、もう一つの配列も同じようにループを回す場合には、以下のように書く必要があります。

{{range $index, $user := Users}}
    {{$user.Name}}
    {{(index $.Infos $index).Age}}
{{end}}

のindexを使って、ループ外部の変数の配列のindexを指定する感じです。

GoogleAppEngine/Goでのディレクトリ構成

GAE/Goで、結構悩むのがディレクトリ構成です。 pkgをどう分けて、どこのディレクトリに何を置くか、自分はどうやってるか紹介します。

関連する記事はこのあたりです。

はじめに

  • 基本的にはGOPATH以下にコードを置くことを想定しています。 以下の例では$GOPATH/src/github.com/tjun/gaesample とします。
  • AppEngineは Go1.6 を利用している場合になります。1.8になると変わる部分があるかもしれません。
  • echoでサーバを書いていますが、構成にはあまり影響がないと思います。
  • vendoringはglideを使ってやってます
  • goappコマンドはあまり使っていません

方針

  • 依存するpkgは、バージョンを固定したいので vendor以下におきます。
  • デプロイスクリプトなどは、_deployなどアンダースコアから始まるディレクトリに入れます

ディレクトリ構成

以下のような感じでやってます。

$GOPATH/src/github.com/tjun/gaesample
|-- main/
|       |-- app.yaml
|       |-- index.yaml
|       |-- main.go  # AppEngineのエントリポイント
|       `-- public/   # img, cssなど静的なファイル
|
|-- controllers/   # コントローラ
|-- models/        # モデル
|-- views/         # view
|-- server/        # echoのサーバのエントリポイント
|-- router/router.go   # ルーティング
...                       # その他のpkgも直下に置く
|-- vendor/  
|
|--Makefile 
|-- _deploys/   # デプロイスクリプト置き場
|
...
  • クライアントのコードは省略していますが、ビルド時に main/public以下に持っていきます。

サーバをローカルで動かすときは

./dev_appserer.py main

で動かせます。 デプロイのときは、

./appcfg.py update main

になります。

この構成のいいところは、内部のpkgのimportが

import (
 "github.com/tjun/gaesample/models"
)

みたいな感じでわりと自然に書けることです。

GOPATH以下にコードを置いてると、CircleCIからのデプロイがそのままじゃできなかったりしますが、そのやり方はまた今度書きます。

tmuxのwindow nameをgitのレポジトリ名にする

window nameっていうのは、tmux で下のstatusに出てくるタブみたいなやつの名前。 どのタブがどのレポジトリで作業しているのか分からなくなるので、レポジトリ名を表示するようにした。 git管理下じゃない場合はカレントディレクトリを表示する。

.zshrcに以下を追加する。

autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git svn
zstyle ':vcs_info:*' formats '%r'

precmd () {
  LANG=en_US.UTF-8 vcs_info
  if [[ -n ${vcs_info_msg_0_} ]]; then
          tmux rename-window $vcs_info_msg_0_
  else
          tmux rename-window `basename $(pwd)`
  fi
}

f:id:taka-jun:20170301201202g:plain