tjun月1日記

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

GAE/Goをdelveでデバッグする

AppEngine/Goでこれまで開発していて、必要な箇所はログを出していれば状態が取れていたのであまりデバッガが使いたくなることがなかったんですが、 最近ちょっとデバッガを使いたい状況があり、AppEngine/Go のローカルサーバに対してDelveをつないでデバッグしたので、やり方を書いておきます。

※基本的に以下はMacでのやり方になります。Linuxもそんなに変わらないと思う。

準備

Delveをインストールします。

go get -u github.com/derekparker/delve/cmd/dlv

GUIを提供する gdlvというのも入れてもいいかもしれません。

サーバを起動

AppEngine/Goのローカルのサーバを立ち上げます。このとき、オプションが必要です。

goapp serve -debug <PATH_TO_YAML_DIR>

または、

dev_appserver.py --go_debugging  <PATH_TO_YAML_DIR>

以前は、delveAppengineなどを使う必要があったみたいですが、今はdebugオプションがあるので不要になりました。

delveをアタッチ

まずアタッチするプロセスのpidを調べます。

$ ps au | grep _go_app

52613 ttys003    0:00.03 /var/folders/wn/xxxxxxxxxxxxxxxxx/T/tmptMHgJNappengine-go-bin/_go_app

アタッチします

dlv attach <pid>

ここは、sudoが必要かもしれません。 また、自分の環境では MacのOS のversionが古いせいか、以下のようにpathを指定する必要がありました。

dlv attach 52613 /var/folders/wn/xxxxxxxxxxxxxxxxx/T/tmptMHgJNappengine-go-bin/_go_app

delveでデバッグする

あとは、delveでブレークポイント貼ってデバッグしていく感じです。 ここでは解説しませんが、 bブレークポイント貼って、cで回して、nでステップ実行して、 sでステップインして、pで見たい変数見て、lで今いるところを確認する、くらい知っておけばとりあえず使えると思います。 delve/Documentation/cli

初めて使いましたが gdbっぽい感じで使えてあまり違和感なかったです。

その他

たぶん設定すればエディタと連携してもっと便利に使えると思います。

以上です。

ドメインをgoogle domainsに移管してみた

きっかけ

放置気味だったDigial OceanからRebootが必要だからバックアップとかしてrebootしろ、という案内が来てた。 Digital Oceanは昔作ったままなのでvagrantでデプロイするような仕組みになってたし、tjun.org のブログの方は放置してたので、もう全部消そうと決意。 tjun.orgは最近使い慣れているGAEに移す方向で、その際DNSとかも直さなきゃということで、なんとなくGoogle Domainsに移すことにした。

移管の流れ

まずはGoogle Domainsの方で、Transfer Inのところで移管したいドメインを入れてみましょう。 未対応のトップレベルドメイン(jpなど)はGoogleDomainsの方で未対応なので移管できません、と言われます。

次に、転出元(自分の場合はさくらインターネット)で取得してたので、まずそちらで転出の準備をします。 ドメイン管理画面のところから、特に問題なく転出の手続きができました。メールが来るまで2-3日かかったと思います。

この際に、Admin のEmailを自分のemailに直してくれるのですが、GoogleDomainsへの移管の承認には Registrant Emailを自分で受け取って承認する必要があります。(ここがさくらのemailアドレスになっていた)  なので、以下の手順で Registrant Emailを変更します。

【JPRS管理】gTLDドメイン 公開情報の変更 – さくらのサポート情報

次に、Google Domainsの方で、Transfer Inのところでドメインを入れて、必要な Auth Codeを入れて、届いた承認メールを確認すれば移管できます。

費用

ちゃんと読んでいなかったけど、 1400円くらいかかりました。 たしか期間を1年延長して移管する、という形になっているので、これは手数料ではなくドメインの更新の費用と思います。ですので、ドメインによって料金は変わります。

特徴など

あまり把握していないですが、以下のような感想です

  • WhoIs情報をprivateにすると、名前も含めて保護されるので、安心感ある
  • ネームサーバは ns-cloud-*.googledomains.com で、これは Googleの Cloud DNSと同じらしい
  • なので、おそらく Cloud DNS相当のパフォーマンスや可用性がある
  • DNSSECなども管理画面で設定すれば利用可能なところも Cloud DNSと同等
  • 管理画面は、普通に使いやすい

簡単ですが以上です。

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アドレスを割り当てる

===201908追記===

今なら Cloud NAT 使うのがいいと思います

===追記おわり===

タイトルのとおりですが、こんなことする人はあまりいないと思います。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を指定する感じです。