こんにちは。 Infrastructure (インフラチーム) 所属の小門です。 RevComm (レブコム) では、電話営業や顧客対応を可視化する音声解析 AI 搭載型のクラウド IP 電話 MiiTel (ミーテル) を提供しています。 miitel.com はじめに MiiTel を含め RevComm ではクラウドプラットフォームに AWS を利用しています。 環境や用途に応じて複数の AWS アカウントを保有しています。 各 AWS アカウントの特権ID (※) を使用できるのは、業務上必要とするごく少数のメンバーのみに制限しており、必要が生じたメンバーには一時的に権限付与する運用をしています。 ※特権ID…ルートユーザーや AdministratorAccess IAMポリシーがアタッチされたユーザーID (= Admin ロール) 特権IDをやみくもに使用することはセキュリティガバナンス面でリスクが高いため、原則必要十分な権限を持つユーザーIDで操作するルールとしています。 例えば、環境の確認をする場合には ReadOnlyAccess、リリース作業をする場合には PowerUserAccess を使用するというイメージです。 今回は、やみくもな特権IDの使用に対するけん制やアカウント流出のリスクに備えるために特権IDの使用を検知する仕組みを構築しました。 また特権ID使用時には用途や作業内容を記載する運用ルールとし、監査証跡として残せるようにしています。 全体構成 ステップ1. ログイン検知 (メール通知) ルートユーザー Admin ロール 通知結果 ステップ2. 個別メンション付きリプライ Slack App 作成 メンション用のユーザーID取得 ボットのコア処理 デプロイ 動作確認 まとめ 全体構成 ステップ1. ログイン検知 (メール通知) ステップ2. 個別メンション付きリプライ 大きく2段階に構成になっています。 ステップ1. ログイン検知 (メール通知) ルートユーザー AWS ルートユーザーがログインしたことを通知するための CloudFormation (以下、CFn) テンプレートが公開されており、これを参考にしました。 CloudTrail を介してルートユーザーのログインを検知し EventBridge、SNS 経由でメールが送信されます。 参考: ルートユーザーアカウントが使用されたことを通知する EventBridge イベントルールを作成する ルートユーザーがログインした CloudTrail イベントレコードは下記のようになり、これが上記 CFn テンプレートの EventPattern に対応しています。また IAM はグローバルリージョンサービスのため CloudTrail イベントデータが us-east-1 リージョンに集約される点に注意します。 CloudTrail イベントレコード (抜粋) { " eventVersion ": " 1.08 ", " userIdentity ": { " type ": " Root ", ... } , " awsRegion ": " us-east-1 ", " eventType ": " AwsConsoleSignIn ", ... } CFn テンプレート (抜粋) Resources : ... EventsRule : Type : AWS::Events::Rule Properties : Description : Events rule for monitoring root AWS Console Sign In activity EventPattern : detail-type : - AWS Console Sign In via CloudTrail detail : userIdentity : type : - Root ... Admin ロール 上記を参考にして特定の IAM ロールが使用されたことを検知するイベントパターンも作成しました。 RevComm では、Google Workspace を用いた AWS SSO によって開発者が IAM ロールを使用するルールになっています。 Google アカウント認証後に IAM ロールを利用すると、裏側で AssumeRoleWithSAML というアクションが実行され CloudTrail で検知できます。 コンソールログイン AWS CLI などによる認証情報取得 ( aws sts assume-role-with-saml ... ) の両操作をカバーできます。 CloudTrail イベントレコード(抜粋) { " eventVersion ": " 1.08 ", " userIdentity ": { " type ": " SAMLUser ", " userName ": " xxx@revcomm.co.jp ", ... } , " eventSource ": " sts.amazonaws.com ", " eventName ": " AssumeRoleWithSAML ", " awsRegion ": " us-east-1 ", " requestParameters ": { ... " roleArn ": " arn:aws:iam::012345678910:role/AdministratorRole ", ... } , ... } CFn テンプレート(抜粋) Mappings : AccountAdminRoleNameMapping : AdminRoleArn : # account-1 "012345678910" : arn:aws:iam::012345678910:role/AdministratorRole # ... Resources : ... AssumeRoleWithSAMLEventsRule : Type : AWS::Events::Rule Properties : EventPattern : detail-type : - "AWS API Call via CloudTrail" detail : eventSource : - "sts.amazonaws.com" eventName : - AssumeRoleWithSAML requestParameters : roleArn : - !FindInMap [ AccountAdminRoleNameMapping, AdminRoleArn, !Ref "AWS::AccountId" ] ... 通知結果 正しく設定できると、下記のようにメール送信することが確認できます (Slack) 。 また、EventBridge の「入力トランスフォーマー」を使用して各種情報を抜粋しています。 ルートユーザーのログイン通知 Admin ロールのログイン通知 ステップ2. 個別メンション付きリプライ ログイン検知から通知までの仕組みができましたが、一歩踏み込んで個別メンションするようにしました。 ステップ1 で Slack に送信されたメールをトリガーにした Slack App (ボット) を作成し、AWS Lambda で動作させています。 ボットの実装には Slack が公式に公開しているフレームワークである Bolt for Python を使用しました。 slackapi/bolt-python: A framework to build Slack apps using Python ドキュメント: Slack | Bolt for Python Lambda ランタイムは Python 3.9、ライブラリは下記のバージョンを使用しています。 slack-bolt==1.11.2 slack-sdk==3.13.0 Slack App 作成 Slack App コンソール から「Create New App」を選び、新規アプリを作成します。 Slack App がメッセージの受信をハンドルできるよう「Event Subscriptions」をオプトインします。※Slack App 自体の詳細な手順は本記事では割愛させて頂きます。 Lambda 関数で利用するトークン情報の取得方法のみ記載します。 SLACK_BOT_TOKEN OAuth & Permissions > OAuth Tokens for Your Workspace > Bot User OAuth Token SLACK_BOT_SIGNING_SECRET Settings > App Credentials > Signing Secret メンション用のユーザーID取得 ボットから送信するメッセージでメンションを付けるには単に @xxx とするのではなく、 <@[user-id]> とする必要があります。 [user-id] は Slack 内部で管理されている固有値です( "W012A3CDE" のような形式)。 個別メンションのためにユーザーIDをメールアドレスから取得する必要があります。 from typing import Optional from slack_sdk import WebClient SLACK_BOT_TOKEN = 'xxx' def get_user_id_by_email_address (email_address: str ) -> Optional[ str ]: client = WebClient(token=SLACK_BOT_TOKEN) response = client.users_lookupByEmail(email=email_address) if not response.data.get[ "ok" ]: return if response.data.get( "error" ) == "users_not_found" : return return response.data[ "user" ][ "id" ] 参考: users.lookupByEmail method | Slack ボットのコア処理 Bolt App を初期化すると @app.event のようにデコレーターで Slack 上のイベントをリッスンして処理を実装できます。 またエントリーポイントとなる lambda_handler もフレームワークのメソッドが用意されているため簡潔に実装することができます。 import json import re from typing import Union from slack_bolt import App from slack_bolt.adapter.aws_lambda import SlackRequestHandler from slack_sdk import WebClient SLACK_BOT_TOKEN = 'xxx' SLACK_BOT_SIGNING_SECRET = 'xxx' app = App( process_before_response= True , token=SLACK_BOT_TOKEN, signing_secret=SLACK_BOT_SIGNING_SECRET ) @ app.event ({ "type" : "message" , "subtype" : "file_share" , }) def reply_to_sns_email (body, logger): """Amazon SNS からのメール通知に対してメンション付きスレッド返信を行う""" attachment = body[ "event" ][ "files" ][ 0 ] # メール以外のイベントはスキップする if attachment[ "filetype" ] != "email" : return { 'statusCode' : '200' , 'body' : json.dumps({ 'message' : 'This message is not email.' }) } # ... email_body = attachment[ "plain_text" ] # " -- " 以降の改行を含む全ての文字を除去する login_info_text = re.sub( "--.*" , "" , email_body, flags=re.DOTALL).replace( " \r\n " , "" ) login_info = json.loads(login_info_text) arn = login_info[ "RoleArn" ] if arn.endswith( "root" ): """root ユーザーが使用された場合 e.g. arn:aws:iam::012345678910:root """ user_id = None message_abstract = "rootユーザーによるコンソールログインが行われました。" else : """Assumed Role Arn からログイン者のメールアドレスを取得する""" email_address = login_info[ "UserName" ] message_abstract = "権限の強いアカウントが使用されました。" # メールアドレスから Slack ユーザID を紐づける user_id = get_user_id_by_email_address(email_address) if user_id is None : mention = "<!here>" else : mention = f "<@{user_id}>" message_text = \ mention + " \n " + message_abstract + " \n " \ "- 作業目的や内容 \n " \ "- タスクチケットURL \n " \ "などを記入してください。" client.chat_postMessage( channel=body[ "event" ][ "channel" ], thread_ts=body[ "event" ][ "event_ts" ], text=message_text ) return { 'statusCode' : '200' , 'body' : json.dumps({ 'message' : 'ok' }) } def lambda_handler (event, context): slack_handler = SlackRequestHandler(app=app) return slack_handler.handle(event, context) デプロイ 上記の Lambda 関数をデプロイし、API Gateway の配下に配置します。 API Gateway は「REST API」ではなく「HTTP API」で作成する必要があるので注意します。 デプロイ後、API Gateway の URL を Slack App のリクエスト URL に登録します。 ※Features > Event Subscriptions > Request URL 動作確認 ここまでの手順が正しく設定できていれば、ルートユーザー/Admin ロールのログインを契機に一連処理を通してボットによる個別メンション付きリプライが届きます。 まとめ 特権IDの使用を通知し、セキュリティガバナンスを改善する取り組みについてご紹介しました。 本記事の取り組みでは下記のメリットを実現しました。 特権IDがやみくもに使用されることをけん制しつつ、使用状況をモニタリングする 特権IDを用いる作業内容の監査証跡を残す RevComm ではお客様に安心して製品をご利用頂けるよう、引き続きセキュリティの向上に力を入れてまいります。 Infrastructure では、プロダクト横断で考慮するセキュリティの管理や IaC 、また一部オンプレ環境の管理・運用を行っています。ご尽力頂けるエンジニアを探しています。 インフラ以外にも全方位でエンジニアを積極的に採用中ですのでぜひ採用サイトをご覧ください! www.revcomm.co.jp
こんにちは、RevComm の玉城です。MiiTel Analytics のサーバーサイドの開発を主に担当しています。MiiTel Analytics は通話や会議の履歴・音声解析結果を集約し可視化する製品です。 MiiTel Analytics では以下の2つを提供するために Amazon OpenSearch を運用しています。 全文検索機能 使用単語頻度レポート この Amazon OpenSearch のシャード数を切り替えるタイミングがありそちらを私が担当しましたので、その時の内容を紹介したいと思います。 シャード数切り替えに至った経緯 リインデックスとは リインデックスの高速化 MiiTel における OpenSearch へのデータ登録の仕組み まとめ シャード数切り替えに至った経緯 OpenSearch には MiiTel Analytics で解析された応対履歴が保存されます。そのため、MiiTel の利用ユーザーが増えると保存されるデータも増大し、書き込み/検索に掛かる負荷が大きくなっていきます。参考として、記事執筆時点では 1 億近いドキュメント数と 300 GB を超えるデータサイズとなっています。 このようにデータ数増大による負荷増大への対応として、OpenSearch にはインデックス毎にシャード数を設定します。シャードとは、OpenSearch でクラスターにデータを分散するときの単位です。 切り替え前のシャード数は 2 で各シャード数の容量は約 65 GB でした。AWS 側では適切なシャードのサイズは 10 〜 50 GiB にすることを推奨しており、既に推奨値を超えている状態でしたので早めの対応が必要でした。 そのため、今回の対応ではシャード数を 2 -> 4 に増やしてデータを分散させることで各シャード毎のサイズを 10 〜 50 GiB 以内に収めました。 参考: Amazon OpenSearch Service ドメインのサイジング - Amazon OpenSearch Service リインデックスとは OpenSearch のシャード数設定はインデックス作成の際に行われます。既存のインデックスのシャード数を途中で変更することはできません。そのため、今回のシャード数切り替えのためにリインデックスを行いました。 リインデックスとは既存インデックスとは別に新しくインデックスを作成し、既存インデックスのデータをまるまる新しいインデックスにコピーすることです。この新しくインデックスを作成する際に、シャード数を 4 に設定することでシャード数切り替えの対応を行います。 新インデックスを作成します。その際に既存インデックスから更新したい設定値を新たに設定します。今回の場合はシャード数です。 既存インデックスから新インデックスに向けて Reindex API を実行します。 既存インデックスから新インデックスへエイリアスを変更し既存インデックスを削除します。 リインデックスの高速化 リインデックスを行う上で注意したい設定があります。それは refresh_interval です。refresh_interval はインデックスのドキュメント更新を検索に反映する操作の実行間隔になります。refresh_interval が 10 秒だった場合、下記の図のようにインターバルの間に起こった変更は次のインターバルにて検索ができるようになります。 refresh_interval の設定を無効にすることでリインデックスの実行時間を早く終わらせることができます。詳細は後述しますが、今回のリインデックス作業中はデータの更新を一時的に止めることで、新しくデータが投入されない状況にすることができました。そのため、refresh_interval の設定を無効にすることができました。 例として約 110 万のドキュメント数に対して refresh_interval を -1(無効)と 100 秒で比較検証したところ、-1 の場合は 5 分 50 秒だったのに対して 100 秒の場合は 11 分 19 秒と倍近くの差が出ました。 MiiTel における OpenSearch へのデータ登録の仕組み MiiTel Analytics で解析された応対履歴は Amazon RDS へ保存されます。OpenSearch へはこの Amazon RDS へデータが保存されたタイミングでデータが同期されます。 応対履歴を RDS へ保存/更新 保存/更新された応対履歴 ID を SQS に送信 Lambda が応対履歴 ID を SQS より取得 Lambda にて応対履歴を OpenSearch へ同期する API を呼び出す 同期 API のリクエストの応対履歴 ID を元に RDS より応対履歴を取得 取得した応対履歴を OpenSearch 用に変換し登録 今回のリインデックス作業では上図の定期実行される Lambda を一時的に止めます。それによりリインデックス中は上図の 3 〜 6 が止まり、OpenSearch へのデータ追加/更新が行われないのでデータの不整合を防ぐことができます。 また、上図の 1 〜 2 はリインデックス中も稼働しているので SQS には新規または更新された応対履歴の ID が溜まっていきます。これらの応対履歴はリインデックスが完了した後に Lambda を再稼働することで OpenSearch へ登録されますので、リインデックス中の応対履歴も取り零すことなくシャード数の切り替えが行えます。デメリットは一点で、データ同期を一時的に止めるのでリインデックス中は最新のデータを扱えなくなります。 *1 上記で説明してきたように、今回のリインデックスはサービスのダウンタイムなしで行えます。しかしながら最新のデータを扱えないままサービスを提供するのはあまり宜しくないため、顧客に事前にメンテナンス時間を通知し、その時間内にシャード数の切り替えを行いました。 データ数が多いのでリインデックスが完了するまでに約 8 時間も掛かりました。お客様にご協力いただいたおかげで refresh_interval を無効にすることができたのでこの時間で収まったことになります。もしも refresh_interval を 100 秒に設定していたら倍の 16 時間は掛かっていたかと思うと恐ろしいです。 まとめ 今回のシステム要件とデータ量の場合、ダウンタイムなしの約 8 時間でリインデックスが完了しました。しかし、近い将来にまた負荷を緩和するためのシャード数の切り替えを行わなければなりません。なぜなら、このリインデックスを行った当時のドキュメント数は約 6,600 万で現在は約 9,300 万弱とありがたいことに利用者は順調に増えているからです。 今回の対応が次回も丸々使えるかと言うと規模が増大しているため使えないと思います。しかし、今回の対応でシャード数切り替えやリインデックスに対する知見は得られたので、今後の Amazon OpenSearch の運用をより良くしていくために模索していきたいと思います。 リインデックスは、リインデックス中のデータ登録/更新/検索が行なえます。しかし、リインデックス中のコピー元のデータ登録/更新はコピー先へ反映されないため、コピー元とコピー先のインデックス内容に差異が生じます。 *1 : リインデックスは、リインデックス中のデータ登録/更新/検索が行なえます。しかし、リインデックス中のコピー元のデータ登録/更新はコピー先へ反映されないため、コピー元とコピー先のインデックス内容に差異が生じます。
はじめまして。 株式会社 RevComm でバックエンドエンジニアをしている近藤です。主に MiiTel Analytics や外部連携の開発に携わっています。 MiiTel Analytics とは、電話やビデオ会議のデータ可視化や MiiTel を構成するマイクロサービスに API を提供するプロダクトです。 執筆時点で 20 名弱ほどのエンジニアメンバーで構成されている、RevComm 内では比較的大きなチームです。 CI / CD ツールとしては GitHub Actions と AWS CodeDeploy を利用しています。 またこれらのツールを ビルド・テスト・リリースフェーズを自動化 ルールの普及 の2つの目的で活用しています。 ここでいうルールの普及とは、開発におけるツールの運用や気をつけるべきことを開発メンバー全体に周知・徹底することです。 前述の通り MiiTel Analytics チームは比較的大きく、さらに拡大し続けています。 このような状況で、例えば GitHub Issue 管理方法、pull request の出し方などのルールを普及させるのは難しいです。そこで、CI / CD ツールを利用してこの課題を解決しようとしています。 この記事では GitHub Issue 管理 GitHub pull request 管理 開発知識の共有 という 3 つの具体的なケースについて、GitHub Actions の定義ファイルも合わせて紹介します。 GitHub Issue 管理 Analytics チームでは GitHub projects を利用しています。いわゆるカンバンというものです。 大きく Todo, In Progress, Done というカテゴリがあり、Todo に関してはさらにプロジェクトごとに分割されています。 「Issue を作ったら対応する Todo に入れてください」とアナウンスしても、なかなか徹底されないのは想像できますね。そこで以下のような GitHub Actions を用意しています。 概要 Issue を新規作成したらラベルを見て Todo に追加する ※ ラベルは 手動 もしくは、 Issue テンプレート機能 のメタ情報にて追加できます 利用 Actions GitHub Project Automation+ https://github.com/marketplace/actions/github-project-automation コード name : Manage Issues for Project on : issues : types : [ labeled ] jobs : automate-project-columns : runs-on : ubuntu-latest steps : - name : to feature1 # NOTE : もしラベルが 'feature1' だったらプロジェクトの Todo - feature1 カラムに追加 if : github.event.label.name == 'feature1' uses : alex-page/github-project-automation-plus@v0.8.1 with : project : miitel-analytics column : Todo - feature1 # NOTE : personal access token repo-token : ${{ secrets.GITHUB_TOKEN }} - name : to feature2 # NOTE : もしラベルが feature2' だったらプロジェクトの Todo - feature2 カラムに追加 if : github.event.label.name == 'feature2' uses : alex-page/github-project-automation-plus@v0.8.1 with : project : miitel-analytics column : Todo - feature2 # NOTE : personal access token repo-token : ${{ secrets.GITHUB_TOKEN }} GitHub pull request 管理 次に、コードを書き終えて pull request を作成した時は 自分を Assignee にする プロジェクトの In Progress に pull request を追加する というのがルールになっています。 こちらも pull request を出せた達成感から忘れてしまいそうなルールです。そこで、以下のような GitHub Actions を用意しています。 概要 pull request を作成した人を Assignee として追加する pull request を Project の InProgress (PR) カラム に追加する 利用 Actions Auto Assign Action https://github.com/marketplace/actions/auto-assign-action GitHub Project Automation+ https://github.com/marketplace/actions/github-project-automation コード pull request を作成した人を Assignee として追加する name : Auto Author Assign on : pull_request : branches : - main types : - opened - ready_for_review jobs : auto-author-assign : runs-on : ubuntu-latest steps : - name : add reviewers uses : kentaro-m/auto-assign-action@v1.1.2 with : # NOTE : Auto Assign Action 専用の config ファイルを利用 # refs: https://github.com/marketplace/actions/auto-assign-action#single-reviewers-list configuration-path : '.github/config_auto_assign.yml' # NOTE : personal access token repo-token : ${{ secrets.GITHUB_TOKEN }} pull request を Project の InProgress (PR) カラムに追加する name : Pull Request Opened on : pull_request : branches : - main types : - opened - reopened jobs : automate-project-columns : runs-on : ubuntu-latest steps : - name : to miitel-analytics uses : alex-page/github-project-automation-plus@v0.7.1 with : project : miitel-analytics column : InProgress (PR) # NOTE : personal access token repo-token : ${{ secrets.GITHUB_TOKEN }} 開発知識の共有 pull request を出した後には、コードレビューがあります。 そこで、開発知識の共有ができる GitHub Actions を用意しています。 MiiTel Analytics はマイクロサービス化されています。しかし、別のプロジェクトに依存してしまっている箇所があり、あるファイルを変更した際には別のプロジェクトのコードも修正する必要があります。 こうしたチームの中だけの暗黙の知識を、漏れなく共有することができます。 概要 file A に変更があった際に、「project B の target.txt も書き換えてください」というコメントを追加する 利用 Actions Create or Update Comment https://github.com/marketplace/actions/create-or-update-comment コード name : Detect File Changes on : pull_request : branches : [ main ] paths : - '**/apps/specific/file/fileA.py' jobs : comment : runs-on : ubuntu-latest steps : - name : Create comments id : create-comments run : | echo "この Pull Request は fileA.py の変更を含んでいます。" >> comments echo "project B の target.txt も書き換えてください" >> comments comments=$(cat comments) comments="${comments//$'\n'/%0A}" echo "::set-output name=values::$comments" - name : Post multi-line comments uses : peter-evans/create-or-update-comment@v1 with : token : ${{ secrets.GITHUB_TOKEN }} issue-number : ${{ github.event.pull_request.number }} body : ${{ steps.create-comments.outputs.values }} まとめ いかがでしたでしょうか。 Analytics チームでの、「複雑な作業の自動化」「ルールの普及」を目的とした CI / CD ツール 活用例について紹介しました。 特に「ルールの普及」については GitHub Actions を利用することで、簡単に自動化することができます。開発体験の向上だけでなく、コミュニケーションコストの削減に役立ちます。 記事中のコード例などが、みなさんのチームの課題解決のアイディアの元となれば幸いです。 一緒に働きませんか? RevComm では、「少ない人数で最大の開発効率を」という意識のもと CI / CD ツールの活用が行われています。 アプリケーションの開発に加えてチームでの開発効率を最大化させることに興味のある方、この記事で RevComm の開発に興味を持ってくださった方、ぜひ採用ページをご覧ください! www.revcomm.co.jp
こんにちは、RevComm の小島です。MiiTel Analytics のサーバーサイドの開発を主に担当しています。MiiTel Analytics は通話や会議の履歴・音声解析結果を集約し可視化する製品です。 MiiTel Analytics の画面例。録音再生、音声解析結果の閲覧などの機能があります。 MiiTel Analytics のサーバーサイドは Python で実装されており、Web フレームワークとして Django を利用しています。今回の記事では Django の ORM を利用している際のアンチパターンである first メソッドの乱用について解説します。 first メソッドの一般的なユースケース アンチパターンの場合 想定されるトラブル例 アンチパターンが発生する理由 解決案 まとめ first メソッドの一般的なユースケース まずは first メソッドの通常のユースケースを確認しましょう。first は Django の QuerySet の先頭のオブジェクトを返すメソッドです。例えば # Post は記事を表すモデル Post.objects.order_by( 'published_at' ).first() などとすれば公開日の一番新しい記事を取得することができます。 つまり複数の候補のうち最新のものや最も古いものなど、順番に意味がある時にこのメソッドは使われます。 アンチパターンの場合 一方、このメソッドを、順番に意味がない時であっても乱用しているコードを目にすることがあります。例えば、次のようなコードです。 # メールアドレスで User テーブルを検索 user = User.objects.filter(email=email).first() if not user: raise NotFound() # 以下 user を使った処理... 上記のコードはメールアドレスでユーザーを絞り込んで first メソッドを用いて先頭のモデルを取り出す、もしユーザーがなかったら例外を返すという挙動をするコード例です。こうした使い方は複数のプロジェクトで目にしたことがあり実際動くのですが、実はこのコードは潜在的な問題を抱えています。 通常、同じメールアドレスを持つユーザーが複数いることを許容しない場合が多いですし、 許容したとして同一メールアドレスのユーザーの取得順には意味がありません 。メールアドレスが同じユーザーが複数いたとしても、そのうちのどれでもいいからひとつを取得したいというケースは考えにくいです。 したがって、 User.objects.filter(email=email) というコードは 2 つ以上のオブジェクトを返すことを想定していないと考えられます。 ところが、上記の絞り込みを実行してもデータがひとつだけ返ってきたのか、複数返ってきたのかについては何の情報もありません。そのため、first メソッドでユーザーを取得すると、意図せず複数のユーザーオブジェクトがデータベースに存在していても、不整合に気づけないのです。 想定されるトラブル例 このようなコードがトラブルを引き起こし得る具体的な状況としては、初期の実装では一意だったものが、要件や仕様の変更で一意性が担保されなくなるケースです。 例えば論理削除をあとで導入する場合が該当します。削除済みレコードと削除済みでないレコードを区別する必要があり、多くのクエリ(filter メソッド)において削除フラグを検証することが必要になります。 1箇所誤って削除フラグを検証してないクエリが残ってしまったとします。この時、メールアドレスが同じで論理削除済みのユーザーと削除されていないユーザーのどちらもテーブルにある場合、コードで削除済みユーザーと削除されていないユーザーのどちらを取得できるかは DB の実装依存になります。場合によっては 本来は取得されるべきでないユーザーの情報を取得することになります 。 さらに怖いことに、first メソッドはエラーを出さないため、本来取得すべきユーザーとは異なるまま処理が継続されます。これは 不正な認証やデータの漏洩、意図しないデータの更新など大きなトラブル につながりかねません。 アンチパターンが発生する理由 このように、場合によっては大きなトラブルを引き起こしかねない first メソッドですが、なぜ様々な場面で使われてしまうのでしょうか。 本来であれば、ただひとつのオブジェクトを取得するメソッドには get というメソッドがあり、こちらを使うのが一般的です。 user = User.objects.get(email=email) ではなぜ get ではなく filter と first を使って書きたくなるのかと言うと、それは条件を満たすオブジェクトが存在しなかった時の挙動の違いにあります。 get は存在しなかった時に例外を送出するため、例外処理を書く必要があります。 try : user = User.objects.get(email=email) except User.DoesNotExist: user = None 一方で filter は、存在しなかった時は単に None を返します。 user = User.objects.filter(email=email).first() 比較すれば明らかですね。後者の方が 簡潔なコードのように見える のです。 どちらも条件に合うオブジェクトがただひとつ、もしくは存在しないのであれば同じ挙動をします。同じ挙動であれば簡潔なコードを書こうとする心理が働き、filter と first を使った書き方をする方が多いのだと思われます。 解決案 アプリケーションで対応できる最もシンプルな解決策は get メソッドを使うことです。 get が返しうる例外は 2 種類あり、ひとつは上記の例にも挙げた DoesNotExist で、もうひとつは MultipleObjectsReturned です。 MultipleObjectsReturned は 2 つ以上の条件を満たすオブジェクトが存在する場合に送出される例外です。よって、もし get を使えば意図せず 2 つ以上のデータがあった場合はシステムエラーとなり、データ不整合に気づくことができます。 しかし、単に get を使うようルール化するだけではアンチパターンを解消するには至りません。このアンチパターンが発生するのは fitler と first を使ったほうが簡潔に書けるという(一見もっともらしい)メリットがあるからです。であれば、単に get を使うというルール化をするだけでなく、コードの簡潔さを維持しつつ get を使えるようにしなければなりません。 その手段として、 MiiTel Analytics では Django の組み込みの QuerySet API をモデルの定義ファイル以外から直接呼ぶことを禁止し、次のようにモデルの定義ファイルに QuerySet に必要なメソッドを追加する形式をとっています。 # models.py from django.db import models class UserQuerySet (models.QuerySet): def get_by_email (self, email: str ) -> Optional[ "User" ]: try : return self.get(email=email) except User.DoesNotExist: return None class User (BaseModel): email = models.CharField(max_length= 256 ) # other fields... objects: UserQuerySet = UserQuerySet.as_manager() ※Django のユーザーモデル (auth_user テーブル) を使っている場合には上記のコードはそのままでは使えません。 このようにすれば、メールアドレスで User を取得するコードは user = User.objects.get_by_email(email=email) となり、filter と first を使うのと同じように簡潔に書くことができます。それに加えて、データ不整合時にエラーが出るため、問題の早期発見につながり、アプリケーションが不正な状態で動き続けることを避けることができます。 まとめ オブジェクトがただひとつ欲しい場合に filter と first を利用するのはデータ不整合を見逃す可能性がある filter と first が使われてしまう理由は簡潔に書けるから ただひとつのオブジェクトを取得するには原則として get を使おう QuerySet に必要なメソッドを定義すれば filter と first を使った方法と同等に簡潔に同じ処理を実装できる first はその名前の通り、あくまで QuerySet の先頭のオブジェクトをとるためのメソッドです。順番に重要性がない、もしくは複数のオブジェクトが返ってくる想定をしていない時の利用は想定されていません。 組み込みのシンプルなメソッドで簡潔に処理が書けるのはたしかに魅力的なことです。しかしこのケースのようにリスクがあるケースもあり、たとえ実装が煩雑になっても用途に合致したメソッドを採用することはとても重要だと思います。 アプリケーションのコードの品質を担保するために、こうした小さな努力の積み重ねを徹底することが非常に重要だと考えています。そして、こうした努力の結果として MiiTel というサービスの安定性や信頼の獲得に繋がるのだと信じています。
はじめに 株式会社 RevComm の Software Engineer 宇佐美です。 RevComm では、電話営業や顧客対応を可視化する音声解析 AI 搭載型のクラウド IP 電話 MiiTel (ミーテル) を提供しています。 miitel.com MiiTel の中核プロダクトである MiiTel Analytics は、フロントエンドが React ・バックエンドが Python (Django) という構成の Web アプリケーションです。メイン言語は日本語を想定していますが、ユーザーが設定言語を変更することで英語で利用することも可能です。 今回、従来はバックエンドで行っていたWebアプリケーションの 国際化対応 (internationalization, i18n) をフロントエンドに移行するという作業を行いました。 この過程でわかった国際化対応の方法や、国際化対応をバックエンドで行う場合とフロントエンドで行う場合それぞれのメリット・デメリットなどを紹介したいと思います。 はじめに 前提・本記事のスコープ 国際化対応とは? 国際化とローカル化 バックエンドでの i18n 対応 事前準備 翻訳文字列の指定 言語ファイルの作成 言語ファイルのコンパイル フロントエンドでの i18n 対応 react-i18next インストールから導入まで ディレクトリ構成 フロントエンドで国際化対応するメリット・デメリット メリット ユーザーに見える部分を柔軟にカスタマイズしやすい 言語ファイルのコンパイルが不要でデプロイが容易 言語ファイルをコンテキストごとに分けて管理しやすい デメリット サーバーサイドに閉じた文言は対応できない パフォーマンスが落ちることがある SEO に弱くなる可能性がある まとめ 補足 参考資料 前提・本記事のスコープ 国際化対応とは? 一口に国際化対応といっても、この言葉が指すスコープはコンテキストやビジネス要件によって大きく変わってきます。 一般的に Web アプリケーションの国際化対応といったときに含まれるものとしては、以下のようなものがあります。 多言語対応 (文字セット、文言、書式など) 日時情報の時差 (Time zones) 通貨情報 Django はこれらすべてを含む国際化対応をフルサポートしていますが、この記事のスコープとしては 多言語対応、それも日英の 2 言語のみの対応 が中心となります。 Webシステム・アプリケーションの国際化対応は非常に幅広く奥深い分野なので、すべてを完璧に対応することは事実上不可能ですが、英語だけでも対応しておくと国際展開の可能性が広がります。 国際化対応の深淵な世界に関心がある方は、以下の動画をご覧になるとその一端がわかるかと思います。 www.youtube.com 国際化とローカル化 国際化と類似の概念として、ローカル化 (localization, l10n) というものがあります。この2つはしばしば混同されがちなので、Django の公式ドキュメント内にある定義を添付します。 国際化 (internationalization) ソフトウェアをローカル化に備えさせることです。通常、開発者によって行われます。 ローカル化 (localization) 翻訳およびローカルな表示形式を記述することです。通常、翻訳者によって行われます。 https://docs.djangoproject.com/ja/3.2/topics/i18n/#definitions 実務ではローカル化も含めて国際化対応と呼ばれることがあったり、開発者がローカル化の作業 (翻訳テキストの用意) まで行うこともあるかと思います。 ただ、最低でもユーザーの目に見える部分に関するローカル化はネイティブレベルのチェックが必要になるでしょう。 バックエンドでの i18n 対応 ここからは実際の i18n 対応のやり方について紹介していきます。まずはバックエンド (Django) での対応です。 前述のとおり、MiiTel Analytics ではもともとバックエンドで国際化対応を行っていました。 Django は非常に多機能なフレームワークなので、国際化対応向けの機能ももちろん用意されています。 国際化とローカル化: https://docs.djangoproject.com/ja/3.2/topics/i18n/ 翻訳: https://docs.djangoproject.com/ja/3.2/topics/i18n/translation/ 事前準備 Django で多言語対応を行う場合に必要となる事前準備がいくつかあります。主に settings.py への値の設定です。 # デフォルトの言語コード LANGUAGE_CODE = ‘en’ # 翻訳用 Middleware を追加 MIDDLEWARE = [ ... 'django.middleware.locale.LocaleMiddleware' , ... ] # 言語ファイルのパス LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale' ), ) なお、LocaleMiddleware は SessionMiddleware の後かつ CommonMiddleware の前に置く必要があります。 https://docs.djangoproject.com/ja/3.2/topics/i18n/translation/#how-django-discovers-language-preference 翻訳文字列の指定 Django で多言語対応を行う場合、アプリケーション内で翻訳を行う対象となるテキストに対して 翻訳文字列 (translation string) を指定し、翻訳対象の文言であることをマークします。 Django はこの翻訳文字列を見て、後述する言語ファイルの内容に従って翻訳対象となる言語に翻訳します。翻訳文字列の指定を行うときは、django.utils.translation から gettext や gettext_lazy モジュールをインポートして使います。 from django.http import HttpResponse from django.utils.translation import gettext as _ def my_view (request): output = _( "TEXT_TO_BE_TRANSLATED" ) return HttpResponse(output) # 翻訳後の結果が HTTP レスポンスに渡される ここでは TEXT_TO_BE_TRANSLATED という文字列が翻訳文字列として指定されています。 テンプレートを使う場合はシンタックスがやや異なり、i18n をロードしてから trans タグを使って翻訳文字列を指定します。 {% load i18n %} <h1>{% trans "TEXT_TO_BE_TRANSLATED" %}</h1> # 翻訳後の結果が HTML ファイルで表示される デフォルト言語を設定しておけば、その言語をそのまま翻訳文字列として利用することも可能です。こうすると、ユーザーの利用言語がデフォルト言語を利用する場合は翻訳文字列がそのまま出力されます。 ただ、本来は翻訳文字列は Django に翻訳対象の文言であることを知らせるための ID としての役割を担っているので、文章よりはユニークな ID として成立する文字列が望ましいでしょう。 翻訳を利用する側の views や templates ですることは、基本的にはこれだけです。 言語ファイルの作成 翻訳文字列の指定ができたら、これに対応した言語ファイル (language file) を作成します。 言語ファイルは、翻訳文字列を実際に各言語にどのように翻訳するかを指示するためのファイルです。 言語ファイルという形式自体は Django や Python に固有のものではなく、UNIX-like な OS で使用されている翻訳システム gettext の一実装である GNU gettext で定められているものです。 https://www.gnu.org/software/gettext/manual/gettext.html#PO-Files 拡張子は .po です。 言語ファイルの構造は大きく分けて以下のようになっています。 メタデータ エントリー msgid (メッセージの ID。翻訳文字列と一致している必要がある) msgstr (翻訳後の文言) このファイルを日本語、英語などの翻訳対象の言語ごとに作成します。 言語ファイルの作成は、Django がコマンドから自動で行ってくれます。 $ django-admin makemessages -l ja makemessages コマンドを実行すると、Django はソースコード全体から翻訳文字列を探し出して言語ファイルを作成します。 言語ファイルはそれぞれの翻訳文字列があるアプリケーション内の locale ディレクトリ内に作成されますが、settings.py 内の文言やアプリケーションに属さないファイルに関するものは LOCALE_PATHS で定義したディレクトリに作成されます。 https://docs.djangoproject.com/ja/3.2/ref/django-admin/#makemessages なお MiiTel Analytics ではバックエンドのアプリケーションで使う言語ファイル以外に、フロントエンドで使うための言語パック API を用意しています。 フロントエンドではこの API から取得した言語パックを state として保持して、メッセージの表示などフロントエンド特有のユースケースで使用していました。 言語ファイルのコンパイル 言語ファイルはそれ自体ではアプリケーションから読み込むことができず、翻訳言語ごとにコンパイルする必要があります。 これも Django のコマンドから行うことができます。 $ django-admin.py compilemessages -l ja コンパイルされた結果、.po ファイルと同じディレクトリに .mo ファイルが生成されます。 これでアプリケーションに翻訳が反映されることになります。 フロントエンドでの i18n 対応 上記のとおり、従来はバックエンド (Django) で処理してきた i18n 対応ですが、今回これをフロントエンドの React に移行しました。 とはいえ、バックエンドで使用していた言語ファイルは膨大な数があるため、すべてを一度に置き換えることは難しく、インクリメンタルに移行していっているという状況です。 react-i18next フロントエンド (React) での i18n 導入にあたって、ライブラリは react-i18next を選びました。 https://react.i18next.com/ 選定の基準としては、ダウンロード数・GitHub Stars 数・更新頻度の多さなどでしたが、すでに RevComm 内の他プロジェクトでも採用していることもありスムーズに導入できると考えました。 参考: npm trends React の i18n 関連ライブラリ (2022 年 1 月時点) ダウンロード数を見ると i18next が一番多いですが、これは Node.js などのバックエンドでの国際化対応も含むライブラリで、react-i18next は i18next をもとにできた React 用ライブラリです。 react-i18next is a powerful internationalization framework for React / React Native which is based on i18next. Check out the history of i18next and when react-i18next was introduced. https://react.i18next.com/#what-is-react-i18next インストールから導入まで react-i18next はドキュメントが充実しているので、基本的にはドキュメントどおりに進めていくと簡単に導入することができます。 https://react.i18next.com/getting-started まずはアプリケーションのルートディレクトリでライブラリをインストールします。 $ npm install react-i18next i18next 翻訳用の JSON ファイルを作成します。これはバックエンドでの言語ファイルにあたります。 " en ": { " test ": { " Welcome to React ": " Welcome to React and react-i18next " } } , " ja ": { " test ": { " Welcome to React ": " React と react-i18next へようこそ " } } 利用側の React コンポーネントでは以下の 2 つを行います。 react-i18next の初期化 翻訳文字列の指定 まず react-i18next を初期化します。一般的にはトップレベルのコンポーネント (index.js や App.js など) で行います。 // App.js import i18n from ‘i18next’; import { initReactI18next } from 'react-i18next' ; import resources from './i18n/resources.json' ; i18n.use(initReactI18next) .init( { lng, fallbackLng: ‘en’, debug: false , resources, } ); i18n.init() 関数に渡す Options は色々と設定できるので、詳細は公式ドキュメントを参照してください。 https://www.i18next.com/overview/configuration-options ユーザーの使用言語は lng で指定します。ブラウザの設定言語を動的に取得したり、後からi18next.changeLanguage() を使って言語を変更することもできます。 (ユーザーの locale に応じて言語を変更する場合などで使えます) 実際に翻訳を行うときは Django で行っていたのと同様に翻訳文字列を指定して、 useTranslation() フックから取得した t 関数を使います。 useTranslation() の引数には JSON ファイル内のキーを指定します。 import React from "react" ; import { useTranslation } from "react-i18next" ; function SomeComponent() { const { t } = useTranslation(‘test’); return <h2> { t( 'Welcome to React' ) } </h2>; // Output: <h2>Welcome to React and react-i18next</h2> } ディレクトリ構成 MiiTel Analytics のフロントエンドでのディレクトリ構成はざっくりと以下のようになっていて、components ディレクトリ内でそれぞれ i18n 用の JSON ファイルを持っています。 ディレクトリ構成 このようにすることで追加や修正などの管理もしやすくなり、グローバルでのメッセージ ID の重複を考慮する必要もなくなります。 ディレクトリごとに i18n の JSON ファイルを管理する場合、 i18n.addResourceBundle() を使ってリソースを読み込みます。 https://www.i18next.com/overview/api#addresourcebundle deep と overwrite オプションを省略した場合、このファイル内では引数に渡された resource で言語ファイルが置き換えられます。 lng (言語) や ns (namespase) などをコンポーネントごとに毎回設定するのは不便なので、下記のように言語ファイルからこれらの情報を抽出する util 関数を作っておくと便利です。 export const I18nUtil = { addResourceJson: (i18n, json) => { Object .entries(json).forEach(( [ lng, value ] ) => { Object .entries(value).forEach(( [ ns, resources ] ) => { i18n.addResourceBundle(lng, ns, resources); } ); } ); } , } ; フロントエンドで国際化対応するメリット・デメリット あくまでも今回の技術スタックを使ったうえでの内容ですが、移行する中で気づいたメリット・デメリットは以下のようなものがありました。 メリット ユーザーに見える部分を柔軟にカスタマイズしやすい バックエンドで対応する方法だと、API レスポンス以外の国際化対応は何らかの形でまとめてフロントエンドに JSON を渡すような方法になってしまいがちです。 フロントエンドで対応することによって、対象のコンポーネントに限って国際化対応を行ったり、ディレクトリ構成を分けて管理しやすくなります。 言語ファイルのコンパイルが不要でデプロイが容易 Django では言語ファイルの更新があったときに再コンパイルが必要だったので、デプロイのタイミングでコマンドを実行する必要がありました (MiiTel Analytics では CI/CD に組み込んで自動実行していました) 。 フロントエンド移行に伴って、言語ファイルの JSON を更新するだけでよくなるので、何かの原因でコンパイルが漏れていて追加したエラーメッセージが翻訳されないといったことがなくなりました。 言語ファイルをコンテキストごとに分けて管理しやすい フロントエンドで使うための言語ファイルをバックエンドから取得する場合、巨大な管理しづらいオブジェクトになってしまいやすいですし、コンポーネント間での props の受け渡しなども発生します。 コンポーネントごとに切ったディレクトリ内で言語ファイルを作ることで、コンテキストを絞って限定された namespace の中で管理することができます。 デメリット サーバーサイドに閉じた文言は対応できない 外部公開している API のエラーメッセージなど、サーバーサイドに閉じていて UI を持たない、かつ国際化対応が必要なものについては、やはりサーバーサイドで対応するしかありません。 パフォーマンスが落ちることがある バックエンドで国際化対応を行う場合はコンパイル済みのメッセージを表示するだけでよくキャッシュも効きやすいですが、フロントエンドでは JavaScript で処理する分のオーバーヘッドが多少なりともあります。 SEO に弱くなる可能性がある Google などの検索エンジンの Bot やクローラーは JavaScript を適切に解釈するとされていますが、サーバーサイドで表示するのと同じレベルでクライアントサイドでの翻訳後の文言を見てくれるかどうかははっきりしません。 ただ、Next.js などで Server-side rendering を行っている場合は、react-i18next でも SSR 対応することが可能なようなので、SEO 重視のサービスであれば導入するのも手です。 https://react.i18next.com/legacy-v9/serverside-rendering まとめ 以上のようにメリット・デメリットはありますが、デメリットの部分がサービス運営上クリティカルでない場合は、フロントエンドで国際化対応を行うというのはアリな選択肢だと思います。 MiiTel Analytics ではまだバックエンドで国際化対応を行っているものも多々ありますので、今後はできるだけフロントエンドに移行していく予定です。 RevComm としてもこれから海外展開に積極的に取り組んでいく時期ですので、興味がある方はぜひ採用ページをご覧ください! www.revcomm.co.jp 補足 余談ですが、日本語から英語の翻訳を行うときに適切な文言を作るちょっとしたコツを共有したいと思います。 ユーザーの目に見える部分に関してはネイティブチェックを必須とすべきですが、内部的に利用するメッセージやコメントなどは開発者自身が英語を考える場面も現実的にはあります。 一番いいのは (身も蓋もないですが) 英語を勉強することですが、即席の対策としては Google 翻訳や DeepL などの自動翻訳サービスを使った方法がおすすめです。 ポイントとしては、単に日本語から英語の翻訳を行うのではなく、 自分で考えた英語訳を Input に入力する 上部の矢印をクリックして日本語と英語を反転させる 日本語に誤りや違和感があれば修正する Output の英語が問題ないか確認する のような順番で行うのがおすすめです。 単語単位よりもなるべく長いセンテンスを入力したほうが、コンテキストがあるので正確で意図したものに近い訳になります。 英語を入力してから言語を反転させる Google 翻訳などの自動翻訳も完璧ではありませんが、日本語話者が翻訳する場合と比較して、英語として成立しない文章や不自然な文章などは生成されにくいです。 サービス展開も開発チームもいつ多国籍になるかわからないので、開発者としては英語力は常に向上させていきたいですね。 参考資料 https://docs.djangoproject.com/en/3.2/topics/i18n/ https://react.i18next.com/ https://www.i18next.com/ https://stackoverflow.com/questions/49137654/internationalization-backend-or-frontend www.youtube.com www.youtube.com
自己紹介 MiiTel Live と MiiTel for Zoom のプロジェクトマネージャーをしている持田と申します。 プロダクトの実装を行いつつ、メンバーが活躍できる環境を整える仕事をしています。 今年の目標は猫背を治すことです。よろしくお願いします。 今回はタイトルの通り音響構成についてのお話なので、ご興味あれば是非最後まで読んでいってください。 自己紹介 オフサイトミーティングの開催 前置き 予備知識 実際の構成 配置図 構成図 この構成で実現できること レンタル・購入機器 オペレーション リハーサルの様子 感想・懸念点 オフサイトミーティングの開催 RevComm は創業当時からフルリモート・フルフレックスを導入しておりますが、対面のコミュニケーションも軽視していない会社です。 (好きな時に出社していいように渋谷にオフィスがあります) 時には対面で社員と顔を合わせて話すことで、より強いチームになっていけると信じています。 そこで、お互いの事をよく知り、働きやすく、ポテンシャルを最大化できるチームにするためにオフサイトでのミーティングの実施をすることになりました。 しかし、必ずしも全員同じ場所に集まれるかというと、海外や国内の遠方で働いてるメンバーもいるため、オフサイトとオンサイトの混合した参加者を許容することが求められました。 そのオフサイトミーティングの運営において、音響・映像の配信を担当することになり、「これは他社でも使える汎用的な構成と運用を考えることができたのではないか」と思い、記事にしました。 前置き 以降のセクションで構成やセットアップに必要な予備知識を記載しますが、後述する構成を決める際に念頭にあった課題は、 「オフサイト/オンサイトメンバーが分け隔てなくコミュニケーションを取れる環境を作る」ということでした。 というのも、今まで経験したオフサイト/オンサイトのメンバーが入り混じる MTG では、リモートで参加しているメンバーとしては、音がよく聞こえなかったり、顔が見れないことにより置いてけぼりになった経験がありました。 この問題を解決したい!という個人的な思いもあり、モチベーションが高い状態で準備を進めることができました。 なお、リハーサルでは期待通りに機能したものの、結局緊急事態宣言で開催には至らなかったことを前もってお伝えさせていただきます😭 予備知識 ミキサーやアンプについて全く知らない場合は入門書を読んでおくことをお勧めします。 自分の場合は、準備のため下記の PA の本を読み、ミキサーやアンプについて軽く理解しました。 忙しくて本を読む暇がない人向けのサマリー (知っておけばいいところだけ超簡単に書いてます) ミキサーで、複数の入力をまとめたり、分けて出力することができる ミキサーで、特定の周波数を絞ることでハウリングを低減させることができる また、実際に使うミキサーの仕様書を読んでおきましょう。 実際にミキサーを見てテンパると多くの時間の損失につながるので、目を瞑っても対象のミキサーが浮かんでくるまで仕様書を読んだら ok です。 自分の場合は、上記に加えてレンタル業者からマイクとミキサーをレンタルして自宅で検証しました。 当日は多くの関係者が集まると思うので、その人たちの1分1秒のコストを考えると、やっておいて損は無いのではないでしょうか。 実際の構成 配置図 構成図 この構成で実現できること 上記の構成にすることで、全ての参加者が、必要な情報を見聞きすることができます。 なお、もし聴衆 (現地) の姿を聴衆 (リモート)に見せてあげたい場合は、PC をさらにもう一台用意してカメラを聴衆 (現地) に向けたものを Zoom に配信してあげれば可能です。 レンタル・購入機器 無線マイク ミキサー アンプ / スピーカー ビデオカメラ (購入済) プロジェクター / スクリーン 各種ケーブル ②で、PC からミキサーに繋ぐ オーディオケーブル (購入) ④で、ミキサーから PC に繋ぐ オーディオインターフェース・ケーブル (購入) ⑤で、ビデオの映像を PC に入れる HDビデオキャプチャーボード オペレーション 映像を中継する人と、音声を中継する人で分かれる。 音声を中継する人は、誤ってノイズが入っている人をミュートにする。 音声を中継する人はハウリングしているマイクをミキサーで切断する。(リハーサル時はハウリングしなかった) 登壇者は、自分の PC で Zoom に参加し、資料を投影する。 リハーサルの様子 80名を収容する部屋の規模感 ミキサーに入出力端子を繋ぐ様子 映像の配信を担う PC 感想・懸念点 映像・音響機器が正常に動いてくれないと、想定していたコンテンツが実施できないため、大きなプレッシャーを感じていました。普段エンジニアとして仕事をする上で、安心して進めるにはやはり検証を重ねることが重要だと理解し、今回は実践できていたいので、リハーサル当日はある程度の自信を持って参加することができました。 上記の写真の通り、ミキサーには多くの端子があり、初めて使うのであればスムーズに接続できないことも考えられるので、時間には余裕を持っておいたほうが良さそうです。 (TKP 日本橋さんの手厚い協力にも助けられました!) 自分の場合は、自宅での検証が効いたのかスムーズに接続できました。多分 30 分くらいあれば足りました。 なお、音声を中継する PC と、映像を中継する PC はそこそこ熱を持っていたので、スペックには気をつけましょう。今回は、MacBook Pro (13-inch, M1, 2020, 16GB) で実施しました。 Windows でも特に困らないはずです。 それと、登壇/聴衆という形式ならば今回の構成で困りませんが、各グループに分けてチームディスカッション等をする場合は、チーム分け時点で、オンラインとオフラインでチームをまとめるように配慮しておく必要がありますのでご注意ください。
はじめまして。 RevComm でエンジニアリングマネージャーを担当している瀬里 (せり) です。 記念すべきテックブログの最初の記事となりますが、この記事を読んでいる方の中にはそもそもRevComm をご存知でない方もいらっしゃると思うのでまずは会社紹介からさせてください。 RevComm とエンジニアリング組織について RevComm では「コミュニケーションを再発明し、人が人を想う社会を創る」というミッションのもと、第一弾サービスとして MiiTel という電話業務向けサービス、直近リリースされたものでは会議などの可視化を行う MiiTel for Zoom というサービス等を提供しています。 miitel.com miitel.com サービスの技術を支えているエンジニアは全体で現在約60名。 3年ちょっと前に私が参加したときには全社員が5名いるかいないかみたいな感じだったので結構人数が増えましたね。集まったエンジニアのバックグラウンドは多岐にわたっており、コンサルタントやSIer、サイト制作会社、元公務員などさまざまです。 部署は大きくテクノロジーとリサーチという2つに分かれており、それぞれ下記のような役割で動いています。 テクノロジー部署:インフラから Web 、モバイルという、お客さんに安定して使いやすいサービスを届ける部署 リサーチ部署:新規技術の研究開発を行い、新たな価値を創出する部署 どういう人が働いているかとか、どんな環境で仕事しているかについてはまた別の機会に発信しようと思っているので楽しみにしていてください。 なぜブログ?今後どんな記事が書かれるの? 今回ブログをはじめることにした主な目的は「エンジニア界隈での知名度アップ」です。 RevComm という会社名はスタートアップ界隈では多少知名度が上がってきてはいるものの、世間的にはまだまだ認知度が低い状況。中で働く者としてはちょっと寂しい。 人数が増えたとはいえ、優秀なエンジニアの方にどんどん入ってきてもらいたいので、まずはエンジニアの方にだけでもちらっと名前くらい覚えていただければなぁというのが願いです。 今後アップされる記事としては、社内で使われている技術から最先端の技術の研究について、といったテック系の内容はもちろんですが、働いている環境や評価制度といったカルチャー的側面まで幅広く記事として公開していく予定です。 終わりに ちょっと調べものをしていたらこのテックブログの記事をみつけて解決できた、というような形で少しでも皆様の役に立てればと思っています。 また、掲載される記事を通して、少しでも皆様に RevComm って技術力高いな、一緒に働いてみたいな、とか、働く環境として面白そうだけど、まだまだ組織として未完成だから自分が入っていろいろ活躍できるのでは?とか感じて頂ければ幸いです。その際には是非応募してください。 hrmos.co それでは今後とも宜しくお願いいたします。