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というのが発表されました。
- Google Cloud Platform Blog: Introducing managed SSL for Google App Engine
- Google App EngineでマネージドSSLが全ユーザーに無料提供、HTTPSの導入が簡単に。証明書の更新もGoogleにおまかせで心配無用 - Publickey
今までも証明書設定して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でルーティングをいい感じにしたい人にはぴったりです。
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を使って設定を書いていきます。
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
独自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}, }
ちょっと例が微妙ですが、users
とinfos
を同じindexでループを回したいようなことがあるかと思います。
goのtemplateでは、単独のループであれば
{{range $index, $user := Users}}
{{$user.Name}}
{{end}}
のように書けるのですが、もう一つの配列も同じようにループを回す場合には、以下のように書く必要があります。
{{range $index, $user := Users}}
{{$user.Name}}
{{(index $.Infos $index).Age}}
{{end}}
のindexを使って、ループ外部の変数の配列のindexを指定する感じです。