patorashのブログ

方向性はまだない

Railsでfavicon.icoがないと言われるケースへの対応

時々発生していた原因不明のfavicon.icoがないと言われるケース…。favicon_link_tagは使っているのでおかしい!と思いつつ、いつか調べようと思っていたら、Seleniumがめちゃくちゃ大量のエラーを出し始めたので慌てて調査開始。

favicon_link_tag について

favicon_link_tagは、デフォルトでassets/images/favicon.icoへのリンクのタグを出力してくれます。

= favicon_link_tag 'favicon.ico'
# => <link rel="shortcut icon" type="image/x-icon" href="/assets/favicon-e39264162177729871fd43ea8f0e1f5837b384c20c1af34f6b41c7b40dbdc210.ico" />

この最初の引数を/favicon.icoにすると、public/favicon.icoに対するパスになるようです。

= favicon_link_tag '/favicon.ico'
# => <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />

これは知らなかった。

pumaがfavicon.icoを出力しない

とりあえず、Seleniumが吐いているエラーログである、http://localhost:3000/favicon.icofaviconが取れるようにしようと思い、public/favicon.icofaviconを置いたところ、pumaのエラーが発生。

Read error: #<TypeError: no implicit conversion of Symbol into String>

puma faviconとかでぐぐったりもしたのですが、あまりいい情報が出ないので落ち着いて先のエラーメッセージとpumaでググり直してみると、出てきました。

medium.com

config.public_file_server.headersのkeyがSymbolじゃなくてStringでないといけない、とのこと。 確認してみたら、Symbolになってました😱

環境毎のconfigファイルを編集

config/environments/development.rbとtest.rbを修正しました。

# NG😵
config.public_file_server.headers = {
  "Cache-Control": "public, max-age=#{2.days.to_i}",
}

# OK😄
config.public_file_server.headers = {
  "Cache-Control" => "public, max-age=#{2.days.to_i}",
}

無事にhttp://localhost:3000/favicon.icofaviconが取れるようになりました!🎉🎉🎉

Macで開発環境のDocker化に取り組んでいるので一旦まとめる

MacRailsアプリを開発しているとなぜかセグメンテーション違反が起きまくって開発に支障が出てきたので、開発環境を全部Dockerに載せてしまおうと思ってここ2週間くらい取り組んでいます。

とりあえず、CircleCIのテスト以外はちゃんと動くようになったかな?と思えるところまできたので、まとめておきます。

ベストプラクティスに学ぶ

当初はDockerのページに載っているサンプルを見ながら自分で書いていこうとしていましたが、Macで動かすとI/Oがあまりにも遅くて使い物にならないレベルでした。twitterで愚痴っていたら、神速さんにアドバイスをもらいました。

とりあえず、これを読み進めながら、自分の環境に合うように修正を進めていきました。

techracho.bpsinc.jp

ファイル群を公開

まずはDocker関連のファイルだけ。

Dockerfile

Dockerfileは参考にしたサイトとほぼ同じ構成です。プロジェクトに.dockerdev/というディレクトリを作成し、その下に置いています。 docker-compose.yml側に引数としてバージョン情報を持つことで、開発環境のバージョン管理をしやすくなっているDockerfileです。また、aptでインストールするパッケージについては、Aptfileという外部ファイルに記述するようにしてあります。 変更点は、ENTRYPOINTを定義しているところです。Postgresが完全に起動したらコマンドを実行するようにしています。

ARG RUBY_VERSION
FROM ruby:$RUBY_VERSION

ARG PG_MAJOR
ARG NODE_MAJOR
ARG BUNDLER_VERSION
ARG YARN_VERSION

# ソースリストにPostgreSQLを追加
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -\
  && echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list

# ソースリストにNodeJSを追加
RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -

# ソースリストにYarnを追加
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -\
  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

# 依存関係をインストール
COPY .dockerdev/Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade &&\
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends\
    build-essential\
    postgresql-client-$PG_MAJOR\
    nodejs\
    yarn=$YARN_VERSION-1\
    $(cat /tmp/Aptfile | xargs) &&\
    apt-get clean &&\
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\
    truncate -s 0 /var/log/*log

# bundlerとPATHを設定
ENV LANG=C.UTF-8\
  GEM_HOME=/bundle\
  BUNDLE_JOBS=4\
  BUNDLE_RETRY=3
ENV BUNDLE_PATH $GEM_HOME
ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH\
  BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH /app/bin:$BUNDLE_BIN:$PATH

# RubyGemsをアップグレードして必要なバージョンのbundlerをインストール
RUN gem update --system &&\
    gem install bundler:$BUNDLER_VERSION

# appコードを置くディレクトリを作成
RUN mkdir -p /app

WORKDIR /app
COPY .dockerdev/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh", "postgres"]

Aptfile

aptでインストールするパッケージはとりあえずvimRailsのCredentialsを変更するのに必須なので入れてます。また、私が担当しているプロジェクトでは画像を扱うのでimagemagickも入れています。

vim
imagemagick

entrypoint.sh

これがエントリーポイントのshell scriptです。今見てみると、別にDockerfile側で引数を渡さなくてもいいかなと思えました…。 やっていることは、pg_isreadyコマンドを使ってPostgreSQLが立ち上がるまで待ち続けて、立ち上がったらコマンドを実行しています。

#!/bin/bash
set -e

# Then exec the container's main process (what's set as CMD in the Dockerfile).
host="$1"
shift
cmd="$@"

until pg_isready -h "$host" -U "postgres"; do
  >&2 echo "Postgres is unavailable - sleeping"
  sleep 5
done

>&2 echo "Postgres is up -executing command"
exec $cmd

docker-compose.yml

とりあえず、現時点でのdocker-compose.ymlを公開します。 基本的には、参考サイトの方針を踏襲しています。backendを引き継いだコンテナが5つあります。

コンテナ名 説明
rails develoment環境でRailsが起動
resque development環境でバックグラウンドジョブを担当するresqueが起動
test test環境でRailsアプリのあるコンテナを起動
runner Railsアプリのある環境のシェルを起動
webpacker webpacker用の設定(うちでは現時点で未使用)

また、ミドルウェア系を全てコンテナ化してあります

各環境ごとに、depends_onブロックを使って依存しているコンテナを表現しています。

version: '3.3'

services:
  app: &app
    build:
      context: .
      dockerfile: ./.dockerdev/Dockerfile
      args:
        RUBY_VERSION: '2.6.3'
        PG_MAJOR: '11'
        NODE_MAJOR: '13'
        YARN_VERSION: '1.21.1'
        BUNDLER_VERSION: '1.17.3'
    image: app-dev:1.0.0
    tmpfs:
      - /tmp

  backend: &backend
    <<: *app
    stdin_open: true
    tty: true
    volumes:
      - .:/app:cached
      - rails_cache:/app/tmp/cache
      - bundle:/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
      - data:/app/data
      - coverage:/app/coverage
      - .dockerdev/.psqlrc:/root/.psqlrc:ro
    environment:
      TZ: "/usr/share/zoneinfo/Asia/Tokyo"
      NODE_ENV: ${NODE_ENV:-development}
      RAILS_ENV: ${RAILS_ENV:-development}
      BOOTSNAP_CACHE_DIR: /bundle/bootsnap
      WEBPACKER_DEV_SERVER_HOST: webpacker
      HISTFILE: /app/log/.bash_history
      PSQL_HISTFILE: /app/log/.psql_history
      EDITOR: vi
      MALLOC_ARENA_MAX: 2
      WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}
    depends_on:
      - postgres
      - redis
      - elasticsearch
      - minio
      - memcached
      - mailhog

  runner:
    <<: *backend
    command: /bin/bash

  rails:
    <<: *backend
    command: ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
    ports:
      - '3000:3000'

  test:
    <<: *backend
    command: /bin/bash
    environment:
      TZ: "/usr/share/zoneinfo/Asia/Tokyo"
      NODE_ENV: ${NODE_ENV:-development}
      RACK_ENV: ${RACK_ENV:-test}
      RAILS_ENV: ${RAILS_ENV:-test}
      BOOTSNAP_CACHE_DIR: /bundle/bootsnap
      WEBPACKER_DEV_SERVER_HOST: webpacker
      HISTFILE: /app/log/.bash_history
      PSQL_HISTFILE: /app/log/.psql_history
      SELENIUM_DRIVER_URL: http://chrome:4444/wd/hub
    depends_on:
      - postgres
      - redis
      - elasticsearch
      - memcached
      - chrome

  resque:
    <<: *backend
    command: ["bundle", "exec", "rake", "environment", "resque:work"]
    environment:
      TERM_CHILD: 1
      QUEUE: "*"

  postgres:
    image: mdillon/postgis:11-alpine
    volumes:
      - .dockerdev/.psqlrc:/root/.psqlrc:ro
      - pg_data:/var/lib/postgresql/data
      - ./log:/root/log:cached
    environment:
      PSQL_HISTFILE: /root/log/.psql_history
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1"]
      interval: 5s

  elasticsearch:
    image: patorash/elasticsearch-kuromoji:5.6.14-alpine
    ports:
      - "9200:9200"
      - "9300:9300"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    environment:
      ES_JAVA_OPTS: "-Xms512m -Xmx512m"
    healthcheck:
      test: ["CMD", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
      interval: 60s
      timeout: 30s
      retries: 3

  redis:
    image: redis:3.2.12-alpine
    volumes:
      - .docker/redis/data:/data
    ports:
      - 6379
    healthcheck:
      test: redis-cli ping
      interval: 10s
      timeout: 3s
      retries: 30

  minio:
    image: minio/minio:RELEASE.2020-02-07T23-28-16Z
    ports:
      - "9000:9000"
    command: [server, /data]
    volumes:
      - .docker/minio/data:/data
    environment:
      MINIO_ACCESS_KEY: access_key
      MINIO_SECRET_KEY: access_secret
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3

  memcached:
    image: memcached:1.5.12-alpine
    ports:
      - "11211:11211"
    healthcheck:
      test: echo stats | nc 127.0.0.1 11211
      interval: 10s
      retries: 60

  mailhog:
    image: mailhog/mailhog:v1.0.0
    ports:
      - '8025:8025'
    environment:
      MH_STORAGE: maildir
      MH_MAILDIR_PATH: /tmp
    volumes:
      - mail_dir:/tmp

  chrome:
    image: selenium/standalone-chrome:latest
    ports:
      - '4444:4444'

  webpacker:
    <<: *app
    command: ./bin/webpack-dev-server
    ports:
      - '3035:3035'
    volumes:
      - .:/app:cached
      - bundle:/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
    environment:
      NODE_ENV: ${NODE_ENV:-development}
      RAILS_ENV: ${RAILS_ENV:-development}
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0

volumes:
  postgres:
  redis:
  bundle:
  node_modules:
  rails_cache:
  packs:
  data:
  coverage:
  pg_data:
  mail_dir:
  elasticsearch_data:
railsコンテナとtestコンテナを分けている理由

テストを実行する際に、railsコンテナで毎回、環境変数RAILS_ENVをtestにするのが面倒だったので、分けました。 また、railsコンテナを起動する時にはChromeコンテナは必要ないため除外するなど、開発時は軽くなるような工夫をしています。 Chromeコンテナはtestのときだけ起動するようにしています。

参考サイトからの学びポイント

参考サイトを読み進めながら、自分が気付いた高速化のポイントやイメージの縮小のためのTipsをまとめます。

Dockerのvolumesを使いこなす

docker-composeのvolumesってホストOSとコンテナでファイル共有をするためのものだと認識していたのですが、違いました。Dockerでvolumeを定義して、そのvolume上にデータを配置することで、MacとDocker間のI/Oの影響を受けずに済むようになりました。(docker-composeを雰囲気で使っていることがわかる…)

gemやnode_modulesなどライブラリ系のデータや、データベースをはじめとするミドルウェアのデータは全てvolumesに逃すことで、高速化できました。

ただし、volumesにデータを置くと、ホストOSからは直接ファイルを見られなくなるので、その点は注意が必要です。そのため、minio(AWS S3の代替)に関しては、ホストOSとファイル共有のままにしてあります。

.dockerignoreを使いこなす

RailsアプリケーションをDocker化する場合は、プロジェクト自体を含んだ状態でdocker buildすることになります。そのため、不要なファイルを置いていると、一緒にコンテナに保存されます。不要なデータがあると、その分buildする時間がかかるようになるので、.dockerignoreを定義して無視させるようにしましょう。これはHerokuでいう.slugignoreと同等のものになります。

.circleci
.docker
.idea
.git
.github
doc

.env*
.gitignore
.ruby-version
.slugignore
*.dump*

コンテナ間のネットワーク指定

ホストOSからミドルウェアのコンテナを参照していた頃と違って、Railsアプリのコンテナからミドルウェアのコンテナを参照する場合には、ネットワークの指定の方法が異なります。

ホストOSからの場合、基本的にhttp://localhost:9200のようにホスト名をlocalhostを指定すればよかったのですが、コンテナ間の場合、http://elasticsearch:9200のように変わります。 そのため、コード上からそれらを全て指定し直しました。

PostgreSQL

database.ymlを修正しました。

default: &default
  adapter: postgresql
  encode: unicode
  host: postgres # <= localhostから変更
  username: <%= ENV.fetch("DOCKER_POSTGRES_USER") { "postgres" } %>
  port: <%= ENV.fetch("DOCKER_POSTGRES_PORT") { 5432 } %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
 <<: *default
  database: app_development

test:
  <<: *default
  database: app_test<%= ENV['TEST_ENV_NUMBER'] %>
  min_messages: WARNING
Elasticsearch

config/initializers/elasticsearch_model.rbを修正しました。

Elasticsearch::Model.client = case
                                when Rails.env.development?
                                  Elasticsearch::Client.new(host: 'elasticsearch:9200/', log: true)
                                when Rails.env.test?
                                  Elasticsearch::Client.new(host: 'elasticsearch:9200/')
                                else
                                  raise 'ELASTICSEARCH_URL not found.' unless ENV['ELASTICSEARCH_URL']
                                  Elasticsearch::Client.new(host: ENV['ELASTICSEARCH_URL'])
                              end
Redis

Redisはresqueで使っています。 config/initializers/resque.rbを修正しました。

Resque.redis = if ENV['REDIS_URL']
                 Redis.new(url: ENV['REDIS_URL'])
               else
                 Redis.new(
                   host: 'redis', # <= 修正
                   port: ENV.fetch('REDIS_PORT', 6379),
                 )
               end
Memcached

Memcachedはgemのdalliを使っているので、その辺りを修正します。 config/environments/development.rb を修正しました。

# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist?
  config.action_controller.perform_caching = true
  config.action_controller.enable_fragment_cache_logging = true
  config.cache_store = :dalli_store, 'memcached:11211' # <= 修正
  config.public_file_server.headers = {
    'Cache-Control': "public, max-age=#{2.days.to_i}",
  }
else
  config.action_controller.perform_caching = false
  config.cache_store = :null_store
end
Minio

MinioはPaperclipで使っているので(未だにPaperclip…。Docker化が終わったらActiveStorageに移行予定)、Paperclipの指定を変更します。 config/environments/development.rb を修正しました。

config.paperclip_defaults = {
  storage: :s3,
  s3_protocol: :http,
  s3_host_name: 'localhost:9000', # <= ん?こいつは…?
  s3_credentials: {
    access_key_id: Rails.application.credentials.aws_access_key_id,
    secret_access_key: Rails.application.credentials.aws_secret_access_key,
  },
  s3_region: Rails.application.credentials.aws_region,
  bucket: Rails.application.credentials.s3_bucket_name,
  s3_options: {
    endpoint: 'http://minio:9000', # <= 変更
    force_path_style: true, # for aws-sdk (required for minio)
  },
  s3_permissions: :private,
  url: ':s3_path_url',
  path: ':class/:attachment/:id/:style/:filename',
}

とりあえず、s3_optionsのendpointを変えたらOKです。s3_host_nameは、publicな場合のみ有効になるっぽくて、期限付きURLを発行するとhttp://minio:9000になってしまいました。

Minioの場合は、ファイルのアップロードに関してはこれでいいのですが、ブラウザからファイルを参照しようとすると、http://minio:9000が見つからないということでエラーになってしまいます。解決策としては、今のところは、/etc/hostsを編集するしかなさそうです。

127.0.0.1 minio

これで、minio経由のファイルを参照できるようになりました。

MailHog

MailHogはメールサーバを代用してくれます。これまではgem letter_openerを使っていたのですが、Docker環境だとうまく機能しないという話を聞き、オススメされました。 確かにうまく機能しませんでした(メールがクリアできなかったり等)。 そのため、MailHogに切り替えました。 config/environments/development.rb を修正しました。

config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address: 'mailhog',
  port: 1025,
}

あっさりと変更できてよかったです。

Chrome

Chromeselenium-chromeのDockerイメージを使うことにしました。 RubyのイメージにChromeを含めるのはイメージサイズも大きくなるし、ビルド時間も長くなるという点、そして、スタンドアロン版のDockerイメージがあるということだったので、 採用するようにしたのですが、これをCapybaraで指定するのにかなりハマッたので、メモを残しておきます。

spec/support/capybara.rbを編集しました。過去の設定も含まれているので、不要なものも多々あるかもしれません。

Capybara.register_driver :selenium do |app|
  options = Selenium::WebDriver::Chrome::Options.new
  options.headless!
  options.add_argument '--disable-gpu'
  options.add_argument '--window-size=1680,1050'
  options.add_argument '--blink-settings=imagesEnabled=false'
  options.add_argument '--lang=ja'
  # options.add_argument '--no-sandbox'
  # options.add_argument '--no-zygote'
  driver = Capybara::Selenium::Driver.new(app,
                                          url: ENV.fetch('SELENIUM_DRIVER_URL'), # <= 追加
                                          browser: :remote, # <= :chromeから:remoteに変更
                                          options: options,
                                          desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
                                            login_prefs: { browser: 'ALL' },
                                            loggingPrefs: { browser: 'ALL' },
                                          ),
  )
  bridge = driver.browser.send(:bridge)

  path = "session/#{bridge.session_id}/chromium/send_command"

  bridge.http.call(:post, path, cmd: 'Page.setDownloadBehavior',
                                params: {
                                  behavior: 'allow',
                                  downloadPath: WaitForDownload::PATH,
                                })
  driver
end

Capybara.configure do |config|
  config.server_host = "test" # <= chromeから見た、テストを実行するイメージ名を指定
  config.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i
  config.app_host = "http://#{config.server_host}:#{config.server_port}" # <= Seleniumが接続するテストを実行するAppHostのURL

  config.javascript_driver = :selenium
  config.default_max_wait_time = ENV['CI'].present? ? 15 : 15
  config.ignore_hidden_elements = true
  config.server = :puma, { Silent: true }
end

肝は、Capybara::Selenium::Driver.newするところでの、urlの指定と、browser: :remoteにするところと、Capybara自体のconfigの設定で、Chrome側から見たtestコンテナへの接続設定です。config.app_hostが重要です。これがないと、Capybaraは真っ白なページを表示して、visitを実行しても何も起きませんでした。

docker-composeで実行するコマンドの紹介

では、このdocker-composeを使って開発する時のコマンドについて書いていきます。

docker-compose build rails
Dockerfileを元にRailsアプリを含むRubyのイメージを作成します。
docker-compose up rails
Railsと、依存するコンテナを全て起動します。通常の開発時はこれでOKです。実行すると、http://localhost:3000にアクセス可能になります。
docker-compose up resque
Resqueと、依存するコンテナを全て起動します。通常の開発時はこれでOKです。
docker-compose run --rm runner
依存するコンテナを起動した状態でRubyコンテナのshellを開きます。`--rm`オプションにより、コンテナは終了時に削除されます。 これを利用することで、railsを起動することなく、様々なことが可能です。例えば、bundle installを行なったり、yarn installを行なったり、rails consoleを開いたり等です。
docker-compose up -d test
testのコンテナをバックグラウンドで立ち上げます。Capybaraの関わるテストが動かないため、バックグラウンドで立ち上げておかなくてはなりません。
docker-compose exec test bundle exec rspec spec/models/hoge_spec.rb
rspechoge_spec.rbのテストを実行します。execではなく、runで実行すると、一時的にコンテナが立ち上がるため、名前解決がうまくいかないため、Capybaraの絡むテストが失敗します。
docker-compose down
コンテナを全て落とします

docker-composeの打ち疲れ対策

この体制で開発を始めると、docker-composeって何度も打つことになるので、エイリアスを設定しておくと楽です。

alias dcu="docker-compose up"
alias dcd="docker-compose down"
alias dcr="docker-compose run"
alias dce="docker-compose exec"
alias dcb="docker-compose build"
alias dcp="docker-compose ps"

今後について

ひとまず、開発環境としては速度的にもストレスのないスピードで開発できるようになりました!神速さんに感謝! でもまだCircleCIでテストが通ってないのでマージできず、本格運用には至っていません…。 今はCircleCI上でこのdocker-compose.ymlを使ってそのままテストができないかを探っている最中です。進捗があったらまたブログ書きます。

MacのRails開発環境でやっぱりセグメンテーション違反起きた

以前にこういう記事を書きました。

patorash.hatenablog.com

しかし、PostgreSQLではセグメンテーション違反は起きなくなったのですが、Redisに接続する箇所で起きるようになったので、抜本的な問題解決にはなりませんでした。

ということで、私の中でMacRailsアプリ開発は厳しい、という結論に。

しょうがないので、Rubyの実行環境自体をMacからDockerにしていこうと思います。

現場からは以上です。

pgのバージョンを0.21.0にしたらセグメンテーション違反が起きなくなった

少し前に、Railsを起動するとセグメンテーション違反が起きたが、原因がわかった的な記事を書いてた。

patorash.hatenablog.com

これで落ち着いたかと思いきや、全然落ち着いておらず、またもやセグメンテーション違反が起きた。シングルプロセスでアクセスする場合は落ち着いていた感じだったのだが、Resque等のバックグラウンド処理用のワーカープロセスを起動すると、落ちまくった。heroku localを実行すると、ほぼセグメンテーション違反が起きた。

周囲に聞いてみても、落ちないですけどねーと言われるので、そのプロジェクトのGemfileを見てみたら、pgのバージョンが0.21.0だった。今は1.2.2が最新である。当初は「いやいや、もう1系出てるのに未だに0.21.0て…。アップグレードしないとダメでしょ」と思っていたのだが、試しにダウングレードしてからheroku localを実行したところ、セグメンテーション違反が全く起きない…。

一旦、ダウングレードして様子見しようかと思う。 とはいえ、これでいいとは思っていないので、時々アップグレードしながら探ってみようかと思う。

Rails 6のCredentials方式をRails 5.2にバックポートした

私が担当している製品の機密情報の管理は、Rails 5.1で導入されたEncrypted Secrets(secrets.yml.encを使う方式)を使っていて、5.2になって導入されたCredentials方式の導入は見送っていました。

理由は、5.2のCredentials方式だと、各環境毎の設定が行えなかったからです。そのため、5.2のリリース後にはCredentialsを複数の環境で扱えるようにするためのgemがいくつか出てきました。しかし、そこまでしてCredentialsに移行しても、ある意味レールから外れたカスタマイズをすることになりますし、Encrypted Secretsが非推奨になったわけではなかったので、Encrypted Secretsを使い続けていました。

現在もまだRails 5.2系を使っているのですが、これを6系にバージョンアップするために引っかかりそうなところを調査していたところ、Rails 6からはCredentials方式を後方互換性を保ったまま、各環境で使えるようになったという記事をRails 6エンジニア養成読本で読みました。

これを使えるようにしたいのですが、いきなり6にアップグレードすることは難しいです。

そこで、まずこの機能を先に取り込めるかどうかを検証しようと思いました。

バックポート用のコードを発見

調べ始めてちょっとしたら、gistで希望通りのコードと思われるものが公開されていました。

Backport Rails 6 per-environment credentials · GitHub

これを検証したところ、Rails 6のCredentialsと同様に各環境用のcredentials.yml.encとmaster.keyが作られました。

導入

導入に関しては、上記のgistを参考にしてください。

各環境のCredentialsを作成する

Rails 6のCredentialsの作成と同様の手順で行えます。下記のコマンドを実行すると、config/credentials/staging.yml.encconfig/credentials/staging.keyが生成された後、VSCodeでstaging用のCredentialsの設定ファイルが開きます。

env EDITOR="code --wait" bin/rails credentials:edit --environment=staging

適当に入力後に保存して、そのファイルのタブを閉じると、設定完了です。簡単。

各環境のCredentialsを見る

Encrypted Secretsの頃は設定項目を見るだけのコマンドがなかったので、わざわざ編集モードで開く必要があったのですが(変更なしでも変更と見なされるのでgitで戻す必要があった)、Credentialsならば、閲覧用のコマンドがあります。便利!

bin/rails credentials:show --environment=staging

Credentialsに移行するために行なった作業

ざっくりとした手順は、以下の通りです。

  1. secrets.yml.keyをコピーしてmaster.keyを作る。
  2. secrets.yml.encから各環境用に値をコピーする。
  3. Rails.application.secretsRails.application.credentialsに一斉置換する。
  4. config/environments/production.rbをはじめとする全ての環境でEncrypted Secretsを使う設定を削除、RAILS_MASTER_KEYを必須とする設定を行う
  5. 開発環境で動作検証を行う
  6. stagingの環境変数RAILS_MASTER_KEYの値をconfig/credentials/staging.keyの値に変更する
  7. stagingにデプロイして動作検証を行う
  8. 問題ないことを確認後、secrets.yml, secrets.yml.enc, secrets.yml.keyを削除する。

master.keyを作る

master.keyには、本番環境で設定されている環境変数RAILS_MASTER_KEYと同じ値を設定します。これはsecrets.yml.keyと同様のはずですので、コピーしてしまいます。

cp config/secrets.yml.key config/master.key

また、忘れずに.gitignoreにmaster.keyを追加しておきましょう。

/config/master.key

各環境用に機密情報をコピーする

以下のコマンドでEncrypted Secretsを表示します。

env EDITOR="code --wait" bin/rails secrets:edit

Encrypted Secretsの頃は、yamlのrootに環境名を設定する必要がありました。

production:
  aws:
    access_key: foo
    access_secret_key: bar

staging:
  aws:
    access_key: foo_staging
    access_secret_key: bar_staging

これを、各環境のCredentialsにコピーしていきます。

production環境

まず、production環境です。以下のコマンドを実行すると、config/credentials.yml.encが生成され、編集モードで開きます。

env EDITOR="code --wait" bin/rails credentials:edit

以下のように設定し、保存します。

aws:
  access_key: foo
  access_secret_key: bar
staging環境

次に、staging環境です。以下のコマンドを実行すると、config/credentials/staging.yml.encconfig.credentials/staging.keyが生成され、編集モードで開きます。

env EDITOR="code --wait" bin/rails credentials:edit --environment=staging

以下のように設定し、保存します。

aws:
  access_key: foo_staging
  access_secret_key: bar_staging

こんな感じで環境の数だけ作っていきます。

development, test環境

secrets.ymlも削除したいため、development, test用のCredentialsも作成しました。 secrets.ymlに設定していた内容をコピーします。

env EDITOR="code --wait" bin/rails credentials:edit --environment=development

test環境も同様に行いました。

機密情報の参照先を一斉置換する

Credentialsは指定方法がRails.application.credentialsなので、Rails.application.secretsRails.application.credentialsに一斉置換します。

各環境でEncryped Secretsを使う設定を削除

config/environments/production.rbなど、Encrypted Secretsを使っていた環境で、以下の行を削除します。

# Attempt to read encrypted secrets from `config/secrets.yml.enc`.
# Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
# `config/secrets.yml.key`.
config.read_encrypted_secrets = true

そして、環境変数RAILS_MASTER_KEY、又はmaster.keyに該当するものを必須とする設定を行います。

# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
config.require_master_key = true

開発環境で動作検証を行う

ここまでできれば、設定が間違っていないか動作検証を行います。

bin/rails s

アプリが起動することと、ある程度動作することを確認します。

staging環境のRAILS_MASTER_KEYを変更する

先に、staging環境の環境変数RAILS_MASTER_KEYを、config/credentials/staging.keyの値に変更します。

うちではHerokuを使っているので、この設定を先にせずにコードをデプロイして検証しようとしたところ、RAILS_MASTER_KEYの値が異なるためCredentialsが解読できずにデプロイが失敗しました。忘れずにデプロイ前に変更しましょう。

stagingにデプロイして動作検証を行う

ここまでできれば、stagingにデプロイできていると思いますので、動作検証をします。

Encrypted Secretsに関するファイルの削除

問題ないことを確認後、secrets.yml, secrets.yml.enc, secrets.yml.keyを削除しましょう。

これで、あとは本番環境にデプロイすれば大丈夫だと思います。

と思いきや…

落とし穴(RAILS_MASTER_KEYがなくてCIがコケた)

secrets.ymlを削除したことで、CIでコケていました…。うちではCircleCIを使っているので、CircleCIにて、環境変数RAILS_MASTER_KEYを作ってconfig/credentials/test.keyの値を設定したところ、テストも無事に通りました。

忘れずにCIにもRAILS_MASTER_KEYの設定を!!

Macでrails起動時に発生したセグメンテーション違反の原因がわかった

過去に、こんな記事を書いていました。

patorash.hatenablog.com

ずっと騙し騙し、gemの再インストールをしながら使っていたのですが、いよいよresqueがまともに動かないときが頻発したので、ちゃんと調査を開始。

ちなみにこの記事を書いている時点での私のPCの環境は以下の通り。

環境 バージョン
OS Mac OS X Mojave(10.14.6)
Ruby 2.6.3(rbenvでインストール)
PostgreSQL 11.x(Dockerで起動)

PostgreSQLとの接続を疑う

過去の経緯から、PostgreSQLの接続が怪しいのかなと思っていたら、こんなStack Overflowの投稿を発見。

stackoverflow.com

回答から抜粋すると、以下のような設定(gssencmode: disable)をdatabase.ymlのdevelopmentとtestに追加したらいい、とあった。

default: &default
  adapter: postgresql

development:
  <<: *default
  gssencmode: disable
test:
  <<: *default
  gssencmode: disable

これは、PostgreSQL 12から追加されたオプションらしい。OS Xだとこれの指定がないとエラーになることがあるとか…。

とりあえず設定はしておいたが、PostgreSQLへの接続はうまくいくときといかない時があったので、設定して様子見をしておく…。

Redisへの接続で落ちる

とりあえず、これで直ったんじゃないか?と思い、heroku localコマンドでpumaとresqueを起動させたところ、ActiveJobの処理が動いたらセグメンテーション違反が発生。今度はエラーメッセージが変わった。

13:18:43 resque.1   |  /Users/****/.anyenv/envs/rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/redis-4.1.3/lib/redis/connection/ruby.rb:210: [BUG] Segmentation fault at 0x000000010cb14a3a
13:18:43 resque.1   |  ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]
13:18:43 resque.1   |  -- Crash Report log information --------------------------------------------
13:18:43 resque.1   |     See Crash Report log file under the one of following:
13:18:43 resque.1   |       * ~/Library/Logs/DiagnosticReports
13:18:43 resque.1   |       * /Library/Logs/DiagnosticReports
13:18:43 resque.1   |     for more details.
13:18:43 resque.1   |  Don't forget to include the above Crash Report log file in bug reports.
13:18:43 resque.1   |  -- Control frame information -----------------------------------------------
13:18:43 resque.1   |  c:0055 p:---- s:0289 e:000288 CFUNC  :getaddrinfo
13:18:43 resque.1   |  c:0054 p:0034 s:0281 e:000280 METHOD /Users/****/.anyenv/envs/rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/redis-4.1.3/lib/redis/connection/ruby.rb:210

今度はRedisに接続できなくなっている…。エラーメッセージでググったら、Rubyのバグレポートを発見。

bugs.ruby-lang.org

Third Party Issueになっていて、RubyじゃなくてMacのgetaddrinfoのバグのせいらしいことを把握。

同様に色々検索してみたのだが、やはりOS Xのgetaddrinfo由来のバグである、というバグレポートがヒットする。

bugs.ruby-lang.org

直す方法が掲示されていないのだが、RubyやGemをコンパイルする時点でバグっていそうだなと思った。

直すためにやったこと

  1. brew upgradeで、gccとかを更新すれば直るんじゃないか?と思い、雑にbrew upgradeを実行。
  2. Rubyを再インストールすれば直るんじゃないか?と思い、雑にrbenv install -f 2.6.3を実行(ここは任意のバージョンを)
  3. gemを全て再インストールすれば(略)と思い、全てのgemを削除。gemを全削除するのはここを参考にした。 qiita.com
  4. bundle installを実行

動作確認

heroku local でpumaとresqueを起動させて、ActiveJobの処理を動かしてみたところ、問題なく動いた。また、頻発していたpgのセグメンテーション違反も起きなくなったので、これで解決かもしれない…!!

Hyper-VでAmazon Linux 2を入れる

勉強のために仮想環境を準備しようと思い、Hyper-VAmazon Linux 2をインストールしてみたのでその備忘録を記します。

目次

イメージをダウンロード

まずは、Amazon Linux 2のイメージをダウンロードします。ダウンロード方法はAWSのページに書いてありますが、色々な仮想化プラットフォームの形式で公開されています。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/amazon-linux-2-virtual-machine.html#amazon-linux-2-virtual-machine-prepare

今回は、Hyper-Vのイメージをダウンロードしました。

cdn.amazonlinux.com

seed.iso 起動ディスクを作成する

次に、seed.isoを作ります。これは、Amazon Linux 2が起動したときに自動で設定される項目を定義しておくisoイメージになります。

作り方は先ほどのAWSのサイトに書いてありますが、ここにも書いておきます。ざっくりとした手順は以下の通り。

  1. seedconfigという名前のディレクトリを作成し、そこに移動
  2. meta-data設定ファイルを作成する
  3. user-data設定ファイルを作成する
  4. コマンドでseed.isoファイルを作成する

meta-dataの設定を行う

meta-dataは、ネットワークの設定を記入します。Hyper-Vで動かす場合は、仮想スイッチと同じネットワーク上に作らないといけないので、仮想スイッチのネットワークを調べましょう。

ネットワークの設定を調べる

仮想スイッチのネットワークですが、コントロールパネルのネットワークとインターネット、ネットワーク接続で確認することができます。今回は、前に作ったVagrantで利用するために固定IPを設定した仮想スイッチのネットワークを使います。Default SwitchのネットワークはPCが再起動するたびにコロコロ変わっていたので、とりあえず今回は使いません。なんか固定する方法もあるらしいけれど。

f:id:patorash:20200117073241p:plain

矢印の仮想ネットワーク上で右クリックをして、プロパティを表示します。

そして、インターネット プロトコル バージョン 4を選択して、プロパティを表示。 f:id:patorash:20200117073759p:plain

固定IPが振ってありました。192.168.100.1/24にしていることが、これで確認できました。 f:id:patorash:20200117073955p:plain

DNS設定が抜けていたので、後で家のルーターIPアドレスを指定しておきました。ここは各自のネットワークに合わせてください。

f:id:patorash:20200118094219p:plain

meta-dataを記入する

local-hostnameは適当に。私の場合はPostgreSQLの勉強用で作ろうとしていたので、postgres.primaryにしました。

network-interfacesは、先ほど確認した仮想スイッチと同じネットワークである192.168.100.0/24に属するように書きます。/24サブネットマスクのことで、255.255.255.0と同義です。gatewayに先ほどの仮想スイッチのIPを指定します。addressに、192.168.100.10を設定しました。

local-hostname: postgres.primary
# eth0 is the default network interface enabled in the image. You can configure static network settings with an entry like the following.
network-interfaces: |
  auto eth0
  iface eth0 inet static
  address 192.168.100.10
  network 192.168.100.0
  netmask 255.255.255.0
  broadcast 192.168.100.255
  gateway 192.168.100.1

user-dataの設定を行う

次に、user-dataを設定します。これはAWSのほうで書かれていた通りに。defaultにしておくと、ec2-userが作られます。個別にユーザーを作成したい人は、usersのところで新しくユーザ名を追記すればいいかと思います。パスワードは自分で設定してください。

また、ローカル環境なので、Cloud-initが1度しか実行されないようにしておきました。

#cloud-config
#vim:syntax=yaml
users:
# A user by the name `ec2-user` is created in the image by default.
  - default
chpasswd:
  list: |
    ec2-user: ***********
# In the above line, do not add any spaces after 'ec2-user:'.

# NOTE: Cloud-init applies network settings on every boot by default. To retain network settings from first
boot, add following ‘write_files’ section:
write_files:
  - path: /etc/cloud/cloud.cfg.d/80_disable_network_after_firstboot.cfg
    content: |
      # Disable network configuration after first boot
      network:
        config: disabled

seed.isoファイルの生成

WSL経由で、seed.isoファイルを生成します。WSLにUbuntuを入れてあるので、そちらからコマンドを入力しました。 seedconfigディレクトリに移動後、次のコマンドを実行します。

$ genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data

これで、seed.isoイメージができました。

VMを作成する

ここからがAWSのサイトに書かれていなくて困ったところです。

イメージの移動

まず、ダウンロードしたHyper-Vのイメージを解凍して、Hyper-Vのイメージが置かれるディレクトリ(C:\Users\Public\Documents\Hyper-V\Virtual hard disks)に移動させました。先ほど作ったseed.isoファイルも同様に移動させました。

これは必要なことではありませんが、VMのイメージがどこにあるのかわかりやすくするためです。

仮想環境の構築

次に、Hyper-Vマネージャを起動します。検索から探すとすぐに見つかります。

f:id:patorash:20200117081427p:plain

仮想マシンの作成

仮想マシンの新規作成を選びます。

f:id:patorash:20200117081626p:plain

ウィザードが出てくるので、名前はお好きなものを。とりあえずAamzon Linux 2としました。

f:id:patorash:20200117081754p:plain

世代の指定ですが、第1世代を選びます。これは、AWSで公開されているHyper-Vのイメージが第1世代だからです。

f:id:patorash:20200117081920p:plain

メモリの割り当ては適当に。私は1024MBにしました。

ネットワークの構成は、先ほど参照した仮想スイッチを指定します。

f:id:patorash:20200117082157p:plain

仮想ハードディスクの接続は、最初にダウンロードしたHyper-Vのイメージを指定します。「既存の仮想ハードディスクを使用する」を選択し、さきほど移動させたHyper-Vのイメージを指定しましょう。

f:id:patorash:20200117082432p:plain

あとは、確認して、完了を押してウィザードを終了します。

f:id:patorash:20200117082605p:plain

ブートの設定

次に、作成した仮想マシンのブート設定を行います。これは、先ほど作成したseed.isoファイル経由で起動させるための設定です。

先ほど作った仮想マシンを選択し、右クリックして設定を選びます。

f:id:patorash:20200117082940p:plain

次に、IDEコントローラーを選択し、メディアにイメージファイルを指定し、先ほど作ったseed.isoを設定します。設定が終わったらOKを押します。

f:id:patorash:20200117083241p:plain

仮想環境を起動する

では、起動してみましょう。

Hyper-VマネージャのAmazon Linux 2上で右クリックして起動を選択します。その後、接続を選択します。

f:id:patorash:20200118092525p:plain

ecs-userでログインするところまで確認できました。

sudo yum updateを実行してみたところ、アップデートパッケージも取得できたので、ネットワークの設定もOKなようです。

これで、家庭でAmazon Linux 2を試せる環境ができました。しかし、まだネットワーク経由で接続できません。Hyper-Vの接続を使うのは表示も小さいし面倒過ぎます。SSHできるようにしましょう。以降、Hyper-Vに入れたAmazon Linux 2のことをVMと書きます。

SSHで接続できるように設定する

一時的にパスワード認証を許可

まず、VM上でsshd_configを編集して、一時的にパスワード認証を許可します。これは、ホストOSとゲストOSでクリップボードが共有できないため、公開鍵を追加するのが困難だったためです。 sudo vi /etc/ssh/sshd_configで編集します。コマンドモードで、/Passwordと打ってEnterを押すと、該当の箇所が検索できるでしょう。

# コメントアウトを外す
PasswordAuthentication yes

これで保存し、sshdを再起動します。

sudo systemctl restart sshd.service

SSHでパスワードで接続できるようになりました。

Windows側でSSHの設定を追加する

公開鍵・秘密鍵の作成

PowerShellで、以下の通りに入力します。

ssh-keygen.exe -t ecdsa

これで、~/.ssh/id_ecdsa~/.ssh/id_ecdsa.pubが出来上がります。

公開鍵をVMに設定する

PowerShellからSSHVMに接続します。

ssh.exe ec2-user@192.168.100.10

パスワード認証で接続後、VM側の~/.ssh/authorized_keysに、さきほど作った公開鍵 id_ecdsa.pub の内容を追加します。コピー&ペーストしてください。

終わったら、SSHを切ります。

SSHのConfigを設定する

WindowsからVMにホスト名で接続できるようにするため、設定しておきます。ここで、ユーザ名と秘密鍵の指定を行います。ホスト名は vm_al2_primaryとしました。ここはお好きにどうぞ。

Host vm_al2_primary
  HostName 192.168.100.10
  User ec2-user
  Port 22
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile "~/.ssh/id_ecdsa"
  IdentitiesOnly yes
  LogLevel FATAL
公開鍵接続できるかチェック

では、試します。

ssh.exe vm_al2_primary

これで、接続できたらOKです。

パスワード認証を無効にする

先ほどと逆のことを行います。 VMで、sudo vi /etc/ssh/sshd_configで編集します。コマンドモードで、/Passwordと打ってEnterを押すと、該当の箇所が検索できるでしょう。

# コメントアウトする
# PasswordAuthentication yes

これで保存し、sshdを再起動します。

sudo systemctl restart sshd.service
パスワードで認証できないことを確認する

PowerShellで、SSHを試してみます。

ssh.exe ec2-user@192.168.100.10

接続拒否されたら、OKです。

まとめ

WindowsAmazon Linux 2を使えるようにするまでを書きました。Hyper-Vだとなかなか情報が見つかりにくい印象がありますが、これで勉強が捗りそうです。