patorashのブログ

方向性はまだない

続・WindowsのVagrantでフォルダ共有でハマったのでメモ

先日、WindowsVagrantのフォルダ共有でハマったのでメモ、という記事を書きました。

patorash.hatenablog.com

この時に、SMBでフォルダ共有をする際にipconfigで取得したIPをsmb_hostに設定すると書いていたのですが、PCを再起動したらこのIPが変わってしまいました😰PCを再起動する度にipconfigをしてsmb_hostを書き換えるのは現実的ではないな…と思ったので、回避策を探したのですが、全然見つかりませんでした。そもそもsmb_hostを設定していない例ばっかりが出てくるのです。これはおかしい…。

PS > vagrant reload
==> default: Attempting graceful shutdown of VM...
    default: Configuring the VM...
==> default: Starting the machine...
==> default: Waiting for the machine to report its IP address...
    default: Timeout: 120 seconds
    default: IP: fe80::215:5dff:fe8f:1b2a
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: fe80::215:5dff:fe8f:1b2a:22
    default: SSH username: vagrant
    default: SSH auth method: private key
==> default: Machine booted and ready!

Vagrant requires administrator access for pruning SMB shares and
may request access to complete removal of stale shares.
==> default: Preparing SMB shared folders...

Vagrant requires administrator access to create SMB shares and
may request access to complete setup of configured shares.
==> default: Mounting SMB shared folders...
We couldn't detect an IP address that was routable to this
machine from the guest machine! Please verify networking is properly
setup in the guest machine and that it is able to access this
host.

As another option, you can manually specify an IP for the machine
to mount from using the `smb_host` option to the synced folder.

仕方がないのでホスト側のIPを取得する処理をVagrantfileに書く、というアプローチを取ろうとしたのですが、インターフェースの情報は取れているのに、IPは取れない。そもそもそれが出来ていたらVagrantもできてるやろ…と後で思いましたが、以下のを試していました。

qiita.com

これをやってもAddrオブジェクトがnilを返してダメでした。

その後、CentOSにしてみる、SSHして固定IPを設定するなど試しましたが、全部ダメ😥。ゲスト側からホスト側のIPにpingを打っても音沙汰なし。しかし、ホスト側からゲスト側のIPにはpingが通りました。ここで再び、ホスト側(Windows)のネットワークの設定を疑いました。

結論からいうと、Vagrantで作られたDefault Switchのネットワークがデフォルトでパブリックになっていることが原因でした。パブリックの場合、他のPCなどから自分のPCが見えないようにする設定がされています。そのため、ゲスト側からホスト側が見えなかったのでしょう。Default SwitchのネットワークはPCの中だけだし、プライベートに変更しました。

カスペルスキーの設定にはなりますが、以下のように設定していきます。

f:id:patorash:20190413120548p:plain
設定画面を開く

f:id:patorash:20190421031215p:plain
ネットワークをクリックする

f:id:patorash:20190421031221p:plain
Vagrantで選択したネットワークがパブリックだったらプライベートに変更する

これで、SMBの設定ではsmb_hostを省略できるようになりました👌設定は以下の通り。

# フォルダ共有設定
config.vm.synced_folder ".",
  "/vagrant",
  create: true,
  type: "smb",
  mount_options: ["vers=3.0"],
  smb_username: '**********' # Windowsのログインユーザー名
  smb_password: '**********', # Windowsのログインパスワード

教訓

ググっても解決策がない場合、FireWallとネットワークの公開範囲設定を疑おう👊

WindowsのVagrantでフォルダ共有でハマったのでメモ

Windowsで開発環境構築をしようとしていていろいろ模索中。

VagrantUbuntuを入れてフォルダ共有で作業しやすくしようと思って色々試したのだけれど、かなり四苦八苦したのでメモを残す。 なお、VagrantのプロバイダはHyper-Vで入れてる。

NFSでファイル共有できず

よく見かける記事はNFSで共有とあるのだけれど、

Windows users: NFS folders do not work on Windows hosts. Vagrant will ignore your request for NFS synced folders on Windows.

と書いてあるように、使えないらしい。

www.vagrantup.com

が、vagrant-winnfsdプラグインを入れれば使えるらしい。

が!😰結論からいうとHyper-Vを使っているからなのか、使えなかった。詳細は、以下のissueを見るとよい。

github.com

結局、vagrant-winnfsdプラグインは削除した。VitrualBoxを使っている人は問題ないと思います😊

sambaで共有する

先ほどのissueに書いてあるけれど、Hyper-Vでやる場合はsambaがいいらしい。速さも遜色ないよ、と。

ということで、sambaでやることに。smb_usernamesmb_passwordは書いておくとvagrant upの都度聞かれなくて済む。 ただ、ハードコードするのはなんか嫌なので、環境変数とかにしたい(後の課題とする)。

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-18.04"
  config.vm.provider "hyperv"

  # フォルダ共有設定
  config.vm.synced_folder ".",
    "/vagrant",
    create: true,
    type: "smb",
    mount_options: ["vers=3.0"],
    smb_host: '172.17.112.161', # ipconfigで取得したvEthernet (Default Switch)のIP
    smb_username: '**********' # Windowsのログインユーザー名
    smb_password: '**********', # Windowsのログインパスワード

  config.vm.provider "hyperv" do |hyperv|
    hyperv.cpus = 4
    hyperv.memory = 4096
    hyperv.maxmemory = 1024 * 16
  end
end

ファイアウォールの設定を変更する

設定はこれでOKなのだけれど、まだダメだった…。 原因は、Windowsに入れてるアンチウィルスソフトのファイアウォール機能だった。これに気づけなくて数時間を無駄にした😫

私はアンチウィルスソフトにカスペルスキーを使っているので、その変更方法を書いておく。

f:id:patorash:20190413120548p:plain
設定画面を開く

f:id:patorash:20190413120626p:plain
ファイアウォールの設定画面を開く

f:id:patorash:20190413120755p:plain
パケットルールの設定を開く

f:id:patorash:20190413120943p:plain
Local Servicesの設定を、ブロックから許可に変更する

これでsambaで使用するポートが解放されるため、WindowsVMが通信できるようになった。

ActiveRecordのmodel名が〜Typeで終わるものをgraphql-rubyで扱う

なかなか情報がなかったので、書いときます。

model名が◯◯Typeで終わることって、ままあるのかなと思います。CompanyモデルとCompanyTypeモデルのような感じで。これをGraphQLでTypeを定義しようとすると、Types::CompanyTypeTypes::CompanyTypeTypeになってしまいます。読みにくすぎる…。

後ろにTypeがつかないといかんのか?っていうところも疑問だったのですが、別にそんなルールもないみたいでした。 GraphQL - Scalars をみていたら、class Types::Url < Types::BaseScalarとやっていたので。

また、デフォルトのディレクトリ構造のままだとtypesディレクトリに大量のファイルが作られて見辛くなってきたので、ディレクトリを分けました。

blog.spacemarket.com

ここを参考に、ほぼ同様の構成にしました。

app/graphql/enum_types/
           /input_types/
           /interface_types/
           /mutations/
           /object_types/
           /scalar_types/
           /types/
           /union_types/

ディレクトリ名を名前空間として、ファイル名をクラス名にした構成にしないといけないため、ファイルの移動後はリネーム祭りに。 Types::FooTypeObjectTypes::Fooのように修正です。

これで、Types::CompanyTypeObjectTypes::Companyへ、Types::CompanyTypeTypeObjectTypes::CompanyTypeへと分かりやすく変更できてよかった!と思っていたのですが、これだけだと動きません。

query {
  company(id: 1) {
    id
    name
    companyType {
      id
      name
    }
  }
}

上記のようなクエリを発行しようとすると、エラーで落ちました。

{
  "error": {
    "message": "Duplicate type definition found for name 'Company' at 'Field Company.companyType's return type' (ObjectTypes::Company, ObjectTypes::CompanyType)",
    "backtrace": [
      # 略

Companyで定義されているcompanyTypeに当てはまるTypeが重複してるぞ!って言われてます。

module ObjectTypes
  class Company < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :company_type, ObjectTypes::CompanyType, null: false # ここ
  end
end

いやいや、ObjectTypes::CompanyTypeって指定してるじゃん!って思うんですが、メタプログラミングしてるんでしょう。多分、別名をつけるメソッドとかあるだろう…と思ってググるDSL形式の頃のgraphql-rubyのコードだったけれど、name = 'CompanyType'みたいなことをしているページを発見。

Shinosaka.rb #27 (GraphQL) に参加した - @znz blog

CompanyTypeのほうに、雑にname 'CompanyType'を追加してみます。

module ObjectTypes
  class CompanyType < Types::BaseObject
    name 'CompanyType' # 雑に追加

    field :id, ID, null: false
    field :name, String, null: false
  end
end

その後、適当にクエリを投げてみると、エラーメッセージが変わりました。

{
  "error": {
    "message": "The new name override method is `graphql_name`, not `name`. Usage: graphql_name \"CompanyType\"",
    "backtrace": [
      # 略

graphql_nameメソッドで別名を定義しろと。

module ObjectTypes
  class CompanyType < Types::BaseObject
    graphql_name 'CompanyType' # 修正

    field :id, ID, null: false
    field :name, String, null: false
  end
end

すると、エラーが消え、ちゃんとcompany(id: 1)に紐づくCompanyTypeの情報が取得できました😀

追記:rails cで検証

検証のために、該当箇所をコメントアウトしてみます。

module ObjectTypes
  class CompanyType < Types::BaseObject
    # graphql_name 'CompanyType' # コメントアウト

    field :id, ID, null: false
    field :name, String, null: false
  end
end

rails cして確認します。

$ bin/rails c
Loading development environment (Rails 5.2.2.1)

Frame number: 0/16
[1] pry(main)> ObjectTypes::CompanyType.graphql_name
=> "Company"
[2] pry(main)> ObjectTypes::Company.graphql_name
=> "Company"

CompanyTypeのgrqphql_nameはCompanyとなってしまい、重複してますね。その後、コメントアウトを解除して再度rails cしてみます。

$ bin/rails c
Loading development environment (Rails 5.2.2.1)

Frame number: 0/16
[1] pry(main)> ObjectTypes::CompanyType.graphql_name
=> "CompanyType"
[2] pry(main)> ObjectTypes::Company.graphql_name
=> "Company"

重複しなくなったのでOK!

GraphQLのスキーマ情報からドキュメントを生成する

漠然と考えていたことが既に書かれていたので、非常に参考になったし面白かった。

developer.kaizenplatform.com

GraphQL APIの設計を中心にフロントエンド、バックエンドをそれぞれ作っていくと、それぞれの実装待ちが発生しなくてよい。しかもAPI仕様のドキュメントを作成することも簡単だった。

graphql-rubyからGraphQLのスキーマ情報を作成する

Railsプロジェクトでgraphql-rubyを使ってスキーマ情報を生成するのは簡単。Rakefileに以下を追記して、rake taskを追加する。

#!/usr/bin/env rake

require File.expand_path('../config/application', __FILE__)
require 'graphql/rake_task' # 追加
Rails.application.load_tasks

GraphQL::RakeTask.new(schema_name: 'SampleSchema') # 追加

確認してみる。

$ bin/rake -T | grep graphql
rake graphql:pro:validate[gem_version] # Get the checksum of a graphql-pro version and compare it to published versions on GitHub and graphql-ruby.org
rake graphql:schema:dump               # Dump the schema to JSON and IDL
rake graphql:schema:idl                # Dump the schema to IDL in ./schema.graphql
rake graphql:schema:json               # Dump the schema to JSON in ./schema.json

rake graphql:schema:dumpで、schema.graphqlとschema.jsonを生成してくれるようになった。

ドキュメントHTMLの生成

ドキュメントHTMLの生成も簡単だけれど、実は結構ハマった。

さきほどの参考記事では、graphdocが紹介されていて、最初はそれを使おうとしたのだけれど、schema.graphqlからドキュメントを作成するときにエラーになった。どうもコメントに対応できていないらしい…。また、リポジトリが1年以上メンテされてないみたいでこれ大丈夫か???と思っていた。 すると、どうもgraphidocsにフォークしてメンテナンスされているらしい。そのため、npmパッケージの名前もgraphdocからgraphidocsに変わっていた。

graphidocsorg.github.io

以下で、グローバル環境にインストールできる。

npm install -g @graphidocs/docs

しかし、node-gyp rebuildが走ってエラーが出ていた(ただ、インストール自体は成功していた…)。 解決方法がわからなかったので(Nodeを6系までダウングレードすれば通る、みたいなのはあったが、今は10系を使っているから嫌だった)、エラーメッセージは放置した。

ただ、プロジェクトのドキュメントを作るのにグローバルにインストールしたくないなぁと思って、そこは変更した。

$ yarn add @graphidocs/docs --dev

エンドポイントからドキュメントを作る

既にGraphQLのAPIがあるのであれば、そこをエンドポイントして指定して実行すれば、生成してくれる。ローカルでやるのであれば、bin/rails sをした状態で、以下を実行する

$ yarn run graphidocs -e http://localhost:3000/graphql -o ./doc/schema

毎回オプションをつけるのは大変なので、package.jsonにオプションを設定しておくことも可能。

{
  "dependencies": {
    # 省略
  },
  "devDependencies": {
    "@graphidocs/docs": "^1.0.4"
  },
  "graphidocs": {
    "endpoint": "http://localhost:3000/graphql",
    "output": "./doc/schema"
  }
}

こうすると、以下でOK。

$ yarn run graphidocs

スキーマからドキュメントを作る

しかし、エンドポイントからドキュメントを作るのでは、さきほどの参考記事のようにフロントエンド、バックエンドでAPI仕様を議論しながらは進められない。必ずバックエンドの実装後になってしまうので、やはりスキーマ情報からドキュメントを作りたい。

スキーマ情報からドキュメントを作るには、オプションを変更する。既にある程度API実装があるのであれば、先ほど追加したrake taskを実行して、スキーマ情報をダンプしておく。

$ bin/rake graphql:schema:dump # schema.graphqlとschema.jsonを生成
$ yarn run graphidocs -schema ./schema.graphql -o ./doc/schema

ここでは、schema.graphqlを指定したが、schema.jsonでも動く。

こちらも毎回オプションをつけるのは大変なので、package.jsonにオプションを設定できる。

{
  "dependencies": {
    # 省略
  },
  "devDependencies": {
    "@graphidocs/docs": "^1.0.4"
  },
  "graphidocs": {
    "schemaFile": "./schema.graphql",
    "output": "./doc/schema"
  }
}

こうすると、以下でOK。

$ yarn run graphidocs

これで、schema.graphqlを介してフロントエンド、バックエンドの人で意見交換して、ドキュメントを生成してそれぞれが開発する土台ができたかなと思う。

GraphQLのドキュメントを見るためのWebサーバを定義する

ドキュメントは./doc/schema配下にできたものの、そこを開きにいくのも面倒なので、WEBrickでWebサーバを作った。 プロジェクトのルートフォルダにgraphidocs.rbを作成した。

# graphidocsで生成したgraphqlのスキーマ情報を見るためのサーバを立ち上げます。
# ./doc/schemaが存在しないか古い場合、まず`yarn run graphidocs`を実行してください。
# その後、`ruby graphidocs.rb`を実行してください。
require 'webrick'
include WEBrick
server = HTTPServer.new(
  DocumentRoot: './doc/schema',
  BindAddress: '0.0.0.0',
  Port: 8000
)

%w(INT TERM).each do |sig|
  trap(sig) { server.shutdown }
end
server.start

これを実行する。

$ ruby graphidocs.rb
[2019-04-03 10:53:47] INFO  WEBrick 1.4.2
[2019-04-03 10:53:47] INFO  ruby 2.5.5 (2019-03-15) [x86_64-darwin16]
[2019-04-03 10:53:47] INFO  WEBrick::HTTPServer#start: pid=4858 port=8000

http://localhost:8000 にアクセスすると、GraphQLのドキュメントが読めるようになった。サーバの終了はCtrl + CでOK。

プロジェクト配下のnodeのコマンドを実行する

Rubyでいうところのbundle exec 〜に該当するNodeのコマンドってなんだろう?と思って雑に呟いたら、色々と教えてもらえました。

というわけで、yarn run 〜がよさそう。npxもいいけれど、間違えて入れていないパッケージの命令を書いたらその場でダウンロードが始まるので遅いし。

GraphQLでWebAPIを作っている

自分が担当のプロダクトのWebAPIを整理していかんとほんまヤバいなぁ〜と思いつつ、数年過ごしていましたが、ここ最近で急激にGraphQL熱が湧き上がり、今お試しで実装していってる最中です。

ビビッときたのは、@gfxさんの書かれたGraphQL徹底入門の記事からです。

employment.en-japan.com

これを元に簡単な実装をしてみて、他の情報を探して…という感じ。

qiita.com

N+1にはgraphql-batch

概要を掴んだら、ModelにマッピングするようにTypeを生成していってたのですが、N+1問題が発生しやすいみたいな話を聞いていた通り、すぐ出始めました。graphql-batchを使えば、発生が抑えられるとのことだったので、情報を集めてやってみたところ、N+1は収まりました。

blog.agile.esm.co.jp

blog.kymmt.com

preloadして、極力クエリの発行を抑えるようになっています。ありがたい。

ちなみに最初は『preloadやeager_loadを使えば別にN+1起きないんじゃない?』と思って、Foo.preload(:bar)とかしていたのですが、これだとbarの情報にアクセスしていなくてもbarの情報を取得するためのクエリが実行されてしまいました(N+1にはならないけれど、無駄なクエリ)。graphql-batchは、barの情報にアクセスするときだけ、barをpreloadしてくれたので、graphql-batchを使った方がいいでしょう。

Loaderの実装は、サンプルからそのまま拝借しました。

graphql-batch/examples at 058213b78775c791135bf7db784b7d10007d5ade · Shopify/graphql-batch · GitHub

Loaderの記述がしんどい

とはいえ、Loaderの記述が多くなると、同じようなコードになってしまい辛いです。

module Types
  class HogeType < Types::BaseObject
    field :id, ID, null: false
    field :piyos, [Types::PiyoType], null: false
    field :fugas, [Types::FugaType], null: false

    # piyosもfugasも殆ど同じ…
    def piyos
      Loaders::AssociationLoader.for(Hoge, :piyos).load(object)
    end

    def fugas
      Loaders::AssociationLoader.for(Hoge, :fugas).load(object)
    end
  end
end

というわけでメタプログラミングします。継承元のTypes::BaseObjectにクラスメソッドpreload_associationsを追加しました。やってることは、引数のシンボル名のメソッドを定義してしまうということだけです。

module Types
  class BaseObject < GraphQL::Schema::Object

    def self.preload_associations(*assciations)
      model_name = self.class_name[0..-5] # HogeTypeのTypeを削除する
      model = Kernel.const_get model_name # model Hoge を取得
      assciations.each do |association_name|
        define_method association_name do
          Loaders::AssociationLoader.for(model, association_name).load(object)
        end
      end
    end
  end
end

これにより、preload_assciationsを呼べば済むようになりました🎉

module Types
  class HogeType < Types::BaseObject
    field :id, ID, null: false
    field :piyos, [Types::PiyoType], null: false
    field :fugas, [Types::FugaType], null: false

    # これでOK
    preload_associations :piyos, :fugas
  end
end

配列型のせいでN+1発生もLoaderで解決

PostgreSQLの配列型を一部で使っているのですが、これもN+1問題が発生しました。配列型ではRailsのAssociationを表現しているわけではないので、自分でLoadしてあげないといけません。

module Types
  class HogeType < Types::BaseObject
    field :id, ID, null: false

    # これらはAssociation
    field :piyos, [Types::PiyoType], null: false 
    field :fugas, [Types::FugaType], null: false

    preload_associations :piyos, :fugas

    # これらは配列型
    field :foos, [Types::FooType], null: false
    field :bars, [Types::BarType], null: false

    # 配列型はload_manyを使って登録しておくとよい
    def foos
      Loaders::RecordLoader.for(Foo).load_many(object.foo_ids)
    end

    def bars
      Loaders::RecordLoader.for(Bar).load_many(object.bar_ids)
    end
  end
end

今の所の感想

graphqlの実装していくのは楽しい!!graphiqlからクエリ発行して芋づる式にデータが取れるのがめちゃくちゃ面白いです。ただし、クエリのネストが深くなると途端に重くなりますが…。 まだ更新系のクエリの実装はしていないので、そちらでも知見が貯まったら何か書こうと思います。

CircleCIでエラーコードを無視して処理する

昨日書いてたやつで、rake releaseが実行されないパターンにあうとどうもエラーで落ちることがわかった。 grepの条件に該当しなかったのでエラーコードが1になっているんだが、それがダメらしい。

しゃーないから最後の行でexit 0を返すようにしてみたんだけれど、それも意味なし。どうもエラーコードが0以外になった時点でもうダメっぽい。 回避策を探していたところ、CircleCIのDiscussがヒットした。

discuss.circleci.com

set +eしろとある。これってなんなのだろう?

journal.lampetty.net

調べたところ、set -eしてあると、エラーコードに0以外が入った時点でスクリプトが終了するという設定らしい。CircleCIはそうなっているのだろう。それを解除するのが、set +eのようだ。

ローカルで該当job(release)を実行して確認したところ、成功するようになった。

$ circleci config process .circleci/config.yml > .circleci/config.processed.yml
$ circleci local execute --config .circleci/config.processed.yml --job release

# 色々あって…
Success!