Rubyをインストールせず Docker だけを使ってrails newを実行する

rails new するときにDockerfileを使う必要は特にない。

むしろ開発時に使うDockerfileとは別物になるので、Dockerfileを作らない方が良いです。

コマンド

$ mkdir example_app
$ cd example_app
$ docker run --rm -v $(pwd):/app -w /app ruby:3.0.0 bash -c '\
    curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
    && apt-get update && apt-get install -y git nodejs \
    && npm install -g yarn \
    && gem i --no-document rails \
    && rails new .'

解説

--rm

一時的な実行でコンテナを残しておく必要がないのでつけてる。

-v $(pwd):/app -w /app

ホストPCのディレクトリをコンテナ内の /app にマウントし、そのディレクトリを作業ディレクトリに指定する。

ruby:3.0.0 bash -c '...'

rubyのイメージのbashを使ってコマンドを実行する。

curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get update && apt-get install -y git nodejs \
&& npm install -g yarn \

rails newすると自動的にbundle installyarn installが実行されるため、必要になるパッケージの類いを追加する。

gem i --no-document rails \
&& rails new .

railsのgemを追加した後、rails new . で作業ディレクトリ(/app)にファイルを作成する。

開発環境で使うDockerfileについて

開発時に使うDockerfileとdocker-compose.ymlは前回に記事を書いてある。

sinsoku.hatenablog.com

最後に

たぶんRubyを入れて、普通に rails new した方が楽だと思う。

Railsアプリの開発環境向けDockerfile + docker-compose.yml

人に説明するときに記事あると便利なので、開発環境向けのDockerfileとdocker-compose.ymlを書いておく。

Dockerfile

FROM ruby:3.0.0

WORKDIR /app

# Using Node.js v14.x(LTS)
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash -

# Add packages
RUN apt-get update && apt-get install -y \
      git \
      nodejs \
      vim

# Add yarnpkg for assets:precompile
RUN npm install -g yarn

# Add Chrome
RUN curl -sO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
    && apt install -y ./google-chrome-stable_current_amd64.deb \
    && rm google-chrome-stable_current_amd64.deb

# Add chromedriver
RUN CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` \
    && curl -sO https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip \
    && unzip chromedriver_linux64.zip \
    && mv chromedriver /usr/bin/chromedriver
  • git: GitHubを参照してgemを入れるときに必要
  • nodejs + yarn: bin/rails assets:precompile で必要
  • vim: bin/rails credentials:edit で使う
  • Chrome + chromedriver: System Test で必要

docker-compose.yml

version: "3.8"
services:
  db:
    image: postgres:12-alpine
    volumes:
      - postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_HOST_AUTH_METHOD: trust

  redis:
    image: redis:5-alpine
    volumes:
      - redis:/data

  web: &web
    build: .
    image: app:1.0.0
    stdin_open: true
    tty: true
    volumes:
      - .:/app:cached
      - bundle:/app/vendor/bundle
      - node_modules:/app/node_modules
      - rails_cache:/app/tmp/cache
      - packs:/app/public/packs
      - packs_test:/app/public/packs-test
    tmpfs:
      - /tmp
    environment:
      BUNDLE_PATH: "/app/vendor/bundle"
      BOOTSNAP_CACHE_DIR: "/app/vendor/bundle"
      WD_INSTALL_DIR: "/usr/local/bin"
      HISTFILE: "/app/log/.bash_history"
      EDITOR: "vi"
      DATABASE_URL: "postgres://postgres:postgres@db:5432"
      REDIS_URL: "redis://redis:6379/"
      RAILS_MASTER_KEY:
    depends_on:
      - db
      - redis
    command: ["bin/rails", "server", "-b", "0.0.0.0"]
    expose: ["3000"]
    ports: ["3000:3000"]
    user: root
    working_dir: /app

  worker:
    <<: *web
    command: ["bundle", "exec", "sidekiq"]
    expose: []
    ports: []

volumes:
  postgres:
  redis:
  bundle:
  node_modules:
  rails_cache:
  packs:
  packs_test:
image: app:1.0.0

imageに名前とバージョンをつけておくことで、バージョンをインクリメントすると docker-compose run のときに自動的にビルドが走る。

Dockerfileを変えたときにバージョンをインクリメントすることで、他の開発者が古いイメージを使い続けてしまう事を防ぐことができる。

stdin_open: true
tty: true

byebug などでデバッグできるようにするため。

volumes:
  - .:/app:cached
  - bundle:/app/vendor/bundle
  - node_modules:/app/node_modules
  - rails_cache:/app/tmp/cache
  - packs:/app/public/packs
  - packs_test:/app/public/packs-test

Docker for Mac遅い問題の対処。

ホストとの同期を減らすことで若干マシになる。遅いけど。

worker:
  <<: *web
  command: ["bundle", "exec", "sidekiq"]
  expose: []
  ports: []

sidekiqを使う場合、基本的な設定はwebと同じなのでエイリアスを使う。

expose, portsはworkerで不要なので上書きする。

使い方

初回の環境構築

Dockerイメージを作成して、 bin/setup を実行する。

$ docker-compose build
$ docker-compose run --rm web bin/setup

サーバの起動

$ docker-compose run --rm --service-ports web

実行すると http://localhost:3000 でアクセスできる。

Railsコマンド類の実行

$ docker-compose run --rm web bin/rails -T

テストの実行

$ docker-compose run --rm -e RAILS_ENV=test web bin/rails db:test:prepare
$ docker-compose run --rm -e RAILS_ENV=test web bin/rails test

コンテナ内で作業する

毎回コンテナの起動をすると遅いので、基本的にコンテナ内で作業した方が楽。

$ docker-compose run --rm web bash
root@b419978e89ec:/app# bin/rails --version
Rails 6.1.3

全てのvolumeを削除する

開発環境を一新したいときに使う。

$ docker-compose down --volumes

追加の説明

Dockerfileでbundle installは不要なのか?

Dockerイメージのビルドでは不要です。

bin/setup の実行で、マウントしてるvolumeにインストールされます。

なぜdocker-compose up webで起動しないのか?

docker-compose up で起動すると標準入力が使えなくなって、byebugでデバッグできなくなるため。

あと、コンテナを止めたときに稀にtmp/pids/server.pidが残ることがある。*1

コマンドが長い

bibendi/dipを使うか、bashエイリアスを使うと楽です。

開発用のイメージが大きい

alpineやmulti stage buildを使えば軽量化できるかもしれないですが、面倒だったので調べてないです。

開発環境用のDockerイメージが少し重くても、大して困らないため。

*1:原因の詳細は調べられていないですが、 docker-compose run では今まで起きていない。

assets:precompileの結果を1世代だけ残す

Sprocketsは assets:clean[0] しても1時間以内に作成したassetsは消してくれません。

参照: https://github.com/rails/sprockets/blob/v4.0.2/lib/sprockets/manifest.rb#L245

さらに assets:clean の後に webpacker:clean を実行してくれるけど、引数は伝搬してくれない。

参考: https://github.com/rails/webpacker/blob/v6.0.0.pre.2/lib/tasks/webpacker/clean.rake#L19-L21

webpacker:clean に引数を伝搬させる

webpackerが登録してるProcを削除し、 enhance でタスクを登録し直す。

# Rakefile

assets_clean = Rake::Task['assets:clean']
assets_clean.actions.reject! { |act| act.source_location[0].include?('webpacker/clean.rake') }
assets_clean.enhance(['webpacker:clean'])

assets:clean[keep,age] に対応させる

with_loggerやmanifestを使えるように binding を取得して頑張る。

# Rakefile

assets_clean = Rake::Task['assets:clean']
assets_clean.arg_names << :age
pos = assets_clean.actions.index { |act| act.source_location[0].include?('sprockets/rails/task.rb') }
act = assets_clean.actions[pos]
assets_clean.actions[pos] = lambda do |_t, args, **opts|
  keep = Integer(args.keep || act.binding.eval('self.keep'))
  age = Integer(args.age || 3600)

  act.binding.eval(<<~RUBY)
    with_logger do
      manifest.clean(#{keep}, #{age})
    end
  RUBY
end

まとめ

これにより bin/rails assets:precompile assets:clean[0] を実行すると、1世代だけassetsを残せる。

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 をつけている。

jobs:
  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.gemfile.lock
      # あとは bundle-install して、テストを実行すれば良い

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