goでjsonの一部のデータだけを後から型を指定して取り出したい
例えばAPIなんかでたまにあるこう言う形。 これを受け取った場合、共通処理としてまずはerrorがあるかどうかを見て、もしerrorがなかった場合にはdataをそのAPIに合わせた型にして取り出したいとする。
{ "error": null, "data": { "title": "fuga" } }
{ "error": null, "data": { "nickname": "foobar" } }
json.RawMessage
を使うと後からjsonの一部だけを型を指定して取り出すことができる。
まさに今回欲しかったものにぴったり。
golang.org
サンプルはこちら https://play.golang.org/p/e2lW0-15o3P
package main import ( "encoding/json" "fmt" "log" ) var userJSON = `{ "error": null, "data": { "nickname": "hoge" } }` var todoJSON = `{ "error": null, "data": { "title": "fuga" } }` type Response struct { Error *ResponseError `json:"error,omitempty"` Data json.RawMessage `json:"data,omitempty"` // <- User か Todo になる } type ResponseError struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` } type User struct { Nickname string `json:"nickname,omitempty"` } type Todo struct { Title string `json:"title,omitempty"` } func parseResponse(data []byte) (*Response, error) { res := &Response{} if err := json.Unmarshal(data, res); err != nil { return nil, err } if res.Error != nil { return nil, fmt.Errorf("error response: %v\n", res.Error) } return res, nil } func main() { // User APIにアクセスした userRes, err := parseResponse([]byte(userJSON)) if err != nil { log.Fatalf("parse error: %v", err) } user := &User{} if err := json.Unmarshal(userRes.Data, user); err != nil { log.Fatalf("error: %v", err) } fmt.Printf("user: %+v\n", user) // TODO APIにアクセスした todoRes, err := parseResponse([]byte(todoJSON)) if err != nil { log.Fatalf("parse error: %v", err) } todo := &Todo{} if err := json.Unmarshal(todoRes.Data, todo); err != nil { log.Fatalf("error: %v", err) } fmt.Printf("todo: %+v\n", todo) }
Private module を含む go サーバの Dockerfile
FROM golang as builder ENV GOFLAGS=-mod=vendor WORKDIR /work COPY go.* ./ RUN go mod download COPY . ./ RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o app FROM alpine:latest RUN apk --no-cache add ca-certificates COPY --from=builder /work/app /app CMD ["/app"]
事前に go mod vendor しておき、 ENV GOFLAGS=-mod=vendor
を付与することで private リポジトリを get しようとしない。
参考: github.com
mysql クライアントのみをインストールした状態で mysql2 gem をビルドする
この記事の続きです。
よく考えると mysql サーバをローカルにインストールする必要はないので、以下のようにやってみました。
brew uninstall mysql brew install mysql-client # mysql との競合を避けるため /usr/local/opt/mysql-client 以下に入るので、以下のようにオプションを調整 bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/mysql-client/lib --with-cppflags=-I/usr/local/opt/mysql-client/include"
これで入りました。
Firebase Authentication の ID トークンを Ruby で検証する
ID トークンを確認する | Firebase にあるように、Fireabse Authentication によって発行された ID トークンを正しく検証することにより、そのユーザの user_id を確認することができます。
Firebase Admin SDK が提供されていればそれを使うことで簡単に検証できるのですが、Ruby 版は提供されていないので Rails から使いたい場合などは自分で検証処理を書くことになります。
検証すべき内容は ID トークンを確認する | Firebase に書いてあるのでそれに沿って書いていきます。
要: JWT gem
# @see https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja # # Usage: # validator = FirebaseAuth::TokenValidator.new(token) # payload = validator.validate! # class FirebaseAuth::TokenValidator class InvalidTokenError < StandardError; end ALG = 'RS256' CERTS_URI = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com' CERTS_CACHE_KEY = 'firebase_auth_certificates' PROJECT_ID = 'YOUR_PROJECT_ID' ISSUER_URI_BASE = 'https://securetoken.google.com/' def initialize(token) @token = token end # # Validates firebase authentication token # # @raise [InvalidTokenError] validation error # @return [Hash] valid payload # def validate! options = { algorithm: ALG, iss: ISSUER_URI_BASE + PROJECT_ID, verify_iss: true, aud: PROJECT_ID, verify_aud: true, verify_iat: true, } payload, _ = JWT.decode(@token, nil, true, options) do |header| cert = fetch_certificates[header['kid']] if cert.present? OpenSSL::X509::Certificate.new(cert).public_key else nil end end # JWT.decode でチェックされない項目のチェック raise InvalidTokenError.new('Invalid auth_time') unless Time.zone.at(payload['auth_time']).past? raise InvalidTokenError.new('Invalid sub') if payload['sub'].empty? payload rescue JWT::DecodeError => e Rails.logger.error e.message Rails.logger.error e.backtrace.join("\n") raise InvalidTokenError.new(e.message) end private # 証明書は毎回取得せずにキャッシュする (要: Rails.cache) def fetch_certificates cached = Rails.cache.read(CERTS_CACHE_KEY) return cached if cached.present? res = Net::HTTP.get_response(URI(CERTS_URI)) raise 'Fetch certificates error' unless res.is_a?(Net::HTTPSuccess) body = JSON.parse(res.body) expires_at = Time.zone.parse(res.header['expires']) Rails.cache.write(CERTS_CACHE_KEY, body, expires_in: expires_at - Time.current) body end end
macOS Mojave にアップグレードしたら mysql2 gem のインストール時にエラーが発生した
手元の環境では High Sierra のときはこの辺の考慮をしなくてもインストールできていた気がするのですが、さっき新規に bundle install をしたら以下のようなエラーになってしまいました。
ld: library not found for -lssl clang: error: linker command failed with exit code 1 (use -v to see invocation) make: *** [mysql2.bundle] Error 1
brew でインストールした openssl が見つからないだけのようで、以下のような対応を行いました。(確か Sierra 以前の頃はこれやってた気がするんですが、なぜ High Sierra のときにはやらずにインストールできていたのだろう。。)
bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include"
参考: https://github.com/brianmario/mysql2/issues/1005
ちなみに今開発しているアプリのデプロイ先が App Engine Flexible Environment (ruby) なので、ruby や node.js をローカルにインストールしちゃう方法で開発を行なっています。 もちろんバージョンは揃えたい(開発者間での差異を無くしつつ、プロジェクトごとに違うバージョンを使えるようにしたい)ので、ruby や node は rbenv / nodenv を使い、MySQL 等のミドルウェアは dokcer-compose を使うようにしていて、コンテナとの付き合い方はこのぐらいの力の入れ具合がちょうどバランスいいかなと個人的には感じています。
とはいえローカルに開発環境を構築している関係上、今回のような個人の環境に依存する問題が発生してしまうことがありますし、実行環境が k8s だったり App Engine の custom runtime だったら全部コンテナ上に乗せて開発する方法を検討すると思いますのでその辺はケースバイケースですね。。
Angular で作った SPA を Docker の nginx を使って動かす
このサイトにある手順をまるっと使わせていただきました。
追加したのは以下の3ファイル
.dockerignore
node_modules
nginx.conf
server { listen 80; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html =404; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
Dockerfile
FROM node as build-stage WORKDIR /app COPY package*.json /app/ RUN npm install COPY ./ /app/ ARG configuration=production RUN npm run build -- --output-path=./dist/out --configuration $configuration FROM nginx COPY ./nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build-stage /app/dist/out/ /usr/share/nginx/html
以下の手順でビルド & 公開
$ docker build -t <IMAGE_TAG> . $ docker run -p 8080:80 <IMAGE_TAG>
これで localhost:8080 でアクセスできる。
一般公開だったら Firebase Hosting とかで良いのですが、今回は社内イントラ内での公開だったのでこのようにしました。
Kubernetes の CronJob で bundle update して PR 投げてくれるようにした
bundle update を定期的に実行し差分がある場合は pull request を投げる、というよくある仕組みを k8s の cronjob で実装しました。
clone ( or pull ) -> 何らかの処理を行う (ここでは bundle update) -> [差分があったら] -> commit & push -> Pull Request を出す、という処理は今回の bundle update に限らず汎用的に使えるものだと思うので、コマンドラインツールとして Go で実装しました。
あとは任意のコンテナの中で上記 prbot のバイナリを取得して実行すれば OK。
以下のような manifest を書きます。 Clone や PullRequest の送信に GitHub の Personal Access Token が必要になるので取得して Secrets として登録しておいてください。
apiVersion: batch/v1beta1 kind: CronJob metadata: name: bundle-update spec: schedule: "0 2 * * WED" # cron式 jobTemplate: spec: template: spec: restartPolicy: Never containers: - image: library/ruby name: ruby command: [ "/bin/sh" ] args: - "-c" - |2 set -e wget https://github.com/satococoa/prbot/releases/download/v${PRBOT_VERSION}/prbot_${PRBOT_VERSION}_linux_amd64.tar.gz tar -xvzf prbot_${PRBOT_VERSION}_linux_amd64.tar.gz mv prbot /bin/prbot chmod +x /bin/prbot gem install bundler prbot env: - name: PRBOT_VERSION value: "0.1.2" - name: GITHUB_REPOSITORY value: "org/repository" - name: GITHUB_ACCESS_TOKEN valueFrom: secretKeyRef: name: bundle-update-secret key: githubAccessToken - name: BASE_BRANCH value: "master" - name: COMMAND value: "bin/bundle update" - name: TITLE value: "pull request のタイトル" - name: AUTHOR_NAME value: "prbot" - name: AUTHOR_EMAIL value: "prbot@example.com"
以上です。 使うイメージについては native extension を含む gem なんかが含まれる場合は library/ruby じゃビルドできないかもしれないので、実際にアプリケーションを動かすコンテナで実行する方が確実だと思います。 library/ruby は buildpack-deps を基に作られていて mysql-client なんかは入ってるため、僕の場合は library/ruby で大丈夫でした。
学び
- GoReleaser が超便利。
- マルチプラットフォーム向けにビルドして バイナリを GitHub の Release にアップロードして、さらに Docker イメージとか Homebrew 用のレシピとかも作れちゃう。
- buildpack-deps という Docker image を知れた。