TECH PLAY

株式会社ラクス

株式会社ラクス の技術ブログ

935

はじめに こんにちは、開発課に所属している新卒 1 年目のke-suke0215です。 Dockerのイメージについて「なんとなく業務で使っているけど、いまいちどうなっているのかわかっていない」という状態だったので、今回はDockerイメージの中身がどのように構成されているのかについて調べてみました。 私のようなDockerをなんとなく使っている方が仕組みを理解する手助けになれば幸いです。 はじめに Dockerイメージの構成要素 そもそもDockerイメージとは Dockerイメージの中身を見る 1. イメージをビルドする 2. イメージをtarファイルとして保存する 3. tarファイルを展開する ハッシュ名のディレクトリ ハッシュ名のJSONファイル manifest.json repositories レイヤーについて 2つのDockerfileからレイヤー構造を理解する まとめ 参考文献 Dockerイメージの構成要素 そもそもDockerイメージとは Dockerイメージは、アプリケーションの実行に必要な情報をまとめたパッケージです。これらのイメージはコンテナ実行の土台となっています。ポータビリティの高さやホストOSから環境を分離できるなどの利点があります。 Dockerイメージの中身を見る ここでは以下のDockerfileをビルドしたDockerイメージの中身を見ていきます。 FROM ubuntu:latest 最新の ubuntu のイメージをベースにするだけの簡単なものです。 中身を見る手順は以下になります。 各コマンドの説明は省略させていただきます。 1. イメージをビルドする 今回は my-ubuntu という名前のイメージでタグは 1.0 としてビルドします。 docker build -t my-ubuntu:1. 0 . 2. イメージをtarファイルとして保存する docker save コマンドを使ってtarファイルにします。 docker save -o my-ubuntu-1. 0 .tar my-ubuntu:1. 0 3. tarファイルを展開する 保存したtarファイルを展開して中身を見れるようにします。 tar xf my-ubuntu-1. 0 .tar これでdockerイメージの中身がどのようになっているのか確認できるようになりました。 どのようなものがあるのか確認してみます。 $ ls 0cf20f556e5f1e2fd508acc18bc53e95974c37b6c7304b6cdcbd5bb1bb52df40 e8b8228e36aef7aaaacedf7b10514683933b62424e35956c02e5659aefbcf3bd.json manifest.json my-ubuntu-1. 0 .tar repositories my-ubuntu-1.0.tar は展開元のファイルなので、それ以外について詳しく見ていきます。 ハッシュ名の ディレクト リ 0cf20f556e5~~~ です。これはイメージを構成する上で核となっているレイヤーと呼ばれるものです。レイヤーについての詳しい説明は後述します。各レイヤーはユニークな ハッシュ値 によって識別されています。各 ディレクト リにはそのレイヤーの内容と関連する メタデータ が格納されます。 ハッシュ名の JSON ファイル e8b8228e36a~~~.json です。中身は以下のようになっています。 { " architecture ": " amd64 ", " config ": { " Env ": [ " PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin " ] , " Cmd ": [ " /bin/bash " ] , " Labels ": { " org.opencontainers.image.ref.name ": " ubuntu ", " org.opencontainers.image.version ": " 22.04 " } , " OnBuild ": null } , " created ": " 2023-06-28T08:37:42.319109064Z ", " history ": [ { " created ": " 2023-06-28T08:37:40.107416121Z ", " created_by ": " /bin/sh -c #(nop) ARG RELEASE ", " empty_layer ": true } , { " created ": " 2023-06-28T08:37:40.172787047Z ", " created_by ": " /bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH ", " empty_layer ": true } , { " created ": " 2023-06-28T08:37:40.235648065Z ", " created_by ": " /bin/sh -c #(nop) LABEL org.opencontainers.image.ref.name=ubuntu ", " empty_layer ": true } , { " created ": " 2023-06-28T08:37:40.292202878Z ", " created_by ": " /bin/sh -c #(nop) LABEL org.opencontainers.image.version=22.04 ", " empty_layer ": true } , { " created ": " 2023-06-28T08:37:42.055763636Z ", " created_by ": " /bin/sh -c #(nop) ADD file:140fb5108b4a2861b5718ad03b4a5174bba03589ea8d4c053e6a0b282f439ff3 in / " } , { " created ": " 2023-06-28T08:37:42.319109064Z ", " created_by ": " /bin/sh -c #(nop) CMD [ \" /bin/bash \" ] ", " empty_layer ": true } ] , " moby.buildkit.buildinfo.v1 ": " eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJhdHRycyI6eyJmaWxlbmFtZSI6IkRvY2tlcmZpbGUifSwic291cmNlcyI6W3sidHlwZSI6ImRvY2tlci1pbWFnZSIsInJlZiI6ImRvY2tlci5pby9saWJyYXJ5L3VidW50dTpsYXRlc3QiLCJwaW4iOiJzaGEyNTY6MGJjZWQ0N2ZmZmEzMzYxYWZhOTgxODU0ZmNhYmNkNDU3N2NkNDNjZWJiYjgwOGNlYTJiMWYzM2EzZGQ3ZjUwOCJ9XX0= ", " os ": " linux ", " rootfs ": { " type ": " layers ", " diff_ids ": [ " sha256:59c56aee1fb4dbaeb334aef06088b49902105d1ea0c15a9e5a2a9ce560fa4c5d " ] } } この json ファイルにはイメージの メタデータ などが記述されています。また、Dockerfileの記述の中でレイヤーにならないものの情報も入っています。レイヤーにならない記述は CMD , ENTRYPOINT , ENV などがあります。 また、イメージの情報を出力すると以下のようになります。 docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE my-ubuntu 1 . 0 e8b8228e36ae 1 days ago 77 .8MB IMAGE ID がこのファイル名と一致していることがわかります。なのでハッシュ名の JSON ファイルはdockerイメージのidにもなっているのです。 manifest. json このファイルの中身は以下の通りです。 [ { " Config ": " e8b8228e36aef7aaaacedf7b10514683933b62424e35956c02e5659aefbcf3bd.json ", " RepoTags ": [ " my-ubuntu:1.0 " ] , " Layers ": [ " 0cf20f556e5f1e2fd508acc18bc53e95974c37b6c7304b6cdcbd5bb1bb52df40/layer.tar " ] } ] manifest.json はDockerイメージの中核となるファイルの一つです。 各記述はそれぞれ以下を指しています。 Config :ハッシュ名の JSON ファイル RepoTags :イメージ名とタグ Layers :使用しているレイヤー このことから manifest.json はこのdockerイメージの設計図のような役割を果たしていると言えそうです。 repositories repositories はの中身は以下のようになっています。 { " my-ubuntu ": { " 1.0 ": " 0cf20f556e5f1e2fd508acc18bc53e95974c37b6c7304b6cdcbd5bb1bb52df40 " } } イメージ名とタグとをハッシュに マッピング することで、具体的なイメージのバージョンや名前を特定する役割を果たします。 レイヤーについて さきほど少し触れましたが、dockerイメージはいくつかの層(レイヤー)で成り立っています。基本的にDockerfileの記述ごとに1つまたは複数のレイヤーが作成されます。( ハッシュ名のJSONファイル の部分でも触れたように、一部レイヤーにならない記述もあります。) レイヤー構成の何がいいかというと、差分のみのレイヤーを作成することでディスクスペースを節約できることです。具体的に見ていきましょう。 2つのDockerfileからレイヤー構造を理解する まず FROM と RUN の記述がある下のようなDockerfile(Dockerfile①とする)からイメージを作成してみます。 FROM ubuntu:latest RUN apt-get update すると以下のレイヤーが作成されます。 FROM ubuntu:latest に基づいて生成されたレイヤー(レイヤーA) RUN apt-get update に基づいて生成されたレイヤー(レイヤーB) 次に下のような FROM と COPY の記述があるDockerfile(Dockerfile②とする)からイメージを作成します。 FROM ubuntu:latest COPY sample.txt /tmp このとき、生成されるのは以下のレイヤーです。 COPY sample.txt /tmp に基づいて生成されたレイヤー(レイヤーC) Dockerfile①と同じ記述である FROM ubuntu:latest のレイヤーは作成されず、すでにあるレイヤーAを使ってイメージを構成します。なのでDockerfile②から生成されるイメージはレイヤーAとレイヤーCからできています。 不足しているレイヤーのみを作成してイメージを生成するため、無駄なレイヤーを増やさずディスク容量を節約できるのです。 まとめ Dockerイメージの内部構成について見てきました。興味がある方の参考になれば幸いです。最後までお読みいただきありがとうございました。 参考文献 Dockerコンテナのレイヤ構造とは? コンテナの作り方
アバター
こんにちは。 株式会社 ラク スで先行技術検証をしたり、ビジネス部門向けに技術情報を提供する取り組みを行っている「技術推進課」という部署に所属している鈴木( @moomooya )です。 ラク スの開発部ではこれまで社内で利用していなかった技術要素を自社の開発に適合するか検証し、ビジネス要求に対して迅速に応えられるようにそなえる 「技術推進プロジェクト」 というプロジェクトがあります。 このプロジェクトで「DBセキュリティ」にまつわる検証を進めているので、その中間報告を共有しようかと思います。 本記事における「DBセキュリティ」とは カバーする範囲 カバーしない範囲 DB暗号化方式は大きく分けて3つ OS機能によるディスク暗号化 DB機能による暗号化(透過的暗号化) アプリケーションによる暗号化 「アプリケーションによる暗号化」が最も強いなら DB暗号化を導入する際の懸念点 最大の懸念は性能面 検証する環境や条件 検証環境 検証内容 次回予告 参考 本記事における「DBセキュリティ」とは カバーする範囲 本稿で扱う「DBセキュリティ」については入室管理、アカウント管理、権限管理、データ暗号化、アクセス監視といった部分を対象とし、中でも技術的な検証が必要になるデータ暗号化について検証を進めていきます。 これらは 個人情報の保護に関する法律についてのガイドライン(通則編) にて 10-6 技術的安全管理措置 として記述されています。 カバーしない範囲 「DBセキュリティ」の範囲として定義した通り、 ウェブアプリケーション 経由のセキュリティは扱いません 。 なので DBMS への通常アクセスによるセキュリティに関しては対象外とします。 DB暗号化方式は大きく分けて3つ 暗号化の強度としては以下の ディスク暗号化 透過的暗号化 アプリケーションでの暗号化 の順に強くなっていきます。 OS機能によるディスク暗号化 こちらはDB内のデータを暗号化するのではなく、DBのデータを保持しているディスク自体を暗号化するものになります。 こちらはOSレベルで提供される機能を用いてディスクの暗号化を行います。 ディスク暗号化ではDBサーバーからのディスク窃取への対策となります。一方、DBサーバーのOSにログインを許してしまうと暗号化の効果はありません。 弊社では RHEL 系OSの利用が多いので、今回の検証では LUKS を用いる予定です。 DB機能による暗号化(透過的暗号化) DBMS 自体もしくは、 DBMS への プラグイン によってDB内にデータ格納時に暗号化する方式です。 透過的暗号化方式(TDE)が該当します。 透過的暗号化ではディスク窃取に加えて、 DBMS が書き込むデータファイルの窃取にも対応します。ただし、 DBMS 自体へのログインを許すと復号されたデータが閲覧可能になってしまいます。 弊社では PostgreSQL を利用することが多いのですが、 PostgreSQL には標準で組み込まれた透過的暗号化機能はありません。そのため、 NEC 製の Transparent Data Encryption for PostgreSQL (TDEforPG) を PostgreSQL に組み込んで利用する予定です 1 。 アプリケーションによる暗号化 こちらはアプリケーションの機能として暗号化処理を実装する形式になります。そのためDBに渡されるのはすでに暗号化したデータであり、使用するDBに左右されない方式となります。 アプリケーションで暗号化した場合には、 DBMS にログインされた場合でもデータは暗号化されたままなので閲覧できません。アプリケーションを経由したアクセスでなければ復号したデータは見れません。 PostgreSQL では暗号化ライブラリとして pgcrypto が用意されているのでこちらを利用して検証を行います。 これらの方式の詳細は少し古い記事になりますが、 @IT にて連載されていた こちらのコラムの2ページ目の表 がわかりやすくまとまっています。 「アプリケーションによる暗号化」が最も強いなら 暗号化の強度とカバー範囲だけを考えると、アプリケーションによる暗号化が最も強いですが、DBとのデータ入出力処理を実装する都度、暗号化/復号化の処理を通さなければなりません。 品質管理の観点からもアプリケーション機能の実装数は少ない方が品質維持が容易になるので、DBもしくはOSに任せられる処理は任せた方が無難です。 理想的なデータ暗号化の想定としては データ全体の暗号化はディスク暗号化もしくは透過的暗号化で対応 マイナン バーやクレジットカード番号などの特に機微な情報に限ってアプリケーションでも暗号化 という組み合わせパターンが理想形だと想定しています。 DB暗号化を導入する際の懸念点 最大の懸念は性能面 今回の検証でデータ暗号化をテーマにした理由にもなりますが、データを暗号化する場合は DBにデータを格納するたびに暗号化処理が実行されます。そのためDBへのIO性能が確実に低下します が、どの程度低下するかはサーバー性能やデータ特性などに影響を受けるため、一概に何%低下するとは単純に語れません。 また、列レベルで暗号化する場合には列によってはインデックスが効かなくなる、といった副作用も想像できますし、暗号化方式によってはdump/restore時に影響が出ないかも心配です。 そのため今回の検証では、影響がありそうだと想定される状況ごとに実際の性能劣化度合いがどの程度なのかを計測していくことを中心に計画しています。 もちろん理想の結果としては、処理性能の劣化が無視できる程度であることですがそれは実際に試さないとわかりません……。 検証する環境や条件 検証環境 各環境のベースには AWS の定常パフォーマンス環境のm4.xlargeを用いる予定で、検証環境のバリエーションとしては以下を用意しようと考えています。 暗号化なし環境 ディスク暗号化(LUKS)環境 透過的暗号化(TDEforPG)環境 アプリ暗号化(pgcrypto)環境 ディスク暗号化+透過的暗号化(LUKS + TDEforPG)環境 また、検証スケジュールに余裕があれば AWS のRDSにてTDEオプションがあるようなので、こちらのON/OFFの違いも参考値として計測したいと考えています。 検証内容 実際のアプリケーションの動作や、運用を想定した動作について計測予定です。 ウェブアプリケーション を想定したクエリ SELECT JOINあり / なし サブクエリあり / なし インデックススキャン / フルスキャン INSERT DELETE UPDATE dump / restore速度 DB同期速度 なおDB同期速度については、正攻法で検証するのであれば各検証環境を2台ずつ用意しないといけないと思うのですが、なんとかもう少し楽に検証できないか検討しています。 次回予告 さて今回はDBセキュリティの概要と、導入時に懸念される項目を検証するための計画を立てる話でした。 今後この計画に沿って環境の構築と検証を進めていきます。次回は計測結果をもとにした現実的なDB暗号化プランの検討をしていきたいと思います。 次回記事の投稿は少し先になりますが、2024年3月頃を予定しています。投稿する際には本記事からもリンクを張っておきます。 ※追記:書きました tech-blog.rakus.co.jp 参考 www.ppc.go.jp atmarkit.itmedia.co.jp jpn.nec.com 商用ソフトウェアのためTDEforPGに関する具体的な性能測定結果は公開を差し控える可能性があります。 ↩
アバター
はじめに  こんにちは、 ラク スでインフラを担当しているftkenjです。  弊社ではサービスの製品サイトを AWS で運用していますが、リソースの追加・変更が発生するたびにコンソールにログインをして画面をポチポチして行っています。 オンプレよりは楽ですが、 クラウド サービスの利点を生かし切れていませんでした。  そこでサービスでも利用してるTerraformで構成管理を行っていくことにしました。 コード化していく中で苦労したことなどを伝えられればと思います。 目次 はじめに 目次 Terraformって何? きっかけ 苦労したこと ディレクトリの構成 リソースの取り込み 実環境との差分埋め 良かったこと terraformの知見が広がった 運用面の変化 今後について おわりに 参考 Terraformって何?  Terraformとは、HashiCorpがGo言語で開発した オープンソース のツールです。 AWS や GCP といった クラウド サービス上のインフラ構成をコード管理するために使用します。 ここにGitも組み合わせることでインフラ構成をバージョン管理することも可能になります。  最終的にはCI/CDまで実装することができれば、複雑な手順や数時間とかかっていた作業も不要になります。 とはいえ、既存環境をコード化するのも一筋縄とはいかず・・・ きっかけ  新規サーバ・サイトの追加時のたびに AWS のWebコンソールで操作をしていました。 コンソールからの操作はどうしても画面遷移が多く、横にもう一つコンソールを出して既存設定と見比べながら設定することが大変でした。 この作業を楽にしたい、というのがコード化のきっかけになります。 苦労したこと ディレクト リの構成  始めはenvおよびmodules配下それぞれを本番環境と検証環境で分けていました。 しかし、これだとmodulesでコードが重複してしまい煩雑になってしまいます。 そのためmodulesは共 通化 して環境の差分は変数(env)で補完する、という構成にしました。 例) . ┣━ env ┃ ┣━ 検証環境 ┃ ┃ ┗━ main.tf ┃ ┃ ┃ ┗━ 本番環境 ┃ ┗━ main.tf ┃ ┗━ modules ┣━ サービス名1 ┃ ┣━ AWSサービス名1.tf(メインとなるtfファイル) ┃ ┣━ variables.tf ┃ ┗━ (output.tf) ┃ ┣━ サービス名2 ┃ ┣━ AWSサービス名2.tf ┃ ┣━ variables.tf ┃ ┗━ (output.tf) ...  どちらかの環境にしか存在しないリソースも存在していますが、 三項演算子 を使用して制御しています。 詳しくは次回紹介できればと思います。 リソースの取り込み  コードを書くよりも、実環境の状態を取り込む方が大変でした・・・ terraform import は取り込むリソースを指定して実行することで、「terraform.tfstate」というファイルに json 形式で取り込まれます。  ただし、取り込みには対象のリソースのIDやARN( Amazon Resource Name)が必要であり、基本的にTerraformでコード化しているすべてのresource分を行わなければいけません。 細かいところですと、Route53のレコードやELBのListener Ruleが1項目ずつ取り込むことになります。   AWS で新しく環境構築する、もしくは始めたばかりのうちにコード化しておく方が結果的に少ない労力で済みますね・・・  一応「terraformer」という既存環境を一括で取り込める OSS があります。 こちらはtfファイルも自動生成してくれますが、各リソースの設定値がハードコーディングされているなど、生成後に手を加える必要があります。 いち早く既存環境をコード化して運用していきたい、という場合は使ってみるのもアリではないでしょうか。 実環境との差分埋め  すべての取り込みが完了したら「terraform plan」(dry-runのイメージ)の実行です。 これで実環境との差分が出なければ、めでたくコード化完了となりますが・・・  実行後、以下のように「変更の詳細」とadd、change、destroyの総数が表示されます。 特にchange、destroyは実環境が壊れてしまうので注意しなければいけません。 ‐ add:新規追加されるリソース数 ‐ change:変更されるリソース数 ‐ destroy:削除されるリソース数 … ここから上は変更の詳細 … Plan: 8 to add, 0 to change, 0 to destroy. ─────────────────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. なお取り込みできないresourceも存在するため、 terraform apply の初回実行までaddが残ってしまうこともあります。 その場合は、取り込めないresource以外に変更されない状態するまで差分を埋めていきます。 良かったこと terraformの知見が広がった  これまでTerraformを触ったことがなかったので、試行錯誤しながら進めていました。 ピンポイントでほしい情報が載っている技術ブログが見つからないなどありましたが、 公式ドキュメント が分かりやすく一番重宝しました。 terraform import 実行時に必要なリソースID等も公式ドキュメントで確認することができます。 上の方でも書きましたが、今回コード化していく中で役立ちそうなところを次回紹介できればと思います。 運用面の変化  コード化してからの運用がどうなったかをかければよかったのですが、まだ運用開始できるところまでできていません。 機会があればコード化前後でどういった変化があったのかをお伝えできればと思います。 今後について  コード化までは完了しているため、Terraformの運用/管理を整理していきます。 作成者でしかTerraformから構成変更・追加ができない状態ではコード化した意味がありません。 CI/CDのパイプラインも設計していきたいですが、こちらは先が長くなりそうです。 おわりに  Terraformを触り始めてまだ半年程度ですが、コードを書くこと自体は難しくはありませんでした。 それよりも煩雑にならないための事前設計と既存環境との差分をなくしていくことが大変でした。  あとはなんといっても terraform import です。 IDやARNが必要になるため、どうしても1つずつしか取り込めないのが手間でした。 そういった意味でも、コード化するのであれば最初からTerraformで構築する方が楽だと実感しました。 もしくは AWS であれば、料金がかかってしまいますがCloudFormationをを利用することでコード化の難易度は下がるのかもしれません。 参考 Terraform by HashiCorp [AWS] Terraformerで複数のリソースを一括importしてみた
アバター
はじめに 初めまして、新卒1年目のm_you_sanと申します。 初学者向けにReactにおけるメモ化の方法を簡単に紹介させていただきます。 目次は以下の通りです。 はじめに そもそもメモ化って? メモ化の方法 React.memo 使用例 注意点 useCallback 注意点 まとめ そもそもメモ化って? メモ化は簡単に言うと、計算結果を保持して、それを再利用する手法です。 Reactの場合、無駄な コンポーネント の呼び出しを減らすことができ、パフォーマンス性が上がります。 特に レンダリング の負荷が大きい処理、頻繫に再 レンダリング される コンポーネント の子 コンポーネント で、メモ化を利用するとよりパフォーマンス性の向上が見込めます。 メモ化の方法 Reactおけるメモ化方法は主に3つ(React.memo、useCallBack、useMemo)あります。 今回はReact.memoとuseCallBackについて紹介いたします。 React.memo React.memoは コンポーネント の不要な レンダリング の回避を目的として使用します。 メモ化の対象は コンポーネント です。 React.memoはpropsが等価であるかをチェックして再 レンダリング の判断をします。 新しく渡されたpropsと前回のpropsを比較して、等価である場合は再 レンダリング は起きません。 使用例 以下のコードは親 コンポーネント に入力フォームがあるコードです。 親 コンポーネント const App: React.FC = () => { const items = [ "食洗器" , "髭剃り" , "冷蔵庫" ] ; const [ itemList , setItemList ] = useState ( items ); const [ item , setItem ] = useState ( "" ); const addItem = () => { setItemList ( [ ...itemList , item ] ); } return ( <> < input type= "text" placeholder = "欲しいもの" value = { item } onChange = { ( e ) => setItem ( e.target.value ) } / > < button onClick = { addItem } > 追加 < /button > < List itemList = { itemList } / > < / > ) } 子 コンポーネント type ListProps = { itemList: string [] } export const List: React.FC < ListProps > = ( { itemList } ) => { console .log ( "レンダリングされました" ); return ( < ul > { itemList.map (( item , index ) => ( < li key = { index } > { item } < /li > )) } < /ul > ) } この場合、フォームに値が入力される度に、親 コンポーネント で管理しているitemが更新されるので、子 コンポーネント が レンダリング されてしまいます。 こうした レンダリング を防ぐために、子 コンポーネント をメモ化します。 下記のコードの場合、itemListが更新されたときのみ、再 レンダリング が起こるようになります。 つまり、追加ボタンが押されない限り、再 レンダリング は起きません。 子 コンポーネント export const List: React.FC < ListProps > = React.memo (( { itemList } ) => { console .log ( "レンダリングされました" ); return ( < ul > { itemList.map (( item , index ) => ( < li key = { index } > { item } < /li > )) } < /ul > ) } ) 動作を確認すると、メモ化することで、再 レンダリング が防げていることが分かります。 また、追加ボタンを押したときのみ、再 レンダリング が起きているのが分かります。 注意点 関数をpropsとして渡す場合、React.memoでは再 レンダリング を防ぐことができません。 例として、先程のコードに、欲しいものの表示、非表示を切り替えるtoggleShowItems関数を作成し、propsとして子 コンポーネント に渡してみます。 親 コンポーネント const App: React.FC = () => { const items = [ "食洗器" , "髭剃り" , "冷蔵庫" ] const [ itemList , setItemList ] = useState ( items ); const [ item , setItem ] = useState ( "" ); const [ isShow , setIsShow ] = useState ( false ); const addItem = () => { setItemList ( [ ...itemList , item ] ); } const toggleShowItems = () => { setIsShow ( ! isShow ); } return ( <> < input type= "text" placeholder = "欲しいもの" value = { item } onChange = { ( e ) => setItem ( e.target.value ) } / > < button onClick = { addItem } > 追加 < /button > < List itemList = { itemList } isShow = { isShow } toggleShowItems = { toggleShowItems } / > < / > ) } 子 コンポーネント type ListProps = { itemList: string [] ; isShow: boolean ; toggleShowItems: () => void ; } export const List: React.FC < ListProps > = React.memo (( { itemList , isShow , toggleShowItems } ) => { console .log ( "レンダリングされました" ); return ( <> < div > < button onClick = { toggleShowItems } > { isShow? "欲しいものを非表示にする" : "欲しいものを表示する" } < /button > < /div > { isShow && ( < ul > { itemList.map (( item , index ) => ( < li key = { index } > { item } < /li > )) } < /ul > ) } < / > ) } ) この場合、toggleShowItems関数の内容は変更されていないにも関わらず、フォームに入力する度に再 レンダリング が発生してしまいます。 なぜこのようになるのかというと、React.memoでは、関数などのオブジェクト型の等価性をチェックする場合、値そのものではなく参照先データを比較するからです。 仮にtoggleShowItems1を レンダリング 前の関数、toggleShowItems2を レンダリング 後の関数とします。 関数の内容自体は変わりませんが、参照先が レンダリング 前後で変わるため、等価ではないと判断されます。 そのため関数が変更されたと見なされ、再 レンダリング が発生してしまいます。 //レンダリング前 const toggleShowItems1 = () => { setIsShow ( ! isShow ); } //レンダリング後 const toggleShowItems2 = () => { setIsShow ( ! isShow ); } //関数の内容は同じだが、参照先が異なるためfalse console .log ( toggleShowItems1 === toggleShowItems2 ); こうした問題を解決するために使用するのがuseCallbackです。 useCallback useCallbackのメモ化の対象は関数です。 useEffectなどと同様に、第二引数に依存配列を設定します。 下記のコードの場合、isShowが更新されたときに、関数を再生成します。 親 コンポーネント const toggleShowItems = useCallback (() => { setIsShow ( ! isShow ); } , [ isShow ] ); 動作を確認すると、フォームに入力しても レンダリング が発生していないことが分かります。 また、表示切替のボタンを押して、isShowが更新されたタイミングで レンダリング が起きていることが分かります。 このように、関数をメモ化した コンポーネント に渡す場合、useCallbackを使用することで、不要な レンダリング を抑えることができます。 注意点 useCallbackをReact.memoと併用して、不要な レンダリング を防ぐ目的ではなく、単純に関数の再生成を防ぐ目的で使用しても、あまり効果はありません。 理由としては、関数の再生成のコストがuseCallbackの実行コストを上回るケースがあまりないからです。 関数の再生成を防ぐ目的であれば、 useMemo の方が適していると思うので、そちらを使用するのが良いと思います。 まとめ 今回は初学者向けにReact.memoとuseCallbackについて、紹介させていただきました。 メモ化について更に詳しく知りたい方は、 公式ドキュメント を参照していただければと思います。
アバター
こんにちは。インフラエンジニアの gumamon です! 最近はSRE的なことも ちょこちょこ やらせて頂いています。 NewRelic、Datadog、モダンな監視(オブザーバビリティ)って良いですよね。 弊社も Kubernetes ( k8s )等を利用した環境が増えてきた折、そろそろ必要になってきた(と思っている)のですが、NewRelic、Datadog等の クラウド サービスは ランニングコスト が安くない。 そこで内製できないかやってみよう!ということになり、試行錯誤をした結果どうにか表題の構成で作ることができたのでご紹介をしたいと思います! この記事では、 k8s を観測対象とし、オブザーバビリティを実現した際の アーキテクチャ 構成、並びに四苦八苦する中で得た観測の勘所( 私見 )についてご紹介します。 目次 目次 オブザーバビリティとは オブザーバビリティ(OSS)の実現事例 全体構成 Elastic Stack Elastic Stack を補うサービス OpentTelemetry関連 取れるデータは手段を選ばず収集する Metrics Log Tracing 収集した情報を分析し、ビジネス自体を観測する 特定リクエストのエラー応答を追跡する k8s環境のリソース使用状況を確認する まとめ 参考 検証環境 ドキュメント オブザーバビリティとは Observabilityと表記され、日本語では「可観測性」と呼ばれる概念です。 より日本語の感覚に寄せた表記をするのであれば、「観測する能力」 (= Observe + Ability)とも言えるかと思います。 観測と監視の違いは何か?と聞かれることがあるのですが、私は「見ている対象が違う」と回答しています。 「顧客体験」を観測する 「システムの正常性」を監視する 言うまでもなく、ビジネス上より価値が高いデータは観測データの方になるかと思います。 近年、 クラウド やコンテナ等の普及によりサービスの構成要素はあらゆるところに分散しています。 これら全体を「監視」し続け、ましてやそこから顧客体験を推察するというのは非常に骨の折れる作業であり、これが今オブザーバビリティに注目が集まっている背景ではないかと私は思います。 さて、改めて 「顧客体験」を観測する方法ですが、こちらは「 ORILLY 入門監視~モダンなモニタリングのためのデザインパターン~ 」が参考になるかと思います。 この記事では深く触れませんが、「顧客体験」の観測に関連する要点は下記である、と書いてあるように私には読み取れました。 ユーザが快適に利用できているかを観測するには ユーザに近い場所から監視せよ サービス利用時のパフォーマンスを測定せよ サービス構成要素から漏れなく情報を収集せよ 収集した情報を分析し、ビジネス自体を観測せよ さらに、上記を実装イメージに寄せてグルーピングしてみると下記になりました。 取れるデータは手段を選ばず収集する (1〜3) 収集した情報を分析し、ビジネス自体を観測する (4) 次のテーマではこれをどう実現したかを見ていきます。 オブザーバビリティ( OSS )の実現事例 全体構成 いきなり結論ですが今回構築した環境の全体像はこうなりました。 ※ k8s 上にデプロイしています。 Elastic Stack arch_all Elastic Stack 緑の コンポーネント がこちらの対象です。 Elasticsearch : Elastic Stack(ES)の核となるサービス。永続化データは全てここに保存する。 Kibana: ESの ユーザーインターフェイス ApmServer: Tracing(後述)データの受け口。OTLPに対応している Fleet: Elastic Agentの管理サーバ Elastic Agent: OS/ k8s のMetrics、Log収集を担当(後述) Elastic Stack を補うサービス 白の コンポーネント がこちらの対象です。 kube-system-metrics (KSM) : k8s API をListenし、メトリクスを生成する(単純な)サービス ElastAlert2 : 3rdパーティ製のアラートマネージャ。ESのアラートマネージャは要有償ライセンス・・(泣) OpentTelemetry関連 青の コンポーネント がこちらの対象です。 stub: 弊社で内製したスタブアプリ(Go)。OTel用に( 計装 )をしている Otel-Collector: OTLP準拠の観測データをバックエンドに フォワ ードする 取れるデータは手段を選ばず収集する 収集対象のデータは、オブザーバビリティでは特にTelemetry(テレメトリ)と呼びます。 Telemetryは特性別に3つに分ける事ができます。 Category Summary Metrics 定期的にグループ化または収集された測定値の集合。統計情報やCPU使用率など Logs 履歴や情報の記録。 コンポーネント の アクセスログ 、エラーログ等 Tracing トランザクション の追跡。特定のWEBリク エス トにレスポンスが返るまでの レイテンシー 、通過 コンポーネント 、等 上記3つは、何れも「手段を選ばず」収集していきます。 今回は下記の実装となりました。 Metrics ElasticAgentを介して収集します。 ※収集経路をオレンジでハイライトしています。 Elastic Stack arch_metrics k8s 周り kube-system-metrics(KSM) )を介して収集します。 KSMは k8s のコントローラ経由で収集したメトリクスを提供するエンドポイントです。 /sys/fs/cgroup/ 等もマウントし参照します。 OS周り /proc 等をマウントし参照します。 ( Metricsではありませんが、 /etc/ 以下もマウントし、ホスト名なども収集します) Log ElasticAgentを介して収集します。 ※収集経路をオレンジでハイライトしています。 Elastic Stack arch_logs k8s 周り * コンテナログ: /var/log/contanirs をマウントし収集します(APPのログは標準出力し、左記に格納します) * システム コンポーネント ログ: 半分は前述コンテナログとして取れます。残りは /var/log/messages 等をマウントして収集します OS周り /var/log/secure 等をマウントして収集します。 Tracing OpenTelemetryを介して収集します。 ※収集経路をオレンジでハイライトしています。 Elastic Stack arch_tracging OTel SDK Tracingの起点であり、追跡用のIDを発行します。 今回は Manual Instrumentation という手法で追跡IDを発行しており、ざっくり説明をするとAPPにOTelのライブラリを読み込ませて API を叩く都度追跡IDを発行をさせています。 OTel Collector APPからTracingを受け、 APM Serverに フォワ ードするのが責務です。 APM Server OTel CollectorからTracingを受け、Elasticsearchに格納するのが責務です。 OTel SDK → APM Serber としても通信は成立するのですが、この場合APPコンテナの設定に APM Serverの情報を含める必要があるなど、プロダクト(を想定したAPP)とESの結合度が高まってしまうため、OTel Collectorを一段挟むことにしました。 では ElasticAgentはどうなのか?という話になるのですが、こちらはそもそもプロダクト(を想定したAPP)が関与しない通信経路であること、 Datadog、NewRelicなどは皆エージェントを持っているというところから、仮にESから他ソリューションに移ることになったとしてもロックインされるリスクは低いと判断し、 Elasticsearchとネイティブに通信できるAgentを使うことを選択しています。 収集した情報を分析し、ビジネス自体を観測する ここから先は、根こそぎ集めたデータの活用フェイズになります。 「ビジネス自体を監視する」というところはプロダクトが目指す方向性となるため、千差万別です。 ここでは、 ユーザーインターフェイス (Kibana)のキャプチャと共に活用例をご紹介します。 特定リク エス トのエラー応答を追跡する OTel SDK が付与した 追跡IDを元に、エラー応答が発生したリク エス ト→レスポンス( トランザクション )を追跡します。 Kiabna stub tracing Kibana stub logs 画面からは以下のことがわかります。 サービス stub のエンドポイント /todos/ で Internal Server Error が発生した 発生時刻は本日 13:56 エラーログから、原因は存在しない URI /todos/hoge にアクセスされた為であることがわかった ---※1 ※1 ログにもOTel SDK が発行した追跡IDを埋めています(Tracingと一致するID)。これをElastic Stack側の APM アプリが解釈し、Tracingと関連するLogとして同じアクセスとしてUIに表示しています。 k8s 環境のリソース使用状況を確認する k8s のMetrics状況から、イン フラリ ソースの使われ方、あるいは しきい値 の設定が適切かを確認します。 Kibana k8s overview 画面からは以下のことがわかります。 メモリが逼迫したPodがいる (これはPod自体に設定したリソースの上限です) Elasticsearch : MEM=max 2GB ElasticAgent: MEM=max 1GB k8s ノード全体のメモリ使用量は4GB/16GB とまだ余裕があります。 Podのメモリ利用効率を改善するか、メモリ利用上限を引き上げることでリソース逼迫を解消できそうです まとめ 今回は Elastic Stack x OpenTelemetryを使ったオブザーバビリティ構成についてご紹介させていただきました。 クラウド サービスを使うケースが大多数だとは思いますが、監視対象への実装では似たようなことを行うことになるかなと思います。 何かの参考にしていただけますと幸いです! ※実装方法については当記事では割愛させて頂きますが、Elastic StackはOperatorをインストールし、(OTel等も含め) kustomizeで書きました。 言うに及ばずですが、自力実装をするには結構なつらみがあり、「まずお試しで」ということであれば有償版をご利用されることをお勧め致します! 以上、最後までお読み頂きありがとうございました! 参考 検証環境 k8s プラットフォーム microk8s (v1.27.5) kubernetes (v1.27) オブザーバビリティ kube-state-metrics (v2.10.0) ECK Operator (v2.9.0) Elastic Stack (v8.9.2) OTel Collector (v0.84.0) elastalert2 (v2.13.2) アプリケーション stub (Go / 弊社内製) Otel SDK ドキュメント ESに関するドキュメント index overview Elastic Cloud on K8s Guide Elasticsearch Kibana Elastic Observability Fleet and Elastic Agent OpenTelemetry 公式Doc kube-state-metrics Github ElastAlert2 公式Doc Github
アバター
はじめに 初めましてこんにちは。 ngerukatakataです。 営業上がりの未経験エンジニアとしてそこそこの期間を働いております。 最近AWSEKS環境なんてものを触り始めました。 k8s 環境に触れるのも初めてなうえに、 AWS もそんなに触ったことない人間なので四苦八苦としています。 簡単な面もあればどうしたら実現できるんだ!なんて面にもぶつかったり… 皆さんも k8s に触れるときには同じような苦しみを感じたんじゃないかなぁって思います。 さて、今回は苦労したものの一つ AWS _EKSの『メトリクス監視』についてお話しさせていただければと思います。 今回お話しするメトリクス監視ツールは ADOT( AWS Distro for OpenTelemetry) についてとなります。 目次 はじめに 目次 背景 EKSの監視を始めよう! ADOT とは? OpenTelemetry とは? ADOTをEKSonFargateに導入するには FargateProfileの作成 IAMの作成 ADOTコレクタの作成 Container Insightsでの確認 ADOTをEKSonFargate+EC2に導入するには ノードグループにラベルを追加 Configmapの修正 まとめ 参考 背景 この度新しい運用基盤へのチャレンジということで k8s 環境への取り組みが始まりました。 新しい運用基盤への取り組みが始まるということは、当然のことながら、 新しい監視について検討をしなくてはなりません。 今までの環境は物理または仮想のサーバ環境に対して、Zabbixエージェントを導入して監視を行っておりました。 ただ調べてみると、EKS環境というのは今まで通りzabbixエージェントを仕込んで…というのはどうも難しそう。 今まで通りのやり方を踏襲してやれば楽勝じゃん!とはいかなそうではありました。 そこでいろいろな賢人たちのブログを読み漁りADOTというものに出会いました。 ADOT( AWS Distro for OpenTelemetry)はOpenTelemetryの仕組みを使って、 いい感じにデータを抜き出してくれる仕組み…これでメトリクスも完成だ!としたところで、 どうやら既存のADOT設定はFargate特化、EC2ノードを追加した構成ではうまく動かないことが分かりました。 そこでcadvisorといわれる仕組みを勉強したりOtelの構造を勉強したりなどして、 なんとかFargate+EC2のEKS構成でもADOTを利用したメトリクス監視構成を作成することができました 今回はそんな新しい監視ツールADOTの説明と、 それをEC2同居構成でどのように使えるようにしたかという説明をさせていただければと思います。 EKSの監視を始めよう! まずは早速ADOTについて説明をさせていただきます。 ADOT とは? AWS Distro for OpenTelemetry は、 AWS がサポートする OpenTelemetry プロジェクトの ディストリビューション です。 ADOT CollectorというPodでメトリクスの情報を収集し、Cloudwatchなどに送信するところまでやってくれています。 OpenTelemetry とは? システム監視におけるメトリクスデータなどの収集や送信を標準化し、 特定のベンダに依存しない形でシンプルに収集/送信をするものです。 ADOTはこちらを利用して収集から送信を AWS 用にいい感じにしてくれるものと捉えてもらえればよろしいかと思います。 ADOTをEKSonFargateに導入するには それでは実際に私が実施した ADOTをつかったメトリクス監視の追加方法について、 実例をもとに説明させていただきます。 今回の実例は、以下に示すように - FargateProfileの作成 - IAMの作成 - ADOTコレクタの作成 - Container Insightsでの確認 の流れになっていますので、ごらんの皆様もイメージしやすいかと思います! FargateProfileの作成 ADOTCollectorはFargateで起動するため、 事前にFargateProfileを作成してEKSに認識させなくてはいけません。 弊社ではTerraformを使って AWS の構成管理を行っているので以下のような記述を作ってFargateProfileを作成しました。 module "eks" { source = "terraform-aws-modules/eks/aws" version = "~> 18.30.2" 中略 fargate_profiles = { default = { name = "default" selectors = [ { namespace = "default" }, { namespace = "kube-system" } ] subnet_ids = var.private_subnets }, fargate-container-insights = { name = "fargate-container-insights" selectors = [ { namespace = "fargate-container-insights" } ] subnet_ids = var.private_subnets iam_role_additional_policies = ["arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"] } } } IAMの作成 ADOT Collector から、メトリクスデータを CloudWatch に送信するために IAM アクセス許可が必要です。 Terraformを使って以下のような記述を作ってIAM許可ルールを作成しました。 module "eks-fargate-adot_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc" version = "3.5.0" create_role = true role_name = "${var.cluster.name}-EKS-Fargate-ADOT-ServiceAccount-Role" role_policy_arns = ["arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"] provider_url = module.eks.cluster_oidc_issuer_url oidc_fully_qualified_subjects = ["system:serviceaccount:fargate-container-insights:adot-collector"] } これはrole_nameの名前でCloudWatchAgentServerPolicyの権限を持ったroleを作成しています。 EKS上のnamespace「fargate-container-insights」のpod「adot-collector」が処理をするときに 本roleにassume出来るようにしています。 該当のEKSには以下のような yaml を実行してnamespaceとServiceAccountを作っておきましょう。 apiVersion: v1 kind: Namespace metadata: name: fargate-container-insights labels: name: fargate-container-insights apiVersion: v1 kind: ServiceAccount metadata: name: adot-collector namespace: fargate-container-insights annotations: eks.amazonaws.com/role-arn: [IRSAARN] ここでいう[IRSAARN]には先ほどTerraformで生成したrole_nameのARNを入力します。 ADOTコレクタの作成 次に以下の yaml を実行してStagtefulsetとしてADOT Collectorを作成しましょう。 以下のようなroleを作成し、 kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: adotcol-admin-role rules: - apiGroups: [""] resources: - nodes - nodes/proxy - nodes/metrics - services - endpoints - pods - pods/proxy verbs: ["get", "list", "watch"] - nonResourceURLs: [ "/metrics/cadvisor"] verbs: ["get", "list", "watch"] さきほど作ったServiceAccountに権限を付与します。 kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: adotcol-admin-role-binding subjects: - kind: ServiceAccount name: adot-collector namespace: fargate-container-insights roleRef: kind: ClusterRole name: adotcol-admin-role apiGroup: rbac.authorization.k8s.io そして重要となるAdotのconfig用のConfigmap、 これがOpenTelemetoryの設定になります。 長すぎるのでコードは閉じておきますが、 「receivers」でデータをどのように受け取るかの設定をし、 「processors」でどのようにデータを取り扱うかの設定をし、 「exporters」で出力先の設定をしています。 ここでは 「receivers」で「cadvisor」というものを使って k8s 環境の情報を取得し、 「processors」で必要な情報をメトリクスデータとして整理し、 「exporters」で「Cloudwatch」宛に出力する設定をしているということだけご認識ください。 >>>>コードを見る<<<< apiVersion: v1 kind: ConfigMap metadata: name: adot-collector-config namespace: fargate-container-insights labels: app: aws-adot component: adot-collector-config data: adot-collector-config: | receivers: prometheus: config: global: scrape_interval: 1m scrape_timeout: 40s scrape_configs: - job_name: 'kubelets-cadvisor-metrics' sample_limit: 10000 scheme: https kubernetes_sd_configs: - role: node tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token relabel_configs: - action: labelmap regex: __meta_kubernetes_node_label_(.+) # Only for Kubernetes ^1.7.3. # See: https://github.com/prometheus/prometheus/issues/2916 - target_label: __address__ # Changes the address to Kube API server's default address and port replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name] regex: (.+) target_label: __metrics_path__ # Changes the default metrics path to kubelet's proxy cadvdisor metrics endpoint replacement: /api/v1/nodes/$${1}/proxy/metrics/cadvisor metric_relabel_configs: # extract readable container/pod name from id field - action: replace source_labels: [id] regex: '^/machine\.slice/machine-rkt\\x2d([^\\]+)\\.+/([^/]+)\.service$' target_label: rkt_container_name replacement: '$${2}-$${1}' - action: replace source_labels: [id] regex: '^/system\.slice/(.+)\.service$' target_label: systemd_service_name replacement: '$${1}' processors: # rename labels which apply to all metrics and are used in metricstransform/rename processor metricstransform/label_1: transforms: - include: .* match_type: regexp action: update operations: - action: update_label label: name new_label: container_id - action: update_label label: kubernetes_io_hostname new_label: NodeName - action: update_label label: eks_amazonaws_com_compute_type new_label: LaunchType # rename container and pod metrics which we care about. # container metrics are renamed to `new_container_*` to differentiate them with unused container metrics metricstransform/rename: transforms: - include: container_spec_cpu_quota new_name: new_container_cpu_limit_raw action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_spec_cpu_shares new_name: new_container_cpu_request action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_cpu_usage_seconds_total new_name: new_container_cpu_usage_seconds_total action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_spec_memory_limit_bytes new_name: new_container_memory_limit action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_memory_cache new_name: new_container_memory_cache action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_memory_max_usage_bytes new_name: new_container_memory_max_usage action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_memory_usage_bytes new_name: new_container_memory_usage action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_memory_working_set_bytes new_name: new_container_memory_working_set action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_memory_rss new_name: new_container_memory_rss action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_memory_swap new_name: new_container_memory_swap action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_memory_failcnt new_name: new_container_memory_failcnt action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_memory_failures_total new_name: new_container_memory_hierarchical_pgfault action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate", "failure_type": "pgfault", "scope": "hierarchy"} - include: container_memory_failures_total new_name: new_container_memory_hierarchical_pgmajfault action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate", "failure_type": "pgmajfault", "scope": "hierarchy"} - include: container_memory_failures_total new_name: new_container_memory_pgfault action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate", "failure_type": "pgfault", "scope": "container"} - include: container_memory_failures_total new_name: new_container_memory_pgmajfault action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate", "failure_type": "pgmajfault", "scope": "container"} - include: container_fs_limit_bytes new_name: new_container_filesystem_capacity action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} - include: container_fs_usage_bytes new_name: new_container_filesystem_usage action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate"} # POD LEVEL METRICS - include: container_spec_cpu_quota new_name: pod_cpu_limit_raw action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_spec_cpu_shares new_name: pod_cpu_request action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_cpu_usage_seconds_total new_name: pod_cpu_usage_seconds_total action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_spec_memory_limit_bytes new_name: pod_memory_limit action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_memory_cache new_name: pod_memory_cache action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_memory_max_usage_bytes new_name: pod_memory_max_usage action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_memory_usage_bytes new_name: pod_memory_usage action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_memory_working_set_bytes new_name: pod_memory_working_set action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_memory_rss new_name: pod_memory_rss action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_memory_swap new_name: pod_memory_swap action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_memory_failcnt new_name: pod_memory_failcnt action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate"} - include: container_memory_failures_total new_name: pod_memory_hierarchical_pgfault action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate", "failure_type": "pgfault", "scope": "hierarchy"} - include: container_memory_failures_total new_name: pod_memory_hierarchical_pgmajfault action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate", "failure_type": "pgmajfault", "scope": "hierarchy"} - include: container_memory_failures_total new_name: pod_memory_pgfault action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate", "failure_type": "pgfault", "scope": "container"} - include: container_memory_failures_total new_name: pod_memory_pgmajfault action: insert match_type: regexp experimental_match_labels: {"image": "^$", "container": "^$", "pod": "\\S", "LaunchType": "fargate", "failure_type": "pgmajfault", "scope": "container"} - include: container_network_receive_bytes_total new_name: pod_network_rx_bytes action: insert match_type: regexp experimental_match_labels: {"pod": "\\S", "LaunchType": "fargate"} - include: container_network_receive_packets_dropped_total new_name: pod_network_rx_dropped action: insert match_type: regexp experimental_match_labels: {"pod": "\\S", "LaunchType": "fargate"} - include: container_network_receive_errors_total new_name: pod_network_rx_errors action: insert match_type: regexp experimental_match_labels: {"pod": "\\S", "LaunchType": "fargate"} - include: container_network_receive_packets_total new_name: pod_network_rx_packets action: insert match_type: regexp experimental_match_labels: {"pod": "\\S", "LaunchType": "fargate"} - include: container_network_transmit_bytes_total new_name: pod_network_tx_bytes action: insert match_type: regexp experimental_match_labels: {"pod": "\\S", "LaunchType": "fargate"} - include: container_network_transmit_packets_dropped_total new_name: pod_network_tx_dropped action: insert match_type: regexp experimental_match_labels: {"pod": "\\S", "LaunchType": "fargate"} - include: container_network_transmit_errors_total new_name: pod_network_tx_errors action: insert match_type: regexp experimental_match_labels: {"pod": "\\S", "LaunchType": "fargate"} - include: container_network_transmit_packets_total new_name: pod_network_tx_packets action: insert match_type: regexp experimental_match_labels: {"pod": "\\S", "LaunchType": "fargate"} # filter out only renamed metrics which we care about filter: metrics: include: match_type: regexp metric_names: - new_container_.* - pod_.* # convert cumulative sum datapoints to delta cumulativetodelta: metrics: - new_container_cpu_usage_seconds_total - pod_cpu_usage_seconds_total - pod_memory_pgfault - pod_memory_pgmajfault - pod_memory_hierarchical_pgfault - pod_memory_hierarchical_pgmajfault - pod_network_rx_bytes - pod_network_rx_dropped - pod_network_rx_errors - pod_network_rx_packets - pod_network_tx_bytes - pod_network_tx_dropped - pod_network_tx_errors - pod_network_tx_packets - new_container_memory_pgfault - new_container_memory_pgmajfault - new_container_memory_hierarchical_pgfault - new_container_memory_hierarchical_pgmajfault # convert delta to rate deltatorate: metrics: - new_container_cpu_usage_seconds_total - pod_cpu_usage_seconds_total - pod_memory_pgfault - pod_memory_pgmajfault - pod_memory_hierarchical_pgfault - pod_memory_hierarchical_pgmajfault - pod_network_rx_bytes - pod_network_rx_dropped - pod_network_rx_errors - pod_network_rx_packets - pod_network_tx_bytes - pod_network_tx_dropped - pod_network_tx_errors - pod_network_tx_packets - new_container_memory_pgfault - new_container_memory_pgmajfault - new_container_memory_hierarchical_pgfault - new_container_memory_hierarchical_pgmajfault experimental_metricsgeneration/1: rules: - name: pod_network_total_bytes unit: Bytes/Second type: calculate metric1: pod_network_rx_bytes metric2: pod_network_tx_bytes operation: add - name: pod_memory_utilization_over_pod_limit unit: Percent type: calculate metric1: pod_memory_working_set metric2: pod_memory_limit operation: percent - name: pod_cpu_usage_total unit: Millicore type: scale metric1: pod_cpu_usage_seconds_total operation: multiply # core to millicore: multiply by 1000 # millicore seconds to millicore nanoseconds: multiply by 10^9 scale_by: 1000 - name: pod_cpu_limit unit: Millicore type: scale metric1: pod_cpu_limit_raw operation: divide scale_by: 100 experimental_metricsgeneration/2: rules: - name: pod_cpu_utilization_over_pod_limit type: calculate unit: Percent metric1: pod_cpu_usage_total metric2: pod_cpu_limit operation: percent # add `Type` and rename metrics and labels metricstransform/label_2: transforms: - include: pod_.* match_type: regexp action: update operations: - action: add_label new_label: Type new_value: "Pod" - include: new_container_.* match_type: regexp action: update operations: - action: add_label new_label: Type new_value: Container - include: .* match_type: regexp action: update operations: - action: update_label label: namespace new_label: Namespace - action: update_label label: pod new_label: PodName - include: ^new_container_(.*)$$ match_type: regexp action: update new_name: container_$$1 # add cluster name from env variable and EKS metadata resourcedetection: detectors: [env, eks] batch: timeout: 60s # only pod level metrics in metrics format, details in https://aws-otel.github.io/docs/getting-started/container-insights/eks-fargate exporters: awsemf: log_group_name: '/aws/containerinsights/{ClusterName}/performance' log_stream_name: '{PodName}' namespace: 'ContainerInsights' region: YOUR-AWS-REGION resource_to_telemetry_conversion: enabled: true eks_fargate_container_insights_enabled: true parse_json_encoded_attr_values: ["kubernetes"] dimension_rollup_option: NoDimensionRollup metric_declarations: - dimensions: [ [ClusterName, LaunchType], [ClusterName, Namespace, LaunchType], [ClusterName, Namespace, PodName, LaunchType]] metric_name_selectors: - pod_cpu_utilization_over_pod_limit - pod_cpu_usage_total - pod_cpu_limit - pod_memory_utilization_over_pod_limit - pod_memory_working_set - pod_memory_limit - pod_network_rx_bytes - pod_network_tx_bytes extensions: health_check: service: pipelines: metrics: receivers: [prometheus] processors: [metricstransform/label_1, resourcedetection, metricstransform/rename, filter, cumulativetodelta, deltatorate, experimental_metricsgeneration/1, experimental_metricsgeneration/2, metricstransform/label_2, batch] exporters: [awsemf] extensions: [health_check] ADOTに接続するためのService設定をClusterIPで設定します。 apiVersion: v1 kind: Service metadata: name: adot-collector-service namespace: fargate-container-insights labels: app: aws-adot component: adot-collector spec: ports: - name: metrics # default endpoint for querying metrics. port: 8888 selector: component: adot-collector type: ClusterIP 上記で設定したConfigmapを元にADOTCollectorをStatefullsetとして作成します。 apiVersion: apps/v1 kind: StatefulSet metadata: name: adot-collector namespace: fargate-container-insights labels: app: aws-adot component: adot-collector spec: selector: matchLabels: app: aws-adot component: adot-collector serviceName: adot-collector-service template: metadata: labels: app: aws-adot component: adot-collector spec: serviceAccountName: adot-collector securityContext: fsGroup: 65534 containers: - image: amazon/aws-otel-collector:v0.15.1 name: adot-collector imagePullPolicy: Always command: - "/awscollector" - "--config=/conf/adot-collector-config.yaml" env: - name: OTEL_RESOURCE_ATTRIBUTES value: "ClusterName=YOUR-EKS-CLUSTER-NAME" resources: limits: cpu: 2 memory: 2Gi requests: cpu: 200m memory: 400Mi volumeMounts: - name: adot-collector-config-volume mountPath: /conf volumes: - configMap: name: adot-collector-config items: - key: adot-collector-config path: adot-collector-config.yaml name: adot-collector-config-volume Container Insightsでの確認 ここまでの設定を行い環境作成が終わり、 k8s 環境にpodを作成すると自動的にContainerInsights上でメトリクスデータが見れるようになっています。 また、こちらの ダッシュ ボードのもととなるメトリクスはCloudwatchメトリクス上でも確認することが可能です。 ADOTをEKSonFargate+EC2に導入するには ここまでの設定でFargateの情報を取得することができるようになりました。 ただし、EC2交じりの構成を組んでいた場合、EC2上のpodのメトリクスは上記の方法では取得できません。 そこで追加で2つの改変を行うことでEC2上のメトリクスも取得できるようにしてみましょう。 ノードグループにラベルを追加 EC2のノードグループにラベルを追加します。 LaunchType:EC2と追加しましょう。 Configmapの修正 次にConfigmapに以下のように修正を加えましょう。 「processors」は現状ではLaunchType:Fargateとなっているもののデータしか収集しないようになっています。 そのためLanchType:EC2も対象となるようにしましょう。 またmemory_utilizationもFargateで作成した場合はPod_memoryというデータになってしまい、 EC2上のPodの情報がうまく取れないので「container_memory_utilization_over_pod_limit」という名前で追加作成しておきます。 最後に「exporters」上に、先ほど作った「container_memory_utilization_over_pod_limit」と「container_memory_working_set」「container_memory_limit」を追加しておきましょう。 apiVersion: v1 kind: ConfigMap metadata: name: adot-collector-config namespace: fargate-container-insights labels: app: aws-adot component: adot-collector-config data: adot-collector-config: | receivers: 中略 processors: # rename labels which apply to all metrics and are used in metricstransform/rename processor metricstransform/label_1: transforms: - include: .* match_type: regexp action: update operations: - action: update_label label: name new_label: container_id - action: update_label label: kubernetes_io_hostname new_label: NodeName - action: update_label label: eks_amazonaws_com_compute_type new_label: LaunchType # rename container and pod metrics which we care about. # container metrics are renamed to `new_container_*` to differentiate them with unused container metrics metricstransform/rename: transforms: - include: container_spec_cpu_quota new_name: new_container_cpu_limit_raw action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate|EC2"} - include: container_spec_cpu_shares new_name: new_container_cpu_request action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate|EC2"} - include: container_cpu_usage_seconds_total new_name: new_container_cpu_usage_seconds_total action: insert match_type: regexp experimental_match_labels: {"container": "\\S", "LaunchType": "fargate|EC2"} 中略 experimental_metricsgeneration/1: rules: - name: pod_network_total_bytes unit: Bytes/Second type: calculate metric1: pod_network_rx_bytes metric2: pod_network_tx_bytes operation: add - name: pod_memory_utilization_over_pod_limit unit: Percent type: calculate metric1: pod_memory_working_set metric2: pod_memory_limit operation: percent - name: container_memory_utilization_over_pod_limit ←追加 unit: Percent type: calculate metric1: new_container_memory_working_set metric2: new_container_memory_limit operation: percent 中略 exporters: awsemf: log_group_name: '/aws/containerinsights/{ClusterName}/performance' log_stream_name: '{PodName}' namespace: 'ContainerInsights' region: ap-northeast-1 resource_to_telemetry_conversion: enabled: true eks_fargate_container_insights_enabled: true parse_json_encoded_attr_values: ["kubernetes"] dimension_rollup_option: NoDimensionRollup metric_declarations: - dimensions: [ [ClusterName, LaunchType], [ClusterName, Namespace, LaunchType], [ClusterName, Namespace, PodName], [ClusterName, Namespace, PodName, LaunchType]] metric_name_selectors: - pod_cpu_utilization_over_pod_limit - pod_cpu_usage_total - pod_cpu_limit - pod_memory_utilization_over_pod_limit - container_memory_utilization_over_pod_limit ←追加 - pod_memory_working_set - container_memory_working_set ←追加 - pod_memory_limit - container_memory_limit ←追加 - pod_network_rx_bytes - pod_network_tx_bytes 後略 こちらのconfigmapを再度適応したADOTCollectorを展開してみます。 そうするとメトリクスをCloudwatchメトリクス上でも確認することが可能です。 ※新しく追加したデータはContainerInsights上では確認が取れませんので注意が必要です。 まとめ さて、実際の流れを通して ADOT の使い方の一例としてEC2podの情報取得方法についてご案内させていただきました。 今回は既存のADOT設定を踏襲するようにしたため、無駄な設定もありもっと改善の余地はあるかと思います。 本記事を参考にADOT使ってみたけどFargateとEC2の両方のメトリクスはどうやってとればいいんだ!って人の参考になれば幸いです。 参考 https://aws.amazon.com/jp/blogs/news/introducing-amazon-cloudwatch-container-insights-for-amazon-eks-fargate-using-aws-distro-for-opentelemetry/ https://opentelemetry.io/docs/ https://kubernetes.io/docs/concepts/cluster-administration/system-metrics/
アバター
はじめに ラク スのサービスでは請求書や領収書をはじめ、様々な文書を取り扱っています。 例えば楽楽精算では領収書の読み取り機能を有しており、この機能にはAIを用いた画像認識を活用しています。 このように文書画像を対象としたAI(以下、本記事では文書画像読解AIと呼びます)は、様々なタスクに応用できます。 そこで今回の記事では、文書画像読解AIではどのようなタスクを解くことができるか、代表的なものを紹介します。 また各タスクに適用できるモデルについて、本記事執筆時点でのSOTAモデル *1 をいくつか簡単に紹介します。 文書画像を扱うタスクやモデルにどのようなものがあるか、概要を知りたい方に向けた内容となっております。 目次 はじめに 目次 サマリー 文書画像読解AIのタスク OCR(Optical Character Recognition、光学文字認識) レイアウト解析 (Document Layout Analysis) 文書画像分類 (Document Image Classification) 情報抽出 (Key Information Extraction) DocVQA (Document Visual Question Answering) 文書画像読解AIのSOTAモデル OCR レイアウト解析 文書画像分類 情報抽出 DocVQA モデルの一例 LayoutLM Donut (OCR-free Document Understanding Transformer) 終わりに 参考文献 サマリー AIで解くことができる文書画像タスクには様々なものがある。特に OCR は別タスクの前処理としても扱われることがあり、重要な基礎技術である。 一つの アルゴリズム で複数のタスクに応用できるモデルがある。 画像とテキスト両方の特徴を活用したマルチモーダルAIが、各タスクで高い精度を発揮している。 文書画像読解AIのタスク 上述のように、文書画像読解AIは様々なタスクに活用できます。 本章ではそのうち5つのタスクを紹介します。 各タスクではAIの入出力のフォーマットが異なるのが特徴です。 OCR (Optical Character Recognition、 光学文字認識 ) 画像データに含まれる文字(活字、手書き)を、PCが処理可能な文字データに変換するタスクです。 AIに画像を入力すると、以下のように文字の内容と座標(検出枠)が出力されます。 OCR の結果例 [1] OCR は主に「検出」と「認識」の2段階の処理に分解できます。 (※検出と認識を組み合わせたような Text Spotting というタスク・手法もありますが、本記事では扱いません。) 検出では文書画像中に含まれた文字の位置を推測し、認識では検出した文字が何であるかを推測します。 OCR は後述の別タスク「情報抽出」などの前処理として採用されることもあり、文書画像読解AIの基礎技術と言うことができます。 レイアウト解析 (Document Layout Analysis) 文書画像に含まれる文章、表、タイトル、図などを検出(またはセグメンテーション処理)するタスクです。 検出の場合、AIの入力値は画像、出力値は座標と分類クラスとなります。 以下は論文画像をレイアウト解析した例で、青が図(figure)、緑が文章(text)、黄色が表(table)、赤がタイトル(title)となっています。 論文をレイアウト分析した例 [2] 文書画像分類 (Document Image Classification) 画像の特徴から文書が何の種類か判定するタスクです。例えば請求書、領収書、納品書などへ分類します。 AIの入力は画像、出力は分類したクラスとなります。 情報抽出 (Key Information Extraction) 文書画像に含まれる特定の項目を推論します。例えば請求書に記載された日付、会社名、請求金額などを推測します。 手法によりますがAIの入力は画像と文字情報(座標と文字の内容)、出力は項目名と値のペア(Key- value pair)となることが一般的です。 例えば以下の例では、レシート画像(a)を入力すると(b)のようにAIから出力されます。(c)は正解値であり、この例ではAIが正確に推論できていることがわかります。 レシートから情報抽出した例 [3] DocVQA (Document Visual Question Answering) 正確にはDocVQAというのはデー タセット の名前で、文書の内容に関する質問を投げると、画像に含まれた情報から回答を出力するタスクです。 AIの入力は文書画像と質問内容となります。 以下はその一例で、”Q”が質問内容、”Answer”が正解値、” Donut ”と”LayoutLMv2-Large-QG”がAIの出力です。 DocVQAの例 [3] 文書画像読解AIのSOTAモデル 本章では、前章の5つのタスクについてSOTAモデルについて簡単に紹介します。 SOTAを紹介する上での補足事項です。 全て紹介すると長くなってしまうので、今回は上位5個程度抜粋しました。同じ アルゴリズム でパラメータ設定などが異なるものについては、最良のモデルのみとしています。 今回紹介するSOTAモデルの結果には”Papers With Code” [4]というサイトや各モデルの論文を活用しております。紹介する内容は本記事執筆時点での情報である点はご了承ください。 各タスクによってデー タセット や評価指標が異なりますが、それらについての詳細解説は省略させていただきます。 OCR OCR は検出と認識に分割できると述べました。 したがって本記事では検出: Scene Text Detectionのモデル、認識: Scene Text Recognition のモデルとしてそれぞれ個別に紹介させていただきます。 ※ Tesseract や PaddleOCR などの OCR エンジンについては本記事では触れておりません。 Scene Text Detection 評価データ Total-Textを使用した場合です。 評価指標は F値 を用いています。[5] モデル F値 (%) 報告年 MixNet 90.5 2023 SRFormer 90.0 2023 DPText-DETR 89.0 2022 FAST-B-800 87.5 2021 TextFuseNet 87.5 2020 Scene Text Recognition 評価データ ICDAR2015の場合です。 評価指標はAccuracy(正解率)を用いています。[6] モデル 正解率 報告時期 CLIP4STR 90.6 2023 PARSeq 89.6±0.3 2022 S-GTR 87.3 2021 MATRN 86.6 2021 CDistNet 86.25 2021 レイアウト解析 PubLayNet val というデー タセット を用いた評価です。 評価指標はmAP@IoU[0.50:0.95]を使用しています。[7] モデル mAP@IoU[0.50:0.95] 報告時期 LayoutLMv3 0.951 2022 DiT-L 0.949 2022 Deit-B 0.932 2020 BEiT-B 0.931 2021 文書画像分類 RVL-CDIPという16種類の文書画像が含まれたデー タセット を使っています。 評価指標はAccuracy(正解率)を用いています。[8] モデル 正解率 報告時期 DocFormerBASE 96.17 2021 LayoutLMv3 Large 95.93 2022 LiLT [EN-R] BASE 95.68 2022 LayoutLMv2 Large 95.64 2020 TILT Large 95.52 2021 Donut 95.3 2021 情報抽出 CORDというデー タセット を使った評価です。 評価指標は F値 となります。[9] モデル F値 (%) 報告年 GeoLayoutLM 97.97 2023 LayoutLMv3 Large 97.46 2022 DocFormer Large 96.99 2021 LiLT 96.07 2022 LayoutLMv2Large 96.01 2020 DocVQA DocVQAというデー タセット での評価結果です。 評価指標はANLS(Average Normalized Levenshtein Similarity)となります。[10] モデル ANLS 報告年 ERNIE-Layout Large 0.8841 2022 TILT Large 0.8705 2021 LayoutLMv2 0.867 2020 LayoutLMv3 Large 0.8337 2021 モデルの一例 ここでは上述のモデルのうち、複数のタスクで使用できる「LayoutLM」と「 Donut 」をピックアップして簡単に紹介します。 これらのモデルは学習に使う データ形式 を変えることで、複数のタスクに対応できるように開発されています。 LayoutLM 適用可能タスク: レイアウト解析、文書画像分類、情報抽出、DocVQA OCR による文字情報と、画像の特徴を両方学習させるマルチモーダルモデルです。 これまでにv1〜v3が開発されています。 LayoutLMv3の概要図[2] OCR の結果(文字情報)と画像情報から推論する。 Donut ( OCR -free Document Understanding Transformer) 適用可能タスク: 文書画像分類、情報抽出、DocVQA 画像とプロンプトを与えると結果を出力します。 OCR 処理を使わないのが特徴です。 Donut の概要図 [3] 画像とプロンプトを入力すると、使用したモデルとプロンプトの内容に応じて結果を出力する。 終わりに 本記事では文書画像読解AIについて、AIが扱えるタスクとそのモデルについて簡単に紹介しました。 新しいモデルが次々に開発される分野ですので、今後も注目していきたいと思います。 また現在 ラク スではAIエンジニアを募集しております。このようなモデル検証や アルゴリズム 実装、実際のサービスへの組み込みまで、 一緒に当社のAI開発を推進していただける方は是非こちらの募集情報もご覧ください! エンジニアリングマネージャー/AI・機械学習 | エンジニア職種紹介 | 株式会社ラクス キャリア採用 AIエンジニア | エンジニア職種紹介 | 株式会社ラクス キャリア採用 参考文献 [1] T.Kil et al., “Towards Unified Scene Text Spotting based on Sequence Generation”, https://arxiv.org/pdf/2304.03435v1.pdf [2] Y. Huang et al., “LayoutLMv3: Pre-training for Document AI with Unified Text and Image Masking”, https://arxiv.org/pdf/2204.08387.pdf [3] G. Kim et al., “ OCR -free Document Understanding Transformer”, https://arxiv.org/pdf/2111.15664.pdf [4] Papers With Code, https://paperswithcode.com/sota [5] Scene Text Detection on Total-Text, https://paperswithcode.com/sota/scene-text-detection-on-total-text [6] Scene Text Recognition on ICDAR2015, https://paperswithcode.com/sota/scene-text-recognition-on-icdar2015 [7] Document Layout Analysis on PubLayNet val, https://paperswithcode.com/sota/document-layout-analysis-on-publaynet-val [8] Document Image Classification on RVL-CDIP, https://paperswithcode.com/sota/document-image-classification-on-rvl-cdip [9] Key Information Extraction on CORD, https://paperswithcode.com/sota/key-information-extraction-on-cord [10] Visual Question Answering (VQA) on DocVQA test, https://paperswithcode.com/sota/visual-question-answering-on-docvqa-test *1 : state-of-the-art モデル、最先端の高い性能を達成しているモデル
アバター
皆さん、こんにちは!もしくはこんばんは! 楽楽精算プロダクトマネージャーのwekkyyyyです。 前回は、「楽楽精算PdMの業務内容を紹介します」というタイトルで記事を書かせていただきました。 tech-blog.rakus.co.jp 今回は、第二弾として PBIの優先度設定方法のポイントと設定することの狙い というテーマでブログを記載します。 今回このテーマで書こうと思ったきっかけは、 弊社内やPdMコミュニティの中で 「なんでこの案件が優先されるんだろう?他にもあるのに。。。」 「次に何開発するんだろう?できれば少しずつ視野にいれていきたいな。。」 といった疑問を持たれている方が一定数いることを確認でき、そういった方達の一助になる対応事例を提供できれば・・・と思ったことです。 かく言う私も、PdM駆け出しの頃は上記のことを思ってました。 それを不満という形で上司にぶつけてしまい、よくないコミュニケーションを取ってしまっていました。(今思うと非常に恥ずかしいし申し訳ない限りです) 前提 PBIの優先度設定の狙い PBIの優先度設定方法のポイント 注意点 今後の執筆内容(変更可能性あり) ラクスのPdMとして活躍してみませんか? 前提 弊社プロダクトの中で「楽楽精算」での事例を記載しております。 優先度設定方法の 一例 として捉えてください。    ※企業によってそれぞれの状況、考え方があるので適した形を探してください。 PBIの優先度設定の狙い 1. 関係者が、先を見据えて行動できる土台をつくる プロダクトマネージャー目線: 顕在化している課題に対するソリューション案を検討し蓄積していくことを狙っています。 それにより開発リソースの空きが出た際にすぐに開発に要求仕様を渡せるようなるためです。 次に調査すべき課題の計画を事前に立てておき、動き出しを早くすることを狙っています。 調査計画を立てるためには意外と時間がかかると考えているためです。(どういう計画?は別記事で書く予定です) エンジニア目線: 技術改善提案をする際に、効果を増加できる案件があるか事前に把握し提案タイミング、内容を図れるようなることを狙っています。 技術に寄ってしまって、ユーザーニーズ、ビジネスゴールと紐づかないことを避けるためです。 2. 優先度について会話のレベルを上げる 「今の優先度設定方法だと〜の課題がある。なので〜のように変えよう。」というコミュニケーションにしていくことを狙っています。 優先度設定方法の土台がないと、前提認識内容が合わずに関係者コミュニケーションが難しくなることが多いと考えているためです。 PBIの優先度設定方法のポイント 1. 開発カテゴリ分類を決める 楽楽精算では、以下の開発カテゴリに分けています。 2. カテゴリの優先度を決める 楽楽精算では、維持管理案件が基本優先されるようにしています。  ※対応期限により後回しにすることはあります。 3. カテゴリ内の優先度指標を決める 維持管理:対応期限(期日が早いものから) 財務効果:失注、解約MRRの合計MRR(金額が高いものから) を楽楽精算では指標として定めています。 財務効果において、「それで事業目標を達成できるの?」という声がありそうですが、それはまた別記事で説明します。 4. 開発リスト入りの条件を決める ここまでを見ると、維持管理に入れてしまえば何でも優先されるようにみえてしまいます。 ですが、その状態は健全ではないのでチェッカー(事業本部長、開発部長の承認)を置いています。 注意点 上記が基本ルールとなりますが、事業戦略的な事情で一部優先度変わる場合もありえます。 ユーザビリティ 観点で見ないわけではありません。失注や解約に結びつかない事象は優先度が下がるという形です。 バグ、インシデント対応は、別途 しきい値 を決めて対応を進めていく必要があります。 コスト削減案件がある場合 財務効果観点で優先度を決めます。 今後の執筆内容(変更可能性あり) 開発案件につなぐ営業/CSからのVoC収集方法 ラク スのプロダクトマネージャーに必要なスキル エンジニアとのコミュニケーション術 ラク スのPdMとして活躍してみませんか? 楽楽精算PdMは、引き続き人材を募集しております。 是非カジュアル面談からお申し込みいただけると幸いです。 プロダクトマネージャー | エンジニア職種紹介 | 株式会社ラクス キャリア採用
アバター
始めに 弊社では、 数行と画像1枚の静的ページを表示させるためだけ に、1台サーバーを構築し保守運用してました。 それだけのために、1台のサーバーを保守運用するの馬鹿らしくね?????? \\\うん!馬鹿らしい/// ということで、 AWS 上に移行すること となりました にしました。 今回は、S3とCloudFrontを利用して静的ページを表示させる設定をご紹介させていただきます。 初歩的な内容となりますので、これから静的ページを作りたいんだけど!といった方向けの内容となります。 始めに 要件 構成について AWSの利用サービス 構成図 実際の設定手順 S3設定 1. バケット作成 2. indexファイルのアップロード CloudFront設定 1. ディストリビューションを作成 2.ポリシー設定 3.接続確認 Route53 設定 1. カスタム SSL 証明書発行 2. CNAME登録 3. ルーティング設定 4.カスタム SSL 証明書設定 5.確認作業 HTTPレスポンスの変更 1. Cloud Front関数 2. Cloud Front紐づけ 3.確認作業 おわりに 要件 今回の要件は下記です。 弊社の ドメイン が利用可能 HTTPレスポンスコード(501 や 503 番台等) が任意のものに変更が可能 https 通信で静的ページが表示可能 弊社のシステム側からのFWの許可設定を追加の必要がない 構成について AWS の利用サービス Route53 : 独自ドメイン を登録するために利用 CloudFront : 独自ドメイン の利用及びレスポンスコードを任意のものに変更するために利用 S3 : 静的ファイル(HTML) を配置 構成図 構成は下記を想定しています。 実際の設定手順 それでは早速設定手順に入らせていただきます。 S3設定 静的ページを保存するためだけに利用しております。 本番サービスで稼働する際には、ログ保存の設定 や 権限の設定などが色々かかわってきますが、 今回は最低限の設定だけとなります。 1. バケット 作成 下記の設定手順で バケット を設定する バケット 名: rakus-tset 他の設定: デフォルト 2. indexファイルのアップロード 下記のファイルを作成した バケット に保存します。 index.html <!DOCTYPE html> <html lang="ja"> <head> <title>hello rakus!</title> </head> <body> <h1>hello rakus!</h1> </body> </html> CloudFront設定 独自ドメイン の利用 及び レスポンスコードを任意のものに変更するために利用します。 まずは、 独自ドメイン を利用せずにhttp通信が可能なところまで設定します。 1. ディストリビューション を作成 ディストリビューション は下記のように設定します。 オリジン ドメイン : 作成したS3 を選択 オリジンアクセス : Origin access control settings (recommended) Origin access control : コン トロール 設定を作成 ※デフォルトで作成 ウェブアプリケーション ファイアウォール (WAF):保護しない 料金クラス : 北米と欧州のみを使用 IPv6 : オフ 他の設定: デフォルト 2.ポリシー設定 該当オリジンのページに戻った際に下記のようにでているため、作成したS3の バケット ポリシーにコピペで設定をします。 3.接続確認 ここまでで下記にアクセスできるようになったため、確認します。 https://ディストリビューションドメイン名/index.html Route53 設定 DNS にCNAME や ルーティングの設定を行います。 この設定を行うことで、CloudFront と 独自ドメイン が紐づき、 独自ドメイン での https 通信を行えるようになります。 1. カスタム SSL 証明書発行 こちらもCloudFrontの画面から設定します。 該当の ディストリビューション の設定から カスタム SSL 証明書 の配下にある [証明書をリク エス ト] を押下します。 設定画面の通りに進んでいきます。 下記の通りで設定していきます。 完全修飾 ドメイン 名: 任意 他の設定: デフォルト 2. CNAME登録 登録後下記のようにでるため、 証明書の表示を押下します。 その後、Route 53 でレコードを作成を押下してRoute53に登録します。 検証が終わるまで待機します。 3. ルーティング設定 Route53 のサーバーへアクセスし、該当ホストゾーンに遷移し、下記の設定を登録します。 レコード名: 設定した ドメイン 名 レコードタイプ : A エイリアス : はい トラフィック のルーティング先 : 作成した ディストリビューション を選択 4.カスタム SSL 証明書設定 該当の ディストリビューション の設定を下記のように変更します。 代替 ドメイン 名 (CNAME) : 先ほど登録したCNAME カスタム SSL 証明書 : カスタム SSL 証明書発行で作成したもの 5.確認作業 下記のコマンド実施します。 curl -I https://ドメイン名/index.html 現状では、レスポンスコードが200番で返ってきます。 HTTPレスポンスの変更 httpレスポンスを400番台や500番台で返したいため、そちらの内容を設定していきます Viewer Response Eventで HTTPレスポンスコードを書き換えます 1. Cloud Front関数 Cloud Frontの関数項目より下記を作成します。 名前: 任意 説明: 任意 説明: 開発コードは下記 ※レスポンスコードが200だった場合 503 番台に変更するようなものとなっております。 function handler(event) { var response = event.response; var contentType = response.headers['content-type'].value; if (response && response.statusCode === 200) { response.statusCode = 503; response.statusDescription = 'test mode'; } return response; } 発行タブより発行を行います。 2. Cloud Front紐づけ 作成した ディストリビューション の画面からビヘイビアを編集します。 関数の関連付け を下記のように設定してください。 ビューワーレスポンス: CloudFront function 先ほど作成した関数 を指定 3.確認作業 下記のコマンド実施します。 curl -I https://ドメイン名/index.html 結果(一部抜粋) HTTP/1.1 503 test mode Content-Type: text/html Content-Length: 143 Connection: keep-alive おわりに S3を静的ページで採用されることは結構あるかと思います。 ドメイン や レスポンス内容の制約により、利用されることを断念するケースもあるかもしれません。 CloudFront functionは非常に便利で色々なことができますので、よろしければ掘ってみてもおもしろいかもしれません。 次は、署名付き URL と署名付き Cookie あたりを触りたいと思います。 最後まで読んでいただきありがとうございました。 皆さまの参考になれば幸いです。
アバター
こんにちは!フロントエンド開発課所属の koki _matsuraです! 今回はものすごく今更感が否めないのですが、Reactのv18で発表された「Suspense」とVercel社が提供しているReact Hooksライブラリの「SWR」によって何を解決してくれるのか、 コンポーネント の表示と実装を例に紹介します。 目次は以下のようになっています。 Suspenseとは SWRとは Suspense・SWRが解決すること Suspense・SWR導入におけるコンポーネント表示の変化 Suspense なし SWR なし Suspense あり SWR なし Suspense あり SWR あり Suspense・SWR導入におけるコンポーネント実装の違い Suspense なし SWR なし Suspense なし SWR あり Suspense あり SWR あり まとめ 終わりに Suspenseとは React16.6で実験的な機能として追加され、React18で正式に追加された機能で、「 コンポーネント のローディング状態をハンドリングする」ことが役割となっています。 基本的にこれだけなのですが、これにより コンポーネント 単位でのロードを可能にします。 SWRとは データ取得のためのライブラリです。 名前の由来はHTTPキャッシュ無効化戦略の"stable-while-revalidate"です。 こちらもやっていることはシンプルで特定のデータをキャッシュし、もう一度、必要になった際にキャッシュからデータを返します。また、定期的に裏側でフェッチをし、最新のデータに更新してくれます。 Suspense・SWRが解決すること 結論からいきます。 Suspenseは以下のことを解決します。 データ取得のローディング状態を宣言的にする コンポーネント 単位でのロードを可能にする コンポーネント の責務を明確にする SWRは以下のことを解決します。 キャッシュにより、更新時の長期のローディングがなくなる API 通信が簡略化される フェッチしたデータの管理をしなくてよくなる よくSuspenseは「ローディング状態をハンドリングするもの」のように思ってしまっている方がいます。もちろん間違ってはいませんが、それだけではないということです。 むしろ、Suspenseのいいところは「 コンポーネント の責務を明確にする」部分です。これが本質だと思っています。 SWRはキャッシュの管理や非同期処理状態の管理をしてくれるため、データのロードを待つ必要がなかったり、データの取得を非常にシンプルにしてくれます。 ここからは コンポーネント の表示と実装を例に、実際にSuspenseとSWRが解決していることを紹介していきます。 Suspense・SWR導入における コンポーネント 表示の変化 表示においてSuspenseとSWRを取り入れることでどのような変化があるのかを紹介します。また、更新時においての違いも見ていきます。 下記のパターンで変化を見ていきます。 Suspense なし SWR なし Suspense あり SWR なし Suspense あり SWR あり 下図のようにページに3つの コンポーネント を表示する例で比較します。それぞれの コンポーネント は上から1秒間、2秒間、3秒間かかるフェッチ処理を行っているため、表示するにも1秒間、2秒間、3秒間以上かかります。 Suspense なし SWR なし まずはSuspenseもSWRも使っていない例です。初期表示時と更新時の動画を載せています。 ・初期表示時 ・更新時 初期表示時は「Suspenseを利用しない」というボタンを押したときに該当のページをリク エス トします。そこから3秒間経過したのち、3つの コンポーネント が揃った状態で画面が表示されています。 更新時も3秒後に「◯秒後に表示される コンポーネント _」の後ろについている文字列が一気に変更されていることがわかります。 これはサーバが完全にHTMLを構築してからクライアントに返していることを表しています。 図で表すと以下のようになります。 ページをリク エス ト API サーバへリク エス ト(3つ) 1,2,3秒後にレスポンスをWEBサーバへ返却 WEBサーバは全て揃ってからHTMLを構築 クライアントへHTMLを返却 HTMLを描画し、JSをロード、HTMLにハイドレーションを実行 少し雑に紹介していますが、大まかな流れを表せていると思います。初期表示や更新時に毎回3秒待たされるのは問題です。また、JSのロードやハイドレーションのことを考えると操作できるようになるには3秒よりもかかると考えられます。 ここにSuspenseを導入し、ページのロード単位を コンポーネント 単位にすることで大きな待ち時間を解決してくれます。 Suspense あり SWR なし Suspenseを導入すると以下のように変化します。 ・初期表示時 ・更新時 初期表示時は「SWRなし」というボタンを押すとページがリク エス トされ、すぐにページが遷移しています。そして、1,2,3秒後にそれぞれの コンポーネント が順に表示されていくことが確認できます。取得中は代わりの コンポーネント (今回はローディング コンポーネント )が表示されます。 更新時も同様に、全体が取得中になり、1,2,3秒で コンポーネント が表示されていきます。 WEBサーバはHTMLが未完成の状態でも返してくれることがわかると思います。 図で表すと以下のようになります。少し見にくくなっています。すみません。 ページをリク エス ト API サーバへリク エス ト(3つ) レスポンスがいらない部分だけのHTMLを構築 クライアントへ3で作成したHTM Lを返却 HTMLを描画、JSをロード、ハイドレーション。取得中の部分は代わりの コンポーネント を表示 「1秒後の コンポーネント 」のレスポンスをWEBサーバへ 「1秒後の コンポーネント 」のHTMLを構築 「1秒後の コンポーネント 」を返却 「1秒後の コンポーネント 」を表示し、その部分のJSをロード、ハイドレーション 以下略... 10〜16は6〜9と同じ流れになるので省略しました。 Suspenseを使うことで コンポーネント 単位でのロード(JSのロード・ハイドレーション)を可能にすることにより、初期表示の大きな待ち時間を解決 してくれています。 しかしながら、更新するたびに完全に表示し直すのに結局3秒以上かかるのは問題です。 ここをSWRのキャッシュ管理により解決します。 Suspense あり SWR あり SWRを導入すると、以下のようになります。 ※ SWRを導入すると、 SSR はできません。 CSR になります。 ・初期表示時 ・更新時 初期表示はSuspenseのみを導入した時と見た目は変わりません。 見た目は変わらないのですが裏側ではフェッチと共にデータをキャッシュしており、更新時にはそのキャッシュを返すことですぐに表示することができています。つまり、SWRにより 更新時の長期のローディングが解決 されていることがわかります。 また、裏で再フェッチしてくれているので最新情報を取得できればすぐに コンポーネント が更新されていることが文字列が変更されていることで確認できます。 コンポーネント の表示で比較することでSuspenseの コンポーネント 単位のロードによる表示速度の解決 とSWRの キャッシュ管理による更新時の表示速度の解決 を実際に見て、理解できたと思います。 次は、コード側から見てSuspenseとSWRが解決してくれることを紹介します。 Suspense・SWR導入における コンポーネント 実装の違い こちらも下記の3パターンで変化を見ていきます。先ほどとは違って、SWRだけを導入した場合とそこにSuspenseを導入した場合で紹介させていただきます。 Suspense なし SWR なし Suspense なし SWR あり Suspense あり SWR あり ユーザ情報を表示する コンポーネント を例にします。 Suspense なし SWR なし 下記はSuspense・SWRを共に使っていないUser コンポーネント で、ユーザ情報を表示するためだけのシンプルな コンポーネント です。 const User = () => { const [ user , setUser ] = useState < User >( null ); useEffect (() => { fetchUser () .then (( res ) => setUser ( res )); } , [] ); if ( user === null ) return < Loading / >; return < Contents user = { user } / >; } ; データがnullの間、つまり取得中の間はローディングを表示、nullじゃなければ、ユーザ情報を表示するようになっています。そのユーザ情報はuseEffect内のfetchUser()で取得され、stateにより管理されています。 まず、問題として挙げられるのは、useEffect内でフェッチをしている点です。本来、useEffectは副作用処理を書くためのものであって、冪等性のない処理を書かない方が望ましいです。 また、User コンポーネント の中でLoadingとContent コンポーネント を出し分けが行われている点も問題です。今回は例に挙げなかったのですが、フェッチのエラー処理もすることがあります。そうなると、Error コンポーネント も出し分けに加わることになります。 本来はユーザ情報を表示することのみが仕事のUser コンポーネント がローディング、エラー、ユーザ情報を表示する コンポーネント になってしまいます。 これらの問題をSWRを導入した時、どのように変わるか見ていきましょう。 Suspense なし SWR あり 先ほどの コンポーネント 表示ではSWRによってキャッシュが働くことを確認できたと思います。 コードは以下のようになります。 const User = ()=> { const { data: user , isLoading } = useSWR ( "user" , fetchUser ); if ( isLoading ) return < Loading / >; return user && < Contents user = { user } / >; state管理・useEffectがなくなり、その代わりにuseSWRフックを使っています。 useSWRは API 通信を簡素化してくれるため、 本来書くべきではないuseEffect内でのフェッチ処理やフェッチ後のデータ管理、キャッシュの管理の問題を解決 することができます。 今回は書いていませんが、useSWRはエラーにも対応しています。 SWRだけでも非常にシンプルになりましたが、まだUser コンポーネント はローディングとユーザの出し分けという複数の責務を持ってしまっています。ここにSuspenseを取り入れてみます。 Suspense あり SWR あり Suspenseを取り入れたUser コンポーネント は以下のようになります。 const User = ()=> { const { data: user } = useSWR ( "user" , fetchUser , { suspense: true , } ); return < Contents user = { user } / >; SWRでSuspenseを使いたい時はuseSWRの第三引数でsuspenseオプションをつけるだけです。 変更点はローディングの出し分けがなくなった部分です。 SuspenseがLoading コンポーネント を出す責務を担ってくれました。たった一行減っただけですが、これで コンポーネント の責務の不明確化 が解決できました。 ちなみに、Suspenseを使うときは該当の コンポーネント をSuspense コンポーネント で囲むだけで実装できます。 今回の場合は以下のようになります。 < Suspense fallback = { < Loading / > } > < User / > < /Suspense > fallbackの中にローディング中に表示したい コンポーネント を入れるだけです。 ローディング状態が命令的なものから宣言的なもの になっています。 Suspense・SWRを取り入れることにより従来の コンポーネント の問題点を解決できることが理解できたと思います。 まとめ コンポーネント の表示による違いと実装の違いを例にSuspense・SWRが何をしてくれるのかを説明してきました。 まとめると、SWRは今まで コンポーネント 内で行っていたデータの管理やフェッチの処理をuseSWRフックのみで完結させ、キャッシュまでも柔軟に管理してくれるライブラリです。 そして、Suspenseはローディング状態を宣言的にしてくれることにより、 コンポーネント 自体を非同期処理として扱え、責務を明確にするとともに、 コンポーネント 単位のロードを可能にしてくれました。 終わりに ここまで読んでいただき、本当にありがとうございます。 間違っている点などがあれば、ぜひコメントでおしえてください! この記事がSuspenseやSWRに興味を持つきっかけになってくれれば幸いです。
アバター
はじめに こんにちは、 MasaKu です。 弊社では、 PHP に関する最新ニュースの発信や気になるお題について議論するイベント「 PHP TechCafe」を毎月開催しております。 本日は、 PHP TechCafe とはどんなイベントなのかのご説明と、過去開催したイベントの中で特に盛り上がったイベントをご紹介させていただきます。 Web × PHP TechCafe はじめに PHP TechCafeの目的 立ち上げからの経緯 参加対象者とその理由 運営メンバー テーマ選定方針 コンテンツ作り 特に評判の良かったテーマ10選 PHPerのための「PHPと型定義を語り合う」 PHP TechCafe PHPerのための「PHPのリーダブルなコード」を語り合うPHP TechCafe PHPerのための「Laravel10の新機能」を語り合う PHP TechCafe PHPerのための「PHPDoc相談会」PHP TechCafe PHPerのための「PHP8.2の新機能」を語り合うPHP TechCafe PHPerのための「Composer」を語り合うPHP TechCafe PHPerのための「PHPフレームワーク」を語り合うPHP TechCafe PHPerのための「静的解析」を語り合うPHP TechCafe PHPerのための「Xdebugの活用方法」を語るTechCafe PHPerのための「PHPUnit の始め方」について語りあう PHP TechCafe おわりに PHP TechCafeの目的 PHP TechCafe は月に一度 ZOOM で開催されているオンラインイベントです。 エンジニア同士の交流の機会を提供する、エンジニアと技術が交差する憩いの場(カフェ)になれるようなイベントを目指しています。 PHP TechCafe というイベントそのものが学びの場となり、運営メンバーも含め、参加者全員がエンジニアとしてレベルアップしていけるように支援することを目的として開催しております。 立ち上げからの経緯 立ち上げ当時は PHP をテーマにした勉強会ではなく、弊社のカフェスペースを利用した社外向けの もくもく会 でした。 もくもく会 で社外のエンジニアと交流を深めていく中で、技術的なテーマで交流できることの楽しさに気づきました。 もっと技術的なテーマで語り合う方法はないかと検討したところ、自分たちの強みである PHP を軸にしたイベントにしてみてはどうかという方向性に至り、以来模索を重ねてきました 最初は PHP のニュースを紹介するだけのイベントでしたが、イベントに参加してくださった参加者にもっと価値を提供できないかと考え、特集コーナーを設けるなどして現在の形に進化してきました。 参加対象者とその理由 PHP TechCafe の参加対象者 PHP TechCafe の参加対象者は PHP 入門後の初級エンジニア ~ シニアエンジニア としています。 理由としましては、 PHP TechCafe を運営しているメンバーのレベル感や弊社の PHP エンジニアの技術領域もこの辺りに位置しているため、イベントを通して参加者と一緒に成長していきたいと考えているためです。 PHP TechCafe のイベントにはエキスパート以上のエンジニアが参加してくださることもあります。 そういった際には積極的に質問するなどして運営メンバーのレベルアップにもつなげていきたいと考えています。 運営メンバー 運営メンバーは実際にイベントに参加するメインメンバーとイベントのコンテンツ作成に協力していただくサポートメンバーで構成されています。 現在以下のメンバー構成で実施しています。 メインメンバー Y-Kanoh , MasaKu サポートメンバー 8名 弊社の PHP 系プロダクトから各2名ずつ参加しています 上記のうちイベントサポーターは2班に分けて隔月で参加してもらっています。 そのため、普段のイベントはメインメンバー2名とイベントサポーター4名の合計6名で作業をしています。 テーマ選定方針 基本的には運営メンバーが学んでみたい PHP 関連の技術をテーマにしています。 直近の業務で扱っていた部分や、業務に取り入れてみたい技術などがテーマに上がることがあります。 他のパターンとしては、以下のようなものがあります。 新しいバージョンの PHP や フレームワーク が公開されたタイミング 新機能の紹介 各種 PHP 関連イベント( PHPカンファレンス 等)が開催されたタイミング ふりかえり会などを開催 ※イベントのふりかえり会を実施する際は事前にイベントの運営元に許可をいただいて実施しております 参加者が多く集まったり、 SNS やZOOMのチャットで盛り上がった会については、イベントのブラッシュアップを行うなどして繰り返し実施することもあります。 テーマが決まったら、イベント当日はどのような流れで話をすれば盛り上がりそうか、ということを考えながら アジェンダ を作成するようにしています。 コンテンツ作り 選定したテーマを元にサポートメンバーと共にイベント当日に公開する ShowNote を作成します。 担当範囲はテーマ選定の時に作成した アジェンダ を元に分担しています。 各々が調べてきた内容をイベント当日までに持ち寄って情報共有を行い、メインメンバーがしっかりとインプットして当日のイベントに備える、という流れで準備をしています。 特に評判の良かったテーマ10選 以下では、特に評判の良かった回をピックアップさせていただきましたので、ご紹介いたします。 PHPerのための「PHPと型定義を語り合う」 PHP TechCafe rakus.connpass.com 概要 動的型付け言語である PHP は、手軽にコードを記述できる反面、 保守性の担保、または意図しない挙動を防ぐため、常に型を意識してコーディングを行う必要があります。 しかし、手軽に書ける反面、 PHP を始めたばかりの人の中には、あまり型を意識せず記載している人もいるのではないでしょうか? そんな方のために型との付き合い方を語り合いました! ShowNoteはこちら! hackmd.io PHPerのための「PHPのリーダブルなコード」を語り合うPHP TechCafe rakus.connpass.com 概要 PHP で読みやすいコードとはどのようなコードでしょう? 同じ処理でも書き方によって可読性、ひいては保守性は大きく変わります。 ビギナーPHPerに伝えたい「可読性の高いコード」について語りました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「Laravel10の新機能」を語り合う PHP TechCafe rakus.connpass.com 概要 Laravel の初版リリースから 11年目に Laravel10 がリリース予定されました。 Laravel の基礎的な内容をおさらいしつつ、Laravel10 の新機能について取り上げました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「PHPDoc相談会」PHP TechCafe rakus.connpass.com 概要 PHPDocは数が多く、ツールによって対応有無も異なります。 そんなPHPDocについて、イベント運営メンバが疑問に思ったことを中心に議論しました! PHP で型定義されている場合、PHPDocでも型を書いた方がいいか? PhpStorm最新版は配列の型、 連想配列 のkey, value の型を検知してくれる? レガシーシステム とPHPDocの向き合い方 など ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「PHP8.2の新機能」を語り合うPHP TechCafe rakus.connpass.com 概要 PHP8.2 は実用的な機能から破壊的な機能まで、様々な機能が追加されました。 PHP8.2 で実装される機能がどのようなものなのか、どういった用途があるのかについて語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「Composer」を語り合うPHP TechCafe rakus.connpass.com 概要 PHP の依存性管理ツールである "Composer" について深掘りしました。 何に使うものなのか、どのように使うのか、Packagist とは何なのか 等について語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「PHPフレームワーク」を語り合うPHP TechCafe rakus.connpass.com 概要 Laravel、 Symfony 、Cake、Slim など、 PHP の フレームワーク について有名どころをリストアップしました。 主催者一同、触ったことがない フレームワーク が多数存在する中、開催までにしっかりと調査して語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「静的解析」を語り合うPHP TechCafe rakus.connpass.com 概要 静的解析とは、コードを実行することなく行うコード検証のことです。 PHP にも PHP _CodeSniffer や PHPStan などの静的解析ツールが存在します。 PHP でなぜ静的解析が必要なのかや、各静的解析ツールの特徴について、深堀して語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「Xdebugの活用方法」を語るTechCafe rakus.connpass.com 概要 Xdebug は PHPer による デバッグ のお供であり、よく使われる "ステップ実行" だけでなく、さまざまな機能を提供する拡張ツールです。 実は、 Xdebug は "ステップ実行" だけでなく、様々な機能を有していますので、便利な使い方について語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp PHPerのための「PHPUnit の始め方」について語りあう PHP TechCafe rakus.connpass.com 概要 PHPUnit に興味はあるけど何から始めればいいの? 学ぶ前に全体像を把握したい!といった方を対象に、 PHPUnit の基本的な知識などを題材にしました。 PHP 初心者の方だけでなく、ベテランエンジニアの方も学び直し・気づきの発掘などの機会になればと思い語り合いました! ShowNoteはこちら! hackmd.io まとめ記事はこちら! tech-blog.rakus.co.jp おわりに PHP TechCafe は現在のオンライン形式になってから 約3年 イベントを継続しています。 イベント運営の苦労もありますが、 PHP 関連のテーマで社外のエンジニアと交流できることは貴重な経験になっています。 これからも PHP TechCafe をどうぞよろしくお願いいたします。 次回の PHP TechCafe は 9月26日 に開催します。 ご参加お待ちしております。 rakus.connpass.com
アバター
はじめに こんにちは akihiyo76 です。現在、私のチームではレビュー ガイドライン を明文化して、レビュアーは ガイドライン に従ってコードレビューを行なっています。この ガイドライン は、チームで運用を開始して2年になりますが、チームでも浸透しレビュー時に必ず利用するようになりました。 はじめに コードレビューの課題感 課題改善に向けて 採用したコードレビュー観点 1. Design(設計) 定義 具体例 2. Simplicity(理解容易性) 定義 具体例 3. Naming(命名) 定義 具体例 4. Style(コードスタイル) 定義 具体例 5. Functionality(機能要求) 定義 具体例 6. Test(テスト) 定義 具体例 7. Document(文章) 定義 具体例 指摘対応の要否 具体的な利用方法 指摘例 最後に コードレビューの課題感 私は現在モバイル開発チームに所属しておりますが、メンバーは若手エンジニアが中心です。一方、弊社のサービスは SaaS が中心であるため、これまでモバイル開発の経験者が少ない状況でした。そのため「モバイル技術のセオリーが分からない」という課題がチームにあり、コードレビューに苦労する状況でした。 その結果として、 メンバーの技術力の伸び悩み リリース後に一定数のバグが発生する といった状況でした。 課題改善に向けて そこで、この課題改善に取り組むことにしましたが、 レビュー指摘を類型化して、メンバーの技術力を定量化できないか と考えました。その手段として、コードレビューに対する ガイドライン を作成して、レビューコメントを類型化・ 定量 化することにしました。 一定の観点を持ってレビューコメントを類型化することで、KPIとして計測が可能( 見える化 )になり、弱点分析をすることができるからです。 ガイドライン を作成する上で、 Google が公開するレビューガイドライン を参考にして以下の7つのレビュー観点を設けることにしました。 採用したコードレビュー観点 では、実際にレビュー ガイドライン で採用した観点を紹介します。 No 観点 概要 1 Design 設計が適切か 2 Simplicity 理解容易性 3 Naming クラス、メソッド、変数名などの 命名 4 Style コードスタイル 5 Functionality 機能(要件)を充足しているか 6 Test テストの記述、パターンが適切 7 Document コメント、ドキュメントに関連 特にNo.1 ~ No.4は、 オブジェクト指向 の観点で非常に重要な観点といえます。しかし、これらの観点と概要だけでは判断が難い場面もあるかと思うので、もう少し具体的にコードベースで説明します。 1. Design(設計) 定義 システムにとって適切な責務・振る舞いになっているか。システムとして アーキテクチャ を遵守できているか。また、システム全体として 一貫性ある設計になっているか。 具体例 基本的には、以下のような オブジェクト指向 の基本である SOLID 原則に反するような場合、指摘の対象になります。 関心の分離原則違反(≒ 単一責任原則違反) 密結合 低凝集 DRY 原則違反 etc. 例えば、以下のコードの場合 add() で様々処理を行なっており、責務超過といえるため Design 指摘の対象になります。 class HogeDiscountManager { lateinit var manager: DiscountManager /** * 商品を追加する */ fun add(product: Product): Boolean { if (product.id < 0 ) { // バリデーション 1 throw IllegalArgumentException () } if (product.name.isEmpty()) { // バリデーション 2 throw IllegalArgumentException () } val temp: Int = if (product.canDiscount) { // 条件分岐 1 manager.totalPrice + manager.getDiscountPrice(product.price) } else { manager.totalPrice + product.price } return if (temp < 3000 ) { // 条件分岐 2 manager.totalPrice = temp manager.discountProducts.add(product) true } else { false } } } 2. Simplicity(理解容易性) 定義 システムとして可読性あるコーディングになっているか。 処理ができるだけシンプルな振る舞いになっているか。 具体例 以下のように実装が複雑になる場合、指摘の対象になります。 ネストが深い if 分 複雑な 三項演算子 文 冗長な SQL stream, filter, map を多用したObject整形文 etc. このように分岐が多い if 分は、Simplicity の指摘の対象になります。 // Before.kt val powerRate: float = member.powerRate / menber.maxPowerRate var currentCondition: Condition = Condition.DEFAULT if (powerRate == 0 ) { currentCondition = Condition.DEAD } else if (powerRate < 0.3 ) { currentCondition = Condition.DANGER } else if (powerRate < 0.5 ) { currentCondition = Condition.WARNING } else { currentCondition = Condition.GOOD } return currentCondition 実際のレビューコメントでは、以下のようにネストを解消するように指摘をする場合などに使用します。 // After.kt val powerRate: float = member.powerRate / menber.maxPowerRate if (powerRate == 0 ) { return Condition.DEAD } if (powerRate < 0.3 ) { return Condition.DANGER } if (powerRate < 0.5 ) { return Condition.WARNING } return Condition.GOOD 3. Naming( 命名 ) 定義 変数やクラス、メソッドに責務を意図した明確な名前が付けられているか。英語文法に誤りがないか。 typo もこれに含まれる。 具体例 このような 命名 に関する指摘をする場合に使用します。 振る舞いと一致しない変数名、関数名 責務と一致しない関数名 英文法の誤り etc. 例えば iOS アプリ開発 時においては、Swift Foundation や Cocoa の 命名規則 に準拠しない場合、Naming の指摘対象になります。基本的な 命名規則 は、利用している フレームワーク や言語の特性によるものが判断基準になります。 4. Style(コードスタイル) 定義 コードスタイル言語仕様に準拠しているか。 具体例 コードスタイルも同様に言語仕様や フレームワーク に準拠させることが基本になるため、これに反する場合に使用します。 静的解析違反 不適切なアクセス修飾子 表記違反(スネーク、キャメルなど) etc. 他にもモバイル開発では、公式( Apple 、 Google 等)で公開している ガイドライン 違反している場合もこれに含まれます。コードスタイルの判断はその人の経歴などの主観的な部分も影響するので、コードフォーマッターを導入し、 機械的 な判断基準を設けることもこの指摘点を減らす有効な手段です。 5. Functionality(機能要求) 定義 システムとして外部仕様を充足しているか。作者が意図した通りの振る舞いであるか。 また、システムの通信量、パフォーマンスに懸念がないか。 具体例 主な観点としては、外部機能を充足しているかという点が対象になります。 外部仕様の未充足(不具合) 概要設計書の フローチャート と異なるフローになっている 不要データを送信している etc. 6. Test(テスト) 定義 システムとして適切な自動テストを兼ね備えているか。自動テストの内容で品質を担保できているか。 また、システムを担保するパラメータ群を備えているか。 具体例 テストコードが期待になっていない場合や、テストでのパラメータに考慮漏れがある場合、指摘の対象になります。 対象のメソッドがテストされていない テストパターンが網羅できていない(パタメータテスト、 閾値 テストの不足) 分岐がパターンが網羅されていない 実装上宣言している静的定数値が直接ハードコードされている アーキテクトに準じたテストになっていない 7. Document(文章) 定義 ソースコード 上に記載されている doc、コメントが適切な内容であるか。 また、関連するドキュメントは更新されているか。その内容は適切か。 具体例 ソースコード に関連するコメントだけでなく、プロジェクトで管理している関連ドキュメント(README)も対象になります。 関連ドキュメントの更新漏れ(README など) doc やコメントの内容が不適切、内容が不適切 指摘対応の要否 更にコードレビューの現場では上記の7つの観点に加えて、指摘修正の要否を4つの累計に分けてコメントしています。 観点 概要 MUST PR、MR をマージするためには必ず修正が必要 SHOULD 修正なしにマージすることはできるが、リリースまでには修正が必要 IMO レビューアー観点の意見。修正不要 NITS IMO より細かい意見など。修正不要 このように、コードレビューでマージするために必要な修正は MUST 指摘となります。MUST と SHOULD の使い分けは難しい部分もありますが、これまでのレビュー ガイドライン の運用では、 SQL のパフォーマンスをより良くするための指摘やテストコードの最適化の指摘などで SHOULD は利用される場面もあります。その場合、修正タスクを Issue に積んだ上で(修正スコープの合意)、マージするようにしています。一方、IMO や NITS は修正は不要ですが、修正しない場合はその旨をコメントに返信してもらい、コメントを閉じてからマージする運用をしています(レビュアーとの合意)。 具体的な利用方法 実際にコードレビューをするとき、上記の7つの観点と修正の温度感をこのように交えた Prefix を付けて、コメントをします。 指摘例 MUST(Design): ドメイン ロジックが Controller クラスに実装されてます。 domain 層の対象 package に新しくクラスを作成して実装を移してください。 このとき Prefix の入力を手入力にしてしまうと、入力の手間や入力がミスが生じることもあるので、カスタム script で入力をサポートするようにしています。 最後に 以上のように、私のチームではコードレビュー ガイドライン を作成してルールを明文化することで、技術力を 見える化 させて課題改善を進めています。レビューコメントをこのように分析することで、個人の弱点に合わせたアプローチ方法も見えてきます。このように技術力に対するアプローチとして PDCA サイクルを回すことで、チームメンバーの技術育成を進めております。 最後に簡単にまとめると、コードレビュー ガイドライン を明文化した場合、 指摘数に応じて技術力を 見える化 できる コードレビューで オブジェクト指向 が学べる スキルアップ のためのアクションプランが検討しやすい といった恩恵を受けることできるので、ぜひチームに合ったコードレビュー ガイドライン を作成してみてはいかがでしょうか。
アバター
はじめに はじめまして。インフラエンジニアの rkyohei です。 Linux サーバの運用やモニタリングにおいて、性能チューニングや トラブルシューティング にはさまざまコマンドを使用すると思います。その中でも、特にリソース使用状況を詳細に分析するために便利なツールの1つが「vmstat」となります。 vmstatコマンドの存在自体は知っていたけど、オプション、実行結果の見方についてあまり知らなかったのですが、先日業務で使用する機会があり、vmstatコマンドについて調べましたのでこのエンジニアブログでみなさんにご紹介したいと思います。 はじめに vmstatとは何か? vmstatコマンドの基本的な使い方 vmstatコマンド実行結果の見方 vmstatコマンドの実行例 1. vmstatコマンドのみでの実行例 2. 更新間隔、表示回数を含めた実行例 3. -sオプション(メモリ統計情報の表示)を使用した実行例 4. -dオプション(ディスクI/O統計情報の表示)を使用した実行例 最後に 参考文献 vmstatとは何か? vmstatとは Virtual Memory Statistics の略であり、 Linux システム上で 仮想メモリ の統計情報を表示するコマンドです。 vmstatコマンドの基本的な使い方 vmstatコマンドの基本的な使い方についてご紹介します。 vmstat [オプション] [間隔(sec) [回数] ] ※[ ] は省略可能です オプション: ここではvmstatコマンドのオプションについて一部ご紹介します。 オプション 説明 -a 仮想メモリ の詳細情報を表示します。プロセスのステート(実行中、スリープ中など)、ページング、メモリ情報などが含まれます。 -s 仮想メモリ の統計情報のみを表示します。各種メモリスタット、ページング、 スワップ 情報などが表示されます。 -d ブロックデ バイス のIO統計情報を表示します。IOのバイト数、リク エス ト数、転送時間などが表示されます。 -D ディスクの統計情報を1項目1行で表示します。 -p < パーティション > 指定した パーティション に関する情報を表示します。 パーティション を指定して詳細情報を取得することができます。 -S 単位 単位をk,K,m,Mで指定します。 -t タイムスタンプを表示します。 これらのオプションを使用することで、さまざまな情報を取得することができます。例えば、 仮想メモリ の詳細情報や統計情報、ブロックデ バイス のIO統計情報などを利用して、システムの性能やリソース使用状況をより詳細に分析できます。 上記以外にもオプションはありますので、オプションの詳細について興味がある方は、 man コマンドを使用してマニュアルページを参照していただければと思います。 更新間隔: デフォルトでは1秒ごとに情報が表示されますが、必要に応じて変更できます。 表示回数: 指定回数だけ情報を表示した後にコマンドが終了します。 vmstatコマンド実行結果の見方 vmstatコマンドの実行結果の見方について、以下にご紹介いたします。ここではvmstatコマンドをオプション無しで実行した結果を例としています。 # vmstat procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st  0  0 123088 154496      0 1168484    0    0     0     1    1    1  0  0 100  0  0 区分 値 意味 procs r 現在実行待ちのプロセス数。CPUが過負荷であるかどうかを示す指標です。 b スリープ中のプロセス数。スリープ中のプロセスはI/Oの完了を待っています。 memory swpd スワップ されたページの数。 スワップ の量を示す指標です。 free 使用されていないメモリの量。大きな値が望ましいです。 buff ファイルの読み取り結果としてキャッシュされているメモリ量。 cache ファイルシステム がキャッシュしているページの量。メモリ使用効率の指標。 swap si スワップ 領域からメモリにページが転送された回数。 so メモリから スワップ 領域にページが転送された回数。 io bi ブロックデ バイス から受け取ったブロック。(blocks/s) bo ブロックデ バイス に送られたブロック。(blocks/s) system in 1秒あたりの割り込みの数。ハードウェアの負荷を示す。 cs 1秒あたりの コンテキストスイッチ (プロセスの切り替え)の数。 cpu us ユーザープロセスが消費したCPU時間。 sy カーネル プロセスが消費したCPU時間。 id アイドル状態のCPU時間。高いほどCPUがアイドルであることを示します。 wa ディスクI/Oの待機時間。ディスクへのアクセスが遅い場合に増加します。 st 仮想マシン から盗まれた時間を示します。 vmstatコマンドの実行例 vmstatコマンドの実行例と結果についていくつかご紹介します。 1. vmstatコマンドのみでの実行例 vmstatコマンドをオプション無しで実行すると、1回のみ結果が表示されます。 # vmstat procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st  0  0 123088 154496      0 1168484    0    0     0     1    1    1  0  0 100  0  0 2. 更新間隔、表示回数を含めた実行例 以下のように実行することで更新間隔、表示回数を指定することができます。ここでは1秒間隔で5回実行されるように指定しています。 また  -t オプションを併せて使用することでタイムスタンプを結果に表示させることもできます。おそらく トラブルシューティング の際には問題となる事象が再発するまでコマンドを継続して実行する必要があり、cronで定期的に実行したり、引数を使用して実行状態のまま経過監視すると思います。 TeraTerm 等のログ保存設定にタイプスタンプを付与することも可能ですが、 -t オプションを使用しタイムスタンプを表示することで問題が発生した時間のログを探しやすくなります。 # vmstat 1 5 -t procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- -----timestamp-----  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st                 JST  0  0 123088 154320      0 1168484    0    0     0     1    1    1  0  0 100  0  0 2023-08-28 12:17:49  1  0 123088 154224      0 1168484    0    0     0     0  141  202  1  0 100  0  0 2023-08-28 12:17:50  0  0 123088 154224      0 1168484    0    0     0     0  150  218  0  0 100  0  0 2023-08-28 12:17:51  0  0 123088 154256      0 1168484    0    0     0     0  128  189  0  0 100  0  0 2023-08-28 12:17:52  0  0 123088 154256      0 1168484    0    0     0     0  139  192  0  1 100  0  0 2023-08-28 12:17:53 3. -s オプション(メモリ統計情報の表示)を使用した実行例 -s オプションを使用することでメモリの統計情報を1項目1行で表示することができます。 # vmstat -s 1728000 K total memory 405420 K used memory 621268 K active memory 539020 K inactive memory 153972 K free memory 0 K buffer memory 1168608 K swap cache 2097148 K total swap 123088 K used swap 1974060 K free swap 3091882 non-nice user cpu ticks 135539 nice user cpu ticks 1694588 system cpu ticks 2524043051 idle cpu ticks 59367 IO-wait cpu ticks 1517573 IRQ cpu ticks 1268497 softirq cpu ticks 487644 stolen cpu ticks 9009973 pages paged in 20842448 pages paged out 74510 pages swapped in 207951 pages swapped out 1668051957 interrupts 2180661177 CPU context switches 1680577615 boot time 393160 forks 4. -d オプション(ディスクI/O統計情報の表示)を使用した実行例 -d オプションを使用することでディスクI/Oの統計情報を表示することができます。ディスクの性能評価や トラブルシューティング に役立つ情報を確認することができます。 # vmstat -d disk- ------------reads------------ ------------writes----------- -----IO------ total merged sectors ms total merged sectors ms cur sec sda 238129 21641 18019946 506118 893700 503648 41682672 4461928 0 2138 sr0 9 0 3 2 0 0 0 0 0 0 dm-0 181053 0 17319481 448718 1182573 0 40014120 8384242 0 2099 dm-1 74608 0 600776 58836 207951 0 1663608 8075290 0 42 ディスクI/Oの統計情報では3つの項目が表示されます。それぞれ読み取り(reads)と書き込み(writes)、実行中のI/Oです。各項目中のmsとsecは合計時間を示しています。 最後に vmstatコマンドは、 Linux サーバエンジニアにとって重要なモニタリングツールです。リソース使用状況の トラブルシューティング や最適化にとても有効だと考えます。 ただし、オプションや実行結果を理解し、覚えることはなかなか難しいと思います。 コマンドを実行する環境があるのであれば、実際にコマンドを実行することで、オプション、結果にふれてみるのも良いと思います。 参考文献 How to read Vmstat output
アバター
はじめに 昨今 書籍や各社Blog記事などでプロダクトマネージャー(以下PdM)の業務内容について記載された媒体が多数でている状況です。 ですが、複数の媒体を参照された方は、こう思われることが多いと考えております。 「見るものによって役割、業務内容違くないか?」 実際、企業・プロダクト・チームといった単位で、PdMの業務内容は変わっていると私も考えております。 弊社 ラク スにも、以下のようにさまざまなプロダクトがございますが、各プロダクトによってPdMの業務内容は異なっています。 その中でも今回は、 「楽楽精算」のPdM業務内容をご紹介します。 スコープ はじめに プロダクト体制 楽楽精算のPdM業務内容 事業KPI貢献に沿った優先順位 PRD(要求仕様書)作成 今後の展望 ラクスのPdMとして活躍してみませんか? プロダクト体制 さっそくPdMの業務内容を説明したいところですが、 まずは楽楽精算を提供・開発する上での体制(概略図)を説明させてください。 (その方が後続の理解がしやすくなるためです) 楽楽精算もARR100億を達成してきたこともあり組織として、大きくなっております。 その中でも特筆する部分は、開発と事業をつなぐ役割としてPdM/PMMを配置している部分です。 楽楽精算プロダクト体制概略図 楽楽精算のPdM業務内容 大きく以下の業務がございます。 事業KPI貢献に沿った優先順位で案件を推進すること 顧客解像度を高めた上で、PRD(要求仕様書)を作成、開発へ渡すこと 事業KPI貢献に沿った優先順位 基本的に、以下図のようにCSから見える「解約原因」営業から見える「失注原因」から財務効果を割り出し案件の優先順位を決めていきます。 会社戦略上 優先することが決定している案件、EOLなどの維持管理案件などはこれの限りではございません。 基本優先順位ロジック PRD(要求仕様書)作成 楽楽精算では、PRDに盛り込む要素は、以下画像のオレンジ部分と定義しており、調査対象に定めています。 (こちらは各PdMによって様々なご意見があるかと思いますが、一旦楽楽精算ではそうしています) 画像は プロダクトマネジメントのすべて から抜粋させていただいております。 (全PdMが読むべき良書と私は思っています) プロダクト4階層  出典:「 プロダクトマネジメント のすべて」p.52の記載を引用し、オレンジ枠は筆者追記 また、調査するための計画Agendaは、現状以下のように定めています。 (ブラッシュアップは続けています) 調査計画Agenda 最終的には、以下のAgendaの内容は最低でもPRDに盛り込むようにしています。 PRD Agenda(一部) 今後の展望 現在以下のようにDACIという フレームワーク を利用して、PdM/PMMの役割分担を決めています。 今後は、インタビュー等の収集業務、プロダクト指標の決定(共に現状0ではないのですが)にも踏み込んでいきたいと考えております。 DACI表(一部) ラク スのPdMとして活躍してみませんか? 今後の展望にも記載の通り、PdMとして役割を広げていきたいと考えております。 そのためにも楽楽精算PdMは、人材を募集しております。 是非カジュアル面談からお申し込みいただけると幸いです。 プロダクトマネージャー | エンジニア職種紹介 | 株式会社ラクス キャリア採用
アバター
はじめに こんにちは akihiyo76 です。Swift Concurrency が WWDC で発表されてから 2 年になりました。各プロダクトではサポートバージョンがアップデートされ、実際に導入が進み始めているプロダクトも多いのではないでしょうか。一方で新規で開発する場合は、前提となる技術だと考えています。弊社でも Swift Concurrency への移行対応を行いましたが、今回は実際に行った導入戦略を紹介したいと思います。 はじめに 導入するメリット 1. 並行処理を簡潔・安全に記述できる 2. データの競合やデッドロックを回避(品質向上) async / await Sendable Actor Task 既存プロジェクトへの導入 1. PoCコードの実装 Strict Concurrency Checking の設定 実装方針の決定 2. スコープ分割 3. 横展開・テスト まとめ 参考 導入するメリット では、動いている既存コードを修正して Concurrency を導入するメリットは何でしょうか。 実際に対応を進める場合、実装コストだけではなく、品質を担保するためのテストコストも必要になります。更にプロダクトによってはリリースコストが必要になるため、そのコストに合うメリットが要求されます。 そこで、実際に Concurrency を導入するメリットについて考えてみたいと思います。 1. 並行処理を簡潔・安全に記述できる まず、Concurrency のメリットとして「並行処理を簡潔・安全に記述できる」という点が挙げられます。 実際のコードを比較してみましょう。以下は従来の Block 構文で実装した通信周りのコード例です。 func fetchImageData (request : URLRequest , completionHandler : @escaping ( UIImage? ) -> () ) { self .session.dataTask(with : request ) { data, response, error in // Image をロードする self .loadImage(data : data ) { image in // Image サイズが適切かチェックする self .checkImage(image : image ) { completionHandler(image) } } }.resume() } この実装例では completionHandler() でコールバックを繋げる実装になっており、それぞれの処理がネスト構造を形成しています。これによりコードの複雑性が増し、分岐処理やエラー処理が追加されると更に実装が複雑になり、可読性も低下し、品質にも影響を及ぼす可能性があります。複雑性のために completionHandler() の記述を忘れた場合、特定の処理でコールバックが得られずアプリの処理が止まるリスクも考えられます。 それでは、この Block 構文で記述されたコードを Concurrency を使用して書き換えてみましょう。 func request (url : URL ) async throws -> UIImage? { let (data, response) = try await URLSession.shared.data(from : url , delegate : nil ) let image = try await loadImage(data : data ) let result = try await checkImageSize(image : image ) return result } async / await を使って書き換えることで、Block 構文の多段ネストが解消され、非同期の実装を簡潔に記述できるようになります。 ただし、コードスタイルや可読性の向上だけで Concurrency 移行のコストを検討するのは、割に合わないかもしれません。 2. データの競合や デッドロック を回避(品質向上) Concurrency 導入のメリットは、データ競合や デッドロック を回避できることです。具体的には Sendable や Actor といった機能の恩恵によるものですが、これについては後ほど具体的に説明します。Concurrency を導入し、これらの機能に準拠することで、品質の確保・改善に期待できる点が大きなメリットだと考えています。 async / await 関数の先頭に async(async throws) を定義することで、その関数を非同期関数として定義できます。定義した非同期関数を実行するためには、 await を使用する必要があります。 func execute () { Task.detached { do { let url = URL(string : "https://api.example.com" ) ! // 実行完了まで待機する let response = try await self .request(url : url ) // 後続処理 } catch { print(error.localizedDescription) } } } // 非同期関数として定義する func request (url : URL ) async throws -> HTTPURLResponse { // 通信処理 return result } このように、 async / await を使って定義することで、非同期関数の定義と実行が可能になります。 Sendable Sendable はデータ競合が起こらないことを コンパイル 時に保証してくれる型で、データ競合を避けて安全に渡せるデータを表す概念として導入されました。Sendable プロトコル に準拠することによって、その型が Sendable であることが コンパイラ に伝えられます。 final class Valid : Sendable { // 定数定義 let name : String init (name : String ) { self .name = name } } final class Invalid : Sendable { // 変数定義 var name : String init (name : String ) { self .name = name } } Valid と Invalid は共に Sendable プロトコル に準拠していますが、name については定数と変数で定義しており、Invalid の name は公開されているため変更が可能であり、データ競合が生じる可能性があります。この状態で コンパイル すると、 Stored property 'name' of 'Sendable'-conforming class 'Invalid' is mutable という コンパイラ のデータ競合警告が発生します。 Actor Swift 5.5 から導入された Actor は Concurrency の一部として、データ競合を防ぐ型です。Actor により作成された インスタンス は、一つのデータへのアクセスが同時に行われないように制御されます。これを Actor isolated と呼び、複数のタスクがデータにアクセスする際にもデータ競合を防ぐことができます。 actor MainActor { // 変数定義 var number : Int = 1 } func increment () { let act = MainActor() Task.detached { // number を更新 await act.number = 100 } } このように Actor で生成された インスタンス の number を変更しようとすると、 Actor-isolated property 'number' cannot be mutated from a Sendable closure という コンパイル エラーが発生します。 Task Task は並行処理を実行する単位で、複数のタスクの構造化(Task Tree)も可能になります。以下のコードのように、 withTaskGroup を使用してタスクグループ(親タスク)を作成し、要素となる子タスクを追加・実行することができます。 func getUser () async -> UserInfo { // 親タスク await withTaskGroup(of : DataType.self ) { group in // 子タスク A group.addTask { let friends = await self .getFriends() return DataType.friends(friends) } // 子タスク B group.addTask { let notes = await self .getNotes() return DataType.notes(notes) } } return UserInfo(friends : friends , notes : notes ) } こうした async / await 、 Sendable 、 Actor 、 Task といった機能を使用することで、Swift Concurrency を活用した効果的な非同期処理を実現できるようになります。 既存プロジェクトへの導入 ここからは既存プロジェクトへの導入方法について紹介します。導入の主な流れとしては、以下のようになります。 PoCコードの実装(Concurrency 設定と実装方針の決定) スコープ分割 各スコープごとの対応 リグレッション テスト 大まかな対応方法としては、実装方針をFIXしてチーム全体で一気に対応していくという流れです。 1. PoCコードの実装 Strict Concurrency Checking の設定 導入に際しては、実装方針(Concurrency の記述方法)を決定する必要があります。まず最初にプロジェクトの設定として、 Strict Concurrency Checking の設定を行います。 設定には Minimal、Targeted、Complete の 3 種類がありますが、 Targeted に設定します。 Minimal Sendable の制約を、明示的に採用された場所にのみ強制し、コードが並行性を採用した場所では actor-isolated チェックを実行します Targeted Sendable の制約を強制し、コードが並行性を採用した場所( Sendable を明示的に採用した場所も含む) actor-isolated チェックを実行します Complete モジュール全体で Sendable の制約と actor-isolated の分離チェックを強制します Complete はモジュール全体に大きな影響を及ぼすため、導入時の制約としては厳しすぎると判断しました。 参考: https://developer.apple.com/documentation/xcode/build-settings-reference#Strict-Concurrency-Checking 実装方針の決定 Strict Concurrency Checking の設定後は、基本的な Concurrency の実装パターンを作成します。 通信実装などのコアな部分が多く対象になると考えられるため、同じ責務の Concurrency 関数を準備します。最終的には既存の実装を削除するため、 @available で deprecated を指定しておきます。以下は withCheckedThrowingContinuation を使用して Concurrency に変換していますが、async / await を使用しても同様に変換可能です。 @available ( * , deprecated , message: "This function is deprecated. Use the requestAsync () instead." ) func request (request : URLRequest , completionHandler : @escaping ( Data? ) -> () ) { self .session.dataTask(with : request ) { data, response, error in completionHandler(data) }.resume() } func requestAsync (request : URLRequest ) async throws -> Data? { return try await withCheckedThrowingContinuation { continuation in self .session.dataTask(with : request ) { data, response, error in if let error = error { continuation.resume(throwing : self.handleNSURLError (error : error as NSError )) return } continuation.resume(returning : data ) }.resume() } } また、requestAsync() の呼び出し元クラスも変更します。以下の fetchUserData() は View 側から実行される想定なので、 @MainActor を付けて Main Thread から実行できるようにし、また nonisolated を指定して非分離メソッドとして指定します。 @MainActor class ViewModel { private let apiService = ApiService() nonisolated func fetchUserData () async throws -> Data? { do { let urlRequest = await self .createUrlRequest() return try await apiService.requestAsync(request : urlRequest ) } catch { throw await self .handleError(error) } } } 最後に fetchUserData() の呼び出しを Task 内で行います。Task の定義は、プロジェクトの アーキテクチャ に合わせて行うのが良いでしょう。 override func viewDidLoad () { super .viewDidLoad() Task { do { let userData = try await viewModel.fetchUserData() self .updateUser(userData) } catch { // エラー処理 } } } } 2. スコープ分割 導入の基本方針が確定したら、具体的な進行方法を検討します。大規模なプロジェクトでは修正が多岐にわたることが予想されますが、一度に行う変更が大きいと、レビューや品質管理が難しくなる可能性があります。そのため、一定の粒度で分割し、対応を進めることで迅速な対処が可能です。チーム全体で進行する場合、各メンバーが実装とレビューを同時に行うことが望ましいでしょう。もし修正すべき箇所が多い場合は、 スプレッドシート で一覧化しておき、ファイルごとやクラスごとなど明確な区分け方法にすることで、作業の分担が容易になります。 3. 横展開・テスト 修正すべきスコープが確定したら、あとは一気に修正を進めます。チームの状況によっては、徐々に移行する方法ありますが、他の機能の実装も進行中の場合、コンフリクトが発生する可能性があるため、一気に対応する方法をオススメします。最後に、@available deprecated を指定した関数を削除することを忘れずに行いましょう。導入実装が完了したら、 リグレッション テストを実施して機能 デグレ がないことを確認します。 まとめ Swift Concurrency の主要な機能に焦点を当てながら、既存のプロジェクトにおける導入戦略を紹介しました。今回紹介した導入戦略は、私自身が実践した方法ですが、既存の実装を保ちながら進行したため、多少冗長な箇所があるかもしれません。これはあくまで一つの戦略として、Swift Concurrency の導入を支援するための手助けとなることを願っております。 参考 developer.apple.com
アバター
こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの( @taclose )です☆ 弊社では先日 テスト駆動開発 (以降、TDDと呼ぶ)ハンズオン勉強会を開催しました! 今回の記事の内容はズバリ2つ 誤解してる!? テスト駆動開発 の良さ!学ぶ事の意味! TDDハンズオン勉強会を開催する意図や実施内容、感想! 読者のターゲットは TDDを誤解している人 TDDハンズオン勉強会を弊社でもやろう!とか思ってる人 を想定していますっ。 誤解されがちなTDD、記事にするには書ききれないTDD...なるべく小難しい内容は省いて興味を持ってもらうための記事を書いてみようと思います! テスト駆動開発(TDD)は良い物だ! テスト駆動開発(TDD)とは何か? TDDに対する誤解 TDDハンズオンについて TDDハンズオンの趣旨 TDDハンズオンの計画 事前準備 スケジュールと概要 TDDハンズオンの感想(反省点や振り返り) 動画は事前に見てもらうべきだった TODOリストの作成にもう少しゆとりを持つと良い まとめ 最後に 参考文献等 テスト駆動開発 (TDD)は良い物だ! テスト駆動開発 (TDD)とは何か? プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き、そのテストが 動作する必要最低限な実装をとりあえず行なった後、コードを洗練させる、という短い工程を繰り返すス タイルである (Wikipediaから抜粋) よくTDDについて調べてみると、だいたい上のような説明に始まり、Red/Green/RefactorのサイクルがTDDであると学んで終わってしまう。でも、この記事に辿り着く読者は「そんな事はわかっている」という人が大半だと思うので、あえてここではこう説明しておきます。 「動作するきれいなコード」。Ron Jeffriesのこの簡潔な言葉が、テスト駆動開発(TDD)のゴールだ。 動作するきれいなコードはあらゆる意味で価値がある。 (「テスト駆動開発」著 Kent Beck 訳 和田卓人のまえがきから抜粋) TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング 2020年にあった和田さんによる基調講演も「TDDとは何か?」という話はここから始まります。是非この動画は見て頂きたいのですが、抜粋して言うと、 「動作するきれいなコードを書く事は、品質・コスト・開発速度どの側面から見ても重要な要素であり、それを実現するのにTDDは1つの解である」 といった内容に感じました。 このゴールから始まる「まえがき」の書き方も、よくよく考えるとTDDっぽくて感慨深いですね!グレイトっ(笑) では、「そんなにTDDが良いのならなぜもっと普及しないのか?」少しだけTDDに対する誤解について話しておこうと思います。 TDDに対する誤解 テストコードを書く事が一般的になった昨今ですが、そんな中で 「TDDは先にテストコードを書く事」という偏った認知 が広がっていったようです。これは誤った認識だという事は声を大にしてお伝えしたいです。 TDDが テストファースト である事には複数の意味がありますが、私が一番しっくりきた解釈としては、 詳細設計と実装の狭間に「どんな風にクラスを分けて、どんなメソッドを定義してあげると良いだろうか?」というのをテストコードという形で箇条書きしている開発フロー だと感じました。 思いつきで書いた動作するコードは、得てしてテストコードが書きづらく、既存実装に対して書きやすさで実装が進んでしまいます。なので、先にどのクラスにどんなメソッドが存在していてほしいかをテストコードとして記載していく事で、本来あるべき実装・振る舞いを実装できる。 また、 「TDDはテストコードの開発に時間がかかる」 というのもよく聞く話ですが、先ほど紹介した動画の中でも最後に解説されていますが、テストの経済性というのは実際そんなに悪くはなく、少なからず担当者が離職する間隔よりも短いタイミングで元は取れるようです。 また、ここであえて「離職する間隔よりも短い」という言い回しをさせてもらったのは、実体験的に当事者意識を持てるか?というのが案外大事なのではないかと思ったから補足させてもらいました。内部品質の高いコードを書く事の見返りは自分にもある事を知っていてほしいです。 xUnit Test Patternsより抜粋 ※上の画像は、先ほど解説した動画の最後の方で出てくるグラフです。 無駄にメンテナンスが困難なテストコードを量産しているとメンテナンスコストが高くなる事もあるので注意です。 ※本来の テスト駆動開発 をやればこうはなりませんよ!深く考えずテストを後から書くような開発してるとこうなっちゃう現実が待ってるという注意喚起です。 TDDハンズオンについて ある意味、ここからが本題ですねっ! TDDハンズオンの趣旨 「TDDの良さを知ってもらって、普段の開発に活かしてほしい。」という事で開催されました。 先にも記載した通り、TDDというのは名前 からし ても誤解されがちな傾向にあり、本当の良さって伝わりにくいものです。 実際に手を動かしてはじめて「あ、確かに開発の流れがしっくりくる!安心感がある!」そんなTDDだからこそハンズオンをやる意味があるのでは? と考えました。 TDDハンズオンの計画 事前準備 TDDを実施する開発環境として Java が動く開発環境が必要になりますが、手間であれば Codinggame.com のようなサイトでやってもらっても良いかと思います。 www.codingame.com ※弊社も一部の社員はこちらを使いました。普段 PHP で開発していて Java 環境がない社員等。 スケジュールと概要 計画 1日目: 弊社では1日目を TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング を一緒に視聴する時間に費やしました。ハンズオン本編以外の部分をスキップしたり、1.25倍速で再生して1時間半に無理矢理短縮しました^^; まずは基礎知識としてTDDの流れについてちゃんと理解してもらいたかったためです。 2日目: うるう年問題は以下としました。 入力した整数がうるう年かどうかを判定するプログラムを書け。 うるう年のときは「true」、それ以外は「false」とプリントすること。 うるう年の定義 ・西暦年号が4で割り切れる年をうるう年とする ・ただし、西暦年号が100で割り切れ、かつ400で割り切れない年は平年とする。 私の所感としては、1時間半でやるハンズオンであれば、丁度いい難易度かなと感じました。 タイムスケジュールとしては以下としました。 うるう年についてのTODOリストの整理(10分) 2チームからTODOリストを発表(6分) ※これを参考に後半タスクを進めてもらう テストコードのサイクル開始(Red/Green/Refactor)(30分) 2チームからTDDの結果を発表(6分) バッファ(20分) TDDハンズオンの感想(反省点や振り返り) 動画は事前に見てもらうべきだった 参加者から頂いたコメントにも 「開催回数が増えてもよいのでビデオは早回し再生ではなく通常再生が良かったです。」 といったご意見がありました。 確かに1.25倍速で抜粋して見てもらったのは苦肉の策で、計画段階でも悩んだポイントでした。 「参加者は事前にこれを見て来てください(倍速で見るとかはお任せ)」とした上で、重要ポイントだけピックアップして解説してから始めるぐらいにした方が、円滑に進んだかも! TODOリストの作成にもう少しゆとりを持つと良い 動画を見てもらうとわかるかとは思いますが、TODOリストとは「どんな振る舞いをしてほしいのかを箇条書きにした仕様書のようなもの」です。 TODO作成時間に時間を割いても良かったように感じました。 TODOがしっかりできていれば後はそれ通りに実装するだけなので、 割合的にはTODO作成時間が多くなるイメージだと個人的にはもう少しうまくできたのかなと思いました。 上記のような意見も後日アンケートで頂きました。 確かに10分なんかじゃ全然足りなかった!初回のTODOリスト作成は20分ぐらいはせめてあるべき、30分でも良いと感じました。 3分クッキングみたいに「出来上がったTODOリストはこちらです」って提示するのは一案かもしれないんですが、ハンズオンの意味が色褪せちゃいますよねOrz せっかくのハンズオンなのでもう少し時間にゆとりがあれば良かったです!反省! まとめ 反省すべき点はありますが、30名程参加頂き、総じてTDDハンズオンの実施で新しい学びがあったという意見は多く、有意義な時間だったなと思います! TDDハンズオン風景(大阪) TDDハンズオン風景(東京) エンジニアは万能ではないので(少なからず私は)ミスする事は多々あります。TDDはそういった普通の人でも、正しい実装を安心して開発していける手法です。 是非、ブログ記事1つで理解したつもりにならず、本を読んだり、TDDハンズオンを実際に体感して、一考してもらえればなと思います。 最後に 和田さんに感謝!TDDブートキャンプの基調講演の動画とても参考になりました。 和田さんの前で「ちゃんとテスト書いてます!」って言えるようにがんばるます! 参考文献等 「テスト駆動開発」著 Kent Beck 訳 和田卓人 TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング xUnit Test Patternsから学ぶユニットテストの6つの目指すべきゴール Codinggame.com
アバター
こんにちはサッカー大好き@neroblubrosです。 ラク スでは月に一度ですが、定時後にビアバッシュを行っています。 開発部隊は東京オフィスと大阪オフィスにいますが、それぞれでビアバッシュを開催しています。 今回は大阪オフィスで開催しているビアバッシュについて紹介いたします。 ビアバッシュとは 6月レポート 1人目「社会人1年目を振り返ろう」 2人目「社会人1年目の振り返り」 3人目「やらかし事例集」 4人目「PHPMDでコード品質を計測する」 5人目「RSSで手軽に更新通知」 6人目「ChatGPTでブログを書いてみた」 7人目「おもしろいDB設計本ミッケ!」 7月レポート テーマ枠は「新卒に伝えたいこと」 4人目「新卒の方々にお伝えしたいこと」 5人目「君たちはどう生きるか」 6人目「新卒に伝えたいことLT」 7人目「最年長のプロダクトに居座り続ける新卒入社のパイセンから伝えたい ”これだけはやっておけ”」。 8人目「新卒の皆さんに知ってほしい!チャットディーラー開発課」 9人目「PHPを支えるコミュニティたち」 次回の8月ビアバッシュの予告 ビアバッシュとは まずはここで軽くビアバッシュの説明をします。ビアバッシュとは、 シリコンバレー が発祥で ビア(beer):ビール バッシュ( bash ): どんちゃん 騒ぎ を組み合わせた造語で、ビールと軽食を食べつつアウトプットする場です。 ラク スの大阪オフィスでは2015年から始め、途中コロナ禍では開催できなかったときやオンラインで開催していましたが、毎月継続して開催しています。 コロナが5種になったことから今年度からアルコール+オフラインでの開催が再開しました。 軽食とアルコールとソフトドリンクが振る舞われますが、それらの費用は会社持ちです! 出席は自由で発表や出席をすると人事評価につながります。 大阪オフィスでは各課の代表者がビアバッシュ推進委員を構成してビアバッシュの運営しています。 以下、 ラク ス大阪オフィスのビアバッシュの内容です。 みんなで語り合うのではなく、希望者が発表をして聞く、質問あり。 発表内容はテーマ枠と自由枠がある。 テーマ枠は新入社員の自己紹介、トラブル事例など 自由枠はITや仕事に関することならなんでもOK。仕事に関係のない趣味の発表とかはNG 発表時間はテーマ枠と自由枠ともに5分~10分 ここから大阪オフィスで6月と7月に開催したビアバッシュの内容をざっと紹介します。 6月レポート 軽食はピザ+ナゲット+ポテトフライ。 最初の3名はテーマ枠の「社会人1年目の振り返り」です 1人目「社会人1年目を振り返ろう」 1人目は「社会人1年目を振り返ろう」です。 コロナ禍に入社した新卒3年目の発表で、新卒研修を終えて、配属部署での研修の話でした。 緊急事態宣言下に研修をすることの苦労話ややらかしたこと、逆に良かったことの発表でした。 2人目「社会人1年目の振り返り」 2人目は「社会人1年目の振り返り」で、前職で社会人1年目に経験したことの発表です。 スマホ アプリの機能追加で最新版をインストールすると起動できなくなり、リリース直後にユーザからのクレームが殺到したなど、いくつかの苦い経験を暗くなりすぎないように、面白おかしく発表してくれました。 個人的には 東日本大震災 の 計画停電 で、フロアの照明が定時で消灯されるため、デスクライトをつけて残業したことが印象に残りました。 3人目「やらかし事例集」 3人目は「やらかし事例集」で、2人目と同じような内容ですが、タイトル通り「自分がやらかしたこと」の発表です。 検証環境のつもりが接続先を間違えて本番環境を変更した 本番環境にテストデータを登録してしまった など、やらかし事例集をクイズ形式での発表でした。 不謹慎ですが、他人の不幸は蜜の味ということで、一番盛り上がった発表となりました。 次からは自由枠です。 4人目「PHPMDでコード品質を計測する」 4人目は「PHPMDでコード品質を計測する」です。 ソースコード の分岐や繰り返し処理がどれくらいあるかを表した数値が循環的複雑度といい、 オープンソース の静的解析ツールで配配メールの循環的複雑度を計測した結果の発表でした。 5人目「 RSS で手軽に更新通知」 5人目は「 RSS で手軽に更新通知」。 RSS を題材にシステム間連携で扱い易いフォーマットを考える発表でした。 6人目「ChatGPTでブログを書いてみた」 6人目は「ChatGPTでブログを書いてみた」で私の発表です。 ChatGPTを使った ライフハック に興味を持っていて、ChatGPTを使ってブログを書く方法を発表しました。 ブログの記事をChatGPTに生成させるにはどのようなプロンプトを作成すればいいかを、実際に使ったプロンプトと完成したブログを用いて説明しました。 7人目「おもしろいDB設計本ミッケ!」 7人目は「おもしろいDB設計本ミッケ!」でERDの本の紹介で、大阪オフィスのテッ クリード による発表です。 単なる本の紹介ではなく、本に書かれている方法で実際に領収書をつかってテーブル設計を行うという、さすがテッ クリード という発表で、参加者はうなずきながら聞いていました。 6月はテーマ枠が「社会人を1年目の振り返り」でしたが、自由枠も実体験に基づいたリアリティに溢れた発表内容でした。 7月レポート 軽食はサンドウィッチ(3種類から選択)+ポテチ。 テーマ枠は「新卒に伝えたいこと」 7月から新卒で入社した新人が配属されたため、新卒の自己紹介とテーマ枠が「新卒に伝えたいこと」でした。 最初は新卒で入社した3名の自己紹介で、それぞれ工夫をこらした発表でした。 以降4人目からの発表を紹介します。 4人目「新卒の方々にお伝えしたいこと」 4人目は「新卒の方々にお伝えしたいこと」で私の発表です。 現在は退任しましたが、取締役のメールを紹介しました。 内容は紹介できませんが、 ラク スの文化を象徴するメールの内容だったので新卒に皆さんに紹介しました。 5人目「 君たちはどう生きるか 」 5人目は「 君たちはどう生きるか 」というタイトルでちょっと身構えてしまいそうですが、自分を守る仕事の仕方の紹介です。 ブラックな環境で働いてきた経験から ラク スがいかに恵まれた環境か、そしてその環境を生かして「楽しく健やかなエンジニアライフ」を過ごす方法の説明で、 ラク ス愛あふれた発表でした。 私も経験がありますが、修羅場をくぐり抜けてきた人の「人生は長い。心身の健康が一番大事。」という言葉に重みがあります。 6人目「新卒に伝えたいことLT」 6人目は「新卒に伝えたいことLT」で「みなさんはプロですか?」という問いかけから仕事に対する姿勢を新卒入社に説明。 「プロの仕事とは?」という問いかけから「プロフェッショナル」な仕事の仕方についての発表でした。 7人目「最年長のプロダクトに居座り続ける新卒入社のパイセンから伝えたい ”これだけはやっておけ”」。 自己研鑽をやっておけという当たり前のような内容からなぜ必要なのか?という説明です。 メールディーラー開発の実装チームのリーダの発表で、説得力があり大阪オフィスのテッ クリード から「すごくいい発表だった」と称賛のことばがもらえました。 8人目「新卒の皆さんに知ってほしい!チャットディーラー開発課」 8人目は「新卒の皆さんに知ってほしい!チャットディーラー開発課」で、チャットディーラー開発課から異動になり、チャットディーラー開発課のいいところの紹介です。 タイトルからもわかるようにチャットディーラー愛にあふれる発表でした。 9人目「 PHP を支えるコミュニティたち」 9人目は「 PHP を支えるコミュニティたち」ということで、タイトル通り PHP の歴史とそのコミュニティの紹介です 7月の発表テーマは「新卒に伝えたいこと」だったので、仕事に対することや 自己啓発 的な発表が多かったです。 次回の8月ビアバッシュの予告 大阪オフィスの8月のビアバッシュのテーマは「納涼! ヒヤリハット 特集」です。 暑い夏を吹き飛ばすようなヒヤリとしたエピソードを共有することで、同じようなミスをしないようにする内容ですね。 次回のビアバッシュレポートもお楽しみにしててください!
アバター
はじめに こんにちは akihiyo76 です。先日 Android 14 Beta 5 がリリースされ、最終リリースまであと僅かとなりました。そこで、今回は Android 14 で提供される新機能の概要をまとめてみました。 はじめに 機能と API の概要 国際化(Internationalization) アプリ固有の言語設定 Grammatical Inflection API grammatical gender の設定方法 ユーザー補正(Accessibility) ユーザー体験(User experience) 共有シートのカスタムアクションの追加 アプリストアの改善 スクリーンショットの検知 予測型「戻る」アプリ内アニメーション まとめ 参考 機能と API の概要 Android 14 の「新機能と API の概要」は、 公式ページ に記載されており、日本語の翻訳も徐々に進んでおりますが、ニュアンスが微妙なところもあるので、可能であれば 英語版 で読むとより理解が進むかと思います。 Android 14 の新機能を大別すると、以下の 3 つに分類されるので、それぞれを順に紹介したいと思います。 国際化(Internationalization) ユーザー補正(Accessibility) ユーザー体験(User experience) API の追加、変更、削除の一覧については、 API 差分レポート でご確認ください。また、新しい API についての詳細は、 Android API リファレンス でご確認ください。 国際化(Internationalization) 国際化の新機能としては、 アプリ固有の言語設定の自動化 と Grammatical Inflection API(文法的な語形変化) が追加になります。 アプリ固有の言語設定 Android 13( API レベル 33)で導入されたアプリ別の言語機能が拡張され、以下の機能が追加されます。 アプリの localeConfig の自動生成 アプリの localeConfig の動的アップデート インプット メソッド エディタ( IME )によるアプリの言語の確認 Android 14では、locales_config. xml の生成を以下の設定を追加することで、自動生成にすることが可能になりました。自動生成には、 Android Studio Giraffe Canary 7 および AGP 8.1.0-alpha07 以降が必要となっています。 1. androidResources > generateLocaleConfig を定義する。 gradle:build.gradle.kts android { androidResources { generateLocaleConfig true } } 2. resources.properties を作成し、デフォルトの言語を設定する。 res/resources.properties unqualifiedResLocale=en-US このように設定するだけで、locales_config. xml の生成を自動生成にすることができます。 Grammatical Inflection API Android 14 では、性別で文法が変わる言語に合わせてユーザー中心の UI を構築するため、アプリを リファクタリング せずに文法上の性別への対応を追加できる Grammatical Inflection API が導入されています。 日本語の場合、文法的な語形変化はなかなかイメージしにくいと思いますが、フランス語での一例を紹介します。 🇫🇷 冠詞の性別 ( 英語 : the ) 男性形単数 : le 女性形単数 : la 🇫🇷 動詞の過去分詞 ( 英語 : spoken ) 男性形単数 : parlé 女性形単数 : parlée Grammatical Inflection API では、このような文法的言語変化に対応することができます。 grammatical gender の設定方法 Grammatical Inflection API で設定できる文法上の性別の設定は以下の 3 パターンになります。 Configration. java public static final int GRAMMATICAL_GENDER_NEUTRAL = 1; // 中性的 public static final int GRAMMATICAL_GENDER_FEMININE = 2; // 女性的 public static final int GRAMMATICAL_GENDER_MASCULINE = 3; // 男性的 アプリで Grammatical Inflection API を利用するには、対象となる Activity に configChanges の指定します。 AndroidManifest. xml <activity android:name=".MainActivity" android:configChanges="grammaticalGender" android:exported="true"> </activity> 実際に grammatical gender を GRAMMATICAL_GENDER_FEMININE に設定する場合は、 setRequestedApplicationGrammaticalGender() API を利用して設定します。 val gIM = context.getSystemService(GrammaticalInflectionManager :: class .java) gIM.setRequestedApplicationGrammaticalGender(Configuration.GRAMMATICAL_GENDER_FEMININE) また、設定された grammatical gender を取得するには、 getApplicationGrammaticalGender() API を利用します。 val gIM = context.getSystemService(GrammaticalInflectionManager :: class .java) val grammaticalGender = gIM.getApplicationGrammaticalGender() このように設定した grammatical gender によって、 -neuter -feminine -masculine の suffix を付けた resources ファイルでリソースを使い分けることができるようになります。 res/values-fr-neuter/strings. xml <resources> <string name="example_string">Abonnement à...activé</string> </resources> res/values-fr-feminine/strings. xml <resources> <string name="example_string">Vous êtes abonnée à...</string> </resources> values-fr-masculine/strings. xml <resources> <string name="example_string">Vous êtes abonné à...</string> </resources> このように Grammatical Inflection API では文法上の性別によって表現を変えることができるため、性別を扱うことが多い SNS アプリなどで活用できる場面もあるかと思いますが、ローカルでのリソース設定に止まるため使える場面は限定的になりそうです。 ユーザー補正(Accessibility) Android 14 では、 非線形 フォントスケーリングが 200% までサポートされます。SP 指定の場合、追加のオプションとスケーリングの改善がアプリのテキストに自動的に適用されるようになります。 (引用: The first developer preview of Android 14 ) このようにスタンダードスケーリングだと表示崩れが生じるため、 200%のフォントサイズで UI テストで確認すること が重要となります。 200% のフォントサイズを有効にする手順は以下の通りです。 設定アプリを開き、[ユーザー補助] > [表示サイズとテキスト] に移動] [フォントサイズ] オプションで、最大フォントサイズの設定が有効になるまで、プラス(+)アイコンをタップ また、アプリで sp 単位を使用している場合、 Android はユーザーが選択するテキストサイズを適用して適切にスケーリングできるよう、テキストサイズは必ず sp 単位 で指定しましょう。 ユーザー体験(User experience) ユーザー体験では、以下の機能が追加になりました。 共有シートのカスタムアクションの追加 アプリストアの改善 スクリーンショット の検知 予測型「戻る」アプリ内アニメーション 共有シートのカスタムアクションの追加 共有シートのカスタムアクションを追加するには、カスタム ChooserAction から ChooserActions のリストを作成します。 MainActivity.kt fun buildCustomShareSheetActions(context: Context): Array<ChooserAction> { val pendingIntent = PendingIntent.getActivity( context, 0, Intent(Intent.ACTION_WEB_SEARCH).apply { putExtra(SearchManager.QUERY, "Search on browser") }, PendingIntent.FLAG_IMMUTABLE ) val actions = mutableListOf<ChooserAction>() for (i in 0 until 5) { val customAction = ChooserAction.Builder( Icon.createWithResource(context, R.drawable.ic_launcher_foreground), "Action${i+1}", pendingIntent ).build() actions.add(customAction) } return actions.toTypedArray() } 次に、作成した ChooserActions のリストを Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS として指定し、共有シートを開きます。 MainActivity.kt fun buildChooserIntent(chooserActions: Array<ChooserAction>): Intent { val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, "dummy text") } val chooserIntent = Intent.createChooser(intent, "Android 14").apply { putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, chooserActions) } return chooserIntent } fun showActionSheet() { val chooserActions = buildCustomShareSheetActions(this) val chooserIntent = buildChooserIntent(chooserActions) startActivity(chooserIntent) } このように複数のカスタムアクションを設定することができます。 アプリストアの改善 Android 14では、ユーザの操作を中断せずにアプリのアップデートが可能になります。具体的には、 以下の操作を API で実現できるようになります。 ダウンロードする前にインストールの承認をリク エス ト 今後の更新に責任を移譲 影響が少ないタイミングでアプリを更新 アプリインストール操作の事前承認では、targetSdk 34 で PackageInstaller が追加され、 requestUserPreapproval() を実行することで、事前承認を行うことができます。 アプリストアの改善機能は、スムーズなアプリインストールを促すことが期待できるため、ユーザーにとっても嬉しい機能になるでしょう。 スクリーンショット の検知 API level 17 以降では、 FLAG_SECURE を設定することで スクリーンショット を無効にすることができましたが、 Android 14 では スクリーンショット の検知ができるようになります。 スクリーショットの検知をするには、 DETECT_SCREEN_CAPTURE の permission を定義します。DETECT_SCREEN_CAPTURE の Protection level は normal です。 AndroidManifest. xml <uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" /> 検知用の Callback は ScreenCaptureCallback を作成して、ライフサイクルに合わせて登録・登録解除を行います。 MainActivity.kt val screenCaptureCallback = ScreenCaptureCallback { // Add logic to take action in your app. } override fun onStart() { super.onStart() registerScreenCaptureCallback(mainExecutor, screenCaptureCallback) } override fun onStop() { super.onStop() unregisterScreenCaptureCallback(screenCaptureCallback) } このように Android 14 では スクリーンショット の検知が非常に簡単に実現できます。アプリ内で スクリーンショット の検知したら、安易に共有させないような注意喚起を行うケースなどで有用かと思います。 予測型「戻る」アプリ内アニメーション バックアクションに対して、事前検知することでカスタムアニメーションなどの処理を追加することが可能になりました。 @RequiresApi(34) で指定している Implement Menbers が Android 14 で追加になります。 MainFragment.kt val callback = object: OnBackPressedCallback(enabled = false) { @RequiresApi(34) override fun handleOnBackStarted(backEvent: BackEvent) { // Create the transition } @RequiresApi(34) override fun handleOnBackProgressed(backEvent: BackEvent) { // Play the transition as the user swipes back } override fun handleOnBackPressed() { // Finish playing the transition when the user commits back } @RequiresApi(34) override fun handleOnBackCancelled() { // If the user cancels the back gesture, reset the state } } this.requireActivity().onBackPressedDispatcher.addCallback(callback) 実際にアニメーションを追加実装サンプルは 公式ページ にあるので、実際の実装例を見てみたい方はそちらをご覧ください。 まとめ Android 14 の新機能概要を紹介させて頂きました。アプリによっては有用な機能もあると思うので、targetSdk 34 対応時に合わせて、アプリの新機能として検討してみてはいかがでしょうか。 参考 Features and APIs Overview Android Developers Blog
アバター
はじめに はじめまして、新卒1年目のTKDSです! 先日、Spring Bootの入力値チェックについて触れる機会があったため、入力値チェックの使い方について調べてました。 今回は、調べた内容と簡単な使いかたについてご紹介したいと思います。 はじめに 入力値チェック アノテーションの実践 ネストされたformの入力値チェック まとめ 参考文献 入力値チェック Spring Bootでは、入力値の検証を行うための便利な機能が提供されています。 これにより、入力データがアプリケーションの要件を満たしているかどうかを確認できます。 入力値チェックはフォームクラスにチェックする内容を示す アノテーション をつけると行えます。 フォームクラスにつける アノテーション にはさまざまなものがあります。 一部を記載します。 アノテーション チェック内容 @NotNull Nullでないか @Max() 最大値以下か @Min 最小値以上か @Pattern 正規表現 に一致するか @Size 要 素数 が最小以上かつ最大以下であるか アノテーション の実践 サンプルアプリを用いて、実際に アノテーション を行ってみます。 検証用にPOSTリク エス トをおくると json を返すアプリを定義します。 まだこのサンプルアプリでは入力値チェックは行われていません。 コントローラー // 一部抜粋 @PostMapping ( "todo-list/{id}" ) public ResponseEntity<Object> post( @PathVariable Integer id, @RequestBody Todo todo,) { List<Todo> todoList = new ArrayList<>(); todo.setId( 1L ); todo.setUsrName( "test001" ); todoList.add(todo); return ResponseEntity.status(HttpStatus.OK).body(todoList); } レスポンスボディを受け取る兼返信用クラス public class Todo { private Long id; private String usrName; private String taskName; @JsonSerialize (using = LocalDateTimeSerializer. class ) @JsonDeserialize (using = LocalDateTimeDeserializer. class ) @JsonFormat (pattern = "yyyy-MM-dd'T'HH:mm:ss" ) private LocalDateTime date; // Getter, Setter, コンストラクタ省略 } このサンプルアプリはタスク名と時刻を JSON に含めてリク エス トを送信するとTodo型のオブジェクトに当てはめて、 JSON 形式で返却します。 リク エス トの送信には、 VS Code 拡張のREST Clientを使用しています。 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " taskName " : " 読書 " , " date " : " 2023-08-02T22:20:00 " } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 02 Aug 2023 15:20:54 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : " 読書 " , " date " : " 2023-08-02T22:20:00 " } ] 現状のコードではバリデーションチェックがなにも行われていないため、Nullなどの値を送ってもそのままエラーチェックをすり抜けます。 例として、taskNameをリク エス トボディに含めずリク エス トを送信します。 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " date " : " 2023-08-02T22:20:00 " } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 02 Aug 2023 15:11:12 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : null, " date " : " 2023-08-02T22:20:00 " } ] リク エス トに含まれていない "taskName" が null になっているのが確認できます。 では、入力値のバリデーションチェックを行い、taskNameが指定されていない場合にエラーを返すようにします。 TodoクラスのtaskNameに @NotNull アノテーション を付けます。 @NotNull private String taskName; これで、Null値を許容しない設定にすることができました。 次にエラーハンドリングができるようにします。 コントローラーを次のように変更します。 @PostMapping ( "todo-list/{id}" ) public ResponseEntity<Object> post( @PathVariable Integer id, @RequestBody @Validated Todo todo, BindingResult result) { if (result.hasErrors()) { return new ResponseEntity<Object>( "bad request" , HttpStatus.BAD_REQUEST); } List<Todo> todoList = new ArrayList<>(); todo.setId( 1L ); todo.setUsrName( "test001" ); todoList.add(todo); return ResponseEntity.status(HttpStatus.OK).body(todoList); } 引数の部分に アノテーション を追加し、エラーハンドリングの処理を追加します。 先ほどと同じ、taskNameがないリク エス トを送信します。 結果は次の通りです。 HTTP/ 1 . 1 400 Content-Type: text/plain; charset =UTF-8 Content-Length: 11 Date: Wed, 02 Aug 2023 15:33:01 GMT Connection: close bad request エラーハンドリングの部分で設定した bad request が返ってきたのが確認できました。 ネストされたformの入力値チェック 前項では、入力値を受け取るクラスのフィールド変数に対して簡単にバリデーションを行うことができることが確認できました。 しかし、フォームクラス内に自作クラスを使用してフィールド変数を持つ場合、従来の方法だとバリデーションチェックが正常に機能しないケースがあります。 具体的には、フォームクラスの変数自体は アノテーション でチェックできるものの、そのフィールド変数が自作クラスであり、かつ空の配列が入力された場合、入力値のチェックがうまく行われないことがあります。 実際に要素を追加して確認してみましょう。 public class Todo { private Long id; private String usrName; @NotNull private String taskName; @JsonSerialize (using = LocalDateTimeSerializer. class ) @JsonDeserialize (using = LocalDateTimeDeserializer. class ) @JsonFormat (pattern = "yyyy-MM-dd'T'HH:mm:ss" ) private LocalDateTime date; // 追加部分 private List<SubTask> subTasks; public static class SubTask { @NotNull private String taskName; // Getter Setter コンストラクタ省略 } // Getter Setter コンストラクタ省略 リク エス ト POST http://localhost:8000/todo-app/todo-list/ 1 Content-Type: application/json { " taskName " : "" , " date " : " 2023-08-02T22:20:00 " , " subTasks " : [ {} ] } レスポンス HTTP/ 1 . 1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Sun, 06 Aug 2023 15:34:33 GMT Connection: close [ { " id " : 1 , " usrName " : " test001 " , " taskName " : "" , " date " : " 2023-08-02T22:20:00 " , " subTasks " : [ { " taskName " : null } ] } ] subTaskのtaskNameが指定されていないにもかかわらず、バリデーションエラーが返ってきません。このことから、SubTask内のtaskNameに対して、 @NotNull は働いていないことがわかります。 この問題は、親クラスでフィールド変数を宣言する際に@Valid アノテーション を付与することで解決できます。 @Valid private List<SubTask> subTasks; これにより、オブジェクトのフィールド変数に対しても アノテーション チェックが行われるようになります。 リク エス ト POST http: //localhost:8000/todo-app/todo-list/1 Content-Type: application/json { "taskName" : "" , "date" : "2023-08-02T22:20:00" , "subTasks" : [ {} ] } レスポンス HTTP/ 1.1 400 Content-Type: text/plain;charset=UTF- 8 Content-Length: 11 Date : Wed, 02 Aug 2023 15 : 33 : 01 GMT Connection : close bad request バリデーションチェックが行われていることが確認できました。 まとめ この記事では、Spring bootにおける入力値のバリデーションについて、調べて結果を紹介しました。 また、サンプルアプリを用いて、入力値のバリデーションを実践しました。 Spring Bootのバリデーションの理解の一助になれば幸いです。 参考文献 https://access.redhat.com/documentation/ja-jp/red_hat_jboss_enterprise_application_platform/7.4-beta/html/development_guide/jakarta_bean_validation https://spring.pleiades.io/specifications/platform/10/apidocs/jakarta/validation/package-summary.html https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html https://spring.pleiades.io/specifications/platform/10/apidocs/jakarta/validation/constraints/package-summary.html
アバター
こんにちは。 ラク ス インフラチーム所属のas119119です。 今回は、タイトルの通りビジネスの場面で重要なスキルといわれている ロジカルシンキング 研修を開催した件について取り上げてみたいと思います。 今回のブログ構成については以下の通りとなります。 ロジカルシンキングとは 研修実施の背景 研修の内容 研修を実施してみて ロジカルシンキング とは 世間でよく聞く ロジカルシンキング とはそもそもどのようなものなのでしょうか。 日本語をそのまま当てはめると「論理的思考」となるようです。 しかし、「論理的思考」といってもあまりにも漠然としていますね。 少なくとも個人的にはそう思います。 巷にあふれる情報の中から論理的思考とは何かと調べてみると、 概ね次にあるような類似の定義にたどり着きます。 「論理的思考」とは、つまるところ直感や感覚的なところで物事を捉えるのではなく、筋道を立てて内容に矛盾や破綻がないように理屈が合うように考え、 結論を導くことという定義に集約できると思います。 研修実施の背景 部署内には新卒や中途、ベテランなど多種多様なメンバーがいる中で課題設定や課題解決能力にばらつきがあり、メンバー間の連携共有をよりスムーズに実施するためにはどうすればよいかという点に端を発しています。 そこでビジネスをする上で重要なスキルである ロジカルシンキング 研修をメンバー向けに実施することで、メンバー間のビジネススキルの底上げを図るとともに、チーム内部で共通のスキルを身に着けることで情報連携力の向上は図ろうとしたのが主催の背景となります。 その意味では、このようなインフラに関連していない試みでもチャレンジ(スモールスタートと言えるでしょう)できる土壌があるという点は ラク スの強みでもあると言えます。 研修の内容 さて、何を研修で扱ったかについて、ここでは取り上げていきたいと思います。 研修としては、業務時間も有効に活用しつつ、1回あたり90分を全3回、2週間から3週間おきに実施し、おおよそ2ヵ月以内で全内容を消化した形となります。 毎回、研修中にハンズオン形式の課題に取り組みつつ、各回の終了後、次回までに必須課題があるため、参加者にとっては通常業務等々をこなしつつ、なかなかヘビーな内容であったと思います。 イメージしやすいように、参考までに使用したテキストのサンプルをご紹介したいと思います。 ロジカルシンキング を検討する上で、様々なツールを使用すると便利であることがわかっているので、研修では一般的なビジネス概念である MECE やロジックツリーなどのツールを用いて、様々はハンズオンを実施しました。 今回は、ベテランから若手まで多様なメンバーが参加していたこともあり、ハンズオンにおける意見交換は活発に実施され、とりわけ若手にとっては色々な意見に触れるよい機会になったのではないかと思います。 研修を実施してみて 研修を ファシリテーター として実施するのも初めてで、研修を実施するための準備について、それなりに別のベテランから指導を受けたつもりでしたが、時間管理やハンズオンのコン トロール などはなかなかスムーズにいかないところも多々ありました。 しかし、全体を通しては実施する価値はあったと確信しています。 実際、参加者に対して毎回、研修の最後にアンケートを依頼していたのですが、概ね好評で、他のエンジニアにも受講を薦めるという意見が多く出ていました。 ある程度、 定量 化できる効果も出せたのではないかと感じています。 というのも、研修の成果物の一環として、参加者に「前後確認シート」と呼ばれるものを研修前と研修後に作成を依頼したところ、当研修で学んだことを活用して業務内における半年先までの目標設定に活用している成果が見受けられたためとなります。 今回、 ロジカルシンキング 研修を内製化するような形で実施したことを踏まえ、比較的習得したいスキルや知識はやりかた次第でできる環境が整備されていると思いました。 他方で新卒などの若手やベテランの分け隔てなく必要な研修を提供できた点は、個人的にも貴重な経験を積む機会となったのではと感じました。 ここまで目を通して頂き、ありがとうございました。
アバター