satococoa's blog

主にサーバーサイド、Web 系エンジニアのブログです。Go, Ruby, React, GCP, ...etc.

zenn.devを使ってみた

zenn.dev

技術的なメモは個人esa.ioだったりこのブログだったりに書いてきたのですが、せっかくなので多くの人の目に触れるところに置く方がメリットがあると考え今話題の zenn を遅ればせながら使ってみました。

エディタもシンプルで必要な機能は揃っている印象ですし、見た目もスッキリしていて好みです。 しばらく使ってみて良い感触だったら人の目に触れさせたい技術メモはzennに積極的に書いていこうと思います。

こっちのブログは読書メモとか個人的な雑記とかに使う感じの棲み分けかなと思います。

WordPressの書籍を読んだ

仕事でまれにプロダクト等の公式サイトを作りたい、という要件が発生します。

コーダーさんが会社にいるわけではないので、ほとんどが「デザインはデザイナさんアサインするから、あとは任せた!」パターンでサーバエンジニアのチームに話が降りてきます。 またそういうサイトはある程度エンジニアなしで更新できることが必須の要件となるため、必然的にWordPressが候補に上がるわけです。

なんとなくWordPressってエンジニアからは避けられがちなイメージもありますが、これだけ普及していてプラグインやテーマなどのエコシステムも強力なOSSなのでせっかくだからきちんと使いこなせるようになっておこう。。という気持ちでこの書籍を手に取りました。

結論から言うと、この本は僕のニーズにぴったりでした。

"デザイン教科書" と言う名前ではありますが、一言で言うとこういう内容です。 → "HTMLとCSSですでに組まれた静的サイトがあり、それをWordPressでよく使われる機能やプラグイン等を使ってCMS化する手順を一通り体験できる。"

HTML、CSS、JS、サーバやPHPのある程度の知識がある人がサクッとWordPressでWebサイトを作るための基本的な知識を速習するのに最適という印象です。 ボリューム感もお手頃で、ささっと斜め読みで2日で一通り読み終えました。

逆に以下についてはあまり詳しく買いていないので、この辺を知りたい人は別の視点から書かれた書籍の方がおすすめかもしれません。

  • PHPの知識 (本書も付録に多少解説はありますが。。)
  • HTML, CSS, JS の知識
  • WordPressのインストールやメンテナンスなどのサーバ運用に関わる知識

少しWordPressへの苦手意識というか嫌悪感がなくなったので、また次にWebサイト構築案件がきたらもうちょいポジティブな気持ちでWordPressにも向き合えそうです。

おまけ: ローカルでWordPressをインストールしてテーマ作成を行うためのdocker-compose.yml

version: "3"
services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    volumes:
      - ./wp-content:/var/www/html/wp-content
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DEBUG: 1
volumes:
  db_data:

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 をビルドする

この記事の続きです。

satococoa.hatenablog.com

よく考えると 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 だったら全部コンテナ上に乗せて開発する方法を検討すると思いますのでその辺はケースバイケースですね。。