Herokuでカスタムドメイン(ルートドメイン)を使うためにCloudFrontを設定する

Route53で取得したカスタムドメインをHerokuで使うためにCloudFrontの設定をしたのでブログに書いておく。

コード例は foo.herokuapp.com で動くアプリケーションを foo-example.com でアクセスできるようにする設定です。

AWS側の設定

  • CloudFrontで使うacmはvirginiaで作成
  • ttlは0にしてキャッシュしない
    • /assets/packs はキャッシュした方が良いけど、まだ未対応
locals {
  foo_domain    = "foo-example.com"
  foo_origin_id = "foo-heroku"
}

module "acm" {
  source  = "terraform-aws-modules/acm/aws"
  version = "2.12.0"

  providers = {
    aws = aws.virginia
  }

  domain_name = local.foo_domain
  zone_id     = aws_route53_zone.main.zone_id
}

resource "aws_cloudfront_distribution" "foo" {
  enabled         = true
  is_ipv6_enabled = true
  aliases         = [local.foo_domain]

  origin {
    domain_name = "foo.herokuapp.com"
    origin_id   = local.foo_origin_id

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1", "TLSv1.1", "TLSv1.2"]
    }
  }

  default_cache_behavior {
    target_origin_id       = local.foo_origin_id
    allowed_methods        = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
    cached_methods         = ["GET", "HEAD"]
    compress               = true
    default_ttl            = 0
    max_ttl                = 0
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      headers      = ["*"]
      query_string = true

      cookies {
        forward = "all"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = module.acm.this_acm_certificate_arn
    minimum_protocol_version = "TLSv1.2_2019"
    ssl_support_method       = "sni-only"
  }
}

Heroku側の設定

カスタムドメインを登録しておく。

$ heroku domains:add foo-example.com

git-notesでコミットにメモをつける

2020年に「コミットログは良くならない」というのを悟ったので、現実的な解決案である「git-notesでメモを残す」について記事を書いておきます。

前回の記事

sinsoku.hatenablog.com

git-notes

詳細は git notes --help を読んでください。

概要は以下の通りです。

  • コミットログとは別にメモを残せる
    • コミットはそのままなのでshaは変わらない
    • shaが変わらないのでCIの再実行が起きない
  • 他人のコミットにメモをつけられる
    • 他人に作業を依頼する必要がない
  • メモもリモートにプッシュできる
  • 過去のコミットにメモを残せる

使い方

メモを書く

git notes edit <sha> でメモを書くと、git log のときに一緒に表示される。

$ git notes edit d2cdf0b
$ git log -1 d2cdf0b
commit d2cdf0be675b44771f950697fc0b19ef0ea453f9
Merge: 25d156673e 0adcec4954
Author: Ryuta Kamizono <kamipo@gmail.com>
Date:   Wed Jun 17 20:29:47 2020 +0900

    Merge pull request #39612 from kamipo/faster_attributes

    PERF: 45% faster attributes for readonly usage

Notes:
    ActiveRrecordが速くなった。 #kamipoさんはすごい人

コミット権がないとプッシュできないですが、ローカルなら自由にメモを書ける。

メモを削除する

git notes remove <sha> で削除できます。

メモをリモートにプッシュ(フェッチ)する

$ git push origin refs/notes/commits
$ git fetch origin refs/notes/commits:refs/notes/commits

簡単にプッシュ(フェッチ)できるようにする

.git/config に以下の設定を追加する。

 [remote "origin"]
   url = git@github.com:sinsoku/dotfiles.git
   fetch = +refs/heads/*:refs/remotes/origin/*
+  fetch = +refs/notes/*:refs/notes/*
+  push = +refs/notes/*:refs/notes/*

これで普通に git fetchgit push できるようになります。

注意

.git/config に設定を追加すると git push origin の挙動が変わります。

  • 変更前: 現在のブランチをプッシュ
  • 変更後: notes をプッシュ

挙動が変わって困る人はエイリアスを使った方が良いです。

$ git config alias.push-notes 'push origin refs/notes/*'

参考ページ

Gitのプルリク(ブランチ)単位でログを追う方法

過去に「よいコミットメッセージとは」みたいな記事を書いたこともある。

sinsoku.hatenablog.com

なぜコミットメッセージは良くならないのか

どうしたらGitのコミットメッセージが良くなるか考えてみたけど、 他人に期待するには無理がある という結論に至った。

技術的な課題

  • Gitで rebase -i を使ってコミットを直すのが難しい
  • コミットメッセージを直すたびにCIが走る
    • デプロイが遅くなる

会社的な理由

  • 雑なコミットメッセージでもマージされるケースが多い
    • レビュワーはコミットメッセージまで読まない
  • コミットメッセージは評価(=給与)に繋がらない

コミットする人の心理

  • コミットメッセージなんて使わない
    • 雑なコミットする人は git-loggit-blame をそもそも使っていない
    • Slackで聞くなり、テレカンで相談すれば済むので

現実的な解決案

プルリクにだけは必ず情報を残すようにしておき、調査のときにコミットメッセージに期待することを諦める。

調べる側が工夫して、GitHubのプルリクを素早く開くなり、git-logでプルリク単位のdiffを閲覧できるようにするしかない。

例題

rails/railsリポジトリで、 ab8b12eaf625d7e7a1ebf589ff4f8dbf85b65b7c のコミットに紐づくプルリクを探す。

GitHubのプルリクを素早く探す

GitHubCLIツールである ghjq を使う。

$ gh api /repos/rails/rails/commits/ab8b12eaf625d7e7a1ebf589ff4f8dbf85b65b7c/pulls \
    -H "Accept: application/vnd.github.groot-preview+json" \
    | jq -r '.[].html_url'
https://github.com/rails/rails/pull/39612

gh pr view でPRの内容をすぐ読める。

$ gh pr view $(gh api /repos/rails/rails/commits/ab8b12eaf625d7e7a1ebf589ff4f8dbf85b65b7c/pulls \
  -H "Accept: application/vnd.github.groot-preview+json" \
  | jq -r '.[].number')

shaを含むブランチ単位の変更を調べる

これだけなので簡単に取れると思ったら、すごく難しかった。

$ git bisect start --no-checkout --first-parent origin/master ab8b12eaf625d7e7a1ebf589ff4f8dbf85b65b7c
$ git bisect run sh -c '! git merge-base --is-ancestor ab8b12eaf625d7e7a1ebf589ff4f8dbf85b65b7c BISECT_HEAD'
$ git diff BISECT_HEAD^-

git help gitrevisions を全部読んだけど、どうやっても プルリクのマージコミット を簡単に取得する方法がなくて、bisectする方法しか見つけられなかった。

これでプルリク(ブランチ)単位の差分をブラウザを使わずに読むことができる。

まとめ

Git初心者やデザイナーの方に rebase -i やコンフリクト解消を頼むのは厳しいので、コミットを良い感じにするのは大変だと思うんですよね。

Git力を上げて、調査する側が頑張るのが妥当なのかなーと。

CIでRailsのmasterブランチを使ってテストを実行する

Rails edgeでCIを動かしたい酔狂な人向けに記事を書いておく。

Rails edgeのGemfileを用意する

# gemfiles/rails_61.gemfile
eval_gemfile File.expand_path('../Gemfile', __dir__)

dependencies.delete_if { |d| d.name == 'rails' }
gem 'rails', github: 'rails/rails'

Rails edge用のGemfileを使う

BUNDLE_GEMFILE を指定することで、別ディレクトリにある Gemfile を使用できる。

$ BUNDLE_GEMFILE=gemfiles/rails_61.gemfile bundle install

CircleCIで定期的に実行する

Gemfile のときのキャッシュが効くように BUNDLE_PATH_RELATIVE_TO_CWD をつけている。

  rspec-edge:
    executor: rails
    environment:
      BUNDLE_GEMFILE: gemfiles/rails_61.gemfile
      BUNDLE_PATH_RELATIVE_TO_CWD: true
    steps:
      - checkout
      - run: cp Gemfile.lock gemfiles/rails_61.lock
      # あとは bundle-install して、テストを実行すれば良い

workflows:
  edge-rails:
    triggers:
      - schedule:
          cron: "0 0 * * 0" # At 00:00 on Sunday
          filters:
            branches:
              only:
                - master
    jobs:
      - rspec-edge:

gemでVERSION定数が定義されていない場合にgemのバージョンを取得する方法

ときどきVERSIONの定義されていないgemに遭遇するので、備忘録として書いておく。

VERSION の無いgem

いくつかのパターンがある。

バージョン情報をBundlerで取得する

Bundlerで取得すれば、確実に文字列のバージョン情報を得ることができる。

Bundler.definition.specs['database_rewinder'][0].version.to_s
#=> "0.9.4"

るりまの開発環境をDockerで作ってみた

はじめに

あとはVSCodeなどでコンテナにアクセスするなどして編集すればOKそうです(もしくはvimとか入れてコンテナ内で編集するとか)

gamelinks007.hatenablog.com

この記事を読んで「ホスト側で編集するようにできそう」と思ったので、試しに開発環境を作ってみた。

rurema/doctree のクローン

まず、Gitリポジトリをクローンします。

$ git clone https://github.com/rurema/doctree.git

Docker の設定

rurema/doctreeディレクトリ直下に Dockerfile を作ります。

FROM ruby:2.7.0

WORKDIR /home

RUN gem install --no-document bundler:1.16.1

COPY Gemfile ./
COPY Gemfile.lock ./
RUN bundle install

ENTRYPOINT ["bundle", "exec"]

次に docker-compose.yml を作ります。

version: "3.8"

services:
  rurema:
    build: .
    volumes:
      - .:/home
      - html-data:/home/_site
    environment:
      HTML_DIRECTORY_BASE: _site

  nginx:
    image: nginx:alpine
    volumes:
      - html-data:/usr/share/nginx/html:ro
    ports:
      - "80:80"

volumes:
  html-data:

htmlを誤ってコミットしないように、 .gitigreno に記載されていた _site にhtmlを出力するようにしてある。

htmlを生成する

$ docker-compose run rurema rake

複数のバージョンをビルドすると時間かかるので、普段はバージョンを指定してビルドした方が良いかも。

$ docker-compose run rurema rake statichtml:2.7.0

ブラウザでhtmlを確認する

$ docker-compose up nginx

http://localhost/2.7.0/ をブラウザで開くとhtmlを確認できる。

Railsで認証機能を自作する?それともDeviseを使う?

定期的にDevise批判の話が出てくるので、個人的な考えを書いてみます。

Railsに詳しくないなら、Deviseを使わないべきか?

認証自作、 Rails 、 Devise」の記事で以下のような記載がある。

「Rails について深い理解がないならば、 Devise は使うな」とあります。この方針は10 年近く前から書かれています。

これ元の英語とあってない気がするんですよね。

If you are building your first Rails application, we recommend you do not use Devise. Devise requires a good understanding of the Rails Framework. In such cases, we advise you to start a simple authentication system from scratch. Here's a few resources that should help you get started:

- Michael Hartl's online book: https://www.railstutorial.org/book/modeling_users
- Ryan Bates' Railscasts: http://railscasts.com/episodes/250-authentication-from-scratch and http://railscasts.com/episodes/250-authentication-from-scratch-revised
- Codecademy's Ruby on Rails: Authentication and Authorization: https://www.codecademy.com/learn/rails-auth

Once you have solidified your understanding of Rails and authentication mechanisms, we assure you Devise will be very pleasant to work with. 😃

私には「最初のRailsアプリではDeviseを使わず、シンプルな認証機能を自作して理解を深めるのを勧める」「Railsと認証メカニズムの理解を深めたら、Deviseが快適に動くのが分かるよ」くらいの内容に読めました。1

IPアドレスが漏れる危険性がある?

試してみた。

require "devise/version"
Devise::VERSION
#=> "4.7.2"
user = User.create(email: 'a@example.com', password: 'password')
user.to_json
#=> => "{\"id\":1,\"email\":\"a@example.com\",\"created_at\":\"2020-08-16T08:31:36.558Z\",\"updated_at\":\"2020-08-16T08:31:36.558Z\"}"
user.attributes.to_json
#=> "{\"id\":1,\"email\":\"a@example.com\",\"encrypted_password\":\"$2a$12$ECNX7kXJQKMSBp8xDcy5neVs/2NeuTdcqVnPyUbcy8fp.p4EWfBa6\",\"reset_password_token\":null,\"reset_password_sent_at\":null,\"remember_created_at\":null,\"sign_in_count\":0,\"current_sign_in_at\":null,\"last_sign_in_at\":null,\"current_sign_in_ip\":null,\"last_sign_in_ip\":null,\"confirmation_token\":null,\"confirmed_at\":null,\"confirmation_sent_at\":null,\"unconfirmed_email\":null,\"failed_attempts\":0,\"unlock_token\":null,\"locked_at\":null,\"created_at\":\"2020-08-16T08:31:36.558Z\",\"updated_at\":\"2020-08-16T08:31:36.558Z\"}"

attributesメソッドには全属性が含まれちゃうので、漏れる可能性はありそう。

ただ、これDeviseの問題なのかな...。
IPアドレスがDBに保存されている場合2、認証を自作してたとしても実装ミスると漏れるときは漏れると思うんですよね。

テストコードで防ぐ

Twitterはてブで「レビューで防ぐべき」とかあったけど、レビューのような人依存な対策だと漏れがあるので、テストコード書くのが良いかなと思いました。

# spec/rails_helper.rb
RSpec.configure do |config|
  # html/jsonに含まれてたらヤバそうな文字列を列挙しておく
  secrets = %w[password lastSignInIp]

  config.after(type: :request) do |ex|
    secrets.each { |secret| expect(response.body).not_to include(secret) }
  end
  config.after(type: :system) do |ex|
    secrets.each { |secret| expect(page.body).not_to include(secret) }
  end
end

カバレッジがある程度高ければ、上のコードでだいたい防げるんじゃないかなと。

機能が少ないので自作の方がメンテしやすい?

個人的な経験ですが、認証機能が少ないままで済んだ事がないです。

  • パスワードリセットが必要
  • パスワード失敗回数でロックしたい
  • KPIのためにログイン回数が知りたい
  • Facebook, Twitterログインが使いたい

のような要望が後から出ることが多い。

特に認証機能はプロジェクト初期に作られるためテストコードが不足していたり、誰も壊したくないので条件分岐が雑に増えていってAbcSizeが高くなる状況が生まれやすいです。

個人的な見解

認証機能を自作する自信がないなら、最初からDeviseを使っておくのが無難じゃないかなーと思ってます。

お金あるなら Deviseを使い慣れているフリーランス技術顧問 してる人と短期の契約をして、相談するのも良いと思います。

認証機能を自作するなら

少なくとも以下の調査はしておいた方が良いです。

  • Deviseのコード内にあるセキュリティ対策のコメントを調査する
    • timing attacks, session fixation attacks, ...etc
  • Deviseのリダイレクト関連の挙動を調査する
    • (認証が必要な)ページA => ログイン => ページA の画面遷移の処理
    • パスワードリセットのURLを踏んだときのリダイレクト先3
  • Deviseのテストヘルパーの挙動を調査する
    • HTTPリクエストを投げずにログイン状態を再現するので速い4
  • Deviseのモジュールと同じ要件がきた時のことを考慮する
    • アカウントロックとか後から出てきがち
  • 他gemが提供するDevise連携を使えない事を理解する

Deviseにあるような機能は将来に必要になる可能性が高いので、ある程度は考慮しておいた方が良いです。

あと、Deviseを避けて認証機能を自作した結果、Deviseが何年も前に対応済みの脆弱性を再実装するのは勿体ないです。

まとめ

Deviseが最高の認証ライブラリとは思ってないけど、十分に便利なライブラリだとは思ってます。

認証機能のセキュリティをちゃんと考えて実装できる人はさておき、Rails初心者〜中級者ならDeviseを使うでも良いのかなと。


  1. 英語力は自信ないので、各自でちゃんと英語を読んでほしい。

  2. アクセス解析、不正なログインの検知などでIPアドレスをDBに保存することはある。

  3. パスワードリセットURLを踏んで、自動ログイン…とか書かないように。メアド間違えたら、他人がログインできてしまう。

  4. Sorceryのテストヘルパーはリクエスト投げてて遅い🐢