production.log

ピクスタ株式会社で開発部の部長をやっている星直史のブログです。

Re:VIEWで書いた文章の校正をCircleCIとtextlintでGitHubのPRに自動コメントする仕組み

概要

技術書典でRe:VIEWを使っていたものの、文章を書き、textlintを回すというCI環境を整えることができませんでした。また、このCI環境は、ブログ執筆においても有効であるため、このタイミングで構築することにしました。

今回はRe:VIEWで書いた文章の校正をCircleCIとtextlintでGitHubのPRに自動コメントする仕組みを作ったのでその紹介をします。

仕様🖋

仕様は下記の通りです。

  • Re:VIEWで書くファイルの拡張子である.reをこれをサポートすること
  • Markdownで書くファイルの拡張子である.mdをサポートすること
  • master以外のブランチにpushした場合にCircleCIでtextlintが実行されること
  • textlintで警告が出た場合は、GitHubのPRに自動でコメントすること

f:id:watasihasitujidesu:20181013193515p:plain

構築手順👨‍💻

textlintの導入

まず最初にローカルでtextlintが実行できるようにします。 textlintはnpmで入れていきます。

$ node -v
v8.11.3

$ npm -v
6.3.0

続いて、textlintのインストールと、textlintで使うルールのインストールを行います。

$ npm init -y
$ npm install --save-dev textlint textlint-rule-max-ten

上記で入れるtextlint-rule-max-tenは「一つの文で出現する読点の数をチェックする」ルールです。

次に、textlintのデフォルトでは、Re:VIEWの拡張子である.reに対応していないため、プラグインを入れる必要があります。

$ npm install --save-dev textlint-plugin-review

最後にtextlintの設定ファイルで、ルールとプラグインを有効にします。

{
  "rules": {
    "max-ten": true
  },
    "plugins": [
      "review"
    ]
}

この状態でtextlintを実行すると下記のような結果となります。

$ ./node_modules/.bin/textlint sample.re

/Users/naoshihoshi/repo/sample.re
  1:11  error  一つの文で""3つ以上使用しています  max-ten

✖ 1 problem (1 error, 0 warnings)

今回は便宜上、textlint-rule-max-tenだけの紹介ですが、そのほかにも様々なルールが存在するので、お好みで設定していくのが良いと思います。

CircleCIの設定

CircleCIは2017年夏あたりにバージョン2.0がリリースされましたので、今回はそれで作っていきます。

バージョン2.0からは.circleci/config.ymlの書き方が変わりましたが、公式のドキュメントがとてもわかりやすかったです。
個人的には、後述するconfig.ymlの紹介で全体感を見た後にドキュメントを読むとより理解が進みやすくなると思います。

circleci.com

.circleci/config.yml

バージョン2.0からはDockerが使えるようになります。また、CircleCIが用意しているイメージも各言語揃っています。 今回、GitHubへのコメントにRubyのgemを使うので、Rubyのイメージを使用します。

version: 2
jobs:
  build:
    docker:
      - image: circleci/ruby:2.4.1-node-browsers
    working_directory: ~/repo
    steps:
      - checkout
      - run:
          command: npm install
      - run:
          command: ./.circleci/review-textlint.sh

最終行で指定している./.circleci/review-textlint.shでtextlintの実行とGitHubのPRヘコメントを実行します。

textlintの実行とGitHubのPRヘコメント

まず、GitHubへの操作をするためのアクセストークンを取得し、CircleCIに設定をします。

GitHubのアクセストークンを取得

GitHubにアクセスし、Settings > Developer settings > Personal access tokensページに遷移し、Generate new tokenでアクセストークンを取得します。

このトークンは次で使うのでメモっておいてください。また、このトークンは、この画面を離れると二度と表示されなくなります。

GitHubのアクセストークンをCircleCIに設定

CircleCIにアクセスし、Settings > ${ユーザー名} > ${リポジトリ名} > Environment Variablesに遷移しAdd Variableボタンを押下します。
すると、モーダルが表示されるので、下記の通り入力します。

  • Name: GitHub_ACCESS_TOKEN
  • Value: GitHubで作ったアクセストークン

f:id:watasihasitujidesu:20181013190945p:plain

これで準備が整いました。最後にスクリプトを書いていきます。

.circleci/review-textlint.sh

#!/bin/bash

# Test if pull request
if [ "$CI_PULL_REQUEST" = "false" ] || [ -z "$CI_PULL_REQUEST" ]; then
  echo 'not pull request.'
  exit 0
fi

REMOTE_MASTER_BRANCH="remotes/origin/master"

gem install --no-document findbugs_translate_checkstyle_format checkstyle_filter-git saddler saddler-reporter-GitHub

# filter files and lint
echo "Put textlint review comments to GitHub"

declare diffFiles=$(git diff ${REMOTE_MASTER_BRANCH} --diff-filter=ACMR --name-only | grep -a '\.[re$|md$]')
echo ${diffFiles}

if [ -n "$diffFiles" ]; then
  echo ${diffFiles} | xargs ./node_modules/.bin/textlint -f checkstyle \
  | sed -e 's/\"/\'/g' \
  | checkstyle_filter-git diff ${REMOTE_MASTER_BRANCH} \
  | saddler report \
      --require saddler/reporter/github \
      --reporter Saddler::Reporter::Github::PullRequestReviewComment
  exit 1
fi

それでは処理をざっくり説明していきます。

GitHubのPRの存在確認
まず、GitHub上にPRがない場合はコメント先がないため、処理をスキップする条件分岐を書いています。

if [ "$CI_PULL_REQUEST" = "false" ] || [ -z "$CI_PULL_REQUEST" ]; then
  echo 'not pull request.'
  exit 0
fi

gem install
次にgem installを実行します。 今回は、GitHubへのコメントはSaddlerを使って行います。

gem install --no-document findbugs_translate_checkstyle_format checkstyle_filter-git saddler saddler-reporter-GitHub

textlint対象ファイル抽出
この処理は、textlintの対象ファイルを抽出するコマンドです。 リモートのmasterと、CircleCIで回しているリポジトリのdiffを取得し、拡張子が.reと.mdのファイルだけを抜き出しています。

declare diffFiles=$(git diff ${REMOTE_MASTER_BRANCH} --diff-filter=ACMR --name-only | grep -a '\.[re$|md$]')
echo ${diffFiles}

GitHubへコメント送信
最後に、textlintを実行した結果をXML化し、GitHubのPRへコメント送信処理を行います。
エラーがある場合は、XMLのエスケープ処理を行なった上で、GitHubへ送信します。
具体的な処理はSanddlerにお任せしています。

if [ -n "$lintResult" ]; then
  echo ${lintResult} | sed -e 's/\"/\'/g' \
  | checkstyle_filter-git diff ${REMOTE_MASTER_BRANCH} \
  | saddler report \
      --require saddler/reporter/GitHub \
      --reporter Saddler::Reporter::GitHub::PullRequestReviewComment
  exit 1
fi

処理は以上です。

動作確認🚨

まず、適当なリポジトリにブランチを切ります。
次に、.reか.mdファイルを作成します。ファイルの中身は読点を3つ以上含んだ文を書いておきます。

例)

読点が、3つ以上の、文にしておくと、GitHubに、コメントが、つくはず。

最後に、そのファイルをpushします。

CircleCIの確認

f:id:watasihasitujidesu:20181013192617p:plain

良い感じに落ちてますね!

GitHubのPRコメントの確認

f:id:watasihasitujidesu:20181013192716p:plain

ばっちりですね👏

まとめ✨

今回は、CircleCIとGitHubのPRを用いて、textlintを実行、コメントしてくれる環境を作りました。
執筆していたときは、都度textlintを実行していたのが本当にめんどくさかったです...。
今回構築してみて、比較的簡単に構築することができたので、これから執筆活動をする方は、まずこのCI環境を作ってから執筆することをお勧めします!

Re:VIEWで執筆とPDF出力を行い、印刷所に不備のない入稿をするポイントの紹介

概要

技術書典5に参加したのですが、自分で本を作るのは初めてだったので、Re:VIEWの使い方から学び、入稿して印刷所のサポートの方から指摘を受けて再入稿して、やっと物理本を完成させることができました。

今回この記事では、Re:VIEWで執筆とPDF出力を行い、印刷所に不備のない入稿をするポイントの紹介をします。

執筆開始

印刷所への入稿はPDFであれば良いため、執筆ツールはなんでも良かったりします。

  • Word
  • Google docs
  • Markdown(HTML)
  • Re:VIEW

当初、書き慣れているMarkdownで執筆をしようとしていました。 しかし、Re:VIEWを調査すると、レイアウト、目次ページ、ページ番号の挿入、表紙挿入、ノンブル設定など書籍執筆を支援する機能が充実していたので、記法を覚える学習コストを払ってでも使うべきだと考え、Re:VIEWで執筆をしました。

Re:VIEW

インストール

まずはRe:VIEWの概要を知り、インストールをしました。 gem化されていたり、Dockerでも動かせるため、導入はとても楽です。

dev.classmethod.jp

執筆

Re:VIEWは独自の記法を覚える必要があります。 ただ、覚えることはそんなに多くはありませんでした。自分はチートシートを見ながら書いていました。 下記リンクが簡潔にまとまっています。

Re:VIEWチートシート · GitHub

PDF出力設定

PDF出力するためには、下記がインストールされている必要があります。

  • Ghostscript
  • Imagemagick
  • MacTeX

下記リンクを参考にしました。 mkdir.g.hatena.ne.jp

textlint

推敲の際、日本語のチェックを機械的にやりたかったので、textlintを使いました。

efcl.info

印刷設定

執筆が終わったら入稿の準備です! サークル仲間の@kaibaさんが先陣を切って日光企画さんに不備を指摘されており、そのポイントがまとめられてました。(めちゃくちゃありがたい!) ポイントは下記の通りです。

  • 出力された用紙サイズをB5にする
  • 通しのページ番号(ノンブルという)をいれる
  • ページが偶数、または4の倍数にする
  • 読み込めていないフォントがあるか確認する
  • 表紙と本文は分けてzipにする <== これは自分の不備

技術書典、Re:VIEWで作った原稿で印刷所(日光企画様)に怒られたポイントと対応まとめ – Pokosho!

入稿

執筆お疲れ様でした!いよいよ入稿です。 印刷所は運営からも案内されている日光企画さんにしました。 サポートの方がめちゃくちゃ優しい方です!

www.nikko-pc.com

日光企画さんは、

  • イベント別入稿期限の早見表がある
  • 早割がある
  • 納品先を会場のサークルスペースに指定できる

という感じで、オペレーションで困ることはあまりありませんでした。

ページ数

平トジと中トジかで、ページ数を調整しなければなりません。

  • 平トジ: 偶数
  • 中トジ: 4の倍数

発行部数

非常に難しいですね。強気でチェック数以上に刷る人や、日和って50部未満にしてしまう人などなど。
爆死リスクと機会損失を天秤にかけなければならないので一概に正解はないと思います。
自分の場合は、在庫を抱えたくなかったので、40部に抑え、ダウンロードカードも準備して機会損失にも対応できるようにしました。

ただ、次回は、日光企画さんの早割(50%)で入稿して、100部くらい刷ろうと思います

blog.naoshihoshi.com

ダウンロードカード

物理本だけではなく、PDFでも売りたい場合、ダウンロードカードを作成しなければなりません。 ダウンロードカードとは、PDF頒布サイトのURL(通常はQRコードが記載されている)とシリアルコードが書かれているカードのことです。 私は対面電書というサービスを使いました。

taimen.jp

対面電書の良いところは下記の点です。

  • 無料でランダムのシリアルコードを3,000個まで発行できる
  • ダウンロードカードのデザインまで作れる
  • ダウンロードにユーザー登録は必要ない

という点が良いところです。

BOOTHも検討したのですが、購入者が会員登録しなければならないのが煩わしいだろうと思い、対面電書にしました。

ダウンロードカードは別途印刷所で刷るのも良いですし、プリンターで印刷したものをカッターで切るのでも良いと思います。 自分は、温かみがあった方が良いと思ったため、プリンターで印刷してカッターで一枚一枚切りました。

印刷する際、A4の1枚の紙から複数のダウンロードカードを作りたかったので、設定をする必要がありました。下記リンクが参考になります。

blog.karasuneko.com

まとめ

執筆から入稿、ダウンロードカード作成までに使ったもの、調べたものは以上です。 初めてだったとはいえ、調べれば意外になんとなることがわかりました! 印刷所への入稿は流石に不安しかなかったのですが、@kaibaさんが指摘されたポイントをまとめてくれたのには頭が下がります!ありがとうございました!

技術書典5に参加したらスマホを買い換えることができました!

概要

10月8日(月)に開催された技術書典5で出店者側として参加しました。 今回は技術書典5の参加レポートと振り返り(KPT)について書こうと思います。

参加して良かった!めちゃくちゃ楽しかった!

技術書典には今回初参加でした。
また、これまでの執筆実績といえばSoftwareDesignに2回寄稿したくらいでした。 そのため、原稿を書いた後の工程はほぼわからない状態で臨みました。 Re:VIEWの存在も入稿の仕方もわからない状態です。

何を書いたか

BFF(BackendsForFrontends)について書きました! 本の内容はこんな感じです。

本当は、下記のようなアーキテクチャで、何かしらのアプリを作るような本を書こうと思ってました。

  • クライアント: ReactNative、SPA
  • BFF: GraphQL, Next.js
  • バックエンド: Rails(APIモード), Go

しかし、8/1(水)の当選通知を受けてから、技術検証を始めるというスロースタートだったので、スコープを絞らないと爆死すると思い、BFFのみに集中しました。

正直、技術検証と執筆を2ヶ月で行うのは結構キツかったです

参加レポート

今回は出店者として参加でしたが、非常に楽しかったです!! 2ヶ月もかけて制作したものが、リアルな本になったことが純粋に嬉しかったです。自分が書いた文章が本になり達成感を感じました。
そして、本が売れていく瞬間はこれまで経験したことがない嬉しさでした。

  • 初めて売れた瞬間
  • 数分に一冊売れている時
  • 事前に買うことを決めた上で買ってもらった瞬間
  • 本を読んだ感想をもらった時

ブースの設営自体は改善の余地はありますし、コミュ力のなさを発揮してしまい、@kaibaさんの営業力に嫉妬したりしましたが、現時点で次回の参加もしようと決めています。

また、イベント自体、ここまで人がくるとは思っていなかった点や、後払いアプリが非常に使いやすく、とても満足でした◎

売上

項目 計算 金額
物理本売上 1,000円 * 43冊 43,000
DL版売上 1,000円 * 15冊くらい 15,000
参加費 7,000 / 2 △3,500
印刷費 物理本 60項40部 △33,772
売上 58,000 - 費用 37,272 20,728

黒字でした!!!!!! 人件費は自分だけなので0円です!素晴らしい! 当初、赤字になってもいいかなぁ〜くらいに思っていたのと、特に利益は求めていなかったで、棚から牡丹餅です!

また、印刷費用に関しては、日光企画さんの場合は早割が存在します。最も早い入稿で割引率は50%です。
原価の多くは印刷費であるため、次回は早割を狙っていきたいです。

利益となった2万円は、タイトルの通りスマホを購入しました!!ひゃっほい!*1

時間別売上推移

時間別売上推移はこんな感じです。

f:id:watasihasitujidesu:20181011120753p:plain

当初、物理本は11時台が最も売れ、その後1時間ごとに半減していくという予想をしていましたが、13時まで一定のペースで売れ続けました。 また、14時以降は見本を手に取ってもらうことが少なくなったため、顧客層の変化を感じました。 その他、物理本がない == 売り切れと認識とされる方が多く、少なからずDL版の売れ行きに影響があったと思われます。

日和って40部にしないで、60部くらい用意しておけば良かったのは反省点です。また、次回は見本を手に取った件数をカウントしていきたいです。

被チェック数と発行部数

参加サークルは、事前に被チェック数*2がわかります。 最終的な被チェック数は170でしたが、印刷時点(イベント開催1週間前)で120でした。
被チェック数から推測される販売数から発行部数を推測した場合、イベント直前になる程、信頼性は高まると思います。
しかし、早割(50%)の入稿期限は1ヶ月前であるため、被チェック数から販売数が推測できないというジレンマがあります。
ただ、原価がざっくり半分になるので、爆死しても痛みは少なくなるでしょう。

値付け

販売数を左右する要素の一つとして、価格は重要だと思います。
同じく参加した@kaibaさんは80部@500円 売り上げていました。
本の内容が異なるので一概には言えませんが、500円で80部売れるものが、1,000円で60部売れるので、価格弾力性は0.25で被弾力的と言えます。
ただ、自分の体感的には1,000円以下の場合はお祭り気分で購入できる価格帯であり、1,500円以上は現実に立ち返り、本題材に対して必要性を感じ、かつ、その本が有用である場合に購入するなぁと思いました。

価格 感じ方
500円 気になるくらいなら買う
1,000円 面白い内容なら買う
1,500円以上 必要性を感じていれば買う

需要から考えると60ページくらいの本であれば、1,000円が妥当なのではないかと思います。
弱小サークルが発行する本であっても、60ページで1,000円であれば60部程度なら売れることがわかりました。

そのため、早割(50%)で入稿し、60~120部用意するの最良の手だと思いますので、次回Tryしてみます。

振り返り(KPT)

次回参加を決めているので、振り返りをして改善していこうと思います。

Keep

  • 初参加で60部も売れた!
  • 売れる喜びを知った!
  • 作る大変さを知った!
  • アウトプットのためのインプットができた!
    • 本を書く上で、自分の知っている知識を切り売りするのではなく、知らない知識を学んでアウトプットした
  • 事前にDLカードを準備できた(@kaibaさんは素人だったからうまくDLさせることができなかった)

Problem

  • 8月から技術検証、9月から執筆となり、入稿がギリギリになってしまった。
  • DLカードの自作が地味にめんどくさかった
  • ブースの設営がチープすぎた
  • ほぼ接客に関与しなかった。「見本どうぞ〜」くらいしか言ってなかった
  • 表紙が白黒でBackendsForFrontendsとしか書かれていない表紙だった。
  • お昼ご飯を食べることができなかった(結果、体力的に消耗が激しかった)
  • 飲み水を用意していなかった

Try

  • 見本を手に取った件数をカウントする
  • 被チェック数を時系列でカウントする
  • 当選落選関係なく、事前に技術検証を済ませて、文章を書いていく。 => 締め切りに追われないようにする
  • 早割(50%)で入稿する(およそ開催日の1ヶ月前)
  • 60~120部発行する(販売価格が1,000円(と相応するページ数)の場合)
  • 新しい技術を使い、アウトプットのためのインプットを心がける
  • ブース設営を頑張る
    • ポップを作る
    • 台座?を持ってくる
  • 商業誌を真似た表紙にする

まとめ

技術書典5に参加し、初めて本を書いたのですが、本が読まれる喜びを知ることができました。 また、値付けや売り上げ予測など書くこと以外にも考えることがあり、良い経験になりました。

次回も参加しようと決めているのですが、今回の学びや反省を活かして改善していこうと思います!!

f:id:watasihasitujidesu:20181011143228j:plain

*1:Huawei nova lite 2

*2:お気に入りリストのようなもの

AWS EC2のルートボリューム(EBS)をダウンタイム0で拡張する方法

概要

blog.naoshihoshi.com

サービスを公開したと同時に、mackerelを導入してみました。 CPU, Memory, filesystemのアラートをデフォルトの閾値で設定した結果、一瞬でfilesystemのアラートが鳴りました。

f:id:watasihasitujidesu:20180727130724p:plain

すぐさま対応できなかったので、とりあえずアラートを止めるために閾値を変えるというワークアラウンド対応をしました(白目)

ただ、ディスク容量の75%ほど使用していたので、根本的に対応しなければサービス断になると思われるため、ボリューム拡張をすることにしました。 今回はAWS EC2のルートボリューム(EBS)をダウンタイム0で拡張する方法について書こうと思います。

手順

AWSマネコンより、EBSのストレージ拡張

f:id:watasihasitujidesu:20180727132326p:plain
何はともあれ、AWS マネジメントコンソール > EC2 > ELASTIC BLOCK STORE > Volumes画面に行きましょう。 Actionsプルダウンを押下し、Modify Volumeを選択します。

f:id:watasihasitujidesu:20180727132331p:plain
続いて、任意のVolume TypeとSizeを選びます。 今回は、Volume Typeの変更はなく、Sizeを8GiBから16GiBに拡張します。 そして、Modifyボタンを押下。

f:id:watasihasitujidesu:20180727132337p:plain
すると、アラート的なものが表示されます。

Are you sure that you want to modify volume vol-xxxxxxxxxxxxx?
It may take some time for performance changes to take full effect.
You may need to extend the OS file system on the volume to use any newly-allocated space.
Learn more about resizing an EBS volume on Linux and Windows.

=> vol-xxxxxxxxxxxxxを変更してもよろしいですか?
=> パフォーマンスの変更が有効になるまでには時間がかかることがあります。
=> 新しく割り当てられた領域を使用するには、ボリューム上のOSファイルシステムを拡張する必要があります。
=> LinuxおよびWindowsでのEBSボリュームのサイズ変更の詳細については、こちらをご覧ください。

とのこと。Yes!!!

f:id:watasihasitujidesu:20180727132501p:plain
モーダルが閉じたところで、念のため、Sizeが変わったことを確認します。 また、Stateがin-use - completed(100%)になるまで少し待ちましょう。

AWS マネジメントコンソール上の操作は以上です。

EC2インスタンスにSSH接続し、コンソール上で変更の反映

さきほど出てきたアラートの通り、インスタンスにSSHアクセスし、コンソール上からボリューム拡張を反映させる必要があります。

使用しているファイルシステムの確認

file -sコマンドで使用しているファイルシステムを確認します。

$ sudo file -s /dev/xvd*
/dev/xvda:  DOS/MBR boot sector; ......
/dev/xvda1: Linux rev 1.0 ext4 filesystem data, .....

Linux ext4 ファイルシステムと DOS/MBR boot sectorというファイルシステムなのがわかりました。

物理ディスクの確認

インスタンスにアタッチされたブロックデバイスを確認するためにlsblkコマンドを使います。

$ lsblk
NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  16G  0 disk 
└─xvda1 202:1    0   8G  0 part /

おぉ〜、しっかり16Gibがアタッチされていますね。
この情報から、ルートボリュームである/dev/xvdf1 は、16 GiB のデバイス(/dev/xvda)に含まれる 8 GiB のパーティションでだということがわかります。
また、この/dev/xvdaはパーティションは/dev/xvdf1以外ありません。そのため、ボリュームの残りの領域を使用するために、パーティションのサイズを変更する必要があるということがわかります。

パーティションのサイズ拡張

growpartコマンドでパーティションのサイズ拡張をします。

$ sudo growpart /dev/xvda 1
CHANGED: disk=/dev/xvda partition=1: start=4096 old: size=16773086,end=16777182 new: size=33550302,end=33554398

再び、lsblkコマンドで、意図した変更になっているか確認します。

$ lsblk
NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  16G  0 disk 
└─xvda1 202:1    0  16G  0 part /

良さそうですね。

ファイルシステム上のディスク領域の変更

このままでは、ディスクのパーティションが切れただけで、ファイルシステムに変更が反映されていません。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
devtmpfs         488M   56K  488M    1% /dev
tmpfs            497M     0  497M    0% /dev/shm
/dev/xvda1       7.8G  5.5G  2.2G   72% /

Linux ext4 ファイルシステムの変更反映はresize2fsで行います。

$ sudo resize2fs /dev/xvda1
resize2fs 1.42.12 (29-Aug-2014)
Filesystem at /dev/xvda1 is mounted on /; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 1
The filesystem on /dev/xvda1 is now 4193787 (4k) blocks long.

最後に変更がうまく行っているか、確認します。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
devtmpfs         488M   56K  488M    1% /dev
tmpfs            497M     0  497M    0% /dev/shm
/dev/xvda1        16G  5.5G   11G   36% /

/dev/xvda1のサイズ16Gibになり、使用率が72% => 36%に下がりましたね!

まとめ

AWSマネジメントコンソール上でEBSのSizeをアップし、数分待つことで、容量が増えました。
また、EC2インスタンス上にSSH接続し、パーティションの容量変更とファイルシステムへの反映を行うだけで、インスタンスを停止せずにルートボリュームの拡張を行うことができました。
インスタンス停止せずにボリューム拡張できるのは素晴らしいですね!

mackerelのグラフも、ぴょこんと上がりました! f:id:watasihasitujidesu:20180727140430p:plain

mackerelをAWS OpsWorks カスタムクックブックでインストールする方法

概要

これまで、Webサービスを作ったことはあれど、サーバーの管理/監視は構築したことがありませんでした。
手軽に導入したかったので、SaaS型サーバー監視サービスを導入しようと考えていました。
そこで、今回はmackerelを使って監視をしようと思います。また、AWS OpsWorksのカスタムクックブックでプロビジョニングしていたので、mackerelをAWS OpsWorks カスタムクックブックでインストールする方法について書こうと思います。

スタートアップガイドを読み、コード化対象のスクリプトを確認

何はともあれ、スタートアップガイドを読みます。今回はAmazonLinuxを使用しているので、それ用のガイドを読みます。

mackerel.io

ステップは超絶簡単ですね。

curl -fsSL https://mackerel.io/file/script/amznlinux/setup-all-yum.sh | MACKEREL_APIKEY='<YOUR_API_KEY>' sh
echo "apikey = '<YOUR_API_KEY>'" >> /etc/mackerel-agent/mackerel-agent.conf
sudo /sbin/service mackerel-agent start

こんだけ!

手順

API Keyの確認

まずはmackerelのダッシュボードでAPIキーの確認をします。

Organization > APIキータブ から確認します。

f:id:watasihasitujidesu:20180718074336p:plain

Chefのレシピ作成

AWS OpsWorks スタック におけるCustom Chef cookbooksを適用する方法 - production.log

こちらを参考にレシピを作っていきます。 レシピはスタートガイドにある通り、3つのコマンドを実行するだけのシンプルなものです。

api_key = node[:deploy][:testrooper][:mackerel_apikey]

execute "mackerel install" do
  user "root"
  command "curl -fsSL https://mackerel.io/file/script/amznlinux/setup-all-yum.sh | MACKEREL_APIKEY='#{api_key}' sh"
  action :run
end

bash "add api key" do
  not_if 'grep "apikey" /etc/mackerel-agent/mackerel-agent.conf'
  code <<-EOC
    echo "apikey = '#{api_key}'" >> /etc/mackerel-agent/mackerel-agent.conf
  EOC
end

execute "run agent" do
  user "root"
  command "/sbin/service mackerel-agent start"
  action :run
end

こちらのコードはGitで管理しているわけですが、APIキーをコードに直接書かないようにしています。
何も知らずにPublicリポジトリにしちゃったりなんだりした場合に、意図せずインターネットにAPIキーが晒されてしまうためです。
そのため、APIキーはOpsWorksのCustomJSONから取得するようにしています。

AWS OpsWorksでCustomJSONの設定

f:id:watasihasitujidesu:20180718074401p:plain
先述の通り、AWS OpsWorksのCustomJSONに記述したAPIキーを使ってプロビジョニングするので、その設定をします。

AWS OpsWorks > Stack > Stack Settings > Edit

{
    "deploy": {
        "testrooper": {
            "mackerel_apikey": "xxxxxxxxxxxxxxx"
        }
    }
}

Execute Recipes

最後に、カスタムクックブックを実行して終了です。
ステップは、Deployments > RunCommandで下記2つを実行するだけです。

  • Update Custom Cookbooksを実行
  • Execute Recipesを実行

f:id:watasihasitujidesu:20180718074431p:plainf:id:watasihasitujidesu:20180718074433p:plain

結果の確認

f:id:watasihasitujidesu:20180718074444p:plain
無事にmackerelのagentが動いていれば、ダッシュボードに1ホストだけACTIVEになっているはずです。

まとめ

無事にmackerelをAWS OpsWorks カスタムクックブックでインストールできました👏
そもそもの導入が簡単というのもありますが、手順をコード化することで、今後サーバーが何台増えてもレシピを実行するだけで済むようになりました。
あとは、mackerelの方で通知の設定などを行うだけで楽々とサーバー管理ができそうです!

GitHubの草を60日間連続で生やしてわかった習慣化のコツ

概要

f:id:watasihasitujidesu:20180716100634p:plain

最近個人プロジェクトの開発をしているのですが、気づけばGithubの草が60日間連続で生えていました。 60日間も毎日草を生やしていると、完全に習慣されたと思ったため、 今回はGithubの草を60日間連続で生やしてわかった習慣化のコツを紹介します。

習慣化のコツ

モチベーション管理

個人の目標における内発的動機と外発的動機の重なりの意識

f:id:watasihasitujidesu:20180716072408p:plain
自分がモチベーションを感じることとしては、学びたいことを学んでいるときや、なりたい自分に近くための学習など「やりたいこと」を実行しているときです。
「やりたいこと」と関連がある学習を行なっているときは、少なくとも2ヶ月くらいは継続的に学習ができていた記憶があります。
一方、必要に迫られた学習など、「やるべきこと」を実行しているときは、あまりモチベーションの高まりを感じることができず、長期的には習慣化できませんでした。

とはいえ、やりたいことを優先しすぎると、成果*1の最大化には繋がらないと思い、「やるべきこと」と「やりたいこと」が重なるように意識しました。

学習の対象が、学びたいものかつ、学ぶべきものだったばあい、「やるべき」と「やりたい」が重なっている状態になります。
この動機のスイートスポット*2が存在する場合は、まずはそこにフォーカスすることをおすすめします。

内発的動機に対する行動結果の紐付け

f:id:watasihasitujidesu:20180716083427p:plain

先述した学習のスイートスポット*3が存在すれば苦労しませんが、多くはないでしょう。
ただ、「やるべきことは、必ずしもやりたいことと重なるわけではないが、少なからず、好影響があるかもしれない」といったように、楽観的に考えることが重要です。
物事は、何にでもグラデーションがあるので、一見、やりたいこととは無関係でも巡り巡って自分のやりたいことに繋がるか?を考える事が重要です。
そのため、やるべきことの中から、やりたいことに近付く要素が最も多いものを取捨選択することになります。

取り得る選択始の中でモチベーションと成果が最大化されるものを吟味していきましょう!

可視化・可視化・可視化

Githubの草を生やす上で欠かせないのはGItHub-Gardenerです。

f:id:watasihasitujidesu:20180716100702p:plain

こんな感じで可視化されるのですが、各種スコアが表示されます。

consecutive days (best record updating) 60 days

特に、継続日数が表示されるこの部分ですが、強烈な強制力があります。
継続日数を増やしていく中で、下記のような嬉しさ、達成感、充実感がありました。

継続日数 嬉しみ
7 days 草が一本に繋がって単純に嬉しい
10 days 値が2桁になって嬉しい
30 days 草から芝生となった嬉しさと、謎の達成感
60 days 習慣化された充実感

目標到達点の認識とリズム作り

目標到達点の認識

先述の通り、GitHub-GardenerはGitHubの芝生を可視化ツールであり、継続日数を数値として表示します。
初心者Gardenerだった自分は、まずは3 daysを目標にしました。3 days達成すると7days, 10 days, 30 days, 60days....、と次の目標を意識することができました。
この効能は、ある程度粒度が大きい到達点(マイルストーン)を認識することで、目標達成に対する意識が芽生えさせることができることです。

また、継続日数をマイルストーンとして設定した場合、自分のリアルなスケジュールを見比べて、「○曜日は仕事の帰りが遅いから次の日の朝に影響が出そうだ。タスクの粒度を極小にしよう」といったリスク回避について考えることもできます。

目標達成に向けてのリズム作り

朝6時に起きたとしても、確保できる時間は3時間前後です。3時間しかないと、できることは多くはありません。
そこで、コミットするタスクを分解して、目標達成に向けてのリズム作りをしました。

よくある継続学習指南では、しばしば「まずは本を開くところから始めよ」「まずは5分から始めよ」というのを目にしますが、まさにこれです。

週: 平日/休日 1日あたりの作業時間 作業内容
1週目: 休日 12h 規模が大きめの実装
1週目: 平日 3h 些細なデザイン調整や規模の小さいリファクタなど、作業開始から30分以内でコミットできるような粒度のタスク
2週目: 休日 12h 技術調査と平日のためのタスク分解
2週目: 平日 3h 休日に行なった粒度の小さいタスクの実装
  • 休日は大きめの作業
  • 平日は小さめの作業

といったように、割り当てることができる時間と、タスクの粒度を合わせることで、無理のないペースで確実に成果を出すことができます。 平日は粒度は小さいタスクの積み重ねがメインとなるのですが、自身で成功体験を積み重ねることと、それを自分自身で承認することが習慣化のコツです。

その点、GitHub-Gardenerは、GitHub-Gardenerを使うことで、自然と上記が達成できるような仕組みになっていると言えます。

すごいぞ、GitHub-Gardener。

生活のリズムを一定にする

GitHub-Gardenerを使うと、草を生やしていない(commitしていない)と、謎の不安感に包まれるようになります。 これは適度な負荷であり、生活にちょっとした緊張感を与えるストレスだと思います。ストレスを力に変えましょう。

GitHub-Gardenerを使うと、朝目が覚めたときに、まず「commitしなきゃ!」と、頭に浮かぶようになりました。 普段は「眠いなぁ〜、もう少し寝ようかなぁ〜...ZZzz」ってなるのですが、この思考のインターセプトはやばいですね。 そのため、結果的に朝、目覚めと同時にcommitするようになりました。

また、朝6時に起きてcommitという生活を続けていると、スケジュールや体調に左右されずに安定して自己研鑽できることがわかりました。

  • 夜早く寝るので、睡眠時間を十分に確保できる
  • 早朝から朝日を浴びることができるので、体調がすこぶる良くなる(安定化)
  • 旅行などの予定が入っていても安定してコミットができる(スケジュールの影響を受けにくい)

素晴らしいぞ、GitHub-Gardener。

まとめ

60日間継続的にアウトプットし続けることができたので、習慣化のコツを書いてみました。

  • 自分は何にモチベーションを感じるのか、学習対象の意義は何かを考える
  • 可視化による達成度合いの認識
  • マイルストーンの設定による、長期的な目標管理と達成に向けたリズム作り
  • 持続的に成果を出すための生活基盤の構築

基本的には、何が学びたいんだっけ?何を得たいんだっけ?を軸に考えていくと継続はできるのではないかと思います。
ただ、人間は易きに流れやすい生き物であるため、ある程度、強制力や自己承認のための仕組みが必要です。
自分は補助的な役割としてGitHub-Gardenerを選びましたが、継続をするための仕組みが無駄なく全て揃った良いサービスでした。
おすすめです。

*1:習得したスキルがどこに繋がるのかといった観点や、仕事で役に立つかといった観点

*2:と、勝手に呼んでいる

*3:やりたいことと、やるべきことが重なり

【Rails】deviseが送信するメールをAWS SESから配信する方法

概要

個人開発において、Railsのdeviseから配信されるメールは、これまでGmailを使用していたのですが、せっかくドメインを取得したので、AWS SESからメールを送信することにしました。
今回はRailsにおいて、deviseが送信するメールをAWS SESから配信する方法について書きます。

前提

  • Rails 5.2.0
  • Route53でドメインを取得済みである
  • PublicIPでアクセスできるサービスが存在する

手順

SESの設定

SESはデフォルトでは特定のメールアドレスには設定さえすれば、送信することができます。
しかし、多くのWebサービスは不特定多数のユーザーにメールを送信するため、その設定を行います。

ドメイン設定

f:id:watasihasitujidesu:20180716090617p:plain
Identity Management > Domains > Verify a New Domain
Verify a New Domain を押下すると、ポップアップが出てドメインの入力を求められます。
取得したドメインを入力し、Generate DKIM Settingsもチェックします。*1

f:id:watasihasitujidesu:20180716091130p:plain
f:id:watasihasitujidesu:20180716091521p:plainf:id:watasihasitujidesu:20180716091534p:plain

Route53でドメインを取得している場合は、Use Route53ボタンを押下すれば、AWSの方で自動でRecord Setを設定してくれます。

f:id:watasihasitujidesu:20180716091941p:plain
しばらく時間をあけると
Identity Management > Domains > 登録したドメイン にアクセスするとVerificationとDKIMのステータスがverifiedになっています。

サンドボックス状態の解除

f:id:watasihasitujidesu:20180716092315p:plain
Email Sending > Sending Statisticsにアクセスすると、上記のような警告がでます。
現状、特定のメールアドレスにしか送信できない送信制限がかかっているので、サポート制限緩和申請をする必要があります。

f:id:watasihasitujidesu:20180716092940p:plainf:id:watasihasitujidesu:20180716092942p:plainf:id:watasihasitujidesu:20180716092945p:plain

上記の通り、各種項目を入力します。
1日くらいすると、AWSの審査の末、SESからメール送信が可能になります。

メール送信テスト

f:id:watasihasitujidesu:20180716093542p:plain

Identity Management > Domains > Send a Test Email から、任意のメールアドレスにメール配信が可能になります。
Send Test Mailボタンを押し、メールが受信できれば成功です。

Rails(devise)の設定

SES自体が使えるようになったので、今度はRails側の設定をしていきます。

AWS SDKをinstall

Railsでaws sdkを使用するためにGemfileに下記を追記します。

gem 'aws-ses'

ActionMailerの設定

config/initializers/aws.rbを新規で作成し、下記の通り、SESを使用するように変更します。

ActionMailer::Base.add_delivery_method(
  :ses,
  AWS::SES::Base,
  access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
  secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
  server: 'email.us-west-2.amazonaws.com'
)

server はSESで選択したリージョンによって異なるので 公式ドキュメントのリージョンと Amazon SES から API (HTTPS) エンドポイントを確認してください。

次に、config/environments/production.rbの設定をSESを使用するように変更します。

config.action_mailer.default_url_options = { host: 'testrooper.com' }
config.action_mailer.delivery_method = :ses

deviseの設定変更

config/initializers/devise.rbのconfig.mailer_senderを修正します。

# config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
config.mailer_sender = 'Testrooper <noreply@testrooper.com>'

Mailerクラスの修正

最後にMailerクラスのfromの設定を変更して終わりです。

# frozen_string_literal: true
 
 class ApplicationMailer < ActionMailer::Base
#  default from: "from@example.com"
   default from: "Testrooper <noreply@testrooper.com>"

まとめ

これまで説明した通り、SESの設定自体はとても簡単です。
送信制限緩和申請のために、1日程度かかってしまうのですが、特に審査に落ちることもなく使えるようになりました。
また、Railsの方の設定も、難しいことはなく、設定ファイルの修正だけでした。

これまでGmailしか使っていなかったのですが、ドメインがあるのであれば、初手でSESを選択した方が良いかもしれませんね。

*1:DKIM とは DomainKeys Identified Mail の略で、受信した電子メールが「正当な送信者から送信された改ざんされていないメール」かどうかを調べることができる電子署名方式の送信ドメイン認証技術

AWS OpsWorks スタック におけるCustom Chef cookbooksを適用する方法

概要

AWS OpsWorks スタックでは、Chef Supermarketで公開されているCookbooksを適用する方法もありますが、自前で作成したCustom Chef cookbooksを適用することもできます。

今回はAWS OpsWorks スタックにおいて、自前で作成したCustom Chef cookbooksを適用する方法を紹介します。

Custom Chef cookbooksの作成

test-chefという名前のPublicリポジトリを作成

test-chefという名前のPublicリポジトリを作成します。そして、下記のようなフォルダ構成にします。

naoshihoshi 10:03:00 test-chef$ pwd
/Users/naoshihoshi/test-chef
naoshihoshi 10:03:02 test-chef$ tree
.
└── hoge
    └── recipes
        └── default.rb

Opsworksではrecipesディレクトリ直下のdefault.rbがデフォルト呼ばれることになります。 default.rbの内容は下記の通りです。とりあえず動かしてみるだけのコードですね。

# hoge/recipes/default.rb
log "Hello Chef"

ログに"Hello Chef"と出力されれば、動作していることがわかります。

AWS OpsWorksでCustom Chef cookbooksの適用

Stack Setting

OpsWorksでスタックを追加し、さきほど作成したcookbooksを適用します。

f:id:watasihasitujidesu:20180701101743p:plain

スタック作成画面で
1. Use custom Chef cookbooksをYesに変更
2. Repository URLは先ほど作成したtest-chefのリポジトリのURLを指定

Layer Setting

Layerを追加し、Recipesの変更を行います。 今回は、Setup時に先ほどのレシピを実行させたいので、このような設定になります。
f:id:watasihasitujidesu:20180701102337p:plain

gitで管理しているリポジトリの直下にあるディレクトリを設定すれば、その配下にあるrecipesディレクトリのdefault.rbが呼び出されるという仕組みです。

naoshihoshi 10:03:02 test-chef$ tree
.
└── hoge <===== このディレクトリが設定された場合、
    └── recipes
        └── default.rb <===== これが呼び出される

インスタンスを作成し、起動

インスタンスを作成し、起動をすると、setupで設定したレシピが実行されます。 インスタンス起動後にログがみれるので、インスタンス名をクリックし、ページ下部のログのshowリンクでログを確認します。

f:id:watasihasitujidesu:20180701103741p:plain

無事にHello Chefが表示されていますね!

任意のレシピを作成し、実行する

default.rb以外に実行したい場合は任意のファイルを作成後hoge::任意のファイル名とします。

.
└── hoge
    └── recipes
        ├── configure.rb
        └── default.rb

configure.rbを作成した場合、Layerの設定ではhoge::configureとなります。

まとめ

小さなコードではありますが、OpsWorksで自前で用意したCustom Chef cookbooksを動かすことができました。 また、レシピを複数用意して、任意のレシピを実行する方法もとてもシンプルで簡単でした。 動かし方さえわかってしまえば、あとはプロビジョニングのためのコードを書き足していくだけです!

WebdriverIOを使ったChromeによるE2Eテスト環境構築の手順 - Amazon Linux -

概要

Amazon LinuxでWebdriverIOによる自動E2Eテストの実験をしようとしていたのですが、 うまくGetting startedできませんでした。

Linux環境においてWebdriverIOのGet Startedはやや説明不足感があるため、今回の記事では、 Amazon LinuxでWebdriverIOを使ったChromeによるE2Eテスト環境構築の手順を書こうと思います。

前提

項目 バージョン
Amazon Linux 2017.9
Java 1.8
selenium-server-standalone 3.12.0
chromedriver 2.40
WebdriverIO 4.13.0
npm 6.1.0
node 10.5.0

手順

まずは作業用のフォルダを作成

mkdir webdriverio-test && cd webdriverio-test

Javaのアップデート

Selenium Server StandaloneをJavaを動かす際、Amazon Linux 2017.9にデフォルトでインストールされているJavaのバージョンが1.7であるため、アップデートが必要になります。

sudo yum update -y
sudo yum install -y java-1.8.0-openjdk-devel.x86_64
sudo alternatives --config java

sudo alternatives --config java を実行すると、1.7か1.8か選択するように聞かれるので1.8を選択。

ChromeDriverのインストール

ざっくりやることは下記の通りです。

  • ChromeDriverをwgetで持ってきて展開
  • Chromeのバイナリをインストール
  • GConf2をインストール

という流れです。

ChromeDriverをwgetで持ってきて展開

wget https://chromedriver.storage.googleapis.com/2.40/chromedriver_linux64.zip
unzip chromedriver_linux64.zip

sudo ln -s /home/user_name/webdriverio-test/chromedriver /usr/local/bin/chromedriver

Chromeのバイナリをインストール

curl https://intoli.com/install-google-chrome.sh | bash

GConf2をインストール

yum install GConf2で入れようとしても、No package GConf2 available.と怒られるので、リポジトリを追加します。 sudo vim /etc/yum.repos.d/centos.repoで新規ファイルを作成し、下記を打ち込んで保存

[CentOS-base]
name=CentOS-6 - Base
mirrorlist=http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=os
gpgcheck=1
gpgkey=http://mirror.centos.org/centos/RPM-GPG-KEY-CentOS-6

#released updates
[CentOS-updates]
name=CentOS-6 - Updates
mirrorlist=http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=updates
gpgcheck=1
gpgkey=http://mirror.centos.org/centos/RPM-GPG-KEY-CentOS-6

#additional packages that may be useful
[CentOS-extras]
name=CentOS-6 - Extras
mirrorlist=http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=extras
gpgcheck=1
gpgkey=http://mirror.centos.org/centos/RPM-GPG-KEY-CentOS-6
sudo rpm --import http://mirror.centos.org/centos/RPM-GPG-KEY-CentOS-6
sudo yum -y install GConf2

WebdriverIO

nvmによるnodeのインストール

sudo yum install -y git
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
source ~/.bashrc
nvm install node

WebdriverIOのインストール

npm install webdriverio

Selenium Server Standaloneをインストールしバックグラウンド実行

curl -O http://selenium-release.storage.googleapis.com/3.12/selenium-server-standalone-3.12.0.jar
java -jar selenium-server-standalone-3.12.0.jar &

実行

test.jsを作成

WebdriverIO公式のtest.jsをコピペして、'firofox'を'chrome'に変更すれば動くだろうと思いきや、 Chromeの場合は、明示的にheadlessモードを指定しなければ動作しませんでした。 そのため、下記の通りに書き換えます。

var webdriverio = require('webdriverio');
var options = {
  desiredCapabilities: {
    browserName: 'chrome',
    chromeOptions: {
      args: ['--headless', '--disable-gpu'],
    },   
  }
};

webdriverio
  .remote(options)
  .init()
  .url('http://www.google.com')
  .getTitle().then(function(title) {
    console.log('Title was: ' + title);
  })
  .end()
  .catch(function(err) {
    console.log(err);
  }); 
node test.js

Title was: Googleと表示されれば、うまくGetting startedができています!

まとめ

WebdriverIOのスタートガイドは、GUI環境上で動かす分には、Chromeが入っていれば、Seleniumが動かすChromeDriverをインストールするだけで動くのですが、CUI上では、Chromeをバイナリでインストールしたり、GConf2を使うためにリポジトリを追加したり、普段やらないようなことをするので骨が折れました。 また、起動時に--headlessを引数で渡さなければ謎のエラーで動作しないことにも地味にハマりました。

Rails 5.2でActive Storage を使った画像のアップロードを試す

概要

ひょんなことから、画像をアップロードする処理を書くことになりました。
これまで、Railsで画像アップロードといえば、CarrierWavePaperclipを使っておけば良いでしょ〜!くらいに思ってたんですが、せっかくRails5.2を使っているのであれば、Active Storageを使ってみよう!と思ったので、
今回はRails5.2でActive Storageの使った画像のアップロードの方法をまとめます。

なぜActiveStorageなのか

今回のアップロード機能の要件は至極単純でした。

  • あらかじめ、特定のフォルダに画像データが配置されている
  • 上記の画像データを特定のモデルにバッチで添付する
  • アップロードされた画像はオリジナルサイズで閲覧ができる

ただこれだけでした。 サムネイル作成などのファイルの変換、バリデーション、セキュリティなどなど、ガン無視でOKみたいな。 そのため、CarrierWavePaperclipは、やや機能過多感があるため、今回はActive Storageに決定しました。 また、Active Storageの特徴を抜粋すると下記の通りです。

  • DBにファイルのメタ情報と中間テーブルを用意することによって、既存のテーブルにカラムを追加しなくても良い => スキーマの変更が柔軟にできそう
  • レコード:ファイルが1:1にも1:多にもできる
  • 既存モデルにはhas_one_attached :nameと書くだけ。
  • 時間制限付きURLを楽に生成できる

今回の要件に置いてはこの機能があれば十分ですね。

Getting start

railsguides.jp ここ読めばGetting startできるわけですが、コマンドとかをまとめようと思います。

config/storage.ymlの編集

どこに保存すんの?という設定を書きます。

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

amazon:
 service: S3
 access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
 secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
 region: us-west-2
 bucket: bucketname

amazonのところの書いた<%= Rails.application.credentials.dig(:aws, :access_key_id) %>については、Rails 5.2からのcredential管理の方法で設定した値を使います。 詳細はこちらの記事を参照してください。

blog.naoshihoshi.com

config/environments/development.rb

config/storage.ymlに設定したawsの設定を反映するために、config/environments/development.rbを修正します。

# Store uploaded files on the local file system (see config/storage.yml for options)
# config.active_storage.service = :local
config.active_storage.service = :amazon

モデルにファイル添付する設定を書く

冒頭で紹介した通り、1レコードにつき1ファイルだけ添付するのであれば、下記設定のみです。

class User < ApplicationRecord
  has_one_attached :avatar
end

ファイルの添付と確認

ファイルの添付と確認はrails consoleで確認します。

$ cd rails_root
$ bin/rails c
user = User.first
user.avatar.attached? #=> false
filepath = File.join(Dir.home, 'filename.jpg')
user.avatar.attach(
  io: File.open(filepath),
  filename: File.basename(filepath),
  content_type: 'image/jpg'
)
user.avatar.attached? #=> true

# アプリケーションを指すblobの永続URLを生成
include(Rails.application.routes.url_helpers)
default_url_options[:host] = 'localhost:3000'

url_for(user.avatar) #=> "http://localhost:3000/rails/active_storage/blobs/09Iiw63f495IkJ09IiwkJ09Iiw6IkJBaH19--7209IiwiZXxsLCJwdXIiOiJibG9iX2lkIn19--726eT09IiwbeaHBEUT09Iiwbe29513f4943f49513f49e4a9c55b4af76f5/filename.jpg"

上記の生成されたURLにアクセスすると画像を閲覧することができます。

まとめ

簡単な設定情報と、モデルにhas_one_attachedという記述を追加するだけでファイルのアップロード機能を作ることができました。さすがRailsですね。 要件によってはリサイズしなければならなかったり、バックグラウンドで処理しなければならなかったりするので、Gemを使うことを検討しなければならないのですが、今回は要件が少なかったため、Active Storageで十分でした。

Rails5.2のcredential管理を試してみた

概要

blog.naoshihoshi.com こちらの機能を開発しているときに、Rails5.2からcredential管理が変更されたことに気づいたので、 今回はRails5.2のcredential管理の方法をざっくり説明しようと思います。

Rails5.2のcredential管理の方法のざっくりとした説明

Ruby on Rails 5.2 リリースノート | Rails ガイド

config/credentials.yml.encファイルが追加され、productionアプリの秘密情報(secret)をここに保存できるようになりました。これによって、外部サービスのあらゆる認証credentialを、config/master.keyファイルまたはRAILS_MASTER_KEY環境変数にあるキーで暗号化した形で直接リポジトリに保存できます。Rails.application.secretsやRails 5.1で導入された暗号化済み秘密情報は、最終的にこれによって置き換えられます。 さらに、Rails 5.2ではcredentialを支えるAPIが用意され、その他の暗号化済み設定/キー/ファイルも簡単に扱えます。 詳しくは、Rails セキュリティガイドを参照してください。

リリースノートのcredential管理の方法を引用しました。

ざっくり言うと、

  • config/master.keyに記述したキー情報でcredential情報を暗号化
  • 暗号化した情報はconfig/credentials.yml.encに書き込む
  • credential情報は普通に人間が読める感じで書ける
  • RailsアプリはRails.application.credentials.dig(:aws, :access_key_id)って感じで簡単に呼べる

って感じです!

使い方の説明

とはいえ、実際使ってみないとよくわからないので、試してみましょう。

$ cd rails_root
$ cat config/master.key
nirejgaks945n3ge9223gsgs
$ cat config/credentials.yml.enc
omtTwkcvAjuPb8CGkx8h2R9EzqMEsEND3cD7he7z7Pd3KKrAas3jrRGGnhDfdYQr

これが初期状態です。 では、続いて、credential情報を登録してみましょう。 登録はrailsコマンドで実行します。

初期状態はsecret_key_baseだけが登録されているのですが、AWSの認証情報を追加してみます。 下記コマンドを実行すると、エディタが開きます。

$ EDITOR=vim bin/rails credentials:edit
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: kgJ3dPHLkTUwNTG3pM9kgJ3dPHLkTUwNTG3pM9k6M8RR4LrRJx7UeKYiebrcqCrh6LNX9GyYXACnqfcFb8Xk6M8RR4LrRJx7UeKYiebrcqCrh6LNX9GyYXACnqfcFb8X

aws:
  access_key_id: AKIAJBVIIRA9LRVFAPOK
  secret_access_key: wNTG3pLkTUX9GyYXM8RR4L7UeKYiebrcFb8Xd

そして、再びconfig/credentials.yml.encを確認すると、情報が追加されている == 値が変更されていることがわかるはずです。

$ cd rails_root
$ cat config/credentials.yml.enc
omtTwkcvAjuPb8CGkx8h2R9EzqMEsEND3cD7he7z7Pd3KKrAas3jrRGGnhDfdYQrwkcvAjuPb8CGkx8hkcvAjuPb8CGGnhDfdYQrwkcvAjuPb82R9EzqMEsEND3MEsEND3c9EzqMEsEND

最後に、Railsから呼び出すことができるか確認します。

$ cd rails_root
$ bin/rails c
irb(main):001:0> Rails.application.credentials.dig(:aws, :access_key_id)
=> "AKIAJBVIIRA9LRVFAPOK"

ばっちりですね!

まとめ

これまでは、.envファイルなどで環境変数を設定したりしていたので、管理が煩雑になっていました。 Rails5.2からは今回紹介したcredential管理の方法を使えば管理が楽になると思います。 今回紹介した通り、使い方もシンプルであるため、おすすめです。

ChromeExtensionのBrowser Actionsで格納したlocalStorageの値をBackground Pagesを使ってContent Scriptsからアクセスする方法

概要

Chrome ExtensionのBrowser Actionsで認証リクエストし、レスポンスのJWTをlocalStorageに格納する処理を作っていました。
その後、さらに拡張してContent ScriptsでJWTを使って他のAPIリクエストをするときに、「あれ?localStorageのスコープ違くね?\(^o^)/」となったので、
Background Pagesを使ってContent ScriptsからlocalStorageにアクセスできるようにしました。
今回はその方法をサンプルコードを用いて紹介します。

また、今回は下記のような色々な前提をすっ飛ばして説明します(!)

  • localStorage
  • Chrome Extensionのスクリプトの種類など
  • パッケージ化されていない拡張機能の確認方法

処理の方針

概要にも書きましたが処理としてこんな感じです。

  1. Browser ActionsでlocalStorageに何かしらの値を格納
  2. 1.で格納した値をBackground Pagesから取得できるようにする
  3. Content ScriptsからBackground Pagesの処理を呼べるようにする

manifest.json

まずはmanifest.jsonを書いていきます。 方針の通り、browser_Actions, content_scripts, backgroundを定義していきます。

{
  ...
  "browser_Actions": {
    "default_popup": "index.html",
  },
  "permissions": ["tabs"],
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "content_script.js"
      ]
    }
  ],
  "background": {
    "scripts": [
      "background.js"
    ]
  },
  ...
}

Browser Actions

ここはサラッといきます! 普通にlocalStorageにsetするだけです。

localStorage.setItem("key","value");

Background Pages

今回のポイント1つめです。 Background Pagesを作る際に考えなければならないのは、Content Scriptsから呼び出せるようにしなければならないことです。 そこで活躍するのがChrome APIのchrome.runtime.onMessage.addListenerです。 Chrome Extensionではスクリプト同士を連携させるためにメッセージを使ってやりとりします。 Background PagesではContent Scriptsから呼び出される側なので、受け口(Listener)を作ります。

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  switch (request.method) {
    case 'getItem':
      sendResponse({data: localStorage.getItem(request.key)});
      break;
    default:
      console.log('no method');
      break;
  }
});

method名と任意のkey名を受け取れるようにして、中身を見てよしなに処理を変えるといった感じです。

Content Scripts

Content ScriptsではBackground Pagesに定義した処理を呼びます。 Background Pagesではchrome.runtime.onMessage.addListenerを使ってメッセージの受け取り側の定義をしましたが、 Content Scriptsでは、chrome.runtime.sendMessageを使って、メッセージの送信側を定義します。

document.addEventListener('click', function(e) {
  chrome.runtime.sendMessage({method: 'getItem', key: "key"}, function (response) {
    if (response.data) {
      console.log(response.data);
    }
  });
}, false);

画面の適当なところクリックしたら、ログに取得した値を表示するといった内容です。

まとめ

今回はBrowser Actionsで格納したlocalStorageの値をContent ScriptsがBackground Pagesを経由して取得する処理について説明しました。
localStorageのスコープの違いは、意識していないとハマってしまうポイントです。
Content ScriptsとBackground Pagesをメッセージ通信させることで、Chrome Extensionの異なるスクリプト同士を連携させることができます。

小さいスクリプトでサクッと確認することができたので、良い学びになりました!

Railsでpreflightリクエストを処理し、no route matches optionsを回避する方法

概要

前回の記事ではRailsを使って、APIの受け側を作りました。
ただ、このままだとJSでフロントエンドからAPIに対してリクエストした場合、preflightリクエストが飛ぶので、No route matches [OPTIONS]エラーが返ってしまい、正常終了できません。
今回は、Railsでpreflightリクエストを処理し、no route matches optionsを回避する方法を書きます。

処理の方針

fetch("http://localhost:3000/auth_user", {
  method: 'POST',
  body:  JSON.stringify({ emai: "emai@email.com", password: "********"})
}).then(function(response) {
  return response.json();
}).then(function(json) {
  ...
});

こんな感じでフロントからリクエストをすることを想定します。
fetchでリクエストする場合、クロスドメインアクセスが可能か確認するするため、OPTIONSリクエストを飛ばして、それが200になってから本来のリクエストを飛ばします。
また、preflightリクエストが成功にするには、2つ条件があります。

  • OPTIONSをルーティングできること
  • response.headersに適切なキーとバリューが設定されていること

今回は、上記2つの条件を満たすための処理を書くことを目標にします。

OPTIONSをルーティングできること

乱暴ではありますが、いかなるpathのリクエストに対してもOPTIONSリクエストであれば、response.headersの設定を行えるようにします。 config/routes.rbに下記を追加しましょう。

match '*path' => 'options_request#preflight', via: :options

今回はoptions_request_controller.rbを作成して、そこで処理をすることにします。

response.headersに適切なキーとバリューが設定されていること

ルーティングファイルに従って、options_request_controller.rbを作成します。 また、response.headersに適切なキーとバリューが設定しなければならないので、このように処理をします。

class OptionsRequestController < ApplicationController
  ACCESS_CONTROL_ALLOW_METHODS = %w[GET OPTIONS PUT DELETE POST].freeze
  ACCESS_CONTROL_ALLOW_HEADERS = %w[Accept Origin Content-Type Authorization].freeze
  ACCESS_CONTROL_MAX_AGE = 86_400

  protect_from_forgery except: :preflight

  before_action :set_preflight_headers!, only: [:preflight]

  def preflight
    response.headers['Access-Control-Max-Age'] = ACCESS_CONTROL_MAX_AGE
    response.headers['Access-Control-Allow-Headers'] = ACCESS_CONTROL_ALLOW_HEADERS.join(',')
    response.headers['Access-Control-Allow-Methods'] = ACCESS_CONTROL_ALLOW_METHODS.join(',')
    response.headers['Access-Control-Allow-Origin'] = '*'
    head :ok
  end
end

こちらの処理の通り、response.headersの中にいくつかキーとバリューを設定しています。
レスポンスヘッダーの詳細説明に関しては、こちらの記事に譲るとして、やっていることをざっくり説明するとこのようになります

キー バリュー 意味
Access-Control-Max-Age 86400 preflightリクエストの結果を24時間キャッシュ
Access-Control-Allow-Headers Accept,Origin,Content-Type,Authorization リクエスト時にどのヘッダーが使用可能か明示
Access-Control-Allow-Methods GET,OPTIONS,PUT,DELETE,POST リソースのアクセス時に許容するメソッドを明示
Access-Control-Allow-Origin * 許容できるアクセス元のURLを明示

まとめ

今回、とりあえずハマりを抜け出すために、簡易な処理を書きましたが、要点としては下記2点です。

  • OPTIONSリクエストを受け付けるようにする
  • 受け付けたOPTIONSリクエストのレスポンスにAccess-Control-Allow-ほげほげをつける

SPAでAPI連携をするときにハマりポイントではありますし、何回作っても毎度ハマるので参考にしていただければと思います

Rails 5.2でJWTとdeviseを使った認証の仕組みを作る

概要

最近フロントエンドからAPIを叩く実装における認証の仕組みをどうするか考えていました。
以前、AWS Cognitoを使った認証仕組みを作ったことはあったのですが、Railsの場合はどのようにJWTを扱うか知りたかったので、作ってみました。

今回は、タイトルの通りRails 5.2でJWTとdeviseを使った認証の仕組みについて解説します。*1

想定しているケース

想定しているケースはシンプルです。
ユーザーの動き的には、こんなことをすることを想定しています。

  1. ユーザーがブラウザからメール / パスワードを入力してユーザー登録
  2. ユーザーがAPIでデータのやりとりをするために、CUIでメール / パスワードを使ってトークンを取得
  3. 1で取得したトークンをヘッダーに入れて、リクエストをすると、APIが叩けるようになる。

この記事では、主に2, 3.の実装について、説明します。

調査したこと

想定しているケースの「ユーザーがブラウザからメール/パスワードを入力してユーザー登録」はdeviseに任せようと思います。 トークンの発行と検証は、よくわからなかったので、調査したこととしてて、2つありました

  • JWTを扱うための仕組みがdeviseにあるか?
  • なければ、それを補うためのgemがあるのか?

JWTを扱うための仕組みがdeviseにあるか?

まだ未実装でした\(^o^)/

なければ、それを補うためのgemがあるのか?

ありました!!

Starの数を見ると、ruby-jwtが圧倒的だったのと、READMEを見ても、使い方がシンプルでわかりやすかったので、採用しました。

ユーザーがブラウザからメール / パスワードを入力してユーザー登録

まずは、ブラウザからアクセスできるようにしたり、ユーザー登録をするための下準備をします。

# app/controllers/top_controller.rb
class TopController < ApplicationController
  def index
  end
end
# config/routes.rb
Rails.application.routes.draw do
  root 'top#index'
end

つづいて、Gemを追加

# Gemfile
...
gem 'devise'
gem 'jwt'
...

その後こちらのコマンドを実行

bundle install
rails g devise:install
rails g devise User
rake db:migrate

トークンの取得、検証処理の追加

ここから、一気にトークン発行、検証の実装をしていきます。

lib/json_web_token.rbを新規作成します。

# lib/json_web_token.rb
class JsonWebToken
  class << self
    def encode(payload)
      JWT.encode(payload, Rails.application.credentials.config[:secret_key_base])
    end

    def decode(token)
      HashWithIndifferentAccess.new(
        JWT.decode(
          token,
          Rails.application.credentials.config[:secret_key_base]
        )[0]
      )
    rescue
      nil
    end
  end
end

JWTの秘密鍵は、Railsのsecret_key_baseを使いましょう。 *2

次に、上記のクラスをいちいちrequireをするのがめんどくさいので、config/initializers/jwt.rbを作って、Rails起動時に読み込まれるようにします。

# config/initializers/jwt.rb
require 'json_web_token'

続いて、ApplicationControllerでトークンのやりとりをするための処理を追加

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  attr_reader :current_user

  protected

  def authenticate_request!
    unless user_id_in_token?
      render json: { errors: ['Not Authenticated'] }, status: :unauthorized
      return
    end
    @current_user = User.find(auth_token[:user_id])
  rescue JWT::VerificationError, JWT::DecodeError
    render json: { errors: ['Not Authenticated'] }, status: :unauthorized
  end

  private

  def http_token
    @http_token ||= if request.headers['Authorization'].present?
                      request.headers['Authorization'].split(' ').last
                    end
  end

  def auth_token
    @auth_token ||= JsonWebToken.decode(http_token)
  end

  def user_id_in_token?
    http_token && auth_token && auth_token[:user_id].to_i
  end
end

最後に、トークンを作るためのAPIの受け口を作っていきます。

# app/controllers/authentication_controller.rb
class AuthenticationController < ApplicationController
  protect_from_forgery except: :authenticate_user
  def authenticate_user
    user = User.find_for_database_authentication(email: params[:email])
    if user.valid_password?(params[:password])
      render json: payload(user)
    else
      render json: {errors: ['Invalid Username/Password']}, status: :unauthorized
    end
  end

  private

  def payload(user)
    return nil unless user and user.id
    {
      auth_token: JsonWebToken.encode({user_id: user.id, exp: (Time.now + 2.week).to_i}),
      user: {id: user.id, email: user.email}
    }
  end

protect_from_forgery except: :authenticate_userを設定しないとAPIを叩いた時に、CORSエラーとなるため、設定しておきます。
※今回の実装だと、JWTは一度発行すると2週間は失効できなくなるので、payloadのexpをよしなに変更するか、別実装は考えておいた方が良いと思います。

config/routes.rbに下記を追加。

# config/routes.rb
post 'auth_user' => 'authentication#authenticate_user'

最後に、top_controllerにこれまで作ってきたactionをbefore_actionとして設定します。

# app/controllers/top_controller.rb
class TopController < ApplicationController
  before_action :authenticate_request!
  def index
    render json: {'logged_in' => true}
  end
end

これで実装完了です!

動作確認

実装が終わったので、意図した実装になっているか、確認します。

ブラウザからユーザー登録していくのはめんどくさいので、rails cでやっちゃいましょう。

$ rails c
User.create(email:'test@test.com', password:'changeme', password_confirmation:'changeme')

これで下準備は完了です。
rails sで起動して、curl http://localhost:3000/を実行してみましょう。

レスポンスとして{"errors":["Not Authenticated"]}が返ってくればOKです。

トークンの取得

curl -X POST -d email="test@test.com" -d password="changeme" http://localhost:3000/auth_user こちらを実行すると、レスポンスとしてトークンが返ってきます。

{"auth_token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MjYxOTA1MDB9.ABVCQGdzF3u2XcAp66vXZxeUy2dhsCuxsg88NsEdoFs","user":{"id":1,"email":"test@test.com"}}

トークンを使ってAPI実行

返ってきたトークンを使えば、認証が通るようになっているはずです。やってみましょう。

curl --header "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MjYxOTA1MDB9.ABVCQGdzF3u2XcAp66vXZxeUy2dhsCuxsg88NsEdoFs" http://localhost:3000

{"logged_in":true}というjsonが返って来れば成功です!

まとめ

基本の形はこれで十分なのではないかと思います。

システムによって検討しなければならない点としては、先述の通り、トークンの失効するタイミングについてです。 この実装では2週間待たなければトークンは失効されず、ずっと有効なままです。そのため、仮に失効しなければならないことがあった場合に、ほぼ対応不能となるので、認証時の処理を少し変える必要があると思います。

とはいえ、RailsでJWTを簡単に扱えるgemがあったので、JWTを使った認証の仕組みをサクッと作ることができました。 deviseが入っているシステムに対しての連携もうまくいったので、良い学びになりました。

*1:Rails, JWT, deviseの説明は省きます

*2:Rails 5.2から秘密鍵周りが変わったので、5.1以下を使っている方は書き方が多少異なります。 おそらくRails.application.secrets.secret_key_baseとなるはず。

Redashのバージョンアップで消えたデータソースを復活させる方法

Redashのバージョン2系から3系にupgradeした際、DataSourceのプルダウンから色々なものが消えてしまいました(!)

  • BigQuery
  • Athena
  • Google Analytics
  • Google Spreadsheet

業務でも使っているデータソースだったので、これが使えなくなるとなかなか厳しいです。
今回は、Redashのバージョンアップで消えてしまったこれらのデータソースを復活させる方法を書きます。

続きを読む