Railsで定数を参照するときは完全修飾名を使った方が良い

発端

とある機能を開発していたところ、以下のエラーが出てwebsocketサーバーが起動しなくなりました。

NameError: uninitialized constant Concerns::XXXable

エラーの発生箇所は、とあるController内の include Concerns::XXXable でした。
Concerns::XXXable 自体は定義してあり、そのあたりの修正もしていなかったため困惑しましたが、どうやら調べていくとquerlyというgemをインストールしてから発生していることがわかりました。
なぜgemをインストールしただけでエラーが発生するようになったか、調査に結構時間がかかったので原因と対策をまとめました。

結論

原因: gem内でトップレベルにConcernsというnamespaceが切られていたこと

ざっくり説明すると、querly内のトップレベルのnamespaceにConcernsというnamespaceが切られていたため、その中で Concerns::XXXable を探そうとして、上記エラーが発生していました。

※すでに当該コードは下記PRで修正されています。

github.com

対策: 定数を参照するときは完全修飾名を使うようにする

標題の通り、 Site::Concerns::XXXable のように完全修飾名を使うことで予期せぬバグは発生しなくなります。
そもそも Concerns::Site::XXXable の方が良くないか?っていう話はいったん置いておきます。

詳しい原因

原因を詳しく説明すると、こんな感じです。

うちのアプリのwebsocketサーバーは、起動時に Rails.application.eager_load! を実行していました。
Rails.application.egaer_load!を実行すると

  1. autoload_pathsを先頭から探索
  2. const_missingが発生したmoduleからトップレベルの名前空間につくまで順に親を探索

の順に定数を探索しにいきます。

querlyをインストールしていない状態で include Concerns::XXXable をしているControllerを読み込むと

  1. Concernsが見つからない
  2. autload_pathsを探索してもConcerns::XXXableが見つからない
  3. 上記Controller内から順に親namespaceを探索し、Site::Concerns::XXXableが見つかる

という風にちゃんと読み込めます。

一方、querlyをインストールしている状態で include Concerns::XXXable をしているControllerを読み込むと Rails起動時にquerlyが読み込まれquerlyのConcernsが読み込まれてしまうため、 上記2の段階でquerlyのConcernsを参照してしまい、定数が見つからないというエラーが発生していました。

(全然わかりやすく説明できない...)

トップレベルにConcernsみたいなnamespaceが切られているgemをインストールしていなくても、Concerns::XXXable を読み込みにいく前に自分でConcerns(Rails標準のapp/controller/concernsなど)を読み込んでしまうと同様のエラーがおきました。

まとめ

定数を参照するときは完全修飾名を使うようにしよう!という話でした。
説明がぜんぜんうまくできませんでした...!

Lean DiagramをWebで書きたい!ので10plateでテンプレート作った

あけましておめでとうございます。
新年早々、初詣もそこそこにサブプロジェクトとして開発中のプロダクトの要件定義やコンセプトメイキングをしていました。
Lean Startupのやり方を参考に進めていたところ、Problem/Solution Fitを達成するために使うLean Diagramというものを知りました。
このLean Diagramの図を作成する良さげなツールがなかったので、10madoさんが最近リリースした10plateを使ってLean Diagramのテンプレートを作ってみました。

10plate.io

Lean Diagramテンプレートの使い方

使い方は至って簡単で、Lean Diagramを開いて、以下のGIFのように右サイドバーのテキストエリアに入力するとLean Diagramの各項目にテキストが入力されます。以上。

f:id:manchose:20180102110254g:plain

10plateを使ってみた感想

1. シンプルで使いやすい

htmlとcssでbodyタグの中身のみ記載してあげることでデザインができあがります。
そこにテンプレート化したい箇所を{{ラベル名}}のように囲ってあげると上記GIFのように右サイドバーに入力欄が表示されます。
特に説明を見ずとも作ることができ、またドキュメントもしっかり用意されているので使いやすかったです。
作ろうと思ってからだいたい1時間で作成できました。

2. テンプレート化する際の表現力が豊富

テンプレート化する際のオプションが豊富に用意されており、デフォルト値を入力しておいたり、プルダウンの選択式にしたりとかゆいところに手が届いています。
APIもすでに用意されているので使い方は色々ありそうです。
詳細はドキュメントをご参照ください。

まとめ

表現力が豊富でAPIも用意されているので応用範囲の広いサービスだなという所感です。
こういうテンプレート作成サービスはキラーコンテンツ(キラーテンプレート)があるとどんどんテンプレートが増えていきそうなので、誰かバズるやつを作ってくれないかな〜って思ってます。
canvacacooは作図ツールなので、こういう人によって図が変わらないものを作ったり共有する場合には不向きです。
そのため、Lean Diagramなどのフレームワークには10plateを使った方が便利だと思いました。

今回作ったテンプレートは高さが伸びたときにデザインが少し崩れたり細部を表現できていなかったりするので、svgcssでデザイン修正をしようと思います。
ソースコードは↓です。普段cssを書かないのでかなり適当です。そのあたり詳しい人レビューコメントとか書いていただけるとめっちゃうれしいです。

github.com

あとはリーンキャンバスのテンプレートなんかもあると便利そうなので作ろうと思います。

AWS WAFで環境毎にCDN(CloudFront+S3)を立てた

この記事は Speeeアドベントカレンダー 18日目 の記事になります。 昨日(一昨日)は、@bino98さんによる、「Slackでヘタクソな日本語/英語の校正をしてくれる「Botlint」を作ってる」でした。

この記事ではAWS WAFを使って環境ごとにCDN環境を構築する方法についてまとめます。

やりたいこと

僕の担当サービスでは、Railsアプリのpublicディレクトリに配置したアセットをnginxで配信するという構成になっていました。

これを速度改善の一環として、S3に配置したアセットをCloudFront経由で配信するという構成に変更することになりました。 そのために行ったことをまとめておきます。

要件

ステージング環境もできるだけ本番環境に近づけるため、ProductionだけでなくStagingとStaging2(ステージング環境が2つある)にもCDNをたてることにしました。
StagingとStaging2には社外からアクセスしてほしくないので自社IPからのみ許可します。
また、S3バケットにURLで直接アクセスすることは制限します。

構成と設定の概要

上記要件を考慮した結果、以下のような構成・設定になりました。

f:id:manchose:20171219105014p:plain

構築の流れ

1. Amazon S3の設定

バケット作成時にアクセス権限を以下の設定にします。

このバケットにパブリック読み取りアクセス権限を付与しない (推奨)

こうすることで、S3のURLでオブジェクトにアクセスすることができなくなります。
全ての環境(Prod/Stg/Stg2)で同じバケットを利用してパスで出し分けることも考えたのですが、以下の理由でバケットをわけることにしました。

  • URLを変更することで別環境のアセットにアクセスできてしまう
  • terraformでバケットを作成する場合、すでにS3バケットが存在してしまうとコケる。

2. AWS WAFの設定

consoleでCreate Web ACLsをクリックするとセットアップウィザードが立ち上がるので、以下の項目を設定していきます。

IP Match Condition

許可/拒否に使うIPアドレスを登録します。
今回は3つIPアドレスを登録しました。

Rules

上記で登録したIPアドレスそれぞれに対して条件式を登録します。
それぞれに以下のRulesを作成しました。

When a request {does} {originate from an IP address in} {IP Match Condition}

これで「リクエストが上記で登録した3つの場合は」という条件式が登録されました。

Whitelist方式かBlacklist方式かを選択

上記で作成したRule(条件式)に当てはまった場合に「アクセスを許可するのか/アクセスを拒否するのか」を選択します。
また、Ruleに当てはまらなかった場合に「アクセス許可するのか/アクセス拒否するのか」も選択します。

3. Distributionの設定

Distributionも環境毎に作成しました。
理由は以下の2つです。

  • 本番にはWAFを設定しないので、少なくとも本番とそれ以外でDistributionをわける必要がある。
  • 1つのDistributionに複数のOriginを設定して送り先を振り分けるのは面倒そう。
Restrict Bucket Access

この設定をすることで、S3へのアクセスはオリジンアクセスアイデンティティからしかできなくなります。
Distributionにオリジンアクセスアイデンティティを作成するという項目が表示されるので、作成して紐付けることでバケットがCloudFrontからのみアクセスを受け付けるようになります。

SSL証明書バージニア北部で発行

ACMSSL証明書を発行して利用する場合、バージニア北部で発行する必要があります。
Asia/Tokyoで発行したものを使おうとしてハマってしまったのでご注意ください。

4. Route53にAliasレコードを追加

Route53にType AでAliasレコード(AWSが独自で作っているもの)を作成する必要があります。
今回でいえば環境ごとのアセットのドメイン(assets.my-domain.jpやassets.stg.my-domain.jp)へのリクエストをDistributionのxxxxxx.cloudfront.netに投げるように設定します。

5. Asset SyncでS3にアセットをアップロード

最後にCapistranoでのデプロイ時にAsset Syncで画像を指定バケットにアップロードするようにすれば準備は完了です。
manifest.jsonファイルはRailsアプリと一緒に配布する必要があるという点には注意する必要があります。
配布を忘れるとdigestの紐付けができなくなり404がでてしまいます。

まとめ

開発を手伝った韻ノート を使ったラップ講座をしようと思っていたのですが、全然まじめな内容になってしまいました。
次こそはラップ講座をしたいと思います。
明日(今日)は @nakana0226 さんで「スポンサー活動を本気でやってみた1年でした」です!お楽しみに!

Bootstrap Night! Vol.2 に参加しました。

12/12(火)に開催されたselfreeさん主催のBootstrap Night! vol.2に参加してきました。

selfree.connpass.com

esa.ioの人の発表の中で

イベント参加後にblog書くのは比較的ハードルが低いし、主催者側うれしいし(略)オススメ

というお話があったのでその気になって参加レポを書いてみます。
(遅れて参加してしまったので主にesa.ioさんの話を書いていきます)

なぜ参加したか

職場の状況が色々かわった結果、プライベートでSaaSを作りたいという熱が高まってきました。
ただ、以下のような不安がありました。

  • 自分ひとり(ないしデザイナーさんとふたり)で運用がまわるのか
  • アウトバウンドの営業をかけないといけないのではないか(平日の日中は動けないのでそれだと厳しい)
  • サーバー費用の負担はきつくなかったか(サービスの性質にもよるが)

とくにesa.ioさんは趣味から始めたサービスだそうなので、このあたりの実際のところどうなの?を聞きたくて参加しました。

esa.io@ken_c_loさんのお話

詳しくは↓をご覧ください。

docs.esa.io

実在する人をイメージし、その人に喜んでもらえるものをつくる

実際に喜んでくれて「これがあることで自分の生活や人生が変わってきている!」と思ってもらえるものをつくるようにする。
逆に、顧客セグメントとかターゲットとかそういう抽象的なものに惑わされて想像上の世界にしかいないユーザーのためのものをつくらないように気をつけている。
というお話をされていました。
この価値観は受託開発として実際にesaを使っている企業で仕事をしていたという点でも一貫していました。
このあたり、競合調査をしていたりマーケティング系の書籍でインプットをしていたりすると知らないうちに忘れていたりするので気をつけたいです。

料金設定にもサービスの哲学が

とくに発表でおもしろかったのが料金設定のお話でした。
esa.ioの料金には◯人まで無料というプランはないのですが、その理由のひとつが

◯人までしかesaのアカウントを渡さないようにする...みたいな使い方が懸念された チームでの情報の透明性を上げるシステムなのに、同じチームでesaの情報を見れる人と見れない人の格差が出てしまうのは本末転倒

というもので、「この料金設定で効果的な使い方をしてくれるか」という観点で料金を決めたそうです。
料金設定もサービスの哲学で決めているという点には驚きましたし、シンプルにかっこいいなって思いました。

懇親会で聞いたこと

CallConnectを運営されているselfreeさんとesa.ioさんに伺いました。
TimeCrowdさんにも伺いたかったのですが、お話をすることができませんでした。

運用はちゃんとまわるのか

運用は問題なかったそうです。
もちろんサービスの性質にもよると思いますが、技術的な問い合わせや個別対応などがけっこうくるのかな?と思っていたので意外でした。

アウトバウンドの営業をかけないといけないのではないか

アウトバウンドの営業は一切していないそうです。 (説明に来てと言われて行くことはあるそうですが)
このあたりは発表でも触れられていて、コミュニティ活動やブログ・イベントでのアウトプットをしているうちに自然に初期ユーザーがついていたそうです。
イベントで発表したら、参加レポで「使ってみた」的な内容を書いてくれた人もいたそうなので、アウトプットはやっぱり重要ですね。(自戒)

サーバー費用の負担はきつくなかったか

ほぼ気にしないでよいレベルだったそうです。

参加して

三社ともスモールチームでSaaSを運営されていて軌道に乗っているサービスなので、非常にためになるお話ばかりでした。
今後もこういうイベントには参加するようにしてアウトプットももっとしていこうと思います。

フォアグラウンドでコマンドを実行した後でバックグラウンドジョブに変更する

バッチの手動実行など終了まで長時間かかるコマンドを実行する場合、 おそらくnohupでSIGHUPを受け付けないようにしてバックグラウンドジョブとして実行すると思います。 しかし、フォアグラウンドジョブとしてコマンドを実行した後で気付くことがたまにあるので、 コマンド実行後にログアウトしても実行されつづけるように変更する方法をメモしておきます。

1. ctrl-z

これでフォアグラウンドのプロセスは一時停止状態になります。 再開するときにはフォアグラウンドでの再開、もしくはバックグラウンドの再開ができます。

2. jobs -s

停止中のジョブ一覧を表示します。 一時停止したコマンドが表示されているので、その一番左の[n]の数字(=Job番号)をメモしておきます。

[1]+  Stopped                 bin/rake -T

3. bg job番号(PIDも可)

上記でメモしたJob番号を引数に指定してバックグラウンドジョブとして再開します。

4. disown %job番号

現在のシェルのジョブテーブルからジョブを削除します。 こうすることでシェルの終了時(ログアウト時)に投げられるSIGHUPを受け取らないようになります。

hオプションを付与してdisown -hと実行した場合は、シェルのジョブテーブルからジョブを削除せずにSIGHUPを受け付けなくすることができます。 このときはnohupと同様の状態になるため、ログアウトするまでは起動中のシェルでジョブを管理できます。

シンプルですが、以上メモでした。

電話番号をうまく扱うライブラリ(libphonenumber)の活用方法を考えてみる

1. 背景

社内で共有された以下の資料で、Googleが作成しているlibphonenumberという電話番号のバリデーションができるライブラリが紹介されていました。
自分が担当しているサービスは電話番号の入力が必須であり何かしら使えそうだということで活用方法を考えてみました。

2. libphonenumberの機能

僕はRubyを使用しているのでmobi/telephone_numberでできることをまとめます。

無効な電話番号を判定する

tel = TelephoneNumber.parse("00000000000", :JP)
tel.valid?
#=> false

電話番号から市区町村を特定する

以下のようにすることで電話番号から市区町村を求めることができます。 フリーダイヤルや携帯電話など市区町村が特定できない場合はnilが返却されます。

tel = TelephoneNumber.parse("0864xx29xx", :JP)
tel.location
#=> "Kurashiki, Okayama"

電話番号の種別を判別する

下記コードのように、TelephoneNumber#valid_typesメソッドを使って電話番号の種別を判別できます。

tel = TelephoneNumber.parse("08051xx93xx", :JP)
tel.valid_types
#=> [:mobile]

判別可能な番号種別

valid_types 種別
:area_code_optional ※日本では使われていないよう
:fixed_line 固定電話
:mobile 携帯電話(070, 080, 090)
:no_international_dialling 国際電話をかけることができないフリーダイヤル(0037, 0066, 0077, 0088など00から始まるフリーダイヤル)
:pager ポケベルやM2M等専用番号(020)
:personal_number 個人向け携帯電話番号(060)
:premium_rate ダイヤルQ2(0990から始まる電話番号※現在は使われていない)
:shared_cost ※日本では使われていないよう
:toll_free フリーダイヤル(0120, 0800, 0070, 0077, 0088, 0037など)
:uan ナビダイヤル(0570)
:voicemail ※日本では使われていないよう
:voip IP電話(050)

固定電話・携帯電話・個人向け携帯電話以外に対してはSMSを送信できないため、 SMSを送信するようなサービスの場合、上記 判別可能な番号種別 をもとに送信対象を絞ることでSMS利用料の削減が期待できそうです。

Dockerをawsコマンド実行環境として使いS3からファイルをディレクトリごとダウンロードする

結論

AWSのS3から特定のディレクトリ以下をごっそりダウンロードするにはawscliを使う必要があります。
ただ、awscliを用意するのが面倒だったので、awsコマンド実行環境としてDockerを使用するワンライナーを書きました。

docker run -it --rm -v ~/docker/mnt:/mnt xueshanf/awscli /bin/bash -c 'export AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY" 
export AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_ACCESS_KEY"
aws s3 cp --region ap-northeast-1 s3://bucket_name/dir_name/ /mnt --recursive'

上記ワンライナーの中の以下の環境変数に値を設定しバケットとディレクトリ名を指定してあげると、マウントした~/docker/mnt以下にファイルがダウンロードされます。

背景

なぜわざわざDockerを使っているかというと、awscliのセットアップにはPythonとpipのインストールが必要です。
しかしPythonの環境分離ツールには色々あり(pyenv, venv, virtualenvなど)、Python力が鼻くそ以下の僕にはどれを選べば良いかわからなかったので、環境を汚さないという理由でDockerを使うことにしました。

まとめ

これでローカルPCを汚さずにawsコマンドを使用できるようになりました。
Dockerは開発サーバーや本番サーバーの構築だけでなく、コマンド実行環境として使用できるのも素晴らしいですね。
以上です。