このページのみ全てのページ
GitBook提供
1 / 78

Precena Tech Book

Loading...

ソフトウェア開発

開発環境構築

Loading...

Loading...

Loading...

Loading...

Loading...

Slack

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

zsh

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

DB

Loading...

フロントエンド

Loading...

Loading...

インフラ開発

AWS

IAM

Loading...

Loading...

Loading...

Loading...

ECS

SES

Loading...

CloudWatch

Loading...

Lambda

Loading...

Heroku

Loading...

Loading...

セキュリティ

Web

Loading...

Loading...

Mail

Loading...

データ分析

Loading...

SaaS

Zendesk

Loading...

イベント

Loading...

Loading...

EMConf

Loading...

やってみた

Loading...

Loading...

Precena Tech Book 管理

Loading...

関連リンク

ngrok

Homebrew

ngrokのアップグレード(v2 to v3)

ngrokが最近メジャーバージョンアップして、バージョン3がリリースされましたので、アップグレード手順を紹介します。

なお、設定ファイルは、下位互換性がなくなっているので、ngrok本体のアップグレード後には、設定ファイルのアップグレードが必要になります。

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が使用できるようになります。

はじめに

Precena Tech Bookとは

株式会社プレセナ・ストラテジック・パートナーズのエンジニアのための知識ベースとして使用しているオンラインサイトです。

株式会社プレセナ・ストラテジック・パートナーズにおけるシステム開発で発生した各種調査、検討結果や他のメンバーへ共有したいノウハウなどのうち、社外にも公開できるものをコンテンツとして蓄積しています。

掲載コンテンツ

以下のような分野に関係するコンテンツを記載しています。

  • ソフトウェア開発

  • インフラ開発

  • セキュリティ

上記以外も、システム開発時に新たに進出した分野があれば追加していく予定です。

想定読者

  • 株式会社プレセナ・ストラテジック・パートナーズ社内のエンジニア、デザイナー

  • 社外のシステムエンジニア、デザイナー

免責事項

本サイトのコンテンツは可能な限り正確を期して記載していますが、本サイトのコンテンツに基づく運用結果については、株式会社プレセナ・ストラテジック・パートナーズは責任を負いかねますので、ご了承の上、内容を閲覧してください。

更新情報

Homebrew用語の意味

この記事は、独特なネーミングがされていて、知っているようで知らない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。

ngrokの導入

ngrokとは

ngrokでホストされるURL(例:http://xxxx.ngrok.io/ )へのアクセスをローカル環境のwebサーバー(http://localhost:3000 など)にトンネリングしてくれます。

例えば、stripeやLINE、その他クラウドサービスのwebhook処理を開発する際に、インターネット上に本番やステージングのwebサーバーを構築する前に、ローカル開発環境でwebhookへの対応コードを実装、テストすることができるようになります。

この記事では、ngrokの開発環境を用意する手順を紹介します。

ngrokのインストール

% brew install --cask ngrok

ngrokアカウントのサインアップ

auth tokenの設定

% ngrok authtoken <your-token>

そうすると、~/.ngrok2/ngrok.yml にtokenの値が書き込まれてサインアップしたngrokアカウントが認識されるようになります。

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のサブドメインの固定

無償のアカウントでは、ngrokを起動するたびに、http://<your-random-subdomain>.ngrok.io の <your-random-subdomain> の部分がランダムなサブドメインになってしまい、そのたびに、クラウドサービス側の呼び出し先設定を更新しなければなりません。

これは、ngrokの有償登録をすることで固定化できます。有償登録をすると、以下のようにサブドメインを他のユーザーが使っていない任意の文字列に固定できます。

% ngrok http 3000 -subdomain=<任意の固定したいサブドメイン>

ngrokのアップデート

ngrok起動中のコンソールでCtrl+U でアップデートが開始されます。

さきほどのエラーメッセージにも、にも説明されている通り、設定ファイルのアップグレードも必要になります。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

以下、用語の定義と意味を説明します。正式な情報はを参照してください。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

()は、httpのトンネリングサービスです。

でダウンロードして、インストールすることもできますが、インストールはmacOSの場合であれば、homebrewを使うほうが楽です。以下のコマンドでインストールできます。

ngrokを使うためには、サービスの利用登録が必要なので、でサインアップを行ってください。

サインアップするとにauthtokenが表示されているので、それをコピーして、以下のように、コンソールから設定します。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

公式サイト
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
公式サイト
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
ngrok
エングロクと読む
公式サイト
公式サイト
ログイン後のページ
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント

Slackの/remind コマンドの形式

当社の日常業務の中で、Slackのリマインダーを設定することは、頻繁にあります。

しかし、Slackのコマンドを使ってリマインダーを指定する際、とくに、日時の指定の仕方を中々覚えられず、公式ヘルプを見に行くことが多いです。

公式ヘルプにも詳しく載ってはいるのですが、特に「When」を指定する部分について、この記事では、一覧で分かるように紹介します。チートシート的な使い方を想定しています。

/remind コマンドの形式

/remind コマンドのフォーマットは以下です。

/remind [@someone or #channel] [what] [when]

この[what] の部分まではわかりやすいのですが、この記事では最後の[when] の部分を詳細に紹介します。

/remind コマンドの[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

[what]にスペースを入れたい場合

以下のように、特に" で囲んだりせずに、普通に指定できます。

/remind #channel aaaaa bbbbb ccccc on 1st Feb

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント

AWS CLI

インストール

公式サイトでインストーラーをダウンロードしてインストールしてください。

設定

基本的に以下の公式サイトに従うだけです。

公式サイトにも載っている内容ですが、以下のコマンドを入力して、

% aws configure

以下4つの項目を設定すればOKです。

  • AWS Access Key ID

  • AWS Secret Access Key

  • Default region name

  • Default output format

追加の設定

次に、多くの場合に必要となるjqとSession Manager pluginをインストールします。

jqのインストール

% brew install jq

AWS Session Manager pluginのインストール

以上。

これも公式サイトに載っているままの情報ですが、利用したいIAMユーザーのAccess Key IDとSecret Access Key は、 から、設定したいユーザーを選択して、「認証情報」タブでアクセスキーを生成してください。

はJSONを操作するためのCLIツールで、近年急速に利用が広がっています。Macの場合はbrewでインストールできます。

Session Manager pluginは、AWS CLIのプラグインで、ECSタスク内のコンテナやCLI経由でEC2にログインする際に必要です。に従ってインストールしてください。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

https://console.aws.amazon.com/iam/
jq
公式ドキュメント
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント

Ruby

当社の開発で使うRubyのバージョンは、プロジェクトごとに異なることが多く、また、プロジェクトごとにRubyのアップデート状況も異なります。

したがって、プロジェクトごとにRubyのバージョンを切り替える仕組みとしてrbenvを導入して使います。

rbenvのインストール

直接rbenvをインストールするか、anyenv経由でインストールするかで手順が変わります。

anyenvでインストールする場合

anyenvがすでに導入されている場合、以下で簡単にrbenvをインストールできます。

% anyenv install rbenv

直接rbenvをインストールする場合

まずは、homebrewでrbenvをインストール。このとき、ruby-buildも同時にインストールされます。

% brew install rbenv

次に、

% rbenv init

を実行して、出力された手順にしたがって、シェルの設定を行います。zshを使っている場合、以下のように出力されるので、この内容に従います。

% rbenv init
# Load rbenv automatically by appending
# the following to ~/.zshrc:

eval "$(rbenv init -)"

つまり、.zshrcファイルに以下を追加します。

.zshrc
# rbenvの初期化
eval "$(rbenv init -)"

使いたいバージョンのRubyをインストール

rbenvがインストールされたら、次は、使いたいバージョンのRubyをインストールします。

まずは、インストール可能なRubyの安定バージョンを確認します。

% 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

プロジェクトで使うRubyのバージョンを固定

プロジェクトで使うRubyのバージョンは、他の開発メンバーとも共有したいため、.ruby-versionファイルを作って、他のメンバーにも同じRubyのバージョンの使用を強制できるようにします。

プロジェクトルートで、以下を実行すれば.ruby-versionファイルがプロジェクトルート直下に作られます。

% rbenv local 2.7.3

Rubyのバージョンを上げる

Rubyのバージョンを上げる手順もanyenvでrbenvをインストールしたかどうかで異なります。

anyenvでrbenvをインストールした場合

rbenv install -lコマンドで、インストールしたいRubyのバージョンが表示されていない場合、anyenv内部のruby-buildのアップデートが必要です。このアップデートのコマンドを楽にしてくれるanyenv-updateを使うのをおすすめします。

anyenv-updateが設定してあれば、以下を実行するだけで、インストール可能なRubyのバージョン情報が最新化されます。

% anyenv update

このあとは、普通に特定のバージョンのRubyをインストールします。

直接rbenvをインストールした場合

同様に、インストールしたいRubyのバージョンが表示されていない場合、先に、rbenvとruby-buildをアップデートする必要があります。rbenvとruby-buildのアップデートにはhomebrewを使います。

% brew upgrade rbenv ruby-build

これで、インストール可能なRubyのバージョンが最新化されるので、特定のバージョンをインストールします。

Prettier

Prettierとは

以下、開発プロジェクトへのインストール手順を説明します。

Prettierのインストール

開発プロジェクトにおいて、yarnでnodeパッケージが管理されている前提です。

まずは、開発環境のみの依存パッケージとして、prettier をインストールします。

% yarn add --dev prettier

prettier-rubyのインストール

必要に応じて、prettier-rubyもインストールします。

% yarn add --dev @prettier/plugin-ruby

Prettierの設定ファイルを追加

プロジェクトルートに以下のファイルを作成します。

.prettierrc
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "bracketSpacing": false,
  "endOfLine": "lf",
  "printWidth": 120
}

上記の設定は、当社で使っているデフォルトの設定です。例えば、printWidthは、デフォルトの80だと少し狭く、フォーマットしたときに、過度に改行が入ってしまうため、120にしています。

Prettierのignore設定を追加

Railsのroutesファイルなどのように、なるべく改行をせずに一行で書いてしまいたいような特殊なコードは、pretterでのフォーマットがかからないように、ignoreファイルをプロジェクトルートに追加します。

.prettierignore
node_modules
yarn.lock
package-lock.json
public

/config/routes.rb

Linterとの競合回避

RuboCop

.rubocop.yml
inherit_from:
  - node_modules/@prettier/plugin-ruby/rubocop.yml # rubocopのルールと衝突しないための設定

また、これ以外にも、rubyの後置ifや後置whileなどのフォーマットをprettierにまかせてしまいたいので、以下のように.rubocop.ymlにルールを追加します。

.rubocop.yml
# 後置ifのフォーマットはprettier-rubyに任せたいので、rubocopのチェックは外す
Style/IfUnlessModifier:
  Enabled: false

# 後置while、untilのフォーマットもprettier-rubyに任せたいので、rubocopのチェックは外す
Style/WhileUntilModifier:
  Enabled: false

Editorの設定

以下、RubyMineやVisual Studio Codeの設定を各自行います。

RubyMine

JavaScriptやTypeScriptだけでなく、Rubyもフォーマットの対象にする場合は、設定画面で、Preferences -> Languages & Frameworks -> JavaScript -> Prettier から、Prettier Packageを{Project Root}/node_modules/prettierに設定した上で、Run for filesの部分でrbも含まれるように、以下のように変えておきます。

Run for files部分の設定
{**/*,*}.{js,ts,jsx,tsx,rb,rake}

On save のチェックをいれておくと、⌘S を押したときにフォーマットがかかります。 設定保存後もprettierが動作しない場合は、一度RubyMineを再起動するとうまくいきます。

Visual Studio Code

上記プラグインをインストールした上で、以下のように、.vscode/settings.jsonをプロジェクトルートに追加して、言語ごとにフォーマットの設定をします。チームメンバーのvscodeの設定状況によっては、このファイルはリポジトリにpushして他のメンバーと共有してもよいです。

.vscode/settings.json
{
  "[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は任意の設定で、保存時に自動的にフォーマットをかけるかを設定します。

Scala

この記事でインストールしているバージョンは古くなっている可能性もあります。最新バージョンに読み替えて利用するようにしてください。

想定環境

  • OS:Mac

  • Scala:2.13.6

Java と sbt のインストール

Scala を使うには Java と sbt が必要になります。バージョン管理ツールを2つ紹介しますが、どちらを使っても構いません。

  • SDKMAN

    • Java と sbt どちらもこれ一つでインストールできるので、 anyenv を使っていないのであればこちらがおすすめです。

  • anyenv + jenv + sbtenv

    • anyenv ですべて管理したい人向けです。

JDK と バージョンについて

また、Java には LTS バージョンが設定されており、このバージョンのみをサポートしているライブラリも多いです。特段の理由がなければ、最新版ではなく 8 や 11 といった LTS バージョンをインストールするのが良いと思います。

SDKMAN

% curl -s "https://get.sdkman.io" | bash

下記コマンドを実行してバージョンを確認してみます。 sdk-man-init.sh の実行は .bash_profile 等の末尾に追加されているので、次からはターミナルを開いた時に自動実行されます。

% source "$HOME/.sdkman/bin/sdkman-init.sh"
% sdk version

SDKMAN 5.12.2

バージョン管理ツールがインストールできたので、 Java と sbt をインストールします。

Java は 8系と 11系が LTS なので、どちらかをインストールします。選択肢が多いのですが、前項で述べた通り Temurin を選択します。

# インストール可能な 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 はとりあえず新しいバージョンを入れておけば良いと思います。

# インストール可能な 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 コマンドを利用します。

% 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

すると .sdkmanrc ファイルが生成され、デフォルトでカレントバージョンが設定されるので、これを利用したいバージョンに変えればOKです。ただ、コメントにもあるように ~/.sdkman/etc/config の sdkman_auto_env を true にしなければ自動的に適用されないので設定しておきましょう。

ちなみに Metals を使ったプロジェクトの場合、 sbt のバージョンに関しては project/build.properties を参照してくれるため、バージョン指定は不要です。

anyenv + jenv + sbtenv

anyenvを利用する場合、java, sbtのバージョン管理ライブラリはそれぞれjenv, sbtenvを利用することになります。jenvはrbenvやnodenvと違って言語のインストール機能は持っていないため、JDKは手動でインストールし、jenvに読み込ませる必要があります。

環境構築の流れは以下の通りです。

  1. anyenvのインストール

  2. JDKのインストール

  3. jenvのインストール&設定

  4. sbtenvのインストール&設定

anyenvのインストール

JDKのインストール

% brew tap homebrew/cask-versions

インストール可能なJDKを検索するとtemurin11が表示されるようになりますので、これをインストールします。複数のJavaバージョンを切り替えて利用したい場合、ここで必要な分だけインストールしておきましょう。

% brew search --cask temurin # インストール可能なバージョンを表示
% brew install --cask temurin11
% ls -l /Library/Java/JavaVirtualMachines # インストールされたJDKを確認

これでJDKのインストールは完了です。

jenvのインストール&設定

anyenvの通常の使い方に沿い、以下コマンドでjenvをインストールします。

% anyenv install jenv
% exec $SHELL -l
% jenv --version # インストールされたことを確認

jenvのexportプラグインを有効化しておきましょう。

% jenv enable-plugin export
% exec $SHELL -l

インストールしたJDKのHomeディレクトリをjenvに認識させます。複数のJDKをインストールした場合はそれぞれ実施しましょう。ここで認識させたJDKがjenvのバージョン切り替え対象に加わります。

% jenv add /Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home
% exec $SHELL -l
% jenv versions # jenvで切り替え可能なJDKの一覧が表示される

jenvで利用するバージョンを指定します。

% jenv local 11.0.12

これを行うと実行したディレクトリに.java-versionというファイルが生成されます(中身は指定したバージョン番号だけ記述されたシンプルなファイルです)。このディレクトリに移動した際にはjenvがこれを読み込み、指定されたjavaのバージョンが自動的に参照されるようになります。

shをリセットすると、先ほど有効化したexportプラグインにより環境変数JAVA_HOMEも設定されます。

% exec $SHELL -l
% env | grep JAVA_HOME
JAVA_HOME=~/.anyenv/envs/jenv/versions/11.0.12

以下を実行すると、.java-versionファイルが存在しないディレクトリでデフォルトで適用するバージョンを設定することができます。必要があれば設定してください。

% jenv global 11.0.12

これでjenvのインストールと設定は完了です。

sbtenvのインストール&設定

以下コマンドでsbtenvをインストールします。

% anyenv install sbtenv
% exec $SHELL -l
% sbtenv --version # インストールされたことを確認

sbtのインストールを行います。バージョンはSDKMANの項と同様に1.5.5をインストールする場合、以下のようにします。複数バージョンのsbtを切り替えて利用したい場合、ここで必要な分だけインストールします。

% sbtenv install sbt-1.5.5

※ gpgが必要 といったエラーが表示された場合、インストールしてsbtの公開鍵を設定してから再実行してください。

% brew install gpg
% gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823

インストール完了後、jenvの時と同様に利用するバージョンのsbtを設定します。

% sbtenv versions # インストールしたsbtのバージョンが表示される
% sbtenv local 1.5.5

以上でanyenv + jenv + sbtenvの環境設定は完了です。

IDE の設定

Visual Studio Code + Metals

scalameta.metals という extension をインストールするだけです。

extension は Scala プロジェクト以外は有効化されないはずですが[1]、私の環境では ruby のワークスペースなどにも .metals ディレクトリが出来てしまいました。基本は disable にしておいて、Scala のワークスペースでだけ手動で enable にするのが良いと思います。

VS Code のサポートについてはこちらに詳しく書かれています。

参考文献

zsh-completion

zsh-completionとは

zshを使うときに、補完用の情報を設定をしておくことで、例えば、git、aws-cli、dockerなどのコマンドやサブコマンドをTabキーで補完してくれるツールです。

公式サイトは、以下です。

zsh-completionのインストール

macOSを使っている前提ですが、homebrewで簡単にインストールできます。

zshのプロファイルスクリプトにzsh-completionの設定をする

zsh-completionをインストールすると、以下のようなメッセージがでます。

なお、このメッセージは、brew info zsh-completionコマンドで再表示できます。

このメッセージにしたがって、まずは、次のように .zshrc ファイル設定します。

zsh関連ファイルの権限設定を行う

次に、zsh関連ファイルの権限設定を行います。これも前述のbrew info で表示されるメッセージに従います。

前述の brew info で表示されるメッセージの /opt/homebrew の部分は、使用しているMacのCPUがIntel製かApple Siliconかによって異なります。

なので、環境に応じて次のようにコマンドを打ちます。

Intel Macの場合

M1などApple Silicon Macの場合

あるいは、以下のコマンドを打てばどちらの環境でもOKです。

なお、この go-w は、g(roup)とo(thers)から(つまりuser以外から)、書き込み権限を削除(-)しています。

各コマンドラインツールでの補完の設定

git

homebrewでgitをインストールすると、自動的に /usr/local/share/zsh/site-functions/ 以下に、git用の補完設定もインストールされるので、それ以上何もする必要はありません。

gitはmacOS標準のものもありますが、上記の補完設定のために、筆者はhomebrewでgitをインストールしなおしてしまいます。

設定がうまくいっている場合、以下のように補完されます。ブランチ名などは、覚えられないので補完を使うと便利になります。

docker

公式サイトに説明があるので、それに従います。手順は公式サイトに記載されていて、また、迷うところもないので、ここに手順を記載するのは省略します。

設定がうまくいっている場合、以下のような補完が使えます。

aws-cli

aws-cliも公式サイトに従うだけですが、少しわかりにくいので、補足説明を追加します。

公式サイトは、以下です。

前述の前提の場合、aws_completer はすでにPATHが通っているので、やることは以下の設定を.zshrcファイルに追加するだけです。

設定後は、zshを再起動します。

設定がうまくいっていれば、以下のようにawsコマンドの補完が効くようになります。

ngrokの導入(Precena Tech Book)

の手順に従います。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

おもにJavaScriptなどのフロントエンド系のソースコードのフォーマッタです。プラグインを追加することでRubyなどの言語にも対応可能です。詳細はを参照してください。

なお、prettier-rubyは、ですが、JS用のprettierと別管理にしたくないので、上記のようにnodeパッケージのプラグインとしてインストールしています。

も併用しているプロジェクトの場合、rubocopに定義されているスタイル関連のルールとprettierのフォーマットが競合して、gitのpre-commitにrubocopのチェックを行っているとリポジトリにコミットできなくなってしまいます。

そういった場合のために、ていて、この設定をプロジェクト用の.rubocop.ymlで継承します(参考:)。

最新のRubyMineでは、Prettierプラグインはデフォルトでインストール済なので、設定だけ行います(参考:)。

プラグインを使います。

本サイトの更新情報は、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ので発信しています。ご確認ください。

なお、この記事では、 の記事に記載さた手順でインストールされている前提で補足説明をします。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

公式サイト
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
公式サイト
gemでもインストール可能
rubocop
rubocopでprettierと競合するルールをオフにするための設定が、prettier-rubyには含まれ
公式サイト
公式サイト
prettier-vscode
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
Eclipse Adoptium
SDKMAN
anyenv公式
jenv公式
Metals
https://scalameta.org/metals/docs/editors/vscode/
公式サイト
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
% brew install 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)
~/.zshrc
# zsh-completionの設定
autoload -Uz compinit
compinit
% chmod -R go-w /usr/local/share/zsh/
% chmod -R go-w /opt/homebrew/share/zsh/
% chmod -R go-w $(brew --prefix)/share/zsh/
% 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
.zshrc
  autoload bashcompinit && bashcompinit   # この行を追加
  autoload -Uz compinit
  compinit
  complete -C '/usr/local/bin/aws_completer' 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          
https://tech-book.precena.co.jp/software/dev-env/ngrok/installtech-book.precena.co.jp

Mac

AWS CLI
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント

OpenAPI

バックエンド

対面での相談を気軽にするためのSlack設定

Slack emoji とGoogle meet を連携して使う

前置き

当社のエンジニアは全員フルリモートで働いているため、対面での相談は Google meet を使うことが多いです。

ただ、対面で相談するまでの準備に手間がかかると、気軽な相談はしづらくなりがちです。

そこで当社では、「Slackのカスタムレスポンスとemoji」にGoogle meetのURLを紐付けています。その結果、Slackでのやりとりが難しいと感じた時に、すぐ対面での相談へと移行できています。

必要なもの

  • Google meet

  • Slack

やりたいこと

  1. Slackへ相談の前フリと :room: などのemojiをポストする

  2. Slack botが相談用のGoogle meetのURLを案内する

設定手順

1. emojiの画像を用意

任意の画像を用意します。

なお、 生成履歴に絵文字を表示する のチェックボックスについては状況によりON/OFFします。

2. Slackにemojiを登録

3. Google meetの固定URLを取得

4. Slack botのカスタムレスポンスとして、emojiの文字列とGoogle meetのURLを設定

Slackbotタブに以下を入力し、保存します。

  • When someone says に、上記2.のemojiの値 (例: :room:)

  • Slackbot responds に、上記3.のGoogle meetのURL

以上で設定は完了です。

リマインダーを設定するSlack Help Center

もし、文字列画像を用意する場合は、絵文字ジェネレータ()などが便利です。

Slackのemoji設定ページ()を開き、文字列 (例: :room:)と 上記1.の画像を登録します。

Google meetのページ()にて、URLを取得します。

Slack botの設定ページ( )を開きます。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

https://emoji-gen.ninja/
https://***.slack.com/customize/emoji
https://meet.google.com/
https://***.slack.com/customize/slackbot
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
macOS での AWS CLI バージョン 2 のインストール、更新、アンインストール - AWS Command Line InterfaceAWS Command Line Interface
設定の基本 - AWS Command Line InterfaceAWS Command Line Interface
Logo
Logo
GitHub - rbenv/rbenv: Manage your app's Ruby environmentGitHub
GitHub - znz/anyenv-update: anyenv plugin that provides `anyenv update` command to update all **env and all pluginsGitHub
Docker for Mac を始めよう — Docker-docs-ja 19.03 ドキュメント
公式サイトのzshの補完設定
GitHub - zsh-users/zsh-completions: Additional completion definitions for Zsh.GitHub
Command completion - AWS Command Line InterfaceAWS Command Line Interface
Logo

ActiveRecordのfind_or_initialize_byメソッドにブロックを渡したときの挙動

過去に書いたソースコードを読んでいて、仕様を理解するのに手間取ってハマったので、共有のために記事を書いておきます。

find_or_initialize_byメソッドの例

以下のようなコードを見かけたとします。

user = User.find_or_initialize_by(email: 'sample@precena.com') do |user|
  user.last_name = 'サンプル'
  user.first_name = '太郎'
end 

上のコードから、どのような仕様を想像するでしょうか?

筆者は、ユーザーが見つかった場合、または、初期化した場合、どちらもlast_name、first_nameがそれぞれ、サンプル と 太郎 に初期化されると思ってしまいました。

しかし、実際の挙動としては、以下でした。

ユーザーが見つかった場合は、ブロック内の処理は実行されず、

ユーザーをinitializeした場合は、ブロック内の処理が実行される。

def find_or_initialize_by(attributes, &block)
  find_by(attributes) || new(attributes, &block)
end

コードを見ても、上記の挙動が実装されているのが確認でき、find_byで該当レコードが見つかった場合には、&block 部分が無視されるのが分かります。

実際に、を見てみると、以下のようになっています。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

コード
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント

Ruby on Rails

M1 Macでの開発環境構築(rosetta 無し)

はじめに

この記事では、M1 Macでの開発環境構築でハマったところを共有するために、記載して行きます。今後、随時新しいハマりどころが発生した場合は、情報を追加していきます。

環境構築の前提

筆者の環境では、環境構築の検証もかねているので、Rosetta 2をインストールしていません。Rosetta 2をインストール済みの場合は前提が異なってくるため、ご注意ください。

また、以下のものはインストール済みとします。

  • rbenv

  • Docker Desktop(4.3.0以上)

  • postgresql(実行用ではなく、gem pgのビルド時に利用する想定。brewでインストールする)

rbenvでの古いRubyのインストール

% 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

1. 環境変数RUBY_CFLAGSを指定してRubyをインストールする

以下のように、RUBY_CFLAGSを指定して、rbenvのインストールコマンドを実行すると上手く行きます。

RUBY_CFLAGS=-DUSE_FFI_CLOSURE_ALLOC rbenv install 2.6.5

筆者もこの方法を先に試して上手く行ったため、この後の2つめの方法を試せていません。

2. brew info libffiコマンドのガイドにしたがって、環境変数を指定する

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-composeコマンドを使わずに、docker compose コマンドを使う

% docker compose up -d

bundle installでネイティブ拡張が含まれるgemをビルドする

bundle install時にネイティブ拡張が含まれるいくつかのgemでインストールに失敗したのでgemごとの対処法を記載しておきます。

gem ffi

% 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

gem pg

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に書き込まれます。

gem rmagick

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を利用することで、エラーを回避できる可能性があります。

上記については、後日、この記事をアップデートして共有しようと思います。

Railsのアプリケーションサーバーのプロセス数とスレッド数の設定方法

はじめに

この記事では、Railsの本番環境におけるアプリケーションサーバー(pumaやunicornなど)のプロセス数とスレッド数のパラメータ設定に関する情報をまとめます。

プロセスとスレッドの特徴概要

アプリケーションサーバーのパラメータを設定する際に、OSにおけるプロセスとスレッドは重要な要素になるので、あらかじめ概要を説明しておきます。

プロセスはOSで実行中のプログラムのことで、プロセス内には1つ以上のスレッドが含まれます。CPUのコアに命令をしているのがスレッドです。

複数のプロセス同士は、同じメモリ領域を共有できません。しかし、同じプロセス内のスレッド同士は、同じメモリ領域を共有できるので、プロセス内に複数のスレッドを作成した方がメモリの利用効率が上がります。

複数のスレッドが同じメモリ領域を共有すると、メモリの利用効率は上がりますが、誤って別のスレッドに影響する情報を書き換えてしまう可能性もあります。いわゆるスレッドセーフではない状態が起こるということです。

1つのスレッドしかないプロセスを複数使用すれば、スレッドセーフかどうかを考慮する必要はなくなりますが、複数のスレッドを使う場合と比較して、メモリの利用効率は下がってしまいます。

1つのプロセス内に複数のスレッドを作った場合のメリットとして、一部のスレッドがIO待ち(例えば、データベースへのリクエストとそのレスポンスを待っているような状態)になってしまっても、プロセス内の他のスレッドで別の処理を進めることができるという点があります。この場合、プロセスとしてのスループットが大きくなります。

もし、1つのプロセスで1つのスレッドしか用意しなかったとしたら、そのスレッドでIO待ちが発生すると処理がブロックしてしまいます。

pumaとunicornの違い

Railsで使うアプリケーションサーバーとして一般的なものにpumaとunicornがあります。

pumaは、マルチプロセス対応で、さらに、各プロセス内に複数のスレッドを作成することができます。

unicornは、マルチプロセス対応ですが、各プロセス内には複数のスレッドを作りません。

したがって、スレッド間でメモリを共有できる分、メモリ効率はpumaの方が良くなりやすいです。ただし、pumaを使う場合は利用しているgemも含めてスレッドセーフな実装でアプリケーションを作っておく必要があります。

一方、unicornは、メモリ効率はpumaと比較して良くありませんが、スレッドセーフであるかどうかを気にする必要がありません。また、詳細な説明は省略しますが、稼働中のサーバーにデプロイを行う際にゼロダウンタイムのデプロイをすることが可能、などの特徴もあります。

この記事では、Rails標準のアプリケーションサーバーであるpumaを使う想定で、以降の説明をします。

RubyのGVLの概要

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コアの数です。クラウドのような仮想環境を利用している場合は、物理的なコア数ではなく仮想CPU数(vCPU数)を把握する必要があるかもしれません。

(訳注:IO待ちの場合は、GVLが開放されるとはいえ)GVLを考慮するとCPUの機能をフル活用するなら、Rubyコードのプロセス数は、CPUのコア数に一致させるのがよい、と記載されています。

利用可能なメモリ量

利用しているサーバーのメモリ量も把握する必要があります。

スレッドごとにRailsアプリケーションの処理が実行されるので、スレッド数が増えればそれだけ必要なメモリ量も大きくなります。また、プロセス数が増えれば必然的に包含するスレッドも最低1つは増えますので、プロセス数を増やす場合も、必要なメモリ量は大きくなります。

利用しているサーバーのメモリ量は、プロセス数やスレッド数の上限を考えるのに必要です。

1スレッドで必要なメモリ量

1スレッドで必要なメモリ量も把握する必要があります。

1スレッドで必要なメモリ量が例えば、400MB前後だとすると、スレッド数を1、2…と増やしていった場合に、必要なメモリは概ね400MB、800MB…と比例して増えていきます。

1スレッドで必要なメモリ量は、アプリケーションの実装によって幅が出てきます。筆者の経験では、Railsアプリケーションの本番環境では、概ね300MB〜1GBくらいの幅で変わるように思います。

もしすでに本番環境やステージング環境を運用しているのであれば、その環境でメトリクス計測ツール(New RelicやHerokuのMetrix機能)で確認するのが確実です。

稼働している本番環境やステージング環境で、実際に合計何スレッドが動作しているのかが分かれば、本番環境で利用しているメモリ量をその合計スレッド数で割れば、1スレッドで必要なメモリの量を概算することができます(厳密には、スレッド間で共有しているメモリがあるはずです。ここで計算しているのは、あくまで概算値です)。

Railsで設定しているコネクションプールの数

Railsには、コネクションプールの仕組みがあり、以下のように、database.ymlのpoolの値で上限値を指定することができます。

/config/database.yml
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を設定する必要があります。

アプリケーションサーバーとjobワーカーの数

一般的なRailsアプリケーションにおいて、データベースに接続するのは、pumaやunicornのようなアプリケーションサーバーからだけではなく、sidekiqのようなjobワーカーも存在します。

次節で説明するように、データベース側に接続上限数が存在するため、jobワーカーが存在するか、そして、それが存在する場合どれくらいの数があり、合計何スレッドくらい実行されるか、も考慮にいれる必要があります。

データベースの同時接続数

データベース側の同時接続数の上限値も把握しておく必要があります。

データベース側の同時接続数が分かれば、全サーバーの全プロセスで作られるスレッド数の合計値が、その同時接続数を超えないようにパラメータを調整しなければなりません。

したがって、結果的に、データベースの同時接続数は、各サーバーのプロセス数やスレッド数の上限値に影響します。

メモリ以外のパラメータの大小関係

これまでの説明を考慮すると、メモリに関するパラメータを除外すれば、少なくとも以下のような大小関係がなりたつように、各パラメータを設定する必要があります。

ただし、この大小関係を満たすという条件は変わりませんが、実際は、次の節で考慮に入れる利用可能なメモリ量で頭打ちになることが多いように思います。

メモリ関連のパラメータの大小関係

上記の大小関係とは別に、メモリによってもパラメータの大小関係が決まってきます。

これは、単純に、以下になります。

Heroku環境での設定例

最後に、当社で良く利用する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が使われるので、サーバーのスペックが不十分な場合は、障害が発生するかもしれません。

※Docker Desktopは、。

によると、以下2つの対処法があるようです。

ように、docker-compose コマンドはv1のdocker-composeが使われるためRosetta 2のインストールが必要です。、これまでdocker-composeコマンドを使用していた場合は、以下のように、docker compose コマンドを使います。

このgemのインストールに失敗する原因は、とと同じです。筆者は、gemのインストール時に発生したエラーに対しては、以下のように、brew info コマンド実行時に表示されるCaveatsの内容にしたがって対処しました。

CPUの数は、設定可能なプロセス数に影響を及ぼします。どのように影響するかについて、では、以下のように言及しています。

Due to the , 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.)

より一部引用

コネクションプール数≥スレッド数コネクションプール数 \geq スレッド数コネクションプール数≥スレッド数

では、コネクションプール数とスレッド数を同じ値にすることを推奨しているようです。実際に、これらの値が同じであっても上記の条件は満たされますし、スレッド数よりも過剰に大きいコネクションプール数を作成したとしても、その分サーバー上のメモリが無駄になるため、基本的にはHerokuの推奨のやり方に従うのが良いでしょう。

全アプリケーションサーバーの合計プロセス数×スレッド数(数式1)\text{全アプリケーションサーバーの合計プロセス数} \times \text{スレッド数} \text{(数式1)}全アプリケーションサーバーの合計プロセス数×スレッド数(数式1)
全jobワーカーの合計プロセス数×スレッド数(数式2)\text{全jobワーカーの合計プロセス数} \times \text{スレッド数} \text{(数式2)}全jobワーカーの合計プロセス数×スレッド数(数式2)
(数式1)+(数式2)≤データベースの同時接続上限数\text{(数式1)} + \text{(数式2)} \leq \text{データベースの同時接続上限数}(数式1)+(数式2)≤データベースの同時接続上限数
1つのサーバーの合計スレッド数に必要なメモリ量≤利用可能なメモリ量\text{1つのサーバーの合計スレッド数に必要なメモリ量} \leq \text{利用可能なメモリ量}1つのサーバーの合計スレッド数に必要なメモリ量≤利用可能なメモリ量

vCPUの値は、を参照。

を参考に確認する。今回の想定はstandard-2とする

全アプリケーションのサーバーの合計プロセス数×スレッド数=4×2=8\text{全アプリケーションのサーバーの合計プロセス数} \times \text{スレッド数} = 4 \times 2 = 8全アプリケーションのサーバーの合計プロセス数×スレッド数=4×2=8

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

バージョン4.3.0以降でもRosetta 2に依存している部分は残っているようですが、依存度は以前と比べて低くなっているようです
gem ffiの公式リポジトリのissue
Docker公式サイトで説明されている
Herokuの記事
GVL
https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#process-count-value
Herokuの記事
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
古いRubyをインストールしたときに出たエラー

Scala

React

公式サイト
公式サイト

tmux + overmind を利用して、複数システムを1コマンドで起動できるよう設定する

はじめに

当社の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システムのリポジトリがあるものとします。

overmind/
├── backend_app/
│   ├── app/
│   ├── bin/
│   ...
└── frontend_app/
    ├── app/
    ├── bin/
    ...

用語について

tmuxとは

terminal multiplexer と呼ばれるソフトウェアのうちの1つです。

1つのターミナルの画面を、複数に分割して利用します。

overmindとは

Herokuで使う Procfile と同じ書式で定義することで、定義したプロセスを管理できます。

チュートリアル

tmux + overmind でシステム全体を起動できるようにする

tmux + overmind をセットアップする

% brew install tmux

% brew install overmind

tmuxの設定を行う

tmuxはデフォルト設定のままでも問題なく使えます。

ただ、慣れないうちはマウス操作はできたほうが便利なため、 tmux の設定を追加します。

~/.tmux.conf ファイルを追加し、以下を記載します。

set-option -g mouse on

Procfileを作成する

チュートリアルのルートディレクトリである overmind に、ファイル Procfile を作成します。

Procfile には、各Railsシステムやジョブワーカーを起動する時のコマンドを記載します。

なお、ワーキングディレクトリを考慮するため、 && を使ってコマンドをチェーンしています。

# 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を起動する

macのターミナルから tmux を起動します。

% tmux

次に、 Procfile のあるディレクトリに移動し、overmind にて各プロセスを起動します。

% overmind s

tmux の画面では、 Procfile で定義した各プロセスの様子が表示されています。

動作確認

外部からのHTTPリクエストを受け付ける frontend_app に対し、 curl でアクセスします。

すると、 frontend_app からJSONレスポンスが返ってきます。

% curl http://localhost:3030/shops
{"shop":{"name":"スーパーマーケット","apples":[{"name":"シナノゴールド"},{"name":"シナノスイート"},{"name":"秋映"}]}}

tmuxを見ると、各システムやジョブワーカーが連携し、JSONレスポンスを返したことが分かります。

動作確認ができたため、いったん Ctrl + C にて overmind での実行を停止します。

1つのプロセスをtmuxの別ペインで表示する

現在はtmuxの1つのペインに、 Procfile で起動したすべてのプロセスのログが表示されています。

ただ、この状態のままでは各プロセスのログを追いづらいです。

そこで、別ペインで表示するよう設定します。

このチュートリアルでは、ウィンドウを上下ペインに分けます。

上ペインはここまで通り overmind のログを表示します。

一方、下ペインでは backend_app のログのみを表示するようにします。

別ペインで表示するための準備

以下の準備を行います。

  • tmuxで Ctrl + b + " を入力し、水平ペインを開く

  • 上ペインにて、以下の操作を行う

    • overmind s を実行し、各システム・ワーカーを起動する

  • 下ペインにて、以下の操作を行う

    • Procfile のあるディレクトリに移動する

    • overmind connect backend_app を実行し、overmindで実行している backend_app のプロセスに接続する

動作確認

再びcurlで frontend_app にアクセスしてみます。

すると、上ペインでは、各システム・ワーカーのログが出力されています。

一方、矢印部分の下ペインでは、 backend_app のログのみ表示されています。

RubyMineにて、overmindで起動したプロセスのデバッグを行う

今までの操作にて、各システム・ワーカーを overmind s だけで起動できるようになりました。

ただ、何か不具合があった時には、各システムをデバッグしたくなるかもしれません。

もしRubyMineを使っている場合は、 overmind で起動したプロセスにアタッチ・デバッグできます。

このチュートリアルでは、RubyMineを使って backend_app のプロセスにアタッチしてみます。

RubyMineでの設定

以下の順番で設定を行います。

  • RubyMineにて、プロセスにアタッチしたいシステムのリポジトリを開く

    • このチュートリアルでは backend_app リポジトリを開きます。

  • RubyMineのメニューにて、 Run > Attach to Process を選択する

  • 実行しているプロセスが表示されるため、 backend_app のプロセスを選択する

    • 下部のイメージ参照

  • RubyMineでブレークポイントを設定する

以上で、デバッグの準備が整いました。

動作確認

curlで frontend_app にアクセスしてみます。

すると、RubyMineで設定したブレークポイントで停止します。

実行時の各変数の内容も表示され、デバッグできていることが分かります。

まとめ

tmux + overmind を利用して、連携する一連のシステムやワーカーを起動できるようにしたことにより、より開発を効率的に行うことができるようになりました。

今後も開発を効率的に行う方法をTechBookにて共有していこうと思います。

Railsを6.1系から7.0系へアップグレードした時に調査したこと

先日、Rails6.1系で動いていたシステムをRails7.0系にアップグレードしました。

そのシステムは

  • Rails製APIアプリケーション

  • コード量・利用者ともに小規模

  • テストコードが充実している

と比較的アップグレードしやすかったこともあり、無事に完了しました。

この記事ではアップグレードの際に調べたことをまとめておきます。

リリースノートやRailsガイドのアップグレードガイドを読む

資料を読み終えたところで、対象のシステムは問題なさそうと判断し、準備を進めることにしました。

各gemがRails7.0対応しているか確認する

Rails7.0がリリースされた直後は、使っているgemがRails7.0対応していないものがありました。

そこで、定期的に各gemのリポジトリを見に行き、Rails7.0対応に関するissueやPull Requestを確認しました。

その後、使用しているすべてのgemがRails7.0対応がなされているのが確認できたため、具体的な作業に入りました。

各gemをバージョンアップする

今回は小規模だったこともあり、

  • bundle outdated でバージョンアップできるgemを確認

  • Rails以外を bundle update --conservative <gem名> でバージョンアップ

  • Railsを bundle update --conservative rails でバージョンアップ

の順で作業をしました。

また、各gemをバージョンアップするごとに、テストを流してパスすることを確認した上でGitブランチにコミットしました。

各種設定ファイルを更新する

まず、削除された設定について見てみましたが、主に

  • Rails7で config/initializers/ 以下が整理されたが、対象システムでは使っていない設定だった

ため、システムから削除しても問題なさそうでした。

次に、設定ファイルの変更で気になった点(後述)については調査を行いました。その結果、変更による影響がなさそうだったため、Rails7.0系での変更を各種設定ファイルに取り入れました。

ここでは、今回気になった点とその対応について以下にまとめます。

app/models/application_record.rb

class ApplicationRecord< ActiveRecord::Base
-  self.abstract_class = true
+  primary_abstract_class
end

config/environments/development.rb

+ config.server_timing = true

- config.assets.debug = true

- config.file_watcher = ActiveSupport::EventedFileUpdateChecker

一方、

  • config.assets.debug は使っていないシステムだった

  • config.file_watcher はDockerを使っていると影響ありそうな情報が散見されるものの、今回の環境では不要そうだった

ため、これらは設定ファイルから削除することにしました。

config/environments/production.rb

-  # 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/environments/test.rb

-  # 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?

そのため、 config.eager_load は従来のまま false と明示的に設定しました。

デフォルト値の影響調査

そこで、Railsのデフォルト値の変更を調べることにしました。

上記の記事にある項目を確認しましたが、今回のシステムに対して影響するものはありませんでした。

次に、さきほどのQiitaの記事で触れられていなかったものについて調査しました。それらを以下にまとめます。

config.active_support.disable_to_s_conversion: true

ある種のRubyコアクラスに含まれる#to_sメソッドの上書きを無効にします。この設定は、アプリケーションでRuby 3.1の最適化をいち早く利用したい場合に使えます。

とあります。

今回は小規模なシステムだったこともあり影響がなかったため、デフォルト値の変更を受け入れました。

action_dispatch.return_only_request_media_type_on_content_type: false

active_storage.multiple_file_field_include_hidden: true

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に記載していこうと思います。

GithubのプライベートリポジトリをGemfileで参照する方法

背景

当社では、社内で共通に使いたい機能をgemに切り出し、機能の利用側のGemfileでプライベートリポジトリを参照しています。

gem "some_internal_library", git: "https://github.com/precena-dev/some_internal_library.git", tag: "v1.0.0"

ローカル端末でのみ利用する場合はgitのURLはgit@ やssh@ で始まるURLを使えば問題なくbundle installできます。しかしCI/CD環境でもbundle installするため、httpsで始まるURLで登録しています。

このプライベートリポジトリをbundle install時に参照する方法について記載します。

Personal Access Tokenを使う方法について

ローカル端末の場合

Github CLIを使う方法

まずCLIをインストールしておきます。Macの場合はbrewコマンドでインストールできます。

% brew install gh

次に以下のコマンドを実行するとブラウザが開きますので、Githubの認可を行います。

% gh auth login

これで、bundle installが成功します。

git configを使う方法

% git config url.git@github.com:.insteadOf https://github.com/

そうすると、sshで参照するようになるためbundle installが成功します。

CI/CD環境の場合

github actionsやAWS codebuildなどのCI/CD環境について記載します。

Github Appsの作成

パーミッションについては、プライベートリポジトリを参照してbundle installするだけであれば、「Contents:Read-only」を選択するだけで良いでしょう。

作成後にPrivate Keyを作れるようになりますので、ひとつ作成して秘密鍵をダウンロードしておきます。

画面上部に表示されているAppIDを控えます

作成したGithub Appのリポジトリへの導入

Github App 左メニューのInstall Appを選択し、歯車アイコンをクリックします。

必要なリポジトリを選択し、Saveします。

Github Actions

控えておいたGITHUB_APP_IDおよびGITHUB_APP_PRIVATE_KEYを、下図のようにActionのsecretsに、登録しておきます。

Github App経由でtokenを取得します。その値を、環境変数BUNDLE_GITHUB__COMに設定します。以下にGithub Actionsの設定例を掲載します。

    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

AWS CodeBuild

Github Actionsとやっていることは同じです。Github Appsを用意し、GITHUB_APP_IDとGITHUB_APP_PRIVATE_KEYを使って、Access Tokenを取得します。ただしgithub actionsのように公開された再利用可能ワークフローがないため、自前でスクリプトを実行してtokenを取得します。

以下が、buildspec.ymlから呼ぶスクリプトです。

# 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);
  })

OpenAPI 定義ファイル分割のすゝめ

OpenAPIとは

RESTfulなWebサービスを記述、生成、利用、可視化するためのインターフェースファイルの仕様です。 以前はSwaggerフレームワークの一部でしたが、2016年にOpenAPI Initiativeが統括する独立プロジェクトとなりました。 Swaggerや他のいくつかのツールは、インターフェースファイルを指定してコード、ドキュメント、テストケースを生成することが可能です。

プロジェクトでの利用法

プレセナの一部プロジェクトでは、API定義をOpenAPI仕様に従って記述するだけでなく、定義ファイル(openapi.yaml)からコードを自動生成して利用しています。

OpenAPI定義ファイルの分割

肥大化するopenapi.yaml

当初はAPI定義ファイルであるopenapi.yamlの1ファイルに全ての記述をしていました。 結果として、下記のような問題が発生しました。

  1. ファイルサイズの増加(約2500行)

  2. コンフリクトの頻発

  3. 編集すべき記述を素早く見つけられない

これらの問題に対処するためにopenapi.yamlの分割をおこなうことにしました。

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 としました。

まとめ

schema.rbで差分が発生する事例とその復旧について

Railsには、スキーマファイルと呼ばれる schema.rb があります。

Active Recordはマイグレーションの時系列に沿ってスキーマを更新する方法を知っているので、履歴のどの時点からでも最新バージョンのスキーマに更新できます。Active Recordは db/schema.rb ファイルを更新し、データベースの最新の構造と一致するようにします。

とあるように、schema.rb はデータベース関連の機能で使われます。

その schema.rb ですが、チームで開発をしている時に

  • 自分の作業を中断して、プルリクエストのレビューをする

  • 自分の作業に戻って、開発を続ける

を繰り返している中で、意図しない差分が schema.rb に発生していると気づき、戸惑うことがあるかもしれません。

そこで、schema.rb に差分が発生してしまうことについて

  • 発生する事例

  • 差分が発生していると何が起こるか

  • どう復旧するか

を記事としてまとめます。

発生する事例

発生する事例として、「どのような流れで発生してしまうのか」を体験できるチュートリアルを用意しました。

チュートリアルは以下の流れで進めます。なお、Railsのバージョンは 7.0.4.2 を想定しています。

  1. mainブランチで、Railsのモデルを作成

  2. mainから派生した第2のブランチ(feature/add_unique_index)で、モデルの変更を伴うマイグレーションを適用

  3. mainから派生した第3のブランチ(feature/add_column)で、モデルの変更を伴うマイグレーションを適用

1. mainブランチで、Railsのモデルを作成

最小構成で rails new します。後ほどgemを追加するため、現時点では bundle install を行いません。

% bundle exec rails new schemaapp --minimal --skip-bundle

% cd schemaapp

今回の動作確認をRSpecで行うため、 Gemfile の末尾に追加します。

group :test do
  gem 'rspec-rails', '~> 6.0.1'
end

Gemfileの準備ができたため、一連のgemをインストールします。

% bundle install

RSpecのセットアップも行います。

% bin/rails g rspec:install

続いてモデルを用意します。今回は isbn 列を持つBookモデルとします。

% 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

ここまでをコミットします。 (もし vendor/bundle 以下をコミットしたくない場合は .gitignore に追加しておきます)

% git init
% git add .
% git commit -m "first commit"

2. mainから派生した第2のブランチ(feature/add_unique_index)で、モデルの変更を伴うマイグレーションを適用

main ブランチから第2のブランチ( feature/add_unique_index )を新しく作成し、「Bookの isbn にUNIQUE制約を追加する」ためのマイグレーションファイルを生成します。

% git checkout -b feature/add_unique_index

% bin/rails g migration AddIndexToBook

生成したマイグレーションファイルにて、Bookの isbnにUNIQUE制約を追加します。

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"

3. mainから派生した第3のブランチ(feature/add_column)で、モデルの変更を伴うマイグレーションを適用

ここからが、意図しない差分を発生させてしまう手順になります。

本来ならば、第2のブランチ(feature/add_unique_index)を離れる前にマイグレーションをロールバックします。

ただ、今回は schema.rbに差分を発生させるため、マイグレーションを適用したまま、

  • 第3のブランチ(feature/add_column)を新しく作成・切り替え

  • モデルに列 nameを追加

  • マイグレーションを適用

を行います。

% git checkout main

% git checkout -b feature/add_column

% bin/rails g migration AddColumnToBook name:string

% bin/rails db:migrate

続いて、マイグレーションの状態を確認します。作業2.のマイグレーションが適用されたまま、今回のマイグレーションも適用されているのが分かります。

% 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

schema.rb を見ると isbn 列にUNIQUE制約が定義されており、意図しない状態となっています。

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"

差分が発生していると何が起きるか

今回はテストコードを使って、何が起こるかを見ていきます。

「第3のブランチ(feature/add_column)ではまだUNIQUE制約を付けるマイグレーションがないから、テストはパスするだろう」と考え、以下のテストコードを書いたとします。

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

しかし、RSpecを実行すると

expected no Exception, got #<ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: books.isbn> with backtrace:

とテストが失敗してしまいます。

今回の場合、UNIQUE制約を追加するマイグレーションファイルが存在しない第3のブランチ(feature/add_column)でも、UNIQUE制約付きでテスト用のデータベースが作成されてしまった結果、テストが失敗しました。

どう復旧するか

今回は、以下の流れで復旧していきます。

  1. UNIQUE制約を追加した第2のブランチ(feature/add_unique_index)へ切り替え、UNIQUE制約のマイグレーション適用をロールバックする

  2. 第3のブランチ(feature/add_column)へ切り替え、 db:migrate を実行する

順に見ていきます。

1. UNIQUE制約を追加した第2のブランチ(feature/add_unique_index)へ切り替え、UNIQUE制約のマイグレーション適用をロールバックする

UNIQUE制約を追加した第2のブランチ( feature/add_unique_index )へ切り替え、マイグレーションの状況を確認します。

Migration IDに対するNameのうち、NO FILE と表示されていたものが Add index to book という表示へと切り替わりました。

% 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 **********

続いて、第2のブランチ( feature/add_unique_index )上で、不要なマイグレーション適用を元に戻します。

なお、 db:migrate:rollback コマンドでは1ステップずつしか戻せないので、バージョンを指定できるコマンドで戻します。

% bin/rails db:migrate:down VERSION=20230306104025

再度マイグレーションの状況を確認すると、当該Migration IDが down という未適用状態になりました。

% 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 **********

ただ、この時点では schema.rb に変更が入っていることから、第3のブランチ(feature/add_column)へ切り替えようとしても

% 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

と、エラーになってしまいます。

そこで、 schema.rb の変更を、当ブランチのコミット時点に戻しておきます。

% git reset HEAD db/schema.rb
% git checkout db/schema.rb

2. 第3のブランチ(feature/add_column)へ切り替え、 db:migrate を実行する

あらためて第3のブランチ(feature/add_column)へ切り替えます。

% git checkout feature/add_column

ここでマイグレーション適用状況を確認すると、第3のブランチ(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

ただ、まだ schema.rb にはUNIQUE制約の定義が残ったままになっています。

そこで、もう一回 db:migrate を実行し、 schema.rb を更新します。

% bin/rails db:migrate

ターミナルの実行結果には何も表示されないものの、git status ではschema.rb の更新が確認できます。

% 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

更新後の schema.rb を見ると、UNIQUE制約の定義が削除されています。

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

これでようやく schema.rb が復旧しました。

あとは忘れずに schema.rbを第3のブランチ(feature/add_column)へコミットすれば、復旧は完了です。

おわりに

今回は schema.rbに差分が発生する事例とその復旧方法を見てきました。

復旧するのには手間がかかることから、もしプルリクエストにマイグレーションファイルが含まれている場合は、schema.rb に余分な更新がないかチェックするのが良さそうです。

PostgreSQLにおける、削除行に対するロック獲得時の挙動

直感とは異なる挙動が観察されたため、記事として残します

環境

  • RDB

    • PostgreSQL 13.3

    • トランザクション分離レベル read committed (デフォルト)

  • 実験環境

    • IntelliJ IDEA 2024.3 (Ultimate Edition)

TLDR

PostgreSQLにおいて、トランザクション内で同一キーの行を delete - insert する場合、同時に実行される他のトランザクションからは当該の行が参照できない場合があります。

現象の説明

次のようなSQLを考えます。

SQL1
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. セッション1で5行目まで実行する。この時 delete によってロックが獲得される

  2. セッション2で4行目まで実行する。するとセッション1で行がロックされているため、待ち状態になる

  3. セッション1で7行目の commit まで実行する。これによりセッション1で獲得されていたロックが解放される

  4. セッション1のロックが解放されたことでセッション2の delete が実行される

この時、4 の結果セッション2では delete が空振りし、insert を実行するもすでに id = 1 の行が存在するため、 [23505] ERROR: duplicate key value violates unique constraint "users_pkey" となります。

比較実験

MySQLの場合

MySQL 8.0.28 で同様の実験を行ったところ、エラーは発生しませんでした。データの更新結果はcommitを後に実行している、セッション2の結果が保存されます。 MySQLのトランザクション分離レベルは repeatable read(デフォルト) です。

PostgreSQLでトランザクション分離レベルをrepeatable readとした場合

ひょっとしたら分離レベルに依存した挙動なのでは?ということでPostgreSQLにて分離レベルをrepeatable readとして実験を行いました。 結果前述4のステップで、以下のようなエラーが返されました。

[40001] ERROR: could not serialize access due to concurrent update

MySQLでトランザクション分離レベルをread committedとした場合

MySQLではrepeatable readの場合と挙動は変わらず、1行返却されました。

select for updateによるロック獲得の場合

PostgreSQLにおいて、delete 文ではなくselect for updateでロックを獲得した場合の挙動についても確認しました。

SQL2
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での挙動になります。 前述の通りMySQLでは異なる挙動となりましたので、RDBMSの実装に依存するようです。

ご興味があれば、ぜひお手持ちの環境でも試してみてください。

Rails Migrationチートシート

Rails歴が長い人でも、意外とmigrationの追加用のコマンドを覚えていられず、毎回調べていているので、実装で使ったもの・使いそうなものを少しずつ追加しています。

この記事は定期的に内容が追加される予定です。

よく使うパターン

空のマイグレーションファイルを作成する

以下のように、マイグレーションの名前を指定しながらrails generate コマンドを入力します。

% rails g migration AddXXXXsToSomeRecords

特定の型のカラムを既存のテーブルに追加する

以下のようにコマンドでカラムと型を指定する(YYY はテーブル名)か、

$ rails g migration AddXXXXToYYY カラム名:データ型
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 クラス)にも参照に使う外部キーと参照先のモデルの設定が必要になります(この記事での説明は省略します)。

Github ActionsでIAMロールを利用してAWSリソースを操作する

はじめに

弊社ではAWSリソースをTerraformやCloudFormationで管理し、plan/applyの実行にはGithub Actionsを使っています。本稿ではGithub ActionsからAWSリソースを操作するためのIAMロールの設定について説明します。

IAMロールを利用するメリット

Github ActionsからAWSリソースを操作するために利用する認証方法には以下のふたつが挙げられます。

  • アクセスキーおよびシークレットキー

  • IAMロール

アクセスキーによる方法にはセキュリティリスクが伴います。アクセスキーとシークレットキーがありさえすればどこからでも使え、漏洩すると不正利用される可能性があるためです。さらに、TerraformやCloudFormationによる処理に割り当てられるIAMポリシーは広大になりがちなので、漏洩した時の被害が甚大になる可能性があります。

一方、IAMロールを利用する方法では、一時的な認証情報(STSトークン)を発行することで、安全かつ柔軟にAWSリソースへアクセスできます。特に、GitHub ActionsのようなCI/CD環境では、OIDC(OpenID Connect)を活用することで、アクセスキーを不要にし、より安全な認証フローを実現できます。

設定手順

IAMロール(OIDC)の設定手順

OIDCプロバイダーをAWSに設定

IAMのメニューから「IDプロバイダ」を選択し「プロバイダを追加」をクリックします。

プロバイダのタイプで「OpenID Connect」を選択します。

以下を入力します。

  • URLにtoken.actions.githubusercontent.com

  • 対象者にsts.amazonaws.com

IAMロールの作成とポリシー設定

GitHub Actions用のIAMロールを作成します。

作成したロールに、必要十分なIAMポリシー(S3、CloudFormation、Lambda等)を設定します。

信頼ポリシーの設定(GitHubリポジトリを許可)

作成したロールの信頼関係タブで以下のように信頼ポリシーを設定します。PrincipalのARNやgithubのリポジトリ名は環境に合わせて変更してください。

GitHub ActionsでIAMロールを利用する

GitHub Actionsワークフローの更新

利用したいGithub Actionsのyamlに以下を記載します。

次に、Github ActionsのSecretsに AWS_ROLE_ARN を登録します。

AWS CLI / SDK での認証確認

これは必ずしも必須ではありませんが、Github Actionsが意図通りに動かない場合は、以下のコードをyamlに追記してIAMロールを適切に引き受けられているのかを確認すると良いです。

以上で設定完了です。アクセスキー方式より煩雑ではありますが、一度行えばセキュリティを保ってGithub Actionsを運用できます。

スイッチロールの設定手順

概要

AWS上で稼働するアプリが増えてくると、アプリごとにOrganizationを作りたくなります。この際、スイッチロールという機能を使うとOrganizationごとにIAMユーザーを作る必要がなくなり、ユーザー管理をシンプルにできます。本記事ではスイッチロールの方法について記載します。

以下、「スイッチ元Organization」および「スイッチ先Organization」のことを単に「スイッチ元」および「スイッチ先」と記載します。

スイッチ先ごとに一度だけやれば良い設定

スイッチ先での設定

管理者アカウントで、スイッチ先にログインします。

ロールを新規作成します。

「別のAWSアカウント」を選択し、アカウントIDにスイッチ元のAWSアカウントIDを入力します。「MFAが必要」にもチェックをつけた方が良いでしょう。

次に進み、スイッチ先で割り当てたい権限をつけます。この例では、Administrator権限を割り当てています。

最後にロール名をつけて、ロールの作成完了します。ロール名は「delegate_root_organization」といった分かりやすい名前が良いでしょう。

作成したロールのARNを控えておきます。

スイッチ元での設定

管理者アカウントで、スイッチ元にログインします。

ポリシーを新規作成します。Resourceには、スイッチ先で作成したロールのARNを入力します。

次へ進み、ポリシーの名前をつけて、ポリシーの作成を完了します。ポリシー名には「delegate_from_スイッチ先Organization名」といった分かりやすい名前が良いでしょう。また、複数のスイッチ先にスイッチできる権限を持つポリシーも作成できますが、スイッチ先ごとにポリシーを作成し各人が関わる必要最低限の権限を付与できるようにした方が良いでしょう。

スイッチ先を利用する各人が実施する必要のある設定

スイッチ元にログインします。

ポリシーの追加

スイッチしたいIAMユーザーに、「スイッチ元での設定」で作成したポリシーを追加します。

スイッチ実行

ではスイッチをしてみましょう。メニューから「ロールの切り替え」を選択します。

スイッチ先のアカウントID、ロール名、表示名を入力します。

「ロールの切り替え」ボタンをクリックすると、スイッチ先にログインしたのと同じ状態になります。

スイッチしていることは、右上のメニューで分かります。上部のバナー全体に色が変わるなどもう少し目立つと良いのですが。

スイッチ元に戻りたい場合は以下のようにメニューから選択します。

ActiveSupportのto_jsonメソッドの注意点

to_jsonメソッドの注意点

ActiveSupportには、to_jsonという便利なメソッドがあります。 Railsで開発しているときに使う場面としては、DBから取得したレコードをAPIのレスポンスとして返す場合があります。

出力される属性がこれだけなら、それほど大きな問題がないようにも見えます。しかし、userモデルには、サービスへの機能追加に伴ってプライベートな情報が追加されやすいため、そういった情報がto_jsonメソッドで出力されてしまったり、あるいは、本人以外のuser情報も併せて一覧で取得するような場合に、他人の emailやプライベートな情報が含まれてしまったりすると個人情報の漏洩問題になる可能性があります。

したがって、to_jsonメソッドで出力する属性を制限するべきかどうかについて、注意し検討する必要が出てきます。

解決策

幸い、to_jsonメソッドでは、オプションを指定することで、出力を絞り込むことができます。

methodsオプションは、特定の属性を絞り込むというよりかは、追加で情報を出力する用途に使いますが、関連する機能なので併せて説明します。

以下、各オプションの使用例を示します。

only

さきほどの例で、onlyを指定すると、以下のように指定した属性だけが出力されます。

except

さきほどの例で、exceptを指定すると、以下のように指定した属性以外のものが出力されます(※)。 ※読みやすくするために、改行を入れています。

include

さきほどの例で、includeを指定すると、以下のように指定したassociationの属性も併せて出力されます(※)。 ※ some_associationという属性がある前提です。

なお、includeで指定したassociation内でも出力する属性を制限したい場合は、以下のように書けます。

methods

さきほどの例で、methodsを指定すると、以下のように指定したメソッドの呼び出し結果も併せて出力されます(※)。 ※some_methodというメソッドがある前提です。

危険なJSON出力を禁止するRuboCopカスタムルールの作成方法

背景

上述のような危険な実装をコードレビューのみに頼らずに、Rubyの静的コード解析ツールであるRuboCopで機械的にチェックする方法を、当ページでは記載します。

手順

rubocop gemはインストール済みの前提で記載します。

チェック対象のコード

以下の4行目をエラーとすることを目標にします。

ASTの出力

rubocop gemを入れていれば使えるruby-parse というコマンドでASTを出力します。

検知したいのは、この

の部分です。また、renderメソッドの第二引数のstatusは省略可能な引数ですので、第二引数の有無にかかわらず検知できるようにしたいです。

カスタムルールの作成

  • renderメソッドを呼び出していて

  • その引数にはjsonという名前付き引数を指定しており

  • 第二引数以降は問わない

というマッチャを記述しlib/custom_cops/dangerous_render_json.rb として配置します。

次に、.rubocop.ymlにて、今作ったカスタムルールを読み込む設定を追記します。

動作確認

rubocopコマンドを実行して動作確認をします。

うまくいけば、以下のように、エラーとして検知できます。

スレッド (Ruby 3.1 リファレンスマニュアル)

Procfile ベースのプロセスマネージャーです ()。

参考:

に従い、 tmux と overmind をインストールします。

参考:

参考:

参考:

本サイトの更新情報は、で発信しています。ご確認ください。

なお、作業はピクシブ株式会社さんのを参考にしながら進めました。ありがとうございました。

Rails6.1から7.0へアップグレードを計画した段階で、Rails7.0系のリリースノートを読みました。

また、Railsガイドのアップグレードガイドの ました。

--conservative オプションにより、指定したgemと指定したgemが直接依存しているgemのみバージョンアップするようになります ()

次に、を見て各種設定ファイルの差分を確認しました。

ため、Springに関する設定だった

なお、調査をする際はさんの記事が参考になりました。ありがとうございました。

を引用します。

を読むと primary_abstract_class の方が良さそうでしたので、差し替えました。

のうち、気になった点は以下でした。

を読むと、server timing ミドルウェアは便利そうでしたので追加することにしました。

のうち、気になった点は以下でした。

config.active_support.report_deprecations = false については、 によると一括でdeprecation warningを消せるオプションでした。 ただ、できる限りdeprecation warningは表示したいため、 true にしておきました。

のうち、気になった点は以下でした。

この変更を提供したところ、 では CI = true が設定されていたことから config.eager_loadが true になってしまい、CI/CDまわりで不具合が出ました。

RailsDiffで差分が出たデフォルト値に、 config/application.rb の中のconfig.load_defaults があり、値が 7.0 に変わっていました ()。

まず、7.0 とした場合の影響については を参照しながら調査しました。

調査する中で、 に変更内容がまとまっていたため、参考にいたしました。ありがとうございます。

には

に詳細な記載がありました。

を読み、今回のシステムには影響しなかったため、デフォルト値の変更を受け入れました。

に詳細がありました。

本サイトの更新情報は、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の内容は以下のとおりです。クラスメソッドさんのより流用、改変しています。

本サイトの更新情報は、で発信しています。ご確認ください。

OpenAPIの正確な仕様については、↓から確認できます。

分割によって見通しがよく開発をすすめることができるようになりました。 Visual Studio Codeで開発しているなら、この拡張をいれることで$ref の記述からファイル参照もできるのでおすすめです。 しばらくこれで開発を進めてみて、課題感がまた出てきたらブラッシュアップしていきたいと思います。

には

これは、で解説があるように、テストでは schema.rb を見てテスト用のデータベースを作っているためです。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

これはPostgreSQLの仕様によるもので、 リピータブルリードトランザクションでは、トランザクションが開始された後に別のトランザクションによって更新されたデータは変更またはロックすることができないため とあります。

PostgreSQLでは削除データはすぐには物理削除されませんが、この辺りが関係しているような気もします。

あるいは、後、次のように、直接、change メソッド内にadd_column メソッドを記載します。

まず、。

本サイトの更新情報は、で発信しています。ご確認ください。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

しかし、を認証に使っているサービスなどで、何も考えずに、user.to_jsonのようなコードを書いてしまうと、以下のようなJSONがレスポンスに返されてしまいます。

option
内容

本サイトの更新情報は、で発信しています。ご確認ください。

userの情報を返すAPIを実装する際、render json: user とするとuserモデルのすべてのフィールドを含むJSONを返してしまい危険です。パスワードはハッシュ化されているものの、deviseが提供するフィールドlast_sign_in_ip などクライアントに返してはならない個人情報が含まれており、情報漏えいにつながってしまうためです。も参考にしてください。

基本的にはの手順通りです。

上記パターンにマッチするようにマッチャを記述します。_や...はワイルドカードです。詳細はをご確認ください。

本サイトの更新情報は、で発信しています。ご確認ください。

Githubリポジトリ
The Procfile | Heroku Dev Center
overmindのREADME
foreman - Procfile start processes in their own working directory - Stack Overflow
overmindの公式ドキュメント
Attach to process | RubyMine
Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
永久保存版Railsアップデートガイド
Railsの公式Blog
Rails 6.1からRails 7.0へのアップグレード
Bundlerの公式ドキュメント
6.1系と7.0系間のRailsDiff
Rails7から、Springがデフォルトに含まれなくなった
TechRacho
RailsDiffでの差分
TechRachoさんの記事
RailsDiffでの差分
TechRachoさんの記事
RailsDiffの差分
TechRachoさんの記事
RailsDiffの差分
Github Actionsのデフォルトの環境変数
該当箇所
Railsガイドの load_defaults の結果
rails7へのバージョンアップを安全に行うために使用するnew_framework_defaults_7_0.rbの各項目をさらっと解説 - Qiita
Railsガイド
edgeのRailsガイド
TechRachoさんの記事
edgeのRailsガイド
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
こちらのドキュメント
ドキュメント
参考
https://github.com/organizations/<your_organization_name>/settings/apps/new
https://github.com/organizations/<organization_name>/settings/installations/
ブログ記事
X(旧Twitter)の株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
https://swagger.io/specification/
https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi
Railsガイド
Railsガイド
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
https://www.postgresql.jp/docs/9.4/transaction-iso.html
https://www.postgresql.jp/docs/9.4/sql-vacuum.html
Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
空のマイグレーションファイルを作った
空のマイグレーションファイルを作ります
{
    "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"
                }
            }
        }
    ]
}
    - name: Assume AWS Role
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1
  - name: Verify identity
        run: aws sts get-caller-identity
{
  \"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\"
  }

only

特定の属性のみに絞り込む場合に使います。

except

特定の属性を除外したい場合に使います。

include

特定のassociation(has_manyやbelongs_toで指定しているような別のモデル)を出力に含めたい場合に使います。

methods

特定のメソッドを呼び出した結果を含めたい場合に使います。

> user.to_json(only: [:id])
=> "{\"id\":1111111}"
> 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\"
}
> 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\"}
}
> 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\"}
}
> 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\"}
}
# some_controller.rb
class SomeController < ApplicationController
  def some_method
    user = User.first
    render json: user, status: 200  # これを検知したい。statusパラメータは省略可能
  end
end
$ 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))))))
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
require:
  - rubocop-rails
  - rubocop-rspec
  - ./lib/custom_cops/dangerous_render_json
% bundle exec rubocop
・・・中略・・・
app/controllers/some_controller.rb:4:5: C: CustomCops/DangerousRenderJson: Do not use render json.
    render json: user, status: 200  # これを検知したい。statusパラメータは省略可能
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
devise
X(旧Twitter)の株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
こちらの記事
公式ドキュメント
ドキュメント
X(旧Twitter)の株式会社プレセナ・ストラテジック・パートナーズエンジニア公式

脆弱性診断 2社同時依頼実施記録

今回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アプリケーション診断とソースコード診断は診断の手法および視点が異なるものであり、出てくる指摘事項も異なるものとなります。 目的に応じてベストな診断は変わってくるかと思います。 適切な診断方法を選択するのが良いでしょう。

AWS CLIでのスイッチロールの設定手順

概要

開発中にスイッチロール先で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

準備: configファイルでの設定

スイッチ元のアカウントを aws configure で設定済みであれば、およそ以下のような設定になっているはずです。

~/.aws/confg
[default]
region = ap-northeast-1
output = json
~/.aws/credentials
[default]
aws_access_key_id = ***
aws_secret_access_key = ***

~/.aws/config に設定を追記し、以下のようにします。

~/.aws/config
[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

このとき、設定内容は以下のようにします。

  • プロファイル名(sw-staging, sw-production): 任意の名前で大丈夫です。CLIで利用するときにこの名前を指定します

  • source_profile: スイッチ元のプロファイルを指定します

  • role_arn: スイッチロールで引き受けるロールのARNを指定します

  • mfa_serial: スイッチ元でMFAを設定している場合、MFAデバイスのARNを設定します

実行方法

インタラクティブに実行できるかどうかで方法が変わってきます。

CLIをインタラクティブに実行するとき

ユーザー自身が aws s3 ls を実行する場合など、インタラクティブに実行できる場合は、以下の2通りの方法が利用できます。

  • --profile 引数にプロファイル名を指定

  • AWS_PROFILE 環境変数にプロファイル名を指定

例としては以下のとおりです。

% 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バケットが表示されます

個人的には、以下のように使い分けると便利かと思います。

  • 引数: チーム内で同じアカウントは同じ名前に統一しておけば、コマンドを共有するだけでスイッチロールして実行させることができます

  • 環境変数: シェルスクリプトにまとまっているときなど、コマンドを書き換えたくないときに便利です

CLIをバッチ実行する必要があるとき

role_wrapper.sh という名前で以下スクリプトを作成します(jq コマンドが必要です)。

role_wrapper.sh
#!/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

Storybookを利用したビジュアルリグレッションテスト

ビジュアルリグレッションテスト(Visual Regression Test)とは

ビジュアルリグレッションテスト(以下、VRT)は、画像回帰テストとも呼ばれます。

VRTは、改修による予期せぬ UI のデザイン崩れを検出することを目的としています。 UIのスクリーンショットを撮り、それらをコミット間で比較して、変更を特定します。

Storybookの運用イメージ

当社のReactを利用したプロジェクトではデザインシステムの構築にStorybookを利用しています。 細かい運用はこの記事では割愛しますが、見た目上のバリエーションが存在するコンポーネントについては、1つのコンポーネントにつき下記の2画面を用意するようにしています。

  • Basic: StorybookのControlsアドオンを利用して動作確認できる

  • All: バリエーションが一覧できる

VRTの導入手順

導入するツール

    • reg-keygen-git-hash-plugin: 比較すべきコミットを特定します。

    • reg-notify-github-plugin: GitHubのPRにレポートを通知します。

    • reg-publish-s3-plugin: 差分レポートをS3にアップロードします

これらのツールを組み合わせることで、Pull Requestにテスト結果の通知が届くようになります。 通知内のリンクから、さらに詳細なレポートを確認することもできます。

package.jsonの記述

yarnやnpmを利用して、パッケージのインストールをおこなってください。 npm-scriptsにも、CIで動かすためのタスクを追加します。

package.json
{
    ...
    "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 --serverCmd "start-storybook -p 9001" http://localhost:9001

CIツールによっては、タスク分割せずに実行が可能かもしれません。

regconfig.jsonの記述

reg-suitの設定ファイルです。

% npx reg-suit init

のコマンドでひな形を生成することができます。

regconfig.json
{
  "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 Actions ワークフローの記述

ワークフローをトリガーするGitHubイベントは、pull_requestではなく、pushである必要があります。

ubuntu-latestのイメージには、日本語が含まれていないため、日本語フォントインストールのジョブを入れています。Storybook上で日本語を利用していない場合は不要です。

.github/workflows/vrt.yml
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 }}

これらのID/KEYは、GitHubのsecretに登録する必要があります。 以上でVRTのために必要な設定は完了です。

画面幅の異なるスクリーンショットを撮る設定

上記の設定だけでもVRTとして正しく機能しますが、プレセナでは画面幅に応じた見た目の変化など細かくUIを確認できるようにしています。

.storybook/main.js
module.exports = {
  ...
  addons: [
    ...
    'storycap', // addonとして、storycapを追加
    ...
  ],
  ...
}
Header.stories.tsx
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',
      },
    },
  },
}

上記のような記述を追加することで、画像幅の異なるスクリーンショットを撮ることが可能になります。これによって、網羅性の高いテストを目指すことが可能です。

Validated を直列に処理したい

概要

cats に含まれる Validated は、複数の入力値のバリデーションを一つにまとめて返すことができる大変便利な型です。

業務において、入力値のバリデーションを行った値をさらに別の入力値として使いたいというケースがままあり、プロジェクトの新規参入者が悩むことがあるため備忘録として記します。

Validated の値を使って、別の 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 であるからで、公式ドキュメントに詳しくかかれていました。

解法:andThen メソッドを使う

andThen メソッドを使うことで Validated な値を直列に処理することができます。

val validatedAge = Age.validate(15).toValidatedNec
val validatedChild = validatedAge.andThen(age => Child.validate(age).toValidatedNec)

当然ですが、直列になるため Age でバリデーションエラーになった場合には Child のバリデーションは評価されません。 Age も Child も満たさない値 2000 で実行してみましょう。

console
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("もう大人です") は追加されていないことが確認できますね。

複数の Validated の値を使って、別の Validated を作りたい

一つ前の例は入力が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 がないのでコンパイルエラー

解法:一度 Either にする

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))

実行してみましょう。両方のバリデーションエラーが合成できていることを確認できます。

console
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 のバリデーションエラーだけが表示されてしまいます。

console
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(そんなに生きられません)))

利用したコード

コード
/build.sbt
import Dependencies._

ThisBuild / scalaVersion     := "2.13.10"
ThisBuild / version          := "0.1.0-SNAPSHOT"
ThisBuild / organization     := "com.example"
ThisBuild / organizationName := "example"

lazy val root = (project in file("."))
  .settings(
    name := "validated_sandbox",
    libraryDependencies ++= Seq(
      "org.typelevel"       %% "cats-core"                % "2.9.0",
      munit % Test
    )
  )
/src/main/scala/example/Main.scala
package example

import cats.data.ValidatedNec
import cats.implicits._

object ValidateTest extends App {
  /*
   * 通常パターン
   */
  val validatedAge = Age.validate(2000).toValidatedNec
  val validatedJob = Job.validate("").toValidatedNec

  val param: ValidatedNec[Error, InputParam] = (validatedAge, validatedJob).mapN(InputParam.apply)

  /*
   * ケース1:Validated の値を使って、別の Validated を作りたい場合
   */

  // ダメな例:flatMap がないのでこうは書けない
  val uncompilableValidatedChild = for {
    validatedAge <- Age.validate(15).toValidated // flatMap がないのでコンパイルエラー
    validatedChild <- Child.validate(validatedAge).toValidated
  } yield validatedChild

  // 解法:andThen を使う
  val validatedChild = validatedAge.andThen(age => Child.validate(age).toValidatedNec)

  /*
   * ケース2:複数の Validated から Validated を作りたい
   */

  // ダメな例: 入れ子になってしまうが、 flatMap 使えないしどうすれば...
  val nestedValidatedAdult = // ValidatedNec[Error, ValidatedNec[Error, Adult]]
    (validatedAge, validatedJob).mapN((age, job) => Adult.validate(age, job).toValidatedNec)
  val validatedAdult = nestedValidatedAdult.flatetn // コンパイルエラー

  // 解法: 一旦 Either にして flatMap してから Validated にもどす
  val validatedAdult1 = (validatedAge, validatedJob)
    .mapN((age, job) => Adult.validate(age, job).toValidatedNec.toEither) // ValidatedNec[Error, Either[NonEmptyChain[Error], Adult]]
    .withEither(_.flatten)
  val validatedAdult2 = (validatedAge, validatedJob)
    .mapN((age, job) => Adult.validate(age, job).toValidatedNec) // ValidatedNec[Error, ValidatedNec[Error, Adult]]
    .withEither(_.flatMap(_.toEither))

  // よくない解法: 並列に処理できるところも直列にしてしまう
  val badValidatedAdult = validatedAge
    .andThen(age => validatedJob
      .andThen(job => Adult.validate(age, job).toValidatedNec))
}

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] =
    Either.cond(age.age < 18, Child(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] =
    Either.cond(18 <= age.age, Adult(age, job), Error("まだ子供です"))
}

case class InputParam(age: Age, job: Job)

Heroku Postgresの運用でよく使うコマンド集

集

コマンド集

データベースのバックアップを保存する

% 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に復元できます。

ダウンロードしたバックアップをローカルの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コマンドを使えるようにしておく必要があります。

Same Origin PolicyとCORS

はじめに

最近、当社の開発では単一のホストだけでWebアプリケーションをホストせずに、フロントエンドとバックエンドを別のサーバーでホストすることが多くなってきました。

そして、これまでは遭遇してこなかったブラウザやJavaScriptに関するセキュリティの仕様に対処することが頻繁に起こるようになってきました。

この記事では、それらに対処する際に調べて把握した知識の一部であるSame Origin PolicyとCORSについてまとめます。

Originとは

2種類のOrigin

WhatWGの定義によると、Originには、以下の2種類があります。

  • An opaque origin

  • A tuple origin

An opaque Originは、定義によると内部的に使うオリジンで、オリジンをシリアライズして復元することはできず、シリアライズした場合はnullとなるようです。あまり利用イメージが湧きませんが、我々の普段のWeb開発ではあまり使うことはなさそうに思います。念の為に定義文を引用しておくと、以下です。

An opaque origin

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)

の組み合わせで構成されます。

Originの比較の例

Same Origin Policyとは

ブラウザにSame Origin Policyが実装されているため、リクエスト先のOriginで、後述のCORSの許可がされていなければ、XmlHttpReuqestやfecth API、<img>タグを使ったリクエストなどは、異なるOriginに対してはできません。

CORSとは

具体的には、Same Origin PolicyによってアクセスできないようなOriginをまたいだリソースに、あらかじめ、HTTPのヘッダをつけておくことで、アクセス可能にする仕組みです。

例えば、以下のようなHTTPのヘッダを使います。

ヘッダは、これ以外にもありますが、ここでは記載を省略します。

RailsアプリケーションにおけるCORSの設定

Railsアプリケーションでは、rack-cors というgemを使って簡単にCORSの設定を行うことができます。

インストール方法や設定方法は、上記公式サイトに記載されているので、ここでは記載を省略します。

Amazon SNS + Slack Workflowを使って、CloudWatch Alarmの通知をSlackチャンネルへ投稿する

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

では、実際に各サービスを設定していきます。

Eメール通知不要なSNSトピックを作成

CloudWatch Alarmを設定する際のウィザードではSNSトピックを作成可能です。 ただ、そのときに作るSNSトピックではEメール通知が必須となってしまいます。

そこで、今回は事前にEメール通知が不要なSNSトピックを作成します。その後、CloudWatch Alarmへ割り当てることにします。

今回のSNSトピックの設定は以下の通りです。 なお、残りの項目はデフォルトのままにします。要件に応じて変更してください。

CloudWatch Alarmを設定

続いて、CloudWatch Alarmを設定します。

今回必要なCloudWatch Alarmの設定は以下の通りです。

なお、設定する途中にあるEメールエンドポイントにて トピックにエンドポイントがありません と表示されますが、Eメール通知を行わない設定の場合はこのままで問題ありません。

ここまででAWS側の準備ができました。次はSlack Workflowの設定になります。

Slack WorkflowでSNSトピックのサブスクリプションを確認できるよう設定

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でサブスクリプション確認を可能にする設定は完了です。

SNSトピックにサブスクリプションを作成

再びAWSの設定へと戻り、SNSトピックにSlack Workflow向けのサブスクリプションを作成します。

上記で作成したSNSトピック hello_alarm_topic を表示し、 サブスクリプションの作成 をクリックします。

続いて、以下の設定を持つサブスクリプションを作成します。

なお、 rawメッセージ配信の有効化 をチェックしないことで、CloudWatch Alarmの情報はSNSの Message キーへと設定されます。一方、rawメッセージ配信を有効化してしまうと Message キーがなくなってしまうので、注意してください。

Slackチャンネルでサブスクリプション確認を行う

ここまででサブスクリプション確認向けの設定が完了したため、実際にサブスクリプション確認を行います。

今回の場合、SNSにサブスクリプションを作成した時点で、Slackチャンネルにサブスクリプション確認用URLが投稿されます。

投稿されたURLをクリックすると、ブラウザが開き、 ConfirmSubscriptionResponse のXMLが表示されます。

続いて、AWSのサブスクリプションページをリロードすると、ステータスが 確認済 になります。

これでサブスクリプション確認は完了です。

正式版にSlackへ投稿するメッセージを修正

サブスクリプションの確認ができたので、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を公開します。

AWS CLIによる動作確認

以上で全体の仕組みが完成したため、動作確認を行います。

動作確認をするためには 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よりもカスタマイズ可能としたい場合の参考となれば幸いです。

HerokuのStackの設定

Stackとは

Herokuで稼働するサーバーのベースとするOSのバージョンのようなものです。

Stack関連のコマンド

使用中のStackを確認

以下で、Stackの確認ができます。2021年10月現在は、heroku-20がデフォルトStackです。

使用するStackを変更

新しいStackに切り替えたい場合や、ステージング環境を作るときに本番に合わせて少し古めのStackを使いたい場合など、StackをデフォルトStackから変更したい場合は、以下のコマンドを使います。

なお、Stackのupgradeは、Herokuの管理者用のWeb画面からも変更できます。downgradeは画面からはできないようです。

SPF、DKIM、DMARCを使用した迷惑メール対策

はじめに

この記事では、メール送信サーバーを設定時に、ほぼ必ず設定することになるSPF、DKIM、DMARCについて、知識の整理と共有のために概要や効果、設定方法を紹介します。

SPF

SPFは、Sender Policy Frameworkの略で、ドメインの所有者が「自分のドメインから送信を許可するメールサーバー(IPアドレスやホスト名)」をDNSに公開する仕組みです。

SPFの効果

SPFの仕組みを使うことで、以下の効果が得られます。

  • 送信ドメインの正当性を受信側が検証できる

  • 攻撃者が特定のドメインを偽装して送ったメールを、受信者のサーバにブロック/減点評価させることができる

  • 送信元は自分が正しい送信元であることを証明することができ、受信側での検証をパスしやすくなり、メールの到達率が向上する

これらにより、送信側としては自社の「なりすまし(スプーフィング)」をした攻撃者のメールの到達率を減らすことができ、受信側は「迷惑メール」判定をしやすくなります。

システムの提供側からすると、結果的に、自社のユーザーがなりすましの被害に遭いにくくなり、また、自社のサービスのメールが迷惑メールとして判定されにくくなります。

SPFレコードの設定

SPFレコードは、DNSのTXTレコードとして以下のような文法で指定します。

設定例:

上記の例では、サーバーのIP (203.0.113.5/32)と Google Workspace 経由のみ許可(+を省略して設定)し、それ以外は拒否しています。

SPFを使った検証の流れ

SPFを使った受信側の検証は、以下のような流れで行われます。

  1. 送信ドメインの取得 受信側 は SMTP セッションの MAIL FROM / HELO などからドメイン名を取得

  2. DNS へ SPF 問い合わせ そのドメインの TXT レコードを検索するため、DNS へクエリを送信

  3. SPF レコード取得 DNS が返した v=spf1 … レコード(または CNAME 追跡先のレコード)を受け取る

  4. 評価して結果を反映 実際の送信 IP とSPFレコード内容を照合し、Pass/Fail などの判定

SendGridのAutomated Securityを利用する場合

システムからのメール送信でよく利用されるSendGridでは、Automated Securityという機能により、SPFレコードの登録を簡略化することができます。SPFレコードの設定では、同じ送信ドメインに対して複数レコードを登録するよくある間違いがあるため、そういった間違いを防ぐのが目的なのではないかと思われます。

Automated Securityを使う場合、ユーザーは以下のように自社のDNSにサブドメインのCNAMEレコードを登録します。

SendGrid側のDNSで、このu123456.em123.sendgrid.net に対して、適切なTXTレコードが登録されているため、SendGridのユーザーはCNAMEレコードの登録だけでSPFレコードの登録ができます。

SPF単体利用の弱点

SPFは、前述のフローでも記載したとおり、MAIL FROM / HELO などからドメイン名を取得するため、ヘッダーFromの詐称に対応できず、また、送信元が正しいかどうかだけしか判断しないため、内容の改ざんが行われているかは評価できません。

また、メールを転送してしまうと、転送サーバーがSPFレコードに定義されていないため、個人が転送した正当なメールやメーリングリストからの配信がFailする場合があります。

それらの問題を解決するために、SPFだけでなく、DKIMとDMARCが併せて使われます。

DKIM

DKIMの効果

DKIMの仕組みを使うことで以下の効果を得ることが出来ます。

  • 改ざん防止

    • ヘッダーや本文が送信元以外の通信途中で変更されていないことを証明

これらにより、SPFの弱点であった部分を補完することができ、システムやサービスの提供元から送られたメールが、受信側で適切に判定されるようになります。

その結果として、システムやサービスの利用ユーザーが第三者の改ざんによる攻撃をうけたとしても迷惑メールとして判定しやすくなり、提供元から送信されたメールは正しくユーザーに届くようになります。

DKIMレコードの設定

DKIMは、DNSのTXTレコードとして以下のような文法で指定します。主要な部分だけ説明します。

DKIM-Signatureヘッダー例:

DKIM-Signatureに含まれる主なタグの内容は以下の表の通りです。

DKIMを使った検証の流れ

DKIMを使った検証は以下のような流れで行われます。

  1. 送信時

    1. 送信元が選択したヘッダー・本文をハッシュ化

    2. 送信ドメインの秘密鍵で署名

    3. 署名結果を DKIM-Signature: ヘッダーとして追加

  2. 受信時

    1. 受信側 がDKIM-Signature: ヘッダーから セレクタ名 (s) とドメイン名 (d) を取得

    2. [セレクタ名]._domainkey.[ドメイン名] の TXT レコードを DNS で取得

  3. 検証

    1. 公開鍵で署名を検証し、Pass / Fail / TempError / PermError を判定

SendGridのAutomated Securityを利用する場合

SendGridでは、SPFと同様に、Automated Securityという機能によって、CNAMEを使ったDKIMの設定をすることができます。

仕組みはSPFと同様で、以下のようなCNAMEレコードを自社のDNSに登録すると、SendGrid側で登録されているDKIM用のTXTレコードを参照させることができます。おそらく、これもSendGridユーザーが誤登録をするのを防ぐための機能と思われます。

DKIM単体利用の弱点

仮にDKIMだけを使って迷惑メールかどうかを評価をすると以下の問題が発生します。

  1. 改ざんされたメールでDKIMでPassしてしまう

    1. 送信されたメールを攻撃者が改ざんし、さらに、改ざん時に攻撃者が管理するドメインの署名を付けることで、DKIM-Signatureには、d=attacker.com のようなタグで署名がつけられる。このとき、攻撃者がattacker.com のDNSに公開鍵を登録しておけば、改ざんされたメールのDKIM認証がPassし、迷惑メールと判定されずユーザーに到達してしまう

  2. メーリングリストなどでの転送時に、DKIMでPassしたメールがSPFでFailしてしまう

    • メーリングリストで転送された場合など、内容が改ざんされていないことをDKIMで証明したとしても、SPFとDKIMのそれぞれで独立して評価を行うと、送信元が変わってしまったことでSPF判定がFailとなり、迷惑メールのように扱われてしまう

1つ目の問題の場合、改ざん時に送信元も差し替わっているはずなので、SPFの情報を併せて評価に使えれば、攻撃を検出しやすくなります。

2つ目の問題の場合、SPFでFailになった場合にDKIMの情報で補完すれば、攻撃による変更ではないことが評価可能です。

上記のような対応をするために、SPFとDKIMの情報を併せて評価するのがDMARCです。

DMARC

DMARCの効果

前述のSPF、DKIMをそれぞれ単体で判定に使った場合と比べて、DMARCを使うことで以下の効果を得られます。

  • 途中で内容が改ざんされ、しかし、DKIM単体ではPassしている場合でも、迷惑メールと判定することが可能

  • メーリングリストで転送され、SPFがPassしない場合でもDKIMの情報に基づいて正しいメールと判定可能

  • ユーザーへの影響を考慮しつつ少しずつ判定を強化していく運用が可能

DMARCレコードの設定

DMARCレコードは以下のようにTXTレコードをDNSに設定します。

adkimとaspfのAlignmentとは、文字通り「調整」の意味です。SPF、DKIMをどのような判定方法で強調動作に使うか、その調整方法を定義します。

DMARCを使った検証の流れ

  1. メール受信

    1. SPF と DKIM をまず評価

  2. Alignment 判定

    1. SPF Pass かつ SPF ドメイン≒From ドメインならSPF Alignment判定をPass

    2. DKIM Pass かつ 署名ドメイン≒From ドメインならDKIM Alignment判定をPass

    3. aとbのどちらかのAlignment判定をPassすればDMARCをPassと判定

  3. ポリシー適用 (p=)

    1. DMARC Fail なら none / quarantine / reject の指示に従う

  4. レポート送信

    1. 集計レポート(RUA)

    2. 詳細レポート(RUF)

検証例:

以下のようなケースで、DMARCの検証がどのようになるかをまとめます。

  • 途中で改ざん(DKIM単体はPass)

    • 途中で内容が改ざんされ、しかし、改ざん者によるDKIM署名の公開により、DKIM単体ではPassしている場合

  • 途中で転送(SPF単体はFail)

    • メーリングリストで転送され、SPFがPassしない場合

上記のケースがDMARCの判定ではどうなるかをまとめると次の表のようになります。

攻撃者に途中で改ざんされた場合がFailになり、攻撃ではない転送がPassになるのが確認できます。

DMARCポリシー運用のベストプラクティス

DMARCのポリシーは、たとえば、何の検証も行わずにすべてrejectにしてしまうと、予期しない悪影響が発生してしまいます。

その状況を緩和し、運用を少しずつ厳格化していくために、以下のような導入ステップが一般的にベストプラクティスとされています。

  1. SPF / DKIM を 設定する

  2. _dmarc に p=none + rua= で 観測開始

  3. レポートで Fail 要因を潰し、pct= を使って一部トラフィックのみを quarantine へ変更して影響を確認

  4. 問題がなくなったら p=reject + pct=100 で 本格運用

DMARCでも防げない問題

DMARCを使ったとしても、たとえば、メーリングリストが転送時に本文やヘッダーの一部分に(悪意のない)転送時の軽微な情報の追加を行う場合は、DKIMの署名が無効化されて改ざん扱いになってしまい、DMARCにおけるDKIM AlignmentもPassしなくなってしまいます。

OpenAPI-Specification/petstore.yaml at main · OAI/OpenAPI-SpecificationGitHub

にて、AWSコンソールでのスイッチロールの方法について記載しました。

また、を利用すると aws-vault exec を使って同様のことが実現できます。をご参照ください。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

が存在しますが、プレセナでは下記のツールを利用しています。 以下、VRTを導入していく手順を紹介します。

: Storybookをクロールし、スクリーンショット画像を取得します。

: 画像の差分をレポートとして出力してくれます。

storycapの公式が推奨するコマンドは下記のため、 ( )

REG_NOTICE_CLIENT_IDは、を参考に、 GitHubにreg-suitアプリを追加して取得してください。

AWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYは、regconfig.jsonに記載したbucketにアクセスできるものをIAMなどで作成してください。 ポリシーなどの詳細は、を参考にしてください。 S3の細かい設定はこの記事では割愛します。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

すべてに記載されている内容ですが、よく使うコマンドをすぐに参照できるように、一覧としてまとめます。

( 公式サイトの説明ページは、)

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

に詳細が記載されていますが、Webのセキュリティモデルの基本となるものです。Originを共有するWebのリソースは互いに信用でき、それらの著作者(著作組織)も基本的には同じであると考えます。

An internal value, with no serialization it can be recreated from (it is serialized as "null" per ), for which the only meaningful operation is testing for equality.

引用:

(日本語訳サイトは)に詳細な説明がありますが、要するに、特定のOriginのページ(html)やスクリプト(JavaScript)が、別のOriginのリソース(APIやページなど)にアクセスするのを防ぐブラウザの仕組みです。

Cross Origin Resource Sharingの略で、 (日本語訳サイトは)や (日本語訳サイトは)に詳しい説明が記載されています。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

項目
値

なお、今回は通知さえ飛ばせればどんなメトリクスでも構いません。AWSドキュメントで紹介されているチュートリアルを参考にメトリクスを設定しても良いでしょう。

項目
値
項目名
値

項目
値

なお、AWS Vaultについての詳細は、を参照してください。

続いてAWS CLIを使ってアラーム状態にします。

を見ると分かりますが、Ubuntu Linuxのバージョン番号とStack名が揃えられているようです。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

2006年にで定義され、で最新版が定義されています。

SPF構成要素
内容

なお、になっており、迷惑メール扱いになる可能性があります。

DKIMは、DomainKeys Identified Mailの略で、公開鍵暗号方式を使ってメールの内容が送信元以外に改ざんされていないことを保証するための仕組みです。で定義されています。

DKIM構成要素
内容
タグ
内容

SPF、DKIMの判定結果を束ねて判断し、失敗時の処理とレポート方法をドメイン所有者側が宣言する仕組みです。、で仕様が定義されています。

DMARC構成要素
内容
ケース
SPF
SPF Alignment
DKIM
DKIM Alignment
DMARC

このようなケースでは、 を ML サーバーが付与するなどの方法がとられますが、この記事ではその詳細の説明を省略することにします。

本サイトの更新情報は、Xので発信しています。ご確認ください。

スイッチロールの設定手順
aws-vault
別記事
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
多くのツール
Storycap
reg-suit
https://github.com/reg-viz/storycap#getting-started
公式のREADME
公式のREADME
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
https://typelevel.org/cats/datatypes/validated.html#of-flatmaps-and-eithers
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
公式サイト
こちら
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント

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)

HTTPヘッダ

ヘッダに記載する内容

Access-Control-Allow-Origin

どのオリジンからのアクセスを許可するか。* も指定可能。

Access-Control-Allow-Methods

どのHTTPメソッドからのアクセスを許可するか。*も指定可能。

タイプ

スタンダード

名前

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
% heroku stack --app (自分のheroku app名)
=== ⬢ (自分のheroku app名) Available Stacks
  container
  heroku-18
* heroku-20
% heroku stack:set heroku-18 --app (自分のheroku app名)
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)

IoT

RubyKaigi

WhatWGの定義
serialization of an origin
https://html.spec.whatwg.org/multipage/origin.html#concept-origin-opaque
https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
こちら
https://developer.mozilla.org/en-US/docs/Glossary/CORS
こちら
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
こちら
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
amazon web services - Confirming AWS SNS Topic Subscription for Slack Webhook - Stack Overflow
アラームのユースケースと例 - Amazon CloudWatch
ステップ 3: サブスクリプションを確認する - Amazon Simple Notification Service
Amazon SNS raw メッセージの配信 - Amazon Simple Notification Service
AWS Vaultを使ったスイッチロール設定手順 | Precena Tech Book
set-alarm-state — AWS CLI 1.34.10 Command Reference
公式サイト
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
RFC4408
RFC7208
同じドメイン名に対して、複数のTXTレコードを登録することは、SPFレコードの検証時にPermErrorが発生する仕様
RFC6376
RFC7489
RFC8616
ARC (Authenticated Received Chain)
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式

AWS Vaultを使ったスイッチロール設定手順

背景

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にスイッチロールした状態になります。

lambrollでAWS Lambda関数をデプロイしたときのTips

前置き

AWS Lambda関数について、

  • ソースコードはgitで管理したい

  • ソースコードのデプロイは容易に行いたい

  • AWSの各リソースはTerraformで管理しており、別途Lambda向けのものを作る必要はない

lambrollはREADMEが充実していることもあり、悩むところは少なくてすみました。

それでも、組織のAWS環境で lambroll を使う場合にはいくつか考慮することがあったため、この記事で紹介していきます。

 

スイッチロール + MFAなAWS環境にてlambrollを使うには

スイッチロール時にMFAを利用するAWS環境の場合、lambrollだけではデプロイすることができません。

 

実際に見ていきます。

まず、 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ではLambda Layerを作成することができません。

  実際に見ていきます。

最初に、 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を使っていてエラーが出たときは

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 SESメールボックスシミュレーターにて、カスタムヘッダや添付ファイル付きのテストEメールを送信する

背景

AWS SESでメール送信環境を構築したときは、合わせて メールが不達になったこと を検知する仕組みも構築します。 AWS SESから送信したメールでメール不達があまりにも多いと、AWS SESの利用が停止されるためです。

  さらに、AWS SESの本番運用を始める前に、メール不達を検知する仕組みの動作確認も大切です。

シミュレーターには バウンス や 苦情 などのシナリオが用意されています。

このシナリオを使えば、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 (拒否)イベントとは何か」 から記載します。

rejectイベントについて、AWSドキュメントの記載は以下です。

拒否イベントのテスト

Amazon SES を介して送信するすべてのメッセージでウイルスがスキャンされます。ウイルスを含むメッセージを送信すると、Amazon SES はメッセージを受け入れ、ウイルスを検出して、そのメッセージ全体を拒否します。Amazon SES でメッセージが拒否されると、メッセージの処理が停止され、受取人のメールサーバーへのメッセージ配信は試行されません。次に、拒否イベントが生成されます。

 

  ただ、「実際にウィルスを含むファイルを作成し、AWS SESでそのファイルを添付してメールを送信する」を試すのは色々問題があります。

その問題を避けるため、AWSドキュメントにあるように、AWS SESでは

拒否イベントは、欧州コンピューターウイルス対策研究所(EICAR)テストファイルを使用してテストできます

という方法で reject イベントの発生をテストできそうです。

 

しかし、シミュレーターではEICARの内容をそのまま添付することはできません。

  文字列を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メッセージをゼロから作るのは手間がかかります。

  • カスタムメールヘッダ 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 が含まれる

というメッセージを受信できました。

  以上より、シミュレーターのシナリオとして用意されていないケースであっても、シミュレーターで試せると分かりました。

 

EMConfJP2025_参加レポート

EMconfJP2025の参加レポート

掲題の通りEMConfJP2025に参加してきました。

テーマは「増幅」と「触媒」。

懇親会付きのチケットも当日分のチケットもすぐに売り切れて、当日も大盛況だったと思います。自分が聞いた限りの講演のテーマとしては「事業・経営戦略と技術戦略の接合」に関してが多かった印象です。

聴講した講演について感想などを簡単に書いていきます。

当日について

当日は朝から連続して講演を聞いていたため、休憩時間は企業ブースに回るなど、慌ただしく過ごしました。スタンプラリーを埋めて景品のTシャツが欲しかったのですが、アンカンファレンスには参加できなかったことと、会場で知り合った方とプレーリーカードを掲げてチェキを撮ったりができなかったので、途中で断念しました。

基調講演:エンジニアリングマネージャーのロードマップ

広木大地さんによる講演で、内容としては「エンジニアリング組織論への招待」に加えて、EMについてと今の潮流と合わせたものとなっていました。正直、この基調講演だけでもチケット代の価値はあったな〜と思えるくらい良い講演でした。

Engineering Managementの4つのP : People/Product/Project/Platform(以前はテクノロジーだった)にまとめられてとてもわかりやすい整理だと思いました。そして「すべてを一人でできる必要はない、スーパーマンになる必要はない」とのことです。

重要なのは何が必要かを正しく理解し、周囲から「調達」してくる力であり、そのために自分自身がその全てを出来る必要はないが、内容をきちんと「わかっている」必要はある​

一番のサビはやはり、『エンジニアリングとは「実現する」ことで、マネジメントとは「なんとかする」ことで、EMの役割は「エンジニアリングマネジメントは価値実現のためになんとかする」ことである』、だったと思います。おそらくみなさんに響いていて、他の登壇者のスライドも当日書き加えられるほどでした。

Potential EM制度を始めた理由、そして2年後にやめた理由

EMを増やすためにEMを目指すハードルを下げつつ、育成に投資をしながら実際に生み出すことを目指してPotential EM制度という形をとったようです。個人的にとても良い制度だと思ったのですが、組織課題解決のための施策は「状況によって正解が変わる」という点が面白かったです。

ベストプラクティスやn=1の方法論が機能するかはケースバイケースというのは当たり前ではあるのですが、組織の時間軸によっても変化するというのは新鮮な視点でした。

エンジニアリング価値を黒字化する、バリューベース戦略を用いた技術戦略策定の道のり

バリューベース戦略というフレームワークを用いて、エンジニアリング組織の戦略を事業・経営戦略に翻訳する、という試みの紹介でした。戦略が会社と組織とでアラインしているというのが重要なのですが、エンジニアリング観点からはそういった定量・定性の観点に起こすことがとても困難であり、どの組織も抱える悩みだと思います。

こういった「翻訳することで橋渡しになる」のもEMの重要な役割であり、そのために多種多様な領域や膨大な役割を持ちあわせる必要があるのだと感じました。

1行のコードから社会課題の解決へ: EMの探究、事業・技術・組織を紡ぐ実践知

事業価値とエンジニアリングについて「事業軸・技術軸・組織軸」の3軸で切り分け、EMに求められる視点と実践的な内容を踏まえた講演でした。

こちらの講演も、戦略をエンジニア組織に下ろす・組織から上げる、といういかにして「翻訳」をするか、というテーマでした。

財務観点もさることながら、開発生産性のFourKeysとQCDの観測を継続し、改善に繋げている組織はすごいと思いました。また資料の密度が凄まじく、時間がある時にじっくりと見直したいです。

Two Blades, One Journey: Engineering While Managing

EMを志す人にとって「マクロな課題とミクロな不安」という切り口はわかりやすく整理されていて良いと思いました。EM(マネージャー)とIC(プレイヤー)のどちらの経験もそれぞれ生きる、と頭ではなんとなくわかっていても、それでも尽きない不安とどう向き合うかについての考え方や姿勢を学べました。

「共創型エンジニアリングマネジメント」の挑戦と実践

組織と事業の規模拡大に伴い、エンジニアリングマネジメントグループを創設し、フィーチャー型組織から職能横断型へと変更して、それぞれのチームにEMを設けるようにした、とのことです。規模も大きく、かなりドラスティックな変化だったようです。

最後の「組織的な課題解決策が次の組織的課題の要因となることを認識すること」というメッセージは、マネジメントや組織経営の難しさを表していると感じました。

サバイバルモード下でのエンジニアリングマネジメント

個人的に一番刺さった・感動した講演でした。

離職も多くなっていき、収益的にも厳しい事業の中でのVPoEの経験を赤裸々に語っておられました。前職のスタートアップに居た時の似た状況を思い出して、当時はメンバーレベルで力及ばない場面も多く、最終的には辞める決断になりましたが、その時のマネージャーの気持ちを知ったような感覚を得ました。

その時にできる引き出しがいち開発者の領域を出ない・技術者としてもまだまだといった状態で、もっとできることがあったら生き延びれたのだろうか、といような思いを馳せました。

基調講演:n=1の経験が紡ぐエンジニアリングマネジメントの可能性

組織のアウトプットの方程式で、メンバーの能力と熱量をいかにして上げつつ、受ける制約や摩擦をいかに減らすか、という観点が面白かったです。「メンバーが向かうべき方向を揃えないと、頑張ったところで役に立たないもの(力を結集して作ったゴミ)ができてしまう」には笑ってしまいましたが、気をつけてないとしばしば起きうる事象だと思います。

またカンファレンスの使い方としては「実は廊下で喋ることが大事」とおっしゃっていて、自分は講演を聞くだけにとどまっていたので、次からは改善したいです。

まとめと個人的感想

とても楽しくも学びのあるEMカンファレンスでした。

本当は懇親会にも参加したかったのですが、チケットが一瞬で売り切れてしまったため別の公式懇親会に参加しました。場所は会場近くの「もうやんカレー」で、話に聞いてたものの初めて行ったのですがとても美味しかったです。

EM界隈のコミュニティは活性化している印象ですが、それでもEMの数が各所足りてない様子でした。懇親会にて日々の課題などを話している中で、試してみたい解決策が見つかったり、方向性が決まるといった発見もあったようです。社外のこういった場が大切なことを再確認できました。

当たり前ですが、登壇者の方々はたくさん本を読んでかつ実践もされていて、「単純にすごい」と刺激を受けることができました。また「世界はシステムで動く」というシステム思考に関する書籍への言及・引用が、異なる講演で3回くらいあったので翌日にはポチってしまいました。他にも読んでおくべき本がたくさん見つかり、またしても積読が捗りそうです。引き続きキャッチアップ等を進めていきます。

このたびの素晴らしいイベントを支えてくださった関係者のみなさまに、深く感謝いたします。ありがとうございました!

問い合わせフォームの項目をサービスごとに出し分け、各サービス担当者に自動で振り分けてメールで通知する

はじめに

Zendeskにおいて、社内の複数サービスの問い合わせフォームを一本化し、そこに送られてきた問い合わせを対象サービスの担当者に自動で振り分けてメールで通知するための設定について記載します。

​前提

  • Zendesk環境

実現したいこと

弊社の以下3サービスの問い合わせフォームをZendeskで一本化します。

  • PLS(eラーニング事業)

  • プレセナアカデミー(toC向け研修事業)

  • Nest(研修管理システム)

また、サービスによってフォームの入力項目を自動的に変更し、各サービスごとに適切な情報を収集できるようにします。 具体的には、「対象サービス」「お名前」「ご利用環境」など全サービス共通で入力してほしい項目は常にフォームに表示しますが、 各サービスにおいて独自で入力してほしい項目(例えば「ユーザーID」など)については、 フォーム内でそのサービスが選択された場合のみ表示するようにします。

さらに、フォームから受け付けた問い合わせを自動的に対象サービスの運用担当者に振り分けた後、メールで担当者に通知するようにします。

なお、以下の手順では、PLSを設定する際の流れを記載します。

設定手順

STEP1:フォームをサービスごとに出し分けできるようにする

  1. カスタムチケットフィールドを作成する

フォームの質問項目を検討する

今回の場合、作成したいチケットフィールドの種類は大きく分けて二つあります。

  • どのサービスにも共通して必要なもの

  • あるサービスのみに必要なもの

上記を踏まえ、以下のチケットフィールドを作成することとします。

  • 対象サービス

  • 所属企業名

  • お名前

  • ご利用環境

  • PLS用フォーム

なお、チケットの件名や説明のフィールドは、標準フィールドとして既に存在しますので、新たに作成する必要はありません。

チケットフィールドを設置する

まずは管理センターのサイドバーにある[オブジェクトとルール]をクリックし、[チケット]の下の[フィールド]を選択後、右上の[フィールドを追加]をクリックします。

ここでの注意点として、[権限]で選択するオプションは[顧客は編集可能]にします。

今回の場合、フォームの内容はすべて顧客に入力してもらうチケットフィールドしかないからです。チーム内での管理のためにエージェントのみが入力できるチケットフィールドを作成する場合は[エージェントは編集可能]を選択します。

また、[顧客は編集可能]を選択することで、ページの下部に以下が表示され、リクエストの送信時に入力を必須とするかどうか選択することができます。

入力が完了したら忘れずに[保存]します。

  1. 問い合わせフォームを作成する

上記で作成したチケットフィールドから必要なものをピックアップしてフォームを作っていきます。

まずは管理センターのサイドバーにある[オブジェクトとルール]をクリックし、[チケット]の下の[フォーム]を選択後、[フォームを追加]をクリックします。

フォームのタイトルを適切なものに設定します。

[利用可能なチケットフィールド]からフォームに追加したいチケットフィールドの[+]を選択します。 チケットフィールド「対象サービス」の選択内容によって他のチケットフィールドを出し分ける設定は後ほど行いますので、ここではフォームの中に含めたいすべての項目を選択しておきます。

なお、選択したチケットフィールドの並べ替えも可能です。 以下赤枠内のアイコンをドラッグすることで並べ替えができます。

フォームに盛り込むチケットフィールドの選択が完了したら忘れずに[保存]します。

  1. 作成したフォームに条件を設定する

チケットフィールド「対象サービス」で選択する内容によって、フォーム内に表示されるチケットフィールドが切り替わるように設定します。 具体的には、「対象サービス」で「PLS」を選んだ場合、PLS用のチケットフィールドがフォームに表示されるようにします。

設定方法

まずは管理センターのサイドバーにある[オブジェクトとルール]をクリックし、[チケット]の下の[フォーム]を選択後、条件を追加したいチケットフォームの右側の[︙]から[条件]を選択します。

[条件の適用対象]を[エンドユーザー]にし、[条件を追加]ボタンをクリックします。

表示されるダイアログボックスで以下の画像のように設定します。 これで、チケットフィールド「対象サービス」で「PLS」が選択されたら「所属企業名」「ご利用環境」「PLS用フォーム」「お名前」(+フィールド設定で必須入力としているもの)のフィールドを表示できるようになります。

なお、チェックボックスにて必須とするかどうかも忘れずに設定します。 チケットフィールドで設定した必須要件よりも、こちらの条件設定チケットフィールドの内容が優先されるため、フォーム上で入力必須としたいものはここでも設定しておく必要があるからです。 ここで必須にチェックを入れない場合、フィールドでは必須に設定していても、実際にフォームで入力する際に必須とならないので注意してください。

入力が完了したら忘れずに[保存]します。

STEP2:受信した問い合わせを対象サービスの担当者に振り分けてメールで通知する

  1. 振り分け先および通知先となるグループを作成する

管理センターのサイドバーにある[メンバー]をクリックし、[チーム]の下の[グループ]を選択後、画面右上の[グループを追加]をクリックします。

[グループ名][グループメンバー]などを設定します。 今回の場合、[グループ名]にはPLS、[グループメンバー]にはPLSの担当者を追加しておきます。

入力が完了したら忘れずに[保存]します。

  1. 振り分けとメール通知を発動する条件を設定する

管理センターのサイドバーにある[オブジェクトとルール]をクリックし、[ビジネスルール]の下の[トリガ]を選択後、[トリガを作成]をクリックします。

[トリガ名][カテゴリ]などを設定します。

どんな時にアクションを実行するかを[条件]で設定します。 条件は複数設定が可能です。 なお、条件に当てはまった際のアクションは後で設定しますので、ここでは設定しません。

今回は以下のように設定しました。

  • チケットフィールド「対象サービス」で「PLS」が選択されていること

  • チケットのステータスが新規であること

  1. 振り分けとメール通知を実行するよう設定する

上記で設定した条件が満たされたときに実行される[アクション]を設定します。 アクションは複数設定できます。

今回は以下のように設定しました。

  • 問い合わせチケットを「PLS」というグループに割り当てる

  • 「PLS」というグループに所属しているメンバーにメールで通知する

なお、メールで通知する際の件名や本文はカスタマイズすることができます。 本文の{{ticket.url}}の部分をプレースホルダといい、チケットに関する様々な情報を取得することが可能です。 プレースホルダは[メールの本文]の入力欄の下にある[使用可能なプレースホルダ]からコピーすることができます。

入力が完了したら忘れずに[保存]します。

これで、以下が完了しました。

  • 一つの問い合わせフォームでサービスごとに項目を出し分ける動的フォームの作成

  • 問い合わせ対象サービスの担当者に自動でチケットを振り分けてメールで通知する設定

参考ページ

コンテンツ執筆時のルール

ルールを設定する目的は、コンテンツ全体の統一感を出したり、定まったルールの元でコンテンツを書くことで、読者にとって読みやすいコンテンツにすることです。

しかし、コンテンツを書くためのハードルを上げたくもないので、ルールを多く管理したくもありません。ルールの設定は、必要最小限に抑えるようにしましょう。

なお、このページに記載するルールは基本原則ではありますが、記事の特性上ルールから外した書き方をしたい場合は、従う必要はありません。

以下、各ルールごとに説明を記載します。

コマンドラインで実行するコードを記載するときはプロンプトを記載する

例えば、次のようなコード例におけるルールです。

% rbenv install 2.7.3

この例では、zshのデフォルトのプロンプトを記載していますが、コマンドラインで実行するコードでは、それが分かるように、プロンプトを記載します。

bashを普段使っている人が記事を書くときに、bashのプロンプトを記載するのはOKとします。

また、インフラ系の記事などでは、rootユーザーのプロンプトを明示的に記載したい場合もあると思いますが、その場合は、[root]# などをプロンプトとして記載します。

見出しを使うときは、「Heading 1」から順番に使う

上の図のように、gitbook.comでは、見出しのスタイルとして、Heading1、Heading2、Heading3が使えますが、これは必ずしもH1タグ、H2タグ、H3タグになるわけではありません。

目次を適切に管理しやすくするために、記事上の見出しは、Heading1から使い、その内部で区切りをつけたい場合は、Heading2、Heading3と階層を下げてコンテンツを記載してください。

リンクの文字列の書式は太字にする

gitbook.comではリンクに下線がつかないことから、リンクに気付きにくいです。

そこで、リンクの文字列の書式は、以下の例のように太字としてください。

Raspberry Pi + PaSoRi + Python で、勤怠打刻マシンを作ってみた

前置き

最近、当社の勤怠システムが更改されるとともに、勤怠打刻のWeb APIも公開されました。

すると、エンジニア間で「わたしのかんがえたさいきょうのきんたい」ブームが起き、いろいろな勤怠打刻方法が生み出されました。

今回は、私の作成した Raspberry Pi + PaSoRi + Python の勤怠打刻マシンをご紹介します。

仕様について

個人的に勤怠打刻に欲しい機能として

  • タイムカードボックスからタイムカードを取り出して打刻

    • 物理タイムレコーダーと同じような操作感がほしいため

  • 「打刻したら音声で挨拶する」「打刻したらSlackで通知する」

    • リモートワークしてても物理出退勤してる感を出したいため

があります。

そこで、Raspberry Piと PaSoRi と FeliCa を使って

  1. 無印のタイムカードボックスからFeliCaを取り出す

  2. FeliCaをPaSoRiにタッチする

  3. PaSoRiのつながっているRaspberry Piが反応し、Web APIで打刻する

  4. 打刻に成功したら、Raspberry Piに接続したスピーカーから音声を出す

  5. Slack API で打刻したことを通知する

  6. 出勤 と書かれたタイムカードボックスに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付属)

      • PasoRiでFeliCaを読むときに使用

勤怠打刻マシンの外観です。

手前の黒いPaSoRiにFeliCaをタッチし、出退勤を打刻します。

設定手順

Slack appの準備

今回、Slack botからSlack通知をするために、Slack appを準備します。

  • Bot tokens を使うSlack appを作成する

    • OAuth & PermissionsのScopesは chat:write

ラズパイのセットアップ

勤怠打刻用のラズパイをセットアップします。

  • SSHを可能にする

  • IPアドレスを固定化する

  • mp3ファイルを再生できるよう、 mpg321 をaptでインストールする

  • /home/pi/projects/dakoku_pi/ ディレクトリを作成する

    • このディレクトリに打刻用プログラムファイルを入れる

Pythonまわりの準備

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  # 退勤する時の音声ファイル

Python環境の準備

必要なライブラリをインストールします。

なお、Slackのトークンなどの秘匿情報はハードコーディングせず、 .env ファイルに記載して python-dotenv で環境変数へロードすることとします。

% pip install requests python-dotenv slack_sdk nfcpy

slack-sdkのラッパーを作成 (slack.py)

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
    )

共通の機能をまとめた親クラスを作成 (dakoku/base.py)

再掲となりますが、このクラスでは

  • 打刻する を呼び出す

  • 音声を出す

  • 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)

打刻方法を実装した子クラスを作成 (dakoku/dakoku.py)

DakokuBase を継承し、 clock メソッドを実装します。

社内の勤怠システムに依存するためここでは公開できませんが、 clock メソッドを実装します。

class Dakoku(DakokuBase):
    def clock(self) -> Optional[AvailableTypes]:
        # 打刻処理
        pass

import用の設定 (dakoku/__init__.py)

import階層が深くなるのを避けるため、 __init__.py にimportを追加します。

from dakoku.dakoku import Dakoku

エントリポイントを作成 (main.py)

今まで作成してきたファイルと 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まわりの準備

次に、systemdを使い「ラズパイへPaSoRiを挿入した時に上記スクリプトを実行することで、常時FeliCaのタッチを待ち受けている」状態にします。

udevによりUSBデバイス挿入を認識するよう設定

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
...

次に、 /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を追加 

続いて、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

以上で完成です。

おわりに

新しい勤怠システムがリリースされてからラズパイ勤怠を利用していますが、特に問題は発生していません。

また、打刻し忘れることもなく、安定して運用できています。

RubyKaigi 2023 に現地参加しました

はじめに

弊社からは4名のエンジニアが現地で参加しました。

この記事では、RubyKaigi 2023に参加したエンジニアの中で印象に残ったセッションをピックアップして紹介します。

印象に残ったセッション

The Resurrection of the Fast Parallel Test Runner

弊社では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のチューニング

についてもお話があり、このあたりの観点でもテストを見直していこうと感じました。

Revisiting TypeProf - IDE support as a primary feature

TypeProf の v2 を作っているというお話でした。

  • v1 はとにかく動作するものを実装する、速度は二の次という前提にしていた

  • 実際に使ってみると本当に欲しかったものは「Rubyの型よりもIDEサポート」ということに気づいた

  • IDEサポートを強化するには解析速度は重要な要素

  • 解析対象のコードは完全なコードではない(書きかけのコード)

  • v2 は解析結果を差分更新して速度を出している

対象としているエディタはVS Codeのようですが、Vimでも動きそうな情報を見かけたので試してみようと思います。

Implementing "++" operator, stepping into parse.y

個人的 one of the best talks on RubyKaigi 2023 です。

実際に"++"を実装する試みが小さなステップで非常にわかりやすく解説されていました。現状の parse.y が i++ というコードをどのように扱っているのかから始まり、具体的に試してみる→できた→まだ問題が…という流れが parse.y 初心者にとってとっつきやすく、自分も parse.y の中身を見てみたいという気持ちが出てきました。

最後の「"++"があるこの言語は Ruby じゃない Ruby++ だ!」というオチに至るまでのストーリーも面白かったです。

Make Regexp#match much faster

なぜ ReDoS が起きてしまうのか、Ruby の正規表現エンジンである Onigmo の特性や、バックトラックにより分岐が指数関数的に増えて行くことをわかりやすく解説していただき非常に勉強になりました。

また、高速化の手法としてメモ化を導入し計算量が線形時間になることや、 Regexp.linear_time? メソッドで正規表現が線形時間になることのチェックが可能であることも知ることができました。

今後の展望として Regexp.linear_time? が false となるような正規表現の場合に警告を出す RuboCop をスピーカー自身で開発するとのことで、弊社でもリリースされたらすぐに導入したいと考えています。

Multiverse Ruby

匿名モジュールを用いて名前空間の衝突を避けようという話でした。

匿名モジュールの名前空間の挙動はこのセッションを聞くまで知らなかったので興味深く聞くことができました。

また、オートロードを匿名の名前空間上で行うために Zeitwerk をフォークして Im という gem を作成した話もありました。リリースインフォに載らないような Ruby の改善をどのように用いて課題を解決したのか、という一連の流れが特に面白かったです。

Public な gem を開発することがなかなかないのでセッションの内容をすぐに活用することは難しいと思いますが、自分も細かいものも含めて Ruby の改善をキャッチアップしてみたいと思わせてくれるようなセッションでした。

おわりに

弊社のエンジニアたちは東北から関西まで様々な地域で暮らしているため、基本的にフルリモートで勤務しています。

日頃はSlackやGatherなどによるオンラインコミュニケーションを取っている一方、オフラインで直接会話する機会がなかなかありません。

そのため、今回のRubyKaigi 2023は、弊社のエンジニアたちが集まってランチを一緒に食べたり直接会話する良い機会となり、とても楽しく過ごせました。

  最後になりましたが、RubyKaigi 2023を運営してくださったみなさま、本当にありがとうございました。

これからは来年沖縄で開催される RubyKaigi 2024を楽しみにして過ごしていきます。

データ分析プロセス

この記事では、一般的に知られているデータ分析プロセスを簡単に紹介します。

データ分析における標準プロセス

データ分析において、一般的に知られている標準プロセスには以下が存在します。

  • 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.

CRISP-DM

Shearerらが提唱しているCRISP-DM(CRoss Industry Standard Process for Data Mining)では、次の図のようなプロセスにしたがって、データ分析を行います。

No
プロセスの要素
詳細

1

ビジネス理解

ビジネスにおける課題を明確にし、データ分析プロジェクトの計画を立てます。

2

データ理解

データを取得し、そのデータが分析に使える状態であるか確かめるなどの探索的データ分析を実施して、データの理解を深めます。

3

データ準備

後続のモデリングで要求される形式にデータを整形するなどの、前処理を実施します。

4

モデリング

分析モデル(予測をするためのアルゴリズム)を決め、 前のプロセスで準備したデータをモデルに学習させます。

5

評価

次は、前のプロセスで作成したモデルを使って、実際に分析を行い、このモデルよる予測がビジネスに利用可能であるかを評価します。

6

適用

評価した結果、問題がなければ、そのモデルによる予測をビジネスに適用して、使います。

図にも表現されているように、CRISP-DMでは、必要に応じて前後のプロセスを行き来しながら分析を進めます。

KDD

CRISP-DMがビジネスにおけるデータ分析プロジェクト全体を考慮しているのに対し、Fayyadらが提唱しているKDD(Knowledge Discovery in Databases *3)は、よりデータ分析部分にフォーカスしています。

KDDのプロセスは次の図のようになります。

No.
プロセスの要素
詳細

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の考え方も参考にはなるでしょう。

Validated
GitHub - cyu/rack-cors: Rack Middleware for handling Cross-Origin Resource Sharing (CORS), which makes cross-origin AJAX possible.GitHub

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

という場合には、Lambdaのデプロイツールである lambroll を使うのが便利です。

そこで、別記事でも紹介している AWS Vault と組み合わせることでデプロイできるようになります。

その代わり、lambrollではContainer Imageでのデプロイにて代替できます。

その動作確認で役立つのが Amazon SES メールボックスシミュレーター です (以降 シミュレーター と表記)。

以下のドキュメントにあるように、シミュレーターでファイルを添付したい場合、ファイルの中身の文字列を base64 エンコードして メッセージ 欄へ設定する必要があるためです。

そこで、AWSドキュメントの をベースに

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

の手順で問い合わせフォームをアクティブ化しておく必要があります

の手順で問い合わせの回答担当者をエージェントとして追加しておく必要があります

次にフィールドタイプを選択します。 各フィールドタイプの詳細についてはを参照してください。 今回の場合、「対象サービス」というフィールドを作成するときにはドロップダウン、それ以外のフィールドを作成するときにはテキストを選択します。

その後の入力方法はの[カスタムチケットフィールドを作成する]>[カスタムチケットフィールドを追加するには]の手順3以降をご覧ください。

例:本サイトの更新情報は、Twitterので発信しています。ご確認ください。

1.0.4

参考:

複数箇所でSlackへの投稿を行うため、の薄いラッパーを用意しておきます。

を見たところ、そのベンダーIDとデバイスIDの組み合わせが FeliCa S380 [PaSoRi] で間違いなさそうでした。

5/11(木)~5/13(土)に、長野県松本市のまつもと市民芸術館にてが開催されました。

とはいえ、RSpec3のサポートに加え、以下のプルリクにある通りRSpec4.0(dev)もサポートしていることから、社内のコードを使って test-queue を試してみるのも良さそうと感じました。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
fujiwara/lambroll: lambroll is a minimal deployment tool for AWS Lambda.
AWS Vaultを使ったスイッチロール設定手順 | Precena Tech Book
https://github.com/fujiwara/lambroll?tab=readme-ov-file#expand-ssm-parameter-values
https://github.com/fujiwara/lambroll?tab=readme-ov-file#deploy-container-image
シミュレーターを使用した Amazon SES でのテストメール送信 - Amazon Simple Email Service
https://docs.aws.amazon.com/ja_jp/ses/latest/dg/send-an-email-from-console.html#send-email-simulator
MIME の使用 - Amazon SES API v2 を使用した raw E メールの送信 - Amazon Simple Email Service
MIMEの使用 | Amazon SES API v2 を使用した raw E メールの送信 - Amazon Simple Email Service
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
こちら
こちら
こちら
こちら
チケットフィールドについて
カスタムフィールドのタイプについて
チケットとフォームへのカスタムチケットフィールドの追加
条件設定チケットフィールドの作成
「グループ」ページについて
トリガを使ったチケットの自動振り分け
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
nfcpy
Basic app setup | Slack
python-slack-sdk
List of USB ID's - linux-usb.org
RubyKaigi 2023
https://rubykaigi.org/2023/presentations/koic.html#day2
https://github.com/tmm1/test-queue/pull/112
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式
エンジニアリングマネージャーのロードマップ
EMConf JP 2025 オフィシャルサイトEMConf JP 2025 オフィシャルサイト
Logo
Logo
Logo
Logo
Logo
Logo
Logo
overmind s した時の様子
JSONレスポンス返した時の各システムの様子
下ペインには backend_app のログのみ表示される
プロセス選択の様子
overmindで起動したプロセスをデバッグできた
Swagger UI で可視化したAPI定義のサンプル
https://storybook.js.org/docs/react/workflows/visual-testing より
ボタンコンポーネントのAll
Pull Requestへの通知
詳細なレポート
gitbook.comで使える見出しのスタイル
CRISP-DMにおけるデータ分析プロセス
KDDにおけるデータ分析プロセス
Logo
Logo