TECH PLAY

WESEEK, Inc.

WESEEK, Inc. の技術ブログ

75

こんにちは、システムエンジニアの Kota です。本記事では、 ハンズオン形式で AWS に EC2 を構築して、Docker を install し、Hello world! と表示させてみたいと思います。 対象の読者 開発環境で Docker を使っているけど、デプロイにも使いたい方 Docker、AWS に触れてみたい方 インフラに興味がある方 過去に Docker についての記事を執筆しているので、基礎知識については、 こちら の記事を参照して下さい。 まずは、完成イメージをご紹介します。 完成イメージ 手順としては以下のようになります。 VPCとサブネットの構築 インターネットゲートウェイ(インターネットへの出入口)を作成し、VPC にアタッチする EC2(サーバー)を立てる セキュリティグループ(ファイアウォール)を設定する ルーティングを設定する SSHでEC2(サーバー)にアクセスする EC2 に Docker をインストールする 作業中は、完成イメージを持っているとより理解が深まりやすいと思います。 今回の作業は、AWS のアカウントが必要になりますので、まだお持ちでない方は下記のリンクよりアカウントを作成してください。 無料アカウント作成 ( 12ヶ月無料で EC2 を利用できます。登録にはクレジットカードが必要です。) では、早速やっていきましょう! 1. VPC と サブネットの構築 まずは、VPC と サブネットの構築です。 VPC とは、Virtual Private Cloud の略で、AWS 上での自分のネットワークの領域のことです。サブネットは、VPC の中をさらに細かく分割する領域のことをいいます。今回は、このサブネットの中にEC2を立てたいので、まずはこれらを構築していきます。 イメージ図 AWS にログインしたら、ナビバーからVPCを検索し、VPC ダッシュボードに遷移します。 サイドバーからVPC を選択します。 すると、VPCの一覧画面に遷移します。 右上のVPCを作成ボタンからVPCを作成していきます。 作成画面に遷移すると、下記のようなフォームが表示されます。 まずは、名前タグに作成するVPCの名前を入れていきます。 今回はhello-world-dockerとします。 次に IPv4 CIDR ブロックです。 ここでは、IPv4 CIDR ブロックの詳しい説明は割愛しますが、 簡単に説明すると使用するIPアドレスの範囲を指定しています。 192.168.1.0/24 と指定することで、192.168.1.0 ~ 192.168.1.255 までの IPアドレスを使用することを定義しています。 IPv6CIDR ブロックはなし。テナンシーはデフォルト。 タグは、名前タグを入力すると自動で入力されているかと思います。 入力が完了したら、右下の VPC を作成ボタンをクリックします。 下記のように正常に作成されましたとメッセージが表示されます。 一覧画面に戻ると、作成したVPCが表示されているはずです。 次に作成したVPC内のサブネットを作成するため、サイドバーよりサブネットを選択して下さい。 サブネットの一覧画面に遷移しますので、右上のサブネットを作成ボタンからフォームに移動します。 VPC ID は先ほど作成した VPC (hello-world-docker) を選択します。 サブネット名は分かりやすいように名前をつけます。 今回は hello-world-docker-subnet とします。 アベイラビリティゾーンは、アジアパシフィック(東京) / ap-northaneast-1a を指定します。 アベイラビリティゾーンとは、AWS のデータセンターのことです。 日本には複数のデータセンターが存在します。一般的なサービスにおいては、災害などのトラブルに備え、アベイラビリティゾーンを分け、冗長化を図ります(別々のデータセンターにデプロイしておく)。冗長化をすることで、一つのデータセンターでトラブルが合った場合でも、サービスを継続して提供することが出来ます。今回はそこまでの対応はしません。 IPv4 CIDR ブロックは、192.168.1.0/25、タグオプションは、サブネット名を入力していれば反映されているかと思います。 入力が済んだら、サブネットを作成ボタンを押します。 初めに作成したVPC内にサブネットを作成することが出来ました。 2. インターネットゲートウェイ(インターネットへの出入口)を作成し、VPC にアタッチする 次に、インターネットゲートウェイ(以降、IGW)を作成し、先ほど構築した VPC にアタッチしていきます。 IGWは、文字通り、インターネットへの出入口になります。VPC は、デフォルトの状態だと、インターネットへの出入口がありません。インターネットから隔離された、ただの領域です。今回はブラウザからEC2 にアクセスしたいので、隔離されたVPC に出入口を設けて、インターネットへ繋がるように設定していきます。 イメージ図 VPCダッシュボードのサイドバーから、IGWを選択します。 選択すると、一覧画面に遷移しますので、右上の作成ボタンを押します。 フォーム画面に遷移しますので、IGWの名前を入力します。 今回は hello-world-docker-internet-gateway とします。 入力したら、作成ボタンを押します。 無事作成できました。 一覧画面に行くと先ほどのIGWが表示されていると思います。次に今作成したものを 1. で作成したVPCにアタッチしていきたいと思います。 一覧画面から先ほど作成したIGWのチェックボックスにチェックを入れ、右上のアクションを押すとVPCにアタッチという項目が表示されるので、クリックします。 フォーム画面に遷移するので、使用可能なVPCから 1. で作成したVPCを選択し、作成ボタンをクリックします。 これで、1. の VPC にIGWをアタッチすることができました。 3. EC2(サーバー)を立てる イメージ図 IGWのアタッチが済んだら、次に VPC のサブネット内に EC2 を立てていきます。 EC2 とは、Elastic Compute Cloud の 略で、AWS 上で構築できる仮想サーバーのことです。 まずは、ナビバーでEC2と検索し、ダッシュボードへ遷移します。 サイドバーのインスタンスをクリックします。 ちなみにインスタンスとは EC2 の単位のことです。 インスタンスの一覧画面右上のインスタンスを起動をクリックします。 最初にAMIを選択します。 AMIとは、Amazon Machine Images の 略で、ページの説明にも記載がある通り、作成するインスタンスに必要なソフトウェアを予めテンプレートとして、用意してくれているものです。 今回は一番上の無料枠になっている Amazon Linux 2 AMI (HVM) を選択します。 次にインスタンスタイプの選択です。 こちらはインスタンスのCPU、メモリ、ストレージ、などのスペックを選択するステップです。こちらは無料枠になっている t2.micro を選択します。 チェックを入れたら右下の次のステップをクリックします。 次にインスタンスの詳細設定です。 細かく設定できますが、今回設定するのは、赤枠の部分のみ設定していきます。 ネットワークは 1. で作成した VPC を選択します。 サブネットは、VPC内に一つしか作成していないので、VPCを選択した時点で、自動で選択されているかと思います。 自動割り当てパブリックIP は有効にして下さい。こちらの設定を有効にすることで、AWS の領域から外のインターネットへアクセスすることができます。 設定が完了したら、右下の次のステップをクリックします。 ストレージの追加に関しては、今回特に設定する必要がないので、そのまま右下の次のステップをクリックします。 タグの追加では、EC2 に名前をつけられるので、分かりやすいように設定していきます。 左下のタグの追加をクリックします。 キーに Name、値に hello-world-docker-ec2 と入力しておきます。 入力したら、次のステップをクリックします。 そのまま次のセキュリティグループの設定をしていきます。 4. セキュリティグループ(ファイアウォール)を設定する イメージ図 ここではセキュリティグループの設定を行っていきます。 セキュリティグループとは、いわゆるファイアウォールのことで、AWSでの名称です。こちらでインターネット(AWSの外側)からの通信を許可するようポートの設定(SSH の 22 番と、HTTP の 80 番)をしていきます。 まず、セキュリティグループの割り当ては、新しいセキュリティグループを作成するにチェックを入れます。そして、セキュリティグループ名には分かりやすい名前を付けます。 今回は、hello-world-docker-ec2-security-group としておきます。 説明にはセキュリティグループの説明を入力できます。(英字のみ) デフォルトで設定されている SSH は、この後、ホストから EC2 へ SSH 接続をし、EC2 に Docker を install する為にそのまま残しておきます。 次に http 接続する為の設定をする為、ルールの追加をクリックします。 タイプは、カスタムTCP、プロトコルはそのままTCP、ポート範囲は 80 、ソースはカスタムの 0.0.0.0/0 を指定します。 0.0.0.0/0 と指定することで、全ての IP アドレスからのインスタンスにアクセスすることが許可されます。 注意 今回はあくまでも実験用として http で port を設定していますが、実運用として使う場合は、 https 化して下さい。 設定が完了したら、右下の確認と作成ボタンをクリックします。 クリックすると、 セキュリティグループ hello-world-docker-ec2-security-group は世界に向けて開かれています。 というメッセージと先ほどまで設定してきた内容が表示されているかと思います。 内容を確認したら、右下の起動ボタンをクリックします。 クリックすると、キーペア選択、作成のモーダルが表示されます。 今回は新しいキーペアを作成していきましょう。 新しいキーペアの作成を選択し、RSAタイプにチェックを入れます。 キーペア名は分かりやすいように名前をつけます。 今回は hello-world-docker としました。 入力が済んだら、キーペアのダウンロードをクリックします。 こちらは、EC2 に SSH 接続に必要な秘密鍵になりますので、必ずダウンロードをしてください。また、この秘密鍵は、一度しかダウンロードできない為、大切に保管して下さい。mac の場合、デフォルトだと、~/Downloads にダウンロードされるかと思います。ダウンロードが完了したら、 .ssh フォルダを作成し、そちらに移動させておきましょう。 ターミナルを開いて、下記のコマンドを実行します。 ~ $ mkdir .ssh ~ $ mv ~/Downloads/hello-world-docker.pem ~/.ssh そして、秘密鍵のパーミッションを下記のコマンドで変更します。 $ chmod 600 ~/.ssh/hello-world-docker.pem 上記が完了したら、ブラウザに戻ってインスタンスの作成をクリックします。 上記の画面が表示され、インスタンスの作成が進行します。 一覧画面に戻り、ステータスチェックの欄を見ると、初期化していますと表示されていると思います。しばらく経ってから、上部の更新ボタンを押し、チェックに合格しましたと表示が変われば、作成完了です。 ↓ しばらくして、更新ボタンを押すと、 これで EC2 の作成と、セキュリティグループの設定が完了です。 5. ルーティングを設定する さて、作業も終盤に入ってきました。次にルーティングの設定をしていきます。 イメージ図 先ほど、立てた EC2 に対して、通信先の定義をしていきます。繰り返しになりますが、今回はブラウザからEC2にアクセスして Hello world! と表示させること(レスポンスを返すこと)が目標なので、(EC2側から)インターネットへ通信が向かうように設定をしていきます。 まずは、VPCのダッシュボードに戻りましょう。 ナビバーのサービスをクリックすると、今まで作業を行ったダッシュボードの履歴が表示されますので、VPC をクリックしてダッシュボードへ戻ります。 サイドバーのルートテーブルをクリックします。 一覧画面へ遷移しますので、右上の作成ボタンをクリックします。 フォームに遷移しますので、 テーブルの名前を入力します。 今回も分かりやすいように hello-world-docker-route とします。 VPC には、作成するルートテーブルを適用したい VPC を選択します。 今回は、hello-world-docker です。 入力が完了したら、作成ボタンをクリックします。 作成が完了したら、一覧画面へ戻ります。 作成したルートテーブルにチェックを入れると、詳細が表示されます。 ルートタブを押すとルートを編集というボタンが表示されますので、そちらをクリックします。 フォーム画面に遷移しますので、ルートを追加をクリックします。 送信先は、全ての ip アドレスに対して、通信を返したいので、0.0.0.0/0 と設定します。 ターゲットでは、2. で作成した IGW を選択します。 入力が完了したら、変更を保存します。 変更が保存されたら、下記の画面が表示されますので、そのままサブネットの関連付けをしていきます。サブネットの関連付けタブをクリックし、サブネットの関連付けを編集をクリックします。 ここでは、作成したルートテーブルと関連付けたVPC内のどのサブネットと関連付けるかを設定します。 今回はVPC内に一つしかサブネットを作成していませんので、表示されているhello-world-docker-subnet にチェックを入れ、関連付けを保存します。 これで一通りのルーティングの設定が完了しました。 6. SSH で EC2 に アクセスする 先ほどまでのステップで、AWS のコンソールで行う作業は全て完了しました。 それでは、お手元のPCから構築した EC2 に SSH 接続したいと思います。 と、その前に先ほど構築した EC2 の IP アドレスを確認したいので、もう一度、EC2 のダッシュボードにアクセスします。 一覧画面で hello-world-docker-ec2 にチェックを入れると、詳細が表示されます。 赤枠のパブリック IPv4 アドレスを控えておきます。 次にターミナルを開きます。 先ほどダウンロードした秘密鍵を使って、EC2 に SSH 接続します。 $ ssh -i ~/.ssh/hello-world-docker.pem ec2-user@13.113.91.85 -i [秘密鍵のpath] でその秘密鍵を使って SSH 接続をします。 今回、EC2を作成するにあたって Amazon Linux 2 AMI (HVM) を使いました。Amazon Linux の場合、SSH で接続するデフォルトのユーザーは ec2-user なので、そちらを指定しています。そして、@に続いて、先ほど控えておいた IP アドレスを指定します。 初めて接続する際、そのまま接続を続けるか聞かれますので、その際は yes を入力して下さい。 コマンドを実行して下記のように表示されれば、接続成功です。 7. EC2 に Docker を install する イメージ図 前のステップで、EC2 に SSH 接続されたままの状態になっているかと思いますので、そのまま EC2 に Docker を install していきたいと思います。 まずは、yum の update を行います。 yum とは、Linuxのパッケージ管理ツールのことです。 EC2インスタンスに install されているパッケージを update する為、下記のコマンドを実行します。 [ec2-user@ip-192-168-1-4 ~]$ sudo yum -y update 次に、Docker を install します。 下記のコマンドを実行します。 [ec2-user@ip-192-168-1-4 ~]$ sudo yum -y install docker install が完了したら、Docker を起動します。 下記のコマンドを実行します。 [ec2-user@ip-192-168-1-4 ~]$ sudo service docker start sudo docker info と実行し、下記のような内容が表示されれば、起動に成功しています。 [ec2-user@ip-192-168-1-4 ~]$ sudo docker info Client: Context: default Debug Mode: false Server: Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 0 Server Version: 20.10.7 Storage Driver: overlay2 Backing Filesystem: xfs Supports d_type: true Native Overlay Diff: true userxattr: false Logging Driver: json-file Cgroup Driver: cgroupfs Cgroup Version: 1 Plugins: Volume: local Network: bridge host ipvlan macvlan null overlay Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog Swarm: inactive Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc Default Runtime: runc Init Binary: docker-init containerd version: d71fcd7d8303cbf684402823e425e9dd2e99285d runc version: 84113eef6fc27af1b01b3181f31bbaf708715301 init version: de40ad0 Security Options: seccomp Profile: default Kernel Version: 5.10.96-90.460.amzn2.x86_64 Operating System: Amazon Linux 2 OSType: linux Architecture: x86_64 CPUs: 1 Total Memory: 965.5MiB Name: ip-192-168-1-4.ap-northeast-1.compute.internal ID: T3TS:4QOT:KB35:2USQ:JDUZ:WFIZ:DRXS:43VU:3XPG:I2DZ:K4UU:6GB4 Docker Root Dir: /var/lib/docker Debug Mode: false Registry: https://index.docker.io/v1/ Labels: Experimental: false Insecure Registries: 127.0.0.0/8 Live Restore Enabled: false 今のままだと、docker コマンドを打つ際に、 sudo を付けなければなりません。 これは、ec2-user にdocker コマンドの操作権限がない為です。 ec2-user に操作権限を与える為、以下のコマンドを実行します。 sudo usermod -a -G docker ec2-user 上記のコマンドは、操作権限をもつ docker グループに ec2-user を加えるコマンドです。詳しく知りたい方は、 公式ドキュメント や、 こちら の記事を参照して下さい。 グループの追加は、シェルを起動し直さないと反映されないので、一度 exit でサーバーからログアウトし、ログインし直して下さい。 sudo なしで、 docker info と実行し、先ほどと同じ内容の出力がされれば、グループへの追加が成功しています。 グループへの追加ができたら、作業用のディレクトリを作成し、ディレクトリ内に移動します。 [ec2-user@ip-192-168-1-53]$ mkdir hello-world-docker [ec2-user@ip-192-168-1-53]$ cd hello-world-docker/ そして、Hello world! と表示させる為の html ファイルを用意します。 [ec2-user@ip-192-168-1-53 hello-world-docker]$ vi hello-world.html vi が開きますので、 i で insert mode にし、 Hello world! と記述して、esc キー、 :wq で保存します。 ls コマンドでディレクトリに今作成した html ファイルが作成されていると思います。 [ec2-user@ip-192-168-1-53 hello-world-docker]$ ls hello-world.html 次にDockerimage を作成する為に、Dockerfile を作成、編集していきます。 今回は、webサーバーに nginx を使用します。先ほど作成した、html ファイルを nginx 上で表示させます。 [ec2-user@ip-192-168-1-53 hello-world-docker]$ vi Dockerfile Dockerfile FROM nginx COPY ./hello-world.html /usr/share/nginx/html/ nginx では、デフォルトの状態だと /usr/share/nginx/html/ 配下がブラウザからアクセスした際の初期表示になっているので、 ./hello-world.html をコピーします。 編集出来たら、先ほどと同じように保存して下さい。 そして、作成したDockerfile を元に image を build します。 下記のコマンドを実行します。 [ec2-user@ip-192-168-1-53 hello-world-docker]$ docker build -t hello-world-docker-ec2 . build が完了したら、container を起動します。 下記のコマンドを実行して下さい。 [ec2-user@ip-192-168-1-53 hello-world-docker]$ docker run --rm -d -p 80:80 hello-world-docker-ec2 今回はあくまで実験として作業をしているので、 --rm オプションをつけてコンテナ終了時に削除していますが、本番を想定する場合は、オプションを付与しなくても良いかも知れません。 docker ps コマンドでコンテナの STATUS が UP になっていれば、起動に成功しています。 [ec2-user@ip-192-168-1-53 hello-world-docker]$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0742571c5bf3 hello-world-docker-ec2 "/docker-entrypoint.…" 14 seconds ago Up 12 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp romantic_greider ここまでで、全ての作業が完了です。 それではブラウザからアクセスしてみましょう。 控えておいた IP アドレス / hello-world.html でアクセスします。 無事に Hello world! と表示させることができました! さて、如何だったでしょうか? 今回は、AWS に EC2 を立て、その中で Docker コンテナを起動させ、ブラウザからアクセスすることをハンズオン形式でやってみました。私自身もインフラに関して、まだまだ勉強中で今回の内容は、かなり初歩的な内容だったかと思います。次の機会には、もう少し発展した内容を書いてみたいと思います。 最後まで読んで頂き、ありがとうございました!
アバター
みなさんこんにちは!WESEEK 戦略企画室 コミュニティーマネージャーの太田です。 前回の「 GROWI Users Meetup、開催までの道のり 前編 」に引き続き、今回も GROWI Users Meetup について、主催者側・コミュニティーマネージャー目線で語っていければと思っています。 前編をまだ読んでいないよ!という方がいらっしゃったら、ぜひそちらを読んでから後編をお楽しみいただければと思います。 ユーザー会開催までの歩み ~夏から冬~ 開催に待った!をかけたのは… 日本がオリンピックの開催にソワソワし始めた7月頃。 Slack のチャンネルも整備できたし、イベントの進行案を固めつつそろそろ協力者を募集したいな~と思っていた矢先、そこに待った!をかけたのは当時鋭意開発中だった GROWI Bot でした。 GROWI Botとは、WESEEK が提供する GROWI の操作を支援するSlack Appです。Slack と GROWI を連携することで、よりシームレスな文章作成・共有が可能となります。 詳細につきましては、 Slackと社内wikiを連携させるGROWI Botとは? や GROWI Docs / Slack連携 をご覧ください。 GROWI Botの魅力を広くユーザーの皆さんにお伝えするためにも、「 最初の GROWI Users Meetup は、 GROWI Bot のリリースを記念した回にしよう! 」という方針が決定。 当時の議事録 GROWI Botのリリースを待って9月後半~10月ぐらいにはユーザー会をやりたい、と当時の議事録には記されておりました。 しかし、GROWI Botの開発が予想よりも遅れてしまったことで、GROWI Users Meetupの開催もどんどん先延ばしに…。 気づけば季節は秋から冬に変わっていったのです。 コミュニティーマネージャーとしてユーザー会を進めていかなきゃ!という焦りとは裏腹に、なかなか終わらないGROWI Botの開発。 GROWI 村議の度に「いつ頃リリースできますか?」と聞くことになってしまい、開発チームの皆さんにはちょっと申し訳なかったです。 「もうすぐリリースできそう」も「しばらくかかりそう」も何回も聞きました。 GROWI Botリリース! そして昨年の12月。遂にGROWI Botが正式にリリースされました! GROWI Botの機能の一つ、Slack 上でのGROWI全文検索機能 ちなみに、GROWI Botが開発をスタートしてから無事リリースされるまでにはかな~りの紆余曲折があったのですが…。 その記録は、来週開催されるWESEEK Tech Conference「 Slack社にも却下された変態的Slackbotの作り方 」で発表されるそうです。一体GROWI Botの開発に何が起きたのか、興味ある方は是非ご参加ください。 急ピッチで準備 GROWI Botのリリースと同時に プレスリリース を公開、そしてずっと温めていた GROWI Users Meetup の開催も告知することに。 ちなみに、GROWI Users Meetup の開催予定日は 12月20日 だったのですが、最初の告知を行ったのはなんと 12月3日 ! 本番まで残り約2週間しかない中、急ピッチで準備を進めなくてはいけません。 私一人では到底準備できないので、GROWI開発メンバーの中から担当者を決めて当日までの準備を進めていきました。 左から宮沢さん・増山さん・一松さん 第1回 GROWI Users Meetupにご協力いただいたのは、インターン生の宮沢さん・増山さん、そして社員の一松さん! かなりタイトなスケジュールではありましたが、皆さん日々の業務の傍らスライド作成や発表原稿の作成、リハーサルなどに協力していただきました。ありがとうございます。 GROWI Users Meetup当日の様子 今回のGROWI Users Meetupは、このようなプログラムでお届けしました。 最近のGROWIのアップデート&今後の開発予定紹介 (宮沢) 開発者が語る、GROWI Botの魅力 (増山) オープン GROWI 村議 (一松) 当日は発表者側の様子を Zoom で配信しつつ、参加者の皆さんとは Slack で交流を行いました。 発表へのリアクションやツッコミ、疑問点などなど、WESEEKからの発表中もとても活発にコミュニケーションがなされていました! GROWI Bot の紹介では実際に Slack 上でBotの操作を行い、検索結果の共有や unfurl の表示を実際に参加者の方に見ていただきながら発表を行いました。 当日の配信画面、コミュニケーションはSlackで! 最後のプログラムである GROWI オープン村議 では、実装検討中のPDFビューワーとショートカットキーについて、ユーザーの方も交えて議論を行いました。 当日の議事録は こちら から閲覧できます! 初参加のユーザーさんが会議で発言をするのはちょっとハードルが高いかも…ということで、気軽に意見を表明できるよう PDFビューワーのデザインについて Slack 上でアンケートを実施! 検討中のPDFプレビュー機能について、アンケートを実施しました もう一つのショートカットキーの話題では、あったら嬉しいショートカットキーについて皆さんから様々な要望をいただきました。 ユーザーさんからいただいた要望を元に、GROWI 上で / を入力すると検索窓にフォーカスされるショートカットキーがユーザー会の4日後に早速追加されました! https://github.com/weseek/growi/pull/5006 GROWI Users Meetup を終えて ということで、構想から約1年!ついに第1回 GROWI Users Meetupを開催することができました。 GROWI Users Meetupを通して、WESEEK の関係者はもちろん、ユーザーさんからも GROWI に関するコメントをいただいたり、アンケートで貴重なご意見をいただくことができました。 GROWIの開発を行っているエンジニアの皆さんからも「ユーザーの反応を見ることができて楽しかった」「今後の開発への意欲が沸いた」という感想をいただきました。 その他、GROWI Users Meetupを通してGROWI 開発陣のチーム意識にも変化があったようです! 代表の武井さんからコメントをいただきました。 日常でのチームの意識にも変化が芽生えたのは副次的な収穫でした。 GROWI の開発はアジャイルでやっているとはいえ、受託開発のように納期があるものではないのでどうしてもチームのスプリント意識が希薄になってしまうという課題は以前からありました。やっぱり追い立てられるものがないと人間急げないんですよね… 今回の Meetup がスケジュールされたことで、ある種の成果発表期限をメンバー一人一人が設定し、それに向けて精力的に取り組む勢いが生まれました。 自分たちが欲しいプロダクトを自分たちで作り、それに共感してくれるユーザーさんに届けるという原点の意識を、Users Meetup をやることで再確認できたんじゃないかなと思います。 今後の予定 今後も1か月に1回程度の開催を目指して、GROWI Users Meetupを通してユーザーさんとの交流を広げていけたらと思っています。 次回の開催日はまだ未定なのですが、ぜひ皆さんも こちら から GROWI の Slack ワークスペースに参加して、GROWI Users Meetupの最新情報をチェックしてみてください!
アバター
GROWI開発チームの皆さん みなさんこんにちは!WESEEK戦略企画室 / コミュニティーマネージャーの太田です。 普段は WESEEK Blog の方で社内イベントや社内施策について発信をしていますが、今回初めて Tech Blog にお邪魔させていただきました! さて、エンジニアではない私がどうして Tech Blog を更新しているかと言いますと、先日開催されました GROWI Users Meetup について、運営に携わったコミュニティーマネージャーの目線で綴ってみようと思い立ったからです。 「GROWI Users Meetup、開催までの道のり」 と題しまして、前編・後編に分けてイベントの裏側をお届けしていければと思います! GROWIとは?GROWI Users Meetupとは? GROWI GROWI とは、WESEEK が中心となって開発を行っている OSS の Wiki システムです。 エンジニアの皆さんになじみ深いマークダウン形式での記述、そして柔軟な階層構造での情報管理が可能です。 オンプレミス環境や各種クラウド VM に導入でき、社内 Wiki やオープン Wiki としてご活用いただいております。 専門的な知識がなくても、GROWI を簡単に運用・管理できるサービス GROWI.cloud も提供しております! GROWI 公式サイト: https://growi.org/ja/ GROWI.cloud 公式サイト: https://growi.cloud/ GROWI Users Meetup そして今回ご紹介する GROWI Users Meetup は、最近の GROWI のアップデート紹介・今後の開発予定などを発表しつつ、オープン村議(会議)を通して GROWI のユーザーさん同士や開発者が交流を深めることができる機会として企画されたイベントです。 ゆくゆくは、GROWI のユーザーさんのコミュニティが活発に活動していくことを目標としています! 一般的には ユーザー会 と呼ばれるようなジャンルのイベントなのかな?と思うのですが、そのまま『GROWI ユーザー会』はちょっとダサいな~ということで『GROWI User Meetup』と英語にしてみました。 ユーザー会に対する開発側の思い 「GROWI のユーザー会を開催したい!ユーザーコミュニティーを作りたい!」という声は結構前から社内で上がっていたようで、実は過去にもユーザーさんに参加していただけるような GROWI の催しが開催されたことがありました。 GROWI SSO ユーザーミーティングやります! 3/3(火) 16:00~ on Google Hangouts Meet https://t.co/HLdf8DyQs3 #GROWI 開発で初の試み、ユーザーさんと一緒に認証周り(特に SAML, SSO)のあれこれをオンラインで話し合います。 参加、ROM 自由です 入室リクエストいただければ承認します。 — WESEEK, Inc. (@weseek_inc) March 2, 2020 この時は、SSO についてユーザーさんから広く意見を募るというミーティングを行ったようです。(当時の議事録は こちら ) この会以来、ユーザーの方の声を直接聞くイベントはほぼなくなってしまったのですが…。ではなぜ今回、GROWI Users Meetup が実現するに至ったのか?をご紹介していきます。 具体的な準備はなんと今年の春から始まっていました。 ユーザー会開催までの歩み ~春から夏~ コミュニティーマネージャー誕生 今年の春から、 GROWI・GROWI.cloud の認知度を高めるための活動 をより強化していくことになり、GROWI Users Meetupはその中の活動の一つとして計画されていました。 そして、コミュニティーマネージャーとして関連施策の中心人物に任命されたのが4月から社員となった私でした。 1年半インターンをやっていたとはいえ、業務で GROWI や GROWI.cloud にはほとんど関わってこなかったので「GROWIとは何か」という説明すら満足にできない状態だったのを覚えています…。 GROWI ユーザーインタビュー GROWI Users Meetup を開催する前に「 今、ユーザーは GROWI に何を求めているのか? 」を調査すべく、4月から5月頃にかけてユーザーインタビューを行いました。 エンジニアやマーケティング担当のメンバーも交えてインタビューをさせていただいたのですが、「 GROWI をこうやって活用しています!、この機能が便利です! 」というありがたいお話から、「 他の Wiki ツールに比べて GROWI はここが足りない、もっとこんな機能を開発してほしい 」というご意見まで伺うことができ、開発チームはもちろん社内全体が刺激を受けているなーと感じていました。 個人的には、数式を書ける MathJax が便利!という声が多くのユーザーさんから聞かれたことがとても意外でした。 インタビューの中では、GROWIの機能面だけではなく OSS 活動についてもお話を伺っていたのですが、「GROWI の OSS 開発者を増やしていきたい」という WESEEK 側の思いとは裏腹に「 GROWI (その他 OSS のプロジェクト) には気軽にコミットできない 」という意見がユーザーさんから多く聞かれました。 よりコミットしやすい・関わりやすいプロジェクトの雰囲気づくりのためにも、GROWI Users Meetupの開催は急務である…!という結論に至ったのです。 Slackの整備 少し話は逸れてしまいますが、GROWI Users Meetup の準備の一環として GROWI の Slack ワークスペースの整備もこの頃から行っていました。 チャンネル解説ページ 他の Slack コミュニティーの事例を参考に、宣伝のためのチャンネルを追加したり、default チャンネルの追加、GROWI Users Meetup 実況用チャンネルの追加などなど! ユーザーインタビューの結果も加味して、より外部の方が発言しやすいワークスペースになるように現在も整備を進めています。 ちなみに、GROWI の Slack ワークスペースは こちら から参加できます。ぜひ皆さんお気軽に join してみてください~! ということで、ちょっと短いのですが前編はここまで! 次回予告 秋も近づき、GROWI Users Meetup 遂に開催!…と思いきや、開発中のGROWI Botによる待ったがかけられ、季節はいつしか冬に。 Slack 社に翻弄され続けた GROWI Bot 開発陣の努力が実り、遂にリリースの時が訪れる。 プレスリリースと同時に、イベントの告知を行ったのはなんと開催2週間前。 果たして、GROWI Users Meetup は無事に開催できるのか? そして何よりも、GROWI のユーザーは GROWI Users Meetup に参加してくれるのだろうか!? 次回「 GROWI Users Meetup、開催までの道のり 後編 」来週更新予定!お楽しみに!
アバター
この記事は、2021/12/23 に行った WESEEK Tech Conference #16 の内容をまとめたものです。 第16回目のテーマは「激白!GROWI.cloudの可用性向上の取り組み」 顧客に何らかのサービスを提供する上で、日常的なサービス可用性の維持・向上に関する業務は必須課題となります。 本発表では、可用性維持・向上に必要な項目を挙げつつ、弊社が実際に提供しているサービスである GROWI.cloud での実施例をご紹介しました。 目次 GROWI.cloud の現状の運用規模 本発表時(2021/12/23 時点)の、GROWI.cloud の規模を示すデータをご紹介しました。 総ノード数: 56 ユーザアプリ数 GROWI: 318 HackMD: 111 Keycloak: 15 可用性(SLI/過去1か月) 通常ノード上の GROWI: 99.995% 全ノード上の GROWI: 99.846% アジェンダ サービス運営の上で必要な監視項目とは SRE の考え方から、本当に監視すべき項目を設定する サービスの可用性を計測・可視化する手法  1. を実現するために必要な情報を揃える手段を実装する 可用性を維持/向上するために必要な取り組み 1./2. を用意した上で、実際にどのような取り組みを実施しているか 1章: サービス運営の上で必要な監視項目とは 監視項目 みなさん、運用しているサービスの監視をどのように実施していますか? 一言に監視項目と言えども、以下のように様々な監視ポイントが思い浮かぶと思います。 闇雲にアラートを出すと アラートの絶対量が人間の対応できる範囲を超える アラートばかりで寝られない そこまで重大じゃないと思われるアラートで起こされる そのアラートはホントに顧客サービスに直接影響するのか? 直接影響しないものもアラートとして出てしまう ほとんどスルーしてよいという認識になり、いずれ対応されなくなる 本当に対応しないといけないアラートが見逃される 運用者は人間 人間が対応できる範囲のアラート量に調整する必要がある サービスの運用者が正しく対応できるようにするための監視・アラートが、必要なときに対応されなかったり、運用者の負担だけを増大化する要因にしてしまいます。 SRE の考え方 上記のような事態を避けるために、 SRE という概念が作り出されました。 Site Reliability Engineering Google が提唱したエンジニアの役割 https://sre.google/ システムの信頼性に焦点を置いている 可用性 がシステム運用における成功の前提条件になっている 以下の用語が定義されている SLO (サービスレベル目標/Service Level Objective) サービスレベルの目標値(ex. 99.9%) SLA (サービスレベル契約/Service Level Agreement) 一定期間で SLO 以上のレベルを満たすことが、利用者との契約に含まれる値 SLI (サービスレベル指標/Service Level Indicator) システムの監視項目が成功した頻度 SLO を下回る場合は何かしらの処置が必要 可用性とは では、成功の前提条件になっている 可用性 とは、どういう定義なのでしょうか。 [1] 可用性(Availability)とは、システムを障害(機器やパーツの故障・災害・アクシデントなど)で停止させることなく稼働し続けること、または その指標 のことをいいます。 長い時間、システムを稼働し続けられることを高可用性(High Availability)ともいいます。 今回の文脈で使われる可用性は、「システムを障害で停止させることなく稼働させるための指標」という意味で捉えられそうですね。 運用で守るべきこと SRE の考え方に沿うと、サービス運用上守るべき事項は以下にまとめられます。 顧客に提供しているサービスが SLO 以上で正常に稼働していること = 可用性を SLO に保つ サービスの裏側で動くミドルウェアの状態が、必ずしも直接顧客サービスに影響するとは限らない ex.) 縮退状態でもサービスは動く(その為の冗長化) SLO 値を算出するために SLI を定義する必要がある GROWI.cloud における SLO GROWI.cloud ではプランごとに 99.4% or 99.9% と設定 Closed β での稼働状況を見つつ、正式リリース時に決定した GCE プリエンプティブルインスタンスを利用したプランでは、24 時間以内に必ず再起動する WESEEK Tech Conference #2 で発表も行っています コスト7割減!Kubernetes本番サービス環境の運用ノウハウ 以下の表を参考に、再起動にかかる時間を考慮し、99.4% という値を設定した 参考) 各 SLO における許容される down 時間 SLO\期間 1日 1か月(30日) 1年(365日) 99.4% 8分38秒 約4時間20分 約52時間半 99.9% 1分26秒 約43分 約8時間45分 GROWI.cloud 上での SLO 表記 GROWI.cloud では、法人プラン/個人プランそれぞれで、各プランにおける SLO を公開しています。 SLO を運用者/利用者で共有することによって、サービスレベルの認識がお互いに合わせられるように努めています。 GROWI.cloud 企業向けプラン紹介ページ GROWI.cloud 個人向けプラン紹介ページ GROWI.cloud における SLI GROWI.cloud 上の GROWI については、以下のような構成で稼働しています。 GROWI.cloud 構成図 SLO を目指すために必要な指標である SLI については、構成図のうち以下の部分から取得できる値を利用しています。 GROWI.cloud 上の SLI 関係部分 まとめ 1章では、SRE の考え方を用い、GROWI.cloud 上での運用方法を考えていきました。 サービス運用で守るべきこと = 顧客がサービスを使える状態であること = 可用性を維持すること ≠ サービス提供に必要なシステム全てが、完璧な状態で稼働していること 可用性の指標 SLO サービスレベルの目標値 SLI 監視項目が成功した頻度 可用性の指標を運用に乗せれば、うまくいきそう 2章: サービスの可用性を計測・可視化する手法 1章で制定した SLO/SLI といった指標を活用した運用体制を整えるため、2章では、 継続的に SLI を計測できているか? 継続的に SLO を達成できているか? という点について、すぐに確認できるような環境を整備する手法をご紹介しました。 可用性の計測・可視化に必要な実施項目 準備が必要となる項目は以下の 2 点です。 監視・可視化ツールの選択 監視ダッシュボードの作成 何をどのように可視化するかを決める システム全体と個別のコンポーネント、それぞれのカットで見られる状態が望ましい ゴールは、「サービス運営に必要な SLI を簡単に確認できること」です。 導入時の前提 GROWI.cloud での導入時の構成は以下の通りでした。 監視・可視化ツールの選択 監視・可視化ツールの候補は、当初以下のようなソフトウェア/サービスが挙げられました。 しかし、マネージドサービスについては、ノード数/コンテナ数による課金体系であったことから、当初から費用をかけたくないという希望と合致せずに、候補から外れました。 GROWI.cloud の事例 GROWI.cloud では、上記の候補のうち「Prometheus」を選択しました。 また、可視化には Prometheus をサポートしている Grafana を選択しました。 選択理由は以下の通りです。 Cloud Native 界隈で話題になっていたから GROWI.cloud が載っている Kubernetes と相性がよさそうだったから Service Discovery 機構がついていたから プロセスの再起動をすることなく、動的に監視ターゲットを増やせる デプロイ手法が整備されていた helm chart(Kubernetes 上にリソースをデプロイするためのマニフェストパッケージ) が既にあった exporter を増やすことで任意のメトリックを溜め、アラートを出すことができるから 採用ツールのご紹介 GROWI.cloud へ導入したツールについて簡単にご紹介しました。 Prometheus Grafana 監視構成 GROWI が稼働している同一クラスタに監視/可視化ツールを加え、以下のように監視を実施することにしました。 GROWI.cloud 上での可視化例 GROWI.cloud では、以下のような可視化のためのダッシュボードを用意しています。 システム全体の情報 こちらのダッシュボードでは、システム全体を俯瞰してすぐに確認できるような情報を表示しています。 以下は表示している項目の一例です。 過去 30 日間の SLI 値 ノードタイプごとに分離して出力している インストール GROWI 数 右上の赤丸部分に、1. の項目が表示されています。 個別コンポーネントの情報 こちらのダッシュボードでは、各アプリ単位でより細かい指標が確認できるような情報を表示しています。 以下は表示している項目の一例です。 各 GROWI ごとの SLI 値、許容される残り down 時間 ミドルウェア情報(CPU/メモリ使用量/コネクション数など) ノード情報(CPU/メモリ使用量など) 上記は、1. の各 GROWI に関する詳細情報の表示例となっています。 まとめ 2章では、1章で制定した SLO/SLI を継続的に確認できるような環境を整備し、どのような構成に至ったのか、どのように確認しているか、についてご紹介しました。 環境整備に必要な実施事項 監視・可視化ツールの選択 監視ダッシュボードの作成 GROWI.cloud では以下の構成で監視を実施している Prometheus/Grafana を選択 全 GROWI / 各 GROWI に関する SLI の値を確認できるようにダッシュボードを用意 GROWI が再起動してしまった原因を探りやすくするために、以下のようなグラフも併せて用意している メモリ/CPU使用率 リクエスト数、等 3章: 可用性を維持/向上するために必要な取り組み 3章では、1章、2章でここまで揃えた情報を使い、実際にどのような取り組みを実施しているかについてご紹介しました。 可用性を維持するためには SLI を SLO 以上に維持できるように意識し、対応する これに限ります GROWI down が発生した場合は、すぐに様子を見る ex.) SLO を下回りそうな GROWI については早急に何らかの対策を講じる いずれ障害になるであろう事象に、前もって対応する ex.) ミドルウェアのストレージ容量減少、冗長性低下 これらの対応に必要な情報が、一目でわかるようなアラートを設定する アラート対応例 アラート対応の一例です。 アラート対応時に見ているもの 先述した Prometheus/Grafana をフル活用しています。 アラートも、運用上 SLO/SLI をすぐ意識できるように、以下のような思想で設定しています。 アラートも Prometheus 上のメトリックをベースに出している アラートには、当該の GROWI で SLO を達成するための許容される残りダウン時間も記載している 一目で GROWI がどういう状態なのかを把握できるようにするための情報を出している 可用性を向上するためには GROWI.cloud では、可用性を向上するために、以下の 2 点を実施しています。 可用性の値を逐次気にして、可用性を高められる箇所がないか探し、対処する 「Availability 向上施策」と銘打って、集中的に行った時期もあった 障害対応履歴を残す Availability 向上施策実例 プリエンプティブルノードを利用したプランでの冗長化を実施 ライト、バリュープラン リリース当初は、プリエンプティブルノードに 1 GROWI しか乗せていなかったが、99.4% の SLO を保てなかった 施策実施前 SLI: 99.00% 施策実施後 SLI: 99.93% 障害対応履歴を残す 障害発生時に、振り返れるように経緯を残すのが大切 ポストモーテム https://qiita.com/an_sony/items/0565ad980f9097c76d11 記録は重要! 対応者以外にも障害内容を伝えられる 週次でチーム全体に報告し、対策内容について検討、実施へ繋げている 有識者の知見を借りられる 今後、似たような障害が起きないように、正しい対策を打てる 対応後に、改めて監視で収集したデータと突き合せ、より効果的な対策へ 今後、似たような障害が起きた時に、自分が/他の人が参考にできる 自分が忘れないため まとめ 本発表のまとめです。 サービス運営の上で必要な監視項目とは 真に監視するべきものは何かを洗い出す SLO を設定する サービスの可用性を計測・可視化する手法 SLI を算出できるメトリックを整備する SLI を簡単に確認できるような環境を整備する ex.) ダッシュボードを整備する、アラート内容に記載する、等 可用性を維持/向上するために必要な取り組み 対応時は、1./2. で整備したものをフル活用 対応後は、対応内容を残してチームに共有 継続的に SLI を改善できるような動きへ 今後の GROWI.cloud 運用展望 GROWI 上で実際に利用されるリクエストのエラー率で SLO を決定できるようにしたい 現状は、ヘルスチェックエンドポイントへの監視成功/失敗で判定している より利用者目線に立った監視ができるようになる 著者プロフィール 今間 俊介 株式会社WESEEK / バックエンドエンジニア 2013年にWESEEKに入社。 node.js/Kubernetes を中心としたインフラ/アプリの設計・構築・運用に携わる。 GROWI.cloud の運用にリリース当初(2018~)より従事。 株式会社WESEEKについて 株式会社WESEEK は、システム開発のプロフェッショナル集団です。 【現在の主な事業】 通信大手企業の業務フロー自動化プロジェクト ソーシャルゲームの受託開発 自社発オープンソースプロダクト「 GROWI 」「 GROWI.cloud 」の開発 GROWI GROWI は、Markdown記法でページを記述できるオープンソースのWikiシステムです。 GROWI.cloud GROWI.cloud はOSSのGROWIを専門的知識がなくても簡単に運用・管理できる、法人・個人向けの商用サービスです。 大手SIer・ISPや中小企業、大学の研究室など様々な場所でご利用いただいております。 【主な特徴】 テキストも図表もどんどん書ける、強力な編集機能 チーム拡大に迅速に対応できる管理者向け機能を提供 充実した機能・サポートでエンタープライズにも対応 【導入事例記事】 インターネットマルチフィード株式会社様 [ https://growi.cloud/interviews/mfeed/?utm_source=connpass-top&utm_medium=web-site&utm_campaign=mf:embed:cite ] 株式会社HIKKY(VR法人HIKKY)様 [ https://growi.cloud/interviews/hikky:embed:cite ] WESEEK Tech Conference WESEEK Tech Conference は、株式会社WESEEKが主催するエンジニア向けの勉強会です。 WESEEKに所属するエンジニアが様々なテーマで発表を行う予定です。 次回は、1/27(木) 19:00~20:00に開催予定です。 GROWI と連携できる GROWI bot の開発を通じて学んだ「Slack社が却下するシステムの作り方」そして「Slack社が譲れない仕様」についてお話します。 現在、connpassやTECH PLAYで参加受付中です。皆様のご参加をお待ちしております! https://weseek.connpass.com/event/234368/ TECH PLAYはこちらから 一緒に働く仲間を募集しています 東京の高田馬場オフィス、大分にある別府サテライトオフィスにてエンジニアを募集しております。 中途採用だけではなく、インターンシップも積極的に受け入れています! 詳しい募集要項は、弊社HPの 採用ページ からご確認ください。
アバター
WESEEKオリジナル プランニングポーカー はじめまして!今回執筆するのは、戦略企画室インターンの塚田です。エンジニアの方々が書く記事とは一風変わった、新鮮なテイストの記事になるかと思いますが、気楽に楽しんで頂けると嬉しいです! さて、今回のテーマは「 プランニングポーカー 」です。 WESEEKでは、タスクの工数見積もりの際にプランニングポーカーを使用しています。具体的には”ポイント付け”といって、「あるストーリーにどのくらいの時間を要するのか」について、メンバーそれぞれがカードを使って同時に数字を示す作業を行っています。その値が皆で近くなるように話し合いをすることで、工数見積もりにおける意見のすれ違いを防いでいるのです。 今年度、WESEEKは社内グッズの一つとしてプランニングポーカーを作成したため、(他の社内グッズについては こちら )この記事では、WS流の工数見積もりやカードの制作秘話についてとりあげようと思います! WESEEKにとってのプランニングポーカー はじめに、プランニングポーカーのメリットや使用上心がけていることなどについて、代表の武井さんとエンジニアの皆さんにインタビューしてみました。 左から貝沢さん、武井さん、大谷津さん、田村さん ーそもそもWESEEKでは、なぜ工数見積もりにプランニングポーカーを使用しているんですか? 【武井】 WESEEK的には、エンジニアが個人で動く時に「次に自分が何をやらなきゃいけないのか」をわかっていて欲しくて、そのために有効な手段としてプランニングポーカーを続けてる。 【田村】 プランニングポーカー導入のきっかけになる出来事はあったんですか? 【武井】 かなり昔の話だけど、少人数で意思疎通が容易だった頃は、スクラムMTGすらやっていなかったんだよね。だけどある時、業務委託の人から「デイリーのMTGをやった方がいい」って指摘されて、導入したら結構よかったんだよ。 それをきっかけに「今は見知った仲で、自分たちのやり方で連携しているけど、他の人が入ってきたらできなくなる可能性もある。世の中で使われている手法も試してみなきゃだね」となった。それでプランニングポーカーも始めてみました。 ー試行錯誤の末に続いている物なんですね。では、そんなプランニングポーカーでポイント付けをする時に、皆さんが気をつけていることはありますか? 【武井】 個人的には、他の人が気づかないことを指摘するようにしている。それがプランニングポーカーをやってて楽しいところでもあるしね。「ここ見落としてるよ」ってあえて皆と違う数値を出して、「なるほど、確かに」って言わせたい。そういう発言をすることで、PJへの理解度を示したいんだけど、みんなはどうですか? 【貝沢】 僕はスコープを気にして突っ込む。というのも、メンバー同士で数値が大きく違う時は、よく聞いてみるとゴールの定義がぞれぞれで違ったりするんだよね。それって個々が悪いんじゃなくて、そもそも対象となっているストーリーのゴールが不明瞭なことが原因なので。これは気にするようにしている。 【田村】 気をつけていることではないけど、僕のPJは、要求されている仕様が複雑なことが多くて、いざポイント付けをすると「まだ不確定な要素があったな」と気づくことがある。この機会のおかげで、より仕様の漏れが潰されていくから良い試みだと思いますね。大谷津さんは? 【大谷津】 わからない部分をはっきりさせるように気をつけていますね。結構みんな暗黙の了解で喋っちゃうけど、いざ確認したらそうでなかったってことがあるので。 ーなるほど。実際に使用する中で、印象だった出来事はありますか? 【大谷津】 そもそも僕は、プランニングポーカーという文化を知らなくて、WESEEKに入社してから始めて触れたんですよ。その意味で入社当時は新鮮だったし、「こんなに開発者同士が近くで、一つのゴールに対して同じものを見るのは、非常に珍しい文化だな」と言う感想を抱きましたね。 ー そうなんですね。てっきり、IT企業だとどの会社でも取り入れているのかと思っていました。 【大谷津】 他社だと、機能やPJごとに分業になっていたりするので、人数が少なくて口頭でもすり合わせが済むこともあるんですよ。だから、こんな風にゲームみたいに、一つの場所に集まってワイワイやることはないかも。 【武井】 僕からは、”無限カード”について話したい。今回のオリジナルカードでは作らなかったけど、最初に買った既製品には”無限カード”と”?ポイントカード”があったんだよね。これらは「見積もりができない」意思表示のカードなんだけど、僕は逃げだと思う。 例えば、仕様が全然決まっていなくて「これ決められないでしょ、わかりません」って自信持って出すならいいけど、みんな考えてるのに、「自分にはスキルがないから見積もりできません」はなしだろう、って怒るようにしてる。 「最初?ポイントカードを出して、次にみんなが出しそうな数字を出していたら、周りが勝手に見積もりしてくれました」だと、君は本当に戦力になってるのかと思うから。そこをちゃんと考えさせるために、無限カードと?ポイントカードはなるべく使わないようにしようと指示している。 その分、皆がしっかりカードを出すことで、フラットかつフェアに意見を言える場という側面もあるよね。 【大谷津】 普段は上司に遠慮しちゃう人もいますしね。でもポイント付けでは「お前が言っていることはわからない」って普通に先輩に言える。メンバーが皆対等に、ストーリーを俯瞰できるいい機会だと思います。 ーそれでは最後に、企画室としてお聞きしたいのですが…。今回企画室がオリジナルのカードを作成しましたが、オンラインのカードよりも、手元にあった方がいいものですか? 【貝沢】 オンラインだとポーカー感はないよね、カードじゃないから。 【武井】 そうだね。あとは、対面だと、メンバーにカードを出してもらった時に、表情もセットで見れる。自信持って出しているのか、おずおずと周りを伺っているかわかるのが違いかな。 【大谷津】 開発メンバーの表情が分かるのは非常に良いですね。ただでさえ無機質になりやすいので。 ーなるほど。コロナ禍で難しいところもありますが、ぜひ対面で使用していただきたいですね。 エンジニアの方々に直接お話を聞いて、プランニングポーカーは、「個人の自立を促すと同時に、チームワークを円滑にする働きもある」と知りました。プランニングポーカーがどのように仕事に活きるのか、エンジニアでない私でもイメージが深まってよかったです! 制作秘話 次に、WESEEKオリジナルのプランニングポーカーについてお伝えします。プランニングポーカーをはじめ、社内グッズは企画室の主導で作成しましたが、コスモレナ社の皆さんにたくさんお力添えをいただきました。コスモレナ社さんは、「デザインを主軸に据えるクリエイティブカンパニー」で、普段の業務でもたくさんサポートして頂いています。 (コスモレナ社の HP はこちら) ということで商品開発の中心となった戦略企画室の太田さんと、コスモレナ社デザイナーの形部さん・押木さんの三人に、カードの制作背景についてお話を伺ってみました。 (左から)太田さん、押木さん、形部さん ーはじめに、プランニングポーカー作成に至った理由を教えていただけませんか? 【太田】 元々はエコバッグやパーカーみたいな、一般的なノベルティを候補に出してました。ただ会議の際に、エンジニアらしいグッズとして、プランニングポーカーが話題に上がって。会社のロゴを入れたら使用の度にアピールになるし、卒業したインターン生にプレゼントもできるよねということで決定しました。 ーそんな意図があったんですね!現物を見ましたが、シンプルかつWESEEKらしい素敵な商品でした。どのようにあのデザインに決定したんですか? 【押木】 当初はデザイン案を私たちと、エンジニアのかおりさんの三人で考えていたんですよね。彼女は元々デザインに興味があったみたいで、我々に組み込まれる形で参加していました。 かおりさんのデザインはハリネズミのイラスト風なやつで、形部さんは王道なもの。私はドット絵を使ってWSロゴを数字にしようと考えていました。結構いろんな方向性からのアプローチができて、面白かったです。 押木さん案 形部さん案 【太田】 企画室としては、数字の見やすさを重視して、形部さんの案を採用しようという結論になりました。押木さんの案もすごく面白いんですが、数字がパッと読み取りづらいなと感じましたね。デザイナーチーム的には、この結果で納得いきましたか? 【押木】 どちらの案もクオリティには自信があったので、あとはお好みで選んでいただければと思っていました。実用性かデザイン性を取るかで、前者を選んだのだなと。 【形部】 自分は顧客満足度を意識して作ったので、実用性が高いものになったのかな。どれだけの物が求められているかわからなかったし、奇をてらうことはせず使いやすいデザインに仕上げました。 ーその点で言うと、デザイナーはもっと凝ったグッズを作りたかったけど、企画室から「そんなに工数をかけないで欲しい」と言う要望があったという話を聞いたんですが… 【形部】 デザイナーからすると、「工数を抑えて簡単なもので良いよ」という注文は苦手なんですよね。「好きなだけこだわって最高のものを作ってくれ」と言われた方が嬉しいし、そっちで解釈しがち。 だけど企画室としては、グッズ開発にリソースを割きすぎて本業に支障をきたしたくないので、「最低限の工数で良いものを」という費用対効果を重視していたんだと思います。そこのズレですかね。 【太田】 そうですね。それに、企画室とコスモレナが初めて関わったのがこの企画だったので、今思うと連携不足だったなと。こんなに気軽にインタビューに呼べる相手だと思ってなかったし、当初の想像以上にデザイナーの皆さんが色々考えてくださって…。もちろんそのおかげで、デザイナーの方々にしか思いつかない案がたくさんできましたし、素敵なグッズができて有難かったです。 ーちなみに、今後WESEEKグッズを作るとしたら何か作りたいものはありますか? 【押木】 WESEEK全体ではなくGROWIのグッズを作ってみたいです。今あるのは木工ロゴとステッカーですよね。GROWIくんで何かできないかな。 【形部】 僕が個人的にやりたいのは、ハードウェアよりPCの中で完結するものかな。例えば、モーショングラフィックを使ったWESEEKロゴのオープニングムービーや、GROWIサービスの説明とか。 【太田】 企画室でYouTubeを再興させる施策を考えているので、YouTubeのOPとかあったらいいですね。 ーお話を聞いていて、WESEEKメンバーの太田さんはもちろん、デザイナーの皆さんが本当に熱心で有難いですし、何より一緒にお仕事ができて嬉しいなと感じました。プランニングポーカーは初めてのグッズ作成で苦労も多かったと思いますが、それを活かしてさらに素敵なグッズを社内・社外に提供していきたいです。 企画室でプランニングポーカーやってみた! さて、ここまでプランニングポーカーについて書いてきましたが、実際にやってみないとわからない!ということで、企画室内でもプランニングポーカーによる工数見積もりを体験してみることにしました。 今回のお題は 「企画室が計画しているInstagramの投稿」 について。 Twitterでの1投稿を「2」とした時に、Instagramの1投稿はどれぐらいの数字になるだろうか?とカードを出し合いました。 1回目の結果は 5・5・13 ! 何と初回から大きな差がついてしまったのです。 3人から出されたカードは…? 13を出したインターン生の安川さんは、Instagramの企画案をまさに練っている最中。 話を聞いてみると、自分がこの施策を進めていくことや、やってみたことがない事への不安を含めての「13」という結果だとわかりました。 不安は一旦置いておいて、純粋に投稿に対する時間・コストとして出し合おう!と2回目を行ったところ、 5・5・8 に。 お互いにこの数字を出した理由を話あうと、例え同じ「5」でも考えている事は違うんだなーという発見がありました。みんなで納得した上で、5と8という隣り合う数値に収束したので、今回は「8ポイント」の採用です! 1回目に13が出た時にはびっくりだったのですが、その分不安を感じていることもわかりましたし、お互いに心配していることを話し合うよい機会になったと思います。 思ったよりも簡単に、しかもお互いの考えを共有できたので、今後は企画室でもプランニングポーカーが活躍する時も増えてくるかも? 非エンジニアの私が感じたこと 私は、今回の記事を書くまでプランニングポーカーを社内グッズという視点でしか捉えていませんでした。でも「どのような目的で使われているのか」「会社にどんなメリットを与えるのか」という、日頃から取り組んでいるエンジニアの皆さんの姿勢を知って、仕事上の重要な道具という認識に変わりました。 製作秘話も、今グッズを作っている立場としてとても参考になりました。ただ作るのではなくて、細部までこだわることで皆の心に残って欲しいと思います。 インタビューにご協力いただいた皆さん、ありがとうございました! おまけ 最後に、私が携わっている商品についても紹介させてください! プランニングポーカーはあくまで社内向けのグッズですが、WESEEKでは社外にも商品を販売しています。今販売しているのは、パーカーとマグカップ。どちらもエンジニアの方にしかわからないこだわりが詰まった、素敵なデザインです。 こちら の記事にて詳しい商品紹介をしていますので、ぜひご覧くださいね。 そして、興味のある方は こちら の販売サイトからご購入お願いします! 積読パーカー Mom's Spaghetti Code Mug
アバター
collletのログイン後のホーム画面 この投稿は、弊社が提供する WESEEK TECH通信 の一環です。 WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。 はじめに こんにちは。システムエンジニアの佐藤です。 この記事では WESEEK社内通過であるWESEEK DOLLER(WSD)の流通が開始して約1年経ち、その中で発生したHyplerledgerFabricにおける証明書の有効期限切れトラブルと振り返って分かったことを物語風に紹介します。 WESEEK DOLLER(WSD)とはWESEEK社内で流通する貨幣のことです。詳細についてはWESEEK Blogの WESEEK DOLLOR や 社内通過WSD、リスタート をご覧ください。 WESEEK DOLLOR 社内通貨WSD、リスタート! WSDはブロックチェーンフレームワークHyperledgerFabricを使っています。HyperledgerFabricについての概要やアプリケーション開発方法については、Qiitaの記事をご覧ください。 Hyperledger fabric で始めるブロックチェーンアプリケーション (1/3) Hyperledger fabric で始めるブロックチェーンアプリケーション (2/3) Hyperledger fabric で始めるブロックチェーンアプリケーション (3/3) HLFコンポーネント証明書の有効期限切れ HyperledgerFabric(HLF)におけるデータや通信の信頼には証明書が用いられます。 証明書には当然有効期限がありますので、この有効期限が切れた場合にはシステムに影響が出ます。 ある日、それは起こった ある日のこと、 「WSD を流通するアプリケーション(colllet)にログインすると、ずっとローディングアイコンが表示されたままになってしまいました。。。 」 という連絡を受けました。 collletではログインすると所持するWSDの金額と、その他の操作画面へのリンクが表示されます。(次の画像参照) collletのログイン後のホーム画面 この画面の中にある"16"は、自身が所持するWSDの金額です。 自身が所持するWSDの金額はHyperledgerFabricにトランザクションを送った結果が表示されます。 colllet frontがHyperledgerFabricにトランザクションを送る通信経路は次のようになっています。 # colllet front # ↑ # |(REST APIアクセス) # ↓ # colllet back # ↑ # |(HLFトランザクション) # ↓ # HyperledgerFabric(Orderer) # ↑ # |(HLFコンポーネント間通信) # ↓ # HyperledgerFabric(Peer) # ↑ # ↓ # Chaincode(WSD) ログを確認すると、frontからbackへのアクセスが行われた後にHLFトランザクションが応答しておらず、また client identity expired の文字がありました。 どうやら証明書の有効期限が切れてしまったようです。 [comm.grpc.server] 1 -> INFO 1922 streaming call completed grpc.service=orderer.AtomicBroadcast grpc.method=Deliver grpc.peer_address=***:33312 grpc.code=OK grpc.call_duration=928.91µs [common.deliver] deliverBlocks -> WARN 1923 [channel: ***] Client authorization revoked for deliver request from ***:33330: client identity expired 74h41m17.76951635s before HLFシステムで用いる証明書は複数あります。 ユーザー証明書 HLF CA証明書 HLF Orderer証明書 HLF Peer証明書 各証明書を確認すると、HLF CA証明書、HLF Orderer証明書、HLF Peer証明書が切れていることが分かりました。 (WSDシステムを導入する時点で発行し、その際に有効期限が1年であったために同時期に切れましたが当然発行した時期と有効期限によっては状況が変わると思います) HLFコンポーネントの証明書が切れるとトランザクション処理ができなくなります。 その結果、collletにログインしてもその後の画面が表示できなくなっていたようでした。 (このような場合、画面上でエラーを表示するべきですが作りこみの甘さは社内利用限定ということで温かい目で見てください ) お時間いただきますと回答 とりあえず証明書の更新が必要であることは分かりました。WSDスタート時からいずれこの日が来るとは分かってましたがそこはご愛敬です。 証明書を再発行するにしても検証する時間も欲しかったので利用者にはしばらく待ってもらい週末に対応することにしました。 HLFコンポーネント証明書の再発行 HLFでは証明書を発行する方法として、enrollとreenrollがあります。 enrollは証明書の発行であり、reenrollは証明書の再発行です。 fabric-ca-client というツールを使うとこれらの処理ができますが、システム構築時に証明書切れが起こった場合を想定してreenrollを実行したことがあり、今回もそれで解消できるだろうと思ってましたが証明書の有効期限が切れたときはreenroll出来ないことが分かりました。 よく考えるとそれもそうかと思いましたがenrollするときにパスワードを指定することから、このパスワードが残っていれば再発行できるものだと勘違いをしていました。 さて、reenroll出来ないとなるとenrollするしかありません。 ここで、HLFでは同一IDにおける証明書の発行回数が制限されています。 fabric-ca-client identity list で表示される"Max Enrollments"がこれに当たります。 $ fabric-ca-client identity list Name: admin, Type: client, Affiliation: , Max Enrollments: -1, Attributes: [{Name:hf.IntermediateCA Value:1 ECert:false} {Name:hf.GenCRL Value:1 ECert:false} {Name:hf.Registrar.Attributes Value:* ECert:false} {Name:hf.AffiliationMgr Value:1 ECert:false} {Name:hf.Registrar.Roles Value:* ECert:false} {Name:hf.Registrar.DelegateRoles Value:* ECert:false} {Name:hf.Revoker Value:1 ECert:false}] HLFコンポーネントの証明書を発行する際は fabric-ca-client register コマンドを使いました。 この際に"Max Enrollments"は指定していなかったのでCAに設定された最大発行回数がdefault値になり、幸いにも"-1"(無限)でした。 ひとまずenrollすれば証明書の発行ができます。 後はひたすら証明書を発行してシステムで利用する証明書と置き換えれば作業は完了です。時間はかかりますが単調な作業ですね。 振り返って分かったこと 地味な証明書の発行と置き換えを繰り返して無事にcollletが復旧してトランザクション処理を行えるようになりました。 振り返って分かったことは次のとおりです。 HLFコンポーネントの証明書有効期限が切れると全ユーザーがトランザクション処理ができなくなる 証明書の有効期限が切れるとreenroll出来なくなる 証明書の発行回数期限は"Max Enrollments"により発行時に設定される fabric-ca-client コマンドでregister時に最大発行回数が指定できる fabric-ca-client コマンドで最大発行回数を指定せずにregisterした場合はCAの最大発行回数がdefault値になる ユーザー証明書の有効期限切れ HLF コンポーネントの他にユーザーにも証明書が発行されます。 再び、それは起こった ある日が起こってからそう間もない日、再びそれは起こりました。 「あるユーザーで colllet にログイン出来なくなりました。他のユーザーではログインできるのですが何か分かりますか・・・? 」 という連絡を受けました。 そうです、ユーザー証明書の有効期限が切れてしまったようです。 ユーザー証明書が切れた場合もHLFにおいてトランザクション処理ができなくなります。但し、HLF コンポーネントの証明書が切れた場合と異なり、期限が切れたユーザーだけがトランザクション処理ができなくなります。 有効期限はユーザーが登録された日から1年間だったため、ユーザーによってはログインできる・できないが違いました。 この時は先日のHLFコンポーネントの証明書有効期限が切れたときに期限一覧を見て気が付いていましたが、 fabric-ca-client コマンドを非エンジニアに実行してもらって再発行するのは現実的ではないのでアプリケーション側で機能実装する必要があると考えていました。 ユーザー証明書の再発行 HyperledgerFabricにおいて、 hf.Type という証明書の拡張属性を使うことで分類することは可能ですが、基本的にユーザー証明書もHLFコンポーネントの証明書も同じです。 そのため、ユーザー証明書の発行・再発行もHLFコンポーネントの証明書発行とやり方は同じです。 しかし、発行した方法の違いで"Max Enrollments"が違ったのです。。。 collletではアプリケーションでユーザー登録ができます。 colllet backはNode.jsで実装されており、 Hyperledger Fabric SDK for Node.js を使っています。 ここで、registerをする際の証明書の発行回数期限がデフォルトでは1だったのです。(該当するコードは次のリンク先を参照) https://github.com/hyperledger/fabric-sdk-node/blob/82199a83a2d4ffd026fbcbdb33effaaa88d97950/fabric-ca-client/lib/FabricCAServices.js#L133 つまり証明書の有効期限が切れた場合はreenroll出来ず、更にはenrollは既に行われているので再びenrollすることもできません。 ではどうすればよいのかを調べていたところ、この"Max Enrollments"は fabric-ca-client identity modify を使って変更出来ることが分かりました。 そこで、以下の対応をすることにしました。 既存ユーザーの"Max Enrollments"を"-1"へ変更する アプリケーション側でユーザー登録時に"Max Enrollments"を"-1"にする ユーザーログイン時に証明書の有効期限に応じて、証明書を再度発行(enrollでありreenrollではない)する 振り返って分かったこと HLFユーザーの証明書有効期限が切れると該当ユーザーはトランザクション処理ができなくなる Hyperledger Fabric SDK for Node.jsのFabricCAServices#registerはdefaultで"Max Enrollments"が"1"になる fabric-ca-client identity modify により"Max Enrollments"は変更できる $ fabric-ca-client identity modify --maxenrollments -1 [USER] さいごに HyperledgerFabricにおいて証明書の有効期限が切れたときの影響と対応方法について紹介しました。 また機会がありましたらブロックチェーンのデータ構造などを紹介しようと思います。
アバター
この記事は、2021/11/25 に行った WESEEK Tech Conference #15 の内容をまとめたものです。 はじめに こういった悩みや疑問をお持ちの方はいませんか? サブスクリプション型課金のサービスを考えているが決済をどうすればいいのか分からない クレジットカードや銀行などが絡んでくると、決済の管理が大変 そもそも定期支払ってどうやって実現するの? そんな悩みを解消してくれる Stripe というサービスを、 WESEEKがサービスを提供する GROWI.cloud を実例としてご紹介します。 「Stripe」とは Stripe は、米 Stripe 社が提供するオンライン決済サービスです。 ライブラリ・API が用意されており、サブスクリプション型の決済を行うプログラムを簡単にサービスに組み込むことができます。 始めるのがとても簡単 メールアドレスさえあれば、誰でも簡単かつ即時にテストアカウントを発行が可能で、思い立ったと同時にすぐに検証を開始できます。 また、本番環境利用の申請についても必須項目を埋めて送信するだけで完了するので、最短1日で利用開始できます。 Stripeのデータの種類 主要なデータの種類 Stripe でサブスクリプション形式の課金を組むために、知っておく必要がある5つのデータの種類についてご紹介します。 顧客 (Customer) ・・・ 一顧客を表すデータ 商品 (Product) ・・・ 提供する商品を表すデータ 料金 (Plan) ・・・商品の料金 ¥〇 / 月 等のデータ インボイス(Invoice) ・・・(料金 × 顧客) 請求情報を表す サブスクリプション(Subscription) ・・・定期支払を表す 上記の 5 種のデータの組み合わせによって、最終的にサブスクリプションが構成されます。 サブスクリプション(Subscription)の仕様 これから、Stripe における Subscription のデータの中身や挙動について深掘りしていきます。 サブスクリプションというデータの中身・挙動 顧客の情報を持つ 期間と期間の区切り、料金プランを持つ 期間の区切りごとに支払い情報を作成する つまり、インボイスを定期的に自動生成して定期支払を実現しているのが「 サブスクリプション 」なのです。 また、課金が発生しない (0円課金) の料金プランをサブスクリプションに設定する場合は、顧客情報に支払い方法を登録する必要がありません。 開発者向け情報 もちろんですが、テスト版アカウントでは引き落としなどは発生しません。 テスト版アカウントでは、Stripe が用意するダミーのカードを使用できます。 また、支払いに成功する場合のみを検証していては、実際に運用することはできませんよね? そういった支払いに失敗した場合を確認するためのダミーカードもあります。 また、その種類も豊富で、失敗してしまう理由が異なるダミーカードが様々用意されています。 ここでStripeの仕様について疑問 Q: 期間の途中で顧客がプランを変更したらどうなるでしょうか? A: Stripe が日割り計算を行って次回請求情報を自動更新してくれます よって、サービス提供者が課金周りをシビアに気にする必要がありません Q: Stripeの月額はいくら? A: 月額の利用料はナシだが、決済成立ごとに 3.6% の手数料がかかる 規模の小さなスタートアップのサービスほど導入しやすい SDK&ドキュメント Stripe では、様々な種類の開発者ツールが提供されています CLI Node.js ライブラリ VisualStudio Code の拡張機能 REST API ドキュメントも充実 API ドキュメントをはじめ、仕様に関するドキュメントが充実しています。 日本語版も現在整備されつつありますが、基本は英語版を翻訳したものであり翻訳が未対応の箇所や言語による若干の表現の差もあります。 安心のサポート 前述で不安を覚える方もいらっしゃると思いますが、ご安心ください。 開発者向けのサポートも手厚く、ドキュメントを読んで不明な点があれば、気軽にサポートへ質問できます。 サポートの体制 日本語でのサポートはメールでの対応となり、翌営業日に回答していただけます。 また急いで確認したい場合は、英語であればチャットでのサポート対応を受けていただけるため、その場で会話しながら不明点の解消を進められます。 GROWI.cloudでの導入事例 GROWI.cloud では、Stripe が提供している Node.js のライブラリ、Stripe React Element というコンポーネントライブラリを使用しています。 商品とプランの管理については、プランごとの料金設定をマイグレーション用ファイルにまとめて一元管理しています。 商品の追加や価格の変更などが発生した際には、該当ファイルを更新しマイグレーションを実行しています。 GROWI.cloud では顧客、サブスクリプション、インボイスをどう扱っているのか? 顧客の作成からプランのご契約までの流れをご紹介します。 アカウント登録時に Stripe 上の顧客情報を作成 const attachStripeCustomer = async(customer, option) => { const stripeCustomer = await stripe.customers.create({ email: customer.email, description: `Customer ID: ${customer.id}`, }); await customer.update({ stripeCustomerId: stripeCustomer.id }, option); }; 同時に課金額0円の料金プランでサブスクリプションを作成 const attachStripeSubscription = async(customer, plan, options) => { return stripe.subscriptions.create({ customer: customer.id, items: [{ plan: plan.id }], expand: ['latest_invoice.payment_intent'], ...options, }); }; 登録完了後、ユーザーの任意のタイミングでプラン変更 const changeSubscription = async(subscriptionId, subscriptionItems, options) => { await stripe.subscriptions.update(subscriptionId, { items: subscriptionItems, expand: ['latest_invoice.payment_intent'], ...options, }); }; 支払い情報が未登録の場合は、登録完了後にプラン変更が可能 Stripe React Element を利用しています Stripeの便利機能PickUp ダッシュボード Stripe が自動集計してくれる様々なデータを閲覧可能 MRR (月間標準収益) ダッシュボードから確認可能な指標の一つで、サブスクリプションが継続している契約全体の月当たりの収益見込みを表します。 サブスクリプションサービスを提供する場合、継続が見込める月間の収益は売り上げと直結するので、サービス戦略を考える指標としても非常に重要な指標です。 クーポン 特定の支払いに対して割引を行う「クーポン」を発行することができます。 割引率 は MRR に影響しません。 (正規の金額通り MRR に反映される) 例)新規に契約される 600 円/月 プランに 50% 割引クーポンを適用した場合 → 顧客の支払金額は 300 円/月 → MRR の変動は +600 円 クーポンは割引される支払いが限定されるため「MRR」つまり「月間標準収益」という扱いの上では、標準の値 (クーポンによる割引を適用しない値) が反映される仕様だと考えるのが妥当です。 トライアル 期限を設けて、期限まで割引した額で請求を行うトライアル提供を行うことができます。 トライアル中のサブスクリプション は MRR に影響しません。 例)600円/月プランを50%割引でトライアル開始した場合 → 顧客の支払金額は 300 円/月 → MRR の変動は ±0 円 トライアルは、収益の見込みがまだ立っていない状態で、名の表す通りトライアル中という状況を作り出すものです。 また、トライアルを開始させるためには、顧客がキャンセルするための様々な条件や同意を明示的に提示する必要があります。 これは、トライアル中の顧客は正式な顧客となる前の段階で、収益が約束された本契約ではないと扱うためそうなっていると考えられます。 よって、トライアル中の状態ではまだ「(月間)標準収益」として扱うことができないため MRR に反映されない仕様だと考えるのが妥当です。 失敗談と対策 初月無料キャンペーンにおける失敗 [問題その1]突貫実装による考慮不足で、支払い失敗が連発してしまう 経緯 他サービスで当たり前となっている「初回〇〇無料!」という販促施策について社内から強い要望があり、優先して実装することになりました そこで我々は Stripe のドキュメントを調査し、クーポンという機能があることを知ります また、調査の結果以下の仕様が分かりました 「〇〇%オフ」のクーポンを作成することができる クーポンを適用することで、1回の支払いにおいて割引を適用できる 100%オフクーポンを使うことで、支払い方法の登録なしのまま有料プランを設定できる 実装も完了し意気揚々と初月無料キャンペーン開始のプレスリリースを打ちました。 キャンペーンの効果もあり、徐々にユーザーが増えてきた・・・が、そこで事件が起こります。 !問題発生! 1か月が経過した初月無料キャンペーン適用ユーザーが現れ始めたころ、その支払いが (ほぼ) 全て失敗するという現象が発生してしまいました。 原因 支払い情報の登録を行わず、100%オフクーポンを使う仕様としたことで、本来であれば有料であるプランを次回の支払いを考慮せずに契約させてしまったこと。 → どういうことか? 初めて請求を行う「初月無料キャンペーン」開始の際は支払い方法が登録されていなくとも問題は発生しなかった 2回目の請求を行った際に、有償のプランを契約した状態のまま支払い方法が登録されていないため失敗していた という、単純な仕様の考慮漏れに気づくことができなかったことが今回の敗因でした。 [問題その2]MRRの数値に悪影響が出てしまった 問題点 MRR の数値を、施策の方針を検討する際の材料として利用していたが、初月無料プラン提供開始後から、実際に課金される値と 「クーポンは MRR に影響しない」という値と値と関係性を正しく理解していなかった。 MRR には 割引を適用しない 金額が加算される つまり、無料期間だけお試しで利用してみるだけの顧客が利用したプランの金額が有効なものとして扱われるため、 本来の課金額が高いプランを利用したユーザーが居れば居るほどプランで MRR が乱高下した。 対策編 トライアルという機能があり、そちらが適切だと気づく ドキュメントをさらに読み込み、トライアルという機能に出会います。 トライアルを利用するよう再度仕様を練り直す事にしました。 結果的に、クーポンを利用した「初月無料キャンペーン」から、「1か月無料トライアル」に乗り換えることになります。 実際のコード変更 元々、 Stripe がトライアルという機能で提供していたものに似たようなものを「クーポン」として提供しようとしていたためか、内部の実装は大きく変わらず実現できました。 await stripe.subscriptions.update(subscriptionId, { items: subscriptionItems, coupon: “COUPON_CODE_XXX” }); ↓↓ await stripe.subscriptions.update(subscriptionId, { items: subscriptionItems, trial_end: addMonths(new Date(), 1) }); 結果 トライアルにすることで、サブスクリプションそのものが MRR に影響しなくなった! 長い間懸念していた MRR の乱高下もかなり落ち着きました。 まとめ Stripe は、現在勢いのあるオンライン決済サービス 導入が簡単 素早く柔軟に変更可能 正しく理解して使えばとても便利 ぜひ利用を検討してみてはいかがでしょうか 著者プロフィール 伊勢 達也 株式会社WESEEK / システムエンジニア 2013年8月から2015年3月まで WESEEK に在籍 2017年7月に WESEEK 初のフルリモートメンバーとして WESEEK に再入社 2018年5月に別府サテライトオフィスを開設 別府サテライトオフィスのメンバー5名を率いて GROWI / GROWI.cloud を開発・運用中 株式会社WESEEKについて 株式会社WESEEK は、システム開発のプロフェッショナル集団です。 【現在の主な事業】 通信大手企業の業務フロー自動化プロジェクト ソーシャルゲームの受託開発 自社発オープンソースプロダクト「 GROWI 」「 GROWI.cloud 」の開発 GROWI GROWI は、Markdown記法でページを記述できるオープンソースのWikiシステムです。 GROWI.cloud GROWI.cloud はOSSのGROWIを専門的知識がなくても簡単に運用・管理できる、法人・個人向けの商用サービスです。 大手SIer・ISPや中小企業、大学の研究室など様々な場所でご利用いただいております。 【主な特徴】 テキストも図表もどんどん書ける、強力な編集機能 チーム拡大に迅速に対応できる管理者向け機能を提供 充実した機能・サポートでエンタープライズにも対応 【導入事例記事】 インターネットマルチフィード株式会社様 [ https://growi.cloud/interviews/mfeed/?utm_source=connpass-top&utm_medium=web-site&utm_campaign=mf:embed:cite ] 株式会社HIKKY(VR法人HIKKY)様 [ https://growi.cloud/interviews/hikky:embed:cite ] WESEEK Tech Conference WESEEK Tech Conference は、株式会社WESEEKが主催するエンジニア向けの勉強会です。 月に2回ほど、WESEEKに所属するエンジニアが様々なテーマで発表を行う予定です。 次回は「激白!GROWI.cloudの可用性向上の取り組み」 12/23(木) 19:00~20:00に開催予定です。 顧客に何らかのサービスを提供する上で、日常的なサービス可用性の維持・向上に関する業務は必須となります。 本発表では、可用性維持・向上に必要な項目を挙げつつ、弊社が実際に提供しているサービスである GROWI.cloud での実施例をご紹介いたします。 どのような形で SLO の策定に至ったのか どのような構成で可用性を数値化しているのか どのような業務を実施して、可用性の維持・向上を達成しているか 現在、connpassやTECH PLAYで参加受付中です。皆様のご参加をお待ちしております! https://weseek.connpass.com/event/231659/ TECH PLAYはこちらから 一緒に働く仲間を募集しています 東京の高田馬場オフィス、大分にある別府サテライトオフィスにてエンジニアを募集しております。 中途採用だけではなく、インターンシップも積極的に受け入れています! 詳しい募集要項は、弊社HPの 採用ページ からご確認ください。
アバター
この投稿は、弊社が提供するWESEEK TECH通信の一環です。 WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。 はじめに こんにちは、システムエンジニアのかおりです。この記事では、TypeScriptで用いられる Enum型 とUnion型の基本的な使い方から、Enumを避けるべきと言われている理由、Union型を用いてEnumのように書く方法などを説明しています。 Enum (列挙型) とは? そもそも Enum とは何か、なんのために使うものなのでしょうか。 ここで説明している Enum とは総称であり、列挙型とも呼ばれます。以下の例は TypeScript のコードですが、他の言語でも存在する概念・実装であり、複数の定数を一つにまとめて定義したり管理したりすることができます。 enum SIZE { Small, Medium, Large, } const newSize = SIZE.Small; console.log(newSize === SIZE.Small); // true console.log(newSize === SIZE.Large); // false Enum (列挙型) を使うメリット 上記の例、単純に文字列として "small" などを変数で定義して比較する方法と比べてどんなメリットがあるのでしょうか。 最も大きな恩恵は、スペルミスや大文字小文字などの人的エラーを防ぐことができる点です。 例えば Visual Studio Code などの TypeScript の補完が可能なエディタ上で SIZE. と入力すると、 enum SIZE で定義されているキーがサジェストされます。 また、Enumは型としても利用できるので、より堅牢なシステムの構築に役立ちます。 JavaScript、TypeScript での enum JavaScript には enum は存在しません。そのため、enum でやりたいことを実現するには const が代用となるでしょう。 const SIZE = { Small: 'small', Medium: 'medium', Large: 'large', }; console.log(SIZE.Small); // small 一方 TypeScript には enum の機能があります。しかし、以下のように少々癖があります。 // 何も指定しない場合、0からの番号が割り振られていきます。 enum SIZE { Small, // 0 Medium, // 1 Large, // 2 } console.log(SIZE.Large); // 2 console.log(SIZE[1]); // Medium 少し冗長ですが、各定数に任意の文字列や任意の数字を割り当てることもできます。 enum SIZE { Small = "small", Medium = "medium", Large = "large", } console.log(SIZE.Large); // large // 一番最初の定数(ここではSmall)に5を指定した場合、定数は5からの数字が割り振られます。 enum SIZE { Small = 5, // 5 Medium, // 6 Large, // 7 } console.log(SIZE.Large); // 7 定数の途中で数字を指定した場合、最初の定数は0から始まり、途中から指定された文字から数字が割り振られます。 enum SIZE { Small, // 0 Medium = 4, // 4 Large, // 5 } console.log(SIZE.Medium) // 4 Enum 型 と Union型の比較について これまで述べたメリットを踏まえた上で、実は界隈では TypeScript の enum については利用の反対派(非推奨派?)が存在します。 このenum型と、非推奨派が推奨するunion型についてお話ししたいと思います。 Enum を使用する際の注意点 1. 意図しない値にアクセスできてしまう 上記の説明で、enumに数値が割り当てられている場合、その数値から値を取得することができると説明しました。これを利用して、割り当てられていない数値からの値の取得が可能となり、意図せずundefinedが返ってきてしまいます。 enum SIZE { Small, Medium, Large, } console.log(SIZE[1]); // Medium // no error!! console.log(SIZE[5]); // undefined 数値のenumにはあらゆるnumberを割り当てることができるので、型安全性に欠けてしまいます。 2. Tree-shaking が作動せず、呼び出されない不要なコードがコンパイルされてしまう TypeScriptで定義されたenumは、Javascriptにトランスパイルされる際に以下のような即時実行関数として変換されます。即時実行関数とは、「定義されるとすぐに実行される」JavaScriptの関数のことです。 var SIZE; (function (SIZE) { SIZE[SIZE["Small"] = 0] = "Small"; SIZE[SIZE["Medium"] = 4] = "Medium"; SIZE[SIZE["Large"] = 5] = "Large"; })(SIZE || (SIZE = {})); // 文字列を割り当てた場合 var SIZE; (function (SIZE) { SIZE2["Small"] = "small"; SIZE2["Medium"] = "medium"; SIZE2["Large"] = "large"; })(SIZE || (SIZE = {})); このように即時実行関数としてトランスパイルされると、Tree-shakingが上手く働かなくなってしまうという問題点があります。 Tree-shakingとは....?Tree-shaking とは、webpackなどのモジュールハンドラがJavascriptファイルを一つにまとめる際に、実行されない余分なコードを削除するといった処理のことです。 木を振ることで、不要なものを落としてボリュームを小さくするイメージだと個人的に解釈しました。 以下のサイトで Tree shakingについて説明されているので、よければご覧ください。 MDN: Web Docs: Tree shaking Webpack rule that may reduce your CSS file size dramatically 3. isolatedModulesがtrueの場合、const enum使用時にコンパイルエラーになることがある --isolatedModulesオプションを有効にする場合、 Transpile each file as a separate module (similar to 'ts.transpileModule'). このオプションをtrueにした場合、const enumはエラーとなりコンパイルできない。 詳細は以下の記事をご覧ください。https://www.kabuku.co.jp/developers/good-bye-typescript-enum 以上の理由から、enumの利用は避けるべきであると言われています。 では、enumのような機能を使いたい場合はどうすれば良いのでしょうか? それは、Union型を使うことで実現できます。 Union型について Union型は、以下のように2つ以上の型をパイプ記号|で繋げて書くことで、複数の型を指定できます。 const middleName: string | null Enumの代わりに、Union型を用いる Enumを使わずとも、Enumのように定義するにはどうすればいいのでしょうか?const assertion(as const), keyof, typeof を使ってenumのような定義をすることができます。 // const assertionを使って、リテラル型として扱う const SizeType = { SMALL: 'small', MEDIUM: 'medium', LARGE: 'large', } as const; // type SizeType = 'small' | 'medium' | 'large' type SizeType = typeof SizeType[keyof typeof SizeType]; 細かく分解すると、以下のようになります。 // type SizeType = { // readonly SMALL: "small"; // readonly MEDIUM: "medium"; // readonly LARGE: "large"; // } type SizeType = typeof Size; // type SizeKey = "SMALL" | "MEDIUM" | "LARGE" type SizeKey = keyof SizeType; // type Size = 'small' | 'medium' | 'large' type Size = SizeType[SizeKey]; const assertion、リテラル型, keyof, typeof に関しては、以下の記事でわかりやすく説明されているので、よければご確認ください。 constアサーション「as const」 (const assertion) リテラル型 typeof, keyof, inの動作確認 unionのリストを手に入れたい場合 javaScriptが提供する Object.values関数を使用して、取得することができます。 // const AllSizeType = Object.values(SizeType); const AllSizeType = Object.values(SizeType); MDN Web Docs: Object.values 完成系テンプレート WESEEK では以下をテンプレートとして使用しています。const assertion に値を追加するだけで union と全値のリストを export できるので便利です。 const SizeType = { SMALL: 'small', MEDIUM: 'medium', LARGE: 'large', } as const; // type SizeType = "small" | "medium" | "large" export type SizeType = typeof SizeType[keyof typeof SizeType]; // 全てのtypeを配列として取得 // const AllSizeType: ("small" | "medium" | "large")[] export const AllSizeType = Object.values(SizeType); まとめ 総称としての Enum (列挙型)について 複数の定数をまとめて定義したり管理するのに便利 いろんな言語で利用できるが JavaScript にはない Enum は型としても使えるので、バグを防ぐことに役立つ TypeScript の enum vs union いろんな記述方法がある enum は、記述は簡単 enum の利用には複数の落とし穴があるので、union型を用いることが一部では推奨されている こだわりがなければ、TypeScript ではenum型ではなくunion型を用いることをお奨めします。 参考にさせていただいたサイト MDN Web Docs: Tree shaking Enum の代わりに使う union 型
アバター
この記事は、2021/10/28 に行った WESEEK Tech Conference #14 の内容をまとめたものです。 第14回目のテーマは「あなたもできる!GASで勤怠入力Slack App構築」 実際に WESEEK で使っている勤怠入力システム「tickrec」の構成を元に、Slackで勤怠入力ができるアプリケーションを1から構築するための方法をレクチャーします。 構成要素の解説はもちろん、実際の画面も交えながら構築する手順を解説します。 これを見た暁には、あなたも勤怠入力 Slack App が構築できるようになっている!!はず。 勤怠システムよもやま話 皆さんの会社では、どんな勤怠システム使っていますか? Web システムや専用アプリ、はたまた紙の管理簿やタイムレコーダーなど使っている方もいると思います。 WESEEK ではあまり大規模なシステムは導入していません。Google Spreadsheet を使って、シンプルに全社員の勤怠を1シートで記録しています。 しかし、 Spreadsheet には Excel 等と同様に Pivot table をはじめとした豊富な集計機能が搭載されているため、管理業務に必要な月の合計稼働時間や平均勤務時間もできます。 勤怠管理の課題 どんなシステムを導入していたとしても、勤怠管理における共通の課題というのがつきまとうのではないでしょうか? それは 入力が面倒 という点です。 Spreadsheet だろうが Web システムだろうが、同じ課題があります。 正直なところ、従業員からすれば、勤怠入力するためだけにツールを開くことさえ面倒です。 その結果、当日中の入力忘れが頻発したり、場合によっては月が変わっても前月分の入力抜けがあったりして、月締め時期にバックヤードチームが個別にリマインドするなどの余計な業務が発生したりします。 それが常態化すると、月末にまとめて適当に入力する行為が横行するなど、「果たして正しく勤務時刻が記録されているのか」、その精度に疑問が残ります 勤怠入力の改善方法 しかし、この課題に対する改善方針はシンプルです。とにかく入力までのstep数を減らすこと に尽きます。 既存の勤怠システムでも、この点を意識したソリューションが見受けられます。 例えば、アプリや Web システムの場合、勤怠入力システムを開いてワンステップで打刻可能な UI にすることで改善されています。 更に、ハードウェアを駆使して改善をねらったソリューションもあります。 例えば IC カードで打刻できるシステムです。 近年、セキュリティ対策の一環として、IC カードをかざすことで執務室を解錠するシステムを採用する会社も多いと思います。 そのようなシステムを導入済みの場合、勤怠用 IC カードリーダを入り口近くに配置することで、シームレスに出勤即打刻が実現できます。 また解錠システムと勤怠システムを連携できるソリューションも存在します。 更に踏み込んだソリューションとして、ビーコンで打刻できるシステムもあります。 これは Bluetooth 等の無線システムを活用しており、各社員のスマホが勤怠端末圏内に入るだけで自動打刻できるというものです。 ここまで来ると、出社するだけでゼロステップで打刻が可能になります。 WESEEK 社内の勤怠入力改善ソリューション と、実に夢の広がる勤怠入力ソリューションの例を紹介してきましたが、これらのシステムの導入と維持にはそれなりのコストがかかります。 WESEEK としては、もう少しコスパの良い手段を導入することにしました。 Tickrec を自作 Tickrec と名付けたツールを作成し、導入しました。 Slack App になっており、 Slack 上で下記のような挙動が実現できています。 毎朝、社内 Slack に DM で勤怠ボタンが飛んでくる ボタンをクリックすると、勤怠入力完了 Google Spreadsheetに入力される 導入のメリット Tickrec の導入により、社内 Slack からワンクリックで勤怠入力ができるようになりました。 WESEEK ではコミュニケーションツールとして Slack を導入しているため、従業員全員が Slack を常時起動させています。 そのため、打刻専用のアプリなどをいちいち開く手間が省けるのです。 また、 Tickrec の Slack App 部分は、ただのヒューマンインタフェースに徹しており、勤怠の記録そのものは従来通り Spreadsheet に記録されます。 そのため、バックオフィスチームの業務を変更する必要もありません。 更にコスト面では、自作かつ無料で使えるサービスの組み合わせで構築されているため、基本的にランニングコストはありません! 内部構成 Tickrec は以下のようなサービスを使って構成しています。 使用しているのは、 Slack App と、 Google Spreadsheet と共に動く Google Apps Script (以降、GAS) だけです。 いずれも専用のツールなどが不要でブラウザで操作が完結でき、かつ基本的に無料で利用できます。 このように、今すぐにでも構築を始められるサービスの組み合わせだけで構成しています。 今すぐ構築してみよう! ということで、これから皆さんに、超簡易版の Tickrec を構築してもらおうと思います。 用意するもの 必要なものは下記の通りです。 App を追加できる権限のある Slack workspace Google Account このレクチャーの目的 今回のレクチャーの主な目的は、皆さんに超簡易版の Tickrec を構築してもらい、「これなら私も Slack App を作れそう!」と思ってもらえることです。 実際に Tickrec を構築するのに必要なのは、各サービスのセットアップと、 Tickrec の中枢であるプログラムの実装です。 ですが、プログラムを一から書くと時間がかかってしまうので、プログラムについてはサンプルコードを用意してあります。 したがって、これからレクチャーするのは、サービスのセットアップ手順が主であり、プログラムの書き方の説明は割愛しています。 ただし、 各機能を実現するための GAS 特有の部分などは、キーポイントとして紹介します。 本日のレシピ ここからは、以下のステップに従って進めていきましょう 仕込み1: GASプロジェクトを作る STEP1: GASで打刻する 仕込み2: Slack Appを作る STEP2: GASでSlackに投稿する STEP3: Slackの操作でGASを動かす STEP4: Slackで打刻する それでは早速始めましょう。 仕込み1 GASプロジェクトをつくる まずは下準備として、Spreadsheet と GAS プロジェクトを作成しましょう。 手順 Spreadsheet の用意 Spreadsheet はサンプルを用意してあるので、これをもとにします。 「超簡易版Tickrec向け勤怠シート」がサンプルなので、まずは「コピーを作成」します。 するとあなたの Google Drive に「超簡易版Tickrec向け勤怠シート のコピー」が作成されます。 これで、 Spreadsheet の準備ができました。 GAS プロジェクトの用意 次に GAS プロジェクトを作成しましょう。 先ほど作成した「超簡易版Tickrec向け勤怠シート のコピー」を開きます。 Spreadsheetのメニューから「ツール」->「スクリプト エディタ」を開きます すると「無題のプロジェクト」という名前の GAS プロジェクトが作成されます。 GAS のスクリプトエディタ画面が表示されているはずですので、その「無題のプロジェクト」の部分をクリックし、タイトルを分かりやすいものに変更します これで GAS プロジェクトの準備もできました。 STEP.1 GASで打刻する 「仕込み1」で最低限の準備ができました。 この環境を使って、まずは GAS をつかって打刻してみましょう。 ゴール 「STEP.1」では下記をゴールにして進めていきましょう。 スクリプトエディタで関数を実行して、打刻(開始時刻・終了時刻)ができる 手順 コードの実装 既に実装済みのサンプルコード GAS_step1.gs を用意してありますので、まずはこれをコピーし GAS のスクリプトエディタに貼り付けて上書きします。 スクリプトエディタの「プロジェクトを保存」アイコンをクリックし、コードの変更を保存します。 これで GAS のコードは完成です。 タイムゾーンの設定 次に、GASのタイムゾーンを設定します。 タイムゾーンを設定するには、一時的に古いスクリプトエディタへ切り替える必要があります。 スクリプトエディタの「以前のエディタを使用」ボタンをクリックします。 古いスクリプトエディタ画面が表示されたら、メニューから「ファイル」->「プロジェクトのプロパティ」を開きます。 開かれた「プロジェクトのプロパティ」のダイアログボックスで、「情報」タブの「タイムゾーン」プロパティの値を「(GMT+09:00) と東京」に変更し、「保存」をクリックします 「新しいエディタを使用」ボタンをクリックし、元のスクリプトエディタ画面に戻ります 検証 これで、 GAS を使って打刻できるようになったはずです。 早速、スクリプトエディタの「実行」機能をつかって動くか試してみましょう。 開始時間を打刻する まずは、開始時刻の入力機能を試してみましょう。 スクリプトエディタの上部に、「実行」機能があります。 「実行する関数を選択」部分で実行したい関数名 (今回は recordAttendance() ) を選択します。 続いて、「実行」ボタンをクリックします。 初回の実行時はアクセス権限の許可が必要です。 「実行」ボタンをクリック後、しばらくすると、「承認が必要です」というダイアログボックスが表示されます。 「権限を確認」をクリックします。 続く画面で自分のアカウントを選択します。 さらに続く画面で「許可」ボタンをクリックします。 すると、アクセス権限の許可が完了し、関数の実行が再開します。 Spreadsheet を見てみましょう。新たな行に、実行した日付と時刻が入力されているはずです。 終了時間を打刻する 次は、終了時刻の入力機能を試してみましょう。 スクリプトエディタの「実行する関数を選択」部分で recordLeaving() を選択し、「実行」ボタンをクリックします。 すると Spreadsheet に今度は終了時刻が入力されているはずです。 キーポイント これで「STEP.1」はクリアです! とはいえ、 GAS のコードはコピペしただけですので、具体的にどんなロジックで打刻機能を実現したのか、目に触れる機会もありませんでしたね。 せっかくの機会なので、どのようなコードになっているか、じっくり読んでみてください。 そのためにも、下記で「STEP.1」のコードとそれを実現するための背景技術について、キーポイントを紹介します。 実装した主な関数は下記の通りです recordAttendance() 日付と開始時刻を記録する recordLeaving() 終了時刻を記録する GAS で Spreadsheet を操作するには、 GAS の Spreadsheet Service を使用します。 今回はこれを使い、書き込み対象のセルの特定や、セルへの書き込みを行っています。 また、 Logger を使用し、 GAS でログ出力をしています。 これを使用すると、任意のログをスクリプトエディタの実行ログで見られる用になります。 サンプルコードでは、どのセルを選択しているかログ出力してあるので、コードの挙動を紐解くときに参考にしてみて下さい。 仕込み2 Slack Appをつくる つぎのステップへの下準備として、 Slack App を作成しましょう。 手順 App を作る slack api の apps page で「Create an App」ボタンをクリックします。 表示される「Create an app」ダイアログボックスで「From scratch」を選択します。 次に表示される「Name app & choose workspace」ダイアログボックスで、アプリの名前入力と、開発に使う workspace の選択をします。 最後に「Create App」ボタンをクリックします。 App を作成すると、その App に関する様々な設定が行えるページが表示されます。 App に必要な権限を設定する 続いて、App に必要な権限 (scope) を設定します。 今回は App とユーザー間で Direct Message (以降 DM と記載) を使用してやりとりします。 そのために DM 書き込みに必要な scope を設定します。 まず「Basic Infomation」ページで「Bots」を選択します。 続いて表示される「App Home」ページで「Review Scopes to Add」ボタンをクリックします。 続いて表示される「OAuth & Permissions」ページの下部にある「Scope」について、「Bot Token Scopes」の Oauth Scope を追加します。 追加する scope は下記の通りです。 chat:write im:write scope を追加するには、「Add an Oauth Scope」ボタンをクリックした後、必要な scope を選択します。 追加するのは「User Token Scopes」ではなく「Bot Token Scopes」なので、注意しましょう。 App を workspace にインストールする 上記で設定した scope で、 workspace に App を導入します。 「OAuth & Permissions」ページ上部の「Oauth Tokens for Your Workspace」部分に「Install to Workspace」ボタンが増えているので、クリックします。 すると、 App に workspace のアクセス権限許可の確認画面が表示されるので、「許可する」をクリックします。 ここまでの操作を完了すると、 workspace に App が追加されているのを確認できるようになります。 また、 App の設定画面では、先ほどの「OAuth & Permissions」ページの「Oauth Tokens for Your Workspace」部分に「Bot User OAuth Token」が表示されます。 この Toekn は、 GAS から投稿するときの認証鍵として後で使用しますので、頭の片隅に入れておいて下さい。 STEP.2 GASでSlackに投稿する 「仕込み2」で Slack App を使って Slack workspace に DM する準備ができました。 この環境を使って、次は GAS から Slack に DM してみましょう。 ゴール 「STEP.2」では下記をゴールにして進めていきましょう。 スクリプトエディタで関数を実行して、SlackにDM投稿ができる 手順 コードの実装 再び、サンプルコード GAS_step2.gs を用意してありますので、これをコピーし GAS のスクリプトエディタに貼り付けて上書きします。 追記ではなく上書きですのでご注意下さい。 上書きしたら、スクリプトエディタの「プロジェクトを保存」アイコンをクリックし、コードの変更を保存します。 これで GAS のコードは完成です。 GAS プロパティの設定 次に、GASのプロパティを設定します。 プロパティは、コードとは別に必要なパラメータなどを保持しておくのに便利な機能です。 今回設定するパラメーターは下記の2つです。 Bot User OAuth Token Slack に投稿するときの認証鍵として必要 Slack User ID DM 相手を特定するのに必要 これらのパラメーターはコードに直接書いてしまう方法もあります。 しかし、これらのパラメーターは、環境(作った App や workspace)ごとに異なる値になるため、コードに直接書いてしまうと、コードの再利用が悪くなります。 また、これらのパラメーターは、他人に機密にするべき情報のため、コードに直接書いてしまうと、そのコードを外部に公開すると危険になります。 従って、パラメーターはコードと分離して取り扱うのがベストプラクティスです。 GASのプロパティを設定するには、一時的に古いスクリプトエディタへ切り替える必要があります。 タイムゾーンの設定をしたときと同様に、スクリプトエディタの「以前のエディタを使用」ボタンをクリックします。 古いスクリプトエディタ画面が表示されたら、メニューから「ファイル」->「プロジェクトのプロパティ」を開きます。 開かれた「プロジェクトのプロパティ」のダイアログボックスで、「スクリプトのプロパティ」タブを選択します 追加するプロパティは下記の通りです。 プロパティ 値 slackBotUserOAuthToken Bot User OAuth Token の値 slackDMUserId Slack workspace の自分のメンバー ID プロパティと値を入力するには、「+行を追加」をクリックした後、表示される入力欄にそれぞれ値を入力します。 「Bot User OAuth Token の値」は「仕込み2」の最後で登場した、「OAuth & Permissions」ページの「Oauth Tokens for Your Workspace」部分に表示される「Bot User OAuth Token」の値を入力します。 「Slack workspace の自分のメンバー ID」は、 Slack workspace から取得します。 workspace の右上にある自分のプロフィール写真をクリックし、「プロフィール」をクリックします。 右側に表示されたプロフィール画面から「その他」をクリックします。 表示されたメニューから「メンバー ID を コピー」をクリックすると、メンバー ID がコピーされるので、 GAS プロパティの値にペーストします。 2つのプロパティを入力し終わり、以下の画面のような状態になったら、「保存」ボタンをクリックします。 最後に「新しいエディタを使用」ボタンをクリックし、元のスクリプトエディタ画面に戻ります。 検証 これで、 GAS を使って DM できるようになったはずです。 早速、スクリプトエディタの「実行」機能をつかって、動作するか試してみましょう。 スクリプトエディタの「実行する関数を選択」部分で postSampleSlackDM() を選択し、「実行」ボタンをクリックします。 すると、再度アクセス権限の許可を求められます。 「STEP.1」と同じ要領で、許可してください。 アクセス権限の許可が完了したら、関数の実行が再開します。 Slack の workspace を見てみましょう。 作成した Slack App から This is a plain text section block. という DM が届いているはずです。 キーポイント これで「STEP.2」はクリアです! キーポイントを紹介します。 実装した主な関数は下記の通りです。 postSampleSlackDM() 仮の文言(This is a plain text section block.)をDM投稿する Slackに投稿するには、 chat.postMessage API を使います。 この API は HTTP を使ってアクセスします。 この Slack API を使うために、 GAS から HTTP Request をするには UrlFetchApp classを使います。 また、この Slack API を使う際には、認証のために「Bot User OAuth Token」と、 DM 相手を指定するために「メンバー ID」が必要です。この値は GAS プロパティに設定しましたね。 GAS プロパティに設定した値を参照するには Properties Serviceを使います。 投稿内容は Block Kit を使って記述するのがモダンです。 Block Kit では、投稿する文言と共に、その表示形式などを指定するためのメタデータを含んだ構造化データを取り扱うため、初見だととっかかりにくく感じるかも知れません。 そんなときは Block Kit Builder を使いましょう。 投稿内容をプレビューしながら Blocks を生成できるので、便利です。 STEP.3 Slackの操作でGASを動かす 今度は、 Slack を操作して GAS を動かしてみましょう。 ゴール 「STEP.3」では下記をゴールにして進めていきましょう。 スクリプトエディタで関数を実行して、 Slack にボタンを表示できる ボタンを押すと、 GAS が応答して Slack に新しい投稿ができる 手順 コードの実装 サンプルコード GAS_step3.gs を用意してありますので、これをコピーし GAS のスクリプトエディタに貼り付けて上書きします。 追記ではなく上書きですのでご注意下さい。 上書きしたら、スクリプトエディタの「プロジェクトを保存」アイコンをクリックし、コードの変更を保存します。 これで GAS のコードは完成です。 Web アプリとしてデプロイする 今回のステップでは、実装したコードを Web アプリとして使用します。 そのために、Web アプリとしてデプロイします。 まずは、スクリプトエディタの上部の「デプロイ」の「新しいデプロイ」をクリックします。 表示された「新しいデプロイ」ダイアログボックスにて、「種類の選択」の右脇にある歯車アイコンをクリックし「ウェブアプリ」を選択します。 すると設定の項目が表示されるので、下記の通りに設定し「デプロイ」をクリックします。 説明 適当な説明を記入する 次のユーザーとして実行 「自分」 アクセスできるユーザー 「全員」 デプロイが完了すると、以下のような画面が表示されます。 「ウェブアプリ」部分に表示される URL は、次の「Slack App の Interactivity を有効化する」で使用します。 Slack App の Interactivity を有効化する 続いて、 Slack App の Interactivity 機能を有効化します。 「仕込み2」で登場した Slack App 設定ページの「Interactivity & Shortcuts」ページを開きます。 「Interactivity」部分の右端に表示されているスイッチを On にします。 すると、「Request URL」を入力するフォームが表示されるので、 GAS の Web アプリをデプロイ後に表示された URL を入力します。 検証 これで、 Slack を操作して GAS を動かせるようになったはずです。 早速、試してみましょう。 出勤ボタンを DM する まずは、 Slack にボタンを表示できるか試しましょう。 スクリプトエディタの「実行する関数を選択」部分で postSampleSlackDM() を選択し、「実行」ボタンをクリックします。 Slack の workspace を見てみましょう。 作成した Slack App から 「出勤」 ボタンが DM で届いているはずです。 出勤ボタンをクリックする 今度は、「出勤」ボタンを押して、 Slack に新しい投稿がされるか試しましょう。 先ほど表示された「出勤」ボタンをクリックします。 しばらくすると、「出勤」ボタンが消え、代わりに ボタンの押下を検知しました. と表示されるはずです。 キーポイント これで「STEP.3」はクリアです! キーポイントを紹介します。 実装した主な関数は下記の通りです。 postSampleSlackDM() STEP.2 で登場したが、今回は Slack にボタンを投稿するように改修済み doPost() GAS の Web アプリ機能で使用する特別な関数 Slackからの通知を受けて、 ボタンの押下を検知しました. と投稿する Slack に投稿したボタンを押したことを外部(GAS)に通知するには、 Slack の Interactivity 機能 を使います。 今回は、 Interactivity 機能によって通知される、 block_actions payloads を利用しています。 これは HTTP Request を使って通知されます。 GAS で block_actions payloads 等の外部(Slack)からの HTTP Request を受けるには、 Web アプリ機能 を使います。 HTTP GET method の場合は doGet() function が、 HTTP POST method の場合は doPost() function が call されるようになっています。 Web アプリ機能を使うには、デプロイが必要です。 また、 GAS のコードを更新した場合も、都度デプロイが必要なので、コードの試行錯誤をする場合は気をつけましょう。 GAS の Web アプリ機能で受けた HTTP Request に対し、 response 内容を指定するには、 Content Service を使用します。 Slack の block_actions payloads の通知時は、空の response をすればよいため、サンプルコードでは大した使い方はしていませんが、 Content Service は他の用途では大切になってくる要素ですので、覚えておきましょう。 block_actions payloads で通知を受けた際に、 新たな DM をしたい場合、「STEP.2」で使用した chat.postMessage API で投稿できます。 しかし、 block_actions payloads に含まれる response_url 宛に HTTP POST request する手段も利用できます。 response_url 宛に request する場合、 Token や DM 先の指定が不要なため、利便性が高いです。 STEP.4 Slackで打刻する 遂に最後のステップです。 このステップでは、今まで歩んできたステップでの経験を生かし、最終目的である「Slackから打刻する」ことを実現しましょう。 ゴール 「STEP.4」では下記をゴールにして進めましょう。 スクリプトエディタで関数を実行して「出勤」ボタンをSlackに表示できる 「出勤」ボタンを押してSpreadsheetに開始時刻を記録できる 開始時刻の記録に成功したら「退勤」ボタンをSlackに表示できる 「退勤」ボタンを押してSpreadsheetに終了時刻を記録できる 手順 コードの実装 サンプルコード GAS_step4.gs を用意してありますので、これをコピーし GAS のスクリプトエディタに貼り付けて上書きします。 追記ではなく上書きですのでご注意下さい。 上書きしたら、スクリプトエディタの「プロジェクトを保存」アイコンをクリックし、コードの変更を保存します。 これで GAS のコードは完成です。 Web アプリの再デプロイ 「STEP.3」のキーポイントにも書いたとおり、 Web アプリに最新のコードを反映するには再度デプロイが必要です。 ただし、「新しいデプロイ」を作る必要はありません。 既に存在する「デプロイ」を編集して、新しいバージョンのコードを反映します まずは、スクリプトエディタの上部の「デプロイ」の「デプロイを管理」をクリックします。 表示された「デプロイを管理」ダイアログボックスの左側の「アクティブ」枠に表示されている、既存のデプロイを選択します。 右側の「設定」の右端に表示されている「編集」アイコンをクリックします。 「バージョン」の項目で「新バージョン」を選択します。 「デプロイ」ボタンをクリックします。 すると「新しいデプロイ」を完了したときのように「デプロイの更新」画面で Web アプリ の URL が表示されます。 この URL は「新しいデプロイ」を完了したときの URL と同一です。 Slack の Interactivity の Request URL を更新する必要はありません。 検証 これで、 Slack から打刻ができるようになったはずです。 早速、試してみましょう。 出勤ボタンを DM する まずは、 「STEP.3」と同様にSlack にボタンを表示できるか試しましょう。 スクリプトエディタの「実行する関数を選択」部分で postSampleSlackDM() を選択し、「実行」ボタンをクリックします。 Slack の workspace を見てみましょう。 作成した Slack App から 「出勤」 ボタンが DM で届いているはずです。 Slack から開始時刻を打刻する 次に、また「STEP.3」と同様に、「出勤」ボタンを押します。 但し、今度は「STEP.3」と結果が変わります。 Slack の workspace を見てみましょう。Attendance recorded. の文章と共に「退勤」ボタンが表示されているはずです。 さらに Spreadsheet を見てみましょう。 新たな行に、実行した日付と時刻が入力されているはずです。 先ほど表示された「出勤」ボタンをクリックします。 しばらくすると、「出勤」ボタンが消え、代わりに ボタンの押下を検知しました. と表示されるはずです。 Slack から終了時刻を打刻する 最後に「退勤」ボタンを押してみましょう。 すると今度は Slack には Leaving recorded. の文章と共に、再び「出勤」ボタンが表示されているはずです。 そして Spreadsheet には終了時刻が入力されているはずです。 キーポイント 遂に全てのステップを完遂しました! 最後のキーポイントを紹介します。 実装した主な関数は下記の通りです。 postSampleSlackDM() 出勤ボタンを投稿 STEP3のコードから変更なし doPost() 出勤ボタンが押された場合 recordAttendance() を実行 出勤時刻を入力 recordAttendance() 自体は STEP.1 のコードから変更なし 応答を投稿する 退勤ボタンを投稿 STEP.3 のサンプル文章投稿からボタン投稿に変更 退勤ボタンが押された場合 recordLeaving() を実行 退勤時刻を入力 recordLeaving() 自体は STEP.1 のコードから変更なし 応答を投稿する 出勤ボタンを投稿 postSampleSlackDM() を参考にすれば比較的簡単に改修可能 まとめ うまくうごきましたか? 今回は、 Slack App と Google Spreadsheet、 Google Apps Script を使って勤怠入力システムを構築しました。 しかし、この構成は勤怠入力に限らず、様々な用途にも応用できます。 例えば家計簿や Slack 面白発言集なども作れそうです。 是非皆さんも、今回の構築経験を生かして、便利な Slack App を構築してみてはいかがでしょうか? 著者プロフィール 貝沢 亮介 株式会社WESEEK / システムエンジニア ネットワーク SIer として、複数の大口顧客ネットワークの運用・設計・構築プロジェクトなどに6年従事した後、プログラミングの分野にも携わりたいと一念発起し、 2016 年に WESEEK へ入社。現在では、ネットワーク技術に明るいエンジニアとしての特性を生かし、大手IX事業者の顧客・設備管理システムやネットワークプロビジョニングシステムの設計・構築やプロジェクト管理業務などに従事している。 株式会社WESEEKについて 株式会社WESEEKは、システム開発のプロフェッショナル集団です。 【現在の主な事業】 通信大手企業の業務フロー自動化プロジェクト ソーシャルゲームの受託開発 自社発オープンソースプロダクト「GROWI」「GROWI.cloud」の開発 GROWI GROWIは、Markdown記法でページを記述できるオープンソースのWikiシステムです。 GROWI.cloud GROWI.cloudはOSSのGROWIを専門的知識がなくても簡単に運用・管理できる、法人・個人向けの商用サービスです。 大手SIer・ISPや中小企業、大学の研究室など様々な場所でご利用いただいております。 【主な特徴】 テキストも図表もどんどん書ける、強力な編集機能 チーム拡大に迅速に対応できる管理者向け機能を提供 充実した機能・サポートでエンタープライズにも対応 【導入事例記事】インターネットマルチフィード株式会社様 株式会社HIKKY(VR法人HIKKY)様 WESEEK Tech Conference WESEEK Tech Conferenceは、株式会社WESEEKが主催するエンジニア向けの勉強会です。WESEEKに所属するエンジニアが様々なテーマで発表を行う予定です。 次回は、11/25(木) 19:00~20:00に開催予定です。 WESEEKがサービスを提供している GROWI.cloud を例に、Stripeで簡単に構築・カスタマイズできるサブスクリプション型課金サービスの作り方をご紹介します! 現在、connpassやTECH PLAYで参加受付中です。皆様のご参加をお待ちしております!connpassTECH PLAY 一緒に働く仲間を募集しています 東京の高田馬場オフィス、大分にある別府サテライトオフィスにてエンジニアを募集しております。中途採用だけではなく、インターンシップも積極的に受け入れています! 詳しい募集要項は、弊社HPの採用ページからご確認ください。
アバター
この投稿は、弊社が提供するWESEEK TECH通信の一環です。 WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。 はじめに こんにちは、システムエンジニアのかおりです。本記事では、mongoose の virtual関数についてお話しします。この記事で取り上げる内容は以下になります。 Mongoose とは mongooseの基礎知識と使い方に関してはこちらの記事を参照ください。また、呼び方は「マングース」です。 Virtuals とは Mongooseが提供するvirtual(仮想)関数を使用することで、仮想的にフィールドを定義することができます。これはMongoDBに永続化(保存)されませんが、ドキュメント取得時には付与されます。 【例】 // (1)userSchema を定義する const userSchema = new Schema({ name: { first: String, last: String } }); // 上記で定義したschemaを用いて、Userモデルを定義する const User = mongoose.model('User', userSchema); // ドキュメント(Userインスタンス)を生成する const user1 = new User({ name: { first: 'Hanako', last: 'Yamada' } }); この時、userのフルネームを出力したい場合、以下のようにします。 // "Hanako Yamada" と出力される console.log(user1.name.first + ' ' + user1.name.last); ただし、上記のように毎回連結するのは、面倒な場合があります。 この時、Virtual関数を使用して、仮想フィールドを作成することができます。 (fullNameがMongoDBに永続化されることはありません) // スキーマを定義した後にvirtual関数を使い、仮想プロパティfullNameを定義 userSchema.virtual('fullName').get(function() { // firstName とlastNameを連結させた値を返す return this.name.first + ' ' + this.name.last; }); こうすることによって、mongooseはfullNameプロパティにアクセスする度に、getter関数を呼び出します。 これは、結婚して名字が変わる場合など、動的なデータを取得したい時に便利です。 console.log(user1.fullName); // Hanako Yamada Virtuals の利点 では、virtualの利点とはなんでしょうか? ここでは主に二つあげます。 一つ目は、「toJSON / toObject に反映されないプロパティの実現」です。 mongooseでは、ドキュメントをオブジェクトまたはJSONに変換するときにデフォルトではvirtualsを含みません。 実装者によってvirtualsを含めるか含めないかを選ぶことができます。 含みたい場合、以下のようにスキーマのオプションで設定する必要があります。 schema.set("toJSON", { virtuals: true }); schema.set("toObject", { virtuals: true }); 二つ目は、「保護されたプロパティの実現」です。 Vitualsで定義された仮想プロパティは、保護されており書き換えができません。 getter と setter を分けて定義する機能をもっています。 これは defineProperty によって実現されていますが、mongoose からは離れるので、この記事では割愛します。詳しくは以下のサイトをご参照ください。 【JavaScript】 definePropertyメソッドとは?通常のプロパティ追加との違い Virtuals を populate する Virtuals の populate とはどういうことでしょうか? Mongooseの公式ドキュメントを例に説明します。 Author と BlogPost という二つのモデルがあるとします。 // Authorスキーマは、著者名と複数のブロク投稿を保持する const AuthorSchema = new Schema({ name: String, posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'BlogPost' }] }); // BlogPostスキーマは、タイトルと複数のコメントを保持する const BlogPostSchema = new Schema({ title: String, comments: [{ author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' }, content: String }] }); const Author = mongoose.model('Author', AuthorSchema, 'Author'); const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost'); 上記の例は悪いスキーマのデザインです。なぜでしょうか? 1万件を超えるブログ投稿を書いている非常に多作な著者がいる場合、その作成者のドキュメントは巨大なものになり、サーバーとクライアントの両方でパフォーマンスの問題を引き起こします。 カーディナリティの原則では、作成者からブログ投稿へのような1対多の関係は、「多」側に保存する必要があると述べています。 つまり、ブログの投稿には「作成者」を保存する必要があり、作成者は「すべての投稿」を保存する必要はありません。 そこで、以下のように書き換えます。 // Authorドキュメントから「ブログ投稿」を省き、「著者名」のみを保持する const AuthorSchema = new Schema({ name: String }); // BlogPostドキュメントに「著者名」を追加する const BlogPostSchema = new Schema({ title: String, author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' }, comments: [{ author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' }, content: String }] }); ただし、これら2つのスキーマは、著者のブログ投稿リストへの入力をサポートしていません。 そこで、仮想populateが登場します。仮想populateとは、以下に示すように、refオプションを持つ仮想プロパティでpopulate()を呼び出すことを意味します。 // `ref`プロパティでvirtualのpopulateを有効にします AuthorSchema.virtual('posts', { ref: 'BlogPost', // 参照するモデル(`BlogPost`) localField: '_id', // 参照元のプロパティ(Authorの`_Id`プロパティ) foreignField: 'author' // 参照先モデルのプロパティ(BlogPostの`author`プロパティ) }); const Author = mongoose.model('Author', AuthorSchema, 'Author'); const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost'); そうすることによって、以下に示すように、作成者の投稿をpopulateできます。 const author = await Author.findOne().populate('posts'); author.posts[0].title; // 最初に投稿したblogのタイトル まとめ Virtualsとは MongoDBには保存されない仮想フィールドをつくることができる ドキュメント取得時に参照することができる ref, localField, foreignFieldを指定することによって、virtualsのpopulateが可能になる いかがでしたでしょうか。 最後まで読んでくださりありがとうございました! 参考にさせていただいた記事 Mongoose Virtuals Mongoose GitHub MDN Web Docs
アバター
この投稿は、弊社が提供するWESEEK TECH通信の一環です。 WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。 はじめに こんにちは!WESEEKインターンの宮沢です。今回の記事ではJavascriptのライブラリであるSocket.ioを用いて簡単なチャットアプリを作ってみたいと思います。 Socket.ioとは 公式ドキュメントによると、「あらゆるプラットフォームに対応した双方向・低レイテンシー通信(執筆者訳)」とあります。Socket.ioは、主にWebsocketを使って動作します。Node.js上で動作するサーバーサイドライブラリ、ブラウザ上で動作するクライアントサイドライブラリが提供されています。 WebSocketはサーバー・クライアント間をイベント駆動で双方向に通信を可能にする技術です。Web通信と言えばHTTPというプロトコルをよく耳にすると思います。これは1つのコネクションで1つのリクエストしか送ることができないため、新しい情報を取得するためにはページをリロードする必要がありました。チャットアプリやSNSなどリアルタイムに情報を取得したい場合これは不向きです。対してWebsocketはサーバーとクライアントが一度コネクションを行えばリアルタイムに情報のやりとりが可能になります。 今回はSocket.Ioの中でも基本的なメソッドである on() と emit() のみに絞ってご紹介します。 // サーバー io.on("connection", (socket) => { // "hello" というイベントを発火させ、"world" という文字列を送信する socket.emit("hello", "world"); }); // クライアント // "hello" というイベントを受信できるようにする // 取得したメッセージを出力する socket.on("hello", (message) => { console.log(message); }); 環境構築 チャットアプリを作成するにあたり、必要なライブラリのインストール、ファイル、ディレクトリを用意していきます。 下記のようなディレクトリー、ファイルを用意します。 simple_chat/ ├── package.json └── src ├── index.html └── index.js package.jsonには以下のように記述します。チャットアプリで必要になるライブラリ(expressとsocket.io)を"dependencies"に入れてあります。 { "name": "simple_chat", "version": "1.0.0", "main": "index.js", "license": "MIT", "dependencies": { "express": "^4.17.1", "socket.io": "^4.3.1" } } 必要なライブラリーのインストールを行います。node、npm、yarnが自分の環境にインストールされている必要があります。 # simple_chatディレクトリに入る $ cd simple_chat # ライブラリーのインストール $ yarn install simple_chatディレクトリー配下に、node_modulesとyarn.lockが新たに追加されていることが確認できると思います。ひとまずこれでチャットアプリが作成できる状態になりました。 チャットアプリを作る さっそくチャットアプリを作成していきます。 編集するファイルは以下の通りになります。 index.js(サーバー) index.html(フロント) まずはexpressを使ってサーバーを作成します。 index.js const express = require('express'); const http = require('http'); const app = express(); const server = http.Server(app); const PORT = 3000; // ルーティングの設定。'/' にリクエストがあった場合 src/index.html を返す app.get('/', (req, res) => { res.sendFile(__dirname + '/index.html'); }); // 3000番ポートでHTTPサーバーを起動 server.listen(PORT, () => { console.log(`listening on port ${PORT}`); }); index.html simple chat ターミナルを開いてサーバーを起動します。 $ node src/index.js サーバー側には listening on port 3000 というログが出力され、ブラウザで http://localhost:3000/ を開くと 以下のように表示されていると思います。 次にSocket.ioの処理を記述していきます。 index.js const express = require('express'); const http = require('http'); // Socket.ioをインポート const socketIo = require('socket.io'); const app = express(); const server = http.Server(app); // 初期化 const io = socketIo(server); const PORT = 3000; app.get('/', (req, res) => { res.sendFile(__dirname + '/index.html'); }); server.listen(PORT, () => { console.log(`listening on port ${PORT}`); }); // クライアントとのコネクションが確立したら'connected'という表示させる io.on('connection', (socket) => { console.log('connected'); }); index.html simple chat // サーバーへ接続 const socket = io(); サーバーを再起動、localhost:3000にアクセスするとサーバー側のログにconnectedが出力されていると思います。 続いてクライアント側でinputにテキストを入力、送信ボタンを押すとサーバー側にメッセージを送信するイベントを作成します。 index.js const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const app = express(); const server = http.Server(app); const io = socketIo(server); const PORT = 3000; app.get('/', (req, res) => { res.sendFile(__dirname + '/index.html'); }); server.listen(PORT, () => { console.log(`listening on port ${PORT}`); }); io.on('connection', (socket) => { console.log('user connected'); // 'sendMessage' というイベント名で受信できる // 第一引数には受信したメッセージが入り、ログに出力する socket.on('sendMessage', (message) => { console.log('Message has been sent: ', message); }); }); index.html simple chat const socket = io(); const clearText = () => { document.getElementById('inputText').value = ''; } // 送信ボタンのクリックイベントを検知 document.getElementById('sendButton').addEventListener('click', () => { // inputに入力されたテキストを取得 let inputMessage = document.getElementById('inputText').value; if (inputMessage === '') { return; } // 'sendMessage' イベントを発火、メッセージを送信 socket.emit('sendMessage', inputMessage); // input 内のテキストを空にする clearText(); }); サーバーを再起動、localhost:3000にアクセスします。inputに適当な文字列を入力、送信ボンタンを押すとサーバー側に入力された文字列が出力されていると思います。 最後にサーバーから全てのクライアントに対して入力した文字列を表示できるようにします。 index.js const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const app = express(); const server = http.Server(app); const io = socketIo(server); const PORT = 3000; app.get('/', (req, res) => { res.sendFile(__dirname + '/index.html'); }); server.listen(PORT, () => { console.log(`listening on port ${PORT}`); }); io.on('connection', (socket) => { console.log('user connected'); socket.on('sendMessage', (message) => { console.log('Message has been sent: ', message); // 'receiveMessage' というイベントを発火、受信したメッセージを全てのクライアントに対して送信する io.emit('receiveMessage', message); }); }); index.html simple chat const socket = io(); const clearText = () => { document.getElementById('inputText').value = ''; } const addMessageList = (message) => { const ul = document.getElementById('messageList'); const li = document.createElement('li'); const text = document.createTextNode(message); li.appendChild(text); ul.appendChild(li); }; document.getElementById('sendButton').addEventListener('click', () => { let inputMessage = document.getElementById('inputText').value; if (inputMessage === '') { return; } socket.emit('sendMessage', inputMessage); clearText(); }); // 'receiveMessage' イベントの発火を検知 // 第一引数には受信したメッセージが入る socket.on('receiveMessage', (message) => { // 受信したメッセージをulタグに挿入 addMessageList(message); }); 本記事冒頭のgifのような挙動になったら完成です!お疲れ様でした。今回扱ったソースコードは、githubに上げてあるので、クローンして気軽に立ち上げることができます。 参考にさせていただいた記事 Socket.io
アバター
この記事は、 2021/9/30 に行われた WESEEK Tech Conference の内容です。 目次 テストについて普段思うこと みなさん普段テストについて思っていることはありますか? テストをどこから書いていけばいいかわからない テストの無いプロジェクトにどうやって導入すればいいのか ましてや、TDDは敷居が高そう テストを書く時間がない テストを書いたが、いちいち手元で実行するのが面倒 仕様と実装の整合性を合わせるのが大変 テストを書く際にこのあたりのことについて思ったりしたことがあるのではないでしょうか。そこで、今回は下記についてお話をしていこうと思います。 テストができるようになるまで ツールの導入 はじめの一歩 テストの書き方 Gitにプッシュしたらテストを自動で実行する方法 CI 挫折しないテストの心得 開発ルール 仕様と実装の整合性を楽にする スキーマファースト 今回の内容は、まだプロジェクトにテストツールを導入したことがない方、テストを書いたことがない方向けの、入門的な内容となります。 テストができるようになるまで ツールの導入 まず、テストを実行できるようになるためのツールの導入を行います。 テストができるようになるまでにやることは下記です。 RSpec の導入 テストデータベースの設定 RSpec の起動を速くする設定 RSpec の並列実行の設定 RSpec の実行 RSpec の導入 Ruby で使えるテストフレームワーク RSpec をインストールします。 これは、 Gemfile に下記を記載して bundle install を行います。 group :development, :test do gem 'rspec-rails', '~> 5.0.0' end $ bunlde install テストデータベースの設定 テストを実行するために、一時的にデータを保存するためのデータベースが必要です。これは、特に設定をしなくとも Rails アプリケーションを作成した際に、自動的に設定されている場合が多いです。 config/database.yml にデータベースの設定が記載されています。 PostgreSQL/MariaDB をデータベースに使用する場合の記載例は下記です。 test: ファクトリからユーザーモデルのデータが作成されています。 テストデータのトランザクション Rspec でのテストデータの取り扱いについて少し説明します。 RSpec はテストケースごとにトランザクションが張られています。1つのテストが終わる度にロールバックするので、テストの独立性が保たれています。 より複雑なトランザクションを制御したい場合は、 Database Cleaner Gem を使いますが、基本的には標準のままで事足ります。 より詳しい RSpec の構文について 今回は時間が限られているため、 RSpec の詳しい書き方については触れませんでした。 RSpec でのテストの書き方についてはより詳しい書籍があるため、そちらを紹介します。 Everyday Rails - RSpecによるRailsテスト入門 Aaron Sumner, Junichi Ito (伊藤淳一), AKIMOTO Toshiharu, 魚振江https://leanpub.com/everydayrailsrspec-jp Gitにプッシュしたらテストを自動で実行する方法 CI CI の導入 自動でテストが実行されていない場合、毎回、ローカル環境でテストを実行するのは面倒ですし、レビュー時にテストがパスしている品質のものが来ているのか不安に思います。 そこで、 Continuous Integration(継続的インテグレーション)を導入しましょう。 CI とは Continuous Integration(継続的インテグレーション) は、Git 等リポジトリへのコードの変更を契機に、自動化されたビルドやテストが実行される環境とその開発手法のことを言います。 CI を実現するものには、 GitHub Actions, Jenkins, CircleCI などがあります。 これから CI を導入するなら GitHub Actions のほうが手軽です。 GitHub Actions での CI 構築 それでは、今回は GitHub Actions で CI を組んでみます。 まず、 CI で何を実施したいかを考えます。 テストに必要なツールの準備 ruby のインストール DB の準備 lint (静的解析ツール) の実行 テストの実行 それでは、構築していきます。 まず、 CI を構築したい GitHub リポジトリの Actions タブを選択します。 次に New workflow ボタンを押します。 すると、テンプレートの一覧が表示されます。今回は、 Ruby の Set up this workflow ボタンを選択しましょう。 workflow の編集画面に遷移します。 workflow の名前を変更しておきます。それでは、実際に編集していきます。 今回は、下記のような YAML を作成しました。 name: Ruby on Rails CI on: push: branches-ignore: - stable jobs: rails-test: runs-on: ubuntu-latest strategy: matrix: ruby-version: - 2.7.4 env: RAILS_ENV: test PROJECT_DATABASE_HOST: 127.0.0.1 PROJECT_DATABASE_PORT: 3306 PROJECT_DATABASE_DBNAME: db_test PROJECT_DATABASE_USERNAME: user PROJECT_DATABASE_PASSWORD: password TZ: Asia/Tokyo services: mariadb: image: mariadb:10.6.0 env: MYSQL_DATABASE: ${{ env.PROJECT_DATABASE_DBNAME }} MYSQL_USER: ${{ env.PROJECT_DATABASE_USERNAME }} MYSQL_PASSWORD: ${{ env.PROJECT_DATABASE_PASSWORD }} MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' TZ: Asia/Tokyo options: >- --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 3306:3306 steps: - name: Checkout uses: actions/checkout@v2 # Resolve gems dependencies - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true # Pre-test setting - name: Setup database run: bundle exec rake db:create db:schema:load # Executing lint - name: Run linters run: bundle exec rubocop # Executing test - name: Run tests run: bundle exec rspec slack-notify: runs-on: ubuntu-latest needs: - rails-test if: always() steps: # run this action to get workflow conclusion # You can get conclusion via env (env.WORKFLOW_CONCLUSION) - name: Receive job statuses uses: technote-space/workflow-conclusion-action@v2 - name: Slack Notification uses: weseek/ghaction-slack-notification@master with: type: ${{ env.WORKFLOW_CONCLUSION }} job_name: '*Test*' channel: '#channel_name' isCompactMode: true url: ${{ secrets.SLACK_WEBHOOK_URL }} 各ブロックを説明していきます。 name には、任意の workflow の名前を設定します。 on は、その workflow をどのような契機で実行するのか指定するものです。 push を指定し、リポジトリにコードがプッシュされる度に workflow が実行するようにします。 branches-ignore: stable とすることで、 stable ブランチへのプッシュの場合は workflow が実行されないようにしています。 job に rails-test という名前を付け、その中でイメージの指定や、環境変数、実行するステップを定義していきます。 env はこの後に説明する service で使用するための環境変数を定義しておきます。 services にはテストの実行に必要なコンテナを定義します。 今回はテスト用のデータベースである mariadb を定義します。 steps には、前述の「CI で何を実施したいか」で考えた内容を設定していきます。 uses で、定義済みの action を利用できます。 uses: actions/checkout@v2 はソースコードをチェックアウトするために使用しています。 uses: ruby/setup-ruby@v1 で ruby のセットアップをしています。 準備ができたら、 Setup database, Run linters, Run tests の3つの name のステップを定義し、それぞれ「データベースの準備」「静的解析の実行」「テストの実行」を行います。 run では、任意のコマンドを実行できます。 run: bundle exec rake db:create db:schema:load と書くことで、テスト用の DB を初期化しています。 slack-notify は、 CI 完了時に slack へ通知するための設定を行っています。 workflow の編集が終わったら、 Start commit でコミットを作成し、保存します。今回は GitHub の workflow 編集ツールで編集を行いましたが、 任意の Git クライアントで /.github/workflows/*.yml をコミット&プッシュして wofkflow を作成しても大丈夫です。 リポジトリにプッシュを行うと自動的に workflow が実行されます。 Actions の workflow 一覧には、実行結果が表示されます。 一覧の何れかの結果を選択すると、このようになっています。 rails-test が実行され、最後に slack-notify を実行していることがわかります。 GitHub Actions は PullRequest とも連携しており、便利です。該当する PullRequest に All checks have passed のように workflow の実行結果が反映されます。 挫折しないテストの心得 開発ルール テストツールの導入、CIの構築で、テストを実行できる環境は準備できました。ここからは気楽にテストを書いていく開発手法を紹介していきたいと思います。 何から書けばいいのか 実際にテストを書いていこうとした時、「何から書いていけばいいのか」悩みます。ざっとテストの種類を挙げると、下記のものがあります。 Model specs Controller specs Request specs Feature specs View specs Helper specs Mailer specs Routing specs Job specs System specs これらのテストを最初からすべて整備しようとすると、モチベーションを維持するのが難しそうです。そこで、最初は下記に絞って、小さく始めましょう。 Model specs Controller specs Request specs Feature/System specs などは最初は書かず、手動でのテストを併用しましょう。 よく聞く TDDで開発したほうがいいのか そもそも TDD とは何でしょうか。 TDD は、下記のサイクルを回して行う開発手法です。 はじめにテストコードを書く テストがパスするコードを書く 罪を犯した書き方をしてもよい リファクタリング まず最初にテストコードを書きます(テストファースト)。 2番目の「テストがパスするコードを書く」では「罪を犯した書き方をしてもよい」とあるので、動けばいいコードを書いても問題ありません。その後のリファクタリングできれいにしていきます。 さて、 TDD (テストファースト) で書いたほうがいいかですが、テストツールの導入期は気楽さを維持するために書かなくてもよいと考えています。先に機能を実装し、同じ PR もしくは少し遅れてテストを書くようにしましょう。ただし、テストを後回しにする代わりに罪を犯した書き方は極力しないようにしましょう。 どの粒度まで書けばいいのか すでにプロジェクトが走っているものに後からテストを導入した際、どこまでテストを書けばいいのか悩みます。『テスト駆動開発』の書籍から引用すると、「不安が退屈に変わるまで書く」とあります。 後からテストを導入した場合は、不安のある箇所からテストを書き始めるか、新しい実装部分からテストを書き始めましょう。それ以外の部分は対応の必要がないならばそのままにしておきます。 テスト駆動開発 Kent Beck 著/和田 卓人 訳https://shop.ohmsha.co.jp/shopdetail/000000004967/ 仕様と実装の整合性を楽にする スキーマファースト さいごに、簡単にスキーマファーストについて紹介したいと思います。 Rails アプリケーションで、 API を組むことがあると思いますが、これのテストを書こうとした時に便利なものです。 OpenAPI 先に、 OpenAPI について説明します。 OpenAPI は Rails など API を実装する際の仕様をまとめるドキュメントです。 committee + OpenAPI OpenAPI で仕様を定義しておくと、これを使って実装が仕様どおりに行われているかテストしたくなってきます。 テストを行うためには、 committee を利用します。 committee は、 assert_response_schema_confirm メソッドを提供し、これを利用して、 OpenAPI ドキュメントに従って、レスポンスの JSON スキーマと一致するかチェックできるようになります。 Gemfile に下記を追加し、 bundle install します。 group :development do gem 'committee-rails' end $ bundle install 下記は committee を利用したテストコードの例です。 assert_response_schema_confirm(200) で、OpenAPI と実装が合致しているかテストしています。 describe 'request spec' do include Committee::Rails::Test::Methods def committee_options @committee_options ||= { schema_path: Rails.root.join('schema', 'schema.json').to_s, query_hash_key: 'rack.request.query_hash', parse_response_by_content_type: false, } end describe 'GET /' do it 'conform json schema' do get '/' assert_response_schema_confirm(200) end end end まとめ 冒頭で気楽に書こうと言いましたが、テスト導入時は準備することが多く大変です。 挫折しないために、最初から頑張りすぎないようにします。 テストファーストで書かないでやってみる 書ける分のテストを書いて、小さく始める 手動テストとうまく併用する 上記ができたら、 手動テストも自動化へ TDD スキーマファースト 等、段階的にテストツールを導入していきましょう。 テストが本当に楽になっていくのと、その恩恵を受けられるようになるのはここからです。 著者プロフィール 田村 貴幸 株式会社WESEEK / システムエンジニア Webシステム開発会社を2社経験後、2019年8月にWESEEKに入社。 現在は、大手IXの業務自動化システムの機能開発やインフラの設計・構築に携わる。 趣味は、電子工作と釣り。 株式会社WESEEKについて 株式会社WESEEKは、システム開発のプロフェッショナル集団です。 【現在の主な事業】 通信大手企業の業務フロー自動化プロジェクト ソーシャルゲームの受託開発 自社発オープンソースプロダクト「GROWI」「GROWI.cloud」の開発 GROWI GROWIは、Markdown記法でページを記述できるオープンソースのWikiシステムです。 GROWI.cloud GROWI.cloudはOSSのGROWIを専門的知識がなくても簡単に運用・管理できる、法人・個人向けの商用サービスです。 大手SIer・ISPや中小企業、大学の研究室など様々な場所でご利用いただいております。 【主な特徴】 テキストも図表もどんどん書ける、強力な編集機能 チーム拡大に迅速に対応できる管理者向け機能を提供 充実した機能・サポートでエンタープライズにも対応 【導入事例記事】 インターネットマルチフィード株式会社様 [https://growi.cloud/interviews/mfeed/?utm_source=connpass-top&utm_medium=web-site&utm_campaign=mf:embed:cite] 株式会社HIKKY(VR法人HIKKY)様 [https://growi.cloud/interviews/hikky:embed:cite] WESEEK Tech Conference WESEEK Tech Conferenceは、株式会社WESEEKが主催するエンジニア向けの勉強会です。WESEEKに所属するエンジニアが様々なテーマで定期的に発表を行う予定です。 次回は、10/28(木) 19:00~20:00に開催予定です。 実際に WESEEK で使っている勤怠入力システム「tickrec」の構成を元に、Slackで勤怠入力ができるアプリケーションを1から構築するための方法をレクチャーします。 現在、connpassやTECH PLAYで参加受付中です。皆様のご参加をお待ちしております!https://weseek.connpass.com/event/226591/TECH PLAYはこちらから 一緒に働く仲間を募集しています 東京の高田馬場オフィス、大分にある別府サテライトオフィスにてエンジニアを募集しております。中途採用だけではなく、インターンシップも積極的に受け入れています! 詳しい募集要項は、弊社HPの採用ページからご確認ください。
アバター
この投稿は、弊社が提供するWESEEK TECH通信の一環です。 WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。 はじめに こんにちは、システムエンジニアのかおりです。本記事では、Node.jsのEventEmitterについてお話します。この記事で取り上げる内容は以下になります。 Event Emitter とは 何かのイベントが発生した時に、それがトリガーとなって、あらかじめ登録しておいた処理を実行することができます。 Event Emitter の作成 Event Emitterを作成するには、Node.jsのeventsモジュールからEventEmitterインスタンスを生成する必要があります。 const EventEmitter = require('events'); const myEmitter = new EventEmitter(); // EventEmitterインスタンスを生成 イベントの発動と、それに対応する処理 Event Emitterの基本的な使い方として、on()とemit()関数についてお話します。 on(eventName , listener ) 第一引数に指定したイベントに紐づけて、listenerを登録します。 emit(eventName , [args]) この関数でイベントが発動し、第二引数以降のargsを引数として渡します。 emit()関数でイベントが発動し、on()関数で登録されていた処理が実行されます。 import { EventEmitter } from 'events'; const eventEmitter = new EventEmitter(); // イベントが発動された時の処理を記述する eventEmitter.on('myEvent', () => { console.log('Emitted Event'); }); // イベントを発動させる eventEmitter.emit('myEvent'); 上記のコードが実行された時に、以下のように出力されます。 > Emitted Event ※on()を使って登録されたlistenerは、そのイベントが発火される度に実行されます。 同期か非同期か Event Emitterによって発生したイベントは、同期的に実行されます。 import { EventEmitter } from 'events'; const eventEmitter = new EventEmitter(); eventEmitter.on('myEvent', (data) => { console.log(data); }); console.log('A'); eventEmitter.emit('myEvent', 'B'); console.log("C"); 上記のコードを実行した場合、以下のように上から順番に出力されます。 > A > B > C listenerの実行順序 EventEmitterによって発生したイベントは、登録された全てのlistenerが順番に実行されます import { EventEmitter } from 'events'; const eventEmitter = new EventEmitter(); eventEmitter.on('myEvent', (data) => { console.log(data, 'FIRST'); }); console.log('A'); eventEmitter.on("myEvent", data => { console.log(data, 'SECOND'); }); // イベントの発動 eventEmitter.emit('myEvent', 'Emitted Statement:'); console.log("B"); 上記のコードが実行された場合、以下のように出力されます。 > A > Emitted Statement: FIRST > Emitted Statement: SECOND > B ある特定のイベントに対して反応させるには、同じEventEmitterインスタンスからemitを実行する必要がある on()で登録したイベントハンドラをトリガーするには、同じEventEmitterインスタンスのemit()を呼び出す必要があります。 以下のコードでは、別のEventEmitterインスタンスが使用されているため、コンソールには何も出力されません。 import { EventEmitter } from 'events'; const eventEmitter1 = new EventEmitter(); eventEmitter1.on('myEvent', () => { console.log('myEvent emitted'); }); const eventEmitter2 = new EventEmitter(); eventEmitter2.emit('myEvent'); 実際の利用時には複数のEventEmitterインスタンスを使うことは稀だと思いますが、イベントが伝播しない等の症状が起こった場合はこの辺りも疑ってみましょう。 Event Emitter クラスの関数 EventEmitterクラスの中で、on() emit()以外の関数を取り上げます。 once(eventName, listener) listenerを一度だけ実行したい場合に使用されます。 このメソッドを使用すると、イベントをリッスンした後に、listenerが破棄されます。 addListener(eventName, listener) これはon()のエイリアスです。 removeListener(eventName, listener) これは、listenerを削除するために使用されます。 removeAllListeners([eventName]) これは、 eventName に登録されている全ての listener を取り除きます。 Event Emitter の使い所 弊社で開発中のナレッジ共有サービス「GROWI」を例に出して説明します。 GROWIでは、ページにコメントが追加された時にDBにコメントのドキュメントが保存されます。 DBにデータが保存されたタイミングで特定の処理をしたい時には、Modelスキーマが提供するpost save hookを利用して実行することができます。 ただし、注意点として、モデルからサービス層などで定義された関数を直接呼び出すのはアーキテクチャの設計的によくありません。 そこで、post save hookにてEventEmitterでイベントを発火、それをサービス層で検知して登録していた処理を実行させることができます。 React EventEmitter Reactで状態管理にEventEmitterを使用する 以下の記事が参考になりますので、よければチェックしてみてください。 Event Emitter instead of lifting state up in React まとめ EventEmitterは 何かしらのイベントをトリガーとして、処理を実行する イベントに登録されているlistenerを同期的に呼び出す イベントに登録されているlistenerを登録された順番に呼び出す いかがでしたでしょうか。 最後まで読んでくださりありがとうございました! 参考にさせていただいた記事 Events | Node.js v16.10.0 documentation Node Event Emitters — For Beginners and Experts Node.jsのEventEmitterでイベント駆動プログラミングをする
アバター
この投稿は、弊社が提供するWESEEK TECH通信の一環です。 WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。 はじめに 今回の記事では Rails の権限管理 gem である CanCanCan について、導入手順と機能の解説を行います。導入手順を飛ばして機能の解説を読みたい方はこちらを押してください。 CanCanCan とは CanCanCan とは、Ruby on Rails の権限管理 gem であり、特定のユーザーがアクセスできるリソースを制限します。 どういうことかと言うと、例えば admin, manager, read_only という3つの権限があったとして、それぞれは以下のことができるように設定を行うことができます。 admin という権限を持っているユーザーは管理者用の画面含め全ての画面を閲覧可能で、ページ上で全てのモデルの操作が可能 manager という権限を持つユーザーは管理者用の画面を閲覧することができないが、それ以外のページでモデルの操作が可能 read_only という権限を持つユーザーはモデルの操作ができず閲覧のみ可能 CanCanCan を用いることでこんな感じの機能を簡単に実装することができます。 CanCanCan を使うための準備 CanCanCan はこの gem 単体で用いることはほとんどなく、多くの場合は devise というログイン機能を実装する gem とセットで使います。 devise により追加された User モデルに今回新しく追加する Role モデルを紐づけて、それを CanCanCan で追加する Ability クラスで管理して使います。 この記事では、devise によってすでに User モデルが存在するという前提で解説をします。 devise に関しては今度機会があれば解説の記事を書こうと思います。 インストール ではまず、CanCanCan のインストールを行います。 Gemfile に以下の記述を追加し、bundle install を実行します。 gem 'cancancan' $ bundle install Gemfile.lock を確認し、CanCanCan がインストールされていることを確認します。 cancancan (3.3.0) Role モデルの作成 インストールが完了したら、ユーザーに付与する権限を管理する Role を作成していくのですが、今回は1つのユーザーに複数の権限を付与できるようにしたいと思います。 なので Role モデルと、User - Role 間の多対多を実現するための中間テーブルとして機能する UserRole モデルの2つを作成します。 $ rails generate migration CreateRoles invoke active_record create db/migrate/20210904000000_create_roles.rb マイグレーションファイルが作成されたので、このファイルに以下の記述を追加します。 カラムは権限名を管理する name を必須カラムとして定義します。 class CreateRoles 0.0144s == 20210904000000 CreateRoles: migrated (0.0146s) ============================= == 20210904000000 CreateUserRoles: migrating ================================== -- create_table(:user_roles) -> 0.0362s == 20210904000000 CreateUserRoles: migrated (0.0363s) ========================= そうしたらモデルファイルの作成を行います。 app/models 配下に role.rb を用意して以下の記述を追加します。 class Role Role.create(name: 'admin') # { :id => 1, :name => "admin", :created_at => Sat, 04 Sep 2021 09:26:22 UTC +00:00, :updated_at => Sat, 04 Sep 2021 09:26:22 UTC +00:00 } 続いて manager, read_only も作成します。 [2] pry(main)> Role.create(name: 'manager') [3] pry(main)> Role.create(name: 'read_only') これで3つの role が作成されました。 権限ごとにルールを設定する admin であれば全ての画面を閲覧可能、read_only であれば 閲覧のみ可能というように、それぞれの Role に対して設定を行なっていきます。 先ほどの rails generate cancan:ability で app/models 配下に ablity.rb というファイルが生成されているはずなので、このファイルを開きます。 class Ability include CanCan::Ability def initialize(user) # Define abilities for the passed in user here. For example: # # user ||= User.new # guest user (not logged in) # if user.admin? # can :manage, :all # else # can :read, :all # end # # The first argument to `can` is the action you are giving the user # permission to do. # If you pass :manage it will apply to every action. Other common actions # here are :read, :create, :update and :destroy. # # The second argument is the resource the user can perform the action on. # If you pass :all it will apply to every resource. Otherwise pass a Ruby # class of the resource. # # The third argument is an optional hash of conditions to further filter the # objects. # For example, here the user can only update published articles. # # can :update, Article, :published => true # # See the wiki for details: # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities end end お、何やら沢山のコメントが書かれているようです。 このコメントに設定方法が記されており、意訳するとこのようになります。 例えばこのようにして ability を定義します。 user ||= User.new if user.admin? can :manage, :all else can :read, :all end can の第一引数には :read, :create, :update, :destroy を定義します。 manage を渡すとこれらの4つ全てが有効になります。 can の第二引数には、第一引数で設定したアクションを実行できるモデルを設定します。 :all を渡すと全てのモデルが対象になります。 can の第三引数ではさらに追加の設定を行うことができます。 詳しくは wiki をご覧ください。 https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities 基本的には第一引数にアクションを定義し、第二引数でそのアクションを実行できるモデルを定義します。 can :read, :all can :update, Book 例えばこのような記述だと、各モデルの一覧画面と詳細画面を閲覧でき、Book モデルだけは編集画面の閲覧とモデルの更新(update)の操作を行えるようになります。 この説明を元に各権限に対して設定を行うと、以下のようになります。 class Ability include CanCan::Ability def initialize(user) user ||= User.new # role が admin のユーザーはモデルの操作を行うことができて管理者画面を閲覧可能 if user.has_role?(:admin) can :manage, :all can :access_admin_page, :all # role が manager のユーザーはモデルの操作を行うことができるが、管理者画面は閲覧不可能 elsif user.has_role?(:manager) can :manage, :all cannot :access_admin_page, :all # role が read_only のユーザーはモデルの操作を行うことができず閲覧のみ可能、管理者画面は閲覧不可能 elsif user.has_role?(:read_only) can :read, :all cannot :access_admin_page, :all # role を持っていないユーザーは全ての画面が閲覧不可能 else cannot :read, :all end end end cannot は can の反対でアクションを実行できなくします。 一部の画面だけアクションを制限させたい場合に、can :manage, :all などの記述と併用することで楽に設定を行えます。 authorize_resource でアビリティをチェックする ability.rb ファイルに権限ごとに設定を行いましたが、これだけでは設定を行なっただけで実際にアビリティのチェックが行われません。 チェックを行うにはコントローラーファイルに設定を記述する必要があります。 以下の例では、ユーザーが edit メソッドを実行した時に :update のアビリティを持っているかチェックし、持っていれば edit 画面を表示するようにしています。 class BooksController エラー画面にならず、root パスにリダイレクトされてメッセージが表示されることが確認できました。 その他詳しい解説と便利な機能 アビリティのエイリアス CanCanCan には :read, :create, :update, :destroy (と :manage) のアビリティが用意されていますが、これに加えて Rails の 7 つの RESTful アクションと同じエイリアスが自動的に設定されています。 read: [:index, :show] create: [:new, :create] update: [:edit, :update] destroy: [:destroy] このエイリアスが存在するおかげで authorize_resource 使用時にアクションに対応するアビリティをチェックできます。 リソースの自動読み込みで Controller ファイルを省略する CanCanCan にはリソースを自動で読み込む便利な機能が備わっています。 この機能を用いた場合、例えば index メソッドだと def index @books = Book.all end show メソッドであれば def show @book = Book.find(params[:id]) end などといったリソースを読み込む際に用いられる一般的な処理を、Controller ファイルに load_resource を記述することによって自動で行ってくれるようになります。 リソースの自動読み込みを有効にした際、モデル名に準ずる処理が実行されます。 例えば Book モデルの場合、@books や @book のインスタンス変数にリソースが格納されます。 これにより :index, :show, :new, :edit の4つのメソッドを完全に省略することができるため、ただの CRUD 機能を持ったモデルの Controller ファイルは以下のように書き換えられます。 class BooksController
アバター
この記事は、2021/9/16 に行われた WESEEK Tech Conference の内容のうち、後編の「OpenID Connect 基盤で複数のバックエンドの認証、認可を統一した話」の部分をまとめた記事となります。 目次 実現したこと まず、最初に本記事で実現したことをご紹介します。 認証認可部分の実装をサービスの外に出し、共通化した どのようなサービスに対しても、OIDC 基盤と nautilus 上にあるユーザ・権限情報を利用して、認証認可を追加できるようになった 全体の構成図としては以下のような形になりました。 モチベーション JPNAP ポータルサイト(My.JPNAP) は発足した nautilus というバックエンド 1 つの状態 nautilus 以外で提供している情報も顧客向けに公開したい(追加要件) nautilus = My.JPNAP backend トラフィックグラフ 実際に顧客向けインターフェースで流れているトラフィック量 Looking Glass etc… 前段に oauth2-proxy を導入することで、既存のサービスに手をいれずに SSO 認証基盤を利用した認証は可能になった 認可は? nautilus ではアプリ内に自前で認可ロジックを入れて、リクエストが来たときにチェックするようにしていた それ以外のサービスの場合、oauth2-proxy だけでは高度な認可は実現できない チェック対象のサービスによらず、URL ごとに必要な権限のチェックができるようにしたい 上記のような追加要件に対して、別のサービスに対しても同じ枠組みで認証/認可の仕組みを入れたい!という要望が出てきました。 アプリ内で認可ロジックを入れている現状を、他のサービスに対してどのように共通化/共有化を実現するかを考えていきました。 技術検討ですが、量が多いため 構成編、認可手法編 の 2 部に分けて説明します。 技術検討(構成編) 前提条件 nautilus が権限情報を持っている どの契約情報に対して、どういう操作を許可するのか 既にあるサービスに手を入れる形にはしたくない 面倒見なければいけないコード量を増やしたくない 違う言語/FW を使ってるアプリだと、増えるのはコード量だけではない OSS だったら拡張して、自前バージョンを作り、保守する必要がある… 稼働環境は Kubernetes を利用している 上記のような前提のもと、サービスにリクエストが到達する前に認可できるようにできないかを考えました。 構成イメージ 構成イメージ/導入前導入後の理想像/認可ロジックはまだ不明 課題まとめ これまで出てきた要件、前提をまとめると、解決すべき課題は以下の 5 点になりました。 認可ロジックだけをどうやって抜き出してサービス前段で実行するのか 利用者からのリクエストは HTTP なので、前段にリバースプロキシを入れる? プロキシにどの実装を使うのか プロキシの設定をどうやって管理/反映するのか 認可ロジックを何で実装するか 認可情報をサービスへどうやって渡すのか 構成編では 1. ~ 3. を扱い、4./5. については認可手法編で紹介します。 手法検討 どのような手法で実現するかという点については、Kubernetes を利用していたこともあり、「サービスメッシュ」という単語がチーム内で出てきたため、以下の 2 手法からどちらを選択するかという話になりました。 サービスメッシュを導入する メッシュのプロキシに認可レイヤーを持たせる 既に Kubernetes を利用していたため、Istio がチーム内で知られていた ただ、認可のレイヤーを追加するだけでは too much 感? サービスの前にプロキシを自前で立てる プロキシ実装の選択、設定などを自分で考え、管理・運用する必要がある 認可対象のサービスごとに異なる設定以外も管理・運用する必要がある どういう構成で動かすのか、どういう設定にするか 考えなければいけないことが多い チームで検討した結果、技術的に面白そう、要件を満たせそう、という点から 1. を選択することになりました。 サービスメッシュ/Istio とは? サービスメッシュとは? サービスメッシュを導入した場合の構成は、サービス本体を「Microservice」、プロキシが「Sidecar」という表現で表されます。 各サービス着の通信、各サービス発の通信はすべて Sidecar として稼働するプロキシを通過する構成となります。 そのため、各サービスに到達する前、各サービスから発信するときに行いたい処理を共通化した設定として記載し、サービスメッシュで管理できます。 共通化する設定としては、代表的に以下のような機能が挙げられます。 サービス間の通信を透過的に暗号化する(mTLS) rate-limit カナリアリリース サーキットブレイキング Observability/Monitoring Istio とは? Kubernetes 上でサービスメッシュを実現するための 1 実装 https://istio.io Sidecar proxy として Envoy を採用 今回認可を共通化するために利用した External Authorization Filter(ext_authz) も Envoy の一機能 メッシュ内の設定は全て Kubernetes 上のリソースとして管理される 他のリソースと同様に YAML として管理可能 既に My.JPNAP では Git リポジトリでリソースを管理していたため都合がよい 課題まとめのうち解消した項目 課題まとめで掲出した 1. ~ 3. に対して、以下のような答えが出ました。 認可ロジックだけをどうやって抜き出してサービス前段で実行するのか 利用者からのリクエストは HTTP なので、前段にリバースプロキシを入れる Istio なら各 Pod に sidecar として Envoy が入ってくれる Envoy だったら External Authorization Filter を差し込んで実行できそう プロキシにどの実装を使うのか Istio は Envoy 一択 プロキシの設定をどうやって管理/反映するのか Istio の Custom Resources で、他の Kubernetes リソースとともに yaml で管理できる 検討結果の構成イメージ 技術検討(認可手法編) 続いて、課題まとめ 4./5. で挙げた項目について考えていきます。 認可ロジックを何で実装するか 認可情報をサービスへどうやって渡すのか 前提条件の確認 構成は決まったが、実際の認可ロジックはどうやって実装する? Istio(Envoy) に対象となるサービスにリクエストを送る前に、外部のサービスに問い合わせて許可/拒否するモジュールはあった(External Authorization Filter) 認可ロジックを実装する外部のサービス部分をどうするか考える必要がある 認可ロジック実装で、考える必要があった事柄 クライアントが送ってきた token の検証方法 認可ロジックの実装方法、利用実装 リクエストしてきた人のログイン情報をサービスに渡す方法 自前実装は増やしたくない token 検証をどうするか? クライアントが送ってきた access token の検証方法 ORY Oathkeeper を利用することに 検証には OAuth2.0 Token Introspection(RFC7662) を使う Hydra/Oathkeeper は実装を持っている Oathkeeper とは? Hydra と同じく ORY 社が開発している Identity and Access Proxy https://www.ory.sh/oathkeeper/ Reverse-proxy モード以外にも、HTTP リクエストを通す/通さないの判定をしてくれる API が備わっている authenticator(認証)、authorizer(認可)など、フェーズごとに設定できる URL ごとにどのような認証認可を行うかを JSON or YAML で書いておく Envoy との連携もサポート Oathkeeper 設定サンプル - id: request-authorization-to-status-api version: v0.38.3-beta.1 match: url: ://status.example.com/api/v1/ methods: - GET - POST - PATCH - PUT - DELETE authenticators: - handler: oauth2_introspection authorizer: handler: allow mutators: - handler: id_token errors: - handler: json 別途エンドポイントの設定は必要ですが、上記の設定だけで、特定の URL/HTTP method で来たリクエストについて、HTTP リクエスト内に入っている access token の検証が可能です。 認可ロジックをどうするか? 認証ロジックの実装方法、利用実装 Oathkeeper にも authorizer の設定はあるが、外部サービスに検証を依頼するのが主で Oathkeeper それ自体ではロジックの定義は不可能 OPA を利用することに OPA とは? Policy-based control for cloud native environments https://www.openpolicyagent.org/ rego という軽量言語にロジックを記載していく JSON を入力値として、rule という単位で条件に当てはまる/当てはまらないを書いていく(Policy as Code) Istio との連携もサポート https://github.com/open-policy-agent/opa-envoy-plugin/tree/main/examples/istio OPA 設定サンプル # envoyから渡されるhttp_requestそのままではほしいデータのキーのが深すぎるため # ショートネームでアクセス出来るようにしている import input.attributes.request.http as http_request # ルールのデフォルト値 default allow = false (snip) # GET /api/v4/user_groups/:user_groups_id/user_groups/:id allow { some user_group_id, ug_id http_request.method == "GET" input.parsed_path = ["api", "v4", "user_groups", user_group_id, "user_groups", ug_id] is_token_valid ABILITY_MANAGE_USER_GROUP == nautilus_authz_data(user_group_id, MODEL_UG, ug_id)["abilities"][_] } 上記例では、/api/v4/user_groups/:user_groups_id/user_groups/:id という URL に対する GET リクエストについて、token が正しいこと、必要な権限が存在することを確認できた場合、リクエストを許可するルールになります。 変数 input に HTTP リクエストに関する情報が入ってくるので、それを基にメソッド、URL 等の情報から条件を書いていくことが可能です。 ログイン情報の受け渡しをどうするか? リクエストしてきた人のログイン情報をOPA/サービスに渡す方法 Oathkeeper の mutator(変換機能) を使い、JWT ID Token を生成し渡すことに サービスは JWT の中身からリクエストしてきた利用者の情報を拾える また JWT には署名機能もついているため、検証を行うことで無効な JWT を拒否することもできる OPA は JWT のデコード/検証に built-in で対応している サービス側でログイン情報を拾う場合は、JWT のデコード/検証ロジックの実装だけ どんな言語でも非常に薄い実装になる! 認可はサービスに到達する前に実施済みなので、考慮不要! 課題まとめのうち解消した項目 課題まとめで掲出した 4./5. に対して、以下のような答えが出ました。 認可ロジックを何で実装するか Oathkeeper と OPA を組み合わせて利用する Oathkeeper では access token の検証、OPA で URL ごとに認可ルールを書いていく 認可情報をサービスへどうやって渡すのか Oathkeeper で JWT を発行して、認可情報を渡す形で実現した 完成構成 構成イメージ 導入前後の構成イメージを比較してみます。 構成イメージ/導入前構成イメージ/導入後 導入後のフロー詳細 点線が Envoy external authorization filter が行う通信を示しています。 まず、oathkeeper で access token の検証、JWT への変換を行い、OPA で各種認可処理が通ると、サービスにリクエストが到達するという構成になりました。 また、サービス側でログイン情報を取得する必要がある場合は、JWT をデコード/検証して情報を取り出せる状態になりました。 サービス側で追加実装した箇所 OPA 向けにユーザが持っている権限を返すエンドポイント(nautilus) OPA で認可チェックをする際に実行される リクエストされるパスごとに、必要となる権限が異なる OPA 側では、本エンドポイントを実行して、権限を取得 その後、必要な権限がある場合は許可、ない場合は拒否するようなロジックを書いている JWT を検証/JWT からリクエストユーザの情報を取り出す middleware 返すデータをサービス側で制御するケースで利用される ex.) 契約一覧などの index action など 苦労したポイント 上手く動かない時に、見ないといけないログが多かった いろいろなソフトを一度に増やしたため、1 つの HTTP リクエストの認可の結果を見るにも、それぞれのソフトのログを一度に見る必要があった (tmux でペインを開いて複数のコンテナを stern しておく…) tmux 実行イメージ Envoy ext_authz の設定を入れる Istio の設定を編み出すのが大変だった ext_authz が Istio native でサポートされていないため、EnvoyFilter というリソースで設定を書かないといけなかった EnvoyFilter は Istio が生成してくれる Envoy コンフィグ中の「どの箇所に」設定を追加する、という書き方をする必要がある 希望の場所にコンフィグを持ってくるための書き方を編み出すのに時間がかかった 文字通りの挙動をしてくれなかったり… ex.) INSERT_FIRST が動かなかった 苦労した設定サンプル configPatches: - applyTo: HTTP_ROUTE match: &config_patches_match context: SIDECAR_INBOUND routeConfiguration: name: inbound|3050|| patch: # どうやっても、INSERT_BEFORE/INSERT_FIRST で default route の前に値を入れられないので、 # 仕方なく、まず MERGE で default を書き換える operation: MERGE value: decorator: &decorator operation: gargant-app.gargant.svc.cluster.local:3050/* match: safe_regex: google_re2: {} regex: (/api/v[0-9]+)?/healthz name: gargant_healthz route: &route cluster: inbound|3050|| maxGrpcTimeout: 0s timeout: 0s typed_per_filter_config: &disable_ext_authz envoy.filters.http.ext_authz: '@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute disabled: true まとめ Istio を導入し、OPA/Oathkeeper をさらに組み合わせることで、認可をサービスの外に出し、共通化できるようになった 自前実装を増やすことなく、ほとんどの箇所を設定で管理できるようにできた どのようなサービスに対しても、OIDC 基盤と nautilus 上にあるユーザ・権限情報を利用して、認証認可を追加できるようになった 新たな認可方式を実装する場合も、OPA/Oathkeeper で実現できれば、サービスにタッチせずに実装可能になった ex.) API token 機能(実装中) ブラウザによる認証不要で API を実行できる期限がない token hydra には期限なしの token を発行できる機構はないため、自前実装 oathkeeper で token の種類の差異を吸収し、OPA/サービス側には影響を与えない設計で実装可能となった 今回紹介した構成をおすすめする対象者 Kubernetes クラスタ上で複数のバックエンドを立ち上げて運用している人におすすめ 構成要素、利用技術はたしかに多い しかし、一度うまく動けば、その後の転用・拡張は比較的容易 構成要素の増加により、拡張ポイントが増えるため 認証認可に関する自前実装も増えない サービスメッシュにより、サービスにリクエストが到達する前に任意の処理を挟めるようになる 付録 運用上ツラいポイント Istio のバージョンアップが早い 3 か月に 1 度メジャーバージョンアップする サポート期限が半年 とはいえ、GKE も自動的にバージョンアップしていき、それに対応した Istio も上げざるを得ないので、ずっと同じバージョンを使い続けるという保守的な使い方はできない 継続的にアップデートできるような枠組みを考える必要がある in-place update は 1.8 からサポートされていて、無停止でのアップデート実績あり 学習コストが高い Istio 1つでも膨大なリソース/ドキュメント量 それに OPA や oathkeeper をさらに乗せたため、初学者は覚えることがどうしても多くなってしまう oauth2-proxy の裏側 実は EnvoyFilter でヘッダーの入れ替えを行っている oauth2-proxy に OP(OpenID Provider) から取得した access token を upstream に渡してくれるオプションが存在する https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/overview/#command-line-options 内の --pass-access-token オプション それが Authorization ヘッダー以外につく(X-Forwarded-Access-Token ヘッダー) そのため、EnvoyFilter で Authorization ヘッダーに入れなおして、oathkeeper にリクエストするように設定している configPatches: - applyTo: HTTP_FILTER match: &match context: SIDECAR_INBOUND listener: filterChain: filter: name: envoy.http_connection_manager subFilter: name: envoy.router portNumber: 8080 patch: operation: INSERT_BEFORE value: name: envoy.filters.http.ext_authz typed_config: '@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz failure_mode_allow: false http_service: authorization_request: headers_to_add: - key: Authorization value: Bearer %REQ(X-FORWARDED-ACCESS-TOKEN)% authorization_response: allowed_upstream_headers: patterns: - exact: authorization path_prefix: /decisions server_uri: cluster: outbound|4456||oathkeeper-api.oathkeeper.svc.cluster.local timeout: 2s uri: http://oathkeeper-api.oathkeeper.svc.cluster.local:4456 status_on_error: code: ServiceUnavailable 著者プロフィール 今間 俊介 株式会社WESEEK / バックエンドエンジニア 某 ISP に 2 年弱勤務した後、2013 年に WESEEK へ入社。 現在は、今回ご紹介するインターネットマルチフィード様開発案件などプロジェクト問わず、Rails/Kubernetes を中心としたインフラ/アプリの設計・構築・運用に携わる。 株式会社WESEEKについて 株式会社WESEEKは、システム開発のプロフェッショナル集団です。 【現在の主な事業】 通信大手企業の業務フロー自動化プロジェクト ソーシャルゲームの受託開発 自社発オープンソースプロダクト「GROWI」「GROWI.cloud」の開発 GROWI GROWIは、Markdown記法でページを記述できるオープンソースのWikiシステムです。 GROWI.cloud GROWI.cloudはOSSのGROWIを専門的知識がなくても簡単に運用・管理できる、法人・個人向けの商用サービスです。 大手SIer・ISPや中小企業、大学の研究室など様々な場所でご利用いただいております。 【主な特徴】 テキストも図表もどんどん書ける、強力な編集機能 チーム拡大に迅速に対応できる管理者向け機能を提供 充実した機能・サポートでエンタープライズにも対応 【導入事例記事】 インターネットマルチフィード株式会社様 [https://growi.cloud/interviews/mfeed/?utm_source=connpass-top&utm_medium=web-site&utm_campaign=mf:embed:cite] 株式会社HIKKY(VR法人HIKKY)様 [https://growi.cloud/interviews/hikky:embed:cite] WESEEK Tech Conference WESEEK Tech Conferenceは、株式会社WESEEKが主催するエンジニア向けの勉強会。WESEEKに所属するエンジニアが様々なテーマで発表を行う予定です。 次回のWESEEK Tech Conferenceのテーマは「Rails+RSpecで気軽に始めるテスト」! 9/30(木)の19:00~20:00開催予定です。 ついつい書くのが億劫になってしまうテストコード。気軽にテストを書いてプロダクトの品質を高める、WESEEK流の実践方法を紹介します。 また、開発と書いたテストコードの実行をどのように行っているのか、CIについても簡単にお話します。 現在、connpassやTECH PLAYで参加受付中です。皆様のご参加をお待ちしております!https://weseek.connpass.com/event/224673/https://techplay.jp/event/830907 一緒に働く仲間を募集しています 東京の高田馬場オフィス、大分にある別府サテライトオフィスにてエンジニアを募集しております。中途採用だけではなく、インターンシップも積極的に受け入れています! 詳しい募集要項は、弊社HPの採用ページからご確認ください。
アバター