2019年 07月の投稿を表示しています

Google Cloud Build + kaniko を試す

Cloud Build のドキュメントを見ていたら、ビルドを高速化するのに kaniko を使うソリューションがあるよ、と書かれていたので、試してみる。

Kaniko キャッシュの使用 | Cloud Build | Google Cloud
GitHub - GoogleContainerTools/kaniko: Build Container Images In Kubernetes

kaniko っていうのはすごいざっくりと理解したのは Docker なしに k8s 上で Docker イメージをビルドできるよ、というもの。
で、そのとき対象の Dockerfile に関して、レイヤーごとに全部キャッシュを取ってくれるので、差分があった部分以降をビルドするだけになり docker build よりも高速化できる、という話っぽい。通常 docker build するときも、レイヤーごとの情報を持っていて、変わった部分以降をビルドすることができる。できるが、そのキャッシュのバックエンドとか、マルチステージにしたときのビルドステップの進め方とかをいい感じにしたっていう話なんじゃあないかなあ。(それ以上の理解はできていない)

さて、ビルドする対象となる Dockerfile はこういう具合。
マルチステージで go で書かれたアプリケーションサーバのビルド、からのそのアプリケーションコンテナのイメージを作る。

FROM golang:1.12 as builder
ARG GITHUB_TOKEN
ARG VERSION
ENV GO111MODULE=on

WORKDIR /go/src/github.com/foo/bar

COPY go.mod go.sum ./
RUN echo "machine github.com login ${GITHUB_TOKEN}" > ~/.netrc
RUN go mod download

COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go install -v -ldflags "-X main.version=${VERSION}" github.com/foo/bar/cmd/server

FROM alpine:latest

RUN apk add --update --no-cache ca-certificates tzdata
COPY --from=builder /go/bin/server /bin/server
CMD ["/bin/server"]

それでもとの cloudbuild.yaml はこうなっている。
変数をバケツリレーしていく。

steps:
- name: gcr.io/cloud-builders/docker
  args:
  - build
  - -t
  - $_IMAGE
  - --build-arg
  - GITHUB_TOKEN=$_GITHUB_TOKEN
  - --build-arg
  - VERSION=$_VERSION
  - -f
  - $_DOCKERFILE_PATH
  - .
images:
  - $_IMAGE

で、これを kaniko に対応させるとこうなる。

steps:
- name: 'gcr.io/kaniko-project/executor:latest'
  args:
  - --destination=$_IMAGE
  - --dockerfile=$_DOCKERFILE_PATH
  - --cache=true
  - --cache-ttl=6h
  - --build-arg=GITHUB_TOKEN=$_GITHUB_TOKEN
  - --build-arg=VERSION=$_VERSION

その他 GCP 設定や Dockerfile などなどはそのままで実行することはできた。

できたが結果はちょっと微妙で、めっちゃ早くなったかというとそうでもない。。。

ビルド時、主に時間がかかるのが go mod download と go install (実際は go build が時間かかる)のふたつで、アプリケーションのコードを変えただけの場合は go mod のキャッシュが効いてくれるが、レイヤーを持ってくる(?)のにやっぱり時間がかかるので、プラスマイナスみて、ややマイナス、みたいな状態。。

具体的な時間としては kaniko を使う前の元々ので 5 分くらい。 kaniko してフルキャッシュ状態(再実行しただけ)で 1-2 分。 go mod のキャッシュが効いてて 2-4 分くらい。キャッシュがまったくなし = go.mod が変更された場合で 5-7 分くらい。ちゃんと測ってなくて数回ためした程度で、実はもっと早いかもしれない。そこは試す余地がある。

あとは単純にお財布で戦って、マシンサイズを上げて強い子にすると早くなるんじゃないかなあ~。


そもそも、コンテナイメージの作成を早くするのが目的なら CI 上でクロスコンパイルしたバイナリを Docker にいれちゃうのがよさそう。
というか CI 上でも golang:1.12 なイメージでビルドするならば別にクロスコンパイルでもないのか。

マルチステージな Dockerfile でアプリケーションのビルドもまるっと!と Cloud Build でビルドすると、余計なゴミが入りにくい、という良い点はありそう。

とはいえ、ワークスペースをアタッチしない、かつ go mod だけ共有してビルドしちゃえば Cloud Build でやるのとほぼ同じ環境になりそうな気がする。そうしてできたバイナリをコンテナ内に COPY して、コンテナレジストリに登録しちゃえばいいんじゃないかな。

WordPress 上で定義した API に nonce による制約を入れる

wp_verify_nonce() | Function | WordPress Developer Resources

wp_create_nonce() | Function | WordPress Developer Resources

wp_create_nonce をしたら nonce な値が生成されるが WordPress にログインされているユーザによって異なる、というのがちょっと気になった。未ログインだと未ログインユーザとして一緒の扱いになり、未ログインでも〜〜みたいな制約をつけようとしたらちょっと弱い感じがする。

例えばリクエスト元 IP(スマホなんかだとコロコロ変わるので難しい)だったり UserAgent(被る可能性が高い)だったり、なんとかフロントエンド、クライアント側にユニークになるような値を作ってもらって nonce を生成するのがよさそう。とはいえ、未ログインユーザとして出来ること、持っている権限は、異なる未ログインユーザであっても基本的には同じになるだろうので、なんか、まあ、やりたいことに合わせて、よしなにしたらいいんじゃないかなあ………。

前回の API を作る記事にあわせて nonce を作って、検証する API も書いてみるとこんな感じ。

class PostAPI
{
    private $namespace;
    private $nonceKey;

    public function __construct(string $namespace, string $nonceKey)
    {
        $this->namespace = $namespace;
        $this->nonceKey = $nonceKey;
        $this->registerNonce();
    }

    protected function registerNonce()
    {
        register_rest_route(
            $this->namespace,
            'nonce',
            [
                'methods' => 'POST',
                'callback' => function(WP_REST_Request $req) {
                    $response = new WP_REST_Response;
                    $response->set_status(200);
                    $response->set_data([
                        'result' => 'success',
                        'nonce' => wp_create_nonce($this->nonceKey),
                    ]);
                    return $response;
                },
            ]
        );
    }

    public function register(string $endpoint, \Closure $handler)
    {
        register_rest_route(
            $this->namespace,
            $endpoint,
            [
                'methods' => 'POST',
                'callback' => function (WP_REST_Request $req) use ($handler) {
                    if (!wp_verify_nonce($req['nonce'], $this->nonceKey)) {
                        $response = new WP_REST_Response;
                        $response->set_status(401);
                        $response->set_data([]);
                        return $response;
                    }

                    return $handler($req);
                },
            ]
        );
    }
}

add_action('rest_api_init', function() {
    $api = new PostAPI('myapi/v1', 'myapi');
    $api->register('/say', function(WP_REST_Request $req) {
        $message = $req['message'];

        $response = new WP_REST_Response;
        $response->set_status(200);
        $response->set_data([
            'result' => 'success',
            'message' => $message,
        ]);
        return $response;
    });
});

うん、うごいていそうだ。 

$ curl -X POST -H 'Content-Type:application/json' -D - '.../wp-json/myapi/v1/say?message=hello'
HTTP/2 401
...

[]


$ curl -X POST -H 'Content-Type:application/json' -D - '.../wp-json/myapi/v1/nonce'
HTTP/2 200
...

{"result":"success","nonce":"a0519f891d"}


$ curl -X POST -H 'Content-Type:application/json' -D - '.../wp-json/myapi/v1/say?message=hello&nonce=a0519f891d'
HTTP/2 200
...

{"result":"success","message":"hello"}

WordPress 上で API を新たに定義する

WordPress で API を定義するには WordPress の REST API の仕組みに乗れる register_rest_route というのを使う。
メソッドも処理もその他差し込み動作も指定し放題。

テンプレート下に php おいて$_POST でできます!はまちがい。これだと WordPress がもってる様々な恩恵に授かれない。

ちなみに環境によっては WordPress のプラグインで API を指定して無効にしたりするものが入っているかもしれないが register_rest_route した API はちゃんと出てくる、はず…。

さてこの register_rest_route の公式説明をみると、プラグイン名で名前をつけろ、とある。
register_rest_route() | Function | WordPress Developer Resources

特に配布するようなものでないとか規模が小さいとかなら api/v1 とかつければいいと思う。
機能や振る舞いに合わせてちゃんと名付けするのがいいとは思うけど。

というわけで基本形の使い方はこういう感じ。
クロージャーでシュッと書けばいいし、別にラップするクラスを作るなどしてよしなにしてもいいのではと思った。

add_action('rest_api_init', function() {
    register_rest_route( 'myapi/v1', '/foo', [
        'methods' => 'POST',
        'callback' => function(WP_REST_Request $req) {
            $message = $req['message'];

            $response = new WP_REST_Response;
            $response->set_status(200);
            $response->set_data([
                'result' => 'success',
                'message' => $message,
            ]);
            return $response;
        },
    ]);
});

クラスで簡単にラップする、例えばこういう感じ。やり方は無限。

class PostAPI
{
    private $namespace;

    public function __construct(string $namespace)
    {
        $this->namespace = $namespace;
    }

    public function register(string $endpoint, \Closure $handler)
    {
        register_rest_route(
            $this->namespace,
            $endpoint,
            [
                'methods' => 'POST',
                'callback' => $handler,
            ]
        );
    }
}

add_action('rest_api_init', function() {
    $api = new PostAPI('myapi/v2');
    $api->register('/foo', function(WP_REST_Request $req) {
        $message = $req['message'];

        $response = new WP_REST_Response;
        $response->set_status(200);
        $response->set_data([
            'result' => 'success',
            'message' => $message,
        ]);
        return $response;
    });
});

リクエストパラメータを受け取るには、$req が WP_REST_Request になっていて、これを使う。
WP_REST_Request | Class | WordPress Developer Resources

ArrayAccess の機能を持っていて、パラメータをよしなに取得できるようになっていてお手軽度が高い。

そしてレスポンスは WP_REST_Response を使う。
これもお手軽度が高く、勝手に json フォーマットに変換してくれたりする。
WP_REST_Response | Class | WordPress Developer Resources

こうして作った API は、とくに認証や制限を書けていないならすぐにでも curl や Javascript での Ajax から呼び出すことができる。

$ curl -X POST -H 'Content-Type:application/json' '{{WordPress url}}/wp-json/myapi/v1/foo?message=its_query'
{"result":"success","message":"its_query"}

$ curl -X POST -H 'Content-Type:application/json' -d '{"message":"its json"}' '{{WordPress url}}/wp-json/myapi/v1/foo'
{"result":"success","message":"its json"}

こういう具合。

grpc-go の interceptor を理解した

元実装はここ
GitHub - grpc/grpc-go: The Go language implementation of gRPC. HTTP/2 based RPC

Interceptor の例としてはここ
GitHub - grpc-ecosystem/go-grpc-middleware: Golang gRPC Middlewares: interceptor chaining auth logging retries and more.

要はなにかというと Laravel をやっていたときに使っていた Middleware と自分では理解した。
Middleware - Laravel - The PHP Framework For Web Artisans

リクエストを処理する前後に、任意の処理を差し込めるもの。

リクエストを中断したり、ウェイトしたり、前処理したり。
あるいはリクエスト処理の後に、かかった時間やステータスについてのメトリクスを取ったり、といったことができる。

これはサーバだけでなく、クライアント側にも差し込めるので、クライアントリクエストする前に何処に対してリクエストしようとしているか、とか、なんか色々できそうではある。

自分でも書いてみる。

func AccessLogUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		clientIP := "unknown"
		if p, ok := peer.FromContext(ctx); ok {
			clientIP = p.Addr.String()
		}

		ua := ""
		if md, ok := metadata.FromIncomingContext(ctx); ok {
			if u, ok := md["user-agent"]; ok {
				ua = strings.Join(u, ",")
			}
		}

		ctxzap.AddFields(
			ctx,
			zap.String("access.clientip", clientIP),
			zap.String("access.useragent", ua),
		)

		return handler(ctx, req)
	}
}

これは UnaryServerInterceptor で、つまりは単一リクエスト用のもの。
peer を使ってリクエスト元 IP と Metadata に入っている UserAgent を取得し、それを ctxzap を使って追加フィールドとして設定しておくもの。

なので go-grpc-middleware の grpc_zap.UnaryServerInterceptor が設定されていないと使えない。(そこで ctxzap が設定されるので)
go-grpc-middleware/logging/zap at master · grpc-ecosystem/go-grpc-middleware · GitHub

peer はここ。リクエスト元についての情報が簡単に入っている。
grpc-go/peer at master · grpc/grpc-go · GitHub

Metadata についてはここ。ざっくりとは HTTP Header みたいなやつ、と理解したけど大体あってそう。
grpc-go/grpc-metadata.md at master · grpc/grpc-go · GitHub

ctxzap は先に出した go-grpc-middleware で定義されるもの。
go-grpc-middleware/logging/zap/ctxzap at master · grpc-ecosystem/go-grpc-middleware · GitHub

gRPC リクエストの context に zap.Logger を出し入れできるもので、リクエストごとに Logger が設定されているような状態にできる。
ので、例えばリクエストの認証情報を詰めたりとかできて便利ちゃん。

実際に組み込もうとするとこんな感じ。

func NewGRPCServer() *grpc.Server {
	return grpc.NewServer(
		grpc_middleware.WithUnaryServerChain(
			grpc_zap.UnaryServerInterceptor(zap.L()),
			interceptor.AccessLogUnaryServerInterceptor(),
		),
	)
}

で、この interceptor を使った gRPC サーバに対してリクエストを送ってみると、こんな感じにログがでる。よさそう

{
  "level": "info",
  "ts": 1562940752.853247,
  "caller": "zap/server_interceptors.go:40",
  "msg": "finished unary call with code OK",
  "grpc.start_time": "2019-07-12T23:12:32+09:00",
  "system": "grpc",
  "span.kind": "server",
  "grpc.service": "helloworld.Greeter",
  "grpc.method": "SayHello",
  "access.clientip": "127.0.0.1:65516",
  "access.useragent": "my-user-agent/1.0.0 grpc-go/1.19.1",
  "grpc.code": "OK",
  "grpc.time_ms": 0.3019999861717224
}

ちなみにクライアントから UserAgent をどうやって設定するかはこうする。

client, err := grpc.DialContext(ctx, address, grpc.WithUserAgent("my-user-agent/1.0.0"), ...)

後ろに grpc-go/1.19.1 的な文言がついてしまうのはそうなんだけど、まあ、いいんじゃないかな。


あと余談なのだけど grpc-go と go-grpc-middleware とで grpc と go が入れ替わってていつもどっちだ!?!?となる


ワンソースでドッと試したのはこんなかんじ。
example にある helloworld な proto をそのまま利用するとお手軽簡単に gRPC 周辺の様子みれて便利。

package main

import (
	"context"
	"net"
	"strings"
	"time"

	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
	"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/peer"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	ctxzap.Extract(ctx).Info("Received", zap.Any("in.name", in.Name))
	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
	port := "127.0.0.1:13000"

	l, _ := zap.NewDevelopment()
	zap.ReplaceGlobals(l)

	// server
	go func() {
		logger := zap.L().Named("Server")

		listener, err := net.Listen("tcp", port)
		if err != nil {
			logger.Error("failed to listen: %v", zap.Error(err))
		}
		defer listener.Close()

		s := grpc.NewServer(
			grpc_middleware.WithUnaryServerChain(
				grpc_zap.UnaryServerInterceptor(logger),
				accessLogUnaryServerInterceptor(),
			),
		)
		defer s.Stop()

		pb.RegisterGreeterServer(s, &server{})

		if err := s.Serve(listener); err != nil {
			logger.Error("failed to serve: %v", zap.Error(err))
		}
	}()

	// client
	go func() {
		time.Sleep(2 * time.Second)
		ctx := context.Background()
		logger := zap.L().Named("Client")

		client, err := grpc.DialContext(
			ctx,
			port,
			grpc.WithUserAgent("my-user-agent/1.0.0"),
			grpc.WithInsecure(),
		)
		defer client.Close()

		if err != nil {
			logger.Error("failed create client", zap.Error(err))
		}

		greeterClient := pb.NewGreeterClient(client)

		logger.Info("Do request")

		response, err := greeterClient.SayHello(ctx, &pb.HelloRequest{Name: "sters"})

		logger.Info("", zap.Any("response.Message", response.Message), zap.Error(err))
	}()

	time.Sleep(5 * time.Second)
	zap.L().Info("Shutdown")
}

func accessLogUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		clientIP := "unknown"
		if p, ok := peer.FromContext(ctx); ok {
			clientIP = p.Addr.String()
		}

		useragent := ""
		if md, ok := metadata.FromIncomingContext(ctx); ok {
			if ua, ok := md["user-agent"]; ok {
				useragent = strings.Join(ua, ",")
			}
		}

		ctxzap.AddFields(
			ctx,
			zap.String("access.clientip", clientIP),
			zap.String("access.useragent", useragent),
		)

		return handler(ctx, req)
	}
}

最近パッケージ読み込み出来るようになったって聞いたので The Go Playground で実行できる。
ココ にある

nginx のアクセスログを LTSV にする

アクセスログ LTSV が便利だよって聞いたので設定するだけ設定した。
活用するのはこれから。

そもそも LTSV って? Labeled Tab-separated Values (LTSV)

Labeled な TSV だそうで、タブ区切りで処理するときにラベル付いてるので 1 番目が〜2 番目が〜みたいなことをしなくて便利。

そのサイトにもやり方がざっくり書いてあるが、いったん全部入りでこうした。
量多すぎて辛かったら一部捨てる感じで。

log_format main "\ttime:$time_local"
    "\thost:$remote_addr"
    "\tforwardedfor:$http_x_forwarded_for"
    "\tuser:$remote_user"
    "\treq:$request"
    "\tmethod:$request_method"
    "\turi:$request_uri"
    "\tprotocol:$server_protocol"
    "\tstatus:$status"
    "\tsize:$body_bytes_sent"
    "\treqsize:$request_length"
    "\treferer:$http_referer"
    "\tua:$http_user_agent"
    "\tvhost:$host"
    "\treqtime:$request_time"
    "\tcache:$upstream_http_x_cache"
    "\truntime:$upstream_http_x_runtime"
    "\tapptime:$upstream_response_time"
    "\tmsec:$msec"
    "\tproxy_add_x_forwarded_for:$proxy_add_x_forwarded_for"
    "\tscheme:$scheme"
    "\tstatus:$status"
    "\tupstream_addr:$upstream_addr"
;
access_log  /var/log/nginx/access.log  main;

Confuluence を Markdown で書く

Confuluence の世界に Markdown で書いてたものを持っていくとか Markdown でサクッと書きたいなあ、などというのが微妙に不便しているので、誰かベストソリューションを教えてほしい。

Markdown for Confluence | Atlassian Marketplace

これは Confuluence のページに Markdown なコンテンツを埋め込める、というもの。
Markdown で Confuluence のページを書けるといえばそう。けど、これだと Markdown の世界と Confuluence の世界がつながらないので、ちょっと微妙…、かも?

Confluence Wiki マークアップ - アトラシアン製品ドキュメント

マークアップ機能はでっかい Markdown や、やや処理にクセがあるっぽく、うまく出来ないときの修正の仕方がいまいちわからない。
ただ Confuluence Wiki 記法はうまく処理されるっぽい。
というわけで Markdown から Confuluence Wiki 記法にするそれっぽいのを書いてみて、手元だけはすこし楽できるようにしてみた。

GitHub - sters/md2cw: Markdown syntax to Confluence Wiki syntax

converter を見てもらうとわかるけれど Markdowm のパーサーやレンダラーとして動いてくれる blackfriday というのがある。

GitHub - russross/blackfriday: Blackfriday: a Markdown processor for Go

ぼくはそれぞれの要素が Confuluence でいうどれにあたるかを書いたくらいしかしていないくらいに、世の中は便利。


git(というよりは svn が近いか)のそれみたいに、プル、プッシュ、マージが出来るようなクライアントとかあってもいいかもなーとか思った。
そうなるともはや Confuluence である必要はそんなにないかもしれないけれども。。


それはそうとして、このリポジトリ、go getでうまくいれられないくない?(日本語崩壊)
go modulesとgo getのお気持ちがちょっとまだわからない…