Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
ngrok(エングロクと読む)は、httpのトンネリングサービスです。
ngrokでホストされるURL(例:http://xxxx.ngrok.io/ )へのアクセスをローカル環境のwebサーバー(http://localhost:3000 など)にトンネリングしてくれます。
例えば、stripeやLINE、その他クラウドサービスのwebhook処理を開発する際に、インターネット上に本番やステージングのwebサーバーを構築する前に、ローカル開発環境でwebhookへの対応コードを実装、テストすることができるようになります。
この記事では、ngrokの開発環境を用意する手順を紹介します。
公式サイトでダウンロードして、インストールすることもできますが、インストールはmacOSの場合であれば、homebrewを使うほうが楽です。以下のコマンドでインストールできます。
% brew install --cask ngrok
ngrokを使うためには、サービスの利用登録が必要なので、公式サイトでサインアップを行ってください。
サインアップするとログイン後のページにauthtokenが表示されているので、それをコピーして、以下のように、コンソールから設定します。
% ngrok authtoken <your-token>
そうすると、~/.ngrok2/ngrok.yml
にtokenの値が書き込まれてサインアップしたngrokアカウントが認識されるようになります。
トンネリングを開始するためには、以下のようなコマンドを実行します。
% ngrok http 3000
これで、以下のような情報がコンソールに表示され、トンネリングに使えるURLが分かります。
ngrok by @inconshreveable (Ctrl+C to quit)
Session Status online
Account <your account name>(Plan: Basic)
Update update available (version 2.3.40, Ctrl-U to update)
Version 2.3.35
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://<your-random-subdomain>.ngrok.io -> http://localhost:3000
Forwarding https://<your-random-subdomain>.ngrok.io -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
この記事では <your-random-subdomain>
と記載しましたが、ここに、トンネリングで使えるインターネットアクセス可能なURLが表示され、また、そのURLがローカルのhttp 3000番ポートにトンネリングされているのが分かります。
この一般公開されているURLをクラウドサービスのwebhookのエンドポイントとして指定しておけば、ローカル環境のwebサーバーを使って動作確認をしながら開発を行うことができます。
無償のアカウントでは、ngrokを起動するたびに、http://<your-random-subdomain>.ngrok.io
の <your-random-subdomain>
の部分がランダムなサブドメインになってしまい、そのたびに、クラウドサービス側の呼び出し先設定を更新しなければなりません。
これは、ngrokの有償登録をすることで固定化できます。有償登録をすると、以下のようにサブドメインを他のユーザーが使っていない任意の文字列に固定できます。
% ngrok http 3000 -subdomain=<任意の固定したいサブドメイン>
ngrok起動中のコンソールでCtrl+U
でアップデートが開始されます。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
株式会社プレセナ・ストラテジック・パートナーズのエンジニアのための知識ベースとして使用しているオンラインサイトです。
株式会社プレセナ・ストラテジック・パートナーズにおけるシステム開発で発生した各種調査、検討結果や他のメンバーへ共有したいノウハウなどのうち、社外にも公開できるものをコンテンツとして蓄積しています。
以下のような分野に関係するコンテンツを記載しています。
ソフトウェア開発
インフラ開発
セキュリティ
上記以外も、システム開発時に新たに進出した分野があれば追加していく予定です。
株式会社プレセナ・ストラテジック・パートナーズ社内のエンジニア、デザイナー
社外のシステムエンジニア、デザイナー
本サイトのコンテンツは可能な限り正確を期して記載していますが、本サイトのコンテンツに基づく運用結果については、株式会社プレセナ・ストラテジック・パートナーズは責任を負いかねますので、ご了承の上、内容を閲覧してください。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
ngrokが最近メジャーバージョンアップして、バージョン3がリリースされましたので、アップグレード手順を紹介します。
なお、設定ファイルは、下位互換性がなくなっているので、ngrok本体のアップグレード後には、設定ファイルのアップグレードが必要になります。
当サイトの導入記事で紹介したように、Homebrewでcask(Mac用アプリケーション)としてngrokをインストールしている場合は、以下のコマンドで本体をアップグレードします。
% brew upgrade ngrok
ちなみに、ngrokには、ngrok update
コマンドもありますが、このコマンドではメジャーバージョンアップはされません。
アップグレード後にバージョンを確認するには、以下を実行します。
% ngrok version
ngrok version 3.0.6
なお、この直後に、ngrokを起動すると以下のようなエラーが出るため、後述の設定ファイルのアップグレードが必要になります。
% ngrok http 3000
ERROR: Error reading configuration file '/Users/<your-os-user>/.ngrok2/ngrok.yml': `version` property is required.
ERROR:
ERROR: If you're upgrading from an older version of ngrok, you can run:
ERROR:
ERROR: ngrok config upgrade
ERROR:
ERROR: to upgrade to the new format and add the version number.
さきほどのエラーメッセージにも、公式サイトにも説明されている通り、設定ファイルのアップグレードも必要になります。
% ngrok config upgrade
これで、~/.ngrok2/ngrok.yml
ファイルが更新されて、通常通りngrokが使用できるようになります。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
この記事は、独特なネーミングがされていて、知っているようで知らないHomebrewの用語の意味を正しく理解するために書きました。
Homebrewは「自家醸造」の意味です。この「醸造」の世界観の元で、Homebrewで扱う一部の概念の名前がつけられていて、直感的に何を指すのかが少しわかりにくいことがあります。
インストール関連のトラブルやPATHを通すパッケージのバージョンを調整したいときに、これらの用語を知っていると、エラー・警告メッセージの内容や、brew info
コマンドなどで説明されている内容を理解しやすくなると思います。
以下、用語の定義と意味を説明します。正式な情報は公式サイトを参照してください。
formula (製法)
homebrewでupstreamのソースからビルドするパッケージの定義。formulaeは、その複数形。
cask (大きいたる)
macOSネイティブアプリケーションをインストールするパッケージの定義。
keg (小さいたる)
任意のformulaの任意のバージョンのインストール先ディレクトリ。 例 : /usr/local/Cellar/[formula]/0.1
rack (棚)
1つ以上のバージョンのkegを含むディレクトリ。 例: /usr/local/Cellar/[formula]
prefix
homebrewでパッケージを配置する親ディレクトリ。 例: Intel Macの場合は、/usr/local
keg-only
kegにパッケージを配置するだけで、prefixのbinディレクトリに、シンボリックリンクが作られないこと。
postgresqlもkeg-onlyなので、brew install postgresql
でインストールしても、PATHが通った状態にはならない。
(keg-onlyなパッケージにPATHを通したければ brew link
コマンドを使う。)
cellar (貯蔵室)
1つ以上のrackを含むディレクトリ。 例: /usr/local/Cellar
Caskroom (cask用の貯蔵室)
1つ以上のcaskを含むディレクトリ。 例 : /usr/local/Caskroom
external command
Homebrew/brewのGitHubリポジトリ外で定義されているbrew
の(追加インストール可能な)サブコマンド。
tap (蛇口)
formula、cask、external command用のディレクトリ(かつ、通常はGitリポジトリ)。
bottle (ボトル)
ソースからビルドするのではなく、cellarやrackに配置するために、事前にビルドされたkeg。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
当社の日常業務の中で、Slackのリマインダーを設定することは、頻繁にあります。
しかし、Slackのコマンドを使ってリマインダーを指定する際、とくに、日時の指定の仕方を中々覚えられず、公式ヘルプを見に行くことが多いです。
公式ヘルプにも詳しく載ってはいるのですが、特に「When」を指定する部分について、この記事では、一覧で分かるように紹介します。チートシート的な使い方を想定しています。
/remind
コマンドのフォーマットは以下です。
/remind [@someone or #channel] [what] [when]
この[what]
の部分まではわかりやすいのですが、この記事では最後の[when]
の部分を詳細に紹介します。
[when]
の部分は、以下のように、細かく指定することが出来ます。
x分後、x日後、x週間後など、現在からの相対的な時間で指定する場合、in
を使います。
/remind #channel 2秒後の例 in 2 seconds
※秒を指定しても、そこまで正確なタイミングではリマインドされないようです。
/remind #channel 2分後の例 in 2 minutes
/remind #channel 2時間後の例 in 2 hours
/remind #channel 2日後の例 in 2 days
なお、時間を指定せずに相対的な日付を指定すると、指定した日の9:00になるようです。
/remind #channel 2週間後の例 in 2 weeks
2日後のX時、など細かい時間を指定したい場合は、atも併用します。
/remind #channel 2日後10時amの例 at 10:00am in 2 days
次ように、at
とon
を使って、時間と日付を指定します。
/remind #channel 日時指定の例 at 10:00am on 1st Feb
次のように曜日を指定すると、指定した曜日が次に来る日に投稿されます。
/remind #channel 曜日指定の例 at 10:00am on Mon
この使い方が一番多い気がします。末尾にevery
をつけて頻度を指定します。
毎月月初にやらないと行けないタスクをリマインドさせるなどに便利です。
/remind #channel 毎月1日の例 on 1st every month
この場合もat
とon
を併用できます
/remind #channel 毎月1日の10:00am at 10:00am on 1st every month
以下のように、特に"
で囲んだりせずに、普通に指定できます。
/remind #channel aaaaa bbbbb ccccc on 1st Feb
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
Slack emoji とGoogle meet を連携して使う
当社のエンジニアは全員フルリモートで働いているため、対面での相談は Google meet
を使うことが多いです。
ただ、対面で相談するまでの準備に手間がかかると、気軽な相談はしづらくなりがちです。
そこで当社では、「Slackのカスタムレスポンスとemoji」にGoogle meetのURLを紐付けています。その結果、Slackでのやりとりが難しいと感じた時に、すぐ対面での相談へと移行できています。
Google meet
Slack
Slackへ相談の前フリと :room:
などのemojiをポストする
Slack botが相談用のGoogle meetのURLを案内する
任意の画像を用意します。
もし、文字列画像を用意する場合は、絵文字ジェネレータ(https://emoji-gen.ninja/)などが便利です。
なお、 生成履歴に絵文字を表示する
のチェックボックスについては状況によりON/OFFします。
Slackのemoji設定ページ(https://***.slack.com/customize/emoji)を開き、文字列 (例: :room:
)と 上記1.の画像を登録します。
Google meetのページ(https://meet.google.com/)にて、URLを取得します。
Slack botの設定ページ( https://***.slack.com/customize/slackbot)を開きます。
Slackbotタブに以下を入力し、保存します。
When someone says
に、上記2.のemojiの値 (例: :room:
)
Slackbot responds
に、上記3.のGoogle meetのURL
以上で設定は完了です。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
公式サイトでインストーラーをダウンロードしてインストールしてください。
基本的に以下の公式サイトに従うだけです。
公式サイトにも載っている内容ですが、以下のコマンドを入力して、
% aws configure
以下4つの項目を設定すればOKです。
AWS Access Key ID
AWS Secret Access Key
Default region name
Default output format
これも公式サイトに載っているままの情報ですが、利用したいIAMユーザーのAccess Key IDとSecret Access Key は、https://console.aws.amazon.com/iam/ から、設定したいユーザーを選択して、「認証情報」タブでアクセスキーを生成してください。
次に、多くの場合に必要となるjqとSession Manager pluginをインストールします。
jqはJSONを操作するためのCLIツールで、近年急速に利用が広がっています。Macの場合はbrewでインストールできます。
% brew install jq
Session Manager pluginは、AWS CLIのプラグインで、ECSタスク内のコンテナやCLI経由でEC2にログインする際に必要です。公式ドキュメントに従ってインストールしてください。
以上。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
この記事では、M1 Macでの開発環境構築でハマったところを共有するために、記載して行きます。今後、随時新しいハマりどころが発生した場合は、情報を追加していきます。
筆者の環境では、環境構築の検証もかねているので、Rosetta 2をインストールしていません。Rosetta 2をインストール済みの場合は前提が異なってくるため、ご注意ください。
また、以下のものはインストール済みとします。
rbenv
Docker Desktop(4.3.0以上)
postgresql(実行用ではなく、gem pgのビルド時に利用する想定。brewでインストールする)
※Docker Desktopは、バージョン4.3.0以降でもRosetta 2に依存している部分は残っているようですが、依存度は以前と比べて低くなっているようです。
% rbenv install 2.6.5
のように、古めのRubyのバージョンをインストールすると、以下のようなエラーが発生しました。
Last 10 log lines:
compiling fiber.c
linking shared-object fiber.bundle
compiling closure.c
closure.c:263:14: error: implicit declaration of function 'ffi_prep_closure' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
result = ffi_prep_closure(pcl, cif, callback, (void *)self);
^
1 error generated.
make[2]: *** [closure.o] Error 1
make[1]: *** [ext/fiddle/all] Error 2
make: *** [build-ext] Error 2
gem ffiの公式リポジトリのissueによると、以下2つの対処法があるようです。
以下のように、RUBY_CFLAGS
を指定して、rbenvのインストールコマンドを実行すると上手く行きます。
RUBY_CFLAGS=-DUSE_FFI_CLOSURE_ALLOC rbenv install 2.6.5
筆者もこの方法を先に試して上手く行ったため、この後の2つめの方法を試せていません。
brew info libffi
コマンドを実行すると、以下のようなCaveatsが表示されます。
==> Caveats
libffi is keg-only, which means it was not symlinked into /opt/homebrew,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.
For compilers to find libffi you may need to set:
export LDFLAGS="-L/opt/homebrew/opt/libffi/lib"
export CPPFLAGS="-I/opt/homebrew/opt/libffi/include"
For pkg-config to find libffi you may need to set:
export PKG_CONFIG_PATH="/opt/homebrew/opt/libffi/lib/pkgconfig"
これにしたがって、環境変数LDFLAGS
、CPPFLAGS
、PKG_CONFIG_PATH
を指定して、以下のように実行すると良いとのことです(筆者は、試せていませんが掲載しておきます)。
% export LDFLAGS="-L/opt/homebrew/opt/libffi/lib"
% export CPPFLAGS="-I/opt/homebrew/opt/libffi/include"
% export PKG_CONFIG_PATH="/opt/homebrew/opt/libffi/lib/pkgconfig"
% rbenv install 2.6.5
※記事のみやすさのために、環境変数ごとにexportをしていますが、一気に複数の環境変数を指定しても良いと思います。
Docker公式サイトで説明されているように、docker-compose
コマンドはv1のdocker-composeが使われるためRosetta 2のインストールが必要です。、これまでdocker-compose
コマンドを使用していた場合は、以下のように、docker compose
コマンドを使います。
% docker compose up -d
bundle install時にネイティブ拡張が含まれるいくつかのgemでインストールに失敗したのでgemごとの対処法を記載しておきます。
このgemのインストールに失敗する原因は、古いRubyをインストールしたときに出たエラーとと同じです。筆者は、gemのインストール時に発生したエラーに対しては、以下のように、brew info コマンド実行時に表示されるCaveatsの内容にしたがって対処しました。
% export LDFLAGS="-L/opt/homebrew/opt/libffi/lib"
% export CPPFLAGS="-I/opt/homebrew/opt/libffi/include"
% export PKG_CONFIG_PATH="/opt/homebrew/opt/libffi/lib/pkgconfig"
% bundle install
pgは、M1 Mac特有のエラーというよりかは、Intel Macでもよく発生する、Ruby開発者ならよく見たことあるエラーですが、以下のエラーが発生します。
checking for pg_config... no
No pg_config... trying anyway. If building fails, please try again with
--with-pg-config=/path/to/pg_config
checking for libpq-fe.h... no
Can't find the 'libpq-fe.h header
*** 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=/home/vagrant/.rbenv/versions/2.5.1/bin/$(RUBY_BASE_NAME)
--with-pg
--without-pg
--enable-windows-cross
--disable-windows-cross
--with-pg-config
--without-pg-config
--with-pg_config
--without-pg_config
--with-pg-dir
--without-pg-dir
--with-pg-include
--without-pg-include=${pg-dir}/include
--with-pg-lib
--without-pg-lib=${pg-dir}/lib
対処法もいつもどおりで、やり方はいくつかありますが、筆者は以下のようにbundleのbuild時のconfigを指定したのち、bundle install
を実行しました。
% bundle config build.pg --with-pg-config=$(brew --prefix postgresql@13)/bin/pg_config
なお、このコマンドを実行すると設定が~/.bundle/config
に書き込まれます。
rmagickもM1固有の問題ではなく、毎回遭遇する以下のエラーが発生します。
./siteconf20220118-1858-ly2is3.rb extconf.rb
checking for clang... yes
checking for Magick-config... yes
checking for outdated ImageMagick version (<= 6.4.9)... no
checking for presence of MagickWand API (ImageMagick version >= 6.9.0)... no
Package MagickWand-6.Q16 was not found in the pkg-config search path.
Perhaps you should add the directory containing `MagickWand-6.Q16.pc'
to the PKG_CONFIG_PATH environment variable
No package 'MagickWand-6.Q16' found
Package MagickWand-6.Q16 was not found in the pkg-config search path.
Perhaps you should add the directory containing `MagickWand-6.Q16.pc'
to the PKG_CONFIG_PATH environment variable
No package 'MagickWand-6.Q16' found
Package MagickWand-6.Q16 was not found in the pkg-config search path.
Perhaps you should add the directory containing `MagickWand-6.Q16.pc'
to the PKG_CONFIG_PATH environment variable
No package 'MagickWand-6.Q16' found
Package MagickWand-6.Q16 was not found in the pkg-config search path.
Perhaps you should add the directory containing `MagickWand-6.Q16.pc'
to the PKG_CONFIG_PATH environment variable
No package 'MagickWand-6.Q16' found
checking for Ruby version >= 1.8.5... yes
Package MagickCore was not found in the pkg-config search path.
Perhaps you should add the directory containing `MagickCore.pc'
to the PKG_CONFIG_PATH environment variable
No package 'MagickCore' found
Can't install RMagick 2.16.0. Can't find the ImageMagick library or one of the dependent libraries. Check the mkmf.log file
for more detailed information.
これは、まず、imagemagickがインストールされていないことが原因で、もしインストール済でこのエラーが発生しているなら、convertコマンドへのPATHが未設定であったり、ビルドに必要なファイルが指定されていなかったりすることが原因です。
以下のように対処します。
まずは、必要なImageMagickをインストールします。Rmagickが対応しているバージョン6を、必ずインストールしてください。
% brew install imagemagick@6
次に、brew info imagemagick@6 コマンドを実行して表示される以下にしたがって、
==> Caveats
imagemagick@6 is keg-only, which means it was not symlinked into /opt/homebrew,
because this is an alternate version of another formula.
If you need to have imagemagick@6 first in your PATH, run:
echo 'export PATH="/opt/homebrew/opt/imagemagick@6/bin:$PATH"' >> ~/.zshrc
For compilers to find imagemagick@6 you may need to set:
export LDFLAGS="-L/opt/homebrew/opt/imagemagick@6/lib"
export CPPFLAGS="-I/opt/homebrew/opt/imagemagick@6/include"
For pkg-config to find imagemagick@6 you may need to set:
export PKG_CONFIG_PATH="/opt/homebrew/opt/imagemagick@6/lib/pkgconfig"
まず、実行時に必要と思われるconvert
コマンドへのPATHを通しておきます。
% echo 'export PATH="/opt/homebrew/opt/imagemagick@6/bin:$PATH"' >> ~/.zshrc
念の為、以下のようにconvertコマンドが使えるかを確認しておくとよいでしょう。
% convert --version
次に、rmagickのビルドに必要な環境変数を指定してから、bundle installを実行します。
% export LDFLAGS="-L/opt/homebrew/opt/imagemagick@6/lib"
% export CPPFLAGS="-I/opt/homebrew/opt/imagemagick@6/include"
% export PKG_CONFIG_PATH="/opt/homebrew/opt/imagemagick@6/lib/pkgconfig"
% bundle install
ここまでで、gemのインストールまで出来ました。
なお、2022年1月17日現在、筆者の環境では、この後、古いnodeをインストールしようとしてエラーが発生して失敗し、対応方法が調べきれず、まだRailsアプリケーションを実行するところまでたどり着いておりません。ただ、今後、nodeをバージョンアップさせる予定なので、新しいバージョンのnodeを利用することで、エラーを回避できる可能性があります。
上記については、後日、この記事をアップデートして共有しようと思います。
zshを使うときに、補完用の情報を設定をしておくことで、例えば、git、aws-cli、dockerなどのコマンドやサブコマンドをTabキーで補完してくれるツールです。
公式サイトは、以下です。
macOSを使っている前提ですが、homebrewで簡単にインストールできます。
% brew install zsh-completion
zsh-completionをインストールすると、以下のようなメッセージがでます。
zsh-completions: stable 0.33.0 (bottled), HEAD
Additional completion definitions for zsh
https://github.com/zsh-users/zsh-completions
/opt/homebrew/Cellar/zsh-completions/0.32.0_1 (142 files, 1.1MB) *
Poured from bottle on 2021-05-03 at 10:22:36
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/zsh-completions.rb
License: MIT-Modern-Variant
==> Options
--HEAD
Install HEAD version
==> Caveats
To activate these completions, add the following to your .zshrc:
autoload -Uz compinit
compinit
You may also need to force rebuild `zcompdump`:
rm -f ~/.zcompdump; compinit
Additionally, if you receive "zsh compinit: insecure directories" warnings when attempting
to load these completions, you may need to run this:
chmod -R go-w '/opt/homebrew/share/zsh'
zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions
==> Analytics
install: 9,459 (30 days), 39,117 (90 days), 147,856 (365 days)
install-on-request: 9,388 (30 days), 38,843 (90 days), 146,385 (365 days)
build-error: 0 (30 days)
なお、このメッセージは、brew info zsh-completion
コマンドで再表示できます。
このメッセージにしたがって、まずは、次のように .zshrc
ファイル設定します。
# zsh-completionの設定
autoload -Uz compinit
compinit
次に、zsh関連ファイルの権限設定を行います。これも前述のbrew info
で表示されるメッセージに従います。
前述の brew info
で表示されるメッセージの /opt/homebrew
の部分は、使用しているMacのCPUがIntel製かApple Siliconかによって異なります。
なので、環境に応じて次のようにコマンドを打ちます。
% chmod -R go-w /usr/local/share/zsh/
% chmod -R go-w /opt/homebrew/share/zsh/
あるいは、以下のコマンドを打てばどちらの環境でもOKです。
% chmod -R go-w $(brew --prefix)/share/zsh/
なお、この go-w
は、g(roup)とo(thers)から(つまりuser以外から)、書き込み権限を削除(-)しています。
homebrewでgitをインストールすると、自動的に /usr/local/share/zsh/site-functions/ 以下に、git用の補完設定もインストールされるので、それ以上何もする必要はありません。
gitはmacOS標準のものもありますが、上記の補完設定のために、筆者はhomebrewでgitをインストールしなおしてしまいます。
設定がうまくいっている場合、以下のように補完されます。ブランチ名などは、覚えられないので補完を使うと便利になります。
% git branch <TAB>
(ここに候補となるブランチ一覧が表示される)
公式サイトに説明があるので、それに従います。手順は公式サイトに記載されていて、また、迷うところもないので、ここに手順を記載するのは省略します。
設定がうまくいっている場合、以下のような補完が使えます。
% docker container <TAB>
attach -- Attach to a running container
commit -- Create a new image from a container's changes
cp -- Copy files/folders between a container and the local filesystem
create -- Create a new container
diff -- Inspect changes on a container's filesystem
exec -- Run a command in a running container
export -- Export a container's filesystem as a tar archive
inspect -- Display detailed information on one or more containers
kill -- Kill one or more running containers
logs -- Fetch the logs of a container
ls -- List containers
pause -- Pause all processes within one or more containers
port -- List port mappings or a specific mapping for the container
prune -- Remove all stopped containers
rename -- Rename a container
restart -- Restart one or more containers
rm -- Remove one or more containers
run -- Run a command in a new container
start -- Start one or more stopped containers
stats -- Display a live stream of container(s) resource usage statistics
stop -- Stop one or more running containers
top -- Display the running processes of a container
unpause -- Unpause all processes within one or more containers
update -- Update configuration of one or more containers
wait -- Block until one or more containers stop, then print their exit codes
aws-cliも公式サイトに従うだけですが、少しわかりにくいので、補足説明を追加します。
公式サイトは、以下です。
なお、この記事では、AWS CLI の記事に記載さた手順でインストールされている前提で補足説明をします。
前述の前提の場合、aws_completer はすでにPATHが通っているので、やることは以下の設定を.zshrc
ファイルに追加するだけです。
autoload bashcompinit && bashcompinit # この行を追加
autoload -Uz compinit
compinit
complete -C '/usr/local/bin/aws_completer' aws # この行を追加
設定後は、zshを再起動します。
設定がうまくいっていれば、以下のようにawsコマンドの補完が効くようになります。
% aws s<TAB>
s3 securityhub sqs
s3api serverlessrepo ssm
s3control service-quotas sso
s3outposts servicecatalog sso-admin
sagemaker servicecatalog-appregistry sso-oidc
sagemaker-a2i-runtime servicediscovery stepfunctions
sagemaker-edge ses storagegateway
sagemaker-featurestore-runtime sesv2 sts
sagemaker-runtime shield support
savingsplans signer swf
schemas sms synthetics
sdb snowball
secretsmanager sns
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
おもにJavaScriptなどのフロントエンド系のソースコードのフォーマッタです。プラグインを追加することでRubyなどの言語にも対応可能です。詳細は公式サイトを参照してください。
以下、開発プロジェクトへのインストール手順を説明します。
開発プロジェクトにおいて、yarnでnodeパッケージが管理されている前提です。
まずは、開発環境のみの依存パッケージとして、prettier をインストールします。
% yarn add --dev prettier
必要に応じて、prettier-rubyもインストールします。
% yarn add --dev @prettier/plugin-ruby
なお、prettier-rubyは、gemでもインストール可能ですが、JS用のprettierと別管理にしたくないので、上記のようにnodeパッケージのプラグインとしてインストールしています。
プロジェクトルートに以下のファイルを作成します。
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"endOfLine": "lf",
"printWidth": 120
}
上記の設定は、当社で使っているデフォルトの設定です。例えば、printWidth
は、デフォルトの80だと少し狭く、フォーマットしたときに、過度に改行が入ってしまうため、120にしています。
Railsのroutesファイルなどのように、なるべく改行をせずに一行で書いてしまいたいような特殊なコードは、pretterでのフォーマットがかからないように、ignoreファイルをプロジェクトルートに追加します。
node_modules
yarn.lock
package-lock.json
public
/config/routes.rb
rubocopも併用しているプロジェクトの場合、rubocopに定義されているスタイル関連のルールとprettierのフォーマットが競合して、gitのpre-commitにrubocopのチェックを行っているとリポジトリにコミットできなくなってしまいます。
そういった場合のために、rubocopでprettierと競合するルールをオフにするための設定が、prettier-rubyには含まれていて、この設定をプロジェクト用の.rubocop.yml
で継承します(参考:公式サイト)。
inherit_from:
- node_modules/@prettier/plugin-ruby/rubocop.yml # rubocopのルールと衝突しないための設定
また、これ以外にも、rubyの後置ifや後置whileなどのフォーマットをprettierにまかせてしまいたいので、以下のように.rubocop.yml
にルールを追加します。
# 後置ifのフォーマットはprettier-rubyに任せたいので、rubocopのチェックは外す
Style/IfUnlessModifier:
Enabled: false
# 後置while、untilのフォーマットもprettier-rubyに任せたいので、rubocopのチェックは外す
Style/WhileUntilModifier:
Enabled: false
以下、RubyMineやVisual Studio Codeの設定を各自行います。
最新のRubyMineでは、Prettierプラグインはデフォルトでインストール済なので、設定だけ行います(参考:公式サイト)。
JavaScriptやTypeScriptだけでなく、Rubyもフォーマットの対象にする場合は、設定画面で、Preferences -> Languages & Frameworks -> JavaScript -> Prettier
から、Prettier Packageを{Project Root}/node_modules/prettier
に設定した上で、Run for files
の部分でrbも含まれるように、以下のように変えておきます。
{**/*,*}.{js,ts,jsx,tsx,rb,rake}
On save のチェックをいれておくと、⌘S を押したときにフォーマットがかかります。 設定保存後もprettierが動作しない場合は、一度RubyMineを再起動するとうまくいきます。
prettier-vscodeプラグインを使います。
上記プラグインをインストールした上で、以下のように、.vscode/settings.json
をプロジェクトルートに追加して、言語ごとにフォーマットの設定をします。チームメンバーのvscodeの設定状況によっては、このファイルはリポジトリにpushして他のメンバーと共有してもよいです。
{
"[ruby]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
なお、editor.formatOnSave
は任意の設定で、保存時に自動的にフォーマットをかけるかを設定します。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
この記事では、Railsの本番環境におけるアプリケーションサーバー(pumaやunicornなど)のプロセス数とスレッド数のパラメータ設定に関する情報をまとめます。
アプリケーションサーバーのパラメータを設定する際に、OSにおけるプロセスとスレッドは重要な要素になるので、あらかじめ概要を説明しておきます。
プロセスはOSで実行中のプログラムのことで、プロセス内には1つ以上のスレッドが含まれます。CPUのコアに命令をしているのがスレッドです。
複数のプロセス同士は、同じメモリ領域を共有できません。しかし、同じプロセス内のスレッド同士は、同じメモリ領域を共有できるので、プロセス内に複数のスレッドを作成した方がメモリの利用効率が上がります。
複数のスレッドが同じメモリ領域を共有すると、メモリの利用効率は上がりますが、誤って別のスレッドに影響する情報を書き換えてしまう可能性もあります。いわゆるスレッドセーフではない状態が起こるということです。
1つのスレッドしかないプロセスを複数使用すれば、スレッドセーフかどうかを考慮する必要はなくなりますが、複数のスレッドを使う場合と比較して、メモリの利用効率は下がってしまいます。
1つのプロセス内に複数のスレッドを作った場合のメリットとして、一部のスレッドがIO待ち(例えば、データベースへのリクエストとそのレスポンスを待っているような状態)になってしまっても、プロセス内の他のスレッドで別の処理を進めることができるという点があります。この場合、プロセスとしてのスループットが大きくなります。
もし、1つのプロセスで1つのスレッドしか用意しなかったとしたら、そのスレッドでIO待ちが発生すると処理がブロックしてしまいます。
Railsで使うアプリケーションサーバーとして一般的なものにpumaとunicornがあります。
pumaは、マルチプロセス対応で、さらに、各プロセス内に複数のスレッドを作成することができます。
unicornは、マルチプロセス対応ですが、各プロセス内には複数のスレッドを作りません。
したがって、スレッド間でメモリを共有できる分、メモリ効率はpumaの方が良くなりやすいです。ただし、pumaを使う場合は利用しているgemも含めてスレッドセーフな実装でアプリケーションを作っておく必要があります。
一方、unicornは、メモリ効率はpumaと比較して良くありませんが、スレッドセーフであるかどうかを気にする必要がありません。また、詳細な説明は省略しますが、稼働中のサーバーにデプロイを行う際にゼロダウンタイムのデプロイをすることが可能、などの特徴もあります。
この記事では、Rails標準のアプリケーションサーバーであるpumaを使う想定で、以降の説明をします。
Ruby には、Giant VM lock (GVL)とよばれるものが存在します。Giant VM Lockが効いている間は、ネイティブスレッドがロックされ、実質1つのスレッドしか同時に実行できません。
しかし、GVLについては、Ruby 3.0.0のリファレンスマニュアル(上のリンク先)には、以下のように記載されています。
ネイティブスレッドを用いて実装されていますが、現在の実装では Ruby VM は Giant VM lock (GVL) を有しており、同時に実行されるネイティブスレッドは常にひとつです。ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には GVL を解放します。その場合にはスレッドは同時に実行され得ます。また拡張ライブラリから GVL を操作できるので、複数のスレッドを同時に実行するような拡張ライブラリは作成可能です。
詳細な説明は省略しますが、要するに、Rubyのスレッドは、IO関連のシステムコールを行う場合にはGVLを開放するので、その場合は複数のスレッドを実行できます。しかし、逆にIO関連以外のシステムコール(例えば数値計算など)の場合には、GVLが効いてしまい、同時に1つのスレッドしか実行されません。
Webアプリケーションにおいては、IO関連のシステムコール(例えばネットワーク越しのリクエスト・レスポンス処理)が使われることも多くなるため、アプリケーションサーバーが、マルチスレッド対応をすることのメリットはあると考えられます。
前置きが長くなりましたが、ここからが本題です。
一般的なRailsアプリケーションを実行する本番環境のプロセス数とスレッド数を設定するには、以下を考慮に入れます。
CPU数(環境によっては、vCPU数、仮想CPU数)
利用可能なメモリの量
1スレッドで必要なメモリの量
Railsで設定しているコネクションプールの数
webとjobワーカーの数
データベースの同時接続数上限
CPU数は、利用しているサーバーのCPUコアの数です。クラウドのような仮想環境を利用している場合は、物理的なコア数ではなく仮想CPU数(vCPU数)を把握する必要があるかもしれません。
CPUの数は、設定可能なプロセス数に影響を及ぼします。どのように影響するかについて、Herokuの記事では、以下のように言及しています。
Due to the GVL, the Ruby interpreter (MRI) can only run one thread executing Ruby code at a time. Due to this limitation, to fully make use of multiple cores, your application should have a process count that matches the number of physical cores on the system.)
(訳注:IO待ちの場合は、GVLが開放されるとはいえ)GVLを考慮するとCPUの機能をフル活用するなら、Rubyコードのプロセス数は、CPUのコア数に一致させるのがよい、と記載されています。
利用しているサーバーのメモリ量も把握する必要があります。
スレッドごとにRailsアプリケーションの処理が実行されるので、スレッド数が増えればそれだけ必要なメモリ量も大きくなります。また、プロセス数が増えれば必然的に包含するスレッドも最低1つは増えますので、プロセス数を増やす場合も、必要なメモリ量は大きくなります。
利用しているサーバーのメモリ量は、プロセス数やスレッド数の上限を考えるのに必要です。
1スレッドで必要なメモリ量も把握する必要があります。
1スレッドで必要なメモリ量が例えば、400MB前後だとすると、スレッド数を1、2…と増やしていった場合に、必要なメモリは概ね400MB、800MB…と比例して増えていきます。
1スレッドで必要なメモリ量は、アプリケーションの実装によって幅が出てきます。筆者の経験では、Railsアプリケーションの本番環境では、概ね300MB〜1GBくらいの幅で変わるように思います。
もしすでに本番環境やステージング環境を運用しているのであれば、その環境でメトリクス計測ツール(New RelicやHerokuのMetrix機能)で確認するのが確実です。
稼働している本番環境やステージング環境で、実際に合計何スレッドが動作しているのかが分かれば、本番環境で利用しているメモリ量をその合計スレッド数で割れば、1スレッドで必要なメモリの量を概算することができます(厳密には、スレッド間で共有しているメモリがあるはずです。ここで計算しているのは、あくまで概算値です)。
Railsには、コネクションプールの仕組みがあり、以下のように、database.yml
のpool
の値で上限値を指定することができます。
default: &default
adapter: postgresql
host: localhost
pool: 5
timeout: 5000
username: postgres
password: postgres
encoding: utf8
development:
<<: *default
database: sample_app_development
test:
<<: *default
database: sample_app_test
コネクションプールとは、Railsの処理がデータベースにアクセスするたびにコネクション接続と切断を行って負荷が高くなったり、パフォーマンスが低下するのを防ぐために、予め決められた上限数を考慮してデータベースとの間に作っておく接続のグループのことです。
また、このコネクションプールは、各プロセスごとに作られます。プロセス間で共有のプールを持つことはできません。
コネクションは、スレッドの処理でデータベースへの接続が必要になった場合に、コネクションプールからスレッドに1つ割り当てられ、処理が終わるとプールに戻されます。
コネクションプールの値がスレッド数より少ない場合、プール数よりも多いデータベースへのアクセスリクエストが発生した際に、新しいスレッドにデータベースとの接続を割り当てることができなくなり、コネクションの割当待ちのような状態になります。
したがって、この割当待ちによるスループットの低下を防ぎたい場合は、
のような関係を保つように、database.ymlを設定する必要があります。
Herokuの記事では、コネクションプール数とスレッド数を同じ値にすることを推奨しているようです。実際に、これらの値が同じであっても上記の条件は満たされますし、スレッド数よりも過剰に大きいコネクションプール数を作成したとしても、その分サーバー上のメモリが無駄になるため、基本的にはHerokuの推奨のやり方に従うのが良いでしょう。
一般的なRailsアプリケーションにおいて、データベースに接続するのは、pumaやunicornのようなアプリケーションサーバーからだけではなく、sidekiqのようなjobワーカーも存在します。
次節で説明するように、データベース側に接続上限数が存在するため、jobワーカーが存在するか、そして、それが存在する場合どれくらいの数があり、合計何スレッドくらい実行されるか、も考慮にいれる必要があります。
データベース側の同時接続数の上限値も把握しておく必要があります。
データベース側の同時接続数が分かれば、全サーバーの全プロセスで作られるスレッド数の合計値が、その同時接続数を超えないようにパラメータを調整しなければなりません。
したがって、結果的に、データベースの同時接続数は、各サーバーのプロセス数やスレッド数の上限値に影響します。
これまでの説明を考慮すると、メモリに関するパラメータを除外すれば、少なくとも以下のような大小関係がなりたつように、各パラメータを設定する必要があります。
ただし、この大小関係を満たすという条件は変わりませんが、実際は、次の節で考慮に入れる利用可能なメモリ量で頭打ちになることが多いように思います。
上記の大小関係とは別に、メモリによってもパラメータの大小関係が決まってきます。
これは、単純に、以下になります。
最後に、当社で良く利用するHeroku環境の例をもとに、具体的な設定例を考えてみます。
以下のような架空の本番環境を想定することにします。
項目
値
CPU数(web dynoで使用できるvCPUの数)
今回の想定では、web dynoはpremium Mとする。
2
利用可能なメモリの量(premium Mで利用可能なメモリ量)
2.5GB
1スレッドで必要なメモリの量(本記事用に、適当に想定)
600MB
Railsで設定しているコネクションプールの数(スレッド数と同じ値を設定)
あとで計算
アプリケーションサーバーとjobワーカー数 (説明を簡単にするため、アプリケーションサーバー数(web dynoの数)を2、jobワーカー数を0と想定)
2
データベースの接続上限数(Heroku Postgresのmax connectionsの数)
400
CPU数は2なので、プロセス数の適切な値は2です。
ただし、メモリ関連のパラメータの大小関係を考慮すると、1スレッドで必要なメモリ量が600MBなので、合計スレッド数 × 600 <= 2500MB
となるように、合計スレッド数を4にすると良さそうです。
したがって、仮にプロセス数を2とすると、設定可能なスレッド数は2です。もし、プロセス数を1とすると、スレッド数は4です。
おそらく、後者の方がメモリの利用効率は高そうですが、GVLを考慮すると、アプリケーションによっては前者の方がスループットが大きい可能性もあります。これは、実際にステージング環境などで、どちらが良さそうかを確認すると良いかもしれません。
ここでは、仮に、プロセス数を2、スレッド数を2にすると決めたことにします。また、これに合わせて、コネクションプール数は2にします。
web dynoは、2つ使用するので、各web dynoごとに2プロセス、2スレッド作る場合、プロセス数は、合計で2 x 2= 4だけ作成することになります。
さて、この前提で、合計のデータベース接続数を念の為に計算すると、
となるため、データベースの接続上限数の400には、まだまだ余裕があり、問題ありません(この前提の場合、もっとスペックの低いデータベースを使用してもいいのかもしれません)。
pumaを使用する想定の場合、デフォルトでは、/config/puma.rbに以下のような起動設定が用意されています。
なお、実際には、説明用のコメントもつけられているはずですが、コードが長くなるので、以下では割愛しています。
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
preload_app!
plugin :tmp_restart
rackup DefaultRackup
この設定ファイルの
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
の部分がスレッド数を指定している部分で、
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
の部分がプロセス数を指定している部分です。
したがって、具体的なパラメータとしては、RAILS_MAX_THREADS
を2に、WEB_CONCURRENCY
を2に設定することになります。設定しない場合、このコードの例では、デフォルト値としてスレッド数5とプロセス数2が使われるので、サーバーのスペックが不十分な場合は、障害が発生するかもしれません。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
vCPUの値は、を参照。
を参考に確認する。今回の想定はstandard-2とする
先日、Rails6.1系で動いていたシステムをRails7.0系にアップグレードしました。
そのシステムは
Rails製APIアプリケーション
コード量・利用者ともに小規模
テストコードが充実している
と比較的アップグレードしやすかったこともあり、無事に完了しました。
この記事ではアップグレードの際に調べたことをまとめておきます。
なお、作業はピクシブ株式会社さんの永久保存版Railsアップデートガイドを参考にしながら進めました。ありがとうございました。
Rails6.1から7.0へアップグレードを計画した段階で、Railsの公式BlogRails7.0系のリリースノートを読みました。
また、Railsガイドのアップグレードガイドの Rails 6.1からRails 7.0へのアップグレードました。
資料を読み終えたところで、対象のシステムは問題なさそうと判断し、準備を進めることにしました。
Rails7.0がリリースされた直後は、使っているgemがRails7.0対応していないものがありました。
そこで、定期的に各gemのリポジトリを見に行き、Rails7.0対応に関するissueやPull Requestを確認しました。
その後、使用しているすべてのgemがRails7.0対応がなされているのが確認できたため、具体的な作業に入りました。
今回は小規模だったこともあり、
bundle outdated
でバージョンアップできるgemを確認
Rails以外を bundle update --conservative <gem名>
でバージョンアップ
--conservative
オプションにより、指定したgemと指定したgemが直接依存しているgemのみバージョンアップするようになります (Bundlerの公式ドキュメント)
Railsを bundle update --conservative rails
でバージョンアップ
の順で作業をしました。
また、各gemをバージョンアップするごとに、テストを流してパスすることを確認した上でGitブランチにコミットしました。
次に、6.1系と7.0系間のRailsDiffを見て各種設定ファイルの差分を確認しました。
まず、削除された設定について見てみましたが、主に
Rails7から、Springがデフォルトに含まれなくなったため、Springに関する設定だった
Rails7で config/initializers/
以下が整理されたが、対象システムでは使っていない設定だった
ため、システムから削除しても問題なさそうでした。
次に、設定ファイルの変更で気になった点(後述)については調査を行いました。その結果、変更による影響がなさそうだったため、Rails7.0系での変更を各種設定ファイルに取り入れました。
なお、調査をする際はTechRachoさんの記事が参考になりました。ありがとうございました。
ここでは、今回気になった点とその対応について以下にまとめます。
RailsDiffでの差分を引用します。
class ApplicationRecord< ActiveRecord::Base
- self.abstract_class = true
+ primary_abstract_class
end
TechRachoさんの記事を読むと primary_abstract_class
の方が良さそうでしたので、差し替えました。
RailsDiffでの差分のうち、気になった点は以下でした。
+ config.server_timing = true
- config.assets.debug = true
- config.file_watcher = ActiveSupport::EventedFileUpdateChecker
TechRachoさんの記事を読むと、server timing ミドルウェアは便利そうでしたので追加することにしました。
一方、
config.assets.debug
は使っていないシステムだった
config.file_watcher
はDockerを使っていると影響ありそうな情報が散見されるものの、今回の環境では不要そうだった
ため、これらは設定ファイルから削除することにしました。
RailsDiffの差分のうち、気になった点は以下でした。
- # Send deprecation notices to registered listeners.
- config.active_support.deprecation = :notify
-
- # Log disallowed deprecations.
- config.active_support.disallowed_deprecation = :log
-
- # Tell Active Support which deprecation messages to disallow.
- config.active_support.disallowed_deprecation_warnings = []
+ # Don't log any deprecations.
+ config.active_support.report_deprecations = false
config.active_support.report_deprecations = false
については、TechRachoさんの記事 によると一括でdeprecation warningを消せるオプションでした。 ただ、できる限りdeprecation warningは表示したいため、 true
にしておきました。
他の項目については、対象のシステムで無効にしていた設定だったため、対応は不要でした。
RailsDiffの差分のうち、気になった点は以下でした。
- # Do not eager load code on boot. This avoids loading your whole application
- # just for the purpose of running a single test. If you are using a tool that
- # preloads Rails for running tests, you may have to set it to true.
- config.eager_load = false
+ # Eager loading loads your whole application. When running a single test locally,
+ # this probably isn't necessary. It's a good idea to do in a continuous integration
+ # system, or in some way before deploying your code.
+ config.eager_load = ENV["CI"].present?
この変更を提供したところ、Github Actionsのデフォルトの環境変数 では CI = true
が設定されていたことから config.eager_load
が true
になってしまい、CI/CDまわりで不具合が出ました。
そのため、 config.eager_load
は従来のまま false
と明示的に設定しました。
RailsDiffで差分が出たデフォルト値に、 config/application.rb
の中のconfig.load_defaults
があり、値が 7.0
に変わっていました (該当箇所)。
そこで、Railsのデフォルト値の変更を調べることにしました。
まず、7.0
とした場合の影響については Railsガイドの load_defaults の結果 を参照しながら調査しました。
調査する中で、 rails7へのバージョンアップを安全に行うために使用するnew_framework_defaults_7_0.rbの各項目をさらっと解説 - Qiita に変更内容がまとまっていたため、参考にいたしました。ありがとうございます。
上記の記事にある項目を確認しましたが、今回のシステムに対して影響するものはありませんでした。
次に、さきほどのQiitaの記事で触れられていなかったものについて調査しました。それらを以下にまとめます。
Railsガイドには
ある種のRubyコアクラスに含まれる#to_sメソッドの上書きを無効にします。この設定は、アプリケーションでRuby 3.1の最適化をいち早く利用したい場合に使えます。
とあります。
今回は小規模なシステムだったこともあり影響がなかったため、デフォルト値の変更を受け入れました。
edgeのRailsガイド に詳細な記載がありました。
TechRachoさんの記事 を読み、今回のシステムには影響しなかったため、デフォルト値の変更を受け入れました。
edgeのRailsガイドに詳細がありました。
In Rails 7.1 and beyond, Active Storage has_many_attached relationships will default to replacing the current collection instead of appending to it.
とのことですが、今回のシステムには影響しなかったため、デフォルト値の変更を受け入れました。
Rails7.0系にアップグレード後も開発・運用を継続していますが、今のところ大きな問題は発生していません。
今後もRailsのバージョンアップを継続して行うとともに、作業で気になったこと等はTechBookに記載していこうと思います。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式で発信しています。ご確認ください。
RESTfulなWebサービスを記述、生成、利用、可視化するためのインターフェースファイルの仕様です。 以前はSwaggerフレームワークの一部でしたが、2016年にOpenAPI Initiativeが統括する独立プロジェクトとなりました。 Swaggerや他のいくつかのツールは、インターフェースファイルを指定してコード、ドキュメント、テストケースを生成することが可能です。
OpenAPIの正確な仕様については、↓から確認できます。 https://swagger.io/specification/
プレセナの一部プロジェクトでは、API定義をOpenAPI仕様に従って記述するだけでなく、定義ファイル(openapi.yaml)からコードを自動生成して利用しています。
当初はAPI定義ファイルであるopenapi.yamlの1ファイルに全ての記述をしていました。 結果として、下記のような問題が発生しました。
ファイルサイズの増加(約2500行)
コンフリクトの頻発
編集すべき記述を素早く見つけられない
これらの問題に対処するためにopenapi.yamlの分割をおこなうことにしました。
openapi.yamlのファイルは大きく分けると下の様なブロックに分かれています。
バージョン情報といったメタ情報:info、serversなど
エンドポイントのURLやリクエスト/レスポンス情報:paths
再利用可能なオブジェクト情報:components
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
・・・略・・・
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
・・・略・・・
主に記述量が肥大化してしまうのは pathsとcomponentsであったため、これらを別ファイルに分割しました。
ファイルの分割は簡単です。
定義内の他コンポーネントを参照できるようにするフィールドである$ref
を相対パスで記述するだけです。
openapi.yaml
...(infoやserversは省略)...
paths:
/pet:
$ref: "./paths/pet.yaml"
/pet/{petId}/upload-image:
$ref: "./paths/upload-image.yaml"
/paths/pet.yaml
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
required: false
schema:
type: integer
format: int32
responses:
'200':
content:
application/json:
schema:
$ref: "../schemas/Pet" # $refの記述
default:
description: unexpected error
content:
application/json:
schema:
$ref: "../schemas/Error" # $refの記述
/schemas/Pet.yaml
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
openapi.yamlをルートファイルとして、同階層にpathとschemasのフォルダを作成しました。
.
├── openapi.yaml
├── paths # pathの定義ファイルを置く
│ ├── pet.yaml
│ ├── pet_find-by-status.yaml
│ ├── ...
│ └── pet_petId_upload-image.yaml
└── schemas # schemasの定義ファイルを置く
├── Pet.yaml
├── User.yaml
├── ...
└── Tag.yaml
paths配下のファイルの命名規則としては、シンプルにエンドポイントURLの「/」を「_」にするファイル名としました。
/paths/pet/find-by-status.yaml
のように階層を深くするアイデアもあったのですが、schemasへの参照を$ref
で記述する際、"../../schemas/Foo"
, "../../../schemas/Bar"
のように 「..」を書く回数の混乱が生じないように現在のような命名規則をとりました。
エンドポイントURLにidが含まれる場合(ex. /pet/{petId}/upload-image)、理想的にはpet_[petId]_upload-image.yaml
のようにしたかったのですが、「[]」を含むと自動生成コードで問題が生じたため、シンプルにpet_petId_upload-image.yaml
としました。
分割によって見通しがよく開発をすすめることができるようになりました。
Visual Studio Codeで開発しているなら、この拡張をいれることで$ref
の記述からファイル参照もできるのでおすすめです。
https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi
しばらくこれで開発を進めてみて、課題感がまた出てきたらブラッシュアップしていきたいと思います。
当社のRailsシステム間連携では、各システムで公開しているWeb APIを使っています。
今までは各システムを bin/rails s
で起動し、開発を行ってきました。
ただ、 連携するシステムが増えたり、各システムで使うジョブワーカーが増えたりした結果、現在では各システムを起動する手間が増えてきました。
そこで、今後も効率的に開発できるよう、以下の設定を行いました。
tmux
+ overmind
にて、連携する各システムやワーカーを1つのコマンドで起動できるようにした
RubyMine
にて、 overmind
で起動したプロセスにアタッチし、デバッグできるようにした
この記事では、複数システムを1コマンドで起動できるようにするために、 tmux
+ overmind
+ RubyMine
にてどのような設定をしたか、チュートリアル形式で共有します。
このチュートリアルでは、以下のシステム構成とします。
mac上で、2つのRailsシステム(frontend_app
とbackend_app
)を開発している
frontend_app
について
外部からのHTTPリクエストを受け付ける
Delayed::Job
でジョブを管理している
bin/rails jobs:work
にて Delayed::Job Worker を起動する
各Railsシステムは、ローカルマシン上での bin/rails s
実行により起動する
各Railsシステムは、データベースを適切に設定している
次の図のように、overmind
ディレクトリの中に frontend_app
と backend_app
という2つのRailsシステムのリポジトリがあるものとします。
terminal multiplexer
と呼ばれるソフトウェアのうちの1つです。
1つのターミナルの画面を、複数に分割して利用します。
Herokuで使う Procfile
と同じ書式で定義することで、定義したプロセスを管理できます。
tmux
はデフォルト設定のままでも問題なく使えます。
ただ、慣れないうちはマウス操作はできたほうが便利なため、 tmux
の設定を追加します。
~/.tmux.conf
ファイルを追加し、以下を記載します。
チュートリアルのルートディレクトリである overmind
に、ファイル Procfile
を作成します。
Procfile
には、各Railsシステムやジョブワーカーを起動する時のコマンドを記載します。
なお、ワーキングディレクトリを考慮するため、 &&
を使ってコマンドをチェーンしています。
macのターミナルから tmux
を起動します。
次に、 Procfile
のあるディレクトリに移動し、overmind
にて各プロセスを起動します。
tmux
の画面では、 Procfile
で定義した各プロセスの様子が表示されています。
外部からのHTTPリクエストを受け付ける frontend_app
に対し、 curl
でアクセスします。
すると、 frontend_app
からJSONレスポンスが返ってきます。
tmux
を見ると、各システムやジョブワーカーが連携し、JSONレスポンスを返したことが分かります。
動作確認ができたため、いったん Ctrl + C
にて overmind
での実行を停止します。
現在はtmuxの1つのペインに、 Procfile
で起動したすべてのプロセスのログが表示されています。
ただ、この状態のままでは各プロセスのログを追いづらいです。
そこで、別ペインで表示するよう設定します。
このチュートリアルでは、ウィンドウを上下ペインに分けます。
上ペインはここまで通り overmind
のログを表示します。
一方、下ペインでは backend_app
のログのみを表示するようにします。
以下の準備を行います。
tmuxで Ctrl + b
+ "
を入力し、水平ペインを開く
上ペインにて、以下の操作を行う
overmind s
を実行し、各システム・ワーカーを起動する
下ペインにて、以下の操作を行う
Procfile
のあるディレクトリに移動する
overmind connect backend_app
を実行し、overmindで実行している backend_app
のプロセスに接続する
再びcurlで frontend_app
にアクセスしてみます。
すると、上ペインでは、各システム・ワーカーのログが出力されています。
一方、矢印部分の下ペインでは、 backend_app
のログのみ表示されています。
今までの操作にて、各システム・ワーカーを overmind s
だけで起動できるようになりました。
ただ、何か不具合があった時には、各システムをデバッグしたくなるかもしれません。
もしRubyMineを使っている場合は、 overmind
で起動したプロセスにアタッチ・デバッグできます。
このチュートリアルでは、RubyMineを使って backend_app
のプロセスにアタッチしてみます。
以下の順番で設定を行います。
RubyMineにて、プロセスにアタッチしたいシステムのリポジトリを開く
このチュートリアルでは backend_app
リポジトリを開きます。
RubyMineのメニューにて、 Run > Attach to Process
を選択する
実行しているプロセスが表示されるため、 backend_app
のプロセスを選択する
下部のイメージ参照
RubyMineでブレークポイントを設定する
以上で、デバッグの準備が整いました。
curlで frontend_app
にアクセスしてみます。
すると、RubyMineで設定したブレークポイントで停止します。
実行時の各変数の内容も表示され、デバッグできていることが分かります。
tmux
+ overmind
を利用して、連携する一連のシステムやワーカーを起動できるようにしたことにより、より開発を効率的に行うことができるようになりました。
今後も開発を効率的に行う方法をTechBookにて共有していこうと思います。
Railsには、スキーマファイルと呼ばれる schema.rb
があります。
Active Recordはマイグレーションの時系列に沿ってスキーマを更新する方法を知っているので、履歴のどの時点からでも最新バージョンのスキーマに更新できます。Active Recordは
db/schema.rb
ファイルを更新し、データベースの最新の構造と一致するようにします。
とあるように、schema.rb
はデータベース関連の機能で使われます。
その schema.rb
ですが、チームで開発をしている時に
自分の作業を中断して、プルリクエストのレビューをする
自分の作業に戻って、開発を続ける
を繰り返している中で、意図しない差分が schema.rb
に発生していると気づき、戸惑うことがあるかもしれません。
そこで、schema.rb
に差分が発生してしまうことについて
発生する事例
差分が発生していると何が起こるか
どう復旧するか
を記事としてまとめます。
発生する事例として、「どのような流れで発生してしまうのか」を体験できるチュートリアルを用意しました。
チュートリアルは以下の流れで進めます。なお、Railsのバージョンは 7.0.4.2
を想定しています。
mainブランチで、Railsのモデルを作成
mainから派生した第2のブランチ(feature/add_unique_index
)で、モデルの変更を伴うマイグレーションを適用
mainから派生した第3のブランチ(feature/add_column
)で、モデルの変更を伴うマイグレーションを適用
最小構成で rails new
します。後ほどgemを追加するため、現時点では bundle install
を行いません。
今回の動作確認をRSpecで行うため、 Gemfile
の末尾に追加します。
Gemfileの準備ができたため、一連のgemをインストールします。
RSpecのセットアップも行います。
続いてモデルを用意します。今回は isbn
列を持つBookモデルとします。
マイグレーション適用後に、状態を確認しておきます。
ここまでをコミットします。
(もし vendor/bundle
以下をコミットしたくない場合は .gitignore
に追加しておきます)
feature/add_unique_index
)で、モデルの変更を伴うマイグレーションを適用main
ブランチから第2のブランチ( feature/add_unique_index
)を新しく作成し、「Bookの isbn
にUNIQUE制約を追加する」ためのマイグレーションファイルを生成します。
生成したマイグレーションファイルにて、Bookの isbn
にUNIQUE制約を追加します。
マイグレーションを適用後、状況を確認します。
作業が終わったため、コミットします。
feature/add_column
)で、モデルの変更を伴うマイグレーションを適用ここからが、意図しない差分を発生させてしまう手順になります。
本来ならば、第2のブランチ(feature/add_unique_index
)を離れる前にマイグレーションをロールバックします。
ただ、今回は schema.rb
に差分を発生させるため、マイグレーションを適用したまま、
第3のブランチ(feature/add_column
)を新しく作成・切り替え
モデルに列 name
を追加
マイグレーションを適用
を行います。
続いて、マイグレーションの状態を確認します。作業2.のマイグレーションが適用されたまま、今回のマイグレーションも適用されているのが分かります。
schema.rb
を見ると isbn
列にUNIQUE制約が定義されており、意図しない状態となっています。
最後に、「この状態に気づかずに、うっかりコミット」した想定にしておきます。
今回はテストコードを使って、何が起こるかを見ていきます。
「第3のブランチ(feature/add_column
)ではまだUNIQUE制約を付けるマイグレーションがないから、テストはパスするだろう」と考え、以下のテストコードを書いたとします。
しかし、RSpecを実行すると
expected no Exception, got #<ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: books.isbn> with backtrace:
とテストが失敗してしまいます。
今回の場合、UNIQUE制約を追加するマイグレーションファイルが存在しない第3のブランチ(feature/add_column
)でも、UNIQUE制約付きでテスト用のデータベースが作成されてしまった結果、テストが失敗しました。
今回は、以下の流れで復旧していきます。
UNIQUE制約を追加した第2のブランチ(feature/add_unique_index
)へ切り替え、UNIQUE制約のマイグレーション適用をロールバックする
第3のブランチ(feature/add_column
)へ切り替え、 db:migrate
を実行する
順に見ていきます。
feature/add_unique_index
)へ切り替え、UNIQUE制約のマイグレーション適用をロールバックするUNIQUE制約を追加した第2のブランチ( feature/add_unique_index
)へ切り替え、マイグレーションの状況を確認します。
Migration IDに対するNameのうち、NO FILE
と表示されていたものが Add index to book
という表示へと切り替わりました。
続いて、第2のブランチ( feature/add_unique_index
)上で、不要なマイグレーション適用を元に戻します。
なお、 db:migrate:rollback
コマンドでは1ステップずつしか戻せないので、バージョンを指定できるコマンドで戻します。
再度マイグレーションの状況を確認すると、当該Migration IDが down
という未適用状態になりました。
ただ、この時点では schema.rb
に変更が入っていることから、第3のブランチ(feature/add_column
)へ切り替えようとしても
と、エラーになってしまいます。
そこで、 schema.rb
の変更を、当ブランチのコミット時点に戻しておきます。
feature/add_column
)へ切り替え、 db:migrate
を実行するあらためて第3のブランチ(feature/add_column
)へ切り替えます。
ここでマイグレーション適用状況を確認すると、第3のブランチ(feature/add_column
)のマイグレーションのみが適用・表示されています。
ただ、まだ schema.rb
にはUNIQUE制約の定義が残ったままになっています。
そこで、もう一回 db:migrate
を実行し、 schema.rb
を更新します。
ターミナルの実行結果には何も表示されないものの、git status
ではschema.rb
の更新が確認できます。
更新後の schema.rb
を見ると、UNIQUE制約の定義が削除されています。
これでようやく schema.rb
が復旧しました。
あとは忘れずに schema.rb
を第3のブランチ(feature/add_column
)へコミットすれば、復旧は完了です。
今回は schema.rb
に差分が発生する事例とその復旧方法を見てきました。
復旧するのには手間がかかることから、もしプルリクエストにマイグレーションファイルが含まれている場合は、schema.rb
に余分な更新がないかチェックするのが良さそうです。
当社では、社内で共通に使いたい機能をgemに切り出し、機能の利用側のGemfileでプライベートリポジトリを参照しています。
ローカル端末でのみ利用する場合はgitのURLはgit@ やssh@ で始まるURLを使えば問題なくbundle installできます。しかしCI/CD環境でもbundle installするため、httpsで始まるURLで登録しています。
このプライベートリポジトリをbundle install時に参照する方法について記載します。
まずCLIをインストールしておきます。Macの場合はbrewコマンドでインストールできます。
次に以下のコマンドを実行するとブラウザが開きますので、Githubの認可を行います。
これで、bundle installが成功します。
そうすると、sshで参照するようになるためbundle installが成功します。
github actionsやAWS codebuildなどのCI/CD環境について記載します。
パーミッションについては、プライベートリポジトリを参照してbundle installするだけであれば、「Contents:Read-only」を選択するだけで良いでしょう。
作成後にPrivate Keyを作れるようになりますので、ひとつ作成して秘密鍵をダウンロードしておきます。
画面上部に表示されているAppIDを控えます
Github App 左メニューのInstall Appを選択し、歯車アイコンをクリックします。
必要なリポジトリを選択し、Saveします。
控えておいたGITHUB_APP_IDおよびGITHUB_APP_PRIVATE_KEYを、下図のようにActionのsecretsに、登録しておきます。
Github App経由でtokenを取得します。その値を、環境変数BUNDLE_GITHUB__COM
に設定します。以下にGithub Actionsの設定例を掲載します。
Github Actionsとやっていることは同じです。Github Appsを用意し、GITHUB_APP_IDとGITHUB_APP_PRIVATE_KEYを使って、Access Tokenを取得します。ただしgithub actionsのように公開された再利用可能ワークフローがないため、自前でスクリプトを実行してtokenを取得します。
以下が、buildspec.ymlから呼ぶスクリプトです。
過去に書いたソースコードを読んでいて、仕様を理解するのに手間取ってハマったので、共有のために記事を書いておきます。
以下のようなコードを見かけたとします。
上のコードから、どのような仕様を想像するでしょうか?
筆者は、ユーザーが見つかった場合、または、初期化した場合、どちらもlast_name
、first_name
がそれぞれ、サンプル
と 太郎
に初期化されると思ってしまいました。
しかし、実際の挙動としては、以下でした。
ユーザーが見つかった場合は、ブロック内の処理は実行されず、
ユーザーをinitializeした場合は、ブロック内の処理が実行される。
コードを見ても、上記の挙動が実装されているのが確認でき、find_by
で該当レコードが見つかった場合には、&block
部分が無視されるのが分かります。
Procfile
ベースのプロセスマネージャーです ()。
参考:
に従い、 tmux
と overmind
をインストールします。
参考:
参考:
参考:
本サイトの更新情報は、で発信しています。ご確認ください。
には
これは、で解説があるように、テストでは schema.rb
を見てテスト用のデータベースを作っているためです。
本サイトの更新情報は、Twitterので発信しています。ご確認ください。
に記載があるように、BUNDLE_GITBHUB__COM の環境変数にgithubのPersonal Access Token(PAT)を登録することでbundle install時にプライベートリポジトリを参照する方法があります。しかし、GithubはPATの利用を推奨していません。に非推奨の「GitHub recommends that you use fine-grained personal access tokens instead」といった言及がされています。したがって、当記事ではPAT以外を利用した方法について記載します。
以下のコマンドを実行します。
まず、 から、Github Appsを作ります。組織内でのみ利用したいため、「Where can this GitHub App be installed?」の項目は「Only on this account」にチェックしておきます。
GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEYに加え、GITHUB_APP_INSTALLATION_IDをActionのsecretsに登録しておきます。installation idは各リポジトリのSettingsメニューの下部Github Appsを選択し、Github Appsの一覧のConfigureボタンを押した先のURLに含まれています。<installation_id> の形式です。このinstallation_idを控えてください。
get_github_token.jsの内容は以下のとおりです。クラスメソッドさんのより流用、改変しています。
本サイトの更新情報は、で発信しています。ご確認ください。
実際に、を見てみると、以下のようになっています。
本サイトの更新情報は、Twitterので発信しています。ご確認ください。
overmind/
├── backend_app/
│ ├── app/
│ ├── bin/
│ ...
└── frontend_app/
├── app/
├── bin/
...
% brew install tmux
% brew install overmind
set-option -g mouse on
# frontendの設定
frontend_app: cd frontend_app && bin/rails s -b 0.0.0.0 -p 3030
frontend_worker: cd frontend_app && bin/rails jobs:work
# backendの設定
backend_app: cd backend_app && bin/rails s -b 0.0.0.0 -p 3031
% tmux
% overmind s
% curl http://localhost:3030/shops
{"shop":{"name":"スーパーマーケット","apples":[{"name":"シナノゴールド"},{"name":"シナノスイート"},{"name":"秋映"}]}}
% bundle exec rails new schemaapp --minimal --skip-bundle
% cd schemaapp
group :test do
gem 'rspec-rails', '~> 6.0.1'
end
% bundle install
% bin/rails g rspec:install
% bin/rails g model Book isbn:string
% bin/rails db:migrate
% bin/rails db:migrate:status
database: db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20230306095752 Create books
% git init
% git add .
% git commit -m "first commit"
% git checkout -b feature/add_unique_index
% bin/rails g migration AddIndexToBook
class AddIndexToBook < ActiveRecord::Migration[7.0]
def change
add_index :books, :isbn, unique: true
end
end
% bin/rails db:migrate
% bin/rails db:migrate:status
database: db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20230306095752 Create books
up 20230306104025 Add index to book
% git add .
% git commit -m "add index"
% git checkout main
% git checkout -b feature/add_column
% bin/rails g migration AddColumnToBook name:string
% bin/rails db:migrate
% bin/rails db:migrate:status
database: db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20230306095752 Create books
up 20230306104025 ********** NO FILE **********
up 20230306104231 Add column to book
ActiveRecord::Schema[7.0].define(version: 2023_03_06_104231) do
create_table "books", force: :cascade do |t|
t.string "isbn"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.index ["isbn"], name: "index_books_on_isbn", unique: true
end
end
% git add .
% git commit -m "add column"
require 'rails_helper'
RSpec.describe Book, type: :model do
describe 'schema.rbの確認' do
before { Book.create!(isbn: '4797399848') }
it 'UNIQUE制約がないこと' do
expect { Book.create!(isbn: '4797399848') }.not_to raise_error
end
end
end
% git checkout feature/add_unique_index
% bin/rails db:migrate:status
database: db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20230306095752 Create books
up 20230306104025 Add index to book
up 20230306104231 ********** NO FILE **********
% bin/rails db:migrate:down VERSION=20230306104025
% bin/rails db:migrate:status
database: db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20230306095752 Create books
down 20230306104025 Add index to book
up 20230306104231 ********** NO FILE **********
% git checkout feature/add_column
error: Your local changes to the following files would be overwritten by checkout:
db/schema.rb
Please commit your changes or stash them before you switch branches.
Aborting
% git reset HEAD db/schema.rb
% git checkout db/schema.rb
% git checkout feature/add_column
% bin/rails db:migrate:status
database: db/development.sqlite3
Status Migration ID Migration Name
--------------------------------------------------
up 20230306095752 Create books
up 20230306104231 Add column to book
% bin/rails db:migrate
% git status
On branch feature/add_column
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: db/schema.rb
ActiveRecord::Schema[7.0].define(version: 2023_03_06_104231) do
create_table "books", force: :cascade do |t|
t.string "isbn"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
end
end
gem "some_internal_library", git: "https://github.com/precena-dev/some_internal_library.git", tag: "v1.0.0"
% brew install gh
% gh auth login
% git config url.git@github.com:.insteadOf https://github.com/
steps:
- name: Generate github token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
・・・中略・・・
- name: Set up Ruby
env:
BUNDLE_GITHUB__COM: x-access-token:${{ steps.generate_token.outputs.token }}
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
# change this to (see https://github.com/ruby/setup-ruby#versioning):
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
# buildspec.ymlから呼ぶscript.
# github app経由でtokenを取得する
npm install axios jsonwebtoken
node ./get_github_token.js > $BUNDLE_GITHUB__COM
// github app経由でtokenを取得するスクリプト
// 成果物のtokenは標準出力に出す
// https://dev.classmethod.jp/articles/register-github-app-and-get-access-token/ を改変
const jwt = require("jsonwebtoken")
const axios = require("axios")
const githubAppId = process.env.GITHUB_APP_ID;
const githubAppPrivateKey = process.env.GITHUB_APP_PRIVATE_KEY;
const githubAppInstallationId = process.env.GITHUB_APP_INSTALLATION_ID;
const payload = {
exp: Math.floor(Date.now() / 1000) + 60, // JWT expiration time
// ちょっとだけ時間を手前にしておくとアクセストークンの発行に失敗し辛いらしい。
// https://qiita.com/icoxfog417/items/fe411b94b8e7ae229e3e#github-apps%E3%81%AE%E8%AA%8D%E8%A8%BC
iat: Math.floor(Date.now() / 1000) - 10, // Issued at time
iss: githubAppId
}
const cert = githubAppPrivateKey;
const token = jwt.sign(payload, cert, { algorithm: 'RS256'});
axios.default.post(`https://api.github.com/app/installations/${githubAppInstallationId}/access_tokens`, null, {
headers: {
Authorization: "Bearer " + token,
Accept: "application/vnd.github.machine-man-preview+json"
}
})
.then(res => {
// 標準出力に出たものをシェルスクリプトでリダイレクトして使う想定
console.log(`x-access-token:${res.data.token}`);
})
.catch(res => {
console.error('error');
console.error(res);
throw new Error(res.data);
})
user = User.find_or_initialize_by(email: 'sample@precena.com') do |user|
user.last_name = 'サンプル'
user.first_name = '太郎'
end
def find_or_initialize_by(attributes, &block)
find_by(attributes) || new(attributes, &block)
end
Rails歴が長い人でも、意外とmigrationの追加用のコマンドを覚えていられず、毎回調べていているので、実装で使ったもの・使いそうなものを少しずつ追加しています。
この記事は定期的に内容が追加される予定です。
以下のように、マイグレーションの名前を指定しながらrails generate
コマンドを入力します。
% rails g migration AddXXXXsToSomeRecords
以下のようにコマンドでカラムと型を指定する(YYY
はテーブル名)か、
$ rails g migration AddXXXXToYYY カラム名:データ型
あるいは、空のマイグレーションファイルを作った後、次のように、直接、change
メソッド内にadd_column
メソッドを記載します。
class AddXXXXsToSomeRecords < ActiveRecord::Migration
def change
add_column :some_records, :column_name, :string
end
end
some_master_records
というテーブルがある前提で、別のsome_transaction_records
テーブルにdefault_some_master_record_id
というカラムを追加して、そのカラムを使ってsome_master_records
テーブルを参照したい場合に使います。
やり方はいくつかあると思いますが、筆者が使うのは、以下です。
その後、以下のようにadd_reference
メソッドにforeign_key
オプションを指定し、そのオプションの中でto_table
を指定します。
class AddXXXXsToSomeTransactionRecords < ActiveRecord::Migration
def change
add_reference :some_transaction_records, #カラムを追加したいテーブル
:default_some_master_record, #これでdefault_some_master_record_idカラムが作られる
{
foreign_key: {to_table: :some_master_records}
}
end
end
これで、some_transaction_records
テーブルに、default_some_master_record_id
カラムが追加され、some_master_records
テーブルへの外部キー制約も作られます。
なお、この場合、ActiveRecordのモデルクラス(SomeTransactionRecord
クラス)にも参照に使う外部キーと参照先のモデルの設定が必要になります(この記事での説明は省略します)。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式で発信しています。ご確認ください。
直感とは異なる挙動が観察されたため、記事として残します
RDB
PostgreSQL 13.3
トランザクション分離レベル read committed (デフォルト)
実験環境
IntelliJ IDEA 2024.3 (Ultimate Edition)
PostgreSQLにおいて、トランザクション内で同一キーの行を delete - insert する場合、同時に実行される他のトランザクションからは当該の行が参照できない場合があります。
次のようなSQLを考えます。
begin;
-- delete-insertによってデータを更新する
delete from users where id = 1; -- id が p_key
insert into users (id, name) values (1, 'jane doe');
commit;
実験のためIntellijを使い、2つのセッション分のクエリコンソールを開きます。 これら2つをそれぞれセッション1、セッション2とします。
セッション1で5行目まで実行する。この時 delete によってロックが獲得される
セッション2で4行目まで実行する。するとセッション1で行がロックされているため、待ち状態になる
セッション1で7行目の commit まで実行する。これによりセッション1で獲得されていたロックが解放される
セッション1のロックが解放されたことでセッション2の delete が実行される
この時、4 の結果セッション2では delete が空振りし、insert を実行するもすでに id = 1 の行が存在するため、
[23505] ERROR: duplicate key value violates unique constraint "users_pkey"
となります。
MySQL 8.0.28 で同様の実験を行ったところ、エラーは発生しませんでした。データの更新結果はcommitを後に実行している、セッション2の結果が保存されます。 MySQLのトランザクション分離レベルは repeatable read(デフォルト) です。
ひょっとしたら分離レベルに依存した挙動なのでは?ということでPostgreSQLにて分離レベルをrepeatable readとして実験を行いました。 結果前述4のステップで、以下のようなエラーが返されました。
[40001] ERROR: could not serialize access due to concurrent update
これはPostgreSQLの仕様によるもので、 リピータブルリードトランザクションでは、トランザクションが開始された後に別のトランザクションによって更新されたデータは変更またはロックすることができないため とあります。 https://www.postgresql.jp/docs/9.4/transaction-iso.html
MySQLではrepeatable readの場合と挙動は変わらず、1行返却されました。
PostgreSQLにおいて、delete 文ではなくselect for updateでロックを獲得した場合の挙動についても確認しました。
begin;
-- for update により、行ロックを取得する
select * from test where id = 1 for update;
-- delete-insertによってデータを更新する
delete from test where id = 1;
insert into test (id, name) values (1, 'jane doe');
commit;
この場合、セッション2は select for update の行で待ち状態となり、セッション1のcommit後、セッション2の select for update が実行され 0行 の返却となりました。
今回、別の問題を調査していく中でたまたまこの現象に遭遇しました。 個人的には全く想定していない挙動でした。
PostgreSQLでは削除データはすぐには物理削除されませんが、この辺りが関係しているような気もします。 https://www.postgresql.jp/docs/9.4/sql-vacuum.html
改めてこちらの記事はPostgreSQLでの挙動になります。 前述の通りMySQLでは異なる挙動となりましたので、RDBMSの実装に依存するようです。
ご興味があれば、ぜひお手持ちの環境でも試してみてください。
userの情報を返すAPIを実装する際、render json: user
とするとuserモデルのすべてのフィールドを含むJSONを返してしまい危険です。パスワードはハッシュ化されているものの、deviseが提供するフィールドlast_sign_in_ip
などクライアントに返してはならない個人情報が含まれており、情報漏えいにつながってしまうためです。こちらの記事も参考にしてください。
上述のような危険な実装をコードレビューのみに頼らずに、Rubyの静的コード解析ツールであるRuboCopで機械的にチェックする方法を、当ページでは記載します。
基本的には公式ドキュメントの手順通りです。
rubocop gemはインストール済みの前提で記載します。
以下の4行目をエラーとすることを目標にします。
# some_controller.rb
class SomeController < ApplicationController
def some_method
user = User.first
render json: user, status: 200 # これを検知したい。statusパラメータは省略可能
end
end
rubocop gemを入れていれば使えるruby-parse
というコマンドでASTを出力します。
$ ruby-parse some_controller.rb
(class
(const nil :SomeController)
(const nil :ApplicationController)
(def :some_method
(args)
(begin
(lvasgn :user
(send
(const nil :User) :first))
(send nil :render
(kwargs
(pair
(sym :json)
(lvar :user))
(pair
(sym :status)
(int 200)))))))
検知したいのは、この
(send nil :render
(kwargs
(pair
(sym :json)
(lvar :user))
(pair
(sym :status)
(int 200))))))
の部分です。また、renderメソッドの第二引数のstatusは省略可能な引数ですので、第二引数の有無にかかわらず検知できるようにしたいです。
上記パターンにマッチするようにマッチャを記述します。_や...はワイルドカードです。詳細はドキュメントをご確認ください。
renderメソッドを呼び出していて
その引数にはjsonという名前付き引数を指定しており
第二引数以降は問わない
というマッチャを記述しlib/custom_cops/dangerous_render_json.rb
として配置します。
module CustomCops
class DangerousRenderJson < RuboCop::Cop::Cop
# キーワード引数jsonを第一引数にしているrenderメソッド
def_node_matcher :render_json_call?, <<~PATTERN
(send ... :render
(hash
(:pair
(:sym :json)
(_ ...)
)
...
)
)
PATTERN
MSG = 'Do not use render json.'
def on_send(node)
return unless node.method_name == :render
add_offense(node) if tojson_call?(node)
end
end
end
次に、.rubocop.yml
にて、今作ったカスタムルールを読み込む設定を追記します。
require:
- rubocop-rails
- rubocop-rspec
- ./lib/custom_cops/dangerous_render_json
rubocopコマンドを実行して動作確認をします。
うまくいけば、以下のように、エラーとして検知できます。
% bundle exec rubocop
・・・中略・・・
app/controllers/some_controller.rb:4:5: C: CustomCops/DangerousRenderJson: Do not use render json.
render json: user, status: 200 # これを検知したい。statusパラメータは省略可能
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
本サイトの更新情報は、X(旧Twitter)の株式会社プレセナ・ストラテジック・パートナーズエンジニア公式で発信しています。ご確認ください。
cats に含まれる Validated は、複数の入力値のバリデーションを一つにまとめて返すことができる大変便利な型です。
業務において、入力値のバリデーションを行った値をさらに別の入力値として使いたいというケースがままあり、プロジェクトの新規参入者が悩むことがあるため備忘録として記します。
以下のようなコードがあります。
case class Error(message: String)
case class Age(age: Int)
object Age {
def validate(age: Int): Either[Error, Age] =
Either.cond(age < 1000, Age(age), Error("そんなに生きられません"))
}
case class Child(age: Age)
object Child {
def validate(age: Age): Either[Error, Child] = // チェック済みの Age を使いたい
Either.cond(age.age < 18, Child(age), Error("もう大人です"))
}
バリデーション済みの Age
を使って Child
のバリデーションを行いたい。
次に記すように flatMap
を使って書けないものでしょうか。
import cats.syntax.all._
val uncompilableValidatedChild = for {
validatedAge <- Age.validate(15).toValidated // flatMap がないのでコンパイルエラー
validatedChild <- Child.validate(validatedAge).toValidated
} yield validatedChild
しかし Validated
には flatMap
がないのでコンパイルエラーになってしまいます。
その理由は Validated
は Monad を実装できず Applicative であるからで、公式ドキュメントに詳しくかかれていました。
https://typelevel.org/cats/datatypes/validated.html#of-flatmaps-and-eithers
andThen
メソッドを使うことで Validated
な値を直列に処理することができます。
val validatedAge = Age.validate(15).toValidatedNec
val validatedChild = validatedAge.andThen(age => Child.validate(age).toValidatedNec)
当然ですが、直列になるため Age
でバリデーションエラーになった場合には Child
のバリデーションは評価されません。 Age
も Child
も満たさない値 2000
で実行してみましょう。
scala> val validatedChild = Age.validate(2000).toValidatedNec.andThen(age => Child.validate(age).toValidatedNec)
val validatedChild: cats.data.Validated[cats.data.NonEmptyChain[example.Error],example.Child] = Invalid(Chain(Error(そんなに生きられません)))
Child
のバリデーションエラー Error("もう大人です")
は追加されていないことが確認できますね。
一つ前の例は入力が1つでしたが、今度は入力が2つ以上のケースです。
case class Age(age: Int)
object Age {
def validate(age: Int): Either[Error, Age] =
Either.cond(age < 1000, Age(age), Error("そんなに生きられません"))
}
case class Job(name: String)
object Job {
def validate(name: String): Either[Error, Job] =
Either.cond(name.nonEmpty, Job(name), Error("空文字はダメです"))
}
case class Adult(age: Age, job: Job)
object Adult {
def validate(age: Age, job: Job): Either[Error, Adult] = // 入力が2つ
Either.cond(18 <= age.age, Adult(age, job), Error("まだ子供です"))
}
普通に書くとネストしてしまいますが、先ほどと同様に flatMap
がないためこのように書くことはできません。
val validatedAge = Age.validate(2000).toValidatedNec
val validatedJob = Job.validate("").toValidatedNec
val nestedValidatedAdult: ValidatedNec[Error, ValidatedNec[Error, Adult]] =
(validatedAge, validatedJob).mapN((age, job) => Adult.validate(age, job).toValidatedNec)
val validatedAdult: ValidatedNec[Error, Adult] = nestedValidatedAdult.flatten // flatMap がないのでコンパイルエラー
withEither
メソッドを使うと、一旦 Either
に変換してから Validated
に戻せるため、 flatMap
を使ってネストを解消できます。
val validatedAdult = (validatedAge, validatedJob)
.mapN((age, job) => Adult.validate(age, job).toValidatedNec.toEither)
.withEither(_.flatten)
または
val validatedAdult = (validatedAge, validatedJob)
.mapN((age, job) => Adult.validate(age, job).toValidatedNec)
.withEither(_.flatMap(_.toEither))
実行してみましょう。両方のバリデーションエラーが合成できていることを確認できます。
scala> (validatedAge, validatedJob).mapN((age, job) => Adult.validate(age, job).toValidatedNec.toEither).withEither(_.flatten)
val res2: cats.data.Validated[cats.data.NonEmptyChain[example.Error],example.Adult] = Invalid(Chain(Error(そんなに生きられません), Error(空文字はダメです)))
andThen
を2回使うことでも一応可能ですが、本来並列にできるはずの Age
と Job
のバリデーションも直列になってしまい、 Age
でバリデーションエラーになると Job
が評価されなくなってしまいます。
val validatedAdult = validatedAge
.andThen(age => validatedJob
.andThen(job => Adult.validate(age, job).toValidatedNec))
実行すると、次のように Job
のバリデーションエラーは表示されず Age
のバリデーションエラーだけが表示されてしまいます。
scala> validatedAge.andThen(age => validatedJob.andThen(job => Adult.validate(age, job).toValidatedNec))
val res1: cats.data.Validated[cats.data.NonEmptyChain[example.Error],example.Adult] = Invalid(Chain(Error(そんなに生きられません)))
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
ActiveSupportには、to_json
という便利なメソッドがあります。
Railsで開発しているときに使う場面としては、DBから取得したレコードをAPIのレスポンスとして返す場合があります。
しかし、deviseを認証に使っているサービスなどで、何も考えずに、user.to_json
のようなコードを書いてしまうと、以下のようなJSONがレスポンスに返されてしまいます。
{
\"id\":1111111,
\"email\":\"xxxxx@yyyyy.zzzzz\",
\"created_at\":\"2023-09-06T14:29:22.188+09:00\",
\"updated_at\":\"2023-09-06T15:29:38.638+09:00\",
\"last_sign_in_user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36\"
}
出力される属性がこれだけなら、それほど大きな問題がないようにも見えます。しかし、userモデルには、サービスへの機能追加に伴ってプライベートな情報が追加されやすいため、そういった情報がto_json
メソッドで出力されてしまったり、あるいは、本人以外のuser情報も併せて一覧で取得するような場合に、他人の email
やプライベートな情報が含まれてしまったりすると個人情報の漏洩問題になる可能性があります。
したがって、to_json
メソッドで出力する属性を制限するべきかどうかについて、注意し検討する必要が出てきます。
幸い、to_json
メソッドでは、オプションを指定することで、出力を絞り込むことができます。
only
特定の属性のみに絞り込む場合に使います。
except
特定の属性を除外したい場合に使います。
include
特定のassociation(has_manyやbelongs_toで指定しているような別のモデル)を出力に含めたい場合に使います。
methods
特定のメソッドを呼び出した結果を含めたい場合に使います。
methodsオプションは、特定の属性を絞り込むというよりかは、追加で情報を出力する用途に使いますが、関連する機能なので併せて説明します。
以下、各オプションの使用例を示します。
さきほどの例で、only
を指定すると、以下のように指定した属性だけが出力されます。
> user.to_json(only: [:id])
=> "{\"id\":1111111}"
さきほどの例で、except
を指定すると、以下のように指定した属性以外のものが出力されます(※)。
※読みやすくするために、改行を入れています。
> user.to_json(except: [:email])
=>
{
\"id\":1111111,
\"created_at\":\"2023-09-06T14:29:22.188+09:00\",
\"updated_at\":\"2023-09-06T15:29:38.638+09:00\",
\"last_sign_in_user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36\"
}
さきほどの例で、include
を指定すると、以下のように指定したassociationの属性も併せて出力されます(※)。
※ some_associationという属性がある前提です。
> user.to_json(include: [:some_association])
=>
{
\"id\":1111111,
\"created_at\":\"2023-09-06T14:29:22.188+09:00\",
\"updated_at\":\"2023-09-06T15:29:38.638+09:00\",
\"last_sign_in_user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36\",
\"some_association\":{\"id\":222222,\"name\":\"名前1\"}
}
なお、includeで指定したassociation内でも出力する属性を制限したい場合は、以下のように書けます。
> user.to_json(include: [{some_association: {only: :name}}])
=>
{
\"id\":1111111,
\"email\":\"xxxxx@yyyyy.zzzzz\",
\"created_at\":\"2023-09-06T14:29:22.188+09:00\",
\"updated_at\":\"2023-09-06T15:29:38.638+09:00\",
\"last_sign_in_user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36\",
\"some_association\":{\"name\":\"名前1\"}
}
さきほどの例で、methods
を指定すると、以下のように指定したメソッドの呼び出し結果も併せて出力されます(※)。
※some_methodというメソッドがある前提です。
> user.to_json(methods: [:some_method])
=>
{
\"id\":1111111,
\"email\":\"xxxxx@yyyyy.zzzzz\",
\"created_at\":\"2023-09-06T14:29:22.188+09:00\",
\"updated_at\":\"2023-09-06T15:29:38.638+09:00\",
\"last_sign_in_user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36\",
\"some_method\": \"some_output\"}
}
本サイトの更新情報は、X(旧Twitter)の株式会社プレセナ・ストラテジック・パートナーズエンジニア公式で発信しています。ご確認ください。
overmind s
した時の様子backend_app
のログのみ表示されるAWS上で稼働するアプリが増えてくると、アプリごとにOrganizationを作りたくなります。この際、スイッチロールという機能を使うとOrganizationごとにIAMユーザーを作る必要がなくなり、ユーザー管理をシンプルにできます。本記事ではスイッチロールの方法について記載します。
以下、「スイッチ元Organization」および「スイッチ先Organization」のことを単に「スイッチ元」および「スイッチ先」と記載します。
管理者アカウントで、スイッチ先にログインします。
ロールを新規作成します。
「別のAWSアカウント」を選択し、アカウントIDにスイッチ元のAWSアカウントIDを入力します。「MFAが必要」にもチェックをつけた方が良いでしょう。
次に進み、スイッチ先で割り当てたい権限をつけます。この例では、Administrator権限を割り当てています。
最後にロール名をつけて、ロールの作成完了します。ロール名は「delegate_root_organization」といった分かりやすい名前が良いでしょう。
作成したロールのARNを控えておきます。
管理者アカウントで、スイッチ元にログインします。
ポリシーを新規作成します。Resourceには、スイッチ先で作成したロールのARNを入力します。
次へ進み、ポリシーの名前をつけて、ポリシーの作成を完了します。ポリシー名には「delegate_from_スイッチ先Organization名」といった分かりやすい名前が良いでしょう。また、複数のスイッチ先にスイッチできる権限を持つポリシーも作成できますが、スイッチ先ごとにポリシーを作成し各人が関わる必要最低限の権限を付与できるようにした方が良いでしょう。
スイッチ元にログインします。
スイッチしたいIAMユーザーに、「スイッチ元での設定」で作成したポリシーを追加します。
ではスイッチをしてみましょう。メニューから「ロールの切り替え」を選択します。
スイッチ先のアカウントID、ロール名、表示名を入力します。
「ロールの切り替え」ボタンをクリックすると、スイッチ先にログインしたのと同じ状態になります。
スイッチしていることは、右上のメニューで分かります。上部のバナー全体に色が変わるなどもう少し目立つと良いのですが。
スイッチ元に戻りたい場合は以下のようにメニューから選択します。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
当社の開発で使うRubyのバージョンは、プロジェクトごとに異なることが多く、また、プロジェクトごとにRubyのアップデート状況も異なります。
したがって、プロジェクトごとにRubyのバージョンを切り替える仕組みとしてrbenvを導入して使います。
直接rbenvをインストールするか、anyenv経由でインストールするかで手順が変わります。
anyenvがすでに導入されている場合、以下で簡単にrbenvをインストールできます。
まずは、homebrewでrbenvをインストール。このとき、ruby-buildも同時にインストールされます。
次に、
を実行して、出力された手順にしたがって、シェルの設定を行います。zshを使っている場合、以下のように出力されるので、この内容に従います。
つまり、.zshrcファイルに以下を追加します。
rbenvがインストールされたら、次は、使いたいバージョンのRubyをインストールします。
まずは、インストール可能なRubyの安定バージョンを確認します。
ここに表示されたバージョンを指定して、開発環境にインストールします。
プロジェクトで使うRubyのバージョンは、他の開発メンバーとも共有したいため、.ruby-version
ファイルを作って、他のメンバーにも同じRubyのバージョンの使用を強制できるようにします。
プロジェクトルートで、以下を実行すれば.ruby-version
ファイルがプロジェクトルート直下に作られます。
Rubyのバージョンを上げる手順もanyenvでrbenvをインストールしたかどうかで異なります。
rbenv install -l
コマンドで、インストールしたいRubyのバージョンが表示されていない場合、anyenv内部のruby-buildのアップデートが必要です。このアップデートのコマンドを楽にしてくれるanyenv-updateを使うのをおすすめします。
anyenv-updateが設定してあれば、以下を実行するだけで、インストール可能なRubyのバージョン情報が最新化されます。
このあとは、普通に特定のバージョンのRubyをインストールします。
同様に、インストールしたいRubyのバージョンが表示されていない場合、先に、rbenvとruby-buildをアップデートする必要があります。rbenvとruby-buildのアップデートにはhomebrewを使います。
これで、インストール可能なRubyのバージョンが最新化されるので、特定のバージョンをインストールします。
の手順に従います。
本サイトの更新情報は、Twitterので発信しています。ご確認ください。
% anyenv install rbenv
% brew install rbenv
% rbenv init
% rbenv init
# Load rbenv automatically by appending
# the following to ~/.zshrc:
eval "$(rbenv init -)"
# rbenvの初期化
eval "$(rbenv init -)"
% rbenv install -l
2.6.7
2.7.3
3.0.1
jruby-9.2.17.0
mruby-3.0.0
rbx-5.0
truffleruby-21.1.0
truffleruby+graalvm-21.1.0
Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all / -L' to show all local versions.
% rbenv install 2.7.3
% rbenv local 2.7.3
% anyenv update
% brew upgrade rbenv ruby-build
AWS CLIやterraformでスイッチロール時にMFAを利用する際、コマンドを実行する都度MFAコードを入力するのは煩雑です。AWS Vaultを使うとスイッチロール後のプロファイルのシェルに入れるようになります。
defaultプロファイルでIAM ユーザーによるログインをし、some-profile
というプロファイルに設定しているAWSアカウントにスイッチロールする、という構成を前提に、本記事は記載します。具体的には以下の設定を前提とします。
% cat ~/.aws/config
[default]
region = ap-northeast-1
output = json
cli_pager=cat
[profile some-profile]
source_profile = default
role_arn = arn:aws:iam::1234567890:role/some_role_for_delegation
mfa_serial = arn:aws:iam::987654321:mfa/someone@example.com
% cat ~/.aws/credentials
[default]
aws_access_key_id=AKIA.....
aws_secret_access_key=........
[some-profile]
source_profile = default
role_arn = arn:aws:iam::1234567890:role/some_role_for_delegation
mfa_serial = arn:aws:iam::987654321:mfa/someone@example.com
homebrewでコマンドをインストールします。
% brew install --cask aws-vault
インストールの確認を兼ねてコマンドを打ってみます。
% aws-vault list
Profile Credentials Sessions
======= =========== ========
default - -
some-profile - -
default profileにcredentialを追加します。以下のコマンドを実行すると、IAMキーとシークレットを求めるプロンプトが出るので、入力します。
% aws-vault add default
最後に、以下のコマンドを実行します。するとMFAのコードを入力を求めるプロンプトが出るので、入力します。
% aws-vault exec some-profile
Starting subshell /bin/zsh, use `exit` to exit the subshell
以上で、上記コマンドを実行したターミナルではsome-profileにスイッチロールした状態になります。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
弊社ではAWSリソースをTerraformやCloudFormationで管理し、plan/applyの実行にはGithub Actionsを使っています。本稿ではGithub ActionsからAWSリソースを操作するためのIAMロールの設定について説明します。
Github ActionsからAWSリソースを操作するために利用する認証方法には以下のふたつが挙げられます。
アクセスキーおよびシークレットキー
IAMロール
アクセスキーによる方法にはセキュリティリスクが伴います。アクセスキーとシークレットキーがありさえすればどこからでも使え、漏洩すると不正利用される可能性があるためです。さらに、TerraformやCloudFormationによる処理に割り当てられるIAMポリシーは広大になりがちなので、漏洩した時の被害が甚大になる可能性があります。
一方、IAMロールを利用する方法では、一時的な認証情報(STSトークン)を発行することで、安全かつ柔軟にAWSリソースへアクセスできます。特に、GitHub ActionsのようなCI/CD環境では、OIDC(OpenID Connect)を活用することで、アクセスキーを不要にし、より安全な認証フローを実現できます。
IAMのメニューから「IDプロバイダ」を選択し「プロバイダを追加」をクリックします。
プロバイダのタイプで「OpenID Connect」を選択します。
以下を入力します。
URLにtoken.actions.githubusercontent.com
対象者にsts.amazonaws.com
GitHub Actions用のIAMロールを作成します。
作成したロールに、必要十分なIAMポリシー(S3、CloudFormation、Lambda等)を設定します。
作成したロールの信頼関係タブで以下のように信頼ポリシーを設定します。PrincipalのARNやgithubのリポジトリ名は環境に合わせて変更してください。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::1234567890:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:precena-dev/some-amazing-repository:ref:refs/heads/main"
}
}
}
]
}
利用したいGithub Actionsのyamlに以下を記載します。
- name: Assume AWS Role
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
次に、Github ActionsのSecretsに AWS_ROLE_ARN
を登録します。
これは必ずしも必須ではありませんが、Github Actionsが意図通りに動かない場合は、以下のコードをyamlに追記してIAMロールを適切に引き受けられているのかを確認すると良いです。
- name: Verify identity
run: aws sts get-caller-identity
以上で設定完了です。アクセスキー方式より煩雑ではありますが、一度行えばセキュリティを保ってGithub Actionsを運用できます。
Herokuで稼働するサーバーのベースとするOSのバージョンのようなものです。
公式サイトを見ると分かりますが、Ubuntu Linuxのバージョン番号とStack名が揃えられているようです。
以下で、Stackの確認ができます。2021年10月現在は、heroku-20がデフォルトStackです。
% heroku stack --app (自分のheroku app名)
=== ⬢ (自分のheroku app名) Available Stacks
container
heroku-18
* heroku-20
新しいStackに切り替えたい場合や、ステージング環境を作るときに本番に合わせて少し古めのStackを使いたい場合など、StackをデフォルトStackから変更したい場合は、以下のコマンドを使います。
% heroku stack:set heroku-18 --app (自分のheroku app名)
なお、Stackのupgradeは、Herokuの管理者用のWeb画面からも変更できます。downgradeは画面からはできないようです。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
AWS Lambda関数について、
ソースコードはgitで管理したい
ソースコードのデプロイは容易に行いたい
AWSの各リソースはTerraformで管理しており、別途Lambda向けのものを作る必要はない
という場合には、Lambdaのデプロイツールである lambroll
を使うのが便利です。
fujiwara/lambroll: lambroll is a minimal deployment tool for AWS Lambda.
lambrollはREADMEが充実していることもあり、悩むところは少なくてすみました。
それでも、組織のAWS環境で lambroll
を使う場合にはいくつか考慮することがあったため、この記事で紹介していきます。
スイッチロール時にMFAを利用するAWS環境の場合、lambrollだけではデプロイすることができません。
そこで、別記事でも紹介している AWS Vault
と組み合わせることでデプロイできるようになります。
AWS Vaultを使ったスイッチロール設定手順 | Precena Tech Book
実際に見ていきます。
まず、 aws-vault exec
により、スイッチロール後の some-profile
のシェルに入ります。
% aws-vault exec some-profile
続いて、lambrollで関数のデプロイを行います。
% lambroll deploy
例えば
staging
production
の2つの環境があり、各環境へ同一ソースコードの関数をデプロイしたくなったとします。
この場合、以下の方法で実現できます。
lambrollでLambdaを定義する時に使うJSON (function.json
) を環境ごとに用意する
例
staging環境向けは function.staging.json
production環境向けは function.production.json
各環境の環境変数は、JSONの Environment
キーの下にそれぞれ定義する
環境変数の値をJSONに含めたくない場合は、AWS SSMから取得するよう定義する
デプロイ時、 --function
で環境にあったJSONファイルを指定する
実際に見ていきます。
まずは function.staging.json
を用意します。
なお、スペースの都合上、例では FunctionName
と Environment
キーだけ記載しています。
{
"FunctionName": "foo-staging",
"Environment": {
"Variables": {
"FOO": "{{ ssm `/bar/baz` }}"
}
}
}
続いて、AWS Consoleなどから、AWS SSMにキーを作成します。
今回は /bar/baz
というキーに値を設定します。
最後に、staging環境向けにデプロイします。
% lambroll deploy --function=function.staging.json
一方、production環境向けにデプロイする場合は以下となります。
% lambroll deploy --function=function.production.json
lambrollではLambda Layerを作成することができません。
その代わり、lambrollではContainer Imageでのデプロイにて代替できます。 https://github.com/fujiwara/lambroll?tab=readme-ov-file#deploy-container-image
実際に見ていきます。
最初に、 aws-vault exec
にて some-profile
のシェルに入ります。
% aws-vault exec some-profile
次に、AWS ECRへdocker loginします。なお、 <>
の部分は適宜読み替えてください。
% aws ecr get-login-password --profile some-profile --region <region> | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com
Enter MFA code for arn:aws:iam::***:
Login Succeeded
続いて、デプロイするDockerfileを用意します。ここでは省略します。
その後、docker buildにて、タグ付きでDockerイメージをビルドします。
% docker build . -t <tag>:latest
docker pushにて、AWS ECRへDockerイメージをpushします。
% docker push <tag>:latest
さらに、lambrollでAWS ECRにあるコンテナイメージを使うよう、 function.json
へ PackageType
と Code
キーへ設定を追加します。
例えば、以下では、AWS SSMにある「ECRにあるイメージのURL( /path/to/ecr_image_url
)」を指定しています。
{
// ...
"PackageType": "Image",
"Code": {
"ImageUri": "{{ ssm `/path/to/ecr_image_url` }}:latest"
}
// ...
}
最後に、上記のJSONを使ってAWS ECRにあるコンテナイメージをAWS Lambdaへデプロイします。
% lambroll deploy --function=function.json
lambrollとAWS Vaultを使ってデプロイをしていると、以下のようなエラーメッセージが表示されるかもしれません。
2024/**/** **:**:** [error] FAILED. failed to load function: template attach failed: template: conf:*:*: executing "conf" at <ssm `/path/to/config`>: error calling ssm: failed to lookup ssm parameter: something went wrong calling get-parameter API: operation error SSM: GetParameter, https response error StatusCode: 400, RequestID: ***, api error ExpiredTokenException: The security token included in the request is expired
これはAWS Vaultのセッションが切れたのが原因です。
解消するには、一度AWS Vaultのセッションを exit
で抜けた後、再度AWS Vaultのセッションに入ります。
AWSリソースに対する異常検知として、CloudWatch Metricsに対して設定したしきい値を超過した場合、CloudWatch Alarmで通知を飛ばすことができます。
日頃Slackをよく見ている身としては、何かあったらSlackチャンネルへ通知されると嬉しいことから、仕組みを考えました。
Slackへ通知する仕組みとして思い浮かぶのはLambdaやChatBotです。
ただ、監視対象が異常となることは稀なものについては、Lambdaよりメンテナンス不要、かつ、ChatBotよりもカスタマイズ可能なものを採用したくなりました。
調べてみたところ、以下のStack OverflowにあるSam氏の回答 Amazon SNS + Slack Workflow 構成で設定・運用できそうでした。
ここでは、実際に構成してみた時の内容を記載します。
今回設定が必要な各リソースは以下となります。
AWS
CloudWatch Alarm
Amazon SNS (以降、SNSと表記)
今回はSlack通知のみ行い、Eメール通知は行わない
Slack
Slack Workflow
では、実際に各サービスを設定していきます。
CloudWatch Alarmを設定する際のウィザードではSNSトピックを作成可能です。 ただ、そのときに作るSNSトピックではEメール通知が必須となってしまいます。
そこで、今回は事前にEメール通知が不要なSNSトピックを作成します。その後、CloudWatch Alarmへ割り当てることにします。
今回のSNSトピックの設定は以下の通りです。 なお、残りの項目はデフォルトのままにします。要件に応じて変更してください。
続いて、CloudWatch Alarmを設定します。
今回必要なCloudWatch Alarmの設定は以下の通りです。
なお、設定する途中にあるEメールエンドポイントにて トピックにエンドポイントがありません
と表示されますが、Eメール通知を行わない設定の場合はこのままで問題ありません。
ここまででAWS側の準備ができました。次はSlack Workflowの設定になります。
Slack Workflowでは、SNSトピックのサブスクリプションを受け取れるように設定します。
ただ、AWSのSNSトピックのサブスクリプションを受け取るには、受け取る側で事前に SNSトピックのサブスクリプションの確認
(以降 サブスクリプション確認
と表記)を済ませておく必要があります。
そこで、Slack Workflowでサブスクリプションの確認ができるよう設定します。
まず、Slackチャンネルの詳細にある Integrations
タブから Add Automation
> New Workflow
> Build Workflow
の順にクリックします。
新しいSlackウィンドウが開きます。
右側に表示されているメニューから From a webhook
をクリックします。
Choose how to start the workflow
ダイアログが表示されます。
ここでは何も入力せず、 Continue
をクリックします。
Slackウィンドウの表示が変わります。次は Starts with a webhook
をクリックします。
Change how this workflow starts
ダイアログが表示されます。
ここでは Set Up Variables
をクリックします。
KeyとData Typeの入力が求められるので、それぞれ次の値を設定し、 Done
をクリックします。
ここで SubscribeURL
というキー名は、サブスクリプション確認をするときに、AWSから渡されてくる項目を指しています。
そのため、 SubscribeURL
というキー名以外で設定した場合はサブスクリプション確認ができなくなるため、注意してください。
次に、Web request URLにある Copy Link
をクリックし、URLをコピーします。
このURLは、AWSの設定でSNSトピックをサブスクライブするときに使います。
これで Change how this workflow starts
ダイアログでの設定は完了するため、 Save
をクリックします。
続いて、 Then, do these things
の設定を行います。
まず、右側のStepsの検索窓に send
と入力します(①)。
次に Send a message to a channel
をクリックします(②)。
Send a message to a channel
ダイアログが表示されるため、設定を行います。
サブスクリプション確認用のメッセージを設定するため、右下にある {} Insert a variable
をクリックし、先ほど作成した SubscribeURL
を選択します。
すると、 Add a message
の中に、 SubscribeURL
が指定されます。
これにより、Slackチャンネルへ投稿する際に、Slack Workflowへ通知された時にキー SubscribeURL
の値が展開されます。
あとはこのまま Save
をクリックします。
設定が終わったので、右上の Finish Up
をクリックします。
Finish Up
ダイアログにて、 Name
に任意の名前(今回は hello_world_slack
)を設定した上で、 Publish
をクリックします。
以上で、Slack Workflowでサブスクリプション確認を可能にする設定は完了です。
再びAWSの設定へと戻り、SNSトピックにSlack Workflow向けのサブスクリプションを作成します。
上記で作成したSNSトピック hello_alarm_topic
を表示し、 サブスクリプションの作成
をクリックします。
続いて、以下の設定を持つサブスクリプションを作成します。
なお、 rawメッセージ配信の有効化
をチェックしないことで、CloudWatch Alarmの情報はSNSの Message
キーへと設定されます。一方、rawメッセージ配信を有効化してしまうと Message
キーがなくなってしまうので、注意してください。
ここまででサブスクリプション確認向けの設定が完了したため、実際にサブスクリプション確認を行います。
今回の場合、SNSにサブスクリプションを作成した時点で、Slackチャンネルにサブスクリプション確認用URLが投稿されます。
投稿されたURLをクリックすると、ブラウザが開き、 ConfirmSubscriptionResponse
のXMLが表示されます。
続いて、AWSのサブスクリプションページをリロードすると、ステータスが 確認済
になります。
これでサブスクリプション確認は完了です。
サブスクリプションの確認ができたので、Slackへ投稿するメッセージを正式版へと修正します。
今回は、
SNSから受け取った Message
キーの値
独自メッセージ
を投稿します。
最初に、先ほど作成したWorkflowを開きます。
続いて、 Starts with a webhook
をクリックし、 Data Variables を編集します。
Key SubscribeURL
をKey Message
へと変更します。
続いて、 Then, do these things
の Add a message
を編集します。
独自メッセージを テストアラートが届きました
へと変更
Insert a variable
で Message
を挿入
最終的には、以下のスクリーンショットのような設定になります。
以上で設定は終わりです。 Save
をクリックした後、右上の Publish Changes
をクリックし、変更後のWorkflowを公開します。
以上で全体の仕組みが完成したため、動作確認を行います。
動作確認をするためには CloudWatch Alarmのステータスを アラーム状態
にする必要があります。
メトリクス対象を操作して アラーム状態
とすることもできますが、手間がかかります。そこで今回はCloudWatch AlarmのステータスをAWS CLIで強制的に変更することで、動作確認とします。
まずは適切なAWS環境を使えるよう、AWS Vaultを使って適切なシェルに入ります。
CloudWatch Alarmを見ると、ステータスが アラーム状態
へと変化しています。
Slackのチャンネルを見ると、CloudWatch Alarmからの情報が投稿されていました。 動作は良さそうです。
確認が終わったので、CloudWatch Alarmのステータスを OK
へと戻しておきます。
もし、今回作成したCloudWatch Alarmを無効化しておきたい場合は、以下の手順で行えます。
アクション > アラームアクション > 無効化
以上で、Amazon SNS + Slack Workflowを使って、CloudWatch Alarmの通知をSlackチャンネルへ投稿することができました。
CloudWatch AlarmからSlackチャンネルへの投稿する方法について、Lambdaよりメンテナンス不要、かつ、ChatBotよりもカスタマイズ可能としたい場合の参考となれば幸いです。
開発中にスイッチロール先でAWS CLIを利用したい場面もあるかと思います。 その場合の設定内容について記載します。
以下のようにアカウント設定されているとします。
スイッチ元アカウント
ID: 123456789012
aws configure
でデフォルトのプロファイルに設定済
スイッチ先アカウント1
ID: 210987654321
スイッチで引き受けるロール: arn:aws:iam::210987654321:role/delegate_root_organization
スイッチするときに使うプロファイル名: sw-staging
スイッチ先アカウント2
ID: 111111111111
スイッチで引き受けるロール: arn:aws:iam::111111111111:role/delegate_root_organization
スイッチするときに使うプロファイル名: sw-production
スイッチ元のアカウントを aws configure
で設定済みであれば、およそ以下のような設定になっているはずです。
~/.aws/config
に設定を追記し、以下のようにします。
このとき、設定内容は以下のようにします。
プロファイル名(sw-staging, sw-production
): 任意の名前で大丈夫です。CLIで利用するときにこの名前を指定します
source_profile
: スイッチ元のプロファイルを指定します
role_arn
: スイッチロールで引き受けるロールのARNを指定します
mfa_serial
: スイッチ元でMFAを設定している場合、MFAデバイスのARNを設定します
インタラクティブに実行できるかどうかで方法が変わってきます。
ユーザー自身が aws s3 ls
を実行する場合など、インタラクティブに実行できる場合は、以下の2通りの方法が利用できます。
--profile
引数にプロファイル名を指定
AWS_PROFILE
環境変数にプロファイル名を指定
例としては以下のとおりです。
個人的には、以下のように使い分けると便利かと思います。
引数: チーム内で同じアカウントは同じ名前に統一しておけば、コマンドを共有するだけでスイッチロールして実行させることができます
環境変数: シェルスクリプトにまとまっているときなど、コマンドを書き換えたくないときに便利です
role_wrapper.sh
という名前で以下スクリプトを作成します(jq
コマンドが必要です)。
これを使って、以下のように実行します。
なお、今回は通知さえ飛ばせればどんなメトリクスでも構いません。AWSドキュメントで紹介されているチュートリアルを参考にメトリクスを設定しても良いでしょう。
なお、AWS Vaultについての詳細は、を参照してください。
続いてAWS CLIを使ってアラーム状態にします。
にて、AWSコンソールでのスイッチロールの方法について記載しました。
また、を利用すると aws-vault exec
を使って同様のことが実現できます。をご参照ください。
本サイトの更新情報は、Twitterので発信しています。ご確認ください。
タイプ
スタンダード
名前
hello_alarm_topic
アラーム状態トリガー
アラーム状態
次のSNSトピックに通知を送信
既存のSNSトピックを選択
通知の送信先
hello_alarm_topic (上記で作成したSNSトピック)
アラーム名
hello_world_alarm
Key
SubscribeURL
Data Type
Text
トピックARN
hello_alarm_topic
のARN
プロトコル
HTTPS
エンドポイント
Slackでコピーした Web request URL
の値
rawメッセージ配信の有効化
チェックしない(デフォルトのまま)
% aws-vault exec some-profile
% aws cloudwatch set-alarm-state --alarm-name hello_world_alarm --state-value ALARM --state-reason "test" --region ap-northeast-1
% aws cloudwatch set-alarm-state --alarm-name hello_world_alarm --state-value OK --state-reason "test" --region ap-northeast-1
[default]
region = ap-northeast-1
output = json
[default]
aws_access_key_id = ***
aws_secret_access_key = ***
[default]
region = ap-northeast-1
output = json
[profile sw-staging]
source_profile = default
role_arn = arn:aws:iam::210987654321:role/delegate_root_organization
mfa_serial = arn:aws:iam::123456789012:mfa/your.mail@example.com
[profile sw-production]
source_profile = default
role_arn = arn:aws:iam::111111111111:role/delegate_root_organization
mfa_serial = arn:aws:iam::123456789012:mfa/your.mail@example.com
% aws --profile sw-staging s3 ls
Enter MFA code for arn:aws:iam::123456789012:mfa/your.mail@example.com: # MFAコード利用の場合、認証コードの入力を求められます
# ...スイッチ先アカウント1のS3バケットが表示されます
% AWS_PROFILE=sw-production aws s3 ls
Enter MFA code for arn:aws:iam::123456789012:mfa/your.mail@example.com:
# ...スイッチ先アカウント2のS3バケットが表示されます
#!/bin/bash
set -euo pipefail
code=$1
shift
# assume-role
aws_credentials=$(
aws sts assume-role \
--role-arn arn:aws:iam::210987654321:role/delegate_root_organization \
--role-session-name session-staging \
--serial-number arn:aws:iam::123456789012:mfa/your.mail@example.com \
--token-code ${code}
)
# assume-roleの結果をAWS環境変数に展開
export AWS_ACCESS_KEY_ID=$(echo $aws_credentials | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $aws_credentials | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $aws_credentials | jq -r '.Credentials.SessionToken')
# 処理を実行
$@
% ./role_wrapper.sh <MFAコード> aws s3 ls
集
すべて公式サイトに記載されている内容ですが、よく使うコマンドをすぐに参照できるように、一覧としてまとめます。
% heroku pg:backups:capture --app your-app-name
% heroku pg:backups --app your-app-name
% heroku pg:backups:download --app your-app-name
特定のバックアップを指定してダウンロードしたい場合は、以下のようにバックアップID(bxxx
と記載した部分)を指定します。
% heroku pg:backups:download bxxx --app your-app-name
※ここでダウンロードしたファイルは、次の節のようにpg_restoreコマンドでPostgreSQLに復元できます。
% pg_restore --verbose --clean --no-acl --no-owner -h localhost -U myuser -d mydb latest.dump
( 公式サイトの説明ページは、こちら)
なお、homebrewでPostgreSQLをインストールした場合、PostgreSQLはkeg onlyなので、pg_restore
コマンドへのPATHが通っていない状態になっています。
その場合は、以下のように、ローカルで利用しているPostgreSQLの特定のバージョンのpg_restore
コマンドに対して、パスを通す必要があります。
% brew link postgres@xx
PostgreSQLのインスタンス自体は、Dockerの中で動作していてもpg_restore
コマンドを実行できます。その場合、コンテナ内のPostgreSQLが動作しているポートがローカルホストのポートにバインドされていなければなりません。
Docker内のPostgreSQLインスタンスに復元する場合でも、pg_restore
コマンドをホスト環境側(macOSのターミナルなど)で実行する場合は、ホスト環境にPostgreSQLをインストールして、pg_restore
コマンドを使えるようにしておく必要があります。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
今回2社同時に弊社Webシステムの脆弱性診断を実施いただく機会がありました。なかなか同時に2社にみていただくという機会はないと思い、記事にしてみたいと思います。
A社とB社の2社に、同時にある弊社Webシステムの脆弱性診断を依頼させていただきました。 A社はWebアプリケーション診断を得意とし、B社はソースコード診断を得意とする会社様です。 Webアプリケーション診断とソースコード診断は、それぞれ以下のような特徴を持ちます。
Webアプリケーション診断
攻撃者目線でWebアプリケーションに対して擬似的に攻撃を行う診断。 「脆弱性診断」と聞くとこちらをイメージする方が多いかもしれません。
ソースコード診断
ソースコードを直接目視にて確認する、ソースコードレビュー型の診断。 プログラム構造や設計に起因する問題を発見できる可能性が高く、外部からの攻撃による診断では発見が難しい脆弱性を発見できる可能性がある。
Webアプリケーション診断、ソースコード診断、どちらも何点か指摘事項をいただきましたが、注目すべきは同じ指摘が1つもなかったことです。
詳しい指摘事項についてここで書くことはできませんが、外部からの攻撃で発見可能な問題と、内側からのソースコード診断で発見可能な問題は異なる可能性がある、ということが実感できました。
もちろんソースコード診断については外部から攻撃されることのない脆弱性であれば問題がない、という見方もできるかもしれません。しかし今回いただいた指摘に関しては、たまたま外部からの攻撃で発見されなかったと考えられるものもありました。
ソースコード診断もWebアプリケーション診断もそれぞれ特徴があることがわかりました。 以下にまとめてみたいと思います。
診断を行う観点が異なるため、出てくる指摘事項の性質も異なります。
Webアプリケーション診断 外部から実際に擬似攻撃を行うため、危険度の高低はあれど、確実な問題を指摘していただける。
ソースコード診断 外部からの攻撃が成立するか否かに関わらず丁寧に診断いただけるため、ソースコードについても将来問題になる可能性を指摘していただける。
Webアプリケーション診断の方が、脆弱性診断の依頼にかかる手間が大きいです。これは以下のような理由によります。
Webアプリケーション診断
診断対象がAPI単位になる。APIの本数で値段が変化するため、予算によっては対象のAPIを絞るといった作業が必要になる。
APIの仕様など、アプリケーションの仕様について連携する必要がある。
脆弱性診断実施用の環境を用意したり、開発用のサーバーを一時的に脆弱性診断用に利用いただくのであれば診断予定日は開発者が利用しないようにするなど、調整が必要になる。
ソースコード診断
基本的にはソースコードを開示すれば良い。
アプリケーションの実行環境は用意するが、攻撃を行うわけではないため、サーバーのブロックなどが必要ない。
今回診断を2社に依頼したところ、複数のご指摘をいただきましたが、重大な脆弱性につながる指摘はなく、アプリケーション自体はよくできている、との評価をいただくことができました。
いただいたご指摘は軽微なものであるものの、すでに対応を実施し、完了しています。
Webアプリケーション診断とソースコード診断は診断の手法および視点が異なるものであり、出てくる指摘事項も異なるものとなります。 目的に応じてベストな診断は変わってくるかと思います。 適切な診断方法を選択するのが良いでしょう。
ビジュアルリグレッションテスト(以下、VRT)は、画像回帰テストとも呼ばれます。
VRTは、改修による予期せぬ UI のデザイン崩れを検出することを目的としています。 UIのスクリーンショットを撮り、それらをコミット間で比較して、変更を特定します。
当社のReactを利用したプロジェクトではデザインシステムの構築にStorybookを利用しています。 細かい運用はこの記事では割愛しますが、見た目上のバリエーションが存在するコンポーネントについては、1つのコンポーネントにつき下記の2画面を用意するようにしています。
Basic: StorybookのControlsアドオンを利用して動作確認できる
All: バリエーションが一覧できる
多くのツールが存在しますが、プレセナでは下記のツールを利用しています。 以下、VRTを導入していく手順を紹介します。
Storycap: Storybookをクロールし、スクリーンショット画像を取得します。
reg-suit: 画像の差分をレポートとして出力してくれます。
reg-keygen-git-hash-plugin: 比較すべきコミットを特定します。
reg-notify-github-plugin: GitHubのPRにレポートを通知します。
reg-publish-s3-plugin: 差分レポートをS3にアップロードします
これらのツールを組み合わせることで、Pull Requestにテスト結果の通知が届くようになります。 通知内のリンクから、さらに詳細なレポートを確認することもできます。
yarnやnpmを利用して、パッケージのインストールをおこなってください。 npm-scriptsにも、CIで動かすためのタスクを追加します。
{
...
"scripts": {
"build-storybook": "build-storybook",
"ci:vrt": "reg-suit run",
"ci:storybook-generate": "build-storybook -c .storybook -o dist-storybook -s public",
"ci:storycap": "storycap --serverTimeout 60000 --captureTimeout 10000 --serverCmd 'npx http-server dist-storybook --ci -p 9009' http://localhost:9009",
},
...
"devDependencies": {
...
"reg-keygen-git-hash-plugin": "0.10.16",
"reg-notify-github-plugin": "0.10.16",
"reg-publish-s3-plugin": "0.10.16",
"reg-suit": "0.10.16",
"storybook-addon-next-router": "3.0.5",
"storycap": "3.0.4",
...
},
}
GitHub Actions で正常に動かなったため、タスクを「Storybookのビルド(ci:storybook-generate)」と「スクリーンショットの撮影(ci:storycap)」に分割しています。
storycapの公式が推奨するコマンドは下記のため、 ( https://github.com/reg-viz/storycap#getting-started )
storycap --serverCmd "start-storybook -p 9001" http://localhost:9001
CIツールによっては、タスク分割せずに実行が可能かもしれません。
reg-suitの設定ファイルです。
% npx reg-suit init
のコマンドでひな形を生成することができます。
{
"core": {
"workingDir": ".reg",
"actualDir": "__screenshots__",
"thresholdRate": 0.001,
"addIgnore": true,
"ximgdiff": {
"invocationType": "client"
}
},
"plugins": {
"reg-keygen-git-hash-plugin": true,
"reg-notify-github-plugin": {
"prComment": true,
"prCommentBehavior": "default",
"clientId": "$REG_NOTICE_CLIENT_ID"
},
"reg-publish-s3-plugin": {
"bucketName": "your bucket name",
"acl": "private"
}
}
}
ワークフローをトリガーするGitHubイベントは、pull_requestではなく、pushである必要があります。
ubuntu-latestのイメージには、日本語が含まれていないため、日本語フォントインストールのジョブを入れています。Storybook上で日本語を利用していない場合は不要です。
name: Visual Regression Testing
on: push
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- name: Checkout
uses: actions/checkout@25a956c84d5dd820d28caab9f86b8d183aeeff3d
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@aa759c6c94d3800c55b8601f21ba4b2371704cb7
with:
node-version: ${{ matrix.node-version }}
cache: yarn
- name: Japanese Font Install
run: sudo apt install fonts-noto-cjk
- name: Install dependencies
run: yarn --frozen-lockfile
- name: workaround for detached HEAD
run: git checkout ${GITHUB_REF#refs/heads/} || git checkout -b ${GITHUB_REF#refs/heads/} && git pull
- name: run storybook generate
run: yarn run ci:storybook-generate
- name: run storycap
run: yarn ci:storycap
- name: run reg-suit
run: yarn ci:vrt
env:
REG_NOTICE_CLIENT_ID: ${{ secrets.REG_NOTICE_CLIENT_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
REG_NOTICE_CLIENT_IDは、公式のREADMEを参考に、 GitHubにreg-suitアプリを追加して取得してください。
AWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYは、regconfig.jsonに記載したbucketにアクセスできるものをIAMなどで作成してください。 ポリシーなどの詳細は、公式のREADMEを参考にしてください。 S3の細かい設定はこの記事では割愛します。
これらのID/KEYは、GitHubのsecretに登録する必要があります。 以上でVRTのために必要な設定は完了です。
上記の設定だけでもVRTとして正しく機能しますが、プレセナでは画面幅に応じた見た目の変化など細かくUIを確認できるようにしています。
module.exports = {
...
addons: [
...
'storycap', // addonとして、storycapを追加
...
],
...
}
import React from 'react'
import Header, { Props } from '@/components/organisms/Header'
import { Story, Meta } from '@storybook/react'
export default {
title: 'Parts/Header',
component: Header,
parameters: {
layout: 'fullscreen',
},
} as Meta
export const Basic: Story<Props> = (args): JSX.Element => <Header {...args} />
Basic.args = {
title: 'タイトル',
}
// 画面幅の異なるスクリーンショットを取るための記述
Basic.parameters = {
screenshot: {
variants: {
small: {
viewport: 'iPhone 5',
},
},
},
}
上記のような記述を追加することで、画像幅の異なるスクリーンショットを撮ることが可能になります。これによって、網羅性の高いテストを目指すことが可能です。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。
最近、当社の開発では単一のホストだけでWebアプリケーションをホストせずに、フロントエンドとバックエンドを別のサーバーでホストすることが多くなってきました。
そして、これまでは遭遇してこなかったブラウザやJavaScriptに関するセキュリティの仕様に対処することが頻繁に起こるようになってきました。
この記事では、それらに対処する際に調べて把握した知識の一部であるSame Origin PolicyとCORSについてまとめます。
WhatWGの定義に詳細が記載されていますが、Webのセキュリティモデルの基本となるものです。Originを共有するWebのリソースは互いに信用でき、それらの著作者(著作組織)も基本的には同じであると考えます。
WhatWGの定義によると、Originには、以下の2種類があります。
An opaque origin
A tuple origin
An opaque Originは、定義によると内部的に使うオリジンで、オリジンをシリアライズして復元することはできず、シリアライズした場合はnullとなるようです。あまり利用イメージが湧きませんが、我々の普段のWeb開発ではあまり使うことはなさそうに思います。念の為に定義文を引用しておくと、以下です。
An opaque origin
An internal value, with no serialization it can be recreated from (it is serialized as "
null
" per serialization of an origin), for which the only meaningful operation is testing for equality.
引用:https://html.spec.whatwg.org/multipage/origin.html#concept-origin-opaque
A tuple originは、以下の要素で構成されます。
Scheme(https、http、ftp、などURLの先頭部分につけるもの)
Host
Port
Domain
Domainは、Hostの一部であるからか、一般的に、以下の3つをOriginを構成する要素と記載されることも多いようです。この記事でも以下の3つでOriginが構成される想定で、以降の説明を記載します。
Scheme
Host
Port
つまり、https://www.precena.com:8080/index.html のようなURLがある場合に、A tuple originとは、
https(Scheme)
www.precena.com (Host)
8080 (Port)
の組み合わせで構成されます。
URL
同じかどうか (True/False)
理由
http://www.precena.co.jp
https://www.precena.co.jp
False
Schemeが異なる
https://www.precena.co.jp/
https://www.precena.co.jp/company/info
True
Path(/
と/company/info
)が異なるが、Scheme、Host、Portは同じ
https://data.precena.co.jp/
https://www.precena.co.jp/
False
Hostが異なる( data.precena.co.jp
と www.precena.co.jp
)
https://www.precena.co.jp/
https://www.precena.co.jp:3000/
False
Portが異なる(80
と3000
)
https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy (日本語訳サイトはこちら)に詳細な説明がありますが、要するに、特定のOriginのページ(html)やスクリプト(JavaScript)が、別のOriginのリソース(APIやページなど)にアクセスするのを防ぐブラウザの仕組みです。
ブラウザにSame Origin Policyが実装されているため、リクエスト先のOriginで、後述のCORSの許可がされていなければ、XmlHttpReuqest
やfecth API、<img>
タグを使ったリクエストなどは、異なるOriginに対してはできません。
Cross Origin Resource Sharingの略で、https://developer.mozilla.org/en-US/docs/Glossary/CORS (日本語訳サイトはこちら)やhttps://developer.mozilla.org/en-US/docs/Web/HTTP/CORS (日本語訳サイトはこちら)に詳しい説明が記載されています。
具体的には、Same Origin PolicyによってアクセスできないようなOriginをまたいだリソースに、あらかじめ、HTTPのヘッダをつけておくことで、アクセス可能にする仕組みです。
例えば、以下のようなHTTPのヘッダを使います。
HTTPヘッダ
ヘッダに記載する内容
Access-Control-Allow-Origin
どのオリジンからのアクセスを許可するか。* も指定可能。
Access-Control-Allow-Methods
どのHTTPメソッドからのアクセスを許可するか。*も指定可能。
ヘッダは、これ以外にもありますが、ここでは記載を省略します。
Railsアプリケーションでは、rack-cors
というgemを使って簡単にCORSの設定を行うことができます。
インストール方法や設定方法は、上記公式サイトに記載されているので、ここでは記載を省略します。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式で発信しています。ご確認ください。
この記事では、メール送信サーバーを設定時に、ほぼ必ず設定することになるSPF、DKIM、DMARCについて、知識の整理と共有のために概要や効果、設定方法を紹介します。
SPFは、Sender Policy Frameworkの略で、ドメインの所有者が「自分のドメインから送信を許可するメールサーバー(IPアドレスやホスト名)」をDNSに公開する仕組みです。
SPFの仕組みを使うことで、以下の効果が得られます。
送信ドメインの正当性を受信側が検証できる
攻撃者が特定のドメインを偽装して送ったメールを、受信者のサーバにブロック/減点評価させることができる
送信元は自分が正しい送信元であることを証明することができ、受信側での検証をパスしやすくなり、メールの到達率が向上する
これらにより、送信側としては自社の「なりすまし(スプーフィング)」をした攻撃者のメールの到達率を減らすことができ、受信側は「迷惑メール」判定をしやすくなります。
システムの提供側からすると、結果的に、自社のユーザーがなりすましの被害に遭いにくくなり、また、自社のサービスのメールが迷惑メールとして判定されにくくなります。
SPFレコードは、DNSのTXTレコードとして以下のような文法で指定します。
設定例:
上記の例では、サーバーのIP (203.0.113.5/32)と Google Workspace 経由のみ許可(+
を省略して設定)し、それ以外は拒否しています。
SPFを使った受信側の検証は、以下のような流れで行われます。
送信ドメインの取得
受信側 は SMTP セッションの MAIL FROM
/ HELO
などからドメイン名を取得
DNS へ SPF 問い合わせ そのドメインの TXT レコードを検索するため、DNS へクエリを送信
SPF レコード取得
DNS が返した v=spf1 …
レコード(または CNAME 追跡先のレコード)を受け取る
評価して結果を反映 実際の送信 IP とSPFレコード内容を照合し、Pass/Fail などの判定
システムからのメール送信でよく利用されるSendGridでは、Automated Securityという機能により、SPFレコードの登録を簡略化することができます。SPFレコードの設定では、同じ送信ドメインに対して複数レコードを登録するよくある間違いがあるため、そういった間違いを防ぐのが目的なのではないかと思われます。
Automated Securityを使う場合、ユーザーは以下のように自社のDNSにサブドメインのCNAMEレコードを登録します。
SendGrid側のDNSで、このu123456.em123.sendgrid.net
に対して、適切なTXTレコードが登録されているため、SendGridのユーザーはCNAMEレコードの登録だけでSPFレコードの登録ができます。
SPFは、前述のフローでも記載したとおり、MAIL FROM
/ HELO
などからドメイン名を取得するため、ヘッダーFromの詐称に対応できず、また、送信元が正しいかどうかだけしか判断しないため、内容の改ざんが行われているかは評価できません。
また、メールを転送してしまうと、転送サーバーがSPFレコードに定義されていないため、個人が転送した正当なメールやメーリングリストからの配信がFailする場合があります。
それらの問題を解決するために、SPFだけでなく、DKIMとDMARCが併せて使われます。
DKIMの仕組みを使うことで以下の効果を得ることが出来ます。
改ざん防止
ヘッダーや本文が送信元以外の通信途中で変更されていないことを証明
これらにより、SPFの弱点であった部分を補完することができ、システムやサービスの提供元から送られたメールが、受信側で適切に判定されるようになります。
その結果として、システムやサービスの利用ユーザーが第三者の改ざんによる攻撃をうけたとしても迷惑メールとして判定しやすくなり、提供元から送信されたメールは正しくユーザーに届くようになります。
DKIMは、DNSのTXTレコードとして以下のような文法で指定します。主要な部分だけ説明します。
DKIM-Signatureヘッダー例:
DKIM-Signatureに含まれる主なタグの内容は以下の表の通りです。
DKIMを使った検証は以下のような流れで行われます。
送信時
送信元が選択したヘッダー・本文をハッシュ化
送信ドメインの秘密鍵で署名
署名結果を DKIM-Signature:
ヘッダーとして追加
受信時
受信側 がDKIM-Signature:
ヘッダーから セレクタ名 (s
) とドメイン名 (d
) を取得
[セレクタ名]._domainkey.[ドメイン名]
の TXT レコードを DNS で取得
検証
公開鍵で署名を検証し、Pass / Fail / TempError / PermError を判定
SendGridでは、SPFと同様に、Automated Securityという機能によって、CNAMEを使ったDKIMの設定をすることができます。
仕組みはSPFと同様で、以下のようなCNAMEレコードを自社のDNSに登録すると、SendGrid側で登録されているDKIM用のTXTレコードを参照させることができます。おそらく、これもSendGridユーザーが誤登録をするのを防ぐための機能と思われます。
仮にDKIMだけを使って迷惑メールかどうかを評価をすると以下の問題が発生します。
改ざんされたメールでDKIMでPassしてしまう
送信されたメールを攻撃者が改ざんし、さらに、改ざん時に攻撃者が管理するドメインの署名を付けることで、DKIM-Signatureには、d=attacker.com
のようなタグで署名がつけられる。このとき、攻撃者がattacker.com
のDNSに公開鍵を登録しておけば、改ざんされたメールのDKIM認証がPassし、迷惑メールと判定されずユーザーに到達してしまう
メーリングリストなどでの転送時に、DKIMでPassしたメールがSPFでFailしてしまう
メーリングリストで転送された場合など、内容が改ざんされていないことをDKIMで証明したとしても、SPFとDKIMのそれぞれで独立して評価を行うと、送信元が変わってしまったことでSPF判定がFailとなり、迷惑メールのように扱われてしまう
1つ目の問題の場合、改ざん時に送信元も差し替わっているはずなので、SPFの情報を併せて評価に使えれば、攻撃を検出しやすくなります。
2つ目の問題の場合、SPFでFailになった場合にDKIMの情報で補完すれば、攻撃による変更ではないことが評価可能です。
上記のような対応をするために、SPFとDKIMの情報を併せて評価するのがDMARCです。
前述のSPF、DKIMをそれぞれ単体で判定に使った場合と比べて、DMARCを使うことで以下の効果を得られます。
途中で内容が改ざんされ、しかし、DKIM単体ではPassしている場合でも、迷惑メールと判定することが可能
メーリングリストで転送され、SPFがPassしない場合でもDKIMの情報に基づいて正しいメールと判定可能
ユーザーへの影響を考慮しつつ少しずつ判定を強化していく運用が可能
DMARCレコードは以下のようにTXTレコードをDNSに設定します。
adkimとaspfのAlignmentとは、文字通り「調整」の意味です。SPF、DKIMをどのような判定方法で強調動作に使うか、その調整方法を定義します。
メール受信
SPF と DKIM をまず評価
Alignment 判定
SPF Pass かつ SPF ドメイン≒From ドメインならSPF Alignment判定をPass
DKIM Pass かつ 署名ドメイン≒From ドメインならDKIM Alignment判定をPass
aとbのどちらかのAlignment判定をPassすればDMARCをPassと判定
ポリシー適用 (p=
)
DMARC Fail なら none / quarantine / reject
の指示に従う
レポート送信
集計レポート(RUA)
詳細レポート(RUF)
検証例:
以下のようなケースで、DMARCの検証がどのようになるかをまとめます。
途中で改ざん(DKIM単体はPass)
途中で内容が改ざんされ、しかし、改ざん者によるDKIM署名の公開により、DKIM単体ではPassしている場合
途中で転送(SPF単体はFail)
メーリングリストで転送され、SPFがPassしない場合
上記のケースがDMARCの判定ではどうなるかをまとめると次の表のようになります。
攻撃者に途中で改ざんされた場合がFailになり、攻撃ではない転送がPassになるのが確認できます。
DMARCのポリシーは、たとえば、何の検証も行わずにすべてreject
にしてしまうと、予期しない悪影響が発生してしまいます。
その状況を緩和し、運用を少しずつ厳格化していくために、以下のような導入ステップが一般的にベストプラクティスとされています。
SPF / DKIM を 設定する
_dmarc
に p=none
+ rua=
で 観測開始
レポートで Fail 要因を潰し、pct=
を使って一部トラフィックのみを quarantine
へ変更して影響を確認
問題がなくなったら p=reject
+ pct=100
で 本格運用
DMARCを使ったとしても、たとえば、メーリングリストが転送時に本文やヘッダーの一部分に(悪意のない)転送時の軽微な情報の追加を行う場合は、DKIMの署名が無効化されて改ざん扱いになってしまい、DMARCにおけるDKIM AlignmentもPassしなくなってしまいます。
Zendeskにおいて、社内の複数サービスの問い合わせフォームを一本化し、そこに送られてきた問い合わせを対象サービスの担当者に自動で振り分けてメールで通知するための設定について記載します。
Zendesk環境
弊社の以下3サービスの問い合わせフォームをZendeskで一本化します。
PLS(eラーニング事業)
プレセナアカデミー(toC向け研修事業)
Nest(研修管理システム)
また、サービスによってフォームの入力項目を自動的に変更し、各サービスごとに適切な情報を収集できるようにします。 具体的には、「対象サービス」「お名前」「ご利用環境」など全サービス共通で入力してほしい項目は常にフォームに表示しますが、 各サービスにおいて独自で入力してほしい項目(例えば「ユーザーID」など)については、 フォーム内でそのサービスが選択された場合のみ表示するようにします。
さらに、フォームから受け付けた問い合わせを自動的に対象サービスの運用担当者に振り分けた後、メールで担当者に通知するようにします。
なお、以下の手順では、PLSを設定する際の流れを記載します。
カスタムチケットフィールドを作成する
フォームの質問項目を検討する
今回の場合、作成したいチケットフィールドの種類は大きく分けて二つあります。
どのサービスにも共通して必要なもの
あるサービスのみに必要なもの
上記を踏まえ、以下のチケットフィールドを作成することとします。
対象サービス
所属企業名
お名前
ご利用環境
PLS用フォーム
なお、チケットの件名や説明のフィールドは、標準フィールドとして既に存在しますので、新たに作成する必要はありません。
チケットフィールドを設置する
まずは管理センターのサイドバーにある[オブジェクトとルール]をクリックし、[チケット]の下の[フィールド]を選択後、右上の[フィールドを追加]をクリックします。
ここでの注意点として、[権限]で選択するオプションは[顧客は編集可能]にします。
今回の場合、フォームの内容はすべて顧客に入力してもらうチケットフィールドしかないからです。チーム内での管理のためにエージェントのみが入力できるチケットフィールドを作成する場合は[エージェントは編集可能]を選択します。
また、[顧客は編集可能]を選択することで、ページの下部に以下が表示され、リクエストの送信時に入力を必須とするかどうか選択することができます。
入力が完了したら忘れずに[保存]します。
問い合わせフォームを作成する
上記で作成したチケットフィールドから必要なものをピックアップしてフォームを作っていきます。
まずは管理センターのサイドバーにある[オブジェクトとルール]をクリックし、[チケット]の下の[フォーム]を選択後、[フォームを追加]をクリックします。
フォームのタイトルを適切なものに設定します。
[利用可能なチケットフィールド]からフォームに追加したいチケットフィールドの[+]を選択します。 チケットフィールド「対象サービス」の選択内容によって他のチケットフィールドを出し分ける設定は後ほど行いますので、ここではフォームの中に含めたいすべての項目を選択しておきます。
なお、選択したチケットフィールドの並べ替えも可能です。 以下赤枠内のアイコンをドラッグすることで並べ替えができます。
フォームに盛り込むチケットフィールドの選択が完了したら忘れずに[保存]します。
作成したフォームに条件を設定する
チケットフィールド「対象サービス」で選択する内容によって、フォーム内に表示されるチケットフィールドが切り替わるように設定します。 具体的には、「対象サービス」で「PLS」を選んだ場合、PLS用のチケットフィールドがフォームに表示されるようにします。
設定方法
まずは管理センターのサイドバーにある[オブジェクトとルール]をクリックし、[チケット]の下の[フォーム]を選択後、条件を追加したいチケットフォームの右側の[︙]から[条件]を選択します。
[条件の適用対象]を[エンドユーザー]にし、[条件を追加]ボタンをクリックします。
表示されるダイアログボックスで以下の画像のように設定します。 これで、チケットフィールド「対象サービス」で「PLS」が選択されたら「所属企業名」「ご利用環境」「PLS用フォーム」「お名前」(+フィールド設定で必須入力としているもの)のフィールドを表示できるようになります。
なお、チェックボックスにて必須とするかどうかも忘れずに設定します。 チケットフィールドで設定した必須要件よりも、こちらの条件設定チケットフィールドの内容が優先されるため、フォーム上で入力必須としたいものはここでも設定しておく必要があるからです。 ここで必須にチェックを入れない場合、フィールドでは必須に設定していても、実際にフォームで入力する際に必須とならないので注意してください。
入力が完了したら忘れずに[保存]します。
振り分け先および通知先となるグループを作成する
管理センターのサイドバーにある[メンバー]をクリックし、[チーム]の下の[グループ]を選択後、画面右上の[グループを追加]をクリックします。
[グループ名][グループメンバー]などを設定します。 今回の場合、[グループ名]にはPLS、[グループメンバー]にはPLSの担当者を追加しておきます。
入力が完了したら忘れずに[保存]します。
振り分けとメール通知を発動する条件を設定する
管理センターのサイドバーにある[オブジェクトとルール]をクリックし、[ビジネスルール]の下の[トリガ]を選択後、[トリガを作成]をクリックします。
[トリガ名][カテゴリ]などを設定します。
どんな時にアクションを実行するかを[条件]で設定します。 条件は複数設定が可能です。 なお、条件に当てはまった際のアクションは後で設定しますので、ここでは設定しません。
今回は以下のように設定しました。
チケットフィールド「対象サービス」で「PLS」が選択されていること
チケットのステータスが新規であること
振り分けとメール通知を実行するよう設定する
上記で設定した条件が満たされたときに実行される[アクション]を設定します。 アクションは複数設定できます。
今回は以下のように設定しました。
問い合わせチケットを「PLS」というグループに割り当てる
「PLS」というグループに所属しているメンバーにメールで通知する
なお、メールで通知する際の件名や本文はカスタマイズすることができます。 本文の{{ticket.url}}の部分をプレースホルダといい、チケットに関する様々な情報を取得することが可能です。 プレースホルダは[メールの本文]の入力欄の下にある[使用可能なプレースホルダ]からコピーすることができます。
入力が完了したら忘れずに[保存]します。
これで、以下が完了しました。
一つの問い合わせフォームでサービスごとに項目を出し分ける動的フォームの作成
問い合わせ対象サービスの担当者に自動でチケットを振り分けてメールで通知する設定
2006年にで定義され、で最新版が定義されています。
なお、になっており、迷惑メール扱いになる可能性があります。
DKIMは、DomainKeys Identified Mailの略で、公開鍵暗号方式を使ってメールの内容が送信元以外に改ざんされていないことを保証するための仕組みです。で定義されています。
SPF、DKIMの判定結果を束ねて判断し、失敗時の処理とレポート方法をドメイン所有者側が宣言する仕組みです。、で仕様が定義されています。
このようなケースでは、 を ML サーバーが付与するなどの方法がとられますが、この記事ではその詳細の説明を省略することにします。
本サイトの更新情報は、Xので発信しています。ご確認ください。
の手順で問い合わせフォームをアクティブ化しておく必要があります
の手順で問い合わせの回答担当者をエージェントとして追加しておく必要があります
次にフィールドタイプを選択します。 各フィールドタイプの詳細についてはを参照してください。 今回の場合、「対象サービス」というフィールドを作成するときにはドロップダウン、それ以外のフィールドを作成するときにはテキストを選択します。
その後の入力方法はの[カスタムチケットフィールドを作成する]>[カスタムチケットフィールドを追加するには]の手順3以降をご覧ください。
v=spf1 <Qualifier1><Mechanism1>: 値 <Qualifier2><Mechanism2>: 値
v=spf1
バージョン識別子
Qualifier
+
(Pass, 許可, Qualifier省略時のデフォルト)
-
(Fail, 拒否)
~
(SoftFail, 受診はするが疑わしい)
?
(Neutral, 判断保留)
Mechanism
ip4:203.0.113.5
→ 指定 IPv4 を許可
ip6:2001:db8::/32
→ 指定 IPv6 範囲を許可
a
/ mx
→ DNS の A / MX レコードに登録されたホストを許可
include:spf.protection.example.net
→ 他ドメインで定義されたSPF を使って評価
[ドメイン名] IN TXT (
"v=spf1 ip4:203.0.113.5/32 include:_spf.google.com -all" )
em123.example.com IN CNAME u123456.em123.sendgrid.net.
[セレクタ名]._domainkey.[ドメイン名] IN TXT (
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFA..." )
[セレクタ名]
公開鍵を特定するために使われる値でメール送信時に付与する電子署名のsタグで指定するものと一致させる。
セレクタを複数登録することで、同じドメインに複数の公開鍵を運営することも可能 (したがってDKIMは同じドメインに対して複数レコードを登録しても誤りではない)
[ドメイン名]
メール送信に使うドメイン
v=DKIM1
バージョン識別子
k=rsa
鍵の種別。現在はrsaのみ。
p
Base64でエンコードした公開鍵本体
DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=s1;
c=relaxed/relaxed; q=dns/txt; t=1713900000;
h=from:subject:date:to:mime-version;
bh=PZC9zB...=;
b=QfN18lz...=
d
署名者ドメイン
s
セレクタ名
h
署名をしたヘッダーの一覧
bh
本文ハッシュ
b
実際の署名値
s1._domainkey.example.com IN CNAME s1.domainkey.u123456.wl123.sendgrid.net.
_dmarc.[ドメイン名] IN TXT (
"v=DMARC1; p=quarantine; rua=mailto:dmarc@ops.example.jp;
adkim=s; aspf=r; pct=100"
)
v=DMARC1
バージョン識別子
p
ポリシー:none / quarantine / reject
の3つを指定可能。
Passしない場合の挙動を定義する。
none
は、何もしない。
quarantine
は、SPAMとして隔離する。
reject
は、SMTPレベルで拒否する。
rua
集計レポート送信先(複数可)
ruf
詳細レポート送信先(任意項目)
adkim
DKIM Alignment:r
=relaxed / s
=strict
r
は、DKIMのd
がメールFromのドメインのサブドメインであってもAlignment判定をPassさせることができる。
s
は、メールFromのドメインとDKIMのd
が厳密に一致したときのみにAlignment判定をPassさせることができる。
r
でもs
でもDKIMがPassしていること(=内容が改ざんされていないこと)が前提。
aspf
SPF Alignment:r
/ s
DKIMと同様。
pct
pで指定したポリシーを全体どのくらいの割合に適用するかを定義。 この指定により、一部の受信者のみに少しずつポリシーを適用うる運用が可能になる。
sp
サブドメイン用ポリシー。(省略時は、サブドメインでもp
と同じ)
途中で改ざん
Fail
Fail
Pass
Fail (メールfromと d のドメインが異なる)
Fail (どちらのAlignmentもFail)
途中で転送
Fail
Fail
Pass
Pass
Pass (DKIM AlignmentがPass)
この記事では、一般的に知られているデータ分析プロセスを簡単に紹介します。
データ分析において、一般的に知られている標準プロセスには以下が存在します。
CRISP-DM(*1)
KDD(*2)
以下、それぞれについて概要を紹介します。
*1 Shearer C., The CRISP-DM model: the new blueprint for data mining, J Data Warehousing (2000); 5:13—22.
*2 Fayyad, Usama; Piatetsky-Shapiro, Gregory und Smyth Padhraic (1996), From Data Mining to Knowledge Discovery in Databases, AI Magazine, American Association for Artificial Intelligence, California, USA, Seite 37–54.
Shearerらが提唱しているCRISP-DM(CRoss Industry Standard Process for Data Mining)では、次の図のようなプロセスにしたがって、データ分析を行います。
1
ビジネス理解
ビジネスにおける課題を明確にし、データ分析プロジェクトの計画を立てます。
2
データ理解
データを取得し、そのデータが分析に使える状態であるか確かめるなどの探索的データ分析を実施して、データの理解を深めます。
3
データ準備
後続のモデリングで要求される形式にデータを整形するなどの、前処理を実施します。
4
モデリング
分析モデル(予測をするためのアルゴリズム)を決め、 前のプロセスで準備したデータをモデルに学習させます。
5
評価
次は、前のプロセスで作成したモデルを使って、実際に分析を行い、このモデルよる予測がビジネスに利用可能であるかを評価します。
6
適用
評価した結果、問題がなければ、そのモデルによる予測をビジネスに適用して、使います。
図にも表現されているように、CRISP-DMでは、必要に応じて前後のプロセスを行き来しながら分析を進めます。
CRISP-DMがビジネスにおけるデータ分析プロジェクト全体を考慮しているのに対し、Fayyadらが提唱しているKDD(Knowledge Discovery in Databases *3)は、よりデータ分析部分にフォーカスしています。
KDDのプロセスは次の図のようになります。
1
データ取得
対象ドメインを理解し、顧客視点から分析の目標を定めた後、必要なデータを取得します。
2
データ選択
取得したデータから、データマイニングに必要なものを選択します。
3
データクレンジング
目的データに対して、外れ値の除去や欠損値への対応などのクレンジングを行います。
4
データ変換
クレンジング済データを、データマイニングに必要な形式に変換します。
5
データマイニング
変換済データに対し、回帰や分類、その他手法などを使ってパターンを抽出する。
6
解釈・評価
データマイニングを行った結果から得られたパターンを解釈し、評価します。
図を見るとわかりますが、KDDにおいても、必要に応じて、前段のプロセスへ戻る可能性があることが明確にプロセスに組み込まれています。
*3 Fayyad, Usama; Piatetsky-Shapiro, Gregory und Smyth Padhraic (1996), From Data Mining to Knowledge Discovery in Databases, AI Magazine, American Association for Artificial Intelligence, California, USA, Seite 37–54.
実際の実務においては、ビジネス理解が必須になるため、どちらかというとCRISP-DMのプロセスが実態に近いですが、データ分析部分のプロセスとして、KDDの考え方も参考にはなるでしょう。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式で発信しています。ご確認ください。
最近、当社の勤怠システムが更改されるとともに、勤怠打刻のWeb APIも公開されました。
すると、エンジニア間で「わたしのかんがえたさいきょうのきんたい」ブームが起き、いろいろな勤怠打刻方法が生み出されました。
今回は、私の作成した Raspberry Pi + PaSoRi + Python の勤怠打刻マシンをご紹介します。
個人的に勤怠打刻に欲しい機能として
タイムカードボックスからタイムカードを取り出して打刻
物理タイムレコーダーと同じような操作感がほしいため
「打刻したら音声で挨拶する」「打刻したらSlackで通知する」
リモートワークしてても物理出退勤してる感を出したいため
があります。
そこで、Raspberry Piと PaSoRi と FeliCa を使って
無印のタイムカードボックスからFeliCaを取り出す
FeliCaをPaSoRiにタッチする
PaSoRiのつながっているRaspberry Piが反応し、Web APIで打刻する
打刻に成功したら、Raspberry Piに接続したスピーカーから音声を出す
Slack API で打刻したことを通知する
出勤
と書かれたタイムカードボックスにFeliCaを入れる
ができるような仕様とします。
ハードウェア
Raspberry Pi 2 Model B (以降、ラズパイと表記)
Raspberry Pi OS, January 28th 2022
PaSoRi RC-S380
100均のスピーカー XYZ-22-A
FeliCa KURURU
ソフトウェア
Python 3.9.2 (Raspberry Pi OS付属)
nfcpy 1.0.4
PasoRiでFeliCaを読むときに使用
勤怠打刻マシンの外観です。
手前の黒いPaSoRiにFeliCaをタッチし、出退勤を打刻します。
今回、Slack botからSlack通知をするために、Slack appを準備します。
Bot tokens
を使うSlack appを作成する
OAuth & PermissionsのScopesは chat:write
勤怠打刻用のラズパイをセットアップします。
SSHを可能にする
IPアドレスを固定化する
mp3ファイルを再生できるよう、 mpg321
をaptでインストールする
/home/pi/projects/dakoku_pi/
ディレクトリを作成する
このディレクトリに打刻用プログラムファイルを入れる
Web APIでの勤怠打刻は、プログラミング言語を問わず利用できるようでした。
そこで、慣れているPythonを使って打刻してみます。
もし今後、勤怠システムの更改があったとしても、今回作成する打刻マシンはなるべく変更箇所を少なくしたいです。
そこで今回は、
共通的な処理を行う親クラス
Web APIで打刻する機能を呼び出す
音声を出す
Slackへ通知する
システムごとの処理を行う子クラス
Web APIで勤怠打刻する
というクラス構成としました。
また、 dakoku_pi
ディレクトリ以下を次のようにしました。
$ tree -L 2
.
├── dakoku/ # 打刻APIに関するPythonスクリプトを入れるディレクトリ
│ ├── base.py # どの打刻APIであっても共通的に使う機能をまとめたファイル
│ ├── dakoku.py # 打刻APIの詳細が記載されたファイル
│ └── __init__.py # main.pyからimportするために使うファイル
├── main.py # エントリポイント
├── slack.py # slack-sdkのラッパー
└── voice/ # 音声ファイル用のディレクトリ
├── clock_in.mp3 # 出勤する時の音声ファイル
└── clock_out.mp3 # 退勤する時の音声ファイル
必要なライブラリをインストールします。
なお、Slackのトークンなどの秘匿情報はハードコーディングせず、 .env
ファイルに記載して python-dotenv
で環境変数へロードすることとします。
% pip install requests python-dotenv slack_sdk nfcpy
複数箇所でSlackへの投稿を行うため、python-slack-sdkの薄いラッパーを用意しておきます。
import os
from slack_sdk import WebClient
class Slack:
def __init__(self):
self.client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN"))
def send(self, message):
self.client.chat_postMessage(
channel=os.environ.get('DESTINATION_OF_CHANNEL_OR_USER_ID'),
text=message
)
再掲となりますが、このクラスでは
打刻する
を呼び出す
音声を出す
Slackへ通知する
の機能を持たせます。
そのため、 打刻する
を呼び出すところは
@abstractmethod
def clock(self) -> Optional[AvailableTypes]:
pass
としておき、子クラスに実装を任せます。
その他の共通的な機能は以下とします。
import os
import pathlib
import subprocess
import time
from abc import ABCMeta, abstractmethod
from enum import Enum
from typing import Optional
BASE_DIR = pathlib.Path(__file__).resolve().parents[1]
VOICE_FILE_OF_CLOCK_IN = f'{BASE_DIR}/voice/clock_in.mp3'
VOICE_FILE_OF_CLOCK_OUT = f'{BASE_DIR}/voice/clock_out.mp3'
class AvailableTypes(Enum):
CLOCK_IN = 'clock_in'
CLOCK_OUT = 'clock_out'
class DakokuBase(metaclass=ABCMeta):
def __init__(self, slack_client, idm, keep_power_on):
self.api_headers = {
'Content-Type': 'application/json',
}
self.slack_bot_token = os.environ.get('SLACK_BOT_TOKEN')
self.slack_message_of_clock_in = os.environ.get('SLACK_MESSAGE_OF_CLOCK_IN')
self.slack_message_of_clock_out = os.environ.get('SLACK_MESSAGE_OF_CLOCK_OUT')
self.slack_client = slack_client
self.idm = idm
self.keep_power_on = keep_power_on
@abstractmethod
def clock(self) -> Optional[AvailableTypes]:
pass
def run(self) -> None:
# 打刻する
result = self.clock()
# 打刻結果を元に音声を出す
self.sound(result)
# 打刻結果を元にSlackへ通知する
self.notify(result)
# 必要に応じてシャットダウンする
self.shutdown_if_needed(result)
def sound(self, result: Optional[AvailableTypes]) -> None:
if result == AvailableTypes.CLOCK_IN:
file = VOICE_FILE_OF_CLOCK_IN
elif result == AvailableTypes.CLOCK_OUT:
file = VOICE_FILE_OF_CLOCK_OUT
else:
return
subprocess.call(f'mpg321 {file}', shell=True)
def notify(self, result: Optional[AvailableTypes]) -> None:
if result == AvailableTypes.CLOCK_IN:
self.slack_client.send(self.slack_message_of_clock_in)
elif result == AvailableTypes.CLOCK_OUT:
self.slack_client.send(self.slack_message_of_clock_out)
else:
return
def shutdown_if_needed(self, result: Optional[AvailableTypes]) -> None:
# 退勤の場合のみシャットダウンを行う
if result == AvailableTypes.CLOCK_OUT and not self.keep_power_on:
self.slack_client.send('シャットダウンします')
# Slack通知が終わってからシャットダウンできるよう、ちょっと待つ
time.sleep(5)
subprocess.call('sudo shutdown -h now', shell=True)
DakokuBase
を継承し、 clock
メソッドを実装します。
社内の勤怠システムに依存するためここでは公開できませんが、 clock
メソッドを実装します。
class Dakoku(DakokuBase):
def clock(self) -> Optional[AvailableTypes]:
# 打刻処理
pass
import階層が深くなるのを避けるため、 __init__.py
にimportを追加します。
from dakoku.dakoku import Dakoku
今まで作成してきたファイルと nfcpy
を使い、FeliCaを読み込むと打刻できるよう実装します。
なお、開発用にコマンドライン引数も用意しておきます。
import argparse
import binascii
import os
import pathlib
import nfc
from dotenv import load_dotenv
from dakoku import Dakoku
from slack import Slack
BASE_DIR = pathlib.Path(__file__).resolve().parent
parser = argparse.ArgumentParser()
parser.add_argument('-d', '--debug', action='store_true', help='FeliCaは読み込まれたものとして実行する')
parser.add_argument('-k', '--keep_power_on', action='store_true', help='退勤時もラズパイを起動させたままにする')
def on_connect(tag):
idm = binascii.hexlify(tag._nfcid).decode('utf-8')
print(f'FeliCa IDm: {idm}')
slack_client = Slack()
if idm != os.environ.get('FELICA_IDM_OF_CLOCK'):
slack_client.send(f'このカードでは打刻できません: {idm}')
return
Dakoku(slack_client, keep_power_on=args.keep_power_on).run()
if __name__ == "__main__":
args = parser.parse_args()
load_dotenv()
if args.debug:
Dakoku(Slack(), keep_power_on=args.keep_power_on).run()
else:
try:
print('読み取り開始')
with nfc.ContactlessFrontend('usb:054c:06c3') as cf:
cf.connect(rdwr={'on-connect': on_connect})
print('終了')
except Exception as e:
print(e)
with open(f'{BASE_DIR}/error.log', 'w') as f:
f.write(str(e))
打刻した時にスピーカーから音声を出すため、mp3形式のファイルを2つ(出勤・退勤)用意します。
文字から音声を作るサービスで作成したり、自分で録音したりしてください。
作成したら voice
ディレクトリの中に
clock_in.mp3
clock_out.mp3
として保存します。
次に、systemdを使い「ラズパイへPaSoRiを挿入した時に上記スクリプトを実行することで、常時FeliCaのタッチを待ち受けている」状態にします。
udev
を使い、ラズパイのUSBポートへのPaSoRi接続を認識するよう設定します。
まず、 udev
の rules
を作成するため、PaSoRiの idVendor
と idProduct
を確認します。
$ dmesg | grep usb
...
[ 4.353996] usb 1-1.4: New USB device found, idVendor=054c, idProduct=06c3, bcdDevice= 1.11
[ 4.354035] usb 1-1.4: New USB device strings: Mfr=1, Product=2, SerialNumber=4
[ 4.354057] usb 1-1.4: Product: RC-S380/P
[ 4.354077] usb 1-1.4: Manufacturer: SONY
...
List of USB ID's - linux-usb.orgを見たところ、そのベンダーIDとデバイスIDの組み合わせが FeliCa S380 [PaSoRi]
で間違いなさそうでした。
次に、 /etc/udev/rules.d/90-rc-s380.rules
を以下の内容で作成し、serviceと関連付けます。
なお、serviceに指定した rc-s380.service
は後ほど作成します。
SUBSYSTEM=="usb", ACTION=="add", ATTRS{idProduct}=="06c3", ATTRS{idVendor}=="054c", GROUP="plugdev", TAG+="systemd", ENV{SYSTEMD_WANTS}+="rc-s380.service", NAME="pasori380"
続いて、systemd用のservice として /etc/systemd/system/rc-s380.service
を作成します。
[Unit]
Description=Pasori RC-S380 service
Requires=dev-bus-usb-001-004.device
After=dev-bus-usb-001-004.device
[Service]
Type=simple
User=pi
Restart=always
RestartSec=5
ExecStart=/usr/bin/python /home/pi/projects/dakoku_pi/main.py
以上で完成です。
新しい勤怠システムがリリースされてからラズパイ勤怠を利用していますが、特に問題は発生していません。
また、打刻し忘れることもなく、安定して運用できています。
弊社からは4名のエンジニアが現地で参加しました。
この記事では、RubyKaigi 2023に参加したエンジニアの中で印象に残ったセッションをピックアップして紹介します。
弊社ではRailsで作っているシステムはRSpecでテストコードを書いています。そのため、「ある箇所で破壊的な修正をしても、他のところでテストが落ちて気づいて救われる」と、テストコードの効果を実感しています。
テストコードのリファクタリングも随時行っているものの、基本的にはテストコードはプロダクトの成長とともに増え続けています。
その結果、最近、CIでのテスト実行時間が増えてきていることに対して課題感が出てきました。
そんな中、このセッションの説明に
I will share some lesser-known parallel testing insights I've gained in reviving the test-queue. And explore possibilities of parallel programming in Ruby.
とあったため、並列テストに関して何か得られそうだと思い参加しました。
セッションで印象に残ったのは、 parallel_tests
と test-queue
の仕組みの違いでした。
parallel_tests
は事前にテストを分割してから各workerで並列実行するため、テスト全体のスループットは一番遅いworkerに引っ張られるとのことでした。
一方、 test-queue
は空いているキューにテストを入れていくため、parallel_tests
の問題点を解決しているとのことでした。
ただ、test-queue
はテストフレームワーク特有の箇所があるため、もしRSpecが実装を変えたら動作しなくなるという説明もありました。
他には、セッションの冒頭にて
10分以内のビルドなど、テストの実行時間を短くするのは大切
RSpecで遅いテストを見つける方法
Database Cleanerのチューニング
についてもお話があり、このあたりの観点でもテストを見直していこうと感じました。
TypeProf の v2 を作っているというお話でした。
v1 はとにかく動作するものを実装する、速度は二の次という前提にしていた
実際に使ってみると本当に欲しかったものは「Rubyの型よりもIDEサポート」ということに気づいた
IDEサポートを強化するには解析速度は重要な要素
解析対象のコードは完全なコードではない(書きかけのコード)
v2 は解析結果を差分更新して速度を出している
対象としているエディタはVS Codeのようですが、Vimでも動きそうな情報を見かけたので試してみようと思います。
個人的 one of the best talks on RubyKaigi 2023 です。
実際に"++"を実装する試みが小さなステップで非常にわかりやすく解説されていました。現状の parse.y が i++
というコードをどのように扱っているのかから始まり、具体的に試してみる→できた→まだ問題が…という流れが parse.y 初心者にとってとっつきやすく、自分も parse.y の中身を見てみたいという気持ちが出てきました。
最後の「"++"があるこの言語は Ruby じゃない Ruby++ だ!」というオチに至るまでのストーリーも面白かったです。
なぜ ReDoS が起きてしまうのか、Ruby の正規表現エンジンである Onigmo の特性や、バックトラックにより分岐が指数関数的に増えて行くことをわかりやすく解説していただき非常に勉強になりました。
また、高速化の手法としてメモ化を導入し計算量が線形時間になることや、 Regexp.linear_time? メソッドで正規表現が線形時間になることのチェックが可能であることも知ることができました。
今後の展望として Regexp.linear_time? が false となるような正規表現の場合に警告を出す RuboCop をスピーカー自身で開発するとのことで、弊社でもリリースされたらすぐに導入したいと考えています。
匿名モジュールを用いて名前空間の衝突を避けようという話でした。
匿名モジュールの名前空間の挙動はこのセッションを聞くまで知らなかったので興味深く聞くことができました。
また、オートロードを匿名の名前空間上で行うために Zeitwerk をフォークして Im という gem を作成した話もありました。リリースインフォに載らないような Ruby の改善をどのように用いて課題を解決したのか、という一連の流れが特に面白かったです。
Public な gem を開発することがなかなかないのでセッションの内容をすぐに活用することは難しいと思いますが、自分も細かいものも含めて Ruby の改善をキャッチアップしてみたいと思わせてくれるようなセッションでした。
弊社のエンジニアたちは東北から関西まで様々な地域で暮らしているため、基本的にフルリモートで勤務しています。
日頃はSlackやGatherなどによるオンラインコミュニケーションを取っている一方、オフラインで直接会話する機会がなかなかありません。
そのため、今回のRubyKaigi 2023は、弊社のエンジニアたちが集まってランチを一緒に食べたり直接会話する良い機会となり、とても楽しく過ごせました。
最後になりましたが、RubyKaigi 2023を運営してくださったみなさま、本当にありがとうございました。
これからは来年沖縄で開催される RubyKaigi 2024を楽しみにして過ごしていきます。
EMconfJP2025の参加レポート
掲題の通りEMConfJP2025に参加してきました。
テーマは「増幅」と「触媒」。
懇親会付きのチケットも当日分のチケットもすぐに売り切れて、当日も大盛況だったと思います。自分が聞いた限りの講演のテーマとしては「事業・経営戦略と技術戦略の接合」に関してが多かった印象です。
聴講した講演について感想などを簡単に書いていきます。
当日は朝から連続して講演を聞いていたため、休憩時間は企業ブースに回るなど、慌ただしく過ごしました。スタンプラリーを埋めて景品のTシャツが欲しかったのですが、アンカンファレンスには参加できなかったことと、会場で知り合った方とプレーリーカードを掲げてチェキを撮ったりができなかったので、途中で断念しました。
広木大地さんによる講演で、内容としては「エンジニアリング組織論への招待」に加えて、EMについてと今の潮流と合わせたものとなっていました。正直、この基調講演だけでもチケット代の価値はあったな〜と思えるくらい良い講演でした。
Engineering Managementの4つのP : People/Product/Project/Platform(以前はテクノロジーだった)にまとめられてとてもわかりやすい整理だと思いました。そして「すべてを一人でできる必要はない、スーパーマンになる必要はない」とのことです。
重要なのは何が必要かを正しく理解し、周囲から「調達」してくる力であり、そのために自分自身がその全てを出来る必要はないが、内容をきちんと「わかっている」必要はある
一番のサビはやはり、『エンジニアリングとは「実現する」ことで、マネジメントとは「なんとかする」ことで、EMの役割は「エンジニアリングマネジメントは価値実現のためになんとかする」ことである』、だったと思います。おそらくみなさんに響いていて、他の登壇者のスライドも当日書き加えられるほどでした。
EMを増やすためにEMを目指すハードルを下げつつ、育成に投資をしながら実際に生み出すことを目指してPotential EM制度という形をとったようです。個人的にとても良い制度だと思ったのですが、組織課題解決のための施策は「状況によって正解が変わる」という点が面白かったです。
ベストプラクティスやn=1の方法論が機能するかはケースバイケースというのは当たり前ではあるのですが、組織の時間軸によっても変化するというのは新鮮な視点でした。
バリューベース戦略というフレームワークを用いて、エンジニアリング組織の戦略を事業・経営戦略に翻訳する、という試みの紹介でした。戦略が会社と組織とでアラインしているというのが重要なのですが、エンジニアリング観点からはそういった定量・定性の観点に起こすことがとても困難であり、どの組織も抱える悩みだと思います。
こういった「翻訳することで橋渡しになる」のもEMの重要な役割であり、そのために多種多様な領域や膨大な役割を持ちあわせる必要があるのだと感じました。
事業価値とエンジニアリングについて「事業軸・技術軸・組織軸」の3軸で切り分け、EMに求められる視点と実践的な内容を踏まえた講演でした。
こちらの講演も、戦略をエンジニア組織に下ろす・組織から上げる、といういかにして「翻訳」をするか、というテーマでした。
財務観点もさることながら、開発生産性のFourKeysとQCDの観測を継続し、改善に繋げている組織はすごいと思いました。また資料の密度が凄まじく、時間がある時にじっくりと見直したいです。
EMを志す人にとって「マクロな課題とミクロな不安」という切り口はわかりやすく整理されていて良いと思いました。EM(マネージャー)とIC(プレイヤー)のどちらの経験もそれぞれ生きる、と頭ではなんとなくわかっていても、それでも尽きない不安とどう向き合うかについての考え方や姿勢を学べました。
組織と事業の規模拡大に伴い、エンジニアリングマネジメントグループを創設し、フィーチャー型組織から職能横断型へと変更して、それぞれのチームにEMを設けるようにした、とのことです。規模も大きく、かなりドラスティックな変化だったようです。
最後の「組織的な課題解決策が次の組織的課題の要因となることを認識すること」というメッセージは、マネジメントや組織経営の難しさを表していると感じました。
個人的に一番刺さった・感動した講演でした。
離職も多くなっていき、収益的にも厳しい事業の中でのVPoEの経験を赤裸々に語っておられました。前職のスタートアップに居た時の似た状況を思い出して、当時はメンバーレベルで力及ばない場面も多く、最終的には辞める決断になりましたが、その時のマネージャーの気持ちを知ったような感覚を得ました。
その時にできる引き出しがいち開発者の領域を出ない・技術者としてもまだまだといった状態で、もっとできることがあったら生き延びれたのだろうか、といような思いを馳せました。
組織のアウトプットの方程式で、メンバーの能力と熱量をいかにして上げつつ、受ける制約や摩擦をいかに減らすか、という観点が面白かったです。「メンバーが向かうべき方向を揃えないと、頑張ったところで役に立たないもの(力を結集して作ったゴミ)ができてしまう」には笑ってしまいましたが、気をつけてないとしばしば起きうる事象だと思います。
またカンファレンスの使い方としては「実は廊下で喋ることが大事」とおっしゃっていて、自分は講演を聞くだけにとどまっていたので、次からは改善したいです。
とても楽しくも学びのあるEMカンファレンスでした。
本当は懇親会にも参加したかったのですが、チケットが一瞬で売り切れてしまったため別の公式懇親会に参加しました。場所は会場近くの「もうやんカレー」で、話に聞いてたものの初めて行ったのですがとても美味しかったです。
EM界隈のコミュニティは活性化している印象ですが、それでもEMの数が各所足りてない様子でした。懇親会にて日々の課題などを話している中で、試してみたい解決策が見つかったり、方向性が決まるといった発見もあったようです。社外のこういった場が大切なことを再確認できました。
当たり前ですが、登壇者の方々はたくさん本を読んでかつ実践もされていて、「単純にすごい」と刺激を受けることができました。また「世界はシステムで動く」というシステム思考に関する書籍への言及・引用が、異なる講演で3回くらいあったので翌日にはポチってしまいました。他にも読んでおくべき本がたくさん見つかり、またしても積読が捗りそうです。引き続きキャッチアップ等を進めていきます。
このたびの素晴らしいイベントを支えてくださった関係者のみなさまに、深く感謝いたします。ありがとうございました!
この記事でインストールしているバージョンは古くなっている可能性もあります。最新バージョンに読み替えて利用するようにしてください。
OS:Mac
Scala:2.13.6
Scala を使うには Java と sbt が必要になります。バージョン管理ツールを2つ紹介しますが、どちらを使っても構いません。
SDKMAN
Java と sbt どちらもこれ一つでインストールできるので、 anyenv を使っていないのであればこちらがおすすめです。
anyenv + jenv + sbtenv
anyenv ですべて管理したい人向けです。
また、Java には LTS バージョンが設定されており、このバージョンのみをサポートしているライブラリも多いです。特段の理由がなければ、最新版ではなく 8 や 11 といった LTS バージョンをインストールするのが良いと思います。
下記コマンドを実行してバージョンを確認してみます。 sdk-man-init.sh
の実行は .bash_profile
等の末尾に追加されているので、次からはターミナルを開いた時に自動実行されます。
バージョン管理ツールがインストールできたので、 Java と sbt をインストールします。
Java は 8系と 11系が LTS なので、どちらかをインストールします。選択肢が多いのですが、前項で述べた通り Temurin を選択します。
sbt はとりあえず新しいバージョンを入れておけば良いと思います。
ディレクトリ内で特定のバージョンを有効にするには、 sdk env
コマンドを利用します。
すると .sdkmanrc
ファイルが生成され、デフォルトでカレントバージョンが設定されるので、これを利用したいバージョンに変えればOKです。ただ、コメントにもあるように ~/.sdkman/etc/config
の sdkman_auto_env
を true
にしなければ自動的に適用されないので設定しておきましょう。
ちなみに Metals を使ったプロジェクトの場合、 sbt のバージョンに関しては project/build.properties を参照してくれるため、バージョン指定は不要です。
anyenvを利用する場合、java, sbtのバージョン管理ライブラリはそれぞれjenv, sbtenvを利用することになります。jenvはrbenvやnodenvと違って言語のインストール機能は持っていないため、JDKは手動でインストールし、jenvに読み込ませる必要があります。
環境構築の流れは以下の通りです。
anyenvのインストール
JDKのインストール
jenvのインストール&設定
sbtenvのインストール&設定
インストール可能なJDKを検索するとtemurin11
が表示されるようになりますので、これをインストールします。複数のJavaバージョンを切り替えて利用したい場合、ここで必要な分だけインストールしておきましょう。
これでJDKのインストールは完了です。
anyenvの通常の使い方に沿い、以下コマンドでjenvをインストールします。
jenvのexportプラグインを有効化しておきましょう。
インストールしたJDKのHomeディレクトリをjenvに認識させます。複数のJDKをインストールした場合はそれぞれ実施しましょう。ここで認識させたJDKがjenvのバージョン切り替え対象に加わります。
jenvで利用するバージョンを指定します。
これを行うと実行したディレクトリに.java-version
というファイルが生成されます(中身は指定したバージョン番号だけ記述されたシンプルなファイルです)。このディレクトリに移動した際にはjenvがこれを読み込み、指定されたjavaのバージョンが自動的に参照されるようになります。
shをリセットすると、先ほど有効化したexport
プラグインにより環境変数JAVA_HOMEも設定されます。
以下を実行すると、.java-version
ファイルが存在しないディレクトリでデフォルトで適用するバージョンを設定することができます。必要があれば設定してください。
これでjenvのインストールと設定は完了です。
以下コマンドでsbtenvをインストールします。
sbtのインストールを行います。バージョンはSDKMANの項と同様に1.5.5をインストールする場合、以下のようにします。複数バージョンのsbtを切り替えて利用したい場合、ここで必要な分だけインストールします。
※ gpgが必要
といったエラーが表示された場合、インストールしてsbtの公開鍵を設定してから再実行してください。
インストール完了後、jenvの時と同様に利用するバージョンのsbtを設定します。
以上でanyenv + jenv + sbtenvの環境設定は完了です。
scalameta.metals
という extension をインストールするだけです。
extension は Scala プロジェクト以外は有効化されないはずですが[1]、私の環境では ruby のワークスペースなどにも .metals ディレクトリが出来てしまいました。基本は disable にしておいて、Scala のワークスペースでだけ手動で enable にするのが良いと思います。
VS Code のサポートについてはこちらに詳しく書かれています。
5/11(木)~5/13(土)に、長野県松本市のまつもと市民芸術館にてが開催されました。
とはいえ、RSpec3のサポートに加え、以下のプルリクにある通りRSpec4.0(dev)もサポートしていることから、社内のコードを使って test-queue
を試してみるのも良さそうと感じました。
本サイトの更新情報は、Twitterので発信しています。ご確認ください。
無償JDKとして広く利用されていたAdoptOpenJDKはプロジェクトに移管されることが公表されており、2021/8/13にはそのサブプロジェクトであるEclipse Temurinプロジェクトから新たなJDKがリリースされました。今回はこちらのJDKを利用します。
のインストール以下のコマンドを実施するだけです。
SDKMANではなくこちらの手順を選択している方は既にインストール済みの方が多いと思いますので、詳細な手順は割愛します。新規にインストールする方はのREADMEに沿ってインストールしましょう。
で推奨されている通りHomebrewでインストールを行います。2021/9/14現在temurinからリリースされているJDKは16系が最新となりますが、先述のとおり今回はLTSである11系をインストールします。つまり最新版以外のJDKをインストールする必要があるため、複数バージョンを扱えるようにしてくれるhomebrew-cask-versionsをインストールします。
以前は Scala といえば IntelliJ だったと思うのですが、最近は という Language Server が出てきており、Metals を使えば Visual Studio Code や Emacs などエディタを選ばずに Scala の開発ができるようになっているようです。
[1] に次の記載あり。The extension activates when the main directory contains build.sbt
or build.sc
file, a Scala file is opened, which includes *.sbt
, *.scala
and *.sc
file, or a standard Scala directory structure src/main/scala
is detected.
本サイトの更新情報は、Twitterので発信しています。ご確認ください。
% curl -s "https://get.sdkman.io" | bash
% source "$HOME/.sdkman/bin/sdkman-init.sh"
% sdk version
SDKMAN 5.12.2
# インストール可能な Java のバージョン一覧を確認
% sdk list java
# 11.0.12-tem をインストール
% sdk install java 11.0.12-tem
# インストールできた事を確認
% java --version
openjdk 11.0.12 2021-07-20
OpenJDK Runtime Environment Temurin-11.0.12+7 (build 11.0.12+7)
OpenJDK 64-Bit Server VM Temurin-11.0.12+7 (build 11.0.12+7, mixed mode)
# インストール可能な sbt のバージョン一覧を確認
% sdk list sbt
# 1.5.5 をインストール
% sdk install sbt 1.5.5
# インストールできた事を確認
% sbt --version
sbt version in this project: 1.5.5
sbt script version: 1.5.5
% sdk env init
.sdkmanrc created.
% cat .sdkmanrc
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=11.0.12-tem
% brew tap homebrew/cask-versions
% brew search --cask temurin # インストール可能なバージョンを表示
% brew install --cask temurin11
% ls -l /Library/Java/JavaVirtualMachines # インストールされたJDKを確認
% anyenv install jenv
% exec $SHELL -l
% jenv --version # インストールされたことを確認
% jenv enable-plugin export
% exec $SHELL -l
% jenv add /Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home
% exec $SHELL -l
% jenv versions # jenvで切り替え可能なJDKの一覧が表示される
% jenv local 11.0.12
% exec $SHELL -l
% env | grep JAVA_HOME
JAVA_HOME=~/.anyenv/envs/jenv/versions/11.0.12
% jenv global 11.0.12
% anyenv install sbtenv
% exec $SHELL -l
% sbtenv --version # インストールされたことを確認
% sbtenv install sbt-1.5.5
% brew install gpg
% gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
% sbtenv versions # インストールしたsbtのバージョンが表示される
% sbtenv local 1.5.5
ルールを設定する目的は、コンテンツ全体の統一感を出したり、定まったルールの元でコンテンツを書くことで、読者にとって読みやすいコンテンツにすることです。
しかし、コンテンツを書くためのハードルを上げたくもないので、ルールを多く管理したくもありません。ルールの設定は、必要最小限に抑えるようにしましょう。
なお、このページに記載するルールは基本原則ではありますが、記事の特性上ルールから外した書き方をしたい場合は、従う必要はありません。
以下、各ルールごとに説明を記載します。
例えば、次のようなコード例におけるルールです。
% rbenv install 2.7.3
この例では、zshのデフォルトのプロンプトを記載していますが、コマンドラインで実行するコードでは、それが分かるように、プロンプトを記載します。
bashを普段使っている人が記事を書くときに、bashのプロンプトを記載するのはOKとします。
また、インフラ系の記事などでは、rootユーザーのプロンプトを明示的に記載したい場合もあると思いますが、その場合は、[root]#
などをプロンプトとして記載します。
上の図のように、gitbook.comでは、見出しのスタイルとして、Heading1、Heading2、Heading3が使えますが、これは必ずしもH1タグ、H2タグ、H3タグになるわけではありません。
目次を適切に管理しやすくするために、記事上の見出しは、Heading1から使い、その内部で区切りをつけたい場合は、Heading2、Heading3と階層を下げてコンテンツを記載してください。
gitbook.comではリンクに下線がつかないことから、リンクに気付きにくいです。
そこで、リンクの文字列の書式は、以下の例のように太字としてください。
例:本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式で発信しています。ご確認ください。
AWS SESでメール送信環境を構築したときは、合わせて メールが不達になったこと
を検知する仕組みも構築します。 AWS SESから送信したメールでメール不達があまりにも多いと、AWS SESの利用が停止されるためです。
さらに、AWS SESの本番運用を始める前に、メール不達を検知する仕組みの動作確認も大切です。
その動作確認で役立つのが Amazon SES メールボックスシミュレーター
です (以降 シミュレーター
と表記)。
シミュレーターを使用した Amazon SES でのテストメール送信 - Amazon Simple Email Service
シミュレーターには バウンス
や 苦情
などのシナリオが用意されています。
このシナリオを使えば、AWS SES環境のバウンス率・苦情率に影響することなく、メール不達を検知する仕組みの動作確認ができます。
さらに、シナリオとして用意されていないケース、例えば
カスタムメールヘッダ付きのメールを送信し、メールが不達になる
reject
イベントが発生するメールを送信し、メールが不達になる
であっても、シミュレーターを使って動作確認ができます。
この記事では、シナリオとして用意されてないケースに対する、シミュレーターの使い方を記載します。
なお、不達を検知する仕組み 「AWS SESで発生したイベントの通知情報が AWS SES → AWS SNS → AWS SQS
の順で流れていく環境」 は、すでに構築済であるとして、ここでは記載しません。
まずはこのケースを試してみます。
今回は、「カスタムメールヘッダ X-My-Custom-Header
を含むメールを送信したが、バウンスによりメールが不達になる」ケースをシミュレーターで試してみます。
まずはシミュレーターで以下の設定を行います。
Eメール形式欄では、 Raw
を選択する
シナリオ欄では、 バウンス
を選択する
メッセージ欄では、以下のようなMIMEメッセージを入力する
To: bounce@simulator.amazonses.com
Subject: バウンステスト
X-My-Custom-Header: hello
Content-Type: text/plain
MIME-Version: 1.0
hello
参考までに、設定した後のスクリーンショットは以下となります。
以上で準備ができました。
では、シミュレーターの テストEメールの送信
ボタンをクリックしてメールを送信してみましょう。
すると、このメールはバウンスし、AWS SQSへとイベント情報が連携されます。
AWS SQSのコンソールにて確認すると、以下のスクリーンショットのようなメッセージを受信できました。カスタムメールヘッダ X-My-Custom-Header
が含まれています。
まず、「 reject
(拒否)イベントとは何か」 から記載します。
rejectイベントについて、AWSドキュメントの記載は以下です。
拒否イベントのテスト
Amazon SES を介して送信するすべてのメッセージでウイルスがスキャンされます。ウイルスを含むメッセージを送信すると、Amazon SES はメッセージを受け入れ、ウイルスを検出して、そのメッセージ全体を拒否します。Amazon SES でメッセージが拒否されると、メッセージの処理が停止され、受取人のメールサーバーへのメッセージ配信は試行されません。次に、拒否イベントが生成されます。
https://docs.aws.amazon.com/ja_jp/ses/latest/dg/send-an-email-from-console.html#send-email-simulator
ただ、「実際にウィルスを含むファイルを作成し、AWS SESでそのファイルを添付してメールを送信する」を試すのは色々問題があります。
その問題を避けるため、AWSドキュメントにあるように、AWS SESでは
拒否イベントは、欧州コンピューターウイルス対策研究所(EICAR)テストファイルを使用してテストできます
という方法で reject
イベントの発生をテストできそうです。
しかし、シミュレーターではEICARの内容をそのまま添付することはできません。
以下のドキュメントにあるように、シミュレーターでファイルを添付したい場合、ファイルの中身の文字列を base64
エンコードして メッセージ
欄へ設定する必要があるためです。
MIME の使用 - Amazon SES API v2 を使用した raw E メールの送信 - Amazon Simple Email Service
文字列をbase64エンコードするには、各プログラミング言語のライブラリを使うのが簡単です。
今回はRubyのirbを使って base64
エンコードします。
% irb
irb(main):001:0> require 'base64'
=> true
irb(main):002:0> Base64.strict_encode64('X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*')
=> "WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo="
これでEICARテストファイルは添付できそうです。
では、実際にシミュレーターで送信してみましょう。
今回は、「カスタムメールヘッダ X-My-Custom-Header
があり、かつ、EICARファイルを添付した状態でメールを送信する」ケースをシミュレータで試してみます。
まずはシミュレーターで以下の設定を行います。
Eメール形式欄では、 Raw
を選択する
シナリオ欄では、 カスタム
を選択する
カスタム受信者欄では、任意の受信可能なメールアドレスを指定する
メッセージ欄では、カスタムメールヘッダと添付ファイルを含んだMIMEメッセージを入力する
ここで、添付ファイルを含むMIMEメッセージをゼロから作るのは手間がかかります。
そこで、AWSドキュメントの MIMEの使用 | Amazon SES API v2 を使用した raw E メールの送信 - Amazon Simple Email Service をベースに
カスタムメールヘッダ X-My-Custom-Header
を追加
元々ある添付ファイルの値を、EICARテストファイルをbase64エンコードした値へと差し替え
という編集を加えて利用します。
具体的には、以下の内容をメッセージ欄へと入力します。
Subject: reject test
X-My-Custom-Header: hello
Content-Type: multipart/mixed;
boundary="a3f166a86b56ff6c37755292d690675717ea3cd9de81228ec2b76ed4a15d6d1a"
--a3f166a86b56ff6c37755292d690675717ea3cd9de81228ec2b76ed4a15d6d1a
Content-Type: multipart/alternative;
boundary="sub_a3f166a86b56ff6c37755292d690675717ea3cd9de81228ec2b76ed4a15d6d1a"
--sub_a3f166a86b56ff6c37755292d690675717ea3cd9de81228ec2b76ed4a15d6d1a
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable
Please see the attached file for a list of customers to contact.
--sub_a3f166a86b56ff6c37755292d690675717ea3cd9de81228ec2b76ed4a15d6d1a
Content-Type: text/html; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable
<html>
<head></head>
<body>
<h1>Hello!</h1>
</body>
</html>
--sub_a3f166a86b56ff6c37755292d690675717ea3cd9de81228ec2b76ed4a15d6d1a--
--a3f166a86b56ff6c37755292d690675717ea3cd9de81228ec2b76ed4a15d6d1a
Content-Type: text/plain; name="sample.txt"
Content-Description: sample.txt
Content-Disposition: attachment;filename="sample.txt";
creation-date="Sat, 05 Aug 2017 19:35:36 GMT";
Content-Transfer-Encoding: base64
WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo=
--a3f166a86b56ff6c37755292d690675717ea3cd9de81228ec2b76ed4a15d6d1a--
次に、シミュレーターの テストEメールの送信
ボタンをクリックしてメールを送信してみましょう。
すると、このメールの送信時にrejectイベントが発生し、AWS SQSへとイベント情報が連携されます。
AWS SQSのコンソールにて確認すると、以下のスクリーンショットのような
eventTypeが Reject
カスタムメールヘッダ X-My-Custom-Header
が含まれる
というメッセージを受信できました。
以上より、シミュレーターのシナリオとして用意されていないケースであっても、シミュレーターで試せると分かりました。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウントで発信しています。ご確認ください。