satococoa's blog

Web や iOS アプリを作るエンジニアの日記です

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

Angular で作った SPA を Docker の nginx を使って動かす

このサイトにある手順をまるっと使わせていただきました。

medium.com

追加したのは以下の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 で実装しました。

github.com

あとは任意のコンテナの中で上記 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/rubybuildpack-deps を基に作られていて mysql-client なんかは入ってるため、僕の場合は library/ruby で大丈夫でした。

学び

  • GoReleaser が超便利。
    • マルチプラットフォーム向けにビルドして バイナリを GitHub の Release にアップロードして、さらに Docker イメージとか Homebrew 用のレシピとかも作れちゃう。
  • buildpack-deps という Docker image を知れた。

複数の pod で並列に kubectl exec を実行する

こんな感じで OK

kubectl get pods -l name=app -o jsonpath="{.items[*].metadata.name}" | xargs -n1 -P4 -I{} kubectl exec {} bin/rails r 'Rails.logger.info(Rails.env)'

xargs の P オプションの値は最大プロセス数。

参考: JSONPath Support kubernetes.io

Rails 5.1 で rails test と rails test:system の RAILS_ENV に関する挙動の違いメモ

RAILS_ENV が指定されている場合 rails test コマンドと rails test:system コマンドの挙動が違う。 おそらくそれぞれ rails command として実装されているか、rake タスクとして実装されているかによる違い。

  • rails test で実行 → RAILS_ENV が test に上書きされて実行される
  • rails test:system で実行 → RAILS_ENV に指定された環境で実行される
    • rails test:db も同様

docker-compose.yml で RAILS_ENV=development としていたコンテナでシステムテストを実行して気づいた。 今はテスト実行のコマンドを rails test test/* とすることで回避している。 (ちなみに rails testシステムテストを実行しないのは rails の意図した動作である。)

これは rails の意図した動作なのか、それとも意図していない動作なので rails/rails を直すべきなのか、またはコンテナに RAILS_ENV を指定しないべきなのか判断がつかなかったけれども、あとで忘れるといけないのでメモだけ残す。