TECH PLAY

株式会社RevComm

株式会社RevComm の技術ブログ

171

本記事の著者はResearch Engineerの大野です。最近は、 ホロウナイト というゲームをやっていましたが、もう少しでクリアというところで敵が倒せず諦めました。 はじめに RevCommは電話営業や顧客応対の通話を支援するAI搭載型のIP電話「MiiTel」を提供しています。 この製品は、通話の文字起こしを保存する機能を備えており、RevCommは数千時間の対話データに接しています。 この対話データに対する支援の1つとして対話要約が考えられます。対話要約とは、入力された対話から、その主要な概念を含むより短い文書(要約)を自動的に作成することです。 ユーザは、要約を作成する手間が省けたり、あるいは要約を読むことで対話の概要をより早く理解できるなどの利点があります。 これから前編と後編の2回に分けて、対話要約に関する記事を書きます。今回の記事では、はじめにいくつかの対話要約のデータセットをご紹介します。 対話要約は、メール・チャット・エージェントと顧客間の対話など、複数のドメインで使用されます。そして、ドメインによって課題やゴールが異なります。そこでデータセットをいくつかご紹介することで、各ドメインの抱える課題やゴールを概観していきます。 また、本記事では要約システムの評価指標についてもご紹介します。長らく単語のオーバーラップに基づく指標が使用されていましたが、他の内容に基づく評価指標が最近になって提案されています。 評価指標を概観することは、要約システムを構築する際の課題を考える良い材料になると考えています。 はじめに データセット オープンドメイン対話要約 チャット 日常会話 TV番組・インタビュー タスク指向の対話要約 会議 メール 医療 カスタマーサービス 評価手法 ROUGE BLEU chrF BERTScore FEQA FactSumm まとめ 参考文献 データセット [Jia 22] は対話要約を2つに分類しました。1つはオープンドメイン対話要約で、もう1つはタスク指向対話要約です。下記に[Jia 22]の図を引用します。 オープンドメイン対話要約は、日常生活やドラマにおける対話、あるいはオンラインフォーラムを対象にした要約です。物語の大筋を獲得したり、与えられたテーマに対する意見をまとめることに焦点を当てます。この記事では、オープンドメイン対話要約のデータセットとして、「チャット」「日常会話」「TV番組・インタビュー」の3つのカテゴリを取り上げます。 タスク指向の対話要約は、主に顧客とサービスプロバイダに所属するエージェントとの間の会話を対象にします。顧客は特定の課題を持っており、それを解消するためにエージェントと対話をします。この記事では、タスク指向の対話要約のデータセットとして、「会議」「メール」「医療」「カスタマーサービス」の4つのカテゴリを取り上げます。 オープンドメイン対話要約 チャット [Gliwa 19]は対話要約のためのデータセットである SAMSum (Samsung Abstractive Messenger Summarization)を作成しました。データセットの名前が示すように、著者はSamsungに所属しています。 データセットはオンラインチャットと要約のペアを約1万6000件含んでいます。[Zhao 21]によると、ターン数は平均して9.9回です。オンラインチャットの性質上、データセットが絵文字・顔文字あるいは略語を含んでいます。下記に[Chen 21]の図を引用して、このデータセットに含まれる対話の例を示します。 日常会話 [Chen 21]は対話要約のためのデータセットDialogSumを作成しました。このデータセットは、学校・ビジネス・レジャーなどの、日常生活における様々なトピックに関連した対話を約1万3000件含みます。 DialogSumは主に下記の3つのデータセットから作成されました。これらは対話要約のためのデータセットではありません。例えば、[Sun 19]のDREAMはdialogue understandingというタスクのデータセットです。 [Li 17]のDailydialog [Sun 19]のDREAM [Cui 20]のMuTual 下記に[Chen 21]の図を引用して、DialogSumに含まれる対話の例を示します。これはビジネス上の対話です。 [Mehnaz 21]は、英語とヒンディー語混じりの対話要約のデータセットである、 GupShup を作成しました。このデータセットは、[Gliwa 19]によって作成されたSAMSumをもとに作成されました。 このデータセットはコードスイッチングと呼ばれる、会話中に話し手が異なる言語を切り替えて話す現象に着目しています。論文には、「会話エージェントやチャットプラットフォームの普及に伴い、世界中の多くの多言語コミュニティにおいて、コードスイッチングは文字による会話に不可欠なものとなっています」と書かれています。 下記に[Mehnaz 21]の図を引用します。青い部分が英語で、紫の部分がヒンディー語です。この例では英語での要約だけが示されていますが、データセットは英語とヒンディー語混じりの要約も含みます。 TV番組・インタビュー [Chen 22]はTV番組の対話要約データセットである SummScreen を作成しました。このデータセットは、次の2つのステップにより作成されました。まず、 TVMegaSite と ForeverDreaming から、TV番組のトランスクリプトを獲得します。次に、そのトランスクリプトの要約として、 Wikipedia と TVmaze から番組概要を獲得しました。 下記に[Chen 22]の図を引用し、SummScreenに含まれる対話の例を示します。青い枠にあるSheldonとLeonardの会話は、物語の大筋に関連が薄いため要約文には出てきません。 [Zhu 21]は米国公共ラジオ放送(National Public Radio; NPR)とCNNで行われたインタビューから、 MediaSum を作成しました。ここでは、一般に公開されているインタビューの原稿を対話文とし、NPRとCNNに掲載されたインタビューの説明を要約文としました。 タスク指向の対話要約 会議 [Carletta 05]は 会議のマルチモーダルなデータセットである AMI meeting corpus を作成しました。架空のデザインチームが、リモコンのデザインを話し合うために行ったミーティングが、この データセット に含まれます。また、視線の方向やホワイトボードの情報などの対話以外の情報も含まれています。このデータセットを日本語に翻訳した データセット も存在します。 [Zhong 21]は QMSum を提案しました。これは、クエリに基づく要約タスクのためのデータセットであり、232件の会議に関連する1808件のクエリと要約の組から構成されています。[Janin 03]による ICSI Meeting Corpus と[Carletta 05]によるAMI meeting corpusと議会のミーティングから作成されました。 クエリとは、システムが出力する要約に対する制限です。クエリの例として「Summarize the discussion about remote control style and use cases.」や「Summarize Project Manager's opinion towards remote control style and use cases.」が挙げられます。 オンラインチャットなどのこれまでご紹介したデータセットと比較して、会議はその文量が多く、また多様なトピックを含みます。そのため、会議から短い要約を作成することは難しいことと、参加者によって知りたい内容が異なることに[Zhong 21]は着目しました。その結果、与えられたクエリに応じて要約を作成する対話要約システムのためのデータセットを作成しました。 下記に[Zhong 21]の図を引用し、クエリに基づく要約の例を示します。3つのクエリが入力され、各々に対して対話要約システムが要約を作成します。 メール [Zhang 21a] は、2549件の電子メールスレッドからなるデータセットである EmailSum を作成しました。各々のスレッドは3件から10件のメールを含みます。また、30単語以下の短い要約と100単語以下の長い要約が、各スレッドに付与されています。 このデータセットは下記の3つのメールのデータセットから作成されました。これらにはスレッド構成が整理されていませんでした。そのため、スレッド構成を整理して、要約を作成することが[Zhang 21a]の貢献と言えます。 [Klimt 04]が作成したEnron [Craswell 06]が作成したW3C Avocado Research Email Collection 下記に[Zhang 21a]の図を引用し、短い要約と長い要約の例を示します。 医療 [Song 20]は公開フォーラムで医者と対話できるプラットフォームである Chunyu-Doctor から対話要約のための データセット を作成しました。フォーラム上の対話に対話要約を適用する目的を、医師の手間を削減するためとしています。 [Song 20]の図を引用し、対話の例を示します。ここでは、患者の発言のうち特定のトピックに関連するものを抜粋しています。 カスタマーサービス カスタマーサービスは、企業に所属するエージェントが顧客に対して何らかのサービスを行うことです。対話要約を適用する理由の1つには、エージェントの負担軽減があります。[Liu 19]は、滴滴出行(DiDi)で働く30万のエージェントと顧客の対話に対して、対話要約を適用した論文です。この論文のモチベーションの項では「要約の作成にはエージェントの時間の約25%が費やされています。DiDiには数千人のエージェントがいます。そのため、要約の自動生成は膨大な人的資源を節約することができます。」と書かれています。 [Lin 21]は中国語で書かれた対話要約のデータセットである CSDS (Customer Service domain Dialogue Summarization)を作成しました。これは[Chen 20]が作成した対話データセットJDDCに要約を付け加えることで作成されています。JDDCは、中国のe-コマース最大手である京東商城(JD)のスタッフと顧客間のプリセールスとアフターセールスの対話から構成されています。 下記に[Lin 21]の図を引用します。注文した商品の配送に関して、スタッフと顧客が話し合っており、これに対して3種類の要約が付与されています。 [Zhao 21]は、5つのドメイン(レストラン、ホテル、アトラクション、タクシー、電車)における、約1万の対話とその要約を含んだデータセットTODSumを作成しました。この論文の著者の所属は3つに別れており、北京郵電大学と美団(Meituan)と中国移動(China Mobile)です。美団はe-コマースプラットフォームである美団と、口コミサイトである大衆点評を運営しています。中国移動は世界最大の携帯電話事業者です。 下記に[Zhao 21]の図を引用し、TODSumに含まれる対話の例を示します。 阿里巴巴(Alibaba)に所属する[Zou 21a]は淘寶(Taobao)で行われた顧客とエージェントのチャットログ109万件を使用して、対話要約に取り組みました。この研究で使用された 実装とデータセット が公開されています。この論文の著者は、[Zou 21b]において、阿里巴巴のコールセンターで行われた顧客とエージェントの対話に対して、対話要約を適用したことを報告しています。ただし、こちらはデータセットが公開されていません。 評価手法 要約の手法を評価するための評価指標をいくつか紹介します。これまでは、正解と定義された要約文とシステムが出力した要約文の類似度を計算し、その類似度が高ければ良い要約文と判断していました。近年になって、質問応答システムを使用するなどの、いくつかの評価指標が提案されています。ここでは数式なしで説明しようと試みており、いくつかの評価指標には数式を示していません。 ROUGE ROUGEは最も広く使われる評価指標の1つで、この指標を提案した[Liu 04]の引用件数は9000件を超えています。 ROUGEは2つの文書に共通する単語数に着目して、2つの文書の類似度を計算します。いくつかの派生があり、ROUGE-1はuni-gram(単語)を使用し、ROUGE-2はbi-gram(連続する2単語)を使用します。Huggingface社のライブラリであるevaluateに 実装 があります。 BLEU [Papineni 02]によるBLEUは共通する単語n-gramの数に着目し、類似度を計算します。nはパラメタであり、1から4がよく使用されます。言い換えれば、単語だけでなく、連続する2単語・3単語・4単語に着目します。また、システムが出力した要約文が、正解と定義された要約文よりも短い時にペナルティを与えるように工夫されています。Huggingface社のライブラリであるevaluateに 実装 があります。 下記に計算式を示します。ここでは、システムが出力した要約文の数と、正解と定義された要約文の数が共に1である時の計算式を示しています。BPはシステムが出力した要約文が正解と定義された要約文よりも短い時のペナルティです。また は与えられた文の単語n-gramを返す関数です。pnは2つの文に存在する単語n-gramのうち、共通する単語n-gramの割合を表します。N=4がよく使用され、1-gramから4-gramに基づきBLEUが計算されます。 chrF [Popovi ́c 15]によるchrFは、上記のROUGE・Bleuと異なり、単語ではなく文字n-gramに着目します。はじめに文字n-gramの集合を、正解と定義された要約文と、システムが出力した要約文のそれぞれから獲得します。次にそれらの 適合率と再現率を求め、最後にF値を計算します。 Huggingface社のライブラリであるevaluateに実装があります。n=6が論文で使用されており、また 実装 でもデフォルト値はn=6となっています。 BERTScore これまでにご紹介した指標では、2つの単語を比較する際に完全一致するかを確認しており、同義語や類義語を扱えませんでした。[Zhang* 20a]によるBERTScoreは言語モデルを使用してこの問題に取り組みます。 はじめに言語モデルを用いて、正解と定義された要約文の各トークンをベクトルに変換したものをxとします。次に、システムが出力した要約文の各トークンをベクトルに変換したものをyとします(論文中ではx^でしたが、今回記事を書いた時に読みにくいと感じたのでyに変更しています)。 下記に計算式を示します。 再現率と適合率を計算した後に、その調和平均を求めます。 数式よりも図を使った説明の方が直感的で分かりやすいかもしれません。下記に[Zhang* 20a]の図を引用し、BERTScoreの概要を示します。Contextual Embeddingの部分に置かれたキャラクタはセサミストリートに登場する Bert です。Importance Weightingの部分は上の手順では説明を省略した部分です。基本的にはBERTScoreは各トークンに重みを付けることはしませんが、idfなどを使用して重み付けすることもできます。 また、[Zhao 19]の図を示します。こちらの方がBERTScoreの概要が理解しやすいかもしれません。青色の「System」がシステムが出力した要約文であり、オレンジ色の「Ref」が正解と定義された要約文です。BERTScoreでは、guyとmanあるいはboatとcanoeなどの類似した語を扱うことができ、単純に共通な単語数を数えるよりもより正確な評価が期待できます。 FEQA [Durmus 20]のFEQAは質問応答システムを利用する指標であり、factual consistencyという観点で要約を評価します。言い換えれば、システムが生成した要約文の内容が、要約前の文書の内容にどれくらい近いかを評価しています。下記に、[Durmus 20]の図を引用し、factual consistencyが欠けた要約の例を示します。ここでは、要約前の文書に「born」と書かれていますが、要約文は「died」と書きました。この単語の違いは内容に大きく影響を与えます。 FEQAは3つのステップから構成されています。はじめにシステムが出力した要約文のあるフレーズを[MASK]に置換します。次に[MASK]が答えになるような質問を生成します。最後に質問と要約前の文書を質問応答システムに入力し、置換前のフレーズが答えとして出力されるか確認します。 [Durmus 20]の図を下記に引用し、この指標の計算例を示します。「The home was built for inspection.」という文がシステムが生成した要約文です。ここから、「Q1: What was the home built for?」「Q2: What was built for inspection?」という 2つの質問が作成されました。これらの質問に対する質問応答システムの出力は「A1’: former australian prime minister malcolm fraser and his wife」「A2’: the home」であり、片方は期待されたものと異なります。 各ステップの実装についても簡単に紹介します。はじめのステップでは固有表現抽出や句構造解析器(constituency parser)を使用して[MASK]に置換する箇所を探します。次のステップでは、 QA2Dデータセット で訓練したseq2seqモデルが使用されます。最後のステップでは、 SQuAD (Stanford Question Answering Dataset)でファインチューニングしたBERTを使用します。 実装 は公開されています。 FactSumm FactSummはトリプル抽出を利用した指標であり、factual consistencyを評価します。トリプルとは、2つのオブジェクトとそれらの関係の3つから構成されたデータの表現方法の一種です。FactSummは、要約文と要約前の文書の各々にトリプル抽出を適用して、その結果を比較することで評価を行います。FactSummは論文として公開されておらず、 GitHub で文書と実装が公開されています。 下記にFactSummのGitHubレポジトリから図を引用し、その処理の概要を示します。要約前の文書からは、(Inception, is, science fiction film)というトリプルが抽出され、要約文からは(Inception, is, action film)というトリプルが抽出されました。この2つは異なるので、要約文はfactual consistencyが欠けていると言えます。 まとめ この記事では下記のことを行いました。後編では、最近の対話要約手法について書く予定です。 オープンドメイン対話要約のデータセットとして、「チャット」「日常会話」「TV番組・インタビュー」の3つのカテゴリを取り上げました。 タスク指向の対話要約のデータセットとして、「会議」「メール」「医療」「カスタマーサービス」の4つのカテゴリを取り上げました。 ROUGE・Bleu・chrF・BERTScore・FEQA・FactSummを取り上げました。FEQA・FactSummは、factual consistencyを評価する指標であり、これまで用いられてきた単語のオーバーラップに基づく指標とは大きく異なります。 参考文献 [Carletta 05] Carletta, J., Ashby, S., Bourban, S., Flynn, M., Guillemot, M., Hain, T., Kadlec, J., Karaiskos, V., Kraaij, W., Kronenthal, M., Lathoud, G., Lincoln, M., Lisowska, A., McCowan, I., Post, W., Reidsma, D., and Wellner, P.: The AMI Meeting Corpus: A PreAnnouncement, in Proceedings of the Second International Conference on Machine Learning for Multimodal Interaction, MLMI’05, pp. 28–39, Berlin, Heidelberg (2005), Springer-Verlag [Chen 20] Chen, M., Liu, R., Shen, L., Yuan, S., Zhou, J., Wu, Y., He, X., and Zhou, B.: The JDDC Corpus: A LargeScale Multi-Turn Chinese Dialogue Dataset for E-commerce Customer Service, in Proceedings of the Twelfth Language Resources and Evaluation Conference, pp. 459–466, Marseille, France (2020), European Language Resources Association [Chen 21] Chen, Y., Liu, Y., and Zhang, Y.: DialogSum Challenge: Summarizing Real-Life Scenario Dialogues, in Proceedings of the 14th International Conference on Natural Language Generation, pp. 308–313, Aberdeen, Scotland, UK (2021), Association for Computational Linguistics [Chen 22] Chen, M., Chu, Z., Wiseman, S., and Gimpel, K.: SummScreen: A Dataset for Abstractive Screenplay Summarization, in Proceedings of the 60th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers), pp. 8602–8615, Dublin, Ireland (2022), Association for Computational Linguistics [Craswell06] Craswell, N., Vries, A., and Soboroff, I.: Overview of the TREC-2005 Enterprise Track, Text Retrieval Conference (TREC), , USA (2006) [Cui 20] Cui, L., Wu, Y., Liu, S., Zhang, Y., and Zhou, M.: MuTual: A Dataset for Multi-Turn Dialogue Reasoning, in Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics, pp. 1406–1416, Online (2020), Association for Computational Linguistics [Durmus 20] Durmus, E., He, H., and Diab, M.: FEQA: A Question Answering Evaluation Framework for Faithfulness Assessment in Abstractive Summarization, in Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics, pp. 5055–5070, Online (2020), Association for Computational Linguistics [Feigenblat 21] Feigenblat, G., Gunasekara, C., Sznajder, B., Joshi, S., Konopnicki, D., and Aharonov, R.: TWEETSUMM A Dialog Summarization Dataset for Customer Service, in Findings of the Association for Computational Linguistics: EMNLP 2021, pp. 245–260, Punta Cana, Dominican Republic (2021), Association for Computational Linguistics [Gliwa 19] Gliwa, B., Mochol, I., Biesek, M., and Wawer, A.: SAMSum Corpus: A Human-annotated Dialogue Dataset for Abstractive Summarization, in Proceedings of the 2nd Workshop on New Frontiers in Summarization, pp. 70–79, Hong Kong, China (2019), Association for Computational Linguistics [Janin 03] Janin, A., Baron, D., Edwards, J., Ellis, D., Gelbart, D., Morgan, N., Peskin, B., Pfau, T., Shriberg, E., Stolcke, A., and Wooters, C.: The ICSI Meeting Corpus, in 2003 IEEE International Conference on Acoustics, Speech, and Signal Processing, 2003. Proceedings. (ICASSP ’03)., Vol. 1, pp. I–I (2003) [Jia 22] Jia, Q., Ren, S., Liu, Y., and Zhu, K. Q.: Taxonomy of Abstractive Dialogue Summarization: Scenarios, Approaches and Future Directions (2022) [Klimt 04] Klimt, B. and Yang, Y.: The Enron Corpus: A New Dataset for Email Classification Research, in Proceedings of the 15th European Conference on Machine Learning, ECML’04, pp. 217–226, Berlin, Heidelberg (2004), SpringerVerlag [Li 17] Li, Y., Su, H., Shen, X., Li, W., Cao, Z., and Niu, S.: DailyDialog: A Manually Labelled Multi-turn Dialogue Dataset, in Proceedings of the Eighth International Joint Conference on Natural Language Processing (Volume 1: Long Papers), pp. 986–995, Taipei, Taiwan (2017), Asian Federation of Natural Language Processing [Lin 04] Lin, C.-Y.: ROUGE: A Package for Automatic Evaluation of Summaries, in Text Summarization Branches Out, pp. 74–81, Barcelona, Spain (2004), Association for Computational Linguistics [Lin 21] Lin, H., Ma, L., Zhu, J., Xiang, L., Zhou, Y., Zhang, J., and Zong, C.: CSDS: A Fine-Grained Chinese Dataset for Customer Service Dialogue Summarization, in Proceedings of the 2021 Conference on Empirical Methods in Natural Language Processing, pp. 4436–4451, Online and Punta Cana, Dominican Republic (2021), Association for Computational Linguistics [Liu 19] Liu, C., Wang, P., Xu, J., Li, Z., and Ye, J.: Automatic dialogue summary generation for customer service, in Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining, pp. 1957– 1965 (2019) [Mehnaz 21] Mehnaz, L., Mahata, D., Gosangi, R., Gunturi, U. S., Jain, R., Gupta, G., Kumar, A., Lee, I. G., Acharya, A., and Shah, R. R.: GupShup: Summarizing Open-Domain Code-Switched Conversations, in Proceedings of the 2021 Conference on Empirical Methods in Natural Language Processing, pp. 6177–6192, Online and Punta Cana, Dominican Republic (2021), Association for Computational Linguistics [Papineni 02] Papineni, K., Roukos, S., Ward, T., and Zhu, W.-J.: Bleu: a Method for Automatic Evaluation of Machine Translation, in Proceedings of the 40th Annual Meeting of the Association for Computational Linguistics, pp. 311–318, Philadelphia, Pennsylvania, USA (2002), Association for Computational Linguistics [Popovi ́c 15] Popovi ́c, M.: chrF: character n-gram F-score for automatic MT evaluation, in Proceedings of the Tenth Workshop on Statistical Machine Translation, pp. 392–395, Lisbon, Portugal (2015), Association for Computational Linguistics [Song 20] Song, Y., Tian, Y., Wang, N., and Xia, F.: Summarizing Medical Conversations via Identifying Important Utterances, in Proceedings of the 28th International Conference on Computational Linguistics, pp. 717–729, Barcelona, Spain (Online) (2020), International Committee on Computational Linguistics [Sun 19] Sun, K., Yu, D., Chen, J., Yu, D., Choi, Y., and Cardie, C.: DREAM: A Challenge Data Set and Models for Dialogue-Based Reading Comprehension, Transactions of the Association for Computational Linguistics, Vol. 7, pp. 217–231 (2019) [Zhang 20a] Zhang , T., Kishore , V., Wu , F., Weinberger, K. Q., and Artzi, Y.: BERTScore: Evaluating Text Generation with BERT, in International Conference on Learning Representations (2020) [Zhang 20b] Zhang, Y., Jiang, Z., Zhang, T., Liu, S., Cao, J., Liu, K., Liu, S., and Zhao, J.: MIE: A Medical Information Extractor towards Medical Dialogues, in Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics, pp. 6460–6469, Online (2020), Association for Computational Linguistics [Zhang 20c] Zhang, Y., Sun, S., Galley, M., Chen, Y.-C., Brockett, C., Gao, X., Gao, J., Liu, J., and Dolan, B.: DialoGPT: Large-Scale Generative Pre-training for Conversational Response Generation, in ACL, system demonstration (2020) [Zhang 21a] Zhang, S., Celikyilmaz, A., Gao, J., and Bansal, M.: EmailSum: Abstractive Email Thread Summarization, in Proceedings of the 59th Annual Meeting of the Association for Computational Linguistics and the 11th International Joint Conference on Natural Language Processing (Volume 1: Long Papers), pp. 6895–6909, Online (2021), Association for Computational Linguistics [Zhang 21b] Zhang, Y., Ni, A., Yu, T., Zhang, R., Zhu, C., Deb, B., Celikyilmaz, A., Awadallah, A. H., and Radev, D.: An Exploratory Study on Long Dialogue Summarization: What Works and What’s Next, in Empirical Methods in Natural Language Processing (EMNLP) 2021 (2021) [Zhao 19] Zhao, W., Peyrard, M., Liu, F., Gao, Y., Meyer, C. M., and Eger, S.: MoverScore: Text Generation Evaluating with Contextualized Embeddings and Earth Mover Distance, in Proceedings of the 2019 Conference on Empirical Methods in Natural Language Processing and the 9th International Joint Conference on Natural Language Processing (EMNLP-IJCNLP), pp. 563–578, Hong Kong, China (2019), Association for Computational Linguistics [Zhao 21] Zhao, L., Zheng, F., He, K., Zeng, W., Lei, Y., Jiang, H., Wu, W., Xu, W., Guo, J., and Meng, F.: TODSum: Task-Oriented Dialogue Summarization with State Tracking (2021) [Zhong 21] Zhong, M., Yin, D., Yu, T., Zaidi, A., Mutuma, M., Jha, R., Awadallah, A. H., Celikyilmaz, A., Liu, Y., Qiu, X., and Radev, D.: QMSum: A New Benchmark for Query-based Multi-domain Meeting Summarization, in Proceedings of the 2021 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, pp. 5905–5921, Online (2021), Association for Computational Linguistics [Zhu 21] Zhu, C., Liu, Y., Mei, J., and Zeng, M.: MediaSum: A Large-scale Media Interview Dataset for Dialogue Summarization, in Proceedings of the 2021 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, pp. 5927–5934, Online (2021), Association for Computational Linguistics [Zou 21a] Zou, Y., Lin, J., Zhao, L., Kang, Y., Jiang, Z., Sun, C., Zhang, Q., Huang, X., and Liu, X.: Unsupervised summarization for chat logs with topic-oriented ranking and context-aware auto-encoders, in Proceedings of the AAAI Conference on Artificial Intelligence, Vol. 35, pp. 14674– 14682 (2021) [Zou 21b] Zou, Y., Zhao, L., Kang, Y., Lin, J., Peng, M., Jiang, Z., Sun, C., Zhang, Q., Huang, X., and Liu, X.: Topic-oriented spoken dialogue summarization for customer service with saliency-aware topic modeling, in Proceedings of the AAAI Conference on Artificial Intelligence, Vol. 35, pp. 14665–14673 (2021)
アバター
この記事は RevComm Advent Calendar 2022 の 25 日目の記事です。 はじめに  株式会社 RevComm 執行役員 CTO の平村 健勝 (@hiratake55) です。2022 年は、AI 搭載 IP 電話 MiiTel の機能改善から大規模なシステム移行、セキュリティ面での改善、さらにインドネシアをはじめとする海外事業の拡大、AI 搭載オンライン商談解析ツール MiiTel for Zoom の正式リリースなど、社会やビジネスにおけるさまざまな課題を解決する製品をリリース・改良してまいりました。  この記事では、このような急速な発展を遂げるために、技術的な挑戦だけでなく、私やほかのエンジニアリングマネージャー陣が実践している、あまり触れられないユニークな工夫について紹介したいと思います。 エンジニア出身ではないCEOの良いところ  すべての会社は「 エンジニア出身の CEO がいる会社 」「 エンジニア出身ではない CEO がいる会社 」の 2 つのタイプに分類されると思います。中には、エンジニア出身ではないものの一定のシステムの知識がある CEO もいるでしょう。そして、弊社 CEO の會田は、大学も文系学部、三菱商事の出身で、エンジニア出身ではなく、後者のタイプです。  開発部門にとって「エンジニア出身ではない CEO」は、一見 デメリットが多い と思われがちですが、必ずしもそうではなく、次の表のようにそれぞれによい点があります。 エンジニア出身の CEO のよいところ エンジニア出身ではない CEO のよいところ(一例) システム構成やシステムの特徴、開発の方法論などを時間をかけて説明する必要がない セキュリティやシステム可用性、保守性など、非機能面でのリスクアセスメントや経営判断ができる 収益性を見積もりにくい R&D などの活動に対して理解が得やすい マーケティングの知識や営業力があるので、事業や案件の拡大が高速 技術的実現性を度外視した、突拍子もないアイデアが出てくる ファイナンス(P/L, B/S, C/F など)を意識した意思決定ができる 他社のエグゼクティブ層との広く深い人脈がある  このため、エンジニア出身ではないCEOの会社においては、 工夫が求められるところは工夫する ことにより、エンジニア出身の CEO の会社には難しいことを 圧倒的にスピーディーに進めることができる と考えています。  これらを踏まえ、エンジニア出身ではない CEO の会社のよいところを最大限生かしながら、 エンジニア出身の CEO の会社のよいところを上回るような組織 を目指して、我々が実践していてうまく機能している方策を 5 つ紹介します。 方策 1: 開発スケジュールや開発方針の説明に比喩を用いる  前例がある開発タスクはある程度正確にスケジュールを見積もることができますが、RevComm では社内どころか世界的に前例がない取り組みであっても、それがユーザーや社会に必要とされているならばむしろ積極的に取り組もうという文化があります。 このため、 工数の見積もりが難しかったり 、 そもそも実現できるのかどうかも不明な状態 から着手することがあります。  このため、予定より遅延したり早期にリリースできてしまうこともしばしばあります。このような状況を説明するには、 建設工事 を例に出すと、スムーズに理解を得ることが多いです。  例えば、すでに何棟も実績がある戸建住宅の建築工事はある程度正確に工事完了の時期を見積もることができるでしょう。しかし、例えば高速道路や地下鉄のような土木工事で、まだトンネルを掘ったことのない場所にトンネルを作るのは、工事の途中で何が起こるかわかりません。快調に進めば予定より早く完了するでしょうし、工事中に想定していなかった硬い地盤や軟弱な地盤が見つかると、数か月や数年単位で遅れが発生してしまいます。 このように、想像しづらいシステム開発の工程を イメージしやすい現実的な比喩 を用いて説明することで、手触り感のある説明にできることが多いです。 方策 2: エンジニアにはそれぞれの得意分野があることを伝える  同様に比喩を用いるケースとして、それぞれのエンジニアに専門分野があることについては、高い専門性を持ったスペシャリストが活躍する 医療現場 に例えるとスムーズに理解を得られることが多いです。  創業間もないシード期には、フロントエンドから、モバイルアプリ、サーバサイド、インフラ、デザインや機械学習に至るまで、幅広い領域をカバーできるフルスタックエンジニアが活躍できることが多くあります。つまりこれは個々の病気に対する専門性は高くないものの幅広い知識を持った " かかりつけ医 " のようなタイプのドクターです。  しかし、組織の規模が大きくなると、高い技術や経験、ナレッジが求められるため、役割が分担されていきます。これは総合病院や大学病院のように、内科、外科、眼科などそれぞれの専門性が高く発揮できる組織に分けられていることに近いです。一方で、フルスタックエンジニアも救急救命医のように、分野横断的な課題の解決に高く貢献できます。  例えば、優先度を高めて対応しないといけない急な ToDo が生まれた際、エンジニア出身ではない CEO は「 エンジニアは全員今の仕事を停めて優先度の高い ToDo に着手すること 」といった指示を出してしまいがちです。例えば、サーバサイドアプリケーションの不具合改修をモバイルアプリエンジニアが担当することは、 外科のドクターが内科の病気を診察する ような状況であり専門領域が違います。 各メンバーが得意な分野で活躍できると、効率的に課題を解決したり早期に目標を達成することができます。このように、役割分担についても納得感があり透明性の高い形で説明することを心がけています。  また、開発プロセスにおいて、フロントエンドエンジニアとサーバサイドエンジニアのように役割分担して開発することを「カレー作り」に例えて説明しました。 下記の図は全社オフサイトミーティングでも説明したスライドの抜粋です。  役割分担が進んでも、企画構想から設計、開発、QA、リリースに至る全体のプロセスを俯瞰することで、技術的に高いレベルの取り組みを高速に進めることの重要性を伝えました。 方策 3: 全メンバーが共通の目標を意識する  これは、CEO に対して行っている取り組みではなく、メンバー全員に対して心がけるようにしている内容です。  製品開発を進めるにあたって、CEO や事業責任者(プロダクトオーナー)が一方的に指示し、開発やデザイン、マーケティング、セールスを行う形の組織もあると思います。しかし、RevComm ではそのような開発方法はそぐわないと判断し、創業初期からエンジニアやデザイナーだけでなく、セールスやカスタマーサクセス、カスタマーサポートのようなプロダクト・ビジネス部門に加えて、コーポレート部門(管理部門)のメンバーも一体となり、 全員が主導で共通の目標を達成することを意識しています 。 これにより、短期間で高い収益、高い満足度が得られるような新しいアイデアを提案したり、トレンドに合わせて製品をアップデートしたり、今後増えるであろうお客様のニーズを先んじて実装することにつながり、 競争力の高い製品をマーケットにいち早くリリースすることができる と考えています。 方策 4: 相手の立場に立って、相手に伝わる言葉を選ぶ  リファクタリングや長期間運用してきたシステムのリニューアルなどのように、収益性が見通しづらい、いわゆる " 守りの開発 " や、避けてほしいこと、お願いしたいことも、言葉を選んで言い換えることで、その重要性をスムーズに理解してもらえることが多くあります。 以下にその一例を挙げます。 望ましくない伝え方 望ましい伝え方 開発しにくくなったので、追加開発は今後半年間ストップして全面的に作り直します 今後企画されている機能を現状のアプリに組み込むとなると、3 か月程度あればできます。 しかし、半年かけて作り直すと、1 か月程度で済むようになります。さらに、今後他の追加開発も短期間で完了でき、プロダクトアップデートのスピードも速くなるメリットがあります 現在商談中の大型案件では、想定されていない規模のユーザーが一度に増えるので止めてほしいです パフォーマンステストを十分にできていないため、導入後トラブルが発生してお客様にご迷惑をおかけする可能性があります 商談中の会社は我々にとっても大事なお客様なので、受注後に解約なされると心象が悪くなってしまい、今後再提案もしづらくなると思うので、1 か月ほど時間をください。その間に、安心して利用できるための準備が整う可能性があります 現在商談中のお客様から寄せられている要望を期間内に すべて 満たすようにリリースすることは不可能です 開発チームのリソースにも限りがあるので、できるだけお客様の期待に多く応えるためにも、重要度の高いものから進めたいので、必須のものを教えてください セキュリティレベルを向上させるため、製品の導入に数百万円の投資が必要です インシデントの発生確率はだいたいこのくらいで、仮に発生した場合、弁護士費用や失った収益、信用失墜などを考慮すると、大体数千万円〜数億円単位の損失が発生します 同様のセキュリティ事故を起こした事例としては○○社があり、このようなリスクを回避できるため、導入を進めたいです 方策 5: 説明すべき事項を整理する  プログラミング言語、フレームワーク、クラウドサービスのような技術選定、開発フローなどに関しては、 あえて CEO に説明していません (もし質問があればもちろん納得いくまで説明しています)。  なぜならば、 技術選定や開発方針については完全に CTO に権限移譲されている からです。エンジニア出身でない CEO にとって専門範囲外の質問や相談を受けても、適切な判断をすることは簡単ではなく、そのような相談がなされても困るでしょう。  仮に誤った判断をしてしまったら、CEO ではなく CTO が責任を持ってリカバリする方針にしているので、CEO にとっても本来やるべきことに集中して安心して気持ちよくビジネスの拡大を推進することができます。 まとめ  RevCommでは " コミュニケーションを再発明し、人が人を想う社会を創る " というミッションを達成するために、上記のような方策を通して、サービスをご利用いただいているお客様の期待に応えることはもちろん、エンジニアやデザイナーが気持ちよく働ける組織を作り、さまざまな工夫やアイデアを組み合わせて目標に向かって取り組んでいます。  ご興味を持った方は、採用中のポジションの詳細を、 RevComm 採用情報 で公開していますので、ぜひご覧ください。 hrmos.co
アバター
この記事は RevComm Advent Calendar 2022 の 24 日目の記事です。 はじめに クリアすべき 4 つのステップ 1. 個人のモチベーション向上 Will Can Need まとめ 2. パフォーマンス向上 条件 1: 強い意志 条件 2: リラックス 条件 3: 手順の明確なイメージ 条件 4: フィードバック 条件 5: ちょっとした混乱(緊張感) まとめ 3.企業の業績向上 まとめ 4. 社会への価値創造   まとめ 最後に はじめに 2022 年 10 月に RevComm にシニアエンジニアリングマネージャーとして入社した樋沼です。前職ではエンジニア組織と兼務でビジネス組織の責任者をしていました。 エンジニア組織を内側と外側から見てきましたが、「エンジニアがイキイキしていないと IT 企業は成長しない」という当たり前のことを改めて痛感しています。そして、企業のミッションを実現させるために「個人の意志」と「企業の理念」をつなぐには何が重要なのか、ようやくわかってきました。 本日は、両者をつなぐためにエンジニアリングマネージャーとしてできることについて書きます。これまで私が経験したことの棚卸しをし、これから RevComm で取り組んでいきたい内容をまとめます。 クリアすべき 4 つのステップ 最初に「個人の意志」と「企業の理念」の間をつなぐための、私が考える 4 つのステップを紹介します。 4 つのステップ これらは段階的にクリアしていくステップになりますが、離脱ポイントも多くあります。例えば個人のモチベーションが高まっても、集中できる環境が整わなければパフォーマンスが発揮できないどころか、モチベーション低下に陥ります。 また、全エンジニアのパフォーマンスが最高であったとしても、ビジネスに直結する開発案件を最適に割り振っていなければ、企業の業績が上がらないという結末もありえます。 後半の「3. 企業の業績向上」や「4. 社会への価値創造」は、エンジニアリングマネージャーが直接影響を及ぼすことは困難な領域ですが、ビジネス視点や経営視点を意識することで最後まで到達できると考えています。 1. 個人のモチベーション向上 まずは教科書的な話になってしまいますが、個人の「やりたいこと = Will」と「できること = Can」を確認し、「求められていること = Need」と突き合わせます。 Will 仕事に対する考え方や譲れない価値観は個人ごとに千差万別で、それは他人に否定されずに尊重されるべきコアなものです。最初にこの Will を丁寧にヒアリングして正しく理解することに努めます。 一括りにエンジニアといっても「技術を手段としてビジネスに貢献したい」や「技術の習得を追求したい」などタイプはさまざまです。RevComm では、等級制度のグレードを段階的に上がっていく過程で、エンジニアはマネジメント (M) かプロフェッショナル (P) でキャリアパスを選択することになります。 Can 次に「これまで何をやってきたか」「これから伸ばしたい強みは何か」を確認します。言語化できる顕在化した強みもありますが、ヒアリングの途中で本人が気付いたり、まだ潜在的である強みをマネージャーが感じ取ることもあります。 RevComm のエンジニアは、各プロジェクトに参画しつつ、職能(フロントエンド/バックエンド/インフラなど)チームにも所属する「マトリクス組織」で業務を行います。自分が伸ばしたい強みを磨くために、マネージャーに相談して新しいチャレンジに取り組むケースもあります。 Need 個人の Will と Can を踏まえた上で、ビジネスサイドからのタスク (Need) を割り振ることになります。ただ、チームがどんなメンバー構成であっても、タスク自体は基本的には変わりません。 ここでマネージャーは、個人の Will や Can に対する意味づけを添えてタスクを渡すことが重要になります。タスクの背景/目的/期待値/得られる経験値など、相手に合わせてタスクの渡し方を工夫することで、個人のモチベーション向上につなげることができます。 まとめ やりたいことを周囲に話すと、誰かがそれを聞いて話が組織内を伝わり、巡り巡って自分の元に機会が訪れることがよくあります。たった数か月でも、RevComm 内で何人もの Will が伝わって実現されていくのを見ているので、改めて“意志の芽”を大切にしていきたいと思います。 2. パフォーマンス向上 エンジニアが高いモチベーションを維持したまま、期間中のパフォーマンスを高めるには、集中できる業務環境が重要になります。その際に、私は「フロー (Flow)」と呼ばれる心理状態に着目しています。 フローとは:時間を忘れるほど目の前のことに夢中になって、集中している状態 ※世羅侑未『3 倍のパフォーマンスを実現するフロー状態 魔法の集中術』(総合法令出版、2019年)より フローな状態になるための条件は以下の 5 つあると言われています。(諸説あり) 条件 1: 強い意志 大前提として、個人がそのタスクに取り組む意志が必要です。前述の「個人の意志」を理解し、そのタスクの目的や自分の成長にとっての意味合いを納得している状態を整えます。 条件 2: リラックス 業務に集中して取り組む上で、力まず平常心を保てる状態が必要です。まず、会社として働きやすい環境を提供できるかが重要ですが、RevComm ではフルリモート/フルフレックス制を採用しています。また、ミーティングは極力短い時間で終わらせるようにするなどで、業務に集中できる時間を確保できるようにしています。 マネージャーとしては、1 on 1 や懇親会で何でも話せる機会をつくり、心理的安全性が構築できているか、本音を話してくれているか声色や表情にも注意を払います。 条件 3: 手順の明確なイメージ どこから手をつけていいかわからないタスクを渡されると頭がスタックしてしまうものです。頭の中である程度の業務フローが展開できれば、スムーズに取り組むことができます。 タスクの難易度は本人のスキルより少し高いハードル設定が望ましいため、どこまで問題を解きほぐした状態で相手に渡すか、マネージャーの見極めがポイントになります。 条件 4: フィードバック 業務に取り組み始めたら、その方向性やスピード感で問題ないか本人が随時確認できることが必要です。 定量的には、作業の進捗率が可視化されたり、顧客の利用状況など KPI がエンジニアでもすぐに確認できる環境を整えることができます。また、定性的には、1 on 1 で順調であることや改善点を伝えることで、安心して業務できる状態を維持します。ただ、マネージャーが改善点を伝える際は、答えを伝えるよりも本人に気付きを促すほうが効果的です。自分の口で対策を言語化できるように、壁打ちの相手になるのが理想的だと思います。 条件 5: ちょっとした混乱(緊張感) 条件 2 のリラックスは必要ですが、度が過ぎると緊張感のない惰性の作業時間になってしまいます。組織の中には「ちょっとした緊張感」があることが望ましいと考えています。 そのため、正論や理想論など思ったことは立場を越えて主張するように投げかけています。私もチーム内からの突き上げを受けて、自分のリミッターを外して行動できるように成長できたという体験があります。組織内で相互にプレッシャーをかけられる関係性が、ちょっとした混乱=緊張感をもたらします。 まとめ パフォーマンスを向上させる上で権限移譲は重要な要素です。RevComm では、マネージャーはタスクの背景 (Why) と開発要件 (What) を明示しますが、実装方法 (How) はチームやエンジニアに一任されます。そして工数見積もりの結果、目標期日 (When) が要件と合わない場合は、優先順位の組み換えをビジネスサイドと調整したり、人員の増強などを全力で後方支援しています。 3.企業の業績向上 個人のパフォーマンスが最大限に発揮されたら、企業の業績に連動されるべきです。ただ、それは業績につながる業務がタスクとして割り振られ、適切な目標値を達成していることが前提となります。 企業の業績はPdM (Product Manager) や PMM (Product Marketing Manager) などビジネスサイドが担うため、エンジニア組織のマネージャーは基本的に責任の範疇外です。ただ、PdM や PMM に対して企業の売上予算がどう因数分解されビジネスサイドがどんな KPI として担っているか、また各開発タスクはどの KPI を期待されているかを確認することはできます。 PM (Project Manager)を含めたエンジニアリングマネージャーは、期初に行われるキックオフなどで共有される目標や KPI を確認するだけでなく、期中も PdM と密にコミュニケーションをとって、エンジニアの OUTPUT が業績につながることを意識する必要があります。 ちなみに、「戦略」と「戦術」のよしあしの組み合わせで、絶対に回避しなければならないパターンはどれだと思いますか? 最もよい状態は A であるのは間違いないですが、最も悪い状態は D でなく C になります。「よい戦略は正しい方向に動く」「よい戦術は遠くに動く」と捉えると、適切でない方向に遠く動いた分だけコストもかかり修正範囲も大きくなります。 まとめ エンジニアリングマネージャーは貴重なエンジニアの工数に責任を持っている立場です。ビジネス面を任せきりにするのでなく、戦略や戦況を読み取り PdM/PMM と議論できるビジネススキルも習得することが重要だと考えます。 4. 社会への価値創造   会社の業績が向上したらその規模の分だけ、社会への価値創造のインパクトが高まります。エンジニアリングマネージャーができることは、企業理念や経営の描くビジョンを自分の言葉で言語化して、相手の価値観に合わせて伝えることだと思います。 RevComm では『コミュニケーションを再発明し、人が人を想う社会を創る』という理念を掲げています。私は、企業活動におけるコミュニケーション不足やコミュニケーションロスを、定例会議、1 on 1、入社面接、カスタマーサポート、ユーザーヒアリングなど様々な場面で実感してきました。これらの内容が解析され相互の意思疎通が改善できたら、大幅な生産性の向上につながると確信して RevComm への入社を決めました。 また、『人が人を想う社会を創る』という表現も、“企業が社員により最適な環境を提供する”や“企業が顧客により便利なサービスを提供する”など、さまざまなシーンに広げて世界観を描くことができます。 時にはマネージャーも自分自身の Will と企業の理念を重ねてみる機会を持つのがよいと思います。 まとめ 企業の MVV (Mission/Vision/Value) は、日常の業務の中では忘れられがちです。ただ、壁にぶつかったときや企業内で対立軸が生じたときに、一度立ち止まって「この会社の存在意義は何か?」「自分は何をやり遂げたくてこの会社にいるのか?」を見つめ直すと解決策が見えるものです。 そんな時にマネージャーが「企業の理念」が実現したいビジョンを自分の言葉で具現化して、「個人の意志」の延長線上に重ねて示せれば、価値創造につながる推進力が生まれるはずです。 最後に エンジニアリングマネージャーは、エンジニアが持つ能力を最大限に発揮させ、それを業績や企業理念の実現につなげる重要な役割を担っています。偉そうなことを語ってきましたが、私も自分で書いた内容を実行するように精進していきたいと思います。 この記事をきっかけに RevComm の企業理念に共感してくださった方がいらっしゃいましたら、ぜひご応募ください。 www.revcomm.co.jp
アバター
この記事は RevComm Advent Calendar 2022 の 22 日目の記事です。 はじめに こんにちは、バックエンドエンジニアのまつどしんたろうです。 私は最近 Rust で Slackbot を作っています。 そのBotは、 @Slack名 ++ と投稿すると、 Thank you @Slack名 (counter: 1) と返答してくれます。 また、もう一度 @Slack名 ++ と投稿すると、 Thank you @Slack名 (counter: 2) となり、感謝された数だけ counter が増えていきます。 この Bot は弊社の制度である 15% ルールを使って開発しました。 15% ルールとは、業務外の活動への積極的な関与を推奨するために、業務時間のうち 15% はコア業務外の活動に取り組んで良いというルールです。 このルールのおかげで、日々、技術力の幅を広げることができています。 今回は、Slackbot にオウム返しをしてもらうところまでを記事にしていきます。 モチベーション 世のため人のために行動するというカルチャーをさらに根付かせたい フルリモートの環境でも感謝の気持ちを気軽に表現できるようにしたい という思いから開発しました。 インスパイア この Bot は、PyConJP Staff 用の Slack に導入されていて、感動しぜひ社内にも取り入れたいと思い開発をはじめました。 PyConJP 2022 でも紹介されているのでぜひご覧になってみてください。 https://2022.pycon.jp/timetable?id=ELUNPR 構成 業務では主に Python を使っていますが、触ってみたいという理由で Rust を採用しています。 今回はお試しのため、EC2 に環境を構築します。 EC2 (Amazon Linux) Rust Axum 今回、出来上がるコードはこちらです。 Cargo.toml [package] name = "slackbot" version = "0.1.0" edition = "2021" publish = false [dependencies] axum = "0.6.1" tokio = { version = "1.0", features = ["full"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = [ "std", "env-filter" ] } dotenv = "0.15.0" reqwest = { version = "0.11", features = ["json"] } .env VERIFICATION_TOKEN= BOT_USER= BOT_USER_OAUTH_TOKEN= main.rs use axum :: { http :: StatusCode, routing :: {get, post}, Router, }; use std :: net :: SocketAddr; use dotenv :: dotenv; mod slackbot ; use crate :: slackbot :: slackbot; #[tokio::main] async fn main () { init ().await; let app = Router :: new () . route ( "/" , get (handler)) . route ( "/slackbot" , post (slackbot)); let addr = SocketAddr :: from (([ 127 , 0 , 0 , 1 ], 8080 )); tracing :: debug! ( "listening on {}" , addr); axum :: Server :: bind ( & addr) . serve (app. into_make_service ()) .await . unwrap (); } async fn init () { tracing_subscriber :: fmt () . with_max_level ( tracing :: Level :: DEBUG) . init (); dotenv (). ok (); } async fn handler () -> (StatusCode, String ) { ( StatusCode :: OK, String :: from ( "Hello, World!" )) } slackbot.rs use std :: env; use axum :: { http :: StatusCode, Json}; use serde :: {Deserialize, Serialize}; #[derive( Debug , Deserialize)] pub struct Request { token: String , challenge: Option < String > , event: Option < Event > , } impl Request { fn is_initialize ( & self ) -> bool { self .challenge. is_some () } } #[derive( Debug , Deserialize)] struct Event { channel: String , user: String , text: String , thread_ts: Option < String > , } impl Event { fn from_bot ( & self , bot_user: String ) -> bool { self .user == bot_user } } #[derive( Debug , Serialize)] struct PostBody { text: String , channel: String , thread_ts: Option < String > , } #[derive( Debug , Serialize)] pub struct Response { ok: bool , challenge: Option < String > , } pub async fn slackbot ( Json (req): Json < Request > ) -> (StatusCode, Json < Response > ) { tracing :: info! ( "slackbot" ); // Validate let verification_token = env :: var ( "VERIFICATION_TOKEN" ). expect ( "VERIFICATION_TOKEN must be set" ); if req.token != verification_token { tracing :: warn! ( "AuthenticationFailed, token: {}" , req.token); let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } // Verify from Slack if req. is_initialize () { let res = Json (Response { ok: true , challenge: req.challenge, }); return ( StatusCode :: OK, res); } // Validate match & req.event { Some (event) => { let bot_user = env :: var ( "BOT_USER" ). expect ( "BOT_USER must be set" ); if event. from_bot (bot_user) { tracing :: debug! ( "From happy bot" ); let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } } None => { let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } } // Execute let event = req.event. unwrap (); let post_body = PostBody { text: event.text, channel: event.channel, thread_ts: event.thread_ts. map ( | x | x), }; post_request (post_body).await; let res = Json (Response { ok: true , challenge: None , }); return ( StatusCode :: OK, res); } async fn post_request (post_body: PostBody) -> reqwest :: Result < () > { tracing :: info! ( "slackbot__post_request" ); let url = "https://slack.com/api/chat.postMessage" ; let bot_user_oauth_token = env :: var ( "BOT_USER_OAUTH_TOKEN" ). expect ( "BOT_USER_OAUTH_TOKEN must be set" ); let client = reqwest :: Client :: new (); let response = client . post (url) . header ( reqwest :: header :: AUTHORIZATION, format! ( "Bearer {}" , bot_user_oauth_token), ) . json ( & post_body) . send () .await ? ; Ok (()) } 準備① Slack API の App 作成 1/4 App 作成 Slack api の Your Apps ( https://api.slack.com/apps?new_app=1 ) の Create New App から App を作成します。 今回は From scratch から作成しました。 2/4 Token 取得 取得した Token は .env ファイルに記載します。 Basic Information ( https://api.slack.com/apps? ) > App Credentials の Verification Token を取得します。 OAuth & Permissions ( https://api.slack.com/apps/XXXXXXXXXXXX/oauth? ) の Reinstall to Workspace からチャンネルにインストールします。 Bot User OAuth Token が現れるので取得します。 3/4 権限の設定 OAuth & Permissions ( https://api.slack.com/apps/XXXXXXXXXXXX/oauth? ) の Scope Bot Token Scopes と EventSubscriptions ( https://api.slack.com/apps/XXXXXXXXXXXX/event-subscriptions? ) の Subscribe to bot events に 必要な権限を追加します。 4/4 Slack チャンネルへの追加 導入したい Slack チャンネルに行き、Slack チャンネル名をクリックし、インテグレーションからアプリを追加します。 実装① Rust のインストールと Rust で POST を受け取れるように設定 1/5 Rust をインストール rustup を使ってインストールします。詳細は 公式ドキュメント を参照してください。 2/5 Axum の Hello world Axum は Rust の Web フレームワークのひとつです。Axum が豊富に example を提供してくれているので、これを元にして Bot の開発をしていきます。 まずは、最小構成で Hello World をやってみます。 https://github.com/tokio-rs/axum/tree/main/examples/hello-world をclone Cargo.toml の axum を axum = "0.6.1" に変更 port を 8080 に変更 (Optional) REST API にしたいので hundler を修正 (Optional) cargo run でサーバーを立ち上げます curl localhost:8080 を叩くと、 Hello, World! が返ってきます main.rs - use axum::{response::Html, routing::get, Router}; + use axum::{http::StatusCode, routing::get, Router}; use std::net::SocketAddr; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler)) - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); println!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } - async fn handler() -> Html<&'static str> { - Html("<h1>Hello, World!</h1>") + async fn handler() -> (StatusCode, String) { + (StatusCode::OK, String::from("Hello, World!")) } 3/5 challenge を返す Slack API に URL を登録する際に、有効な URL だと Slack に知らせる必要があります。 POST で verification_token と challenge というランダムな値が送られてくるので、そのまま challenge を返してあげることで有効だと判断されます。 serde クレート *1 を追加します。serde はシリアライズ/デシリアライズをしてくれるクレートです。 別ファイルでメソッドを追加します。 slackbot.rs use axum :: { http :: StatusCode, Json}; use serde :: {Deserialize, Serialize}; #[derive( Debug , Deserialize)] pub struct Request { token: String , challenge: Option < String > , } #[derive( Debug , Serialize)] pub struct Response { ok: bool , challenge: Option < String > , } pub async fn slackbot ( Json (req): Json < Request > ) -> (StatusCode, Json < Response > ) { let res = Json (Response { ok: true , challenge: req.challenge, }); ( StatusCode :: OK, res) } 3. main.rs に新しく POST を受け取れる Route を追加します。 main.rs - use axum::{http::StatusCode, routing::get, Router}; + use axum::{ + http::StatusCode + routing::{get, post}, + Router, + }; use std::net::SocketAddr; + mod slackbot; + use crate::slackbot::slackbot; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler)) + .route("/slackbot", post(slackbot)); let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); 4/5 .env を導入する 「準備① 2/4 Token 取得」で取得できた値 .env ファイルに設定します。読み取りには dotenv クレートを使います。 main.rs use std::net::SocketAddr; + use dotenv::dotenv; mod slackbot; use crate::slackbot::slackbot; (中略) async fn init() { tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .init(); + dotenv().ok(); } 5/5 tokenでバリデートする。 「4/5 .env を導入する」で設定した値でバリデートします。 slackbot.rs + use std::env; + use axum::{http::StatusCode, Json}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, } #[derive(Debug, Serialize)] pub struct Response { ok: bool, challenge: Option<String>, } pub async fn slackbot(Json(req): Json<Request>) -> (StatusCode, Json<Response>) { + // Validate + let verification_token = + env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); + + if req.token != verification_token { + tracing::warn!("AuthenticationFailed, token: {}", req.token); + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } let res = Json(Response { ok: true, challenge: req.challenge, }); (StatusCode::OK, res) } 準備② URL の登録 EC2 で適当なインスタンスを立ち上げて、SSH と HTTP で通信ができるように設定します。 EventSubscriptions ( https://api.slack.com/apps/XXXXXXXXXXXX/event-subscriptions? ) で、上記で準備した EC2 の URL (例: http://ec2-12-345-678-90.ap-northeast-1.compute.amazonaws.com/slackbot) を RequestURL の欄に入力します。 Slack API から送られてきた challenge を返せていれば Verified になります。 実装② Bot から Slack に投稿する 1/2 Slack からの入力を受け取る デシリアライズ Event を追記します。 slackbot.rs #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, + event: Option<Event>, } + #[derive(Debug, Deserialize)] + struct Event { + channel: String, + user: String, + text: String, + thread_ts: Option<String>, + } 2/2 Bot からオウム返しする reqwest クレートで Slack に 投稿します。 Botからの投稿に返答してしまうと無限ループになってしまうため、無視します。 slackbot.rs #[derive(Debug, Deserialize)] struct Event { channel: String, user: String, text: String, thread_ts: Option<String>, } + impl Event { + fn from_bot(&self, bot_user: String) -> bool { + self.user == bot_user + } + } (中略) // Validate let verification_token = env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); if req.token != verification_token { tracing::warn!("AuthenticationFailed, token: {}", req.token); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } + // Validate + match &req.event { + Some(event) => { + let bot_user = env::var("BOT_USER").expect("BOT_USER must be set"); + if event.from_bot(bot_user) { + tracing::debug!("From happy bot"); + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } + } + None => { + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } + } ここでデバッグ用途に tracing クレートを追加しました。 2. リクエストを challenge のあるなしで分岐させます。 slackbot.rs #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, event: Option<Event>, } + impl Request { + fn is_initialize(&self) -> bool { + self.challenge.is_some() + } + } (中略) // Validate let verification_token = env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); if req.token != verification_token { tracing::warn!("AuthenticationFailed, token: {}", req.token); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } + // Verify from Slack + if req.is_initialize() { + let res = Json(Response { + ok: true, + challenge: req.challenge, + }); + return (StatusCode::OK, res); + } // Validate match &req.event { Some(event) => { let bot_user = env::var("BOT_USER").expect("BOT_USER must be set"); if event.from_bot(bot_user) { tracing::debug!("From happy bot"); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } } None => { let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } } + let res = Json(Response { + ok: true, + challenge: None, + }); + return (StatusCode::OK, res); 3. Slack への POST リクエストを追加します。 slackbot.rs impl Event { fn from_bot(&self, bot_user: String) -> bool { self.user == bot_user } } + #[derive(Debug, Serialize)] + struct PostBody { + text: String, + channel: String, + thread_ts: Option<String>, + } #[derive(Debug, Serialize)] pub struct Response { ok: bool, challenge: Option<String>, } (中略) + // Execute + let event = req.event.unwrap(); + + let post_body = PostBody { + text: event.text, + channel: event.channel, + thread_ts: event.thread_ts.map(|x| x), + }; post_request(post_body).await; let res = Json(Response { ok: true, challenge: None, }); return (StatusCode::OK, res); } + async fn post_request(post_body: PostBody) -> reqwest::Result<()> { + tracing::info!("slackbot__post_request"); + + let url = "https://slack.com/api/chat.postMessage"; + let bot_user_oauth_token = + env::var("BOT_USER_OAUTH_TOKEN").expect("BOT_USER_OAUTH_TOKEN must be set"); + + let client = reqwest::Client::new(); + let response = client + .post(url) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", bot_user_oauth_token), + ) + .json(&post_body) + .send() + .await?; + + Ok(()) + } できたコードを EC2 にデプロイして、実際に Slack で入力をすると、オウム返しができるようになりました。設定したチャンネルで何か投稿してみてください。 まとめ そのうち以下のようなテーマも学習していきたいと考えています。 ファイル分割 ORM の導入 エラーハンドリング また、今後実運用を見据えての検証として、 test 導入 linter formatter 導入 などを考えております。 面白かったところ 所有権 Option、Some 構造体、型 はまったところ ファイル、ワークスペース分割のルールが一番はまりました。 面白かったところとはまったところは紙一重で、Python とは書き方が違うところかなと思います。 上記以外にも細かいつまずきが多かったですが、とても楽しく学ぶことができました。 Python は書きやすさを重視した言語だと言うことを改めて実感しました。普段あまり考えずに書いていたところを見直す良い機会となりました。 *1 : Rust では Python のパッケージに相当するものをクレートと呼びます。
アバター
この記事は、 RevComm Advent Calender 21 日目の記事です。 RevComm の宇佐美です。認証基盤開発チームで開発および Project Manager を担当しています。 OpenID Connect (OIDC) を利用して Cognito user pool と外部の Identity Provider (IdP) の連携を行う方法について調べる機会があったので、まとめてみました。 IdP には Azure AD (Microsoft が提供するクラウドベースの ID およびアクセス管理サービス)を使います。一部 Azure AD 特有の要件がありますが、基本的には OIDC での連携はほぼ同じ流れでできると思います。 この記事について この記事のゴール Cognito user pool と外部 IdP を OIDC で連携して、IdP へのサインインで Cognito user pool の既存のユーザーとして Cognito から各種 token を払い出す方法をまとめます。 前提知識 OAuth 2.0, OIDC の概要 Cognito の基本的な仕様 Azure AD の設定方法 AWS CLI の一般的な知識 注意事項 この記事で記載している ID 連携の手順はあくまで動作するために必要最低限のものであり、本番環境での運用を想定したものではありません。 実務では、CSRF や Open redirect などの OAuth 2.0 / OIDC における既知の脆弱性に対して対策を行う *1 ことが必要で、Authorization code flow における PKCE (Proof Key for Code Exchange) などのベストプラクティスに対応することも推奨されます。 本文 外部 IdP での ID 連携を行うことの利点 これから詳細に説明していくとおり、連携形式を問わず IdP との ID 連携を行うためにはそれなりの手間がかかりますが、連携を行うことによるメリットはどこにあるのでしょうか。 ID 連携を行う大きな利点としては以下のような点が挙げられます。 ユーザー側の利点 ユーザー情報の統一管理 ユーザーを IdP で一元的に管理できるため、複数のサービスごとに ID やパスワード、MFA デバイスなどの認証情報を管理する必要がなくなります。IdP でサインイン状態が継続していればそれぞれのサービスに都度サインインする必要がないので、UX も向上します。 セキュリティ向上 認証情報をサービスごとに管理していると、どうしてもパスワードが使いまわされたり、サービス提供者側のセキュリティインシデントなどによる漏洩が起きたりするリスクがあります。IdP でのサインインに統一しておけば、これらのリスクを軽減することができます。 認証手段 IdP 側で MFA や FIDO などの認証手段を提供している場合、サインインしたい対象のサービスが対応していなくてもこれらを利用することができます。 アプリケーション提供側の利点 認証情報管理の委譲 アプリケーション提供者としても、パスワードなどの認証情報を管理することは極力避けたいものです *2 。データベースに保管する際の不可逆なハッシュ化やログのマスキングなどの基本的な対策に加え、アプリケーション内で認証情報を扱うあらゆるところで特別な配慮が必要になります。IdP に認証情報の管理を委譲してしまえば、これらのうち一部はアプリケーション内で管理する必要がなくなります。 Cognito user poolの概要 Cognito は AWS が提供するウェブおよびモバイルアプリの認証、承認、およびユーザー管理サービスです。 Cognito user pool は、Cognito 内でユーザーを管理するためのディレクトリに相当するもので、ユーザーのサインアップおよびサインイン、IdP 経由でのサインイン、簡易的な Web UI (Hosted UI) などの機能を提供しています。 Cognito user pool にユーザー ID とパスワードでサインインすると、各種 token が払い出されます。 Cognito には Identity pool というものもあって紛らわしいですが、これは本記事で実現しようとする IdP 経由での user pool へのサインインとは異なる機能なので注意が必要です。 OpenID Connect を使った ID 連携の概要 OpenID Connect (OIDC) は、OAuth 2.0 をベースとして構築された認証プロトコルです。つまり OIDC は OAuth 2.0 の拡張として位置づけられますが、OAuth 2.0 が認可 (authorization) のプロトコルであるのに対して、OIDC は認証 (authentication) のためのプロトコルです。 平たく言うと、OAuth 2.0 はあるユーザーがどのリソースにアクセスできるかを制御するための仕様であるのに対し、OIDC ではこれに加えてそのユーザーが誰であるかという認証情報の提供についても規定しています。 そのため、今回のように既存の Web アプリケーションにおけるユーザー管理機能と統合して、ID 連携を行うことができます。具体的には、以下のような流れで認証を行うことになります。 Web アプリケーションから IdP に認証リクエスト ブラウザ上で認証画面にリダイレクト ユーザーが IdP の認証情報を入力 IdP で認証成功 token 払い出し ID token をデコードしてユーザー情報を取得 ユーザー情報を使って Web アプリケーションにサインイン この流れは非常にシンプルに書いたもので、実際にはセキュリティ的な対応など途中でさまざまな処理が入りますが、大枠としてはこのような流れになります。 Azure AD App のセットアップ それでは実際に Azure AD を IdP として、Cognito user pool との連携を行う流れを見ていきます( Microsoft の公式ドキュメント )。 なお、各サービスの画面キャプチャーは 記事作成時点の 2022 年 12 月のものです。 事前に必要なものは以下のリソースです。 Cognito user pool User pool の App Client User pool のユーザー Azure AD のテナント(ディレクトリ) まずは Azure の Portal にサインインして、ホーム画面から以下のように進みます。 Manage Azure Active Directory App registrations + New registration App 登録画面 ここでは App の Name など最低限の情報のみ入力します。 Redirect URI は認証リクエストの redirect_uri として指定できる URI です。一旦 http://localhost だけで問題ありません。 Register を押して App が作成されたら、その App を選択して詳細画面を見てみましょう。 ここで必要となるのは Application (client) ID なので、値をコピーしておきます。 作成した App 画面上部の Endpoints を選択すると、OIDC や SAML での連携に必要となる各種エンドポイントをまとめて確認できます。この中の OpenID Connect metadata document に、OIDC で利用するエンドポイントの情報があります。 次に、Client secret を作成します。 左ペインから Certificates & secrets を選択して、+ New client secret から作成します。 作成された Client secret のうち、value を使用するので忘れずにコピーしておきましょう。一度ページ遷移すると、この後は value が表示されませんので注意です。 次に API 権限の設定を行います。 同じく左ペインから API permissions を選択し、+ Add a permission から権限追加ができます。Microsoft Graph の Delegated permissions から、以下の項目にチェックを入れます。 email openid User.Read これで Azure AD 側での準備はひとまず終わりです。 Cognito user pool のセットアップ ここからは Cognito 側の設定に移ります。 まず、Cognito user pool を作成します( AWS の公式ドキュメント )。設定は一般的なもので問題ありませんが、Cognito user pool sign-in options は Email を指定し、 Required attributes に name を追加しておきます。 User pool ができたら、Azure AD に存在するユーザーの Email でユーザーを作成しておきます。 次に App integrations から App client を作成します。 Hosted UI を有効化し、App client の App type は Public client にしておきます。 Allowed callback URLs には http://localhost を設定します。これは Cognito での認証リクエストが成功した後、Authorization code を受け取るエンドポイントになります。 Authentication flows の USER_PASSWORD_AUTH も有効化して、OpenID Connect scopes には Email OpenID Profile を設定します。 App client ができたら、作成したユーザーで Hosted UI からサインインができることを確認しておきましょう。サインインが成功するとパスワード変更を促されるので、ここで新しいパスワードを設定します。 Hosted UI を起動してサインイン パスワード変更が終わると、Callback URL に指定した localhost にリダイレクトされるはずです。 あわせて AWS CLI からも認証できることを確認します。 aws cognito-idp initiate-auth \ --auth-flow USER_PASSWORD_AUTH \ --auth-parameters USERNAME = < your-user-name > , PASSWORD = < your-password > \ --client-id < app-client-id > client-id に指定するのは、上記で作成した App client の Client ID です。 うまくいけば、レスポンスの AuthenticationResult の中に AccessToken や IdToken 、 RefreshToken が入っているのが確認できるはずです。 これで、Cognito の Email とパスワードでサインインできることを確認できました。 Azure AD と Cognito user poolの連携 ここからは用意した Cognito user pool のユーザーで Azure AD 経由のサインインを試してみます。ゴールは、Cognito の Email とパスワードを使用せずに Azure AD の認証を行うことで Cognito から取得したのと同じ token が得られることです。 まず、Cognito user pool の Sign-in experience タブで Federated identity provider sign-in から Add identity provider を選択します。 Identity provider の追加 Identity provider の選択肢は OpenID Connect (OIDC) を選び、Set up OpenID Connect federation with this user pool の各項目にはそれぞれ以下の値を入れます。 Provider name 名称。Hosted UI のボタンに表示されます。 Client ID Azure AD で作成した App の Client ID Client secret Azure AD で作成した App の Client secret (value) Authorized scopes email profile openid Identifiers 一意の ID(任意) Issuer URL Azure AD で作成した App の OpenID Connect metadata document リンク先 issuer の値 Map attributes between your OpenID Connect provider and your user pool name: name email: preferred_username これで Cognito user poolでの IdP 作成は完了です。 次に、この IdP を App client から利用できるようにするため、App integration タブから作成した App client を選択します。 Hosted UI の Edit を押下し、Identity providers の項目に先ほど作成した Identity Provider を追加します。 作成した IdP にチェックを追加 Save してから再度 Hosted UI を開いてみると、追加した Identity provider のボタンが表示されているはずです。これで Cognito と Azure AD の OIDC 連携はとりあえずできている状態になります。 Sign in with your corporate ID に追加した IdP が表示される ここまで来たら、一旦 Azure AD のポータルに戻って App の設定を追加します。 左ペインから Authentication を選択して、Redirect URIs に以下の URL を追加します。 https://<your-cognito-domain>/oauth2/idpresponse ドメインは Hosted UI のドメインと同一です。 このエンドポイントでは以下のようなことが行われます *3 。 IdP での認証が終わった後に Authorization code を受け取る IdP に token request を送信 受け取った Access token で IdP の Userinfo エンドポイントにリクエスト Userinfo のレスポンスを元に Cognito user pool のユーザーにマッピング あらためて Hosted UI を開き、IdP 経由でサインインしてみましょう。うまくいけば、 http://localhost/?code=<cognito-authorization-code> のようにリダイレクトされて、Cognito の Authorization code が取得できるはずです。 この Code を使って、Cognito に以下の token request を送ると Cognito 発行の token が得られます *4 。 curl --location --request POST ' https://<your-cognito-domain>/oauth2/token ' \ --header ' Content-Type: application/x-www-form-urlencoded ' \ --data-urlencode ' code=<cognito-authorization-code> \ --data-urlencode ' redirect_uri =http://localhost \ --data-urlencode ' client_id=<cognito-client-id> ' \ --data-urlencode ' grant_type=authorization_code ' --data-urlencode ' scope=openid email profile ' 得られた token のうち、 id_token を jwt.io などでデコードしてみましょう。 Cognito の通常の Claim に加えて、 identities という Claim に IdP の情報が入っているのがわかります。 Azure AD でのシングルサインオン これで OIDC での ID 連携は完成、と言いたいところですが、前段で Cognito の Authorization code が適切に取得できている場合、User pool に新しいユーザーが作成されているはずです。 Confirmation status が External provider となっていて、外部 IdP によって作成されたユーザーということがわかります。 同じメールアドレスで新しいユーザーができてしまう この状態だと、既存のユーザーと IdP 経由で作成されたユーザーが同じ Email アドレスなのに別ユーザーと認識されてしまいます。これでは、この記事のゴールだったはずの「既存の Cognito user pool のユーザーとして Azure AD ユーザーでサインインする」という点が実現できていません。 既存ユーザーとして IdP でのサインインを行うためには、AWS CLI で admin-link-provider-for-user という API を使います *5 。 事前に Azure AD から ID token を取得しておく必要がありますので、 ドキュメントに沿って取得しておきましょう 。 ID token を取得できたらこれをデコードします。External provider で作成されたユーザーは一度削除してから、以下を実行します。 aws cognito-idp admin-link-provider-for-user \ --user-pool-id < your-user-pool-id > \ --destination-user ProviderName =Cognito, ProviderAttributeValue = < cognito-user-name > \ --source-user ProviderName = < idp-name > , ProviderAttributeName =Cognito_Subject, ProviderAttributeValue = < user-sub-for-idp > cognito-user-name Cognito での User name idp-name Cognito の Identity provider として登録している IdP の name user-sub-for-idp IdP でのユーザーの識別子(ID token の sub) 紐付けが成功すると、Cognito 内で User pool のユーザーと IdP のユーザーが同一だと認識してくれるので、IdP 経由のサインインで既存ユーザーとしての token を払い出してくれます。 なお、一度紐付けされたユーザーを再度紐付けしようとすると、すでに同一のユーザーとしてみなされているため、 InvalidParameterException が発生します。紐付けを解除するためには、 admin-disable-user を使います *6 。 紐付けができたら再度 Hosted UI を開いて、IdP 経由でサインインしてみましょう。 Redirect 先で取得できる Code を使って、Cognito に token request を送り、token を取得します。 取得した ID token をデコードすると、email や sub などの値が Cognito の既存ユーザーと同じであることが確認できます。 また、紐付け前に IdP サインインした時とは異なり、User pool に新しいユーザーが作成されていないはずです。紐付けを行ったことにより、IdP のサインインでも既存のユーザーとしてのサインインとしてみなされているためです。 あとはこの token をアプリケーション側の認証方式に応じて Authorization header などに使って、User pool のユーザーとして各種リソースにアクセスすることができます。 User pool には先ほどと違って新しいユーザーは作成されておらず、ユーザーの詳細を確認すると User attributes の identities に IdP と連携した情報が記録されているのがわかります。 Cognito 既存ユーザーの User attributes 以上で、Cognito の既存ユーザーとして Azure AD 経由での Cognito サインインができたことになります。 まとめ Cognito も Azure AD もそれぞれドキュメントは充実していて、よく読みながら手順通り進めていけば ID 連携を行うことは可能です。 一方で、OIDC で両者を連携する手順をウォークスルーした資料はあまりなく、最初にやや苦労する点がありました。特に、Cognito の idpresponse エンドポイントでは裏側で何が起きているのかわかりづらく、AWS Support に相談して回答を得られたことが助けとなりました。 本記事が、今後同じような開発を行う方にとって有益であれば幸いです。 おわりに RevComm では OAuth 2.0 や OIDC などの技術を活用した外部連携や認証基盤開発を行っています。興味がある方は、下記から採用情報をチェックしてみてください! www.revcomm.co.jp *1 : https://openid-foundation-japan.github.io/rfc6819.ja.html *2 : https://www.youtube.com/watch?v=8ZtInClXe1Q *3 : https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-flow.html *4 : https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/token-endpoint.html *5 : https://awscli.amazonaws.com/v2/documentation/api/2.1.21/reference/cognito-idp/admin-link-provider-for-user.html *6 : https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cognito-idp/admin-disable-user.html
アバター
この記事は、RevComm Advent Calender 20 日目の記事です。 はじめに こんにちは。服部 ( @keigohtr ) です。趣味は MLOps 調査とクラフトビールです。最近はエルゴノミクスキーボードを探し続けています。担当は RevComm Research 配下の開発部のエンジニアリングマネジメントです。本日は「RevComm Research 開発部ってどんな場所なの?」に答えたいと思います。 会社紹介 RevComm は電話営業や顧客応対を可視化する音声解析AI搭載型のクラウドIP電話「MiiTel(ミーテル)」を開発しています。 私の所属する RevComm Research は MiiTel のコアバリューである音声認識や話者分離、感情認識などの機能を開発しています。プロダクトは順調に成長し続けていまして、ユーザー数の増加に比例して解析対象の商談数も増え続けています。お客様が安心してサービスを使えるように、Research の開発部には機械学習サービスの高い安定性と高いスケーラビリティが求められています。 ML推論環境の変遷 RevComm Research では構成の大刷新を進めています。私が入社した当初(ちょうど 1 年前の 2021 年 12 月)の構成はざっくりと下図のようになっていました。EC2 を使ったシンプルな構成ですが、Worker に多くの処理(e.g. ロジック、機械学習処理)を押し込めていたことや、長年保守し続けてきた AMI に色々と限界を感じたことから、マイクロサービス化してコンテナベースの運用に切り替えようとしていた時期でした。 2021 年 12 月時点のおおまかな構成 そして下図が現在の構成です。移行作業としては、まず Worker に押し込めていたいくつかの処理をマイクロサービスとして分離しました。Worker の規模がある程度小さくなったので、現在は Worker をコンテナ化しています。ただし構成としてはまだまだ道半ばといったところです。 2022 年 12 月時点のおおまかな構成 現時点で目指している構成は下図になります。Worker を完全に分解してワークフローエンジンに乗せて管理します。ワークフローエンジンの恩恵をフルに享受するのが狙いです。ML モジュールは用途に応じてサービスとバッチを使い分け、コストとリソースの最適化を図ります。この構成は理想に対するベースであり、ここから更に DevOps や MLOps のベストプラクティスを導入していく予定を立てています。やりたいことが多いのでワクワクしています。 2022 年 12 月時点で目指しているおおまかな構成 DX の変遷 この 1 年間に整備した開発者の環境について紹介します。 モノレポの採用 RevComm Research には多くの ML モジュールがあります。私が入社した当初は Worker の中に全ての ML モジュールが内包されている状態でした。機械学習のライブラリも Tensorflow、PyTorch、sklearn と使っており、ライブラリの依存関係で身動きが取れない状態でした。そこで ML モジュールを Worker から分離してマイクロサービス化するというプロジェクトが進行しました。 Worker から分離したマイクロサービスはモノレポで管理しています。メリットは CICD のセットアップが一箇所で済む。 共有コンポーネント(e.g. Log Formatter, gRPC proto)の参照が簡単にできる。 共有コンポーネントを変更した時、依存する全てのコンポーネントのテストが簡単にできる。 チームの技術的な知見を集約できる。 トラブルシュートのときにここをみれば良いので、認知負荷を下げることができる。 ビルドシステムには pants build を採用しました。有名なビルドシステムには Bazel がありますが、RevComm Research は Python をメインに開発していたこともあり、Python との親和性の高さを評価して pants build に決めました。pants build によってモノレポの中の各プロジェクトやパッケージの依存関係が明らかになり、CI ではコードの変更箇所に関連するテストだけが実行されるようになりました。モノレポではコードの規模が大きくなるにつれてテスト時間やビルド時間が長くなるので、先行投資として最初期からビルドシステムを組み込みました。 その他の工夫としては、 CODEOWNERS の設定があります。CODEOWNERS は GitHub の機能で、特定のディレクトリに対して責任者を設定できます。執筆現在の時点で我々がモノレポで管理しているものは、Python で書かれた複数の ML モジュールと、各 ML モジュールで共有するパッケージ、そして ML モジュールを配信するために必要な Terraform リソースです。それぞれに対して CODEOWNERS を設定し、コードレビューでは CODEOWNERS の承認を必須としました。こうすることでエンジニアの Ownership を育むとともに、CODEOWNERS に任命されたエンジニアがリソースの中で守りたい一貫性を守れるようにしました。 例えば、「DDD を採用しているのに気づいたらドメインが漏れ出していた」という事態は CODEOWNERS の設定で避けやすくなります。 GitOpsの採用 モノレポの採用の部分で CI を整備しました。次は CD です。私が入社した当初は EKS にはたったひとつの ML モジュールのみが配信されており、そのリリースは手作業で Kustomize を実行していました。そしてチームで Kubernetes を触れるエンジニアは一人だけという状況で、つまりリリースができるのも一人だけという状況でした。CD に ArgoCD の検討はされていたものの運用には至っておらず、CD の自動化は急務でした。 CD は ArgoCD を採用しました。ArgoCD はチームで運用には至っていなかったものの、既にメンバーが検討したときの資産がいくつかあったこともあり、再整備をして運用することにしました。コードはモノレポで管理し、マニュフェストは別のレポジトリを用意し、マニュフェストレポジトリを ArgoCD で監視しています。マニュフェストレポジトリにある各 ML モジュールのイメージタグの更新は GitHub Actions で自動化しています。開発環境とステージング環境は ArgoCD の Auto Sync を有効にして、コードの変更を即座に環境に反映するようにしています。本番環境はまだテストが十分に自動化できていないこともあり Auto Sync を有効にできていないですが、将来的には sync windows を設定して営業時間外に自動的に更新できるようにする予定です。 執筆現在の時点で、ArgoCD は Pull 型の GitOps で使っています。Pull 型の GitOps を採用した理由はセキュリティ面でのメリットです。リリースするときに GitHub 側に EKS クラスタへのアクセス権限を持たせなくて済みます。 おわりに 今回紹介したのは機械学習の推論環境です。今回紹介した内容以外にも推論環境でやるべきことはまだまだあります。例えば A/B テスト、Canary release、Feature flag、Traffic shadow、etc。また、機械学習の学習環境もやりたいことがたくさんあります。RevComm Research では Engineering / DevOps / MLOps を積極採用していますので、皆様の応募をお待ちしています。 採用中のポジション hrmos.co
アバター
この記事は、RevComm Advent Calender 19 日目の記事です。 はじめに TL;DR CDK for Terraform とは 特徴 技術比較 実践 前提条件 環境構築 resources 作成 Deploy GCP認証 deploy内容の確認 終わりに はじめに こんにちは、株式会社 RevComm でフロントエンドチームでエンジニアをしている高橋( @katakana_33 )と申します。 今回は、私が RevComm に入って初めて見たソースコードが Terraform だった事と関連して、CDK for Terraform と GCP の連携について、実践した使用感を書きました。 RevComm の良さの1つとして、GitHub Organization 内の他プロダクトのリポジトリを見れる事でプロダクト全体の理解を深め、自身の専門職種外の技術スキルにも目を向けられる点があります。 RevCommのオンボーディング期間のその日のオンボードが終了した後 、私が初めて見たソースコードが担当するプロダクトの Terraform でした。未経験の技術だったこともあり、そこで今回は、CDK for TerraformとGCPの連携について調査と実践をしました。 TL;DR 0 から IaC を導入する場合は、メリットの方が大きい。 移行コストと導入必要性を考慮した際、移行しない事も選択肢の 1 つ。 CDK for Terraform とは CDK for Terraform by Hashicorp AWS CDKチームとHashicorp 社が共同開発 をしたツールで、 AWS CDK と Terraform の良い所を合わせたものです。 AWS CDK の開発者 kit を使用して、インフラリソースの定義とデプロイを行えます。 公式より以下概念図 特徴 HCL の学習を必要とせず、普段使用しているメジャーな言語で IaC を記述できる。以下がサポートされているプログラミング言語。 TypeScript、Python、Java、C#、Go。(2022 年 12 月時点) Terraform エコシステム全体にアクセスできて、各機能のテストを行える。 マルチプロバイダが可能な為、ベンダーロックインを回避できる。 技術比較 Terraform CDK CDK for Terraform HCLの学習 必要 不要 不要 マルチプロバイダ 可能 不可能(AWSのみ) 可能 参考文献(有志の記事を含む) 多い 多い 少ない(有志の記事が少ない、イレギュラーケースがまだ明るみになっていない可能性が高い) 実践 今回 は、web サービスでよく使いそうな GCP の以下の resources(本記事ではcloud infrastructure resources を指します。) を作成し deploy します。 Compute Network Compute SubNetwork Compute Disk IP address 前提条件 あらかじめ以下の環境を用意してください Terraform CLI Node.jsおよび npm v16 gcloud ( install 方法 ) GCP project( project の作成方法 ) Typescript 実行環境 環境構築 上記の環境を構築したら、CDK for Terraform 独自の project を構築します npm install --global cdktf-cli@latest // cdktf package install cdktf help // check install mkdir learn-cdktf && cd learn-cdktf // create workingDirectory cdktf init --template=typescript --local // create project(入力すると以下質問が出る) ? Project Name // 任意の名前 ? Project Description // 任意の説明 ? Do you want to start from an existing Terraform project? (デフォルトNo) ? Do you want to send crash reports to the CDKTF team? (デフォルトYes) ? What providers do you want to use? google 質問に答えると、以下の構成ファイル群が生成されます。 . ├── __tests__ ├── cdktf.json ├── help ├── jest.config.js ├── main.ts ├── package-lock.json ├── package.json ├── setup.js └── tsconfig.json GCP provider を使えるようにするには、このファイル群があるディレクトリにて以下のcommand を実行します。(GCP 以外の各種 provider が欲しい際は公式を参照ください。) npm install @cdktf/provider-google これで CDK for Terraform × Typescript × GCP の環境が構築できました。 resources 作成 構築する provider resources は、ルートディレクトリ直下の main.ts の Stack( Hashicorp:Stacks )に記述します。 以下は初期状態です。 TerraformStack を継承した MyStack が自動生成されています。 // Copyright (c) HashiCorp, Inc // SPDX-License-Identifier: MPL-2.0 import { Construct } from "constructs" ; import { App , TerraformStack } from "cdktf" ; class MyStack extends TerraformStack { constructor( scope: Construct , id: string ) { super( scope , id ); // define resources here } } const app = new App (); new MyStack ( app , "project-name" ); // 環境構築時に質問で回答したproject名がここに入る app.synth (); MyStack の constructor の中で provider の instance を生成します。 config 情報の project の value には、実存する GCP project 名を記入ください。 class MyStack extends TerraformStack { constructor( scope: Construct , id: string ) { super( scope , id ); new GoogleProvider ( this , "google" , { project: "project-name" , } ) // define resources here } } project 名を正確に書かずに deploy をした場合は以下の error がでます。 (project 名を hoge として deploy した結果) │ Error: Error creating Disk: googleapi: Error 400: Consumer 'projects/hoge' is invalid: Resource projects/hoge could not be found.. provider instance を作成したら、任意の resources を書きます。 今回は以下ソースコードの resources を作成し deploy します。 resources は constructor 内に定義します。 使用するGCP resourcesの使用方法は、公式リファレンスには殆ど記載がないので、 以下の GitHub のソースコードから使用する resources を import して、各 resources に必要な key と value を渡します。 GitHub: cdktf-provider-google class MyStack extends TerraformStack { constructor( scope: Construct , id: string ) { super( scope , id ); // define resources here const ZONE = "asia-northeast1-b" ; const PROJECT_ID = "katakana-id" ; // 任意のID 各resourcesに付与する const PROJECT_NAME = "katakana-tr" ; // GCP projectの名前 const PROJECT_PREFIX = "example" ; const PROJECT_PREFIX_NAME = ` ${ PROJECT_PREFIX } - ${ PROJECT_NAME } ` ; const PROJECT_PREFIX_ID = ` ${ PROJECT_PREFIX } - ${ PROJECT_ID } ` ; new GoogleProvider ( this , "google" , { project: PROJECT_NAME , } ) const network = new ComputeNetwork ( this , ` ${ PROJECT_PREFIX_ID } -network` , { name: ` ${ PROJECT_PREFIX_NAME } -network` , routingMode: "REGIONAL" , autoCreateSubnetworks: false , } ) const subnetwork = new ComputeSubnetwork ( this , ` ${ PROJECT_PREFIX_ID } -subnetwork` , { name: ` ${ PROJECT_PREFIX_NAME } -subnetwork` , network: network.selfLink , ipCidrRange: "10.10.0.0/16" , region: "asia-northeast1" , } ); const address = new ComputeAddress ( this , ` ${ PROJECT_PREFIX_ID } -address` , { name: ` ${ PROJECT_PREFIX_NAME } -address` , subnetwork: subnetwork.selfLink , region: subnetwork.region , addressType: "INTERNAL" , } ); const disk = new ComputeDisk ( this , ` ${ PROJECT_PREFIX_ID } -disk` , { name: ` ${ PROJECT_PREFIX_NAME } -disk` , zone: ZONE , type : "pd-standard" , size: 10 , image: "ubuntu-os-cloud/ubuntu-2204-lts" , } ); new ComputeInstance ( this , ` ${ PROJECT_PREFIX_ID } -instance` , { name: ` ${ PROJECT_PREFIX_NAME } -instance` , zone: ZONE , machineType: "e2-medium" , bootDisk: { autoDelete: false , source: disk.selfLink } , networkInterface: [ { networkIp: address.address , subnetwork: subnetwork.selfLink , } ] , canIpForward: false , tags: [ "iap" ] } ); } } const app = new App (); new MyStack ( app , "dev" ); new MyStack ( app , "stg" ); new MyStack ( app , "prod" ); app.synth (); Stack を 環境毎に初期化することで、同じ宣言内容で個別の deploy が可能となります。 Deploy 最後に deploy です。 手順は以下のとおりです。 GCP 認証・deploy 内容の確認 対象を指定して deploy GCP認証 以下のコマンドで GCP への認証をします。 gcloud auth application-default login GoogleProviderClass の config という引数の credentials という入力用変数でも認証を行うことができますが、今回は楽に認証が行えるコマンド認証を使っています。 deploy内容の確認 cdktf plan コマンドで確認します。出力内容を全てを載せると量が多くなる為、 以下は、google_compute_instance の deploy 内容です。 cdktf plan create-disk # google_compute_instance.example-katakana-id-instance (example-katakana-id-instance) will be created + resource "google_compute_instance" "example-katakana-id-instance" { + can_ip_forward = false + cpu_platform = (known after apply) + current_status = (known after apply) + deletion_protection = false + guest_accelerator = (known after apply) + id = (known after apply) + instance_id = (known after apply) + label_fingerprint = (known after apply) + machine_type = "e2-medium" + metadata_fingerprint = (known after apply) + min_cpu_platform = (known after apply) + name = "example-katakana-tr-instance" + project = (known after apply) + self_link = (known after apply) + tags = [ + "iap", ] + tags_fingerprint = (known after apply) + zone 省略 対象を指定し deploy 複数の stack がある場合は、stack 名を指定しないと以下の error になります。 cdktf plan (stack 名) Usage Error: Found more than one stack, please specify a target stack. Run cdktf deploy <stack> with one of these stacks: (stack名), (stack名2) deploy 対象に間違いなければ deploy します。 この場合も、複数 stack がある場合は、指定します。 deploy 時、以下の3つの選択肢があります、Approve をすると deploy されます。 cdktf deploy (stack名) Approve Dismiss Stop error が無ければ、cdktf plan で出力された内容が出力され、 最後に以下の出力が表示されて、deploy 完了です。 Apply complete! Resources: 5 added, 0 changed, 0 destroyed. 画像は、私の個人環境にて、今回 deploy された resources になります。 これで、GCP の Network、SubNetwork、IP address、 Disk が作成できました。 GCP deploy 終わりに 今回の実践にあたり、 「CDK for Terraform × Typescript × GCP」で IaC と deploy を行った使用感は以下となります。 Terraform の利点を享受できたこと。(環境構築、diff、deploy のしやすさ。) Typescript フレンドリーに package の import、provider resources の実装ができる。 各 provider で使用する resources class を把握すれば、ESLint 等の Lint を入れてる人は恩恵がわかりやすく、言語補完が効くのでスラスラ書ける。 変数や関数の再利用が書きやすく、読みやすい。 私が個人開発をする際は、CDK for Terraform を使うと思います。 RevComm では一緒に働く仲間を募集しています。 このブログを読んで興味を持ってくれた方、そうでない方も、 ぜひ採用サイトをチェックしてみてください! www.revcomm.co.jp
アバター
この記事は RevComm Advent Calendar 2022 の 17 日目の記事です。 こんにちは大谷( @sara_ohtani_mt2 )です。 今年 10 月に RevComm に入社してバックエンドエンジニアをしています。 今年もあっという間にアドベントカレンダーの季節がきてしまいましたね。 不確実性と闘う皆さん、今年も一年お疲れさまでした! 『THE 有頂天ホテル』という大晦日のホテルを舞台にした映画の中の「年が変われば、いいこともあるわ」という台詞が大好きです。年末になるといつも思い出します。 新年を迎えるというのはただ年がインクリメントされるだけのことのようにも考えられますが、自分の頭と心を整理整頓してまた新しく始めるには、やはりぴったりの機会です。 そこで今日はセルフメンテナンスの基本と 2022 年最新ニュース、そして私がセルフメンテナンスのために行っているオリジナルフレームワークについてご紹介したいと思います。 INDEX 『エンジニアリングマネージャーのしごと』に学ぶセルフメンテナンスの基本 セルフメンテナンス関連キーワードと 2022 年最新ニュースのピックアップ セルフメンテナンスのための振り返り 振り返りフレームワーク「GNT」のススメ(Notion テンプレート付き) 不調からの復活をより再現性のあるものにする RevComm のセルフトークのカルチャー 参考 『エンジニアリングマネージャーのしごと』に学ぶセルフメンテナンスの基本 今年 8 月に発売された書籍『エンジニアリングマネージャーのしごと』を読みました。今後、組織課題に関心がある人への定番本となることは間違いないと感じさせる内容でした。 印象的だったのは、部下や他者に対するアプローチに関することだけでなく、セルフマネジメントや自分のメンタルをクリーンな状態に維持する方法についてもかなりページが割かれていたことです。 本の中では繰り返し「まずは自分をマネジメントしなければいけない」と書かれています。これは、自分の中が整頓されていないと仕事でも頭が回らず力が発揮できないから、ということ以外にも、さらにもう 1 つ意味があると思っています。 組織というのは、人の集まりであり、人数が多くなるほど複雑になっていきます。 組織の形で一番小規模なのが、メンターとメンティの 1 対 1 の関係ですが、さらにその前のレベルが、セルフメンテナンスです。 つまり、自分自身をメンティとして、コーチングやケアをするのです。 それによって組織問題解決のためにある様々な手法を試行して勘所や効果を探ることができます。 『エンジニアリングマネージャーのしごと』のセルフメンテナンスに関連するところを切り取ると、例えばこんなことが書いてあります。 チェックリスト形式にしてみました。いくつできているでしょうか? ツールを活用して情報を整理整頓している? 人脈を広げ、職場に友人ができて支えられていると感じられる状態にしている? 上司と語らい、同僚と語らい、家族や友人と語らう時間をもっている? 外に出て楽しいことをするようにしている? 嫌な感情をもったときには自分と向き合い、なぜそんな感情を持つのか探るようにしている? 周囲の面倒を見ることに全力を尽くすのと同じように自分の面倒も見ている? 何もしない時間枠を確保している? 緊急事態や割り込み、他の人のフォローに備えて、仕事の量を自分のキャパシティの 85% に押さえている? 8 時間の良質な睡眠をとっている? 1 時間に 5 分は体を動かしている? マインドフルネスやっている?(できれば年単位で) 自分のこの先 10 年のビジョンと計画、スキルバックログを書き出して適宜更新している? Checked: 個 / 12 個 並べてみるとあまりに基本的なことだと思う人もいるかもしれません。 しかし基本的なことにもかかわらず、ついつい忙しくなってくると優先度を下げてしまうことではないでしょうか。 忙しいときほど、これらの基本ができているか自分自身を見直す必要があります。 セルフメンテナンス関連キーワードと 2022 年最新ニュースのピックアップ 先程のチェックリストの中で「マインドフルネス」が出てきましたが、正直にいうと私自身はマインドフルネスをこまめには行えていません。 やることはシンプルですが、効果を実感するのは難しいと感じてしまって継続的に実施できずにいます。 ジョン・カバット・ジン(マサチューセッツ大学医学大学院教授・同大マインドフルネスセンターの創設所長)がマインドフルネスについて「何年かやってみれば、どうなるかわかるよ」と言っていたと『エンジニアリングマネージャーのしごと』内で書かれていますが、その境地にはなかなかたどり着けなさそうです。 IT 界隈でマインドフルネスといえば Google ですが、Google がマインドフルネスについて研究開発し始めたのが 2007 年頃とすると、それから 15 年経っていることになります。 *1 2022 年現在のトレンドはどうなっているのでしょうか。 近年でマインドフルネスと関連するキーワードとしては、「ウェルビーイング」が浸透しつつあるようです。 「ウェルビーイング」とは「幸福」「健康」などと訳されるワードです。 SDGs にも登場しており、そこでは「GOOD HEALTH AND WELL BEING(すべての人に健康と福祉を)」という形で掲げられています。 *2 また 2021 年には、内閣府の「成長戦略実行計画」に、「国民が Well-being を実感できる社会の実現」という記載があり、政府の KPI でもウェルビーイングに関するものが設定されています。 *3 *4 新型コロナウイルスの影響や生産性・創造性の向上効果への期待から、特に 2021 年後半以降からウェルビーイングのためのメンタルヘルステックへの市場の関心は引き続き高まっているようです。 *5 *6 emol 社による国内メンタルヘルステックカオスマップの 2022 年版で企業向けアプリがかなり増えていることからもそれは伺えます。 *7 引用元より転載 個人が日々のセルフメンテナンスのために利用するアプリの中で、私が広告をよく見かけるのは「Calm」や「Meditopia」というマインドフルネスアプリです。 *8 *9 そして同じくらいよく見かける「 Awarefy 」というアプリが Google Play ベスト オブ 2022「隠れた名作部門」で大賞を受賞したとのことでした。 *10 隠れた名作部門とは、まだ知られていないけれど、イノベーティブでファンを増やしているアプリを Google Play が選出したものです。 Awarefy ではマインドフルネスも行えますが、より広い枠組みである「認知行動療法」をメインコンテンツにしています。 認知行動療法とは、比較的働きかけやすい自分の認知(考え)と行動にアプローチすることで、本来変化させづらい感情や身体反応にもアプローチするという手法です。 例えばネガティブな感情やストレスによる影響の暴走を抑えるトレーニングになります。 アプリでは現在の状態とありたい姿、そしてそれを踏まえた一日の振り返りを記録することができます。 私も実際に Awarefy を使ってみましたが、認知行動療法の振り返りはアジャイル開発の振り返りのワークにも通ずるところがあり、KPT などの振り返りフレームワークを行ったことがある人には特に感覚がつかみやすいように思いました。 セルフメンテナンスのための振り返り 組織関連の仕事に関わっていると表に出せない話も多いと思います。 例えば 1on1 でした話は基本的に他言できないし、評価のことも採用のことも…。 そんな中でモヤモヤがたまってしまうことがあったらどうしたらよいでしょう?(そしてたいてい、ほぼ確実にそれはたまります) 正直になんでも自由に話せる場は本来限られています。 もしどんなことでも話せる心理的安全性が特別高い場を、どこに行っても常に確保できるとしたら、それは自分の強力な武器になりそうです。 そこで、定期的に一人で振り返りフレームワークを行うことをおすすめします。 自分自身をメンティ・メンターとして一人で振り返りを行うぶんには、話してはいけないことなどありません。 そして転職して会社が変わったとしても絶対についてきてくれるメンターを手に入れることができます。 自分を味方につけるということはセルフメンテナンスにおいてとても重要なことです。 振り返りフレームワーク「GNT」のススメ(Notion テンプレート付き) 自分自身を整理整頓するときには、振り返りと向き直りのセットが必要だと考えています。 振り返りは過去をもとに良かったところや問題を検証してより良くしていくワークで、向き直りは「そもそもどこへ向かいたいんだっけ?」ということを改めて整理してそこを目指すためのアクションを考えるワークです。 私はセルフメンテナンスのために、一人で行う振り返りを 2019 年頃から行っています。 最初は KPT をただ一人で行っているというスタイルでした。 KPT は Keep(今後も続けたいこと)、Problem(今後の課題)、Try(次にやってみること) を書き出すシンプルな振り返りフレームワークです。 しかし向き直りとセットにしていなかったことで、目の前のことにばかり囚われすぎて Problem の落とし込みがうまくできていませんでした。 そのぼやけた Problem から出した Try を実行してもどこかズレを感じて、改善サイクルを回しているはずなのに少しずつストレスになっていました。 そこで、「セルフメンテナンスのために一人で行う振り返り」というコンセプトで KPT をアレンジして、GNT (Good / Noise / Try) というフレームワークを作りました。 GNT 形式にアップデートしてから 2 年半ほど経ちましたが、今はこの形で落ち着いています。 GNT実施例サンプル、Vision の列が Noise と Try の間にある 進め方は下記の通りです。 事前準備として、任意のタイミングで Vision(ありたい姿)を書き出しておく Good(よかったこと)を書き出す Noise(モヤモヤしてること)を書き出す 特に気になる Noise をピックアップして深堀りし、Problem を見つける Try を書き出す 実行するTry を決める KPT との主な違いはこの 2 点です。 Vision を視界に入れながらワークを行う とにかく吐き出す Noise パートと必要に応じて深堀りする Problem パートに分ける Noise から Problem を見つける例 細かいやり方と Notion テンプレートはこちらです。 note.com 「最近、漠然と調子が悪いな」と思ったときは特に、ぜひ今日の『エンジニアリングマネージャーのしごと』からつくったチェックリストの内容ができているかも合わせて振り返ってみましょう。 不調からの復活をより再現性のあるものにする 闘う人にとって一番重要なリソースは何でしょうか? 資金力?時間?権力? いえいえ自分の気力でしょう!と私は思います。 「元気があればなんでもできる!」ではないですが、気力がないと問題を前にしても踏ん張れずに受け流してしまったりするものです。 しかし、気力が常にほとばしっていて尽きない!という人ばかりではないと思います。 少なくとも私はそうではありません。 ただ、気力が尽きてしまっても、できるだけ早く切り替えて立ち直ることならできるかもしれない、と思うのです。 そのために自分の不調に早めに気づいてケアをし、そして過去にモチベーションをあげるのに役立った Try や Good を記録しておくことで、不調からの復活をより再現性のあるものにしていっています。 日々、不確実性と闘う皆さんの中の気力が尽きてしまいそうな人の参考になれば幸いです。 RevComm のセルフトークのカルチャー RevComm には社内向けのカルチャーブック「RCCB ( R ev C omm C ulture B ook)」があり、その中にセルフトークについての項目があります。 自分自身を磨き続けよう ・セルフトークの時間を設け、プライベートとビジネス両面について自分自身と対話しよう。 ・人と比較して勝つのでなく、昨日の自分に勝とう。昨日の自分と比べて今日はどれだけ成長できるかに励もう。 ・自分自身の経験だけで学ぶのではなく、他者から一つでも多く学ぶことで、より自分の価値を高めよう。 ・成功するためには苦労が必要ですが、成功しているときも常に失敗が待っています。成功しているときも怠けることなく全力で取り組もう。 ・自分の身に起こることより、それに対してどう反応したかで決まります。思考は言葉に、言葉は行動に、行動は習慣に、習慣は性格に、性格は運命になります。 (RCCB 一部抜粋) 主要プロダクトである MiiTel も、営業電話のやり取りを解析、可視化したデータをもとに振り返り・セルフコーチングしていただくことで生産性向上につなげるというコンセプトがあります。 10 月に入社したばかりですが、自分と向き合い、こまめにメンテナンスすることを大切にしている会社なのだと感じています。 振り返り好きな方、ぜひご連絡ください! www.revcomm.co.jp それではよいお年を! 参考 エンジニアリングマネージャーのしごと ―チームが必要とするマネージャーになる方法 サーチ・インサイド・ユアセルフ――仕事と人生を飛躍させるグーグルのマインドフルネス実践法 *1 : あなたも実践できる、Google発のマインドフル・メソッドとは? https://careercompass.doda-x.jp/article/335/ *2 : ウェルビーイングとは? 注目される理由と、SDGsや経営の視点からみた重要性|SDGsにまつわる重要キーワード解説 https://sdgs.kodansha.co.jp/news/knowledge/40247/#section-1-3 *3 : 内閣府ホームページより 成長戦略実行計画案 令和3年6月18日 資料3 https://www5.cao.go.jp/keizai-shimon/kaigi/minutes/2021/0618/shiryo_03.pdf *4 : Well-beingに関する関係省庁の連携 https://www5.cao.go.jp/keizai2/wellbeing/action/index.html *5 : メンタルヘルステック投資額、1~3月6割減 前四半期比 https://www.nikkei.com/article/DGXZQOUC236LT0T20C22A6000000/ *6 : ウェルビーイング、市場も注目 社員の幸せ実現する会社 https://www.nikkei.com/article/DGXZQOCD0113Q0R01C22A0000000/ *7 : 「国内メンタルヘルステックカオスマップ 2022年版」を公開! https://prtimes.jp/main/html/rd/p/000000013.000043787.html *8 : カリフォルニア発 睡眠・瞑想・リラクゼーションの世界No.1アプリ『Calm』がついに日本上陸 https://prtimes.jp/main/html/rd/p/000000002.000071029.html *9 : トルコ発、Z世代向けマインドフルネスアプリが日本参入 https://project.nikkeibp.co.jp/behealth/atcl/feature/00004/120600327/ *10 : AwarefyがGoogle Play ベスト オブ 2022「隠れた名作部門」で大賞を受賞! https://prtimes.jp/main/html/rd/p/000000039.000057374.html
アバター
この記事は、 RevComm Advent Calender 16 日目の記事です。 フロントエンドチームに所属する関口です。フロントエンドエンジニアとして活動するかたわら、MiiTel の一部の製品のプロジェクトマネージャーを兼任しています。 なぜこのタイミングで Chrome 拡張機能がテーマなのかというと、最近 Manifest V3 への対応 を弊社の MiiTel Phone Chrome 拡張機能 に対して行い、知見ができたことがその理由です。各 Chrome 拡張機能の機能定義を行うフォーマットが Manifest ファイルです。このフォーマットの V2 から V3 への移行が Google から促されており、その 移行猶予期間が来年の 1 月まで となっています。 この記事について この記事のゴール Chrome 拡張機能の開発手順を最低限確認した後、TypeScript が使えるシンプルな開発環境を作るチュートリアルを提供します。 この記事で説明しないこと Chrome 拡張機能の配布方法 Node.js やパッケージのバージョンについて Lint やユニットテスト、CI について バージョン管理ツールの設定や操作 対象読者 TypeScript 開発経験のあるソフトウェアエンジニア 導入 Chrome 拡張機能は何ができるか ブラウザが備える JavaScript API に加え、Chrome ブラウザのローカルな機能にアクセスするための Chrome API が利用できます 。Chrome API を通じてブックマークのデータにアクセスしたり、ブラウザのコンテキストメニューを編集したり、といったことが可能になります。 Chrome 拡張機能のファイル構成 Chrome 拡張機能は Manifest、Service worker、Content scripts、HTML ページといったファイルで構成されています。 オフィシャルドキュメントによるそれぞれの定義 を見ていきます。 The manifest ファイル名は manifest.json で、その拡張機能のルートディレクトリに置く必要がある。拡張機能の定義(利用したい API や構成するファイルなど)をここに書く。 The service worker ブラウザのイベントを拾うイベントハンドラを定義する。 Chrome API を利用できるが、Web ページのコンテンツには直接アクセスできない。 Content scripts Chrome で閲覧中の Web ページにインジェクトされる。DOM にアクセスできることに加え、Chrome API の一部の機能が使える。拡張機能の Service worker とメッセージを送受信してやりとりができる。 The popup and other pages popup (拡張機能アイコンをクリックした時に表示するコンテンツ)、 option page (拡張機能の設定ページ)、 その他任意の HTML ページ を追加できる。Chrome API にアクセス可能。 基本的な開発方法についてはここでは説明しません。公式ドキュメントの Development Basics に簡潔に説明されているので、そちらをご参照ください。 モチベーションと技術選定方針 TypeScript を使って簡単な Chrome 拡張機能を作りたい。 依存パッケージを最小限にし、ラーニングコストがかからない開発環境にしたい。 とにかくシンプルにしたいので、React や Vue といった JavaScript フレームワークも使わず、HTML も CSS も生で書くことにします。ここでは扱いませんが、もし React や Vue を使いたい場合、 CRXJS を使って環境構築するとよさそうです。 チュートリアル Hello World Development Basics の Hello World チュートリアルをベースに、簡単な拡張機能を作ってみましょう。まず拡張機能のために空のディレクトリを用意して、package.json を作ります。 $ mkdir extension-directory $ cd extension-directory $ npm init -y 空の manifest.json ファイルを作り、 $ touch manifest.json エディタで開いて以下のように書き込みます。 { " manifest_version ": 3 , " name ": " Hello Extensions ", " description ": " Base Level Extension ", " version ": " 1.0 ", " action ": { " default_popup ": " hello.html ", " default_icon ": " hello_extensions.png " } } hello_extensions.png はこちらからダウンロード して、同じディレクトリに置きます。これがツールバーに表示される拡張機能のアイコンになります。 同ディレクトリに hello.html ファイルを作り、以下のように書き込みます。 < html > < body > < h1 > Hello Extensions </ h1 > </ body > </ html > そして、Chrome の拡張機能ページを開き画面右上にある「デベロッパーモード」をオンにして表示される「パッケージ化されていない拡張機能を読み込む」ボタンをクリックすると、そのディレクトリの必要なファイルを読み込んで拡張機能として実行することができます。 パッケージ化されていない拡張機能を読み込む Chrome のツールバーに Hi アイコンが表示されるはずですが、これをクリックすると、Hello Extensions というポップアップが表示されます。 ここまでは Development Basics のチュートリアル の内容です。 Popup の JavaScript では、この hello.html に JavaScript を追加します。 < html > < body > < h1 > Hello Extensions </ h1 > < script src = "popup.js" ></ script > </ body > </ html > popup.js ファイルの中身は以下のようにしましょう。 const $body = document .querySelector( 'body' ); const $p = document .createElement( 'p' ); $p.innerHTML = 'Hello Popup' ; if ($body) { $body.appendChild($p); } 拡張機能ページのリロードボタンをクリックすると 拡張機能ページのリロードボタン 拡張機能はこのように更新されるはずです。 この popup.js を少し最適化してみます。 + import { HELLO } from './constants.js'; const $body = document.querySelector('body'); const $p = document.createElement('p'); - $p.innerHTML = 'Hello Popup'; + $p.innerHTML = `${HELLO} Popup`; if ($body) { $body.appendChild($p); } constants.js という新しいファイルを作り、 export const HELLO = 'Hello' ; と書き込みます。 このように、一部のテキストを constants.js に定数として切り出しました。更新ボタンをクリックしてから拡張機能を表示すると、該当のテキストが表示されていません。拡張機能管理ページを確認すると「エラー」ボタンが表示されています。 拡張機能管理ページのエラー表示 エラーボタンをクリックすると、エラーの内容を見ることができます。 エラーの詳細 Uncaught SyntaxError: Cannot use import statement outside a module と表示されていますね。 import 構文を使うには script タグに type="module" 属性が必要です。 hello.html を修正します。 <html> <body> <h1>Hello Extensions</h1> - <script src="popup.js"></script> + <script type="module" src="popup.js"></script> </body> </html> 拡張機能をリロードして確認してみましょう。今度は該当のテキストが表示されるはずです。 また、拡張機能管理ページのエラーボタンはそのまま表示されているはずです。いつの時点のエラーなのかを区別するため、エラー内容ページに遷移し、現時点でのエラー報告はすべて削除しておくとよいでしょう。 Content scripts を追加する さらに、Webページにインジェクトされる Content scripts も書いてみます。content_scripts.js ファイルと content_styles.css を追加し、以下のように manifest.json を修正します。 { "manifest_version": 3, "name": "Hello Extensions", "description": "Base Level Extension", "version": "1.0", "action": { "default_popup": "hello.html", "default_icon": "hello_extensions.png" }, + "content_scripts": [ + { + "matches": [ + "http://*/*", + "https://*/*" + ], + "css": [ "content_styles.css" ], + "js": [ "content_scripts.js" ] + } + ] } content_scripts.js const $body = document .querySelector( 'body' ); const $helloContent = document .createElement( 'div' ); $helloContent.className = 'hello-content' ; $helloContent.innerHTML = 'Hello content scripts' ; if ($body) { $body.appendChild($helloContent); } content_styles.css .hello-content { position : fixed ; z-index : 999 ; bottom : 0 ; right : 0 ; width : 10em ; padding : 1em ; background-color : white ; box-shadow : 0 0 5px gray ; text-align : center ; } 拡張機能を更新して適当な Web ページ(この例では https://www.google.com )を表示すると、右下に Hello content scripts というボックスが表示されるはずです。 Google トップページに挿入された Content scripts TypeScript 化する TypeScript で開発できるようにしていきます。 まず、必要なパッケージをインストールします。 $ npm install -D typescript @types/chrome tsconfig.json も用意します。ここはお好きにどうぞ、と言いたいところですが、ここでは以下のように設定します。 src ディレクトリに .ts ファイルを置き、コンパイル後の出力は dist ディレクトリに行う設定です。 { " compilerOptions ": { " target ": " ESNext ", " lib ": [ " ESNext ", " DOM ", " DOM.Iterable " ] , " useDefineForClassFields ": true , " module ": " ESNext ", " rootDir ": " ./src ", " outDir ": " ./dist ", " moduleResolution ": " node ", " baseUrl ": " src ", " resolveJsonModule ": true , " allowJs ": true , " checkJs ": true , " removeComments ": true , " esModuleInterop ": true , " strict ": true , " noImplicitAny ": true , " noUnusedLocals ": true , " noUnusedParameters ": true , " noImplicitReturns ": true , " skipLibCheck ": true } , " include ": [ " src " ] , " exclude ": [ " node_modules ", " dist ", " ./tsconfig.json " ] } ファイルを以下のように移動します。 .js ファイルは .ts にリネームします。 extension-directory/ ├── node_modules ├── dist │ ├── content_styles.css │ ├── hello_extension.png │ ├── hello.html │ ├── manifest.json ├── src │ ├── constants.ts │ ├── content_scripts.ts │ ├── popup.ts ├── packages.json ├── tsconfig.json packages.json に build コマンドを追加します。 "scripts": { "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc" }, npm run build コマンドを実行してみましょう。 dist ディレクトリ以下に、コンパイルされた constants.js、content_script.js、popup.js の 3 つができたら成功です。 manifest.json の置いてあるディレクトリが dist に変わったので、拡張機能を読み込み直します。 拡張機能ページで該当の拡張機能をいったん「削除」し、再度「パッケージ化されていない拡張機能を読み込む」で dist ディレクトリを指定します。 パッケージを削除してから読み込み直す Content scripts の最適化 content_script.ts も import を使って共通のモジュール(constants.ts)を利用するように最適化します。 + import { HELLO } from './constants'; + const $body = document.querySelector("body"); const $helloContent = document.createElement("div"); $helloContent.className = "hello-content"; - $helloContent.innerHTML = "Hello content scripts"; + $helloContent.innerHTML = `${HELLO} content scripts`; if ($body) { $body.appendChild($helloContent); } npm run build でビルドしてから、拡張機能をリロードします。適当な Web ページを表示してみると…画面右下の拡張機能が表示されません。Chrome DevTools の Console を見ると、 Uncaught SyntaxError: Cannot use import statement outside a module エラーメッセージが表示されています。 Content scripts のエラー表示 Content scripts は自動的にドキュメントにインジェクトされるため、 script タグに type="module" をつけるといった対処は行えません。 …となると、 import 構文を使わないように書くか、バンドラーを使うしかない、ということになってきます。今後の拡張性も考慮して、バンドラーを導入することにします。 Vite の導入 バンドラーは Webpack でも Rollup でもなんでも構いませんが、洗練された開発環境を提供し、日本語ドキュメントもある Vite をここでは採用します。 まずはインストール $ npm i -D vite vite.config.js ファイルをルートディレクトリに作り、以下のように設定を書きます。 import { resolve } from 'node:path' ; import { defineConfig } from 'vite' ; export default defineConfig((opt) => { return { root: 'src' , build: { outDir: '../dist' , rollupOptions: { input: { content_scripts: resolve(__dirname, 'src/content_scripts.ts' ), popup: resolve(__dirname, 'src/hello.html' ) } , output: { entryFileNames: '[name].js' , } , } , } , } ; } ); 既存のファイルも以下のように移動します。 extension-directory/ ├── node_modules ├── dist ├── src │ ├── public │ │ ├── content_styles.css │ │ ├── hello_extension.png │ | ├── manifest.json │ ├── constants.ts │ ├── content_scripts.ts │ ├── hello.html │ ├── popup.ts ├── packages.json ├── tsconfig.json ├── vite.config.js packages.json の build コマンドを以下のように書き換えます。 "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "tsc" + "build": "tsc && vite build" }, vite は TypeScript のトランスパイルを行うので、 tsc でのトランスパイルは不要になります。一方、vite は型のチェックを行わないため、 tsc を型チェックのためだけに利用するように動作を変えます。 tsconfig.json を以下のように修正します。 "rootDir": "./src", - "outDir": "./dist", "moduleResolution": "node", ... "removeComments": true, + "noEmit": true, + "isolatedModules": true, "esModuleInterop": true, noEmit を指定することで型チェックだけ行ってファイルを出力しないようにします。また、 isolatedModules の指定は vite にとって必要な設定 です。 hello.html も調整が必要です。読み込むファイルを .js から .ts に変更します。 <body> <h1>Hello Extensions</h1> - <script type="module" src="popup.js"></script> + <script type="module" src="popup.ts"></script> </body> ここまで設定したら npm run build を実行してみましょう。 ルートの dist ディレクトリに以下のようなファイルが出力されたら成功です。 extension-directory/ ├── dist │ ├── assets │ │ ├── constants.xxxxxxxx.js │ ├── content_scripts.js │ ├── content_styles.css │ ├── hello_extensions.png │ ├── hello.html │ ├── manifest.json │ ├── popup.js さて、出力された content_scripts.js を開くと… import { H as n } from "./assets/constants.xxxxxxxx.js" ; const t= document .querySelector( "body" ),e= document .createElement( "div" );e.className= "hello-content" ;e.innerHTML= ` ${n} content scripts` ;t&&t.appendChild(e); このような内容になっているはずです。あれ… import が使われていますね。これは意図している出力結果ではありません。 モジュールのファイル内容を展開して出力結果から import 構文を無くすには、rollup の inlineDynamicImports オプションを使うと実現ができます。ただし、このオプションはインプットとなるファイルが 1 つの場合のみ利用できるという制約があります。今回のインプットは content_scripts と popup の 2 つがあるので、このままでは使えません。さてどうするか。 Vite の設定を工夫する vite.config.js を 2 つに分割してしまうことで、この問題を回避します。 vite.config.content_scripts.js ファイルを追加し、以下のような設定にします。 format: 'iife' は即時実行関数( "immediately-invoked function expression" )のフォーマットでファイルを出力する設定で、script タグで読み込まれるスクリプトに適しています。 import { resolve } from 'node:path' ; import { defineConfig } from 'vite' ; export default defineConfig((opt) => { return { root: 'src' , build: { outDir: '../dist' , emptyOutDir: false , rollupOptions: { input: { content_scripts: resolve(__dirname, 'src/content_scripts.ts' ), } , output: { entryFileNames: '[name].js' , inlineDynamicImports: true , format: 'iife' , } , } , } , } ; } ); これに合わせて vite.config.js も修正します。 build: { outDir: '../dist', + emptyOutDir: true, rollupOptions: { input: { - content_scripts: resolve(__dirname, 'src/content_scripts.ts'), popup: resolve(__dirname, 'src/hello.html') }, output: { entryFileNames: '[name].js', }, }, }, emptyOutDir は出力先のディレクトリを実行前に空にするかどうかの設定です。 最後に packages.json のコマンドを修正しましょう。 "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "tsc && vite build" + "build": "tsc && vite build && vite build --config=vite.config.content_scripts.js" }, config ファイル違いでビルドを 2 回実行します。前に実行する設定では emptyOutDir: true にし、後に実行する設定では emptyOutDir: false にするのが肝です。後の実行で emptyOutDir: true になっていると、先に実行した出力ファイルが削除されてしまうためです。 今回は vite.config.js でビルドしているのが popup.js(インプットは hello.html)だけですが、今後 Service worker スクリプトやオプションページで利用するスクリプトを追加したくなったら、この設定ファイルのインプットに足していきましょう。これらのスクリプトでは先に説明したように import 構文が使えます。Content scripts だけは、vite.config.content_scripts.js でビルドするようにします。 これで準備はできました。 npm run build します。出力されたファイルは設定変更前とほぼ同じですが、 content_scripts.js の中身を見てみましょう。 ( function () { "use strict" ; const n= "Hello" ,t= document .querySelector( "body" ),e= document .createElement( "div" );e.className= "hello-content" ,e.innerHTML= ` ${n} content scripts` ,t&&t.appendChild(e) } )(); constants.ts の内容が展開されています。意図どおりの出力です。 拡張機能をリロードして確認していきます。エラーも表示されず、Popup、Content scripts それぞれ正しく動作していることが確認できたらこのチュートリアルは完了です。 チュートリアルの要点 Content scripts では import 構文が使えない。tsc でトランスパイルすると、この問題でつまづく。 import 構文は使いつつ上記の問題を回避するにはバンドラーを使う必要がある。 Vite でうまいことやるには vite.config.js ファイルを 2 つに分けて、片方を Contents scripts のビルド専用にする。そちらでは inlineDynamicImports: true を設定することで出力後のファイルを 1 つにまとめる。 今回のチュートリアルの最終的な成果物は "TypeScript で Chrome 拡張機能を開発する" チュートリアル ソースコード から参照可能です。 References Development Basics - Chrome Developers TypeScript: tsconfig リファレンス Vite rollup.js 記事を読んでくれた方へ Web サイトや Web アプリケーションの開発に比べると、Chrome 拡張機能を開発するのはニッチな機会ですし、情報も決して多くはないジャンルです。そんな中、よりよい開発体験を得ようと試行錯誤してこの記事にたどりついたあなたのようなエンジニアを、弊社では必要としています。 弊社の採用情報 も是非チェックしてみてください。 www.revcomm.co.jp
アバター
この記事は RevComm Advent Calendar 2022 の 15 日目の記事です。 はじめに こんにちは。RevComm シニアエンジニアマネージャーの瀬里です。 普段はエンジニアリング組織づくりをメイン業務として行っています。 「組織づくり」というと聞こえが良いですが、実際は年間数百人の方と面談して一緒に働きたいと思える優秀な方をアトラクトするということがとても重要な業務だったりします。 早いもので、RevComm で働き始めてこの 12 月で 5 年目に突入しました。 この間に自分の働き方がどのように変わったのか、そしてエンジニア組織としてもどのように変わってきたのか、今後どのような組織を目指しているのかについて今回は書いていきます。 マネジメント論やスタートアップ組織論だとかいうものはネットや本を探せばいくらでも出てくるのでほぼ割愛して、リアルにやってきたこと、感じたことを書くことで多少なりとも誰かの役に立てればと思います。 2018~2019 年:組織未満 1 ヶ月の業務委託を経て 2018 年 12 月に入社した際、エンジニアリング組織は業務委託と社員で合わせても 5-6 名くらいだったかと思います。既に MiiTel という主力製品が世に出てから数ヶ月経った頃です。 当時は明確な部署やチームというものもなく、とにかく各々が貢献できる部分を全力でやるという感じでした。 スタートアップでよくあるような障害や、予想もしていない問題が起きて徹夜でやりきるということを一番体験した期間だったりします。 この頃の課題の 1 つは、 過去の自分のインタビュー記事 でも書いていますが個人事業主の集まりという感じでチームとしてのまとまりが少なかったことです。ただ、今となって振り返ると、少ない人数で事業を急成長させるために、それぞれのメンバーが個別に動いてとにかく目の前の課題に全力を尽くすという働き方は当時の最適解であったとも思えます。 少人数なのにチーム制であるとかフォーマットが統一された正確なドキュメント化だとか、不確定な未来について語り合うといったことは時間をとられるだけで開発の速度は上がりません。 一方で反省点があるとすれば、最初期から最低限文書化だけははじめからある程度ルールを設けておけばよかったな、ということです。 技術者としては README.md や ARCHITECTURE.md などで導入や技術選定、機能の開発の意図などをごく簡単にでも残しておくと後で楽です。このちょっと面倒くさくて省いてしまいたくなる作業は、後々になって人数が増えてくるとかなり効いてきます。 小まとめ 実際の業務 / 立場 サーバーサイド・フロントエンド・インフラの設計・実装(がんがんプログラミングする) 入社後半年も過ぎない頃から採用面談もしていた 組織の形 未形成。かろうじてマイクロサービスという単位でオーナーが決まっている程度 全社的に「部署」というものがなかった 求める資質 自ら課題を発見し、率先して取り組めること 重要な課題 人が足りない 組織運営の上での施策例 特になし。そもそも「部署」という概念が存在せず、必要なかった とにかく全員ができることを全部やって会社の成長を図る 2020 年:担当範囲の決定 エンジニア全体の人数が 20 名程度になりました。 徐々にマイクロサービスを起点としたチーム制が成り立ってきた感じです。自分自身も明確にWebアプリケーションを担当するという形で Web チーム全体(バックエンド・フロントエンド)のリーダーとして動いていました。 毎月、徐々にメンバーが増えるという状況でしたが大きな混乱はなく、オンボーディングはメンバーの入社時に適宜行うスタイルでした。工夫していたのは、例えば障害対応の方法について、一度説明を受けたメンバーが次に入ってきたメンバーに説明するようにする、といった後任を育てるという点です。 オンボーディングやKT(Knowldege Transfer)の内容を録画して見てもらうようにするということも考えましたが、ほぼ浸透しませんでした。完全リモートワークなのにここまでやってしまうと人間味がなくなるという理由もあります。 小まとめ 実際の業務 / 立場 Webチームのマネージャー と言いつつ、サーバーサイド・フロントエンド・インフラの設計・実装(がんがんプログラミングする) 組織の形 マイクロサービスチーム 求める資質 自ら課題を発見し、率先して取り組めること 重要な課題 人が足りない 組織運営の上での施策例 オンボーディング用質問チャンネル この頃に社内で分からないことがあるメンバーを積極的に助ける Slack チャンネルとして、QA チャンネルというものがたちあがりました。 (Question & Answerの略) フルリモートワークということで、隣のメンバーの肩を叩いて「ねぇ、これどうやるの?」と気軽に聞くことができない環境において、みんなが助け合うという趣旨の場所は重要です。 ※今は QA = Quality Assuarance(品質)のチームが内部で立ち上がったこともあり、チャンネル名は 「helpme」チャンネルとなっています。 helpme チャンネルに今でもピン留めされている初期の自分の投稿をそのまま転載します。 RevComm特有の開発プロセスとか資料どこ、とかについては即質問。(悩むな。わかりやすく用意できていない先達たちが悪い。あなたのせいじゃない) 1時間調べて悩んで試してもわからなかったら、すぐ訊くべし。 リモートワークだからこそ、困ったときは同僚を頼れる場を作っておくことが安心感に繋がります。また、皆が回答をしているのをみて「自分も何か手伝えることはないか」と自発的に行動してくれるようになる副次的効果があったりします。 エンジニア独自の評価プロセス 明確な評価制度というものが全社で導入され、全社共通で行われるバリューを基にした 360 度評価と、テック独自のスキルも加味した MBO(目標管理 : Management by Objectives)と評価期間内に取り組んだことのプレゼンテーション評価というものを組み合わせて行うようになりました。 2021 年:明確な組織化 エンジニアが 30 人を超えました。いわゆる 30 人の壁というものが出てきます。 実際に様々な技術レベル・価値観の方が入ってくることによって、全員が同じレベル、モチベーションで働くということが難しくなってきます。 良いか悪いかは別として、休日に Slack のメンション通知が少なくなってきたのもこの頃です。以前は四六時中 Slack 通知が届き、自分も含めて「この人いつ寝てるの?」とか思うメンバーもちらほらいました。 今回の Advent calendar で初日を担当した、リサーチディレクターの橋本さんがジョインしたのもこの頃で、リサーチ部門がテクノロジー部門と明確にわかれました。 小まとめ 実際の業務 / 立場 テクノロジー部門の組織づくり 障害対応の場合は手を動かすことも 組織の形 マトリックス型組織 求める資質 自ら課題を発見し、率先して取り組めること 重要な課題 人が足りない 組織運営の上での施策例 技術領域単位でチーム組成 コーポレートエンジニアリング(社内システム向けの業務ツールを担う)という特殊な領域以外に関して、プロジェクトにはそれぞれのチームからメンバーがアサインされてプロジェクトを推進するマトリックス型組織と呼ばれる形を採用しました。 1 チームは極力 5 名以下。経験上、非定型的なエンジニアの業務では 5 名程度までが 1 人のマネージャーが見れる限界だと思います。 採用プロセスの整備 エンジニアの採用には、該当技術領域のチームのメンバーが一度は必ず面談する技術領域単位での採用プロセスを取り入れています。例えばフロントエンドならフロントエンドの現場メンバーが出ます。 スカウトも各チームが責任を持って行う体制へと移行。 これには良い面と悪い面の両方があります。現場メンバーが出ることで、候補者の技術レベルや「一緒にプロジェクトをやっていけそうか」など判断し易くなる一方で、エンジニアの開発時間が面接に割かれて開発が遅れることにも繋がります。 2022 年:常に改善中 さて、人数がだいぶ増えました。 いつのまにか 50 名を突破し、いまこの記事を書いている時点でエンジニアの総数は 80 名。その数が 100 名に近づこうとしています。 組織的には順調に人数も増え、開発やQAなどのプロセスもしっかりとまとまってきています。 日々の技術的課題への自発的な取り組みはもちろんですが、Tech Talkという社内ライトニングトークやもくもく会といったものがエンジニアから自発的に企画されて行われています。 会社全体ではフットサル・アウトドア・ゲームといった各種クラブ活動も続々と生まれてきています。 もちろん、ここまできても課題は出てきます。 主なところでは以下のようなことです。 メンバーは増えているが、開発スピードをさらに高めたい 様々なミッションを担当しているメンバーが全員納得するような評価制度を目指してアップデートを図りたい 課題は解決するしかないので日々改善施策を試す日々が続いています。 小まとめ 実際の業務 / 立場 テクノロジー部門のマネジメント・執行役員 たまに設計・コーディングなども 組織の形 マトリックス型組織 求める資質 自ら課題を発見し、率先して取り組めること 重要な課題 人が足りない 組織運営の上での施策例 マネジメントとプロフェッショナルの分離 従来はシニアエンジニアやテックリードといった、プロフェッショナルなメンバーが管理職(ラインマネジメント)も兼務しているケースが多かったです。 しかし、事業成長速度を出すために 1on1 や評価といったマネジメント業務よりも技術的な課題解決、エンジニアリングなどにプロフェッショナルとして集中してもらう方が良いと判断し、マネジメント業務から離れてもらう方向を取っています。 マネジメントとプロフェッショナルの職種は取り組む課題に違いがあるだけで、どちらが上ということはありません。興味がある分野でそれぞれが最高の成果をあげれば組織としても成長して大きくなり、より大きなことに取り込むことができるようになった結果としてそれぞれが望むキャリアを歩んでいけると思っています。 RevComm ではエンジニアのキャリアアップとして組織的な課題に取り組むマネジメントか、技術的な課題を専門として取り組むプロフェッショナルどちらにより興味があるかヒアリングし、それぞれに合ったキャリア形成を行っていけるように取り組んでいます。 評価制度の見直し 組織の拡大に伴って評価対象の人数も増え、マネージャーの負担が増えてきています。マトリックス型組織においてありがちな課題として、MBO やスキルベースのマネージャーによる評価だけではプロジェクトの貢献度に対する評価がおろそかになりがちということがあります。 現在は、この点を改善をすべく正当な評価をより低負荷で行うことを目標に制度の見直しを行っています。 さいごに RevComm のエンジニア組織は 100 名に近づいてきていますが、まだまだ組織的には「これがベストのやり方である」とか「こういう方が必ずフィットする」という答えはありません。 必要なのはその時々においてベストだと思える状態に組織を持っていくことであり、組織、そして中にいるメンバーのマインドも変わり続けることだと思います。 最後まで目を通していただき気づいた方がいるかもしれませんが、初期から今まで組織的として「求めている資質」と「重要な課題」は同じです。 組織がいくら拡大したとしても一緒に働く仲間に最も求めていることは一貫して同じであり、自ら課題を発見して率先して取り組んでいくような自走力です。 個人的には、プログラミングやビジネスマインドは後からでも鍛えようがありますが、自走力というものは価値観や長年の経験などからくるものであり、一朝一夕では身につくものではないと考えています。そのため、採用の段階で必ず重視してみています。 また、不思議なことにエンジニアが 100 名近くまで増えた現段階においても、やりたいことすべてを行うためにはまだ人が足りていない状況は変わっていません。おそらくこれは人が増えるに伴い会社としてやりたいことも際限なく出てくるので、永久にこの課題は解決されないのかもしれないと思っています。 ここまでで、本の引用を避けてこれまでに体験してきたこと、その時点で感じたことなどを書いてきました。重要なことは、本の内容は知識として活用しつつ組織の実情に応じて必要な仕組みを作り出すこと、組織の全員が自分ごとと捉えて自発的に動ける場を作り出すことだと思っています。 急拡大しながら日々変わり続ける組織で働くことに興味をもっていただいたら、是非弊社の採用サイトを覗いてみてください。エンジニアだけではなく様々なポジションで募集しています。 是非一度気軽に話しましょう。 www.revcomm.co.jp
アバター
この記事は RevComm Advent Calendar 2022 の 14 日目の記事です。 はじめに こんにちは。株式会社 RevComm でモバイルアプリを開発している長尾です。 普段は MiiTel Phone Mobile の機能開発やメンテナンスを行っています。 さて、アプリをリリースした後もユーザーさんに快適に使用してもらうためには、アプリのパフォーマンスを定期的に計測し、アプリが軽快に動作しているかを確認し続けることが重要です。 本記事では、iOS アプリのパフォーマンスを計測する方法について、まとめています。 iOS アプリのパフォーマンスを測定するためのツール iOS アプリのパフォーマンスを計測する方法として以下の三つのツールを紹介します。いずれのツールも Xcode をインストールすれば追加でツールをインストールする必要はなく、すぐに使い始めることができます。 Debug Navigator Instruments XCTest metrics Debug Navigator Xcode でデバッガを接続しているときに自動で立ち上がっている画面です。 メモリ、CPU、消費電力やネットワークなどの使用状況が UI に表示されます。デバッグ中に簡易的にパフォーマンスを確認するのに便利です。コード作成中に、何らかのミスで循環参照になっているなどして、無限にメモリを消費してしまうような状況になっていることがあります。そのようなケースでは、メモリの使用状況のグラフを見ると、状況が一目で分かります。デバッガを接続している際は特に設定もなく使用可能なので、とりあえず何か起こってないか確認したいときに使用するツールだと思います。 Instruments 測定したいメトリクスをカスタマイズでき、測定できるメトリクスの種類も豊富です。測定したい項目の選択はテンプレートとして保存することができ、よく測定する項目をさまざまなシーンで容易に測定することができます。 XCTest metrics ユニットテストの中で、特定のメソッドに対して、パフォーマンスを測定することができます。測定できる項目は Instruments ほど多くはありませんが、経過時間、メモリ使用量、CPU 使用率、ストレージに関するメトリクスなどが測定できます。他のツールにない特徴としては、正常に完了したテストのメトリクスを baseline として、次回以降のテストにおいて baseline からの乖離を評価できます。乖離が大きくなっている場合、テストが失敗したとみなすことで、パフォーマンスの低下や予期せぬ不具合を検出することができます。 これらのツールはそれぞれ用途が異なっているので、どれが優れているというわけではありません。適材適所で使用することで、各々のツールの特徴を生かすことができます。 実際にパフォーマンスを測定してみる では、これらのツールを用いて、アプリのパフォーマンス評価を行ってみましょう。 今回評価するアプリは、最近 Apple が公開した StableDiffusion を利用できるライブラリである ml-stable-diffusion を組み込んだ簡単なアプリです。 今回作成したアプリで生成した「a photo of an astronaut riding a horse on mars」 Debug Navigator で大まかなアプリのパフォーマンスを確認する 開発段階においては、Debug Navigator を用いて大まかなアプリのパフォーマンスを把握します。中央にある Memory のセクションを見ると、Memory Use が 2.1GBと表示されています。対象のアプリは、かなり多くのメモリを使用していることが分かります。 本格的に画像生成が始まった際は、メモリ使用量が大幅に増えている また、メモリを大量に使用するのは画像を生成している間のみで、画像が生成されるとメモリが解放されること (2.1 GB -> 48.6 MB) が、下記の画像の Memory Use の値やグラフの落ち込みなどから確認できます。 画像が生成されるとメモリが解放される Instruments を使ってアプリの詳細なパフォーマンスを確認する アプリが意図通りに動作していることがある程度確認できたので、もう少し詳細なパフォーマンスを確認していきます。 最初に挙げた三つのツールの中で、最も詳細にパフォーマンスを測ることができるツールである Instruments を使って、パフォーマンスを見ていきましょう。 ml-stable-diffusion は、CoreML を利用しています。またライブラリが要求するシステム要件も iOS16.2 以上、iPhone 12 以上となっていることから、ml-stable-diffusion を実行するために、機械学習の処理に特化したチップである Neural Engine を利用していることが推測されます。これを Instruments を使って確認してみましょう。 Xcode から Develop tools > Instruments とたどり、Instruments を起動させます。Instruments を起動させると、テンプレートを選択する画面が表示されます。 パフォーマンスを確認したいタイプのテンプレートを選択すると、確認したい項目がセットされた状態で、以下のような画面が起動します。 Instrumentsのテンプレート選択画面 今回は、Core ML のテンプレートを用いてパフォーマンスを確認してみました。 Instrumentsの実行結果の例 推測したとおり、しっかりNeural Engineが使われているようです。他にも Core ML のメトリクスでは、モデルの名前が確認できます。 XCTest metrics を使ってアプリのパフォーマンスを継続的に確認する 最後に、XCTest metrics を用いて、特定メソッドのパフォーマンスを確認します。 XCTest metrics は、単体テストの一部として実装します。 普通の単体テストの一メソッド内で、 XCTestCase クラスの measure メソッド を実装します。このメソッドは引数にクロージャを取り、クロージャに定義された処理に対する実行時間を測定します。クロージャに入った処理を複数回実行することで、メトリクスの baseline を算出します。今回は以下のようなコードでパフォーマンスを測定しました。 func testMetrics () throws { let metrics : [ XCTMetric ] = [XCTClockMetric(), XCTMemoryMetric(), XCTCPUMetric(), XCTStorageMetric()] self .measure(metrics : metrics ) { let _ = ViewController.generate(prompt : “a photo of an astronaut riding a horse on mars”) } } 実行結果は以下のように表示されます。 一度 baseline を設定してからほぼ何も修正せずに再度テストしてみたのですが、14% 数値が悪化してしまいました。一回の実行時間が 200s を超えているので、10% としても 20s。テスト環境の関係上、完全に条件が揃えられているわけではありませんが、パフォーマンスが安定していないように感じました。筆者の主観ですが、ml-stable-diffusion は、ファーストパーティである Apple が GitHub にサードパーティ製ライブラリとして配布していることを考慮すると、プロダクションに投入するには時期尚早と判断したのかなと考えています。 baselineを設定した後の実行結果 おわりに 本記事では、iOS アプリのパフォーマンスを計測するツールをご紹介しました。計測することは、パフォーマンスを向上させる第一歩となります。 今日紹介したツールを使って効率的にボトルネックとなっている箇所を見つけ、アプリのパフォーマンスを向上させるきっかけとなれば幸いです。 公式サイトにはさらに詳しい情報がまとめられていますので、より深く知りたい方は こちら をご確認いただくとよいと思います。
アバター
この記事は RevComm Advent Calendar 2022 の 13 日目の記事です。 はじめに RevComm の小門です。 普段はインフラエンジニア / SRE としてクラウド (AWS) の全社横断的なセキュリティ強化、統制を推進しています。 また、私自身ここ数年間はサーバサイドエンジニアとして Python、特に Web アプリケーションフレームワークである Django をキャッチアップしています。 RevComm では最近、開発プロジェクトにも関わりはじめました。 RevComm のプロダクトと Django RevComm の主要プロダクトである MiiTel は営業やコールセンター業務に活用できる IP 電話の SaaS 製品です。 MiiTel の音声解析 AI による分析、可視化した結果を確認するためのダッシュボードである MiiTel Analytics を Web アプリケーションとして開発しています。 MiiTel この Web アプリケーションはフレームワークに Django を採用しており、本記事ではこのリポジトリについて説明していきます。 プロダクトの初期ローンチ以来、機能拡張や刷新(v1 -> v2)を重ねており、今日まで基本的に 1 つのリポジトリで開発を続けています。 ※もちろん、必要に応じて機能分離して別リポジトリ化することもあります。 リポジトリの運用開始時期:2019 年ごろ Django アプリケーション数:約 40 リポジトリの関係メンバー:約 20 名 このように 3 年以上運用しているプロダクトのリポジトリであり、社内では最も大きなコードベースになっています。 また、ありがたいことに RevComm にはほぼ毎月新しいメンバーが入社してくれていて、オンボーディングが終わり次第、開発プロジェクトにアサインされます。 新規メンバーが大規模なリポジトリの開発でスタートダッシュするために、オンボーディングでのルール周知と「仕組み化」の構築が必要だと思います。 数名程度の規模のチームなら問題にならないかもしませんが、数十名以上のチーム規模の場合に個人の裁量を過度に大きくしてしまうと統制が取れなくなり、将来的に開発スピードが低下する可能性が高くなります。 以下では、大規模なリポジトリで円滑なチーム開発を続けていくために工夫している取り組みと、今後の課題について紹介していきます。 工夫している点 Django は MTV という、一般的な MVC フレームワークに似たアーキテクチャを採用しているフレームワークです。 これについては公式ドキュメントでも言及されています。 FAQ: General | Django documentation | Django 以降は Django の MTV に対応した用語で説明します。 レイヤードアーキテクチャ 一般的な MTV アーキテクチャにおいて、ドメインロジックを実装するのは View とされることが多いです。 多くのケースでは、標準のままの Django でも拡張性と保守性を保ちながら開発していくことが可能だと思います。 しかし、大規模なコードベースにおいて多くの処理が View に集中すると、全体の見通しが悪くなり保守性が低下してしまいます。いわゆる Fat Controller(MTV でいうと Fat View)です。 そこで RevComm では個別のドメインロジックを Usecase 層 として View 層から切り離し、View を薄い層として保つようにしています(Usecase 層は View 層と Model 層の間に位置する)。 社内 TechTalk の発表資料より抜粋 ちなみに MiiTel Analytics のプロジェクトは Django に加えて Django REST framework(DRF)も使用しているため UI 層は存在せず、フロントエンドアプリケーションの別リポジトリもあります。 CI/CD の整備 多くのメンバーが関わるリポジトリにおいて運用性と保守性、そして開発利便性の観点から CI/CD を整備することは重要です。 RevComm のリポジトリでは下記のように CI/CD を整備しており、主なツールに GitHub Actions を利用しています。 CI Docker コンテナイメージをビルドする ビルドしたコンテナイメージを使ってユニットテストを実行する CD 特定ブランチへのマージを起点に、AWS 環境へのデプロイを実行 GitHub Actions の活用例については過去の記事でも紹介していますので、ぜひご覧ください。 MiiTel Analytics 開発チームの CI / CD ツール活用を紹介します。 - RevComm Tech Blog エディタ拡張機能の共通化 開発に有用な拡張機能やユーティリティもリポジトリに含めて、チームメンバー間で共有できるようにしています。 (Visual Studio Code を使っているメンバーが多いです) 例を挙げると、API リクエストに便利な拡張機能である REST Client と、その拡張機能で使用する設定ファイル(.http ファイル)をメンバー間で共有しています。 $ tree .vscode ├── extensions.json └── settings.json http ├── my-request.http ├── … .vscode/extensions.json { " recommendations ": [ " humao.rest-client ", … ] } .vscode/extensions.json を活用すると VSCode の拡張機能をプロジェクト内で共通化できて便利です。 課題と対応案 以上はすでに工夫している点でしたが、もちろん現状課題となっている点もあります。 課題点と(採用するかはさておき)取りうる対応案について考えてみます。 アプリケーション起動の複雑性 アプリケーション設定値を環境ごとに使い分けるために環境変数を使用することはよくあると思います。 ところが、大規模な Django ではどうしても必要になる環境変数が増えてきます。 必要な環境変数は .env ファイル化しておけば Docker コンテナ起動時の --env-file オプションで一括で読み込むことができます。 一方で、エディターと連携してデバッガーを起動したり、ユニットテストのためにローカル環境(=非 Docker コンテナ環境)でアプリケーションを起動したりしたいことがあります。 .env ファイルの環境変数を都度 export するのは面倒です。 シェルのワンライナーも考えられますが 、多くのメンバーがいるチームでの運用は現実的ではありません。 そこで、Docker コンテナの起動オプションと同様に、 .env ファイルから動的に環境変数を読み込む方法を考えてみます。 これを実現するには、python-dotenv ライブラリが有用です。 https://github.com/theskumar/python-dotenv $ pip3 install python-dotenv settings.py from pathlib import Path BASE_DIR = Path(__file__).resolve().parent dotenv_path = BASE_DIR / "path/to/.env" if dotenv_path.exists(): from dotenv import load_dotenv load_dotenv(dotenv_path, override= True ) … .env がある場合のみ環境変数を読み込むようにすることで、本番環境では Amazon ECS(コンテナオーケストレーター)でアプリケーション起動する挙動差異を吸収することができます。 テスト実行時間の長さ 大きなコードベースではどうしても CI(特にユニットテスト)の実行時間がネックになってきます。 並列実行 対応策としてまず考えられるのは、テストを並列実行することです。 Django 標準の test コマンド(manage.py test)で並列実行用に --parallel オプションがあり、これが有用そうです。 ※なお Django v3 系では -b / --buffer オプションと --parallel オプションは併用ができず、v4 系で可能になるようです。 同僚の松土が Django Congress 2022 で言及していました。 資料 マイグレーションのスキップ また、Django App があるリポジトリをしばらく運用しているとマイグレーション履歴も多くなってきます。 マイグレーションファイルが増えてくると当然マイグレーション実行の所要時間が長くなり、ローカルで何度もテストを実行したい場合に利便性が下がってしまいます。 そのような場合には、Django test コマンドの --keepdb オプションが有用です。 簡単なデモで動作を追ってみます。 1 つの Django App に 1 つのモデル、そして 1 つのテストケースがあります。 $ tree ├── django_project │ ├── __init__.py │ ├── settings.py │ ├── … ├── manage.py ├── myapp │ ├── __init__.py │ ├── apps.py │ ├── migrations │ ├── models.py │ ├── tests.py … myapp/models.py from django.db import models class MyModel (models.Model): name = models.CharField( "name" , max_length= 128 ) myapp/test.py from django.test import TestCase from .models import MyModel class TestmMyApp (TestCase): def test_sample (self): MyModel.objects.create(name= "hello" ) self.assertEqual(MyModel.objects.count(), 1 ) settings.py の中でテスト時にはデータベース名「test-db」を使用するようにしています。 django_project/settings.py … DATABASES = { 'default' : { "ENGINE" : "django.db.backends.postgresql_psycopg2" , "NAME" : "app" , ..., "TEST" : { "NAME" : "test-db" } } } --keepdb オプションを指定した場合とそうでない場合、それぞれ 2 回ずつテストを実行して、動作の違いを確認します。 まずは --keepdb がない場合。 $ # 1-a. 1回目 $ python manage.py test -v 2 Found 1 test ( s ) . Creating test database for alias ' default ' ( ' test-db ' ) ... Operations to perform: Apply all migrations: myapp Running migrations: Applying myapp.0001_initial... OK System check identified no issues ( 0 silenced ) . test_sample ( myapp.tests.TestmMyApp ) ... ok -------------------------------------------------------------------- -- Ran 1 test in 0 .025s OK Destroying test database for alias ' default ' ( ' test-db ' ) ... $ # 1-b. 2回目 $ python manage.py test -v 2 Found 1 test ( s ) . Creating test database for alias ' default ' ( ' test-db ' ) ... Operations to perform: Apply all migrations: myapp Running migrations: Applying myapp.0001_initial... OK System check identified no issues ( 0 silenced ) . test_sample ( myapp.tests.TestmMyApp ) ... ok -------------------------------------------------------------------- -- Ran 1 test in 0 .025s OK Destroying test database for alias ' default ' ( ' test-db ' ) ... 1-a、1-b ともに実行ログに Applying myapp.0001_initial... OK とあり、2 回ともマイグレーションが行われていることが分かります。 次に --keepdb を指定する場合。 $ # 2-a. 1回目 $ python manage.py test -v 2 --keepdb Found 1 test ( s ) . Using existing test database for alias ' default ' ( ' test-db ' ) ... Operations to perform: Apply all migrations: myapp Running migrations: Applying myapp.0001_initial... OK System check identified no issues ( 0 silenced ) . test_sample ( myapp.tests.TestmMyApp ) ... ok -------------------------------------------------------------------- -- Ran 1 test in 0 .010s OK Preserving test database for alias ' default ' ( ' test-db ' ) ... $ # 2-b. 2回目 $ python manage.py test -v 2 --keepdb Found 1 test ( s ) . Using existing test database for alias ' default ' ( ' test-db ' ) ... Operations to perform: Apply all migrations: myapp Running migrations: No migrations to apply. System check identified no issues ( 0 silenced ) . test_sample ( myapp.tests.TestmMyApp ) ... ok -------------------------------------------------------------------- -- Ran 1 test in 0 .009s OK Preserving test database for alias ' default ' ( ' test-db ' ) ... ポイントは 2-b 実行結果の中の No migrations to apply. で、2 回目のテストでマイグレーションがスキップされていることが分かります。 一方でテーブルデータはテストケースごとに初期化されるので、何度実行してもテストが通ることも分かります。 大規模なアプリケーションの開発時にローカルで何度もテストを実行したい場合におすすめです。 まとめ RevComm における 大規模な Django アプリケーションをチームで開発していくために工夫している取り組みについて紹介しました。 同じように Django で開発している方にとって参考になれば幸いです。 上記以外にも多くのノウハウがあり、一方でもちろん課題もあります。 私はプロジェクトに参画してまだ 1 か月程度ですが、改善できる点は積極的に対応して、チーム全体のパフォーマンスを高めていきたいと考えています。 RevComm のプロダクト開発には Python が広く使われており、主要プロダクトではバックエンドに Django を採用しています。 一緒にプロダクトを開発してくれるエンジニアを積極募集中です。 hrmos.co
アバター
Electron この記事は RevComm Advent Calendar 2022 の 12 日目の記事です。 はじめに 1. キルスイッチを用意する 2. パッケージに含まれる内容を確認する 3. Electron が機能を提供しているか確認する 4. キーボードショートカットを設定する 5. 署名・公証 6. ライセンス表記 7. アップデートに備える おわりに はじめに Electron は、JavaScript、HTML、CSS などの Web 技術を使用して、クロスプラットフォームのデスクトップアプリケーションを構築するためのフレームワークです。内部的には Node.js と Chromium が使用されており、OS ネイティブな機能をユーザーに提供することができます。Electron は Slack 、 Visual Studio Code 、 Discord などの多くの人気アプリケーションで使用されています。 デスクトップアプリケーションの開発には、ブラウザ上でのみ動作する Web アプリケーションの開発とは異なる注意点があります。本記事では、筆者が業務で Electron 開発中に得た知見の中から、開発をはじめる前に知っておきたかったことをまとめています。 1. キルスイッチを用意する 「キルスイッチ」は古いアプリを使用しているユーザーに対してアップデートを促す、もしくは利用を停止させる仕組みのことです。 PAGNI 法則 のひとつとしても挙げられており、ユーザーに後から提供することは難しい一方で、将来的には確実に必要になる重要な機能です。 筆者のチームでは electron-updater を使って自動アップデートの仕組みを実装し、アップデートを検知してダイアログを表示、自動更新することを可能にしました。また、WebView にアプリケーションのバージョンを連携するようにし、WebView 内のコンテンツ上で警告を表示して利用を停止させることも可能にしています。 2. パッケージに含まれる内容を確認する Electron ではソースコードを asar 形式 でパッケージングします。asar 形式は元のファイルの情報をそのまま含むので、すべてを復元することができます。ビルドツールによっては初期設定のままだとソースコードだけでなく、リポジトリ内部のすべてファイルをパッケージに含むこともあるので注意が必要です。 機密情報やパスワードを書いたファイルなどが流出しないよう、これらをパッケージに含めないように気をつけてください。 3. Electron が機能を提供しているか確認する 提供されている機能以外を使用すると OS の仕様などを考慮する必要が発生し、修正難易度が高くなるので注意が必要です。 例えば、Node.js のプロセスから通信を行う場合に「Electron axios」などで検索すると結構な数の記事が出てきますが、落とし穴があります。ブラウザであれば、Basic 認証のプロンプトを出すことや OS のプロキシ設定の読み取りなどはブラウザがサポートしている機能なので自ら実装する必要はありません。しかし、外部ライブラリを使用し独自の実装を行った場合は、それらを追加で実装する必要があります。このケースに対して、Electron は net モジュール を提供しています。net モジュールは Chromium を介した通信を行い、認証や OS 設定の読み取りを実行してくれます。 ブラウザと OS が連携されるような機能を用いる場合は、一通り公式ドキュメントに目を通して機能が提供されていないか確認しておくべきです。 4. キーボードショートカットを設定する Electron は Copy や Paste などの標準的なキーボードショートカットであっても、デフォルトの状態では追加されません。 menu モジュール を使用して明示的に追加する必要があります。 5. 署名・公証 署名は、開発者情報をアプリケーションに付与して開発元を明らかにするものです。 Windows では署名がない場合、セキュリティダイアログが表示されます。署名は Windows Authenticode に対応した証明書で行う必要があります。 macOS では署名することに加え、公証を受ける必要もあります。公証は、Apple がアプリケーションに対してセキュリティチェックを実施し、悪質なものでないかを確認することです。macOS 10.14.5 のリリース以降、署名済みアプリケーションは公証を受けなければ GateKeeper に引っ掛かり、ユーザーがインストールできないようになっています。 https://www.electronjs.org/ja/docs/latest/tutorial/code-signing https://developer.apple.com/jp/developer-id/ 6. ライセンス表記 MIT や Apache Software License などオープンソースライセンスには頒布物に著作権表記を含めることが条件に含まれています。ソースコードが閲覧可能な場合はそこに含めればよいのですが、 asar やバイナリに変換して配布した場合は、ユーザーが閲覧可能な形で別途ライセンスを公開する必要があります。筆者のチームでは、アプリケーション内のユーザーが閲覧可能なディレクトリ以下に LICENSE( yarn license にて作成) LICENSE.electron.txt(node_modules/electron/dist からコピー) LICENSES.chromium.html(node_modules/electron/dist からコピー) の 3 点をコピーしています。使用した node_modules の他に、Electron と Chromium のライセンスも含める必要があることに注意してください。 7. アップデートに備える Electron には version support policy が定められており、 Chromium が 4 週間毎にリリースされるのに合わせて 8 週間毎にリリース されることになっています。Microsoft Store に Chromium ベースのアプリケーションを公開する場合に 最新から 2 以上のメジャーバージョンの遅れは許容されない など、アップデートが必要になることがあります。 Electron を最大限に活用するために、初期の段階でテストやリリースを自動化するなど、積極的に投資していくことが望ましいです。 おわりに 以上、Electron 開発中に知った、開発をはじめる前に知りたかったことでした。 Web エンジニアが Electron に触れることは、普段ブラウザに支えられていて知らない事柄に触れるよい機会となると思います。よき Electron ライフを! www.revcomm.co.jp
アバター
この記事は RevComm Advent Calendar 2022 の 10 日目の記事です。 こんにちは!12 月 10 日のアドベントカレンダーを担当します、RevComm フロントエンドチーム所属の小山と申します。今回は、対象読者をフロントエンドの開発者として、「ペア設計」を通して、私の React に対する理解が深まったことを記事にしていきます。 ペア設計とは私の所属しているチームで実践している取り組みで、プロダクトの設計方針や懸念点を議題として、私が持っていって壁打ちをさせていただいているミーティングです。現在は週に 2 回、各 40 分程度行っており、フロントエンドチームの中でも JavaScript、TypeScript の開発経験が豊富な方にお願いしています。 ペア設計は RevComm 全体で導入されている制度ではありません。よりよいプロダクトを開発するために、このやり方で進めたいと提案して始めました。RevComm ではそれぞれのプロジェクトチームが自分たちにフィットする開発手法を創り出し実践しています。 今回の記事のテーマは、そんなペア設計の中で議題になったものです。では、そろそろ本題に入っていきましょう! コンポーネントの外でフックを使ったときに生じる Invalid hook call エラー 私の担当しているプロダクトでは、Next.js を使っており、GraphQL のクライアントとして Apollo Client を使い、状態管理に Recoil を使っています。 開発を進めていく中で、Apollo Client の状況に応じて Recoil の atom の値を変更したいケースが出てきました。これができると、バックエンド側でエラーが起きたときに通知を出したり、フォームの値を保存中に変更できないようにマスクをかけたりということが一括してできるようになります。 これは調べていくと、ApolloLink という、Apollo Client とサーバーの間でデータの流れをカスタマイズできる仕組みを使うことで実装できそうなことがわかりました。しかし、コンポーネントに反映させるところでつまづきました。 試しに下記のように ApolloLink の引数に Recoil のカスタムフックを使った関数を渡してみたところ、トランスパイルはできました。 const sampleRecoilLink = new ApolloLink (( operation , forward ) => { const setSampleState = useSetRecoilState ( sampleState ); setSampleState ( true ); return forward ( operation ) .map (( data ) => { return data ; } ); } ); しかし、実行時に以下のようなエラーがコンソールに表示されました。 Error: Invalid hook call. Hooks can only be called inside of the body of a function component. … 普段 React のコードを書いているときには遭遇しないエラーなので調べたところ、Recoil のリポジトリに同様の issue が起票されていました。 https://github.com/facebookexperimental/Recoil/issues/289 issue をみると recoil-nexus というライブラリでも解決できるようです。ただ、この問題の解決だけのために理解せずに導入したくないなという気持ちでした。その他、ここだけ Apollo Client で状態管理をするやり方も検討しましたが、あまり良いやり方ではありません。そこで、ペア設計の時間で相談することにしました。 結果として、Recoil のフックを使用するタイミングでファクトリー関数を使うことによって調整することで解決しました。 Invalid hook call エラーが発生するコード例 まず、以下が Invalid hook call エラーが発生したコード例です。 ルートの(基底となる)コンポーネント内に ApolloProvider を書いて、client (後述)を読み込ませています。Recoil も使いたいので RecoilRoot も追加しています。 function MyApp ( { Component , pageProps , router } : AppProps ) { return ( < RecoilRoot > < ApolloProvider client = { client } > < Component { ...pageProps } / > < /ApolloProvider > < /RecoilRoot > ); } client は別ファイルにて定義します。Apollo Client の以下のドキュメントを参考に実装すると、以下のようなコードになります。 https://www.apollographql.com/docs/react/data/subscriptions const httpLink = new HttpLink ( { uri: SAMPLE_HTTP_URL } ); const wsLink = new GraphQLWsLink ( createClient ( { url: SAMPLE_WS_URL } )); const splitLink = split ( ( { query } ) => { const definition = getMainDefinition ( query ); return definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ; } , wsLink , httpLink ); // 今回追加したいApolloLink const sampleRecoilLink = new ApolloLink (( operation , forward ) => { const setSampleState = useSetRecoilState ( sampleState ); // 実行時に Error: Invalid hook callとなる箇所 setSampleState ( true ); return forward ( operation ) .map (( data ) => { return data ; } ); } ); export const client = new ApolloClient ( { // ApolloLinkを追加 link: from( [ sampleRecoilLink , splitLink ] ), cache: new InMemoryCache (), } ); この書き方では setSampleState(true) の箇所で、Invalid hook call が発生します。 ファクトリー関数とフックの組み合わせ 私は原因が特定できていない状態だったのですが、ペア設計の中でヒントをもらえました。「フック を使用するのは React の中でなければダメで、Recoil の フック を使用するのは RecoilRoot コンポーネントより後でなければダメだよ」といった内容です。あまりピンとこなかったので、もうちょっと掘り下げてみると、「Recoil は基本 React 内でしか読めないので、 ApolloClient の生成をファクトリー関数で行い、Recoil の値が必要な場合は必要な情報を React 内からファクトリー関数に渡せばよい」とのこと。 聞いたときはなんとなくの理解だったのですが、手を動かしながら理解が深まりました。 // 関数に変更 const createSplitLink = () => split ( ( { query } ) => { const definition = getMainDefinition ( query ); return definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ; } , wsLink , httpLink ); // 関数に変更 const createSampleRecoilLink = ( setSampleState ) => new ApolloLink (( operation , forward ) => { setSampleState ( true ); return forward ( operation ) .map (( data ) => { return data ; } ); } ); // フックとして扱うためにプレフィックスを追加。 export const useClient = () => { const setSampleState = useSetRecoilState ( sampleState ); return new ApolloClient ( { link: from( [ createSampleRecoilLink ( setSampleState ), createSplitLink () ] ), cache: new InMemoryCache (), } ); } ; sampleRecoilLink を関数にすることで、定義した段階で実行されないようになっています。その他、client をカスタムフックとして、その中で useSetRecoilState を使うように変更しています。 上記のコードでは、useClient が呼び出された段階で、Recoil の useSetRecoilState が使われます。これで、RecoilRoot 配下のコンポーネント内で使う準備ができました。 図で簡単に示すと以下のようになるかと思います。ファクトリー化とカスタムフックを利用することで、コンポーネントの内部でuseClient を実行したタイミングで Recoil の useSetRecoilValue が実行されます。 ApolloProvider を RecoilRoot の後に読み込ませるように調整 あともう少しだけルートコンポーネントの調整が必要です。RecoilRoot の中で useClient() が使われるように、コンポーネントを分離します。 function MyApp ( { Component , pageProps , router } : AppProps ) { return ( < RecoilRoot > < ApolloProviderWrapper Component = { Component } pageProps = { pageProps } router = { router } / > < /RecoilRoot > ); } const ApolloProviderWrapper = ( { Component , pageProps , router } : AppProps ) => { const client = useClient (); return ( < ApolloProvider client = { client } > < Component { ...pageProps } / > < /ApolloProvider > ); } ; こうすると、以下の図のように Recoil で状態管理をしている内部で、フックを使うという構成にすることができます。 上記の手順で RecoilRoot の内側に Recoil のフックを入れて Recoil の状態を更新ができるようになりました。これで、Apollo Client の状態をみて、通信の途中でフォームの値を変更できないよう制御できるようになります。 終わりに 今回、ペア設計でもらえたヒントを元に実装を進めたおかげで、今まで自分が意識できていなかった React のルートコンポーネント周りでの読み込み順を意識できるようになりました。 ペア設計は、自分の理解できていないことを認識できる場になりますし、その他にもこちらで用意した実装方針が状況に合わせたベターなものになっているか確認できる機会としても機能しています。議題をしっかり持っていってわからないことに向き合うことで成長でき、プロダクトに生かすことができます。自分 1 人ではできない成長ができる Happy な場だと考えて、これからも向き合っていきたいです! RevComm ではエンジニアを募集しています。このブログを読んで興味を持っていただけたら、ぜひ採用サイトをチェックしてみてください。 www.revcomm.co.jp
アバター
この記事は RevComm Advent Calendar 2022 の 9 日目の記事です。 はじめに Hello World ! サーバーサイドエンジニアの矢島です。普段は MiiTel Analytics の開発を行っています。 2022年7月に入社してすぐに新機能の開発を任されて、実務で初めて Terraform を使った開発を経験しました。 この開発は、Terraform を使うのが初めてだったことに加えて以下のような要因も重なり、試行錯誤の連続でした。 しばらく変更されていないファイルが多々あったこと M1チップの MacBookという、利用者があまり多くない環境だったこと そこでこの記事では、初めての Terraform 開発で直面したトラブルとその解決方法をまとめました。M1 Mac でなくても発生しうる内容もありますので、特に僕のような Terraform ビギナーの方に有益な記事となれば嬉しいです。 初めてのTerraform 開発でよく起きたトラブルとその解決方法 M1 Mac で使えない provider が存在する $ terraform init Error: Failed to install provider M1 Mac では、 arm64 という命令セットアーキテクチャが採用されており、Intel Mac と実行できるバイナリファイルが異なります。 そのため、更新されなくなった provider や古いバージョンの provider を使用する際は、マニュアルインストールが必要になります。 1.Go をインストール( anyenv の利用を前提とします) $ anyenv install goenv $ goenv install 1.18.4 $ go version go version go1.18.4 darwin/arm64 2.provider を clone, build $ git clone git@github.com:hashicorp/terraform-provider-template.git $ cd terraform-provider-template $ make build 3.build されたファイルを配置 $ mkdir -p ~/.terraform.d/plugins/registry.terraform.io/hashicorp/template/2.2.0/darwin_arm64 $ cp ~/go/1.18.4/bin/terraform-provider-template ~/.terraform.d/plugins/registry.terraform.io/hashicorp/template/2.2.0/darwin_arm64 これで M1 Mac でも該当の provider が使えるようになり terraform init を実行できます。 Template provider に限らず、他の provider でも同様の方法で解決できます。 既存のコードが非推奨になっている 例えば、先に出てきた Template provider は、すでに非推奨となっています。 Terraform Registry Terraform や provider は頻繁に更新されているため、既存コードが非推奨になっているケースに遭遇することもあるかと思います。余裕があれば、書き換えていくのがいいでしょう。書き換えた際には、リソース自体に差分がでていないことを確認します。 1.Template provider の記述を削除 data "template_file" "task_definitions" { template = file("./task_definitions.json") vars = { container_app_name = "app" container_app_image = “XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app" } } 2.Templatefile function として追加 resource "aws_ecs_task_definition" "ecs_task_definition" { # 省略 container_definitions = templatefile("./task_definitions.json", { container_app_name = "app" container_app_image = “XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app" }) } 3.差分を確認 $ terraform plan No changes. Your infrastructure matches the configuration. Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed. Lockファイルの checksum error が発生する terraform init を実行したときに作成される lock ファイル( .terraform.lock.hcl )は下記のような形式になっています。h1 にローカルで使用しているプラットフォームのハッシュ値が、zh にprovider が配布するパッケージのハッシュ値が記録されています。 provider "registry.terraform.io/hashicorp/aws" { version = "4.45.0" constraints = ">= 4.45.0 hashes = [ "h1:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "h1:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "zh:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "zh:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", ] } 下記の checksum error はすでに lock ファイルがあり、これまで自分と同じプラットフォームの使用者がいなかった場合に発生します。 例えば M1 Mac で以下のエラーが発生した場合 darwin_arm64 が、これまでにないプラットフォームなので互換性がない状態です。 $ terraform init Error while installing hashicorp/template v2.2.0: the local package for registry.terraform.io/hashicorp/template 2.2.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms) これを解消するには、lock ファイルを削除した後に再度 terraform init を実行する必要があります。 しかしそれだけだと、darwin_arm64 にしか互換性のない lock ファイルが生成されてしまい、その他のプラットフォームで terraform init できなくなってしまいます。 そこで以下のように、さまざまなプラットフォームに互換性のある lock ファイルを生成します。 $ terraform providers lock \ -platform=windows_amd64 \ -platform=linux_arm64 \ -platform=linux_amd64 \ -platform=darwin_amd64 \ -platform=darwin_arm64 Command: providers lock | Terraform | HashiCorp Developer 万が一 terraform providers lock の後に terraform init をしても checksum error が発生する場合、以下を実行すると解消します。 Lock ファイルを削除 terraform init をして作られたlockファイルの h1 を退避 Lock ファイルを削除後に terraform providers lock 生成された lock ファイルに退避していた h1 を追加 環境ごとに provider のバージョンに差が出る 前述の通り、Terraform や provider は頻繁に更新されているため terraform init や -upgrade 、 terraform providers lock をしたときに一気にバージョンが上がったり、他の環境とのバージョンの差が出てまうことがあります。 ローカルでこれらの操作をするときにバージョンを変えたくない場合は、 required_providers の version を一時的に固定する方法があります。 terraform { # 省略 required_providers { aws = { source = "hashicorp/aws" version = “4.45.0" } } } Provider Requirements - Configuration Language | Terraform | HashiCorp Developer 最後に 以上が初めての Terraform 開発で起きたトラブルとその解決方法でした。 Terraform 開発は tfstate 管理、IAM role やデプロイフローの設計、リモートとの差分発生の防止など、アプリケーションとは異なるチームでの開発・運用体制が求められると思います。RevComm ではスタートアップとしてアジリティが高くかつ安定した Terraform の開発・運用体制が作られており、開発体験がとてもよかったので、その辺りについても今後記事にできたらと思っています。 RevComm ではエンジニアを募集しています。このブログを読んで興味を持っていただいたら、ぜひ採用サイトをチェックしてみてください。 www.revcomm.co.jp
アバター
この記事は、RevComm Advent Calender 8日目の記事です。 はじめに こんにちは。PBX チームの山崎です。 RevComm では毎週 Tech Talk と題して社内勉強会が実施されています。 その中で Docker の hello-world というイメージの存在を知り、早速使ってみました。 $ docker run --rm hello-world Hello from Docker! This message shows that your installation appears to be working correctly. ( snip ) Hello World が表示されました。これ自体は特に面白みはありません。 ところが思いのほかイメージサイズが小さく、これはちょっと気になります。 $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu latest 3c2df5585507 4 weeks ago 69 .2MB hello-world latest 46331d942d63 8 months ago 9 .14kB ということで調べてみました。 中身を見てみよう 普段使うイメージは Linux のユーザーランドがほぼそのまま動いているので、まずはシェルに入ってみます。 $ docker run -it --rm hello-world /bin/sh docker: Error response from daemon: failed to create shim: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: " /bin/sh " : stat /bin/sh: no such file or directory: unknown. 失敗しましたね。 このサイズなら当然 sh は入らないので失敗するのは妥当です。 しかし中身が分からないのは困ったなとマニュアルを眺めていると、docker image save というコマンドがありました。このコマンドはイメージを tar ball にできるとあるので早速やってみます。 $ mkdir docker-work $ cd docker-work/ $ docker image save hello-world | tar xvf - $ tree --noreport . ├── 42e000e434c92e9a1ddac6cccd9219b2043d53ddf81e9abbb285dc58edea4f79 │ ├── VERSION │ ├── json │ └── layer.tar ├── 46331d942d6350436f64e614d75725f6de3bb5c63e266e236e04389820a234c4.json ├── manifest.json └── repositories 取り出せました。manifest.json が何やらメタデータっぽい雰囲気を出しているので確認してみます。 [ { " Config ": " 46331d942d6350436f64e614d75725f6de3bb5c63e266e236e04389820a234c4.json ", " RepoTags ": [ " hello-world:latest " ] , " Layers ": [ " 42e000e434c92e9a1ddac6cccd9219b2043d53ddf81e9abbb285dc58edea4f79/layer.tar " ] } ] 名前から、layer.tar がファイルシステムのレイヤーかなと想像できます。解凍してみましょう。 $ cd 42e000e434c92e9a1ddac6cccd9219b2043d53ddf81e9abbb285dc58edea4f79/ $ tar xvf layer.tar x hello いかにもなファイルが出てきました。ではこれを実行してみると... $ docker run -– rm -it -v $( pwd ) :/work -w /work ubuntu root@6cbf4ec43930:/work# ./hello Hello from Docker! This message shows that your installation appears to be working correctly. ( snip ) Hello World が表示されましたね。 となると、この hello ファイルが何であるか気になります。調べてみましょう。 root@6cbf4ec43930:/work# apt update && apt install -y file binutils less root@6cbf4ec43930:/work# file hello hello: ELF 64-bit LSB executable, ARM aarch64, version 1 ( SYSV ) , statically linked, stripped root@6cbf4ec43930:/work# objdump -D hello | less static link なので、きっと libc もリンクして単一バイナリで動くようにしているのかなと思いきや、その割にはコードが小さいですね。逆アセンブルしてみるかと objdump を眺めてみても strip されているのでなかなかつらいものがあります。 ここまで調べたら答えを見てもいいでしょうと言い訳しながら答えを開いてみます。 https://github.com/docker-library/hello-world/blob/master/hello.c なるほど。システムコールを直接実行していますね。 すなわち、hello-world イメージでは単体で動作するバイナリを起動しているということがわかりました。 中身を入れ替えてみよう 中身を理解できたら、今度は書き換えてみたくなりますよね。ということで違うメッセージを表示してみましょう。 ビルド環境を整えるのが面倒なので、元の hello ファイルを書き換えることにします。 root@6cbf4ec43930:/work# apt install bsdmainutils root@6cbf4ec43930:/work# hexdump -c hello | less ( snip ) 0000c20 300 003 _ 326 \n H e l l o f r o m 0000c30 D o c k e r ! \n T h i s m e s 0000c40 s a g e s h o w s t h a t ( snip ) 0x0c30 = 3120 バイト以降を書き換えればよさそうですね。やってみましょう。 root@6cbf4ec43930:/work# echo -n ' RevComm ' | dd of =hello bs = 1 seek = 3120 count = 7 conv =notrunc root@6cbf4ec43930:/work# ./hello Hello from RevComm ( snip ) メッセージが変わりました。後片づけをして、Docker ホストに戻ります。 root@6cbf4ec43930:/work# rm layer.tar root@6cbf4ec43930:/work# tar -cf layer.tar hello root@6cbf4ec43930:/work# rm hello ここまでで、以下のようなディレクトリ構成になっています。 $ tree --noreport . ├── 42e000e434c92e9a1ddac6cccd9219b2043d53ddf81e9abbb285dc58edea4f79 │ ├── VERSION │ ├── json │ └── layer.tar ├── 46331d942d6350436f64e614d75725f6de3bb5c63e266e236e04389820a234c4.json ├── manifest.json └── repositories 変更したファイルを docker image load で取り込みます。 しかしながら、このままではうまくいきませんでした $ docker image rm hello-world $ tar -c . | docker image load efb53921da33: Loading layer [==================================================>] 10.75kB/10.75kB invalid diffID for layer 0: expected "sha256:efb53921da3394806160641b72a2cbd34ca1a9a8345ac670a85a04ad3d0e3507", got "sha256:695cd43dbd538aae8230019f942d69bc53390dd7710063aae6b58cbb469414e1" sha256 ハッシュが異なると言っています。おそらく layer.tar を書き換えたので、そのハッシュ値を各所で更新する必要がありそうです。 layer.tar の sha256 の値と、元の hello-world イメージ内設定ファイルを眺めていくと一致する箇所がありました。試行錯誤したところ、以下のように書き換えると動きました。 sha256sum layer.tar の結果で、46331d(省略).json の "diff_ids" を更新する 46331d(省略).json のファイル名を、自分自身の sha256 の値に変更する manifest.json の "Config" を、2.で変更したファイル名にする でもってワクワクしながら実行してみると... $ tar -c . | docker image load 695cd43dbd53: Loading layer [==================================================>] 10.75kB/10.75kB Loaded image: hello-world:latest $ docker run --rm hello-world Hello from RevComm (snip) 動きました! まとめ 最初はなんだ Hello World かと思っていましたが、探ってみると Docker の仕組みに対する理解が深まりました。Hello World といえど奥が深いですね。 おわりに RevComm にはさまざまなバックグラウンドを持ったエンジニアが集まっています。Tech Talk の他にもさまざまな場面で知らない技術に触れ、そして業務で挑戦する機会が数多くあります。 あなたのご参加をお待ちしております! hrmos.co
アバター
この記事は、RevComm Advent Calender 7 日目の記事です。 RevComm インフラチーム所属の平島です。主にクラウド (AWS) を担当しています。 今回は、最近 RevComm で導入した AWS Support App in Slack について紹介したいと思います。 AWS Support App in Slack とは 前提条件 導入方法 IAM Role を作成する Slack channel を作成 Slack と AWS を連携させる Slack Workspace の認証 Slack channel を登録する channel に AWS Support App を招待する (補足)複数の AWS アカウントで利用する場合 使い方 ケースの起票 備考 AWS Support からの回答への対応(返信 / 解決) ケースの検索 Service Quota (上限緩和)申請 まとめ AWS Support App in Slack とは 2022 年 8 月 24 日に発表された比較的新しいサービスです( 公式ブログ )。 Slack のチャットを通じて AWS Support への問い合わせや回答への対応ができるようになります。 以前は言語の選択ができず英語でしかケースの起票ができなかったため、導入したものの社内でなかなか使ってもらえませんでした。 ところが最近、いつの間にか 日本語も選択できる ようになっていました! 日本語話者のエンジニアが使う上での障壁が 1 つなくなりました。 今後社内でのさらなる利活用が期待できそうです。 前提条件 Slack を利用していること AWS Support のプランが Business plan 以上であること 導入方法 IAM Role を作成する Slack からの AWS Support への接続を許可するための IAM Role、また role にアタッチする policy を作成します。 policy は AWS Support アプリへのアクセスの管理 - AWS Support を参考にカスタムの policy を作成してもいいですが、既に以下の AWS Managed Policy が用意されているので RevComm ではこちらを採用しました。 AWSSupportAppFullAccess AWSSupportAppReadOnlyAccess ケースの閲覧だけでなく、ケースの起票や返信をする場合は AWSSupportAppFullAccess をアタッチする必要があります。 role の 信頼関係には以下を指定します。 { " Version ": " 2012-10-17 ", " Statement ": [ { " Sid ": "", " Effect ": " Allow ", " Principal ": { " Service ": " supportapp.amazonaws.com " } , " Action ": " sts:AssumeRole " } ] } Slack channel を作成 Slack で AWS Support App を利用する channel を作成します。 channel は public/private どちらでも利用できますが、セキュリティの観点から、 private channel を採用し関係者のみを招待して使うことが推奨 されています。 Slack と AWS を連携させる Slack Workspace の認証 AWS Management Console (以下、コンソール)から Support Center にアクセスし、左メニューから「AWS Support App in Slack」を選択します。 「Authorize workspace」をクリックすると、認証画面に遷移するので「Allow」をクリックします。 Slack channel を登録する 左メニューから Slack Configuration を選択します。 ここで Slack 上で表示するアカウント名を変えることもできます。 「Add channel」ボタンをクリックして channel の登録作業をします。 Slack workspace、 Slack channel、 Permissions (IAM Role) はそれぞれ事前に作成したものを選択します。 private channel を使う場合は Channel ID が必要になります。 Channel ID は channel のリンク URL の最後のスラッシュ以降の文字列で、 C01234A5BCD のような形式のものです。 通知設定(Notifications)はサポートの利用状況に応じて選択してください。 RevComm は今のところ問い合わせが頻発するような状況ではないため、とりあえずすべての通知を ON にしています。 channel に AWS Support App を招待する 作成した Slack channel に AWS Support app を招待する必要があります。 channel 内で /invite @awssupport を入力します。 (補足)複数の AWS アカウントで利用する場合 複数のAWS アカウントで AWS Support App in Slack を利用する場合、 アカウントごとに channel を作成する 1つの channel で複数アカウントに対応させる という選択肢が考えられますが、RevComm では後者を採用しています。 1 つの channel で複数アカウントに対応させるためにには、それぞれのアカウントで IAM Role の作成 Slack WorkSpace の認証 Slack channel の登録 が必要になります。 複数のアカウントで設定をすると、例えばケース起票の際にプルダウンでアカウントが選択できるようになります。 使い方 AWS と連携させた channel 内で、以下のコマンドをチャット欄に入力して操作します。 ケース起票: /awssupport create または /awssupport create-case ケース検索: /awssupport search または /awssupport search-case Service Quota (上限緩和)申請: /awssupport quota これらのコマンドをチャット欄に入力すると、対応する入力フォームが表示されます。 ケースの起票 /awssupport create をチャット欄に入力し送信すると、ケース起票用のモーダルが表示されます。 指示にしたがって 3 ページにわたり必要事項を埋めていきます(コンソール での起票作業の場合と全く同じ内容です)。 モーダルの 3 ページ目に 「Select Language」 のプルダウンがあるので、日本語で用件を書いた場合は「日本語」を選択してください。 最後まで入力して「Review」をクリックすると、以下のように入力内容がメッセージとして表示されます。 修正する場合は「Edit」を押すと再度モーダルが開き、編集することができます。 起票を取りやめる場合は、このメッセージ自体を削除してください。 スクリーンショットなどのファイルを添付する場合は 「Attach file」をクリック チャット欄で「+」ボタンからファイルを添付 @AWS Support のメンションをつけて送信 すると、ファイルがアップロードされ Review の内容が更新されます。 内容に問題がなければ「Create case」をクリックします。 ケースの起票に成功したら、以下のような通知が届きます。 以降、当該ケースに関するサポートとのやりとりはこのスレッドに追加で通知されます。 備考 コンソールから起票した場合でも channel に通知が届きます。 言語で English を選択した場合はオペレーターとのやりとりに Live Chat も選択でき、Slack 上で直接オペレータとチャットができるようです(こちらはまだ試していません)。 AWS Support からの回答への対応(返信 / 解決) AWS Support からの通知には「See details」のボタンが設置されています。 これをクリックすると、ケースの詳細がメッセージとして表示されます。 メッセージ下部には以下のようにボタンが設置されています。 「Reply」を押すと返信用のモーダルが表示されます。 「Resolve」または「Reopen」を押すとケースを解決または再開できます。 ケースの検索 /awssupport search をチャット欄に入力し送信すると、ケース検索のフォームが表示されます。 検索条件を入力し「Search」を押すと結果が表示されます。 検索結果の各ケースの「See details」を押すと詳細が表示され、回答への返信やケースの解決または再開ができます。 Service Quota (上限緩和)申請 /awssupport quota をチャット欄に入力し送信すると、Service Quota 申請用のモーダルが表示されます。 デフォルト・現在の設定値が両方とも表示されるのが便利です。 必要事項を記入し終えたら「Submit」を押して申請完了です。 まとめ 今回は AWS Support App in Slack の導入方法と使い方について紹介しました。 コンソール での操作と比較して感じたメリットとしては ログインの手間が省ける (ブラウザで発生する)画面遷移時のロード時間がない or 体感で少ない ケース起票者以外の関係者でもサポートからの回答がきたことに気づきやすい ケースを探す際に Slack 検索を使ってキーワード検索ができる /awssupport search ではできない といったところでしょうか。 一方で、障害や不具合の発生時にサポートに問い合わせる際は、コンソールでリソースの ID を調べつつ文章を書くことも多いです。 その場合、結局コンソールにログインするため、「ログインの手間が省ける」がメリットになる状況は限定的かもしれません。 導入したてなこともあり社内で活用されてるとはまだまだ言い難い状況ですが、より便利な使い方を模索して、多くの社内のエンジニアに使ってもらえるようにしていきたいです。
アバター
はじめに この記事は 2022 年の RevComm アドベントカレンダー 6 日目の記事です。 こんにちは。株式会社RevCommで社内向けシステム開発・運用を担当している Machida Kensuke です。 みなさんは GCP を利用したサービスの開発をされることはありますか?RevComm では基本的にクラウド基盤として AWS を採用していますが、一部では GCP も採用しています。私はデータ基盤として BigQuery を利用しているサービスの開発に参加しました。 BigQuery は他のデータウェアハウスと比較してもコスト効率が良い課金形態で、社内システムでも通話料計算やデータ分析等の用途で使用しています。 環境構築に必要なものが多岐にわたるため、開発の導入部分でとても手こずりました。 この経験から、初めて GCP や BigQuery を触る方でもすぐに着手できるようになったらいいなと思い、この記事を書くことにしました。 手順 前提条件 準備 クライアントライブラリをインストール Cloud Platform プロジェクトを作成 認証設定 クライアントを初期化 データセットを作成 テーブルを作成 BigQuery APIを使用したテーブルの CRUD 操作 やってみる では早速やっていきましょう! 前提条件 Python はインストール済みとします。 準備 BigQuery API の Cloud クライアント ライブラリをインストールする 公式のクライアントライブラリ を使用します。 ※2022/12/06現在、対応の Python バージョンは3.7~3.10 です。 pip install google-cloud-bigquery Cloud Platform プロジェクトを作成 「 プロジェクトを作成 」からプロジェクトを作成します。 プロジェクトでは、API の管理、課金の有効化、共同編集者の追加と削除、Google Cloud リソースに対する権限の管理などを行います。 認証設定 Python から作成したプロジェクトへ API アクセスできるように認証を設定します。 API へのアクセスの有効化 からAPI アクセスを有効化します。 Google Cloud のホームページ の手順にしたがって、サービスアカウントを作成します。 Google Cloud のホームページ の手順にしたがって、サービスアカウントキーを作成して service-account-key-file.json をダウンロードします。この json ファイルを GCP の認証に使用します。 service-account-key-file.json クライアントを初期化 設定が完了したので、実際に Python から BigQuery を動かしてみましょう! まずは、下記のコマンドを実行してファイルを作成します。 touch create_dataset.py 作成した create_dataset.py にライブラリのインポートと認証情報を使用して BigQuery クライアントを初期化します。 from google.cloud import bigquery from google.oauth2 import service_account # 前ステップでダウンロードした service-account-key-file.jsonのフルパスを指定 key_path = "/path/to/service-account-key-file.json" credentials = service_account.Credentials.from_service_account_file(key_path, scopes=[ "https://www.googleapis.com/auth/cloud-platform" ]) client = bigquery.Client(credentials=credentials, project=credentials.project_id) 今回は、 google.oauth2.service_account.Credentials.from_service_account_file を使用して、サービスアカウントキーファイルによる認証を行います。 データセットを作成 次に、データセットを作成します。 BigQuery におけるデータセットとは「テーブルを作成するために必要な箱」のようなものです。 参考: データセットの概要 | Google Cloud dataset_id = "{}.sample_dataset" .format(client.project) dataset = bigquery.Dataset(dataset_id) dataset.location = "asia-northeast1" client.create_dataset(dataset) 前述したクライアントの初期化を含めたソースコード全体は下記のとおりです。 from google.cloud import bigquery from google.oauth2 import service_account def init_client () -> bigquery.Client: key_path = "/path/to/service-account-key-file.json" credentials = service_account.Credentials.from_service_account_file( key_path, scopes=[ "https://www.googleapis.com/auth/cloud-platform" ] ) return bigquery.Client(credentials=credentials, project=credentials.project_id) def create_dataset (client: bigquery.Client): dataset_id = "{}.sample_dataset" .format(client.project) dataset = bigquery.Dataset(dataset_id) dataset.location = "asia-northeast1" client.create_dataset(dataset) if __name__ == "__main__" : client = init_client() create_dataset(client) Python コマンドから上記のコードを実行します。 $ python3 create_dataset.py Cloud コンソール から作成したデータセットを確認して、 sample_dataset というデータセットが作成されていれば OK です。 データセット情報 テーブルを作成 次に、作成したデータセットの中にテーブルを作成します。 テーブルは、先ほど作成したデータセット ID とスキーマを定義することで作成できます。 from google.cloud import bigquery from google.oauth2 import service_account def create_table (client: bigquery.Client): dataset_id = "{}.sample_dataset" .format(client.project) dataset = client.get_dataset(dataset_id) table_id = "{}.{}.sample_table_name" .format(client.project, dataset.dataset_id) schema = [ bigquery.SchemaField( "name" , "STRING" , mode= "REQUIRED" ), bigquery.SchemaField( "age" , "INTEGER" , mode= "NULLABLE" ), ] table = bigquery.Table(table_id, schema=schema) client.create_table(table) if __name__ == "__main__" : # init_client() は前述のものを使用 client = init_client() create_table(client) 上記のコードを実行して、Cloud コンソールから sample_table_name という名前の空テーブルが作成されていれば OK です。定義したスキーマも設定されています。 テーブル情報 BigQuery APIを使用したテーブルの CRUD 操作 空のテーブルが作成できたところで、テーブル上のレコードを CRUD 操作してみます。 データの登録(Create) まずはデータの登録から。 方法は色々ありますが、今回は Python でよく使用される Pandas DataFrame のデータをテーブルに登録します。 参考: テーブルデータの管理 | Google Cloud データの登録は、先ほど作成したテーブルのスキーマを job_config に定義することで行えます。BigQuery のジョブとは、データの読み込み、データのエクスポート、データのクエリ、データのコピーなど、BigQuery がユーザーに代わって実行するアクションのことです。 参考: BigQuery ジョブの概要 | Google Cloud from google.cloud import bigquery from google.oauth2 import service_account def create_data (client: bigquery.Client): dataset_id = "{}.sample_dataset" .format(client.project) dataset = client.get_dataset(dataset_id) table_id = "{}.{}.sample_table_name" .format(client.project, dataset.dataset_id) schema = [ bigquery.SchemaField( "name" , bigquery.enums.SqlTypeNames.STRING, mode= "REQUIRED" ), bigquery.SchemaField( "age" , bigquery.enums.SqlTypeNames.INTEGER, mode= "NULLABLE" ), ] job_config = bigquery.LoadJobConfig(schema=schema) dataframe = pd.DataFrame( [ { "name" : "Machida" , "age" : 2 , }, { "name" : "“Kensuke”" , "age" : 5 , }, ] ) job = client.load_table_from_dataframe(dataframe, table_id, job_config=job_config) job.result() if __name__ == "__main__" : client = init_client() create_data(client) 上記のコードを実行して、Cloud コンソールから作成したテーブル sample_table_name に 2 行 2 列のレコードが作成されていれば OK です。 テーブルのプレビュー データの取得(Read) 続いて、データの取得です。 BigQuery では標準 SQL によるクエリ実行が可能です。 データの取得は、SELECT 句を使用して BigQuery クライアント ライブラリのクエリジョブで取得します。 先ほど作成した {"name": "Machida", "age": 2} と {"name": "Kensuke", "age": 5} の 2 レコードを取得します。 pd.to_dataframe() メソッドを使用すると Pandas DataFrame のデータとして取得できます。 from google.cloud import bigquery from google.oauth2 import service_account def read_data (client: bigquery.Client): dataset_id = "{}.sample_dataset" .format(client.project) dataset = client.get_dataset(dataset_id) table_id = "{}.{}.sample_table_name" .format(client.project, dataset.dataset_id) query = f """ SELECT * FROM `{table_id}` LIMIT 2; """ dataframe = ( client.query(query) .result() .to_dataframe( create_bqstorage_client= True , ) ) print (dataframe) if __name__ == "__main__" : client = init_client() read_data(client) 上記のコードを実行して、 {"name": "Machida", "age": 2} と {"name": "Kensuke", "age": 5} の Pandas DataFrame 型データを取得できていれば OK です。 データの取得 データの更新(Update) 続いて、データの更新です。データの更新は UPDATE 句を使用したクエリジョブを使用します。 先ほど作成した "name": "Machida", "age": 2 レコードの name を Yamada に更新します。 from google.cloud import bigquery from google.oauth2 import service_account def update_data (client: bigquery.Client): dataset_id = "{}.sample_dataset" .format(client.project) dataset = client.get_dataset(dataset_id) table_id = "{}.{}.sample_table_name" .format(client.project, dataset.dataset_id) query = f """ UPDATE `{table_id}` SET name = "Yamada" WHERE name = "Machida"; """ client.query(query).result() if __name__ == "__main__" : client = init_client() update_data(client) 上記のコードを実行して、Cloud コンソールから sample_table_name テーブルの {"name": "Machida", "age": 2} レコードが {"name": "Yamada", "age": 2} レコードに更新されていれば OK です。 データの削除(Delete) 最後は、データの削除です。データの削除は DELETE 句を使用したクエリジョブを使用します。 先ほど変更した "name": "Yamada", "age": 2 レコードを削除します。 from google.cloud import bigquery from google.oauth2 import service_account def delete_data (client: bigquery.Client): dataset_id = "{}.sample_dataset" .format(client.project) dataset = client.get_dataset(dataset_id) table_id = "{}.{}.sample_table_name" .format(client.project, dataset.dataset_id) query = f """ DELETE FROM `{table_id}` WHERE name = "Yamada"; """ client.query(query).result() if __name__ == "__main__" : delete_data(client) 上記のコードを実行して、Cloud コンソールから sample_table_name テーブルの "name": "Yamada", "age": 2 が削除されていれば OK です。 最後に いかがでしたか? 本記事では、 Python と BigQuery を使用した開発の始め方と基本的なテーブル操作を記述しました。本記事が Python と BigQuery を使用した開発を始める方の一助となれば幸いです。
アバター
この記事は、RevComm Advent Calender 5日目の記事です。 RevComm の渋谷といいます。MiiTel Phone Mobile のバックエンドや E2E テストなどを主に担当しています。 それとは別に、TechTalk (エンジニア主体の技術共有の場) 運営にも 2021 年 8 月頃から参加しています。 今回は TechTalk 運営企画として RevComm のエンジニアメンバーからアンケートで募集した VSCode おすすめ拡張機能の中から 10 件を選りすぐってご紹介させていただきます。 コーディング補助 Code Spell Checker - スペルチェッカー Error Lens - エラー該当行にエラーメッセージを表示 indent-rainbow - インデントの色分け Python postfix completion - Python 版後置テンプレート ビューワー・エディター audio-preview - 音声ファイルビューワー Rainbow CSV - CSV ビューワー& SQL ライク実行環境 Git Graph - Git 履歴ビューワー メモ Bookmarks - コードの指定行にブックマークを追加 open Junkfile - 指定拡張子のテンポラリファイルをワンコマンドで作成 番外編 vscode-pets - VSCode 内で飼えるペット さいごに コーディング補助 Code Spell Checker - スペルチェッカー Code Spell Checker ID: streetsidesoftware.code-spell-checker スペルチェックを行う拡張機能です。コードやコメントにタイプミスがあった場合に下線が引かれます。 PR などで指摘を受けるとちょっと恥ずかしいので、事前にチェックが入るのは嬉しいですね。 推薦者からも「タイポがなくなる!」と力強いコメントをいただきました。 ただし、チェック対象の文字列が別の意味をもつ場合には指摘されないので、やはり人の目が重要になってきます。 Error Lens - エラー該当行にエラーメッセージを表示 Error Lens ID: usernamehw.errorlens エラー該当行にエラーレベル(エラーや警告など)とエラーメッセージを描画してくれる拡張機能です。 エラーがとても見やすく表示されますので、見落としが劇的に減ります。 発表中にも早速導入してみたメンバーから「いい感じ」というコメントをいただいています。 indent-rainbow - インデントの色分け indent-rainbow ID: oderwat.indent-rainbow インデントごとに段を色分けしてくれる拡張機能です。Python や YAML など、インデントが意味をもつ言語には非常にありがたい機能です。 色の表示の仕方はライン(引用画像左側)と背景色(引用画像右側)から選ぶことができます。 発表中にも何名かのメンバーが早速導入して「素敵!」と感想をいただいています。 Python postfix completion - Python 版後置テンプレート Python postfix completion ID: filwaline.vscode-postfix-python 変数の後にドットで処理を入力するとテンプレートに従って構文を補完してくれる、いわゆる「後置テンプレート」機能拡張機能です。( 参考 ) 簡単な式を入力する際にカーソルを前後に移動させる必要がなくなります。 サジェスト機能もありますので、該当する変数に対して適用できるテンプレートを見つけやすいです。 ビューワー・エディター audio-preview - 音声ファイルビューワー audio-preview ID: sukumo28.wav-preview 音声を扱うことの多い RevComm らしいおすすめ拡張機能です。VSCode 上で音声の波形を確認しながら再生することができます。 推薦者からは「サーバー上の音声聞くのに超便利なので、 PBX と Reseach のメンバーは全員インストールしといてください!」という熱烈なコメントをいただきました。 wav、mp3 、 aac などにも対応しています。 Rainbow CSV - CSV ビューワー& SQL ライク実行環境 Rainbow CSV ID: mechatroner.rainbow-csv CSV ビューワーの拡張機能です。列別に色分けして表示したり、SQL ライクなクエリを実行して該当行のみを抽出したりすることができます。 推薦者からは「ちょっとした調査に便利。SQLに比べてできないこともありますが、GROUP BY とかできます。」とコメントをいただきました。 SQL を実行するためには CSV ファイルのエンコードを UTF-8 にする必要があります。 Git Graph - Git 履歴ビューワー Git Graph ID: mhutchie.git-graph Git の履歴をグラフで確認でき、Git 操作も行える拡張機能です。 推薦者からは「コミット履歴・ブランチ状況が手軽にグラフで見られるのでお気に入りです」とコメントをいただいています。 該当コミットのコードを VSCode 上で変更を比較できたり、ブランチに対する操作が行えます。 筆者はブランチ名を簡単にコピーできる機能が地味にお気に入りです。 メモ Bookmarks - コードの指定行にブックマークを追加 Bookmarks ID: alefragnani.Bookmarks 特定行に対してブックマークを付けられる拡張機能です。 推薦者からは「前日までの作業箇所やポイントなどを bookmark できて、どこで何をやったかが明確になる」とコメントをいただいています。 ブックマークにはラベルを付けることもできますので、どういった意図で付与したのかを後で見返すのも簡単です。 open Junkfile - 指定拡張子のテンポラリファイルをワンコマンドで作成 open Junkfile ID: hidenba.open-junkfile 日時+指定拡張子のテンポラリファイルを簡単に作れる拡張機能です。ちょっとした覚え書きやスクリプトなどに便利ですね。 こちらは発表中にメンバーからコメントで教えていただきました。 番外編 vscode-pets - VSCode 内で飼えるペット vscode-pets ID: tonybaloney.vscode-pets VSCode 内でいろいろな種類のペットを飼うことができる拡張機能です。 推薦者からは「ボールを投げて遊ぶことも出来ます」とコメントをいただきました。 コーディングに疲れた時に息抜きで遊ぶのもいいかもしれませんね。 さいごに RevComm では、このようにチームを超えて皆でナレッジを共有しながら共に良いものを作り上げていこうというチームワークがあります。 TechTalk ではほぼ毎週さまざまなテーマの発表があります。 この記事で RevComm に興味を持ってくださった方がいらっしゃいましたら、ぜひ奮ってご応募ください。 採用情報|株式会社RevComm(レブコム)
アバター
この記事は RevComm Advent Calendar 2022 の 4 日目の記事です。 RevComm で音声処理の研究開発を担当している加藤集平です。私は ADHD (注意欠陥・多動症)という障害を抱えています。私の場合は仕事をしていく上で困難がある(障害を持たない人と同じやり方では困難に直面する)のですが、それらの困難にどのように対処しようとしているのかを紹介します。また、弊社の働き方の特徴であるフルフレックス・フルリモート環境が及ぼす影響についても取り上げます。 加藤集平(かとう しゅうへい) シニアリサーチエンジニア。RevCommには2019年にジョインし、音声処理を中心とした研究開発を担当。ADHDと付き合いつつ業務に取り組む2児の父。 個人ウェブサイト X → 過去記事一覧 本記事を読むにあたっての注意 私は医師でもその他の ADHD の専門家でもありません。 ADHD に関する正確な情報は、専門家の発信をご参照ください 。 ADHD の症状(困難に直面するポイントあるいは本人の特性)は人によって異なる ことが知られています。また、同じ症状に対して同じ対処が有効とは限りません。本記事で取り上げるのは私の症状と私が実践している対処法であり、万人に通用するものではありません。 ADHD の診断は医師のみが行うことができます 。自己判断はかえって困難を増大させるおそれがあります(たとえば、症状が似た違う病気かもしれません)。 ADHD とは ADHD (注意欠陥・多動症)とは、精神障害のうち発達障害に分類されるものの一つです。発達障害とは、生まれつきみられる脳の働き方の違いにより、幼児のうちから行動面や情緒面に特徴がある状態です *1 。発達障害の中でも ADHD は不注意・多動性・衝動性の3症状を主な特徴としており、それらの症状の影響で日常生活・学業・仕事などに様々な困難が生じることがあります *2 。かつては子供だけに見られる病気と考えられていましたが、現在では大人になっても症状が継続する場合があることが知られています。 私と ADHD 私が ADHD と診断されたのは、2017 年(30 歳頃)のことでした。当時は前年に発症した強迫性障害 *3 という病気の治療のために心療内科に通っており、通院・治療の過程で ADHD であることが発覚しました。 強迫性障害とあわせて障害者手帳の交付を受けています 思えば物心ついた頃から忘れ物や物をなくすのは日常茶飯事で、部屋は常に散らかっており、コツコツ勉強することは決してなく、学校のテストではよく不注意で失点をしていました。 大人になり仕事を始めてからは、順序立てて仕事を処理することが苦手で締切に間に合わなかったり、他人に出した指示をすっかり忘れたり、単調な作業ですぐに寝てしまったり、体調に波があるために毎日 8 時間パフォーマンスを出し続けることが難しかったりして、仕事の遂行に支障をきたしていました。また、かつての勤務先では毎日オフィスに出社していたのですが、電話番をすることや周囲の話し声(雑音)が苦痛で頭がいっぱいになったりといった困難もありました。 診断を受けてからは、定期的に通院の上、服薬および日常生活の中での治療を続けています。 フルフレックス・フルリモート環境における恩恵と困難 ADHD を持つ人にとって、弊社のようなフルフレックス・フルリモート環境は適しているのでしょうか?私の場合は恩恵のほうが大きく勝りますが、フルフレックス・フルリモートならではの、オフィス出社にはない困難も感じています。これらの恩恵と困難を紹介します。 恩恵 体調の波を吸収しやすい(フルフレックス) ADHD を持つ人すべてに当てはまるわけではないと思いますが、私は体調に比較的大きな波があります。つまり調子のいい日と悪い日の仕事のパフォーマンスの差がかなり大きいです。 フルフレックスの制度下では体調に合わせて比較的柔軟に勤務時間(長さおよび時間帯)の調整ができます。当然、打合せやプロジェクトの進行状況などの制約条件があるので完全に自由に調整できるわけではありませんが、それでも毎日決まった時間に仕事をしなければならない状況よりはずっとよいです。 静かな環境で仕事ができる(フルリモート) 自宅や家族構成などの諸条件に左右されますが、オフィスよりも静かな環境を用意することができる場合があります(私は用意できています)。私の場合は雑音が多い環境が苦手なので、静かな環境は集中力を高めるのに役立っています。 困難 自主的にやる気を管理する必要がある(フルフレックス・フルリモート) フルリモート環境では、オフィスのように衆人環視の中で仕事をするわけではありません。人の目がない環境だとどうしても怠けやすくなります。しかし怠けすぎると、仕事の成果が出ず問題になります。 逆に、やる気に満ちあふれている時には過剰な長時間労働をするおそれもあります。フルフレックスの制度下では(法令の範囲内で)極端な時間の使い方をすることも不可能ではありませんが、健康の観点や、組織の一員として周囲と協調しつつ働く観点からは望ましくないでしょう。 ADHD を持つ人にはやる気のある時とない時の差が激しい人が少なくありませんが、やる気のない時に最低限のやる気を出すことと、やる気に満ちあふれている時に働きすぎないようにする工夫は、体調を整えつつ安定したパフォーマンスを出す上で重要だと考えています。 家事などの私生活と仕事のバランスを意識して取る必要がある(フルフレックス・フルリモート) フルフレックス・フルリモート環境では、仕事中にいつでも私用を挟むことができます。特に在宅勤務の場合は、仕事の合間に家事をすることは珍しくないでしょう。 ところが、ADHD を持つ人には一度集中したら他のタスクになかなか移り難い傾向のある人が少なくありません(過集中)。つまり、家事を始めたらいつまでも仕事に戻れなかったり、逆に仕事に熱中して家事が疎かになったりすることがあります。 仕事に戻れないことは当然問題になりますし、家事が疎かになることも私生活においては問題になりえます。 私が困難に対処している方法の例 「自主的にやる気を管理する必要がある」に対して 朝起きたら布団を畳む ADHD 以前の行儀の問題かもしれませんが、朝起きてしばらくしたら布団を畳みます(ベッドではなく、床に布団を敷いて寝ています)。床に布団が敷いてあっては、いつでも簡単に寝ることができてしまいます。畳んだ状態では、布団を敷くのにひと手間かかるので、簡単に寝ることができません。「布団を敷くのは面倒くさい」という ADHD ならではの感情を利用した工夫です。 仕事前に着替える 在宅勤務では、打合せがなければパジャマのままでも仕事をすることが可能です。打合せがあっても、下半身はパジャマのままでもバレません。しかし、私の場合は気持ちを仕事に切り替えるために、仕事前に必ずパジャマから着替えることにしています。オフィスに出社していれば通勤時間で気持ちを切り替える人も多いかと思いますが、在宅勤務は通勤時間がないので代わりにしっかり着替えることにしています。パジャマよりも寝心地が悪いので、安易な昼寝を防止する効果も期待できます。 筆者の仕事着の例。在宅勤務でもこのような服装で仕事をしています 専用の仕事部屋で仕事をする 誰もが実践できる方法ではありませんが、私はほぼ仕事専用の部屋を用意しています。私生活の場と空間を分けることで、仕事に対するやる気を出しやすくなります。やる気に乏しい日でも、机に座ってしまえば仕事ができることは珍しくありません。 コンテンツブロッカーを使う 会社の PC であれば不要な場合もあると思いますが、ネットサーフィン防止のために自主的にコンテンツブロッカーを設定することは有効です。在宅勤務では私物のスマートフォンをいくらでも見ることができるので、私は私物のスマートフォンにも設定しています。 適度に打合せを入れる 打合せは相手がいるので、安易に欠席することはできません。時々やってくるひどくやる気の出ない日でも、打合せ後は仕事ができることもあります。打合せを入れすぎてパニックになると本末転倒ですが、毎日適度に打合せがあることは安定したパフォーマンスを発揮するのに有効かもしれません。 労働時間をトラッキングする 以上はやる気のない時にやる気を出すための工夫でしたが、やる気に満ちあふれている時に仕事をしすぎない工夫も必要です。そのために、私は Toggl Track で労働時間をトラッキングしています。労働時間をトラッキングすることで、労働時間を毎日適当な範囲に収める助けになります。 「家事などの私生活と仕事のバランスを意識して取る必要がある」に対して リマインダーに頼る 過集中の状態になると、目の前のタスクに集中しすぎて、私生活も仕事も他のタスクを忘れがちになります。忘れてしまうこと自体は仕方ないので、私はリマインダーに頼っています。多すぎて無視してしまわない程度に、何でもリマインダーに登録しています。 私生活に関するタスクは私物のスマートフォンのリマインダーを、仕事に関するタスクは Asana (タスク)・ Google カレンダー (スケジュール)・ Slack (メッセージに対するアクション忘れ防止)などを利用しています。 私生活に関するタスクのリマインダー 私生活の時間はカレンダーをブロックしてしまう 私は昔、仕事に熱中するあまり昼食を取り損ねることがよくありました。そこで、最近は昼食の時間 (12:00 – 13:00) はカレンダーをブロックして、かつリマインダーで知らせるようにしています。 スマートスピーカーに頼る(タイマー・アラーム) 洗濯をしたのに、つい仕事に熱中して何時間も干し忘れたことはありませんか?私はあります。そんな時にはタイマーやアラームが便利です。洗濯のできあがる頃合いに設定して知らせてもらいます。最近はスマートスピーカーに声で指示を出して設定するのが便利でよく使っています。 おわりに 以上の困難や工夫は私にとって一部であり、他にも仕事・私生活を問わず様々な困難に対してさまざまな工夫を日々行っています(ADHD をお持ちの方はお分かりになるかもしれません)。また、周囲の方々の支援なくして良好な社会生活を送ることはできません。改めて周囲の方々に感謝いたします。 RevCommでは一緒に働く仲間を募集しています。詳しくは採用サイトをご覧ください。 www.revcomm.co.jp *1 : 厚生労働省による発達障害の説明 *2 : アメリカ精神医学会によるADHDの解説 *3 : 厚生労働省による強迫性障害の説明
アバター