めもめも のーと

ハマったこととか、覚えたこととか

Rails7 (zeitwerk)でgRPC (Protocol Buffers)を使ったらハマった

概要

Amazon.co.jp: スターティングgRPC 技術の泉シリーズ (技術の泉シリーズ(NextPublishing)) eBook : 武上 将樹: Kindleストア

こちらの書籍にあるGo(サーバ) × Rails(クライアント)のサンプルを写経していた際、zeitwerkの仕様にはまってエラーが出たので、回避方法をメモしておきます。

zeitwerkはRails6から採用されたオードロード機構ですが、Rails6ではActive Supportで実装されたものがclassicモードとして利用可能で、それであればエラーは発生しないとのことでした。

Rails7からはclassicモードが使えずzeitwerkで動かす必要があるため、下記サイトを参考にさせていただき対応しました。

qiita.com

アプリケーション外観

最終的なディレクトリ構成は下記の通り。

.
├── api    # サーバ(Golang)
│   ├── gen
│   │   └── pb
│   │       ├── filemeta.pb.go
│   │       ├── image_upload_request.pb.go
│   │       └── image_upload_response.pb.go
│   ├── go.mod
│   ├── go.sum
│   ├── handler
│   │   └── image_upload_handler.go
│   └── server
│       └── server.go
├── client  # クライアント(Rails)
│   ├── Gemfile
│   ├── app
│   │   ├── gen
│   │   │   └── pb
│   │   │       └── image
│   │   │           └── upload
│   │   │               ├── image_upload_request
│   │   │               │   └── filemeta_pb.rb
│   │   │               ├── image_upload_request_pb.rb
│   │   │               ├── image_upload_request_services_pb.rb
│   │   │               └── image_upload_response_pb.rb
│   │   ├── models
│   │   │   └── image_uploader.rb
│   ├── config
│   │   ├── application.rb
├── proto
│   └── image
│       └── upload
│           ├── image_upload_request
│           │   └── filemeta.proto
│           ├── image_upload_request.proto
│           └── image_upload_response.proto

ポイント1)1つのmessageにつき1つのprotoファイル

まずprotoファイルでスキーマを定義します。

最初は一つのprotoファイル内に3つのmessageを記載していましたが、参考先にも記載がある通り、1protoファイルにつき一つのmessageとしました。zeitwerkによる定数読み込みでエラーになるためです。結果的に3つのprotoファイルを書き、大元のimage_upload_request.protoで他の2つをimportして使うようにしました。

定義できたらprotocおよびgrpc_tools_ruby_protocコマンドで各々GoとRubyのコードを自動生成します。

ポイント2)Rails側で読み込みパスを追加

config/application.rbに下記の通り追加します。

config.paths.add Rails.root.join('app',  'gen',  'pb').to_s, eager_load: true

protoで定義して自動生成した定数の名前空間を正しく認識させるために必要でした。

ポイント3)カスタムInflectorを定義

Qiitaの記事にある通り、自前のInflectorを定義して利用します(記事の通りなので割愛)

なお自動生成したrubyのファイルimage_upload_request_services_pb.rbについて、ファイル名はservicersとなっているが中身のmodule名はImageUploadRequestServiceとなり、このままだとzeitwerkの読み込みエラーになったため、取り急ぎここのカスタムInflectorで対応しました(ファイル名末尾にpbがつく場合、かつ指定のファイル名の場合は、当該ファイル名を単数系に変換)。

以上の通り修正し、下記コマンドを実行してexpected file XXX to define constant XXX のようなエラーが出なければOKです。

% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!

Laravelアプリをサブディレクトリで動かした

ある要件でlaravelアプリをドメインURLのサブディレクトリで動かしたかったが、 ネットで調べてヒットした記事が、環境差異なのか、ことごとくハマらず動かなかったので、 最終的に動くようになった設定を公開しておく。

やりたいこと

環境

docker-composeにて以下のコンテナを作成。ちなみにホストはWindows10なので、docker for windowsを使用。

  • nginx
  • php-fpm

ホスト側のディレクトリ構成

myapp/
├── docker-compose.yml
├── html
│   └── laravelapp
        ├── public
            ├── index.php
├── nginx
│   ├── default.conf
│   └── nginx.conf
└── php
    ├── Dockerfile
    ├── php.ini
    └── run.sh
  • nginxとphp-fpmを別々のコンテナで動かすので、各コンテナがlaravelアプリ見にいけるように共有のディレクトリhtmlを用意し、そこにlaravelのアプリをおく。

docker-composeの設定

version: '3'
services:
  nginx:
    image: nginx:alpine
    ports:
      - "19080:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./html:/var/www/html
    depends_on:
      - phpapp
  phpapp:
    build: ./php
    volumes:
      - ./html:/var/www/html
      - ./php/php.ini:/usr/local/etc/php/php.ini
  • nginxのconf設定ファイルはホストで用意したものを反映させるためにvolumesに追記した。
  • DocumentRootは/var/www/htmlとし、ホスト側のhtmlディレクトリと対応させた。
  • またphp-fpmコンテナからもlaravelアプリが見れるようにするため、nginxのDocumentRootと同じディレクトリを見るようにした。
  • 今回はphp.iniも自前のものを使いたかったので、volumesにおいてコンテナに反映させた。

nginxの設定

nginx.conf内でincludeされるdefault.confに設定を追記したが、nginxが動けばどこに書いてもいいと思う。

server {
    listen       80;
    server_name  localhost;

    access_log  /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log debug;

    root   /var/www/html;

    location ~ ^/laravelapp/(.+\.(?:gif|jpg|css|png|svg)$) {
        alias /var/www/html/laravelapp/public/$1;
        break;
    }

    location ~ ^/laravelapp((/)?(.+))?$ {
        alias /var/www/html/laravelapp/public/; # 最後に / が必要

        index index.php; # serverディレクティブ直下に書いてもOK

        try_files $uri $uri/ @admin;

        location ~ [^/]\.php(/|$) {
            # (.+\.php) => $fastcgi_script_name   (/.*) => $fastcgi_path_info
            fastcgi_split_path_info ^(.+?\.php)(/.*)$;
            # FastCGIサーバへリクエストをプロキシする
            fastcgi_pass phpapp:9000;
            # / で終わるURIの後に追加されるファイル名を設定
            fastcgi_index index.php;
            # 設定ファイル読み込み
            include fastcgi_params;
            # FastCGIサーバへ送られるパラメタ
            fastcgi_param SCRIPT_FILENAME ${document_root}index.php;
            fastcgi_param PATH_INFO $fastcgi_path_info;
        }
    }

    location @admin {
        rewrite /laravelapp(.*)$ /laravelapp/index.php$1 last;
    }
}
  • 重要なのは、locationディレクティブ内でaliasでパスを指定しているところ。
    • 今回のディレクトリ構成の場合、rootを使うとどうやってもうまくいかなかった。
    • パス指定の最後に / が無いとアクセスが publicindex.phpのように解釈されて動かなかった。
  • 解釈が合っているか分からないが、
    • try_filesで@adminが適用されるとrewriteがかかり、laravelapp/index.phpへのアクセスになる。
    • そうすると location ~ [^/].php(/|$) のディレクティブに合致し、php-pfmへアクセスが流れる。
    • laravelはpublic/index.phpがフロントコントローラとして動くがURLにindex.phpは通常含めないので、上記のようなrewrite設定が必要になる。
    • その際、URLにindex.phpは含まれないので、nginx側でindex index.phpの設定をしている。

php-fpmの設定

Dockerfileの中身は下記の通り、ホスト側で用意したスクリプトrun.shをコンテナ内で実行する。

Dockerfile

FROM php:7.1.8-fpm

ADD run.sh /tmp/run.sh
RUN chmod +x /tmp/run.sh && sh /tmp/run.sh

run.sh

#/bin/bash

apt-get update && apt-get install -y \
  git \
  zip \
  unzip \
  libpq-dev \

# install composer
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer

# install laravel
composer global require "laravel/installer=~1.1"

ちなみにlaravelアプリはphp-fpmコンテナができたらコンテナ内に入ってcreate-projectで新規作成するか、既存のソースコードをmyapp/htmlの直下に置く。

ここまできたらdockerを起動してコンテナを作る。

$ docker-compose up -d

http://localhost:19080/laravelapp/ をブラウザで開き、Laravelトップ画面が出たら成功。

ちなみに最後のスラッシュ(トレイリングスラッシュ=trailing slash)をなくしたい場合は、nginx側で別途設定が必要。

その他はまったところ

サブディレクトリでの動作に直接は関係ないが、今回のdocker構成ではまったところをメモ。

  • セッションが保持されない
    • => php.iniを見ると「session.auto_start = 0」であった。1に変更する。

サブディレクトリ対応はApacheでやればaliasを設定するだけですぐできたが、nginxは一筋なわではいかなかった。。

ユーザをグループに追加することで別ユーザのディレクトリに書き込めるようにした話

グループに追加することで書き込み権限を得る話

やりたいこと

appユーザに所有権があるディレクトリに対して、hogeユーザがファイル書き込みを行いたい。

=> appユーザが属するグループにhogeユーザを追加する

手順

初期状態(ユーザ確認)

# cat /etc/passwd
~ 省略 ~
app:x:500:500::/home/app:/bin/bash
hoge:x:501:501::/home/hoge:/bin/bash

対象ディレクトリ作成(今回は新規作成)

# mkdir /var/test
# ls -ld /var/test
drwxr-xr-x 2 root root 4096 Feb  7 01:25 test

appユーザに所有権を与える

# chown -R app:app /var/test

appユーザは書き込みできることを確認

# su - app
$ touch /var/test/test.log
$ ll /var/test/
-rw-rw-r-- 1 app app 0 Feb  7 01:25 test.log

ここからが本題

hogeユーザは書き込みできないことを確認

# su - hoge
$ touch /var/test/hoge.log
touch: cannot touch `/var/test/hoge.log': Permission denied

グループの確認

# groups app
app : app

# groups hoge
hoge : hoge

hogeユーザをappグループに追加

# usermod -aG app hoge

appグループに書き込み権限が無ければ与える

# ls -ld /var/test
drwxr-xr-x 2 app  app  4096 Feb  7 01:25 test
# chmod -R g+wr /var/test
# ls -ld /var/test
drwxrwxr-x 2 app  app  4096 Feb  7 01:25 test

追加確認

# groups hoge
hoge : hoge app

hogeユーザが書き込めるようになったことを確認

# su - hoge
$ touch /var/test/hoge.log
$ ll /var/test/
-rw-rw-r-- 1 hoge hoge  0 Feb  7 01:36 hoge.log
-rw-rw-r-- 1 app  app  28 Feb  7 01:26 test.log

(おまけ) ファイルのグループはディレクトリのグループを継承するようにする

対象ディレクトリにSGIDビットを立てることで可能となる。

実施前

# ls -ld /var/test
drwxrwxr-x 2 app  app  4096 Feb  7 01:25 test

SGIDビットを立てる

# chmod 2774 /var/test

確認

# ls -ld /var/test
drwxrwsr-- 2 app app 4096 Feb  7 01:37 /var/test

ファイルを作る

# su - hoge
$ touch /var/test/hoge2.log
$ ll /var/test/
-rw-rw-r-- 1 hoge app   0 Feb  7 01:37 hoge2.log
-rw-rw-r-- 1 hoge hoge  0 Feb  7 01:36 hoge.log
-rw-rw-r-- 1 app  app  28 Feb  7 01:26 test.log

以上

iPhoneの画面が割れたので修復までの過程をまとめた

先日iPhoneの画面が割れてしまいました。

しかし思ったほど修理に時間が掛からなかったので、 誰かの参考になればと思い、過程をまとめておきます。

最初にまとめ

  • 画面が割れてから修理済みのiPhoneを手に取るまでの時間 : 約24時間
  • かかった費用 : 3672円(Apple Careサポート有の場合)
  • 修理依頼したお店 : Apple正規サービスプロバイダ(ビックロ新宿東口店)

画面破壊

外出してて、駅のホームでうっかり落とす。

f:id:l-light-note:20180923210746j:plain:w300

近くのauショップ

  • キャリアがauだったので、とりあえずauへ。
  • Apple Careのサポートがあることを確認。
  • しかし、auショップではiPhoneの修理は受けられないので、 Appleストアか正規のサービスプロバイダへ行くように言われる。

新宿のビックロ

  • 新宿にはAppleストアもあるが混んでそうだったので。
  • 予約がいっぱいなので今日の受付は無理と言われる
  • 翌日10時に当日受付の予約枠が解放されるので、ネットで予約してから来るのがおすすめ、と言われる。

予約状況の確認

Apple以外でiPhone修理をしてくれる店を探す

  • 幸いたくさん見つかるが、料金は少々高め
  • Apple以外でiPhone端末を開けられると、Apple Careの期間内であっても、もうAppleのサポートは受けられなくなるのが気になる。。
  • でも今までサポートを受けたこともないし、残りのサポート期間が過ぎて更新しなければればどのみちサポートは受けられなくなるし、、
  • 何よりしばらく壊れたガラス画面を使うのは指が危なかった。
  • とりあえず翌日10時にAppleストアで予約が取れなければ修理屋に頼むことに決めた

(翌日)予約状況の確認

  • 8時頃見てみたら、本日受付が可能な店舗がちらほら!
  • ただしもたもたしてるとすぐになくなってしまう。
  • これは10時まで待つ必要ないな!と思い、こまめに予約状況を更新
  • 行きやすい店舗で当日予約枠が表示されたので、予約ゲット(確認画面もなくすぐに取れ、メールが届いた)

f:id:l-light-note:20180930150904p:plain:w300

  • この時、少しでも早い時間で、かつ自宅から近い店舗をと思っていたので、先に取れた二子玉川秋葉原、銀座はキャンセル。
  • 一人で複数の予約が取れるようですが、予約を取りたい人の迷惑になるのでやめましょう。

予約時間に店舗へ(ビックロ新宿東口店)

  • 15分程度待ったが、受付をしてくれた。
  • 予約が無い当日受付もいたが、やはりかなり待つ模様 f:id:l-light-note:20180924131527j:plain:w300
  • 諸々説明を受け、指定の時間に取りに来るよう言われる。約1時間後だった。
  • バッテリー交換もしてくれるとの事なので、お願いした(サポートに入っているので無償)

指定の時間に受け取りへ

  • 30分くらい待ったが、無事画面が綺麗になったiPhoneを取り戻した! f:id:l-light-note:20180924170953j:plain:w300

感想

最近はバッテリー交換のために予約がいっぱいとのことだが、 中1日で完全に復活してくれて大変助かりました。

近くに対応してくれる店舗があって良かったが、 そうでない人は配送になると1週間くらいは手元にない状況になるのだろうか。。

あと予約が基本的にネット経由なので、普段ネットを使わない人だと手間取りそう。 サポートアプリもあるけど、もし本体が壊れてたら見えないし、 パソコンが無い人もいるだろうから。。 AppleIDも普段使わないから忘れがちだし。

連休が少し潰れましたが、無事に修理してくれたので良かったです。 端末サポートは大事。

久しぶりにRailsのbundle installではまった件

windows10のホスト上にvagrantCentOSを載せている環境。

bundle installを実行したら以下のエラー。

$ bundle install --path=vendor/bundle

~略~

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /vagrant/privatepj/rails42/vendor/bundle/ruby/2.3.0/gems/nokogiri-1.8.2/ext/nokogiri
/root/.rbenv/versions/2.3.4/bin/ruby -r ./siteconf20180608-12060-kysmi7.rb extconf.rb
checking if the C compiler accepts ... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/root/.rbenv/versions/2.3.4/bin/$(RUBY_BASE_NAME)
        --help
        --clean
/root/.rbenv/versions/2.3.4/lib/ruby/2.3.0/mkmf.rb:456:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
        from /root/.rbenv/versions/2.3.4/lib/ruby/2.3.0/mkmf.rb:571:in `block in try_compile'
        from /root/.rbenv/versions/2.3.4/lib/ruby/2.3.0/mkmf.rb:522:in `with_werror'
        from /root/.rbenv/versions/2.3.4/lib/ruby/2.3.0/mkmf.rb:571:in `try_compile'
        from extconf.rb:138:in `nokogiri_try_compile'
        from extconf.rb:162:in `block in add_cflags'
        from /root/.rbenv/versions/2.3.4/lib/ruby/2.3.0/mkmf.rb:629:in `with_cflags'
        from extconf.rb:161:in `add_cflags'
        from extconf.rb:410:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /vagrant/privatepj/rails42/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0-static/nokogiri-1.8.2/mkmf.log

Text file busy @ unlink_internal - ./siteconf20180608-12060-kysmi7.rb

Gem files will remain installed in /vagrant/privatepj/rails42/vendor/bundle/ruby/2.3.0/gems/nokogiri-1.8.2 for inspection.
Results logged to /vagrant/privatepj/rails42/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0-static/nokogiri-1.8.2/gem_make.out

An error occurred while installing nokogiri (1.8.2), and Bundler cannot continue.
Make sure that `gem install nokogiri -v '1.8.2'` succeeds before bundling.

原因はプロジェクトの作成先ディレクトリが、vagrantWindowsと共有しているフォルダだったための模様。 確かにエラーログを見るとビルドそのものに失敗している感じだった。

こういうエラー、久しぶりだ。Rails学習の初期にWindows上に環境作ろうとしてよくはまってた。 最近は仮想環境かMac上にしか作ってなかったから油断した。

てか、まだWindowsと相性悪いのかRails。。

Railsのrequestで取得できる情報の具体例をまとめた

Railsのリクエスト情報取得のメソッドがたくさんあってよく分からなかったのでまとめました。

request.path
  # /book/231/reg/top
-----
request.fullpath
  # /book/231/reg/top?uid=d4f4efb0-a2dc&type=2
request.original_fullpath
  # /book/231/reg/top?uid=d4f4efb0-a2dc&type=2
-----
request.url
  # https://test.jp/book/231/reg/top?uid=d4f4efb0-a2dc&type=2
  => protocolがhttpまたはhttpsの2択
request.original_url
  # https://test.jp/book/231/reg/top?uid=d4f4efb0-a2dc&type=2
  => base_urlを使っており、その中でschemaメソッド内を使ってget_headerメソッドでプロトコルを取得しているので、http/https以外でもOK

解説

fullpathとoriginal_fullpathの違い

original_fullpathは環境変数"ORIGINAL_FULLPATH"に値があれば優先して使用する。 なければfullpathを使うので、ほぼ同じ意味のようです。

def original_fullpath
  @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath)
end

urlとoriginal_urlの違い

urlの場合はプロトコルがhttpかhttpsの2択、original_urlはhttp(s)以外も扱える。

# actionpack/lib/action_dispatch/http/url.rb
def url
  protocol + host_with_port + fullpath
end

def protocol
  @protocol ||= ssl? ? "https://" : "http://"
end
# actionpack/lib/action_dispatch/http/request.rb
def original_url
  base_url + original_fullpath
end

# lib/rack/request.rb
def base_url
  url = "#{scheme}://#{host}"
  url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
  url
end

# lib/rack/request.rb
def scheme
  if get_header(HTTPS) == 'on'
    'https'
  elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
    'https'
  elsif get_header(HTTP_X_FORWARDED_SCHEME)
    get_header(HTTP_X_FORWARDED_SCHEME)
  elsif get_header(HTTP_X_FORWARDED_PROTO)
    get_header(HTTP_X_FORWARDED_PROTO).split(',')[0]
  else
    get_header(RACK_URL_SCHEME)
  end
end

参考 https://easyramble.com/rails-request-url-original_url-differences.html

webpack-dev-serverが起動できず Unexpected token: name

webpackでローカルサーバを起動するためにwebpack-dev-serverを起動しようとしたら Unexpected token: name (urlParts)というエラーが出た。 日本語の解決記事が見つからなかったので残す。ちなみに原因はバージョン差異ということ以外は不明。。

環境

Mac OS X 10.11.6
npm 5.4.2
node v8.7.0 (nodebrewで管理)

経緯

# package.json
{
  "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "devDependencies": {
    "webpack": "^3.8.0"
  }
}

# webpack.config.js
var webpack = require('webpack');
module.exports = {
  entry: './src/main.js',
  output: {
    path: `${__dirname}/build`,
    filename: 'bundle.js'
  },
  devServer: {
    contentBase: 'build',
    port: 8081
  },
  devtool: 'source-map',
  plugins: [
    // JSファイルのminifyを実行する
    new webpack.optimize.UglifyJsPlugin({
      // minify時でもソースマップを利用する
      sourceMap: true
    })
  ]
};

上記のpackage.jsonでサーバを起動したら、下記エラーが発生。

$ npm run start

> @ start /Users/hogehoge/study/webpack/myproject
> webpack-dev-server

(...省略...)

ERROR in bundle.js from UglifyJs
Unexpected token: name (urlParts) [(webpack)-dev-server/client?http:/localhost:8081:24,0][bundle.js:3694,4]
webpack: Failed to compile.

ググった結果、webpack-dev-serverのバージョンが新しいため?との情報 を得たので、 バージョンを落として再度インストールしてみた。 https://github.com/webpack/webpack-dev-server/issues/1101

$ npm uninstall webpack-dev-server
$ npm install -D webpack-dev-server@2.7.1

再度npm run startを実行すると、無事にサーバが起動した。

なおバージョンはどこまであげられるのか試したところ、2.8.0では同様のエラーとなってしまったため、 2.7系の最新である2.7.1までという結果になった。