UUUMエンジニアブログ

UUUMのエンジニアによる技術ブログです

ECSコンテナを手元からコマンドラインで操作するecsgoを紹介

皆様ごきげんよう。エンジニアの久保寺です。

【第2回】UUUM攻殻機動隊オンラインハッカソンを開催しました! - UUUMエンジニアブログ

過去内定者インターンのときにブログを書きましたが、そんな自分も入社してから1年以上経っていました。

👇👇👇新卒インタビューなるものも記事になっているので、是非見てください👇👇👇

www.wantedly.com

ecsgoとは?

まずはじめに、今回紹介するecsgoの簡単な説明をします。

ecsgoとはECSタスクのコンテナに対話式で接続し、手元からコマンドラインで操作することを可能にするツールです。

これとても便利で、僕のチームのメンバーは全員使っています!

導入

https://github.com/tedsmitt/ecsgo

brew tap tedsmitt/ecsgo
brew install ecsgo

こちらのコマンドを叩くだけなんですが、AWS CLIをインストールしてコマンドラインでAWSに繋げるようにしないといけないのでインストールしてください。

AWS CLI バージョン 2 を使用するための前提条件 - AWS Command Line Interface

さらに、session-manager-pluginというAWS CLIのプラグインも必要になるのでインストールします。

使ってみる

ecsgo

シンプルにecsgoと叩けば、接続したAWSの全てのクラスターが表示されます。

↑↓キーでお目当てのクラスターを選択しましょう。

ecsgo -p 〇〇

-p オプションを使えば、プロファイルを選択することもできるので複数のAWSアカウントにも対応できます。

僕が開発担当しているアプリである、CREASのコンテナに入る画面を例として貼っておきます。

※CREASを詳しく知りたい方はこちらをクリック。

ステージングと本番のクラスターがありますが、今回はステージングを選択します。

👇

タスク定義を選択します。

👇

コンテナを選択します。

👇

これでコンテナをコマンドラインで操作できる状態になりました。

試しにphp -vを叩いて、PHPのバージョンを調べてみました。

最後に

対話式でクラスターやコンテナを選べるのがとても便利だなって思ってます。

ECSを使っている方は是非ecsgoの導入を検討してみてはいかがでしょうか。

Mac 歴 8 年のエンジニアが便利な Mac アプリを紹介するぞ!

Apps thumbnail

はじめに

こんにちは、UUUM に入社してから 544 日目1aoki_k です。

エンジニアブログなので基本的には技術関連の記事をメインに書くことになっていますが、汎用性のある記述記事はふだんから個人的に投稿しているので、今回ここでは少し技術からは離れた内容をお届けしようかなと思います。

題して『Mac 歴 8 年のエンジニアが便利な Mac アプリを紹介するぞ!』です。

エンジニアという職業もあって、ほとんどの時間は PC に向き合っています。筆者は仕事でもプライベートでも Mac を使用しており歴もそれなりに長めなので、様々な有用アプリを紹介できるのではないかなと思いこの題に決めました。

余談

今回がはじめての UUUM エンジニアブログでの記事執筆となりますが、実は初登場ではありません。以前に UUUM エンジニア デスクツアー vol.1 という記事で取り上げていただきました。

実はあれから引越をしたり持ち物を整理したりしているので、物理的な環境も上記の記事を執筆いただいたときからだいぶ変わってはいるのですが、それはまた別の機会に。

アプリ紹介

冒頭が少し長くなってしまいましたがそろそろアプリの紹介をしようと思います。

なお、Google Chrome や LINE などのメジャーなものや、Docker や Visual Studio Code などエンジニアならほとんどが認知している (使用している) もの、特定のサービス専用のものなどに関しては除外しました。

Alfred 5

Alfred 5

アプリケーションの呼び出しや計算、特定のコマンドの実行など、幅広い操作を数タイプのキーボード操作のみで簡単に実行できる常駐アプリです。いわゆる Spotlight の強化版です。

マウスフォーカスがどこにあっても、どのアプリケーションウィンドウがアクティブになっていても、ショートカットひとつで呼び出せるので、ちょっとした調べ物や計算、めんどくさい処理を自動化するのに非常に便利です。

スニペットを登録しておいて、設定したスニペット名を入力するだけで数十行あるテンプレートを一括挿入してくれたりなど、かゆいところに手が届くアプリです。

ちゃんと使いこなすにはそれなりに設定や慣れが必要なのですが、その分、作業効率は格段にアップすること間違いなしです。

オススメ度

★★★★☆

インストール

GUI

Alfred 5

CLI

brew install --cask alfred

Amphetamine

Amphetamine

Mac を一時的にスリープしないようにできる常駐アプリです。

重いファイルをダウンロードしたり、パッケージをインストールしたりビルドしたりするときって時間がかかりますよね。その間、他の作業をしたりすることもできるのですが、しばらく操作していなくて勝手にスリープされてしまっては困ります。

そのようなときは、Mac のシステム環境設定から一時的にスリープまでの時間を長くしたり自動スリープ無効にしたりすることで対策できますが…… スリープさせないようにするたびにシステム環境設定から設定するのはめんどくさいですよね。しかも一時的に設定を変更したことを忘れて、そのままスリープしない状態になっていた…… なんてことも。

このアプリは、メニューバーにあるアイコンをクリックするか、あらかじめ設定したショートカットを押すだけで、事前に設定したスリープ時間に上書きしてくれます。たとえば Mac の設定で自動スリープまでの時間を 15 分に設定していたとして、Amphetamine の設定で 1 時間に設定していたとすると、Amphetamine が有効になっている間は Mac の設定が無効となり、1 時間後に自動スリープするようになります。自動スリープまでの時間を無限にすることもできるので、一時的に自動スリープさせないようにすることもできます。

こういうときのためだけに常に自動スリープさせないようにしたり自動スリープまでの時間を極端に長くしたりすると電気代がもったいないですし、Mac のディスプレイの寿命が早まったりバッテリー駆動の場合はバッテリー消費も多くなったりもします。このアプリを使えば、適宜そのときの状況にあわせてスリープ時間を簡単に調整できるようにできます。

最近は Mac の性能も上がってインストールやビルドにそこまで時間がかからなくなったため、あまり使用頻度は高くないですが……。

オススメ度

★★☆☆☆

インストール

GUI

Amphetamine

CLI

mas install 937984704

AppCleaner

AppCleaner

アプリケーション削除時に、設定ファイルやメタデータなども一緒に削除してくれるアプリです。

Windows と違って、Mac ではアプリのアンインストーラが付属していないことが多いです。Mac App Store からインストールしたアプリは iOS のようにアプリケーション一覧画面から削除することができますが、そうでないアプリは Finder や CLI から削除する必要があります。

ただ、アプリだけを削除しても、設定ファイルやメタデータは削除されません。しかも、それらのファイルがどこにあるのかは、アプリによってまちまちです。

Finder と AppCleaner を起動して Finder から AppCleaner にアプリアイコンをドラッグ・アンド・ドロップすると、そのアプリに関連するすべてのファイルを Mac から検索してくれます。その後、残しておきたいファイルと削除したいファイルを取捨選択して、まとめてファイルを削除することができます。

もう使わなくなったアプリを削除するときには関連するファイルも削除されていないと気持ち悪いと感じてしまうタイプなので、アプリ削除時にはお世話になっています。

オススメ度

★★★☆☆

インストール

GUI

AppCleaner

CLI

brew install --cask appcleaner

Bartender 4

Before After
Bartender 4 (before) Bartender 4 (after)

メニューバーから常駐アプリのアイコンを非表示にできる常駐アプリです。

ドックにあるアプリアイコンは自分である程度整理することができますが、メニューバーには、起動している常駐アプリがすべて表示されてしまうためごちゃごちゃになりやすいです。常駐アプリとして起動している以上はどこかにアイコンが存在していなければいけないのはたしかですが、ただ起動しているだけで意味のある常駐アプリのアイコンがメニューバーにずっと表示されているのが気になってしまう人もいるでしょう。かくいう筆者も OCD なのでよく目にするものは整理整頓されていないと落ち着きません。

このアプリを使うと常駐アプリのアイコンをふだんは非表示にすることができます。必要になったときはこのアプリのアイコンを操作することで一時的に表示することができます。ただし時計やコントロールメニューは非表示にできません。

最近はメニューバー自体をデフォルトで非表示に設定してしまったので、あまりこのアプリのメリットは感じられていないです……。それから、そもそもアイコンがごちゃごちゃしていても気にならない人には不要なアプリですね。

オススメ度

★★☆☆☆

インストール

GUI

Bartender 4

CLI

brew install --cask bartender

Be Focused Pro

Be Focused Pro

ポモドーロアプリです。これだけでなんのことかわかったらもはや説明は不要です。

簡単に言ってしまえばタイマーアプリなのですが、1 セッションの時間や休憩時間、何回のセッションで長い休憩にするのかなどを設定できたり、休憩時間の開始を自動で行ってくれたりなど、基本的な設定は網羅しており、UI がシンプルなところが気に入っています。

ポモドーロテクニックはかなり認知度も上がってきており、それにともないポモドーロアプリもかなりたくさんあるので、あえてこれを選ぶ理由があるかと言われると微妙なところですが、様々なアプリを試した結果、結局これに落ち着いたという感じです。

オススメ度

★★★☆☆

インストール

GUI

Be Focused Pro

CLI

mas install 961632517

BetterSnapTool

BetterSnapTool

ウィンドウの最大化、左半分、右半分、画面 1/4 など、ウィンドウの大きさを簡単に変更できる常駐アプリです。

操作はショートカットもしくはウィンドウを画面の端に向かってドラッグすることで行います。ドラッグ操作は Windows と似たようなものと思っていただければイメージしやすいかなと思います。

Mac 標準の操作ではウィンドウ上部をダブルタップすることで最大化することができますが、アプリによってはなぜかうまく動作しなかったり、縦方向にしか最大化されないなど不便な点があります。また、左半分に寄せるなどの操作もできません。

このアプリでは、ウィンドウを端に向かってドラッグするという、直感的な操作で画面の大きさを変更することができます。また、ショートカットを割り当てることもできます。

ウィンドウ上部をダブルタップよりも操作しやすいですし、ウィンドウの配置やサイズ調整も応用が効くので重宝しています。

オススメ度

★★★★★

インストール

GUI

BetterSnapTool

CLI

mas install 417375580

Browserosaurus

Browserosaurus

ブラウザ以外のアプリのリンクをクリックした際に、どのブラウザで開くかを都度選択することができる常駐アプリです。

Visual Studio Code など、ブラウザ以外のアプリ内の HTTP/HTTPS リンクをクリックした場合、通常はデフォルトのブラウザでそのリンクが開かれます。

しかし、用途ごとに使用するブラウザを使い分けたい場合があります。たとえば会社関連のリンクは Chrome で開きたいけど、個人関連のリンクは Safari で開きたい、などです。

このアプリは常駐アプリではありますが、OS から見るとブラウザ扱いとなります。このアプリをシステム環境設定からデフォルトのブラウザに設定すると、リンクをクリックした際に Chrome などの通常のブラウザの代わりに Browserosaurus が起動します。Browserosaurus はあらかじめ Mac にインストールされているブラウザすべてをチェックして、リンクをクリックした際にどのブラウザで開くかを確認する小さなポップアップのような画面が表示されます。あとは所望のブラウザを選択することで、指定したブラウザでそのリンク先のページを開くことができます。

複数のブラウザを並行して使用している人におすすめのアプリです。筆者も以前は会社用のブラウザと個人用のブラウザを分けるために、それぞれ Chrome (Stable) と Chrome Beta を使っていましたが、最近は Chrome のプロフィール機能で分けているため、使用頻度は下がりました。

オススメ度

★★☆☆☆

インストール

GUI

Browserosaurus

CLI

brew install --cask browserosaurus

Create File Menu

Create File Menu

Finder のメニューバーおよび右クリックメニューにファイルを新規作成する項目を追加するツールです。

Windows だとデフォルトで右クリックメニューからテキストファイルを作成することができますが、Mac ではできません。それを可能にするのがこのツールです。

ふだんは CLI でファイル操作を行うので頻繁に利用するものではないのですが、Finder を使うときにちょっとしたメモをディレクトリに追加したいときなどに重宝します。

オススメ度

★★★☆☆

インストール

GUI

Create File Menu

CLI

mas install 1440519779

Google Japanese IME

Google Japanese IME

定番の日本語用 IME です。

Mac 標準搭載の IME を使用している人もそれなりにいるようなのですが、ミーティング等の画面共有で標準 IME を使っている人の変換の挙動を見ると、あまり賢いとは言い難いです。

Google Japanese IME は、さすが Google 製ということもあって、変換の精度がそこそこ高いです。たまにおかしな誤変換をすることもありますが、たいていの場合は正しく変換してくれるのでストレスなく文字入力を行うことができます。

もちろん辞書ツールもあるので、自分がよく使う単語や表現、記号なんかを事前に登録しておくことでさらに入力効率が上がります。たとえば「よろ」と入力すると「よろしくお願いいたします」に変換されるなどですね。

また、先ほど誤変換することもあると説明しましたが、誤変換する用語と正しい用語を辞書に登録しておくことで、誤変換を防ぐというテクニックもあります。

さらに、これは人によっては抵抗があるかもしれませんが、筆者は「めあど」と打つと自分のメールアドレスに変換されるようにしています。単純にメールアドレスを毎回手動で入力するのがめんどくさいというのもありますが、誤植があってメールが届かなかった、という被害も防ぐことができるのでとても助かっています。

個人的には、常に半角で入力する設定が何気に一番便利だったりします。日本語入力中に数字や記号を入力すると、デフォルトでは全角数字や全角記号が入力されてしまうのですが、これを半角にすることができます。つまり、英数入力モードであっても日本語入力モードであっても数字や記号は常に半角で入力されるので、日本語入力の途中で数字や記号をちょっと入力したいためだけにモード切替をする必要がなくて文字入力がとてもはかどります。

記号ごとに半角と全角を選択できるので、「、」や「。」などは全角にしつつも、"+" や "=" などは半角にする、といった細かい設定もできます。

筆者は長年 Google Japanese IME を使っています。有料の IME はより便利で精度が高いかもしれませんが、無料の範囲内でここまでストレスなくタイピングできるので個人的にはこれで満足しています。

オススメ度

★★★★★

インストール

GUI

Google Japanese IME

CLI

brew install --cask google-japanese-ime

Karabiner-Elements

Karabiner-Elements

キーマップを変更できるアプリです。

筆者は US 配列キーボードを使用しているのですが、日本語を使用する環境だと日本語入力切替に少し難があります。

デフォルトだと Ctrl + Space で入力ソースの切替ができるのですが、頻繁に切り替えをするにしては少々押しづらいです。なにより筆者が一番キライなトグルタイプ2なので、常用には耐え難いマッピングです。

また、US 配列だと一番左の上下中央の位置 (ちょうど左手の小指辺り) にあるのが Caps Lock なんですよね。Caps Lock がキライな理由は…… もう言うまでもないですね。こんなに押しやすくて便利な位置にこんなに不要なものがあるのは非効率極まりないです。

このアプリを使うとそうした不満を解消できます。キーのマッピングを変更できるのはもちろんのこと、修飾キーを単体で押した場合のみ挙動を変えたり、二回連続で押した場合に反応するようにしたり、複数のキーボードのうち特定のキーボードのみマッピングを変更したりなど、かなり複雑な設定も自由自在です。諸事情で JIS 配列と US 配列の両方のキーボードを使用しなければいけない環境では特定のキーボードのみに設定できるのはかなり便利なのではないでしょうか。

参考までに、筆者の設定を共有しておきます。

  • Fn → Escape
    • Escape キーは良く使うのに一番左上にあってホームポジションからは押しづらいため
  • Caps Lock → Ctrl
    • Mac は標準で Emacs 風のキーバインドが割り当てられており、Ctrl キーを多用するため
  • Left Command (単体押し) → 英数
    • いわゆる JIS 配列的なマッピング
  • Right Command (単体押し) → かな
    • いわゆる JIS 配列的なマッピング
  • コロンとセミコロンの入れ替え
    • 筆者は Ruby などのスクリプト言語を書くことが多く、セミコロンよりコロンのほうが圧倒的に使用頻度が高いため
  • Command + Q (二回連続押し) → Command + Q (一回押し扱い)
    • 誤入力でアプリケーションを終了させてしまうのを防止するため

Fn キーと Caps Lock キーがどこにも割り当てられていませんが、この 2 つは使わないのでいらないです。

また、英数・かなをそれぞれ単体のキーとして割り当てることによりトグルキーではなくなります。アルファベットや数字を入力したいときは Left Command キー、日本語を入力したいときは Right Command キーを、文字入力前にとりあえず 1 回押しておけば良いので間違いがないです。

このアプリは個人的に必須です。これがないと、正直、作業ができないレベルです。

ちなみに余談ですが、高級キーボードで有名な REALFORCE では、Caps Lock を Ctrl に変更するためのキーキャップが付属しています。REALFORCE さん、よくわかっていらっしゃる。

オススメ度

★★★★★

インストール

GUI

Karabiner-Elements

CLI

brew install --cask karabiner-elements

KeyboardCleanTool

KeyboardCleanTool

キーボードの操作を一切受け付けないようにするアプリです。

名前のとおり、キーボードを掃除したいときに使います。外部キーボードなら Mac から切断して掃除すれば良いですが、内蔵キーボードの場合は掃除中にキーが押されて勝手に反応してしまいます。電源を切れば良いのですが、掃除しようと思ったときにいちいち電源を切らなければいけないのは面倒です。

このアプリを使うと一時的にキーを無効化できるので、誤入力することなく内蔵キーボードを掃除することができます。

ただし注意点があり、fn キーなど一部のキーは無効化されません。また、MacBook のトラックパッドは反応してしまうため注意が必要です。

そこまで頻繁にキーボードを掃除する機会がないのと、誤入力しても問題ない状態で掃除すれば良いことに加え、外部キーボード勢には関係のない話なので、オススメ度はそこまで高くはないです。

オススメ度

★☆☆☆☆

インストール

GUI

KeyboardCleanTool

CLI

brew install --cask keyboardcleantool

Lunar

Lunar

画面の輝度やコントラストを調整しやすくする常駐アプリです。

外部モニターの明るさ調整って結構めんどくさいですよね。明るさ調整キーを使っても、反映されるのは Mac 本体のディスプレイのみです。

このアプリを使うと外部モニターの明るさをキーボードやメニューバーから変更できるようになります。明るさ調整キーで操作できるのはもちろん、キーボードショートカットを割り当てれば輝度やコントラストを最大にしたり、逆に最小にしたりするのが簡単にできます。メニューバーのシークバーを使って細かい輝度調整を行うことも可能です。

筆者はそのときの部屋の明るさにあわせて手動で変更していますが、Mac 本体の明るさセンサーに連動して外部モニターの輝度を自動調整するモードや、時刻や住んでいる地域を基準として日の出から昼間にかけて徐々に明るく、夕方から日の入りに向けて徐々に暗くするモードなどもあります。

筆者はそのときの部屋の明るさに応じて細かく画面の明るさを調整したいタイプなので、このアプリはかなり重宝しています。

オススメ度

★★★★★

インストール

GUI

Lunar

CLI

brew install --cask lunar

Noizio

Noizio

様々な環境音や雑音、特定の周波数帯の音を出すことができる常駐アプリです。

鳥の鳴き声や雨の音、焚き火の音などの環境音から、電車の音、ホワイトノイズなどの雑音系、α波、β波などの特殊な音に至るまで幅広く揃っています。ちょっと変わったものだと心臓の鼓動なんてのもあります。

もちろん複数の音を組み合わせることもできますし、音の種類ごとに音量を細かく調整しバランスを取ることもできます。また、気に入った組み合わせの環境音ができたら、それを保存しておいていつでも切り替えることができます。ショートカットを設定することができるので、一時的にオンにしたりオフにしたりも簡単にできます。

筆者は HSP なので、ほんの些細な物音が気になって集中できなかったり眠れなかったりします。そのため、外出中やお風呂に入っているとき以外のほとんどの時間はこのアプリで環境音を流しています。これがないと仕事に集中できないし眠れません。

音楽を聞きながら作業する人も多く見かけますが、筆者は 2 つ以上の物事に集中することが本当に苦手なので、音楽を流しているとどうしてもそちらに注意が向いてしまいます。環境音だとほぼ同じ音声の繰り返しなので、曲ごとに BPM が変わるなどで気がそれたりしないため非常に重宝しています。

オススメ度

★★★★☆

インストール

GUI

Noizio

CLI

mas install 928871589

Witch

Witch

アプリケーションの切替をキーボードで行うことができる常駐アプリです。Mac だと Command + Tab でアプリケーション切替ができますが、これの強化版です。

Mac 標準の Command + Tab もとても便利なのですが、長年使い続けているとちょっとした不満や欲が生まれます。たとえば Finder3 が邪魔だったり、特定のアプリは除外したかったり。

このアプリは通常の Command + Tab ではできない機能を提供してくれます。先述した特定のアプリを対象外とすることもできますし、ショートカットを変更することもできます。もちろん Command + Tab を上書きすることもできます。

個人的に一番便利だと感じているのが、ウィンドウ間の切替です。標準機能でも同じアプリケーションの複数のウィンドウ間を切り替えるショートカットを設定することはできるのですが、ドックにしまっているウィンドウを自動で開いたりできませんし、Command + Tab のようにウィンドウ切替メニューが画面中央に表示されません。ショートカットを押した瞬間、次のウィンドウに切り替わってしまいます。

Witch はアプリケーション切替だけでなくウィンドウ切替にも対応しているため、アプリケーション切替のような操作感でウィンドウ切替を行うことができます。ドックにしまっているウィンドウを選択した場合は自動で開きます。

ウィンドウ切替を別のショートカット (Option + Tab など) に割り当てることもできますし、アプリケーション切替とウィンドウ切替を統合することもできます。Windows の Ctrl + Tab のように、すべてのアプリケーションのウィンドウを一つのメニュー内で表示させて選択することができます。

一時的にシステム環境設定などを開いていたりするときに、いちいちそれが選択肢に表示されると邪魔だなあと感じることがあったのですが、このアプリを使い始めてから見事にその問題が解消されました。

オススメ度

★★★★★

インストール

GUI

Witch

CLI

brew install --cask witch

おわりに

全 14 個のアプリを紹介しましたが、いかがだったでしょうか。

長年 Mac を使用しており、不便だと感じたことを解決するアプリを探したり、便利だと思うアプリを見つけたりするたびにとりあえず試してみることが多かったので、本記事を執筆する前はもっとたくさん紹介できそうだと思っていました。

しかし、いざ自分がインストールしているアプリをまとめてみると、ふだんから常用しているアプリは意外と少ないなということに気がつきました。これまで様々なアプリを使ってきたのはたしかなのですが、環境の変化などで結局使わなくなったり不要になったりするものも出てきたので、本当に必要なものだけが洗練されていったと考えることもできます。

人によって趣味趣向や求めているもの、環境はまちまちなので、すべてのアプリがみなさんにとって有用であることはおそらくないと思いますが、この中で一つでも気になるアプリがあれば、本記事を執筆した意味はあるのかなと思います。

今回は久しぶりに技術関連ではない記事を執筆しましたが、ふだんは Zenn などで技術記事を主に投稿しておりますので、興味があればそちらもご覧ください。

それではまた!


  1. 2022 年 12 月 26 日現在
  2. トグルキーは、現在の状態がどちらなのかによって押すべきか押すべきではないかが変わってしまい、いちいち記憶しているか現在の状態を確認しないと高確率で判断を誤るのが嫌いな理由です。たとえば、しばらくマウス操作をしたあとに、日本語の文章を入力したくなった場合、前回、英数字を入力していた場合は切り替える必要がありますが、日本語入力をした場合は切替を行うと逆に英数字モードになってしまいます。キー操作から時間が少しでも開くと現在の状態がどちらなのか忘れてしまうし、そもそもその状態を覚えようと意識しながら作業をしているわけではないので、個人的にトグルキーは非常にストレスです。
  3. ちなみに Finder が邪魔な件に関しては Finder を終了させる こともできます。

AWS Step FunctionsとAmazon AthenaでCSVデータを自動集計したい!

はじめに

こんにちは、UUUMに入社して3年ほど経つishiharaです。

私が今担当しているプロジェクトに大量のCSVデータを集計するシステムがあり、そこで最近いくつもの集計作業を一連のワークフローとして自動化する仕組みを構築したので簡単に内容を紹介していこうと思います。

利用サービスについて

AWS Step Functionsとは

Step Functionsは、AWSの様々なサービスと連携して、1つのワークフローとなるアプリケーションを構築できるサービスです。

Step Functionsはステートマシンとタスクから構成されています。ここでのステートマシンはワークフローのことで、タスクはワークフロー内の1つ1つの状態(ステップ)になります。
例えば、 1,2,44,32,5433,... のような適当な数値の羅列からなるCSVデータがあったとして、そこから5以上の数値を取り出したいとします。
作業として、
CSVデータの取得→5以上の数値を抜き出す→抜き出したデータの保存(や通知)
といった一連の流れがワークフローであり、→で区切られた1つ1つの作業がタスクとなります。

また、1つのワークフローは1つのJSONファイルから構成されており、Amazon States Language に則って定義していくことでワークフローを実行することができます。

Amazon Athenaとは

↓を参考にしてください。

system.blog.uuum.jp

構成の概要

構成イメージ

下準備

ワークフロー作成前に、バケットを用意してデータを格納し、Athenaでテーブル定義してSQLを実行できるようにしましょう。

データは下記のようなデータを用意したとします。

sample-athena-step-functions-bucket.csv

video_id channel_id views video_type date
WJzSBLCaKc8 UCZf_ehlCEBPop-sldpBUQ 103200 none 2020-11-01
WJzSBLCaKc8 UCZf_ehlCEBPop-sldpBUQ 12132 none 2020-11-02
FUb-lE0tQC4 UCZf_ehlCEBPop-sldpBUQ 100031 short 2020-11-01
FUb-lE0tQC4 UCZf_ehlCEBPop-sldpBUQ 10001 short 2020-11-02
uJwa7y6Q6Sw UCgMPP6RRjktV7krOfyUewqw 102203 none 2020-11-01
uJwa7y6Q6Sw UCgMPP6RRjktV7krOfyUewqw 12203 none 2020-11-02

S3バケット

テーブル定義のクエリ

CREATE EXTERNAL TABLE IF NOT EXISTS `default`.`sample_athena_step_functions_bucket` (
  `video_id` string,
  `channel_id` string,
  `views` int,
  `video_type` string,
  `date` string
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.OpenCSVSerde'
WITH SERDEPROPERTIES ('separatorChar' = ',')
STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://sample-athena-step-functions-bucket/'
TBLPROPERTIES (
  'classification' = 'csv',
  'skip.header.line.count' = '1'
);

テーブル作成

Athenaを実行する

まずはStep Functionsのサービスへ行き、ステートマシンを作成しましょう。 作成方法はデフォルトのまま次へ進んでいきましょう。

ステートマシンの作成

そうすると下記のような画面になると思います。
ここでは、Athenaのクエリを実行するためのAPIのAthena: StartQueryExecution をタスクとしてワークフローに追加します。 そしたら状態名を任意で変更し、APIに渡すパラメータをJSON形式で入力していきます。

タスクの追加

APIパラメータ

{
  "QueryExecutionContext": {
    "Database": "default"
  },
  "QueryString": "select video_id, sum(views) as views from sample_athena_step_functions_bucket group by video_id",
  "ResultConfiguration": {
    "OutputLocation": "s3://sample-athena-query-result/"
  },
  "WorkGroup": "primary"
}

注)OutputLocationで指定しているバケットは、Athenaのクエリ実行時にデータを格納する場所で、なければ作成する必要があります。

入力したらあとはそのまま進んでステートマシン名とロールを適宜入力して作成します。
作成したら「実行の開始」を押下してみてください。

作成されたオブジェクト

ワークフローが周り、クエリの実行結果がCSV形式で指定のバケットに保存されていることが確認できます。

ただ、ファイル名がこのようでは少しわかりづらいですよね。
次にファイル名を変更し、ついでに実行結果を格納するのとは別に必要なレポートを格納するバケットを別に用意しようと思います。

ファイル名を変更する

実行結果を格納するバケットとは別に新たに「sample-step-functions-report」というバケットを用意します。
その後、ステートマシンの編集画面に戻り、先程と同様に今度はAPIの S3: CopyObject というタスクを追加します。

タスクの追加

APIパラメータ

{
  "Bucket": "sample-step-functions-report",
  "CopySource.$": "States.Format('sample-athena-query-result/{}.csv', $.output.QueryExecutionId)",
  "Key": "sample_report_group_by_video_id.csv"
}

こちらにある CopySource.$.$ はパラメータを参照するためのものです。
AWS Step Functionsの組み込み関数であるStates.Format関数を利用してパラメータを展開しています。
また、ここで利用しているパラメータは1つ前のタスクでの実行結果であり、デフォルトで引き渡されるものではないので1つ前のタスクから引き渡されるようにしないといけません。

パラメータの引き渡し

最後に、このままではAthenaによってオブジェクトがS3に生成される前にCopyObjectを実行しようとしてエラーが出てしまうのでエラー処理も施します。

エラー処理

これで準備完了です。 先程のように「実行の開始」を押下するとファイル名が変更され、指定のバケットに保存されたことが確認できると思います。

レポートの作成

並列処理にする

最後に、直列の処理だけではなく、並列して実行できる作業がある場合、並列処理にすることで集計スピードを早めることができます。 動画ごとにまとめる以外にチャンネル別に集計したい場合、それぞれ並列して処理をしてみたいと思います。

並列処理

並列処理を追加したら、あとはこれまでと同様にタスクを追加すれば完了です。APIパラメータや状態名を必要に応じて変更していきましょう。

チャンネルごとにまとめる

{
  "QueryExecutionContext": {
    "Database": "default"
  },
  "QueryString": "select channel_id, sum(views) as views from sample_athena_step_functions_bucket group by channel_id",
  "ResultConfiguration": {
    "OutputLocation": "s3://sample-athena-query-result/"
  },
  "WorkGroup": "primary"
}

チャンネルごとにまとめたファイル名の変更

{
  "Bucket": "sample-step-functions-report",
  "CopySource.$": "States.Format('sample-athena-query-result/{}.csv', $.output.QueryExecutionId)",
  "Key": "sample_report_group_by_channel_id.csv"
}

レポートの作成

まとめ

いかがでしたでしょうか。
ここでの内容は基礎的なものであり、実際に業務で利用する場合にはさらにいくつものAWSのサービスと連携していく必要はあるかと思いますが、Step Functionsを活用すれば自動化できる作業の範囲が広がっていくと思います。
また、並列処理などを活用することで自動化による作業コストの削減だけでなく、作業スピードの改善につなげることもできます。
この内容が少しでも皆さんのお役に立てば幸いです。

Cloud Scheduler を使って、Cloud Functions を定期実行させたい!

▼ 自己紹介

こんにちは!UUUMシステムユニットの高橋です!


私はUUUM内でデータ出入力関連の作業を担当しておりますが、そこで頻繁に利用するサービスとして Google Cloud Scheduler というものがあります。

これは様々なジョブを定期実行してくれるGoogleのサービスで、私の場合で言えば、毎日一回作成するレポートを自動的に作成してくれています。

今回は、Google Cloud Scheduler の使用例の一つとして、Cloud Functionsの定期実行について書こうと思います!

▼ 構成の概要


利用するサービスの繋がりを図にしました↓

今回使用する言語がpythonなのでそのアイコンを使いましたが、他の言語も使えます!


▼ ローカルでコードを書く

まずは定期実行するコードを書いていきます
今回は実行結果の確認を簡単にするために、Slackにメッセージを送るコードを書こうと思います!


とりあえず、Slackにメッセージを送る関数 send_text() を作り↓

from slack_sdk import WebClient
import requests


def send_text(input_token,input_channel_id,input_text):
    '''---
    内容:
        指定したチャンネルにテキストを送信する
    引数:
        input_token: slack bot のトークン
        input_channel_id: 投稿先チャンネルのID(ex C02ABCDEFGH Slackのチャンネルリンク内に記載)
        input_text: 投稿するテキスト
    '''
    urlpo='https://slack.com/api/chat.postMessage'
    headers = {'Authorization' : f'Bearer {input_token}'}
    data = {
        'token': input_token,
        'channel': input_channel_id,
        'text':input_text
    }

    requests.post(urlpo, headers = headers, data=data)
    return


これをmain() で呼び出します↓

import datetime,os

def main():
    #タイムゾーン設定
    JST=datetime.timezone(datetime.timedelta(hours=+9), 'JST')
    now = str('{0:%Y-%m-%d_%H:%M:%S}'.format(datetime.datetime.now(JST)))

    # slackへメッセージを送る

    # slack_token, slack_channel_idを環境変数から取得する
    SLACK_TOKEN=os.getenv('SLACK_TOKEN')
    SLACK_CHANNEL_ID=os.getenv('SLACK_CHANNEL_ID')
    # messageを作る
    str_message=f'''
    【実行時間】{now}
    Cloud Pub/Sub トピックのメッセージからトリガーされました\n
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    '''
    # Slackへ送信
    send_text(SLACK_TOKEN,SLACK_CHANNEL_ID,str_message)

main()


全体としてはこんな感じで完成↓

import datetime,os,requests
from slack_sdk import WebClient


def main():
    #タイムゾーン設定
    JST=datetime.timezone(datetime.timedelta(hours=+9), 'JST')
    now = str('{0:%Y-%m-%d_%H:%M:%S}'.format(datetime.datetime.now(JST)))

    # slackへメッセージを送る

    # slack_token, slack_channel_idを環境変数から取得する
    SLACK_TOKEN=os.getenv('SLACK_TOKEN')
    SLACK_CHANNEL_ID=os.getenv('SLACK_CHANNEL_ID')
    # messageを作る
    str_message=f'''
    【実行時間】{now}
    Cloud Pub/Sub トピックのメッセージからトリガーされました\n
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    '''
    # Slackへ送信
    send_text(SLACK_TOKEN,SLACK_CHANNEL_ID,str_message)

#------------------------------------------------------------------------

def send_text(input_token,input_channel_id,input_text):
    '''---
    内容:
        指定したチャンネルにテキストを送信する
    引数:
        input_token: slack bot のトークン
        input_channel_id: 投稿先チャンネルのID(ex C02ABCDEFGH Slackのチャンネルリンク内に記載)
        input_text: 投稿するテキスト
    '''
    urlpo='https://slack.com/api/chat.postMessage'
    headers = {'Authorization' : f'Bearer {input_token}'}
    data = {
        'token': input_token,
        'channel': input_channel_id,
        'text':input_text
    }

    requests.post(urlpo, headers = headers, data=data)
    return

#------------------------------------------------------------------------

main()


実行するとSlack にメッセージを送信できていたのでOK↓(ここはまだ手動で実行しただけ)


これでコードの作成は完了です!


▼ Pub/Subにトピックを作る

今回はCloud Functionsに登録したコードをPub/Subをトリガーに実行したいと思います!
その為に、Pub/Subにトピックを作っておきます


Pub/Subのページから「トピックの作成」を押下し、トピックIDを決めて作成できます↓

今回は「test_pubsub」というIDで作成しました
このIDは次のステップで使用します!


これでトピックの作成は完了です!


▼ Cloud Functionsにコードを移す

次に、Cloud Functionsにコードを移していきます!

Cloud Functionsのページから「関数の作成」を押下し、
関数名を決めて、先ほど作成したトピックを選択します↓


作ったコードの中で使用する環境変数はこの欄で設定できます↓


コードを転記します↓


requirements.txtの中身は↓のようになっています

# Function dependencies, for example:
# package>=version
requests==2.28.1
slack-sdk==3.18.4


これでCloud Functionsへの移行は完了です!


▼ Pub/Subから実行できるか試してみる

ここまでで、Pub/SubをトリガーにCloud Functionに登録したコードを実行できるようになったはずなので、テストしてみます!


まずはPub/Subのページから作成したトピックを押下し、メッセージタブを選択します。
次にトピックをプルダウンで指定して、「メッセージをパブリッシュ」を押下します↓


メッセージを入力し、「公開」を押下します↓


期待通りSlack に届きました↓


これでPub/Sub → Cloud Functionsの起動テストは完了です!


▼ Google Cloud Scheduler で定期実行してみる

最後に Cloud Scheduler の設定をしていきます!


Cloud Schedulerのページから「ジョブの作成」を押下し↓


各欄に値を入力して登録できます↓


「2分毎に実行」で登録しましたが、強制実行で確認してみると...↓

Slackにメッセージが届いたので、大丈夫そうです!↓


これでCloud Scheduler の設定は完了です!


▼ 定期実行されてるか確認

さて、これで定期実行されるはずですが、
実際どうなのか10分ほど見守ってみると...↓

問題なく動いているみたいです!


これで全て完了です!

▼ 最後に

お疲れ様でした!
簡単な例ではありましたが、定期実行を登録することができました
実行されるコードの部分を書き換えれば、

  • 毎日の株価を取得してくる とか
  • Youtubeの急上昇動画のURLを通知する とか
  • データベースの点検をさせる とか

自分が普段行っている作業を代わってもらうことができそうですね
皆様の何かの参考になれば幸いです!

API 経由で起動する Shopify Flow のトリガーを自作してみる

こんにちは、UUUM に入社してからちょうど2年半くらいの kazama です。

弊社には数々のプロジェクトが存在しており、それぞれにエンジニアが数名アサインされているわけですが、自分は少し前まで EC 部門のプロジェクトに関わっていました。

そこでは世界最大級の e コマースプラットフォーム「Shopify」をベースとした運用・開発が行われており、個人的になかなか貴重な体験ができたので今回はその辺の知見をアウトプットしておきたいと思います。

タイトル通り、テーマは「API 経由で起動する Shopify Flow のトリガーを自作してみる」です。

Shopify Flow とは

Shopify のエンタープライズプラン「Shopify Plus」 を利用すると使えるようになるオートメーション機能の事。

Shopify で構築した自身のストア上における顧客の行動をトリガーに様々なアクションを自動で行ってくれます。

Trigger → Condition → Action

  • Trigger(トリガー)
    • ワークフローを開始させるイベント
  • Condition(条件)
    • アクションを実行するかどうかの条件
  • Action(アクション)
    • 条件が満たされた時に行われる内容
例)
・ ある商品の在庫数が◯個以下になった際に Slack 通知を飛ばす
・ ○円以上の買い物をした顧客に対して特別なタグを付与する
・ 顧客が新規登録されるたびにそのデータをスプレッドシートに記録する

https://apps.shopify.com/flow

この Shopify Flow を回し始めるには「トリガー」と呼ばれるきっかけが必要になるのですが、Shopify が標準で用意しているトリガーは基本的にストア上の顧客行動が対象となっているため、それ以外の要因で引き金を引く事が難しくなっています。

もちろん、特に奇を衒った事をしようとしなければそれでも十分だと思いますが、個人的に「外部からちょこっと API を叩くだけで任意に起動できるトリガーを作れないかなぁ」みたいな願望があったため、色々調べてみました。

結果、なんとかなりそうな方法があったので紹介してみたいと思います。

API 経由でワークフローを回す事ができるようになれば、Shopify ストアとは別の自社アプリ内などに組み込んで色々と連携が捗ったり運用の幅が広がるかもしれません。

カスタムアプリを作成

https://www.shopify.com/jp/partners

Shopify Parnters のダッシュボードに入り、「アプリ管理」→「すべてのアプリ」へと進み、「アプリを作成する」ボタンをクリックします。

  • カスタムアプリ
  • 公開アプリ

の2種類ありますが、今回は「カスタムアプリ」の方を選択してください。

諸々入力したら、右上の「アプリを作成」ボタンをクリックしましょう。

トリガーを作成

カスタムアプリの作成に成功するとアプリ詳細画面に飛ぶはずなので、「拡張機能」という部分をクリックしてください。

「作成+」をクリック。

拡張機能タイプは「Flow/トリガー」を選択してください。

適当な名前をつけて「Save」

トリガーの設定を行うために「Working draft」をクリック。

  • トリガーのタイトル
    • 任意
  • トリガーの説明
    • 任意
  • プロパティ
    • お客様

プロパティについては、デフォルトの状態だと

  • お客様
  • 注文
  • 商品

といった3種類が用意されており、Trigger 起動時のリクエストに

  • customer_id
  • order_id
  • product_id

を含める事で Action 実行時に shopify 上のオブジェクトを参照する事ができます。

https://shopify.dev/apps/flow/triggers#shopify-properties

When you create a trigger, you add the properties that your trigger sends to Shopify Flow. You can add a custom property or a predefined Shopify property. Shopify property lets you send the identifier of a Shopify resource to Shopify Flow. Merchants can then use the entire resource in their conditions and actions. You can add one of each Shopify property to a trigger: For example, your trigger sends a customer ID to Shopify Flow. The merchant can create a condition that checks the customer's total spend amount. In their action, the merchant can include the template variables for customers (such as {{customer.email}}).

設定が済んだら右上の「Create version」をクリックし、「Minor version」を選択して「Create」

作成が完了したら、「Publish」をクリックしましょう。

アクセストークンを取得 & ストアにインストール

諸々の準備が整ったので、自身のストアに先ほど作ったカスタムアプリをインストールしていきます。

と、その前に、カスタムアプリと通信を行うためのアクセストークンを取得しなければなりません。

アクセストークンの取得方法は色々ありますが、今回は

  • Ruby
  • Ruby on Rails
  • ngrok

を使って Web サーバーを準備し、そこで取得していきます。

各種ディレクトリ & ファイルを作成

$ mkdir shopify-access-token-getter && cd shopify-access-token-getter
$ touch Dockerfile docker-compose.yml entrypoint.sh Gemfile Gemfile.lock
FROM ruby:3.0

RUN curl https://deb.nodesource.com/setup_14.x | bash

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs yarn

ENV APP_PATH /myapp

RUN mkdir $APP_PATH
WORKDIR $APP_PATH

COPY Gemfile $APP_PATH/Gemfile
COPY Gemfile.lock $APP_PATH/Gemfile.lock
RUN bundle install

COPY . $APP_PATH

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
version: "3"
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - mysql-data:/var/lib/mysql
      - /tmp/dockerdir:/etc/mysql/conf.d/
    ports:
      - 3306:3306
  api:
    build:
      context: .
      dockerfile: Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
      - ./vendor/bundle:/myapp/vendor/bundle
    environment:
      TZ: Asia/Tokyo
      RAILS_ENV: development
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  mysql-data:
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails", "~> 6"
# 空欄でOK

rails new

APIモードで作成します。

$ docker-compose run api rails new . --force --no-deps -d mysql --api

database.ymlを編集

デフォルトの状態だとデータベースとの接続ができないので「database.yml」の一部を書き換えます。

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password # デフォルトだと空欄になっているはずなので変更
  host: db # デフォルトだとlocalhostになっているはずなので変更

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

production:
  <<: *default
  database: <%= ENV["DATABASE_NAME"] %>
  username: <%= ENV["DATABASE_USERNAME"] %>
  password: <%= ENV["DATABASE_PASSWORD"] %>

コンテナを起動 & データベースを作成

$ docker-compose build
$ docker-compose up
$ docker-compose run api bundle exec rails db:create

localhost:3000 にアクセス

スクリーンショット 2021-07-24 0.30.48.png

localhost:3000 にアクセスしてウェルカムページが表示されればOKです。

ngrok を起動

# 追記
config.hosts << ".ngrok.io"
$ ngrok http 3000

https:// を割り当てるために上記コマンドで起動します。

アクセストークン取得用の処理を記述

gem "faraday"
$ docker-compose build

HTTP クライアントの「faraday」をインストールしてきましょう。

$ docker-compose run api bundle exec rails g controller api/shopify
class Api::ShopifyController < ApplicationController
  def auth
    shop = params[:shop]
    client_id = "CUSTOM_APP_API_KEY" # カスタムアプリの API キー
    scope = "write_customers,read_customers"
    redirect_uri = "https://************.ngrok.io/api/shopify/token" # ngrok で起動したサーバーの URL
    state = SecureRandom.random_bytes(16)

    query = {
      client_id: client_id,
      scope: scope,
      redirect_uri: redirect_uri,
      state: state
    }.to_query

    url = "https://#{shop}/admin/oauth/authorize?#{query}"

    redirect_to url
  end

  def token
    shop = params[:shop]
    code = params[:code]

    client_id = "CUSTOM_APP_API_KEY"            # カスタムアプリの API キー
    client_secret = "CUSTOM_APP_API_SECRET_KEY" # カスタムアプリの API シークレットキー

    res = Faraday.post("https://#{shop}/admin/oauth/access_token",
      client_id: client_id,
      client_secret: client_secret,
      code: code
    )

    p res.body
  end
end

アクセストークン取得用の処理をコントローラーに書いていきます。

ルーティングを定義

Rails.application.routes.draw do
  get "api/shopify/auth", to: "api/shopify#auth"
  get "api/shopify/token", to: "api/shopify#token"
end

これで

にアクセスした際に先ほど定義した処理が走るようになりました。

アプリ URL & リダイレクト URL を変更

https://localhost/」 で暫定的に埋めていた

  • アプリ URL
  • リダイレクト URL

を先ほど立ち上げた Web サーバーのものに置き換えます。

インストールリンクを作成

諸々の準備ができたので、いよいよカスタムアプリをストアにインストールしていきます。

インストール先のストアドメインを入力し、「リンクを生成する」をクリックしてください。

あとは生成されたリンクをコピーし、アドレスバーにペーストしてアクセスしてみましょう。

上手くいけばこんな感じのインストール画面が出てくるので、右上の「アプリをインストール」をクリック。

インストールが完了したら、アプリケーションのログを見てみましょう。

"{\"access_token\":\"shpca_********************\",\"scope\":\"write_customers\"}"

shpca_が接頭辞として付いている文字列がアクセストークンなので、メモに控えておいてください。

ワークフローを作成

ストアにカスタムアプリをインストールできたら、ワークフローを作成します。

ワークフロー作成画面が開いたら、トリガーとして先ほど作成したカスタムアプリから「Test Trigger」を選択してください。

次に、アクションとして「Add customer tags」を開き

  • test-trigger

適当な文字列を入力。

トリガーとアクションの設定が終わったら、右上の「Turn on workflow」からワークフローを稼働させ始めます。(条件(Condition)は今回割愛しました)

動作確認

curl -X POST "https://*******.myshopify.com/admin/api/2022-04/graphql.json" \
-H "Content-Type: application/graphql" \
-H "X-Shopify-Access-Token: shpca_******************************" \
-d '
mutation {
  flowTriggerReceive(body: "{
    \"trigger_id\": \"**********-****-****-****-**********\",
    \"properties\": {
      \"customer_id\": *****************
    }
  }") {
    userErrors {
      field,
      message
    }
  }
}'
  • *******.myshopify.com
    • 自身のストアドメイン
  • X-Shopify-Access-Token
    • 事前に取得したアクセストークン
  • trigger_id
    • トリガーID
  • customer_id
    • 対象の顧客ID

トリガーIDについては、「カスタムアプリ」→「拡張機能」→「Test Trigger」→「Edit draft」を開き、ペイロードのプレビュー という部分を確認してください。

上記 Curl コマンドを実行し、

{
    "data": {
        "flowTriggerReceive": {
            "userErrors": []
        }
    },
    "extensions": {
        "cost": {
            "requestedQueryCost": 1,
            "actualQueryCost": 1,
            "throttleStatus": {
                "maximumAvailable": 2000,
                "currentlyAvailable": 1999,
                "restoreRate": 100
            }
        }
    }
}

こんな感じのレスポンスが返って来ればOKです。

customer_id で指定した顧客情報も確認してみてください。「test-trigger」というタグが付与されているはず。

これで API 経由で Shopify Flow を起動させる事に成功しました。

あとがき

以上、Shopify Flow のトリガーを自作してみました。

API 経由で起動できる事から、Shopify ストア以外の場所でも何かしらのアクションを起こせるようになり、活用の幅が広がりそうな気がします。 何かのお役に立てれば幸いです。

Railsのbulk insert事情

negishiです。
みなさんは、Railsでアプリケーションの機能やバッチ処理でデータをまとめてinsertする場合どうしていますか?

選択肢としては

の2択になると思いますが、できる事が微妙に異なるのでざっくりまとめました。

TL;DR

*1

insert_all activerecord-import
実行速度
batch size指定 ×
自動timestamp ○ (ver. 7 ~)*2
arg:hash
arg:model objects ×
association ○ (ver. 6.1 ~)*3
validation ×
callback ×

特に理由がない限り*4activerecord-import使いましょう。(執筆時点 Rails ~ 7.0.4)

実行速度

下記コードでベンチマークしました。*5

require 'benchmark'

class Users < ApplicationRecord
  def self.bulk_insert_benchmark
    hash_data = 10_000.times.each_with_object([]) do |_, users|
      users << {name: 'hoge太郎', email: 'hoge@example.com'}
    end

    model_data = 10_000.times.each_with_object([]) do |_, users|
      users << User.new(name: 'hoge太郎', email: 'hoge@example.com')
    end

    Benchmark.bm 30 do |r|
      r.report "insert_all" do
        ActiveRecord::Base.transaction do
          User.insert_all hash_data, record_timestamps: true
          raise ActiveRecord::Rollback
        end
      end

      r.report "import hash + columns" do
        ActiveRecord::Base.transaction do
          columns = %i(name email)
          User.import columns, hash_data
          raise ActiveRecord::Rollback
        end
      end

      # この場合、配列内のhashが一貫している必要があるので注意してください。
      # https://github.com/zdennis/activerecord-import/issues/507
      r.report "import hash" do
        ActiveRecord::Base.transaction do
          User.import hash_data
          raise ActiveRecord::Rollback
        end
      end

      r.report "import model objects" do
        ActiveRecord::Base.transaction do
          User.import model_data
          raise ActiveRecord::Rollback
        end
      end
    end
  end
end

それぞれの実行速度は下記です。*6
なお、transactionが計測範囲に入ってしまっています。(再計測しないという強い意志)

user system total real
insert_all 0.775897 0.010493 0.786390 ( 1.673050)
import hash + columns 1.835435 0.021656 1.857091 ( 2.724070)
import hash 2.055377 0.150236 2.205613 ( 2.610810)
import model objects 2.807487 0.033700 2.841187 ( 3.461411)

insert_allを使う上での注意点

  • batch sizeの指定が現行のversion(~ 7.0.4)ではできません!(痛い)
  • created_atやupdated_atのtimestampを自動で入れられるのはRails7系からなので、Rails6系の場合は明示的にhashに含める必要があります。
  • 重複キーの場合、そのレコードはスキップされます。*7
  • 直接SQLを実行する為、validationやcallbackは使えません。

所感

insert_allの実行速度が一番早いですが、これまでの内容を踏まえるとまだ使用する場面はかなり限られるかな...といった感じです。 しばらくは引き続きactiverecord-importのお世話になりそうです。

*1:activerecord-importでできて、insert_allでできない基準で書いてます。

*2:https://github.com/rails/rails/pull/43003

*3:https://github.com/rails/rails/pull/38899

*4:どうしても"Railsで"、かつbatch sizeを気にしなくてよく、とにかく早くinsertしたい!とかでなければ

*5:本当は1000万件↑で計測したかったですが、メモリーやらMySQL側のpacket size等々考慮するのがめんどくさいので妥協しました。

*6:各10回試行した結果の平均を算出。見やすさ重視で当該コードは省略しています。

*7:insert_all!にすれば例外発生します。

Ruby on Rails Model関連のクラスで定義するscopeとクラスメソッドの違いについて

自己紹介

皆さんこんにちは。ohashi_tと申します。

弊社ではバックエンドにRuby on Railsを採用しているプロジェクトが多数あり、 日々押し寄せる業務要件に応え、アプリケーションがスケールしていく中でも絶えず改善が行われております。

私も日々の業務を通じて様々な要件に対応する機会があり、 その都度Ruby on Railsに対する理解が段々と深まって来ていると 実感を得られています。 今回、その中で特に印象に残っている 「Model関連のクラスで定義するscopeとクラスメソッドの違いについて」 事例を1つご紹介したいと思います。

はじめに

scopeとクラスメソッドの違いについてググるとこれに関する記事が沢山出てきます。 しかし、個人的には誤解のある表現が多いと感じ なかなか想定する動作を実現出来ず、振り回された経験がありました。 この度記事としてアウトプットさせていただき、 少しでも皆様のお役に立てるよう願っております。

今回扱うデータはこちらになります。

irb(main):001:0> Weather.all
  Weather Load (0.4ms)  SELECT `weathers`.* FROM `weathers`
=>
[#<Weather:0x000000010fc8a4a0
  id: 2,
  temperature: 21.4,
  place: "Bankok",
  status: "cloudy",
  observed_at: Sat, 01 Apr 2017 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:50:52.170025000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:50:52.170025000 UTC +00:00>,
 #<Weather:0x000000010fde3978
  id: 3,
  temperature: 20.6,
  place: "Tokyo",
  status: "rainy",
  observed_at: Fri, 01 Jun 2018 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:37.625832000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:37.625832000 UTC +00:00>,
 #<Weather:0x000000010fde38b0
  id: 4,
  temperature: 34.7,
  place: "Sydney",
  status: "sunny",
  observed_at: Thu, 01 Aug 2019 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:46.206855000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:46.206855000 UTC +00:00>,
 #<Weather:0x000000010fde36f8
  id: 5,
  temperature: 27.8,
  place: "Newyork",
  status: "windy",
  observed_at: Thu, 01 Oct 2020 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:54.038531000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:54.038531000 UTC +00:00>,
 #<Weather:0x000000010fde3630
  id: 6,
  temperature: 0.0,
  place: "London",
  status: "snowy",
  observed_at: Wed, 01 Dec 2021 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:54:01.782352000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:54:01.782352000 UTC +00:00>]

多分一番分かりやすいと思われる違い

何もしないクラスメソッド・scopeの比較

早速定義して動作を確認してみましょう。

class Weather < ApplicationRecord
  .....

  scope :do_nothing_by_scope, -> {}

  def self.do_nothing_by_class_method; end
end
scopeを使用した場合
irb(main):001:0> Weather.do_nothing_by_scope
  Weather Load (0.6ms)  SELECT `weathers`.* FROM `weathers`
=>
[#<Weather:0x0000000113a6ff90
  id: 2,
  temperature: 21.4,
  place: "Bankok",
  status: "cloudy",
  observed_at: Sat, 01 Apr 2017 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:50:52.170025000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:50:52.170025000 UTC +00:00>,
 #<Weather:0x0000000113be6f40
  id: 3,
  temperature: 20.6,
  place: "Tokyo",
  status: "rainy",
  observed_at: Fri, 01 Jun 2018 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:37.625832000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:37.625832000 UTC +00:00>,
 #<Weather:0x0000000113be6e78
  id: 4,
  temperature: 34.7,
  place: "Sydney",
  status: "sunny",
  observed_at: Thu, 01 Aug 2019 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:46.206855000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:46.206855000 UTC +00:00>,
 #<Weather:0x0000000113be6db0
  id: 5,
  temperature: 27.8,
  place: "Newyork",
  status: "windy",
  observed_at: Thu, 01 Oct 2020 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:54.038531000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:54.038531000 UTC +00:00>,
 #<Weather:0x0000000113be6ce8
  id: 6,
  temperature: 0.0,
  place: "London",
  status: "snowy",
  observed_at: Wed, 01 Dec 2021 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:54:01.782352000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:54:01.782352000 UTC +00:00>]

どうやら戻り値が偽(false, nil)であればActiveRecord_Relationsのインスタンス群を返すようです。

クラスメソッドを使用した場合
irb(main):002:0> Weather.do_nothing_by_class_method
=> nil

一方クラスメソッドで定義した場合はそのまま戻り値が返ります。

調べてみたところ、どうやらここのinstance_exec(*args, &block) || selfの箇所で||演算子が使用されているため、lambdaで定義された評価結果がnilもしくはfalseの場合にselfを返すようです。

github.com

    def _exec_scope(name, *args, &block) # :nodoc:
      @delegate_to_klass = true
      _scoping(_deprecated_spawn(name)) { instance_exec(*args, &block) || self }
    ensure
      @delegate_to_klass = false
    end

気をつけなければいけないパターン

条件分岐をする場合

弊社のプロジェクトでもよく見るパターンですが、 例えばデフォルト引数でnilを宣言しておき、空でない値が指定された時のみ絞り込みを行うパターンが考えられます。

def self.filter_by_class_method(status = nil)
  where(status: status) if status.present?
end

scope :filter_by_scope, ->(status = nil) { where(status: status) if status.present? }

引数が空でない値が指定されれば同等の出力結果が期待出来ますが、 引数を指定せず呼び出した場合は先程の例のように差異が発生してしまいます。

もしscopeと同等の結果を得たいならば下記のように引数が指定されなかった場合の条件も明示する必要があります。

def filter_by_class_method(status = nil)
  return where(status: status) if status.present?

  all
end

scopeを使用する際のイメージとしては、幾つかメソッドチェーンして抽出条件を絞り込んでいくのがscopeと言う名称と役割がマッチしていて用途が適切なのではないかと思います。

そのためには全ての条件で戻り値がActiveRecord::Relationsを返すように工夫する必要があります。 注意しなければいけないのは全てのActiveRecordに関するクエリがActiveRecord::Relationsを返すわけではなく、 例えばfirsttakeは戻り値にインスタンスや配列を返す(ActiveRecord::Relationsを返さない)ため、 メソッドチェーンさせることが出来ません。

このようなパターンでは役割の違いを明確にするため、 クラスメソッドを使用するのが良いのではないかと思います。

ちなみにscopeでfind_byを使用する場合は更に注意が必要で、 普段であれば該当するレコードが見つかればそのレコードに関するインスタンスを返す、無ければnil..... のはずがnilではなくレシーバーで評価され得るインスタンス群全てが返ってくるので 挙動の違いをしっかりと把握していないと想定外のエラーに見舞われます。

scopeで1件も返したくない場合

ActiveRecordではnoneというメソッドがあり、空のActiveRecord::Relationsを返します。こちらはrubyで例えると空配列に相当するので、 もしこのようなケースではこちらを使用するのが良いと思います。

scopeでのブロックの役割はメソッドと異なる

scopeでのブロックの役割はscopeが適用されたActiveRecord_Relationsのみに追加で名前空間を提供 (そのscopeを介したActiveRecord::Relationの集合体だけに更にメソッドやscopeを追加) することが出来ます。

このようにscopeではブロックの役割が既に決まっておりクラスメソッドで用いていた要領でブロックを用いることが出来ません。

普段使用しているような要領でメソッド内のタイミングでブロックの評価をさせたい場合なども クラスメソッドを使用する方が良いのではないかと思います。

さいごに

「scopeはクラスメソッドに比べて短く宣言出来て素敵~」 みたいにscopeの意図を十分に掴めていないままアウトプットに踏み切った記事が見られましたので 上記で説明したように

「クラスメソッドとscopeでは挙動や用途が異なる」

ということをしっかりと理解された上で使い分けることをおすすめします。

最後にこちらが今回取り上げたscopeに関するソースコードです。ざっと目を通してみましょう。

github.com

def scope(name, body, &block)
  unless body.respond_to?(:call)
    raise ArgumentError, "The scope body needs to be callable."
  end

  if dangerous_class_method?(name)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but Active Record already defined " \
      "a class method with the same name."
  end

  if method_defined_within?(name, Relation)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
      "an instance method with the same name."
  end

  extension = Module.new(&block) if block

  if body.respond_to?(:to_proc)
    singleton_class.define_method(name) do |*args|
      scope = all._exec_scope(*args, &body)
      scope = scope.extending(extension) if extension
      scope
    end
  else
    singleton_class.define_method(name) do |*args|
      scope = body.call(*args) || all
      scope = scope.extending(extension) if extension
      scope
    end
  end
  singleton_class.send(:ruby2_keywords, name)

  generate_relation_method(name)
end

この辺はガード説ですね。同名のメソッドが既に定義されていたり、Procクラスでは呼べるであろうcallを持って無ければここで弾かれます。

  unless body.respond_to?(:call)
    raise ArgumentError, "The scope body needs to be callable."
  end

  if dangerous_class_method?(name)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but Active Record already defined " \
      "a class method with the same name."
  end

  if method_defined_within?(name, Relation)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
      "an instance method with the same name."
  end

先程も説明したとおり、scope独自の名前空間の作成に使用されます。

extension = Module.new(&block) if block

そしてこのタイミングでメソッドが定義されて、 呼び出された時にdo...end内が評価されるんですね。


if body.respond_to?(:to_proc)
  singleton_class.define_method(name) do |*args|
    scope = all._exec_scope(name, *args, &body)
    scope = scope.extending(extension) if extension
    scope
  end
else
  singleton_class.define_method(name) do |*args|
    scope = body.call(*args) || all
    scope = scope.extending(extension) if extension
    scope
  end
end

最後にリレーション間でもメソッドが呼び出せるになるようです。

  generate_relation_method(name)

以上です。ありがとうございました!