TECH PLAY

ニフティ株式会社

ニフティ株式会社 の技術ブログ

487

NIFTY Tech Talkは、ニフティ株式会社の社員が主催するトークイベントです。 本イベントでは、ニフティグループの社員が業務を通じて学んだことを発信しています! テーマ 第9回目のテーマは「SvelteKit, Next.jsの導入事例紹介など 〜ニフティのフロントエンドの今とこれから〜」です。 ニフティのフロントエンド開発に関わるメンバーから、業務で培われたノウハウや、今後のニフティのフロントエンド技術について事例紹介などLT形式で発表します。 概要 日程:02月21日(火)12:00〜13:00 配信方法:YouTube Live 視聴環境:インターネット接続が可能なPC/スマートフォン 参加方法 YouTube Liveにて配信いたします。 connpass にて登録をお願いいたします。 YouTube LiveのURLは決定後、connpass内の参加者への情報欄に記載いたします。 こんな方におすすめ SvelteKit, Next.js、自社制作、アクセシビリティに興味がある方 ニフティの制作現場、新規サービス開発の技術選定について興味がある方 テンプレートエンジンの移行に興味がある方 ニフティの技術や風土に興味がある方 タイムテーブル 時間 コンテンツ 12:00 – 12:05 オープニング+会社紹介 12:06 – 12:15 ニフティトップのNext.jsでのキャッシュ戦略を考えた話 (佐々木 優) 12:16 – 12:25 自社製CMSのテンプレートエンジンからNext.jsに置き換える (宮本 達矢) 12:26 – 12:35 ニフティのWEBサイト制作の流れ~社内制作のメリットデメリット (新田 万智) 12:36 – 12:45 新サービスにSvelteKitを導入してみた! (たけろいど) 12:46 – 12:55 社内にアクセシビリティを取り入れていきたいなぁと思っている話 (関 歩武) 12:56 – 13:00 まとめ+クロージング 登壇者プロフィール 筑木 信成 (ファシリテータ) 技術イベントの企画やキャリア採用を担当しています。 フロントエンドの知識を深めたく、社内で行われているフロントエンド勉強会に参加しています。 今回はファシリテーターとして参加します! 佐々木 優 (登壇者) @niftyトップページで主に開発・運用を担当しています。 最近はサービスのインフラコストを削減できないか、モダン技術を取り入れられないかを学びながら実践しています。 宮本 達矢 (登壇者) @niftyトップページの開発・運用担当。 フロントエンドに限らず、バックエンドやインフラも触っています。最近はデザイン周りを勉強中です。 新田 万智 (登壇者) 主にWEB制作ディレクターをしています。 最近はアプリのUI設計や動画コンテンツ企画~編集なども行っています。 デザインやフロントエンド周り、初心者スタートだったので今も絶賛勉強中です!よろしくお願いします! たけろいど (登壇者) ニフティのオプションサービスの開発・運用担当をしています。 ニフティのフロントエンド開発をさらに楽しくするために日々邁進しています! 関 歩武 (登壇者) ニフティポイントクラブの開発・運用担当をしています。 業務ではバックエンド系を行っていますが、フロントエンド技術やアクセシビリティに興味があり勉強中です! ニフティグループでは一緒に働く仲間を募集中です 新卒採用、キャリア採用を実施しています。ぜひ リクルートサイト をご覧ください。 ニフティエンジニアが業務で学んだことやイベント情報を エンジニアブログ にて発信しています! ニフティエンジニアのTwitterアカウントを作りました NIFTY Tech Talkのことや、ニフティのエンジニアの活動を発信していきます。 Tweets by NIFTYDevelopers 「NIFTY Tech Day 2022」を開催しました 技術イベント「NIFTY Tech Day 2022」のアーカイブはこちら NIFTY Tech Day 2022 アンチハラスメントポリシー 私たちは下記のような事柄に関わらずすべての参加者にとって安全で歓迎されるような場を作ることに努めます。 社会的あるいは法的な性、性自認、性表現(外見の性)、性指向 年齢、障がい、容姿、体格 人種、民族、宗教(無宗教を含む) 技術の選択 そして下記のようなハラスメント行為をいかなる形であっても決して許容しません。 不適切な画像、動画、録音の再生(性的な画像など) 発表や他のイベントに対する妨害行為 これらに限らない性的嫌がらせ 登壇者、主催スタッフもこのポリシーの対象となります。 ハラスメント行為をやめるように指示された場合、直ちに従うことが求められます。ルールを守らない参加者は、主催者の判断により、退場処分や今後のイベントに聴講者、登壇者、スタッフとして関わることを禁止します。 もしハラスメントを受けていると感じたり、他の誰かがハラスメントされていることに気がついた場合、または他に何かお困りのことがあれば、すぐにご連絡ください。 ※本文章はKotlinFest Code of Conductとして公開された文章( https://github.com/KotlinFest/KotlinFest2018/blob/master/CODE-OF-CONDUCT.md )を元に派生しています。 ※本文章はCreative Commons Zero ライセンス( https://creativecommons.org/publicdomain/zero/1.0/ ) で公開されています。
アバター
はじめに こんにちは、ニフティ株式会社 基幹システムグループの石坂です。普段の業務では課金系システムの開発運用をしています。 今回は、社内のサーバーを監視するためにPrometheusを触ってみましたので、共有したいと思います。 Prometheusとは PrometheusはOSSのリソース監視システムです。 監視対象に監視エージェント(Exporter)を入れることでHTTP経由でメトリクス収集を行います。Exporterには様々な種類がありますが、今回はログ監視に用いるgrok-exporterを使ってみます。 また、Prometheusには収集したデータをグラフ表示する機能がありますが、Prometheus単体だと機能が充実していません。そのため、データ可視化ツールであるGrafanaと組み合わせて使われることが多いようです。 実際にログ監視してみる Prometheusはdockerイメージが提供されているため、ローカルで簡単に試すことができます。今回はPrometheus、Grafana、監視対象としてCentOSのコンテナをdocker-composeを用いて起動します。 docker-compose.yml 監視対象のサーバーを「target-server」としています。 version: '3' services: prometheus: image: prom/prometheus:v2.41.0 container_name: prometheus volumes: - ./:/etc/prometheus/ - ./prometheus-data:/prometheus ports: - 9090:9090 network_mode: dockernet grafana: image: grafana/grafana:8.5.2 container_name: grafana volumes: - ./grafana-data:/var/lib/grafana ports: - 3000:3000 network_mode: dockernet target-server: image: centos:centos7 tty: true container_name: target-server ports: - 9144:9144 network_mode: dockernet prometheus.yml Prometheusの設定ファイルです。監視対象のサーバーをここに記載しておきます。 global: scrape_interval: 15s external_labels: monitor: 'codelab-monitor' scrape_configs: - job_name: 'prometheus-test' static_configs: - targets: ['target-server:9144'] それでは実際に起動していきます。 docker compose up -d 監視対象サーバーに入ってgrok_exporterをインストールします。 docker exec -it -u 0 target-server bash [root@9deda05f745d /]# yum install -y wget [root@9deda05f745d /]# cd usr/local/src/ [root@9deda05f745d src]# wget https://github.com/fstab/grok_exporter/releases/download/v1.0.0.RC5/grok_exporter-1.0.0.RC5.linux-amd64.zip [root@9deda05f745d src]# yum install -y unzip [root@9deda05f745d src]# unzip grok_exporter-1.0.0.RC5.linux-amd64.zip [root@9deda05f745d src]# mv grok_exporter-1.0.0.RC5.linux-amd64 ../grok_exporter [root@9deda05f745d src]# 以下が今回ログ監視したいサンプルログファイルとなります。 [root@9deda05f745d src]# cd ../grok_exporter [root@9deda05f745d grok_exporter]# head -5 example/exim-rejected-RCPT-examples.log 2016-04-18 09:33:27 H=(○○○.○○○.○○○.○○○) [○○○.○○○.○○○.○○○] F=<z2007tw@○○○.com.tw> rejected RCPT <alan.a168@○○○.net>: relay not permitted 2016-04-18 12:28:04 H=(○○○.○○○.○○○.○○○) [○○○.○○○.○○○.○○○] F=<z2007tw@○○○.com.tw> rejected RCPT <alan.a168@○○○.net>: relay not permitted 2016-04-18 19:16:30 H=(○○○.○○○.○○○.○○○) [○○○.○○○.○○○.○○○] F=<z2007tw@○○○.com.tw> rejected RCPT <alan.a168@○○○.net>: relay not permitted 2016-04-18 19:26:22 H=(○○○.○○○.○○○.○○○) [○○○.○○○.○○○.○○○] F=<z2007tw@○○○.com.tw> rejected RCPT <alan.a168@○○○.net>: relay not permitted 2016-04-26 04:41:25 H=(○○○.○○○.○○○.○○○) [○○○.○○○.○○○.○○○] F=<z2007tw@○○○.com.tw> rejected RCPT <alan.a168@○○○.net>: relay not permitted [root@9deda05f745d grok_exporter]# 以下の設定ファイルに監視設定を記載します。 metricsのtypeで監視対象ログから取得する値の種類を指定することができます。 サンプルでは、typeを「counter」として、エラーログの件数を取得する設定が記載されています。 typeには他にもgaugeやhistogram、summaryなどがあり、例えばgaugeを指定すると、一致する各ログ行で記録される数値を取得することができます。 [root@9deda05f745d grok_exporter]# cat example/config.yml global: config_version: 3 input: type: file path: ./example/exim-rejected-RCPT-examples.log readall: true # Read from the beginning of the file? False means we start at the end of the file and read only new lines. imports: - type: grok_patterns dir: ./patterns grok_patterns: - 'EXIM_MESSAGE [a-zA-Z ]*' metrics: - type: counter name: exim_rejected_rcpt_total help: Total number of rejected recipients, partitioned by error message. match: '%{EXIM_DATE} %{EXIM_REMOTE_HOST} F=<%{EMAILADDRESS}> rejected RCPT <%{EMAILADDRESS}>: %{EXIM_MESSAGE:message}' labels: error_message: '{{.message}}' logfile: '{{base .logfile}}' server: protocol: http port: 9144 [root@9deda05f745d grok_exporter]# 設定ファイルを指定してgrok_exporterを起動します。 [root@9deda05f745d grok_exporter]# /usr/local/grok_exporter/grok_exporter --config=/usr/local/grok_exporter/example/config.yml & Grafanaにログインします。(username: admin / password: admin) http://localhost:3000/login データソースとしてPrometheusを設定します。 設定→Configuration→Add data sourceから、Prometheusを選択して画像のようにURLを設定。Save&testを押下したら紐付け完了です。 あとはDashboardからPanelを追加して、可視化したいメトリクスを指定することでグラフを表示することができます。 以下の画像は「Unrouteable address」のサンプルエラーログが何件発生してるかを表示しています。 おわりに PrometheusやGrafanaは今も頻繁にアップデートされており、UIや仕様変更が多いため、参考にできるナレッジが少ない印象でした。 ですが、今回dockerで試してみたように動作確認することは比較的やりやすいので、興味のある方は一度触ってみると面白いかもしれません。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
はじめに こんにちは!新卒入社4年目の松下です。管理会計・データ分析ユニットで購買データやお客様の声のデータ分析をしています。 先日「NIFTY Tech Talk #8 ニフティのデータ分析」というイベントを開催しました。その様子をご紹介します。 イベント概要 NIFTY Tech Talk は、ニフティ株式会社の社員が主催するトークイベントです。 本イベントではニフティ社員が業務を通じて学んだことを発信しています。 今回は、第8回目となる「データ分析」に関するテーマで開催しました。データ分析には様々な視点がありますが、今回は3つの視点からニフティのデータ分析について話をしました。 ニフティのデータ基盤の話 データエンジニア視点 Tableau、TableauServerの社内の活用事例 データアナリスト視点 ChatGPTで賑わう自然言語処理技術、ニフティのデータサイエンスについて データサイエンティスト視点 今回の Tech Talk のアーカイブを YouTube にアップロードしております。ぜひご覧ください。 内容レポート 各セッションから一部抜粋して、どのような内容だったかご紹介したいと思います。 ニフティのデータ基盤の話 -データエンジニア視点から 会員システムグループのN1!データアーキテクトの黒羽さんが、実際に社内でデータ基盤を作った時の経験から、データ基盤はどのような考えで作っていけばよいのかを解説しました。 ここに行き着いた理由として最初は社内のデータを集めることを目的としてデータ基盤を作っていたのですが様々な不都合がでてきた、というところからでした。 終わらないデータ収集 あったら使う、便利かもというデータを集めるが使わない 活用は進まず、時間も溶ける そこで考えを新たにし、データ分析とセットでデータ基盤を考えるようにするとうまく回り始めました。 また、ニフティのデータ基盤の変遷も紹介しています。 今はdbtなどの周辺技術、ツール類が進歩していることでデータエンジニアを始める際に下駄が履きやすい(始めやすい)環境になっていることも説明いただきました。 Tableau、TableauServerの社内の活用事例 -データアナリスト視点から 事業推進グループ、N1!Data Ninjaの打矢さんより、ニフティではBIツールであるTableauをどのように使っているのかを解説いただきました。 ニフティではBIツールとしてTableauを利用していますが、Tableauを選定した理由として大きく3つあげています。 探索的な分析がしやすいこと 様々な種類のデータソースのデータを結合して使えること スケールに合わせた規模・コストで導入ができること Tableauを使うことで定期的に最新の状態にフォーカスされた状態で重要な数値を見ることができます。 また、探索がしやすい構造になっているためスピード感を持った対応を行うことができます。 そして、注力課題に対する指標の全社共有としてもTableauServerを使うことで簡単に共有を行うことができています。 ChatGPTで賑わう自然言語処理技術、ニフティのデータサイエンスについて -データサイエンティスト視点 最後に事業推進グループ、N1!データサイエンティストの瀬川さんからニフティのデータサイエンスについてどのようなことをやっているのか解説いただきました。 ニフティでもデータサイエンスの技術を用いておりニフティニュースでの活用事例やお客様アンケートの要望抽出に使用しています。 ニフティのデータ活用は「お客様理解」のために行っているというところをベースにしています。 また、データサイエンティストという立場で面白いところ、重要なとこをを説明していただきました。 まとめ 今回の Tech Talk では、ニフティのデータ分析について3つの視点から紹介しました。 1 時間という短い枠だったので、今回話しきれなかったものもありますが、参考にできるものがあれば幸いです。 今後も NIFTY Tech Talk は継続して実施していきますので、ぜひご参加ください。 またセッションの中でも紹介しましたが、この度2023年2月に開催予定のDevelopers Summit(デブサミ)にも中村が たった1人から始めて深層学習によるニュース要約をプロダクトに実装した方法~ファーストペンギンでやりきる力~ としてセッションを行う予定です。興味のある方はぜひ視聴をおねがいします! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
イベント概要 NIFTY Tech Talkは、ニフティ株式会社の社員が主催するトークイベントです。 本イベントでは、ニフティグループの社員が業務を通じて学んだことを発信しています! 2023年最初のテーマは「LT大会」です。 ニフティでは長年定期的に社内LT大会を実施しており、バラエティに飛んだ発表がなされています。 今回はそんなLT大会の雰囲気を感じ取れる発表を選りすぐってお伝えします。 概要 日程:1月27日(金)18:00〜20:00 配信方法:Remo 視聴環境:インターネット接続が可能なPC(ブラウザはChrome、Edge推奨) 参加方法 Remoにて実施いたします。 RemoのURLは決定後、参加者への情報欄に記載いたします。 こんな方におすすめ 他社のLT大会の雰囲気を知りたい方 ニフティの技術や風土に興味がある方 ニフティのエンジニアと話してみたい方 タイムテーブル 時間 コンテンツ 18:00 – 18:05 オープニング+会社紹介 18:05 – 18:15 ニフティLT大会について 18:15 – 18:22 LT1 効率化小ネタ集 18:22 – 18:29 LT2 新人のすゝめ 18:29 – 18:36 LT3 AI画像生成で神絵師になれる仕組みをやさしく説明する 18:36 – 18:43 ゲスト枠1 18:43 – 18:50 ゲスト枠2 18:50 – 18:55 まとめ+懇親会のご案内 18:55 – 20:00 懇親会 ※ 各LTは7分とします ※ ゲスト登壇者がいない場合には以降の予定を繰り上げます テーマ ニフティの社内LT大会で優秀発表賞を受賞した若手エンジニアが登壇いたします。 バラエティ豊かなLTからニフティの雰囲気を感じ取っていただければ幸いです。 登壇者プロフィール 小浦 由佳(登壇者) ニフティ株式会社 インフラシステムグループ 社内情報システムチーム ニフティのLT大会の企画、運営をやっている人です。 普段の業務は情シスで、スクラムチームのスクラムマスターをやったりしています。 南川 大樹(LT) ニフティ株式会社 基幹システムグループ サービスインフラチーム 新卒入社3年目。シングルサインオンやユーザーサインアップなどの開発や運用を担当。効率化、自動化が好き。 湊谷 のぞみ (LT) ニフティ株式会社 基幹システムグループ サービスインフラチーム 新卒入社3年目。主に顧客情報管理システムやシングルサインオンなどの開発や運用を担当。特技は起床です。 中村 伊吹(LT) ニフティ株式会社 会員システムグループ 第二開発チーム 新卒入社4年目。ニフティニュースの開発・運用を担当し、最近はモダンなフロントエンド開発を学習中。男女混成チアリーディング元日本代表。 ニフティグループでは一緒に働く仲間を募集中です 新卒採用、キャリア採用を実施しています。ぜひ リクルートサイト をご覧ください。 ニフティエンジニアが業務で学んだことやイベント情報を エンジニアブログ にて発信しています! ニフティエンジニアのTwitterアカウントを作りました NIFTY Tech Talkのことや、ニフティのエンジニアの活動を発信していきます。 Tweets by NIFTYDevelopers 「NIFTY Tech Day 2022」を開催しました 技術イベント「NIFTY Tech Day 2022」のアーカイブはこちら NIFTY Tech Day 2022 アンチハラスメントポリシー 私たちは下記のような事柄に関わらずすべての参加者にとって安全で歓迎されるような場を作ることに努めます。 社会的あるいは法的な性、性自認、性表現(外見の性)、性指向 年齢、障がい、容姿、体格 人種、民族、宗教(無宗教を含む) 技術の選択 そして下記のようなハラスメント行為をいかなる形であっても決して許容しません。 不適切な画像、動画、録音の再生(性的な画像など) 発表や他のイベントに対する妨害行為 これらに限らない性的嫌がらせ 登壇者、主催スタッフもこのポリシーの対象となります。 ハラスメント行為をやめるように指示された場合、直ちに従うことが求められます。ルールを守らない参加者は、主催者の判断により、退場処分や今後のイベントに聴講者、登壇者、スタッフとして関わることを禁止します。 もしハラスメントを受けていると感じたり、他の誰かがハラスメントされていることに気がついた場合、または他に何かお困りのことがあれば、すぐにご連絡ください。 ※本文章はKotlinFest Code of Conductとして公開された文章( https://github.com/KotlinFest/KotlinFest2018/blob/master/CODE-OF-CONDUCT.md )を元に派生しています。 ※本文章はCreative Commons Zero ライセンス( https://creativecommons.org/publicdomain/zero/1.0/ ) で公開されています。
アバター
はじめに こんにちは。セキュリティチームの添野隼矢と申します。 今回、冬季のインターンシップ(12/09 開催)に講師として参加しました。 インターンシップについて ニフティでは新卒採用活動の一環として、インターンシップを開催しています。 現在ニフティでは下記のインターンシップを実施しています。 スクラム体験演習コース サイバー攻撃対応演習コース システム障害対応演習コース サービス企画コース 詳しくは、 こちら をご覧ください。 本記事では、その4つのインターンシップの中で先日開催したサイバー攻撃対応演習コースについて紹介させていただきます。 本インターンシップのおすすめポイント! 1dayで手軽に実際のサイバー攻撃への対応を体験でき、ISPについても知ることができ、なおかつニフティの雰囲気も感じられるインターンシップです。(必要な物は、PCのみです。) サイバー攻撃対応演習コース概要 インターン期間 1day(午後のみ) 開催日程(2022年度の場合) 夏2回(2022/08/26、2022/09/02) 冬2回(2022/12/02、2022/12/09) 開催形式 Zoom+Slackによるオンライン実施 タイムスケジュール インターンシップ内容紹介 本インターンシップでは最初に座学を行い、後半はグループに分かれてグループ演習を行います。 座学について 座学では、以下の内容についての講義をします。 ISP・ネットワーク 昨今の社会情勢やネットワークの基礎的な知識について 質問等も用意されていて、みんなで楽しみつつ学べます。 ex) 今年最大インターネットトラフィックを記録したイベントは何か?またそのトラフィック量はどれくらいか?等 セキュリティ 昨今のサイバーセキュリティ事情について 演習について 1グループ5~6人のグループに分かれてもらい、Zoomのブレイクアウトルーム機能とSlackを用いてグループワークをしてもらいます。 Slackに担当社員からある状況が与えられ、その与えられた状況に対しての対策の議論をしてもらいます。 演習の内容としては、実際のシステムの担当者役として、ある障害事例をもとにした机上の業務体験となります。 実際のサイバー攻撃では、攻撃者は攻撃の手は止めてくれません。 演習でも、議論中に新しいイベントが起こり続けるので、その新しいイベントに対しても議論をしてもらいます。 また各グループには、現場社員1名がメンターとして付き、学生のみなさんの演習のサポートをさせていただきます。 演習終了後、グループ内で最初の振り返りをしてもらい、その振り返りを全体で発表してもらいます。 振り返り発表後、シナリオの種明かしがあるので、その種明かしを踏まえ、チームで他にどんなことができただろうかと再度振り返りができます。 また再度の振り返りでは、メンターによるフィードバックもさせていただきます。 懇親会について インターンシップ実施後には、現場社員と就活についてや社内の雰囲気について話せる場として懇親会の時間を設けています。 参加者の感想 インターンシップに参加した学生のみなさんからの感想をいくつかご紹介します。 実際にサイバー攻撃を受けた際の対策や考え方を実戦形式で体験することができたため、座学より深く理解することができた。 サイバー攻撃のモデルケースについて、実際に攻撃されたシステムの担当者役として何をすべきか考えることで、多角的な視点からサイバー攻撃の対処法を考える力が身についた。 成果としてはISP業界という今まで曖昧だった領域に関して知見を得た事です。通信事業者やプロバイダなど周辺的な知識を浅く知っていた点が多かったので、今回のイベントで深く理解できた点は良かったです。 また以下のような想定外の成果を得ることができたという感想もいただきました。 自分自身昔から議論に関して苦手な部分がありましたが、今回の議論では自身の考えをうまく発信することができたことが自分自身驚いております。 ラフな懇親会で、メンター社員さんと気軽に会話、社内の雰囲気を聞くことができた。 個人的にセキュリティというと技術的な面に目が行きがちであったため、お客様への対応や他部署や他社との関わりなどが必要不可欠であることが意外であった。 終わりに 学生の方とお話をした際、セキュリティエンジニアやインフラエンジニアに興味ありつつも、普段どんな業務をしているのかが分からないという学生の方が多くいらっしゃいました。そのような学生の方にとっても本インターンは役立つインターンシップだと思います。 セキュリティエンジニアやインフラエンジニアにご興味ある方、ご応募をお待ちしています。また最初でご紹介したサイバー攻撃対応演習コース以外のインターンシップへのご応募もお待ちしています。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
はじめまして!新卒3年目の湊谷です。 はじめに 突然ですが、皆さん「事前確認」をする機会ってありますか? 普段私は主にニフティのお客様の情報を管理するシステムの開発・運用を行っています。 その性質上、「顧客情報を取得するAPIを利用したい」「動作確認用のテストIDを発行してほしい」といった依頼をよくいただきます。 顧客情報を扱う業務の都合上、依頼の正当性を事前確認するルールとなっており、Slack で受付調整をお願いしているのですが… いやあ、めんどくさいなあ…。 フリーフォーマットでの受付調整は依頼する側も事前確認する側も大変です。 これをどうにか改善していきたいと思います。 Slack ワークフローを使って文字入力を極力なくそう! まずはSlackのワークフロー機能を使ってみます。 ワークフローとは、複数のステップで構成される自動化されたタスクまたはプロセスです。コーディングの必要はありません。ワークフローは自分の Slack ワークスペースで開始されます。Slack で直接実行することも、他のツールやサービスと連携させることもできます。 https://slack.com/intl/ja-jp/help/articles/360053571454-Slack-%E3%81%A7%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B ワークフローを使って事前確認の文章を送信できるようにしてみましょう。 このようなワークフローを作成しました。 依頼側が記入するフォーム フォームの内容を基にワークフローを開始したチャンネルへメッセージを送信する 「承認ボタン」を押すと確認したという文章がスレッドに返信される こうすることで、依頼側は記入することが少なくなり、確認側はボタン一つで承認し、必要な情報を伝えることができるようになりました。 ですが…全ての申請のワークフローを作成しようとすると、少し依頼側に不親切なつくりになりそうです。 事前確認の他にもワークフローはたくさん作っているのですが、これだけで表示を埋め尽くしてしまいます。一気にばーっと表示されるのも分かりづらいです。 そこで、「申請前の事前確認用ワークフロー」を一つのみ作成し、そこで選択された内容によって回答を変更する仕組みを作りたいと思います。 Slack Bolt for Javascriptを使ってbotを作成しよう! Bolt は Slack API を使いやすくするための Node.js フレームワークです。 一から API を使ってアプリを開発していくことと比べると、このフレームワークを使うともっとサクっと作っていくことができると思います。 https://api.slack.com/lang/ja-jp/hello-world-bolt このBoltを使用して、チャンネルに投稿されたメッセージを受け取り、そのメッセージによって返事を変化させるBotを作成してみようと思います。 Slackアプリの構築の仕方については、上記のリンクや Bolt 入門ガイド に詳細があるため省略させていただきます。 まずは、改めて「申請前の事前確認用ワークフロー」を作成します。 依頼側が記入するフォーム フォームの内容を基にワークフローを開始したチャンネルへメッセージを送信する 今のままワークフローを送信するとこのようになります。 「■申請項目」の中身によって、どのような返事を返すのかを決めていきます。 ワークフローを少し編集しましょう。 作成したSlackアプリに対してメンションを送るようにします。 こうすることで「アプリに対してメンションが送られ、かつ特定の単語が投稿された時に反応する」実装が可能となります。 // アプリにメンションがつけられた時に反応する app.event('app_mention', async ({ body, say }) => { // 投稿された文章を取得する const message = body.event.text; // 文章の中に「テストID発行」があった場合 if (message.includes('テストID発行')) { // 文章の中からワークフロー送信者のIDを取得する const userId = message.match(/(?<=<@)(.)*(?=>さん)/)[0]; // const userId = getUserIdAddress(message); // ワークフローに返信する本文を作成 const text = `<@${userId}>\n事前確認を受け付けました。\n<任意のメンション> 内容を確認の上、承認してください。`; // メッセージを送信 await say({ // ワークフローの投稿のスレッドに返信 thread_ts: body.event.ts, attachments: [ { // 通知欄に表示する文章を指定 fallback: text, color: '#ffa500', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: text, }, }, // 「承認」ボタンを作成 { type: 'actions', elements: [ { type: 'button', text: { type: 'plain_text', text: '承認', }, // 「テストID発行」が送信された時の承認ボタンのIDを設定 action_id: 'testid_button_click', }, ], }, ], }, ], }); } }); この状態でワークフローを送信すると以下の様になります。 メッセージに対して自分の好きな色をつけたりなど装飾ができるのも良いですね。 次に、承認ボタンが押された時の実装をしていきましょう。 // action_id: 'api_button_click'のボタンが押された時に反応する app.action( { type: 'block_actions', action_id: 'testid_button_click' }, async ({ body, say }) => { // 文章の中からワークフローを送信した人のIDを取得する const userIid = body.message.attachments[0].fallback.split('\n')[0]; const text = `${userIid}\n<@${body.user.id}>が確認しました。△△△フォームから申請をお願いします。`; await say({ text: text, // スレッドに返信する thread_ts: body.container.message_ts, }); }, ); これで承認ボタンを押すと返信されるようになりました! あとは、申請項目の数だけ作りましょう。IDの取得や、sayで投稿する中身は別のファイルに切り出すと、よりスッキリと書けそうですね。 最後に Slackワークフロー+Boltを使って作成したBotアプリを組み合わせた活用例を紹介しました。 ワークフローはコードを書かなくてもできることがとてもたくさんあります。 そこにアプリを組み合わせることで、細かい実装を実現できるかなと思います。他にも特定の単語が投稿されたことに反応して、「Notionに新規ページを作成する」「Jira・Githubにチケットを作成する」といった仕組みも作っています。 みなさんも是非Slackの様々な機能を活用していきましょう! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
はじめに こんにちは。ニフティ株式会社の並木です。 2022年4月17日(日)と2022年10月9日(日)に応用情報技術者試験を受けてきました。 2回受験しているということで、すでにお察しかと思いますが1回目は不合格でした。(2回目で無事に合格しました) 今回は、勉強方法や実際に受験した時に感じたことなどを振り返り、1回目の試験が不合格となった原因について考えてみました。 これから受験される方の参考になれば幸いです。 応用情報技術者試験とは IPA(独立行政法人 情報処理推進機構)が実施している国家試験の1つ 詳細: https://www.jitec.ipa.go.jp/1_11seido/ap.html 午前試験 試験時間:9:30~12:00(150分) 問題数:80問 形式:マークシート(四肢択一) 午後試験 試験時間:13:00~15:30(150分) 問題数:11問から5問選択(解く分野を選択することができる) 形式:記述式 受験対策について ①参考書を読む 1冊のみ購入 4月の試験に向けて、10月から読み始めた。一通り読み終わったのは2月末 一通り読み終わった後は、過去問を解いた時に分からなかったところをピックアップして読んでいた ②午前対策 11月から4月の試験当日までほぼ毎日、ひたすら過去問を解いた 1日あたり10~20問程度。時々、いっぺんに1回分全部(80問)解いてみたりもした 5年分(10回分)の問題を解いた。直近2回分の過去問はあまり出題されないという噂を聞いたのでそこは外した ③午後対策 午後の過去問と解説が載っている問題集を1冊購入 4月の試験に向けて、12月末から解き始めた。3月末に全問解き終わる 全問解き終わった後、その結果をもとにしてどの分野を解くか決めた(午後試験は、解く分野を選択することができる) 分野 解くかどうか  • ◎:絶対解く  • ○:解くつもり  • △:↑がダメな場合の保険  • ×:解かない 理由など 問1 情報セキュリティ ◎ 問1は必須(問2~問11の中から4問選択) 問2 経営戦略 ○(計算問題がなければ解く) 計算問題が苦手。それ以外は解けそう 問3 プログラミング × 過去問を解いた結果、解くのに時間がかかったため 問4 システムアーキテクチャ × 過去問を解いた結果、解くのに時間がかかったため 問5 ネットワーク △ 問6 データベース ◎ 過去問を解いた結果、できが良い方だった 問7 組込みシステム開発 ○(計算問題がなければ解く) 計算問題(クロック周波数)が苦手。それ以外は解けそう 問8 情報システム開発 ◎ 過去問を解いた結果、できが良い方だった 問9 プロジェクトマネジメント △ 問10 ITサービスマネジメント △ 問11 システム監査 △ 受験した感想 上手くいったこと 時計を持参した 会場がホテルの宴会場みたいなところで、時計が無かったので持ってきて良かった お昼ご飯を持参した 休み時間が1時間(準備時間などがあるので実質40分くらい)しかないので、買いに行く時間がない ちなみに、午前試験を途中退出している人も結構いた気がする。途中退出すればもう少し時間を捻出できるかもしれない(私は途中退出しなかった) 午前問題は見覚えのある問題がたくさん出てきた 「過去問5年分解いておけば大丈夫」という噂を聞いたが、その通りかもしれない 失敗したこと 午後問題の時間配分ミス 事前に立てた計画 時刻 時間 やること 13:00~13:10 10分 どの分野を解くか決める 13:10~13:35 25分 問1:セキュリティ 13:35~14:00 25分 問6:データベース 14:00~14:25 25分 問8:情報システム開発 14:25~14:50 25分 問2か問7(難しい場合は問5か問9~11) 14:50~15:15 25分 問2か問7(難しい場合は問5か問9~11) 15:15~15:30 15分 見直し 現実 時刻 時間 やったこと 思ったこと 13:00~13:10 10分 どの分野を解くか決める ・10分かけて問題を見たが、迷っているうちに時間が過ぎた。結局この10分ではどの分野を解くか決めきれなかった ・この時間をもっと短くして問題を解く時間にあてれば良かった 13:10~13:35 25分 問1:セキュリティ ・25分間、目一杯時間を使ってしまったが、これ以上答えが埋まらないと思った時点で早めに切り上げて他の問題を解くべきだった ・1問あたり25分間使うと決めていたのが仇となった 13:35~14:15 40分 問6:データベース ・問題を読むのに時間がかかってしまった ・25分で解こうと気を付けてはいたが、ここで中途半端にやめるよりは解ききった方が良いのでは…?と思い、結局15分くらいオーバー 14:15~14:40 25分 問8:情報システム開発 ・データベースと情報システム開発は絶対に解くと決めていたが、文章量が多くて苦労した ・問題を読んでいてもなかなか頭に入ってこなくてキツかったので、25分経ったところで一旦やめて後回しにした 14:40~15:05 25分 問7:組込みシステム開発 15:05~15:25 20分 問11:システム監査 ・残り1問どれ解くか迷ったが、1番文章量が少なそうに見えた監査にした 15:25~15:30 5分 問8:情報システム開発 ・どうしようもないので適当に埋めた どの問題を解くか決めるか10分もかけて悩んでしまった どの問題が解けそうか、素早く見極められる力が必要 時間が足りず焦ってしまい、問題文が頭に入ってこなかった 試験中に「問8:情報システム開発」の問題文を読んだ時はなかなか理解できなかったが、後で落ち着いて問題を読んでみたら、解けない問題ではなかった 実際に試験を受ける前に、時間を計って模擬試験をやれば良かった こんなに時間が足りなくなって焦ると思わなかった 結果 どちらも60点以上で合格です。 午前:73.75点(合格) 午後:58.00点(2点足りず不合格) 午後は手ごたえ的には全然できていないような感じがしたのですが、思ったよりも点数が取れていました。 時間配分さえミスしなければ、もしかしたら受かっていたのでは…と思うと悔しいですが、上記の失敗を糧に、10月の試験は無事に合格することができたのでよしとします。 試験勉強をすることに加えて、実際の試験でどう行動するか(どの問題を解くか、時間配分はどうするか)を念入りにシミュレーションしておくことが、合格の鍵となるのではないかと感じました。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
この記事は、 ニフティグループ Advent Calendar 2022 24日目の記事です。 はじめに 昨日に引き続き、会員システムグループの山田です。 前回はJetpack Composeのよかった点についてでしたが、今回はイマイチだった点になります。 イマイチだった点 前回同様、記載するコードは簡略化の都合上、一部の属性値などを省略しています。 Navigation Composeがつらい Jetpack Composeで画面遷移を実装しようとする場合、手段は大きく2通りあります。 FragmentでComposeを描画し、Activity上でFragmentを切り替えることにより画面遷移する fragmentTransactionを使うか、 Navigation Component を利用する Fragmentを使わず、Activity上で描画するCompose関数を切り替える 後者を実現するためのライブラリが Navigation Compose です。これはNavigation ComponentのJetpack Compose版なのですが、使い勝手が大きく異なっています。 定義の違い Navigation Componentでは、画面遷移の定義は以下のようにXMLで行っていました。 <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph" app:startDestination="@id/fragment1"> <fragment android:id="@+id/fragment1" android:name="com.example.navigationsample.Page1Fragment"> <action android:id="@+id/action_fragment1_to_fragment2" app:destination="@id/fragment2" /> </fragment> <fragment android:id="@+id/fragment2" android:name="com.example.navigationsample.Page2Fragment" android:label="Fragment2" tools:layout="@layout/fragment_page2"/> </navigation> <navigation>の中に各画面を<fragment>として設置し、IDやFragmentのクラスなどを指定 最初に表示される画面はstartDestination属性で指定 画面間の遷移は<action>として指定 一方、Navigation Composeでは以下のように指定します。 @Composable fun Router() { val navController = rememberAnimatedNavController() NavHost( navController = navController, startDestination = "page1", ) { composable("page1") { Page1(navController) } composable("page2") { Page2(navController) } } } NavHost関数の中に各画面をcomposableとして設置し、呼び出すComposable関数を指定 最初に表示される画面は引数のstartDestinationで指定 画面間の遷移の定義は存在しない actionの定義がない以外はNavigation Componentとほぼ同じような記述ですが、使ってみると辛さが表れてきます。 画面遷移が型安全でない Navigation ComponentではSafe Argsという機能があり、XMLの定義からクラスを自動生成してくれます。これを利用して画面遷移が以下のように行えました。 val action = Page1FragmentDirections .actionFragment1ToFragment2() findNavController().navigate(action) Page1FragmentDirectionsが自動生成されたクラスで、これはXMLに記載されたactionの定義から作られています。このため、XMLに記載されていない遷移を呼び出すことを防止できていました。 一方、Navigation Composeでは以下のようになります。 navController.navigate("page2") 画面遷移は遷移先の画面を文字列で指定します。誤った文字列を与えてしまうことも起こり得ますし、リファクタリングで名前を変えたとしても追従できません。 画面間の引数渡しがつらい 画面遷移時に画面間でデータの受け渡しを行おうとするとより辛くなります。 Navigation Componentではactionに引数を設定することができます。 <action android:id="@+id/action_fragment1_to_fragment2" app:destination="@id/fragment2"> <argument android:name="text" app:argType="string" android:defaultValue="hoge" /> </action> こうして設定された引数や引数の型は自動生成クラスにも反映されるので、以下のように型安全に利用できます。 val action = Page1FragmentDirections .actionFragment1ToFragment2("hoge") findNavController().navigate(action) 内部ではBundleが利用されており、引数がBundleに詰められて遷移先Fragmentに渡されることになります。 一方でNavigation Composeでは以下のようになります。 composable( "page2/{text}", arguments = listOf( navArgument("text") { type = NavType.StringType } ) ) { backStackEntry -> val text = backStackEntry.arguments?.getString("text") Page2(navController, text) } 画面間のデータ渡しはURL形式の文字列により行われます。必須引数はパスパラメータ、非必須パラメータはクエリパラメータの形で記述します。 遷移の呼び出し側は以下のようになります。 navController.navigate("page2/hoge") ここでも型安全性が失われています。文字列としてはなんでも渡すことができてしまうため、ビルド時に誤りに気づくことは不可能です。 加えてURL形式文字列であることもネックで、 引数にスラッシュなどが含まれる 引数がバイト列である などの場合はエスケープやBASE64エンコードを自分で行う必要があります。当然、受け取る側は逆の処理が必要になります。 これらをなるべく安全に扱えるように、マイ ニフティではsealed classによる遷移先管理を行っています。Googleの公式サンプルでは定数値やenumなどで管理を行っているようです。 sealed class MainRoute(val route: string) { object Page1 : MainRoute("page1") object Page2 : MainRoute("page2/{text}") { fun createRoute(text: string) { return "page2/${text}" } } } 遷移アニメーションの不具合 Navigation Componentでは画面遷移時のアニメーションが設定できたのですが、Navigation Composeにはその機能が(2022年12月現在)存在しません。 従来あった機能でJetpack Composeに未実装な機能を補完するものとして、 Accompanist ライブラリが存在しています。事実上の半公式ライブラリで、ここにNavigation Animationというものが存在するのでこちらを併用することになるのですが、これを利用したアニメーションに不具合があります。 まずはNavigation Componentで実装したものです。 1つ目の画面の上に2つ目の画面がスライドインし、戻る操作でスライドアウトします。 一方でNavigation Composeでの実装です。 スライドインは正常に行われるのですが、スライドアウトがおかしくなっています。 現状のAccompanistの実装では画面のスタック状態を考慮しておらず、常に遷移先の画面が上に描画されます。このため、戻る操作を行うと描画順が逆転してしまい、このような結果になります。 マイニフティではこれを解消できなかったため、遷移時に画面同士が重ならないようなアニメーションのみを利用することで回避しています。 先月公開されたAccompanist 0.27.1で z-orderの変更が入った ため、現在ではこの不具合は修正されている可能性があります。 AppBarを操作できない 従来のActivityとFragmentによる実装の場合、画面間で共通して利用するAppBarやBottomAppBarのようなコンポーネントはActivityで実装し、切り替わる画面のみをFragmentで実装することが多かったかと思います。マイ ニフティを従来の方法で実装するなら以下のように分割したでしょう。 共通利用とは言いつつ、AppBarの表示内容は画面によって異なることが多いかと思います。AppBarの領域はFragmentの管理外になるはずですが、FragmentのonCreateMenu()を利用することで例外的に書き換えが可能でした。 Navigation ComposeではFragmentを利用しないため、このような書き換えが不可能です。 画面別に出し分けを行うには、上位の現在の画面状態をStateに持って TopAppBar( title = { if (currentScreen == "page1") { Text("Page1") } else { ... } } ) のような分岐処理を入れざるを得ず、画面数が増えるほど分岐が増える好ましくない実装になってしまいます。 非効率にはなってしまいますが、マイ ニフティでは共通部分を作らず、各画面で別々にAppBarを持たせる実装としています。 従来のViewとの連携 Jetpack Composeがリリースされたとはいえ、すべての機能がJetpack Composeで記述できるようになったわけではありません。WebViewをはじめとして、旧来の仕組み(View)でしか記述できないものは残っています。 Viewとの混在は想定されていて、例えばComposeの中でWebViewを使いたければAndroidViewというものがあります。単純な実装ではこれで十分でしょう。 @Composable fun ComposeWebView(url: string) { AndroidView( factory = { context -> WebView(context) }, update = { view -> view.loadUrl(url) } ) } しかしマイ ニフティの場合はPull to Refreshの機能が存在しており、実装当初は Pull to RefreshをJetpack Composeで実装 WebViewをAndroidViewでラップして実装 という状態でした。Pull to RefreshとWebViewはどちらもスクロールの機能を持つため、Nested Scrollを利用してスクロールを制御する必要があるのですが、これがJetpack ComposeとViewの間で伝播せず、どちらかのスクロールが機能しない状態となっていました。 このため、マイ ニフティではPull to Refresh機能ごとView側で実装しているのですが、この弊害としてWebView利用部分のプレビューが行えないという状態になっています。 一部端末での不具合 一部の端末で予期しない動作をすることがありました。例えばXiaomiのMIUI 13を搭載した端末において、作成していない真っ白の画面が挿入される問題を確認しており、 issue に上げています。 ほとんどの端末では問題なく動作するのですが、特にOSに対するカスタマイズが多いメーカーの端末では注意する必要がありそうです。 おわりに Jetpack Composeのイマイチな部分についてご紹介しました。 見ていただければ分かるとおり、ほとんどの問題はNavigation Composeによるものでした。 ここだけは従来の方法と比べて機能的なデグレードが大きく、明確に使いづらいと言える部分です。画面遷移を引き続きFragmentで行うような実装も可能なので、すべてJetpack Composeで書くことを諦めるということも十分選択肢に入るのではないかなと思います。 総合的にはJetpack Compose導入の利点の方が大きく、今後はこちらが主流になっていくと思われますので、まだ導入されていない皆様もぜひ導入を検討してみてください。 明日はたけろいどさんによる「サービス開発にSvelteKitを導入するために行なったアプローチ」です。お楽しみに。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
この記事は、 ニフティグループ Advent Calendar 2022 23日目の記事です。 はじめに 会員システムグループの山田です。 ニフティでは昨年12月(iOS)と今年3月(Android)に、会員様向けアプリとして マイ ニフティ をリリースしました。 ニフティとしては久しぶりの新規アプリ開発となり、既存アプリのレガシー化も進んでいたことから、本アプリの開発ではゼロベースで技術スタックを見直し、現在のアプリ開発において標準的な技術に揃えることにしました。 その中でAndroidにおいてはUI構築にJetpack Composeを選定したので、その結果どうだったか、ということについてお話しします。 Jetpack Composeとは 2021年にバージョン1.0.0が公開された、Android用の新しい公式UIツールキットです。 Androidではその登場以来、XMLでUIを記述し、Java/Kotlinで操作するというスタイルでUIの構築が行われてきました。DatabindingやView Bindingなどの補完技術が登場しても、この基本は変わらず一貫していました。 Jetpack Composeはこれを覆す転換点となるツールキットです。主に以下のような特徴を持ちます。 UIをすべてKotlinの関数により記述する 宣言的UIの採用 特にReactで一大ムーブメントを巻き起こした宣言的UIの採用が特徴的で、今までの「XMLに書いたものをコードから変化させる」スタイルから「UIをを予めすべて定義しておいて、データ状態に応じて自動的に変化する」スタイルに一変しています。 詳細についてはAndroid Developersに 公式の解説 がありますので、そちらもご参考ください。 よかったこと 記載するコードは簡略化のため、一部の属性値などを省略しています。 記述がシンプルになる UIが非常にシンプルに書けるようになり、UI記述にかかる時間を大幅に削減することができています。 特に以下の点が効いています。 レイアウトのネストを気にする必要がない 上のようなレイアウトを組もうとした場合、従来の方法でシンプルに書くと以下のような構造になります。 <LinearLayout android:orientation="horizontal"> <ImageView /> <LinearLayout android:orientation="vertical"> <TextView /> <TextView /> </LinearLayout> </LinearLayout> しかしこれはよくないとされる記述です。 従来のAndroidのレイアウトではネストが深ければ深いほど、加速度的にレンダリング時間が増加するという問題が存在します。したがってこのような記述は避け、ConstraintLayoutをはじめとする複雑なレイアウト方法をとり、なるべくフラットに記述する必要がありました。これは学習負荷が高く、またサッと組むには時間のかかる方法です。 Jetpack Composeではネストの問題が解決されているため、このような考慮が不要です。 @Composable fun ArticleRow(article: Article) { Row { AsyncImage(model = article.imageUrl) Column { Text(article.title) Text(article.body) } } } Jetpack Composeではアノテーションを付けた関数(Composable関数)でUIを記述します。この中でRowとColumnが従来のLinearLayoutに対応しています。 このように見た目通りの構造を記述しても、レンダリング速度が大きく落ちることがありません。 リストもシンプルに書ける 従来、リスト形式のUIを作成する場合はRecyclerViewを使用することが多かったと思います。 RecyclerViewを使うためには面倒な準備が必要で、 DiffUtilを使ってデータの同一性判定を定義 RecyclerView.ViewHolderを継承してView保持クラスを定義 RecyclerView.Adapterを継承してデータとのバインディングを定義 という手順を踏んでようやく使えるようになります。 Jetpack ComposeではLazyColumnを使えばよく、 @Composable fun ArticleList(articles: List<Article>) { LazyColumn { items( items = articles, key = { article -> article.id } ) { article -> ArticleRow(article) } } } このように簡単な記述でリストが記述できます。 プレビューが容易 従来のXMLでもプレビューはできるのですが、コードから動的に書き換えられる部分のプレビューが難しいという問題がありました。またレイアウトのネストを深くできないという制約上、UIを細かいコンポーネントに分割してプレビューすることもなかなか難しいということも課題でした。 Jetpack Composeのプレビューは非常に簡単です。UIコンポーネントが全て関数で記述されるので、プレビュー用のデータを引数に与えるだけでレンダリング可能な状態になります。あとは@Previewアノテーションを付与した関数でラップすればプレビューの完成です。 @Preview @Composable fun ArticleRowPreview() { MyTheme { ArticleRow( Article( id = "xxx", title = "タイトル", body = "本文", imageUrl = "https://example.com/image.png" ) ) } } 複数パターンを試したければ引数を変えたものを別途用意するだけで済みます。細かい関数に分けていけばその単位でプレビューが可能なので、ReactなどにおけるStorybookのようなコンポーネントカタログとしての利用が可能です。 React Hooksに近い Jetpack ComposeはReactのHooks APIに近い概念や文法を持ちます。 UIを関数で記述するという基本文法もそうですし、状態や副作用処理に関しても const [state, setState] = useState(0); useEffect(() => { ... }, [state]) val state = remember { mutableStateOf(0) } LaunchedEffect(state) { ... } このように大まかな対応が取れます。 ニフティでは新人教育の中でReactを取り入れているため、アプリ開発にジョインする際の学習コストを抑えることができています。 状態管理と強依存しない Jetpack Composeは専用に用意されたStateの仕組みによって状態を管理します。では全ての状態をStateで管理する必要があるのかというとそうではなく、RxJavaやLiveData、Flowといった従来の状態管理の仕組みと連携することが可能です。 例えばViewModelに保存されたFlow型変数を、以下のようにState型に変換できます。 val state = viewModel.flow.collectAsState() 変換が用意されているため、UIはJetpack Composeで書きつつ、状態管理やデータアクセスは従来通り、Jetpack Composeに依存しない形で記述することが可能です。このため、UI以外のレイヤーは従来と同様の設計を行えばよいことになります。 マイ ニフティではモダン化を行った分、新規に採用するライブラリも多く、経験の少なさがバグや工数増大のリスクになり得ました。その点、中核となる設計に従来の知見を使えることはリスクを低く保つことに大きく貢献しました。 最新のAndroidが必要ない マイ ニフティではiOS版でも同様に技術スタックの見直しを行ったのですが、SwiftUIの採用には至りませんでした。これは開発初期の時点でiOS 10を最低バージョンとしており、SwiftUIに非対応であったためです。便利な機能であってもそれがOSに対して実装される場合、採用可能になるまでは数年間待つ必要があります。 Jetpack ComposeはAndroidXと呼ばれる公式補助ライブラリの一部として開発されており、Android本体とは独立しています。Jetpack Compose 1.0.0の時点でAPI 21(Android 5.0)以上に対応しているため、問題なく採用できました。 おわりに マイ ニフティでJetpack Composeを採用してよかった点を見てきました。特に記述のシンプルさは追加開発や新人のジョイン時にも大きく貢献しており、社内のアプリの中でも頻度の高いアップデートを行うことができています。 一方で開発していく中でイマイチだと感じる点もありました。次回はそちらについてご紹介する予定です。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
この記事は、 ニフティグループ Advent Calendar 2022 (カレンダー1) 25日目の記事です。 こんにちは、ニフティでAWS/GCPや開発寄りのSaaS管理などしている石川です。 年末ですね、大掃除のシーズンです。 オフィスやキャビネットの掃除はしてても社内情報の掃除はみなさんしているでしょうか。 本日は Notion様の事例紹介として載せさせていただいた内容 の詳細版として、ConfluenceをNotionに移行する前に行った大掃除とNotion上での情報の配置の話をしようと思います。 本記事の以下の流れで説明していきます。 削除と取捨選択 分類分け 再配置 Confluence Serverの大掃除 まずは移行前に身軽にしておきたいので大掃除から、と言っても棚卸し作業が主です。 重複添付ファイルの削除 移行対象の取捨選択 ほぼ空のスペースの削除や集約 重複添付ファイルの削除 Confluence Serverはページコピーの際に添付ファイルも複製されます。定例ページなどコピーして新規ページを作っていると雪だるま式に添付ファイルが増えていってしまいます。これが大量にあるので一番古いファイル以外を消します。 (Confluence豆知識:2016年からページコピーで添付ファイルが複製されるようになり、2019年に複製しないオプションが追加されましたがデフォルトでは複製します。Cloud版も同様の仕様ですが、複製しないオプションはCloud版にはないため必ず複製されます) 削除方法ですがSQLで抽出してAPIで消すだけです(かなり時間はかかります) SQLで同一スペース内にあるファイル名&サイズ&バージョンが同じものが n個以上あるファイルを抽出 APIを叩いて消していく 直近で行った結果だと、 全体の15%ほどが重複ファイルでした 。2020年に初めて行ったときは 50%近くが重複ファイルだった ので、これでも重複数は減った状態です 移行対象の取捨選択 移行先にゴミを持ち込まないために重要なステップです。 今回はヒアリングシート作って移行先(4択)を入力してもらう形で行いました。 デフォルト値が空だとどうしても 「使うかわからないけどとりあえず移行するか」 と考える方が多い ので、各スペースの利用統計から推奨値を入れておくのはやっておいたほうがいいです。また明らかに情報が少ないスペース(議事録だけ数ページだけある)は、全社共通議事録DBを移行先にする対応を行うとより良いですね。 例:長年更新がない、アーカイブスペースになっている → デフォルト 移行しない 例:数年更新がない、近年アーカイブスペースになった → デフォルト エクスポート 移行先問わず使える知見がAtlassianから提供されているので一度目を通してみるといいと思います。 参考: Confluence のベストプラクティス | アトラシアン | Atlassian 参考: ACE#43 Atlassian Cloudへの道のり – YouTube     情報の分類分け 次に移行前にやっておきたいのは情報の分類分けです。 Confluenceのスペースを維持した情報管理 Confluenceから移行した関係上、スペースという概念を保持しておく必要がありました(Notionらしく全部ひとつのDBでドキュメント全部管理するというのは新規ならいいが移行だと難しい)。そのためニフティでは特定階層にあるページを スペースページ と呼称し、情報を取り扱うひとつの粒度として扱っています。 移行の際にConfluenceのスペースをざっくり Dept 、 Knowledge 、 Product に分類し3つのチームスペース内にスペースページとして移しています。メタ情報的にはもう少し細かく分類しているのですが、チームスペースとしてはこの3種にしています。 会社の組織構造に合わせてチームスペースを切っていないのは、もともとプロダクト・プロジェクト単位で情報を集めていることが多かったのでそれを踏襲しています。   Notionで実現したかったことと情報配置 仕上げにNotionでどう配置しているのかについて。 検索ノイズを減らし ここになかったらないですね を実現したい 情報が貯まっていくと、検索ノイズが多くなり検索結果は返ってくるけど目当てのものがない、そもそもストック情報として存在しているかどうかもわからない、という状況がままあります。 そこで検索ボリュームが大きい、全従業員向け・特定職務向け・特定ツール利用者向け・オンボーディングなどはすべて Knowledgeチームスペース に集約しました。 ここを検索することで正確性が担保された情報を見つけやすくなる上に、見つからなかったらNotion上にはなくまだストック化されていない情報だと判別することができるようになります。 なにかしら検索結果が返ってきてしまうと、うまく探せてないのではないかと再検索を繰り返してしまうものですが、結果が返ってこないか少数であれば ここにはないのか と思い、早い段階で別のアクションに移ることができます。   俯瞰して全体を眺め 上から辿っていけるようにしたい 行なったことは大きく3点です。 スペースページDBの作成 カオスマップの再現 索引の作成 スペースページDBの作成 Confluenceにあったスペースディレクトリの代わりとして、スペースページのインデックスDBを用意しました。   ツールカオスマップをNotionで再現 探すべきスペースページが多すぎると選べない場合があるため、カオスマップをNotion上で再現するようにリンクを配置したページを用意しました(ここはいずれDBでうまく再現したい) 索引の作成 主要なDBへリレーションして索引として機能するDBも用意しました。 Confluenceのときは問い合わせ先一覧という大きなテーブルを作っていたのですが、これに索引の機能を付与しました。主要なDBにリレーションするだけで情報が集まってくるため前よりもメンテナンス性が上がったと思います。ここはNotion事例紹介にあった企業様のやり方を一部真似させてもらいました。 参考: 入社1ヶ月でNotionを使いやすい形にデザインした話|西山 将平(Shohei Nishiyama)|note タグとして単語選びと粒度は今後どう運用していくかは課題となっています(一旦Folksonomiesで運用して数を増やし、後でTaxonomyとして整理管理するのがいいか?)   最後に Confluenceを現在お使いの方もこれからCloud版や他へ移行予定の方も、移行前に掃除をしっかり行っておくと移行数も移行時間も大きく削減されるので是非やりましょう。 情報の整理については、既存の情報を実際に見にいってどういう情報がどういう形で置かれているのか把握するのは重要だと感じました。時間はかかりますが、やる前と後では情報に対する解像度が変わります。知識的なものとしてはオライリーの情報アーキテクチャの本が頭の整理に役立ちました。 参考: O’Reilly Japan – 情報アーキテクチャ 第4版 Notion上での組み方ですが、移行前のページ構造を大きく変えずに一部全社共通DB使っていたりとNotionっぽさも混ぜ込みつつ、ひとまずいい感じには使えているのではないかと思っています。組み方に正解はなく各組織ごとに合った構成を見つけていくしかないので難しい部分ですよね、ここは今後も適宜改善していくつもりです。 本記事で ニフティグループ Advent Calendar 2022 は終わりとなります。 多種多様な記事が揃っていますので、是非上記リンク先から他の記事も訪れてみてください。   We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
この記事は、 ニフティグループ Advent Calendar 2022 25日目の記事です。 こんにちは、たけろいどです。SvelteKit v1.0.0リリースおめでとうございます!! この日を心待ちにしていました。まさに魔法のように動き、コードを書くたびに驚かされています。 はじめに この記事ではSvelteKitの技術的知見を多くは提供しません。 どちらかというとSvelteKitを本番環境へ採用するまでに至った心構えや気を付けていることを述べていきます。 とはいっても… なにも紹介しないというのも釈然としないので、情報をどう集めているかを残しておきます。 これからSvelteKitを使っていく人の参考になればとてもうれしいです! SvelteやSvelteKitはとても新しいほうのフレームワークです。そのため日本語の情報は少ないです。さらにSvelteKitのv1.0.0の情報は日本語に限らず情報が少ないです。 その時に頼りになるのが公式ドキュメントです! Svelte SvelteKit Svelteは公式ドキュメントがとても優秀で開発に必要な情報は7割方こちらを参考にしています。 UseCaseが欲しい場合はExamplesを見てみましょう これら公式ドキュメントは有志の方々が翻訳してくださっています。本当にありがとうございます。 SvelteJP SvelteKitJP 他3割はSvelteのGithubを見ることが多いです。issueやdiscussionを覗いてみましょう。より詳細な情報を入手できるでしょう。 よきSvelte lifeを ということで主題に入ります。 SvelteKit導入までの道のり チーム理解 「SvelteKitを使いたいです!」といっても「Nextでいいじゃん…」と言われることが大半です。 負けないように説得しましょう。確かにエコシステムでは圧倒的な差がありますが… SvelteKitのメリットをチームに紹介しましょう。 コード記述量が少ない。まさに魔法のようなコード Vueのようなテンプレート記法を扱える 仮想DOMを使わない コンパイラであること その他Svelte・SvelteKitのメリットを感じるには Tutorial を触って実際に体験してみましょう! またSvelteについては別の記事で紹介しました。 そちら も読んでみてください。 それに加えて新しい技術にトライすることの楽しさを伝えます。しっかりとした論理を説明できた上で、熱意をぶつければ理解を得やすいです。 ちなみに旧サービスでJinja2などのテンプレートエンジンを扱っているのなら、記法が似ているのでオススメできるポイントが増えます。 サービス理解 そのサービスにSvelteKitは本当に必要でしょうか? サービスの規模感を考えてみるのがよいと思います。 確かにSvelteKitは優秀なフレームワークですが、実用例が極々少ないです。 それゆえの不安を捨てきれません。 大きなサービスを作るのなら引継ぎや保守・運用も大変です。そんなサービスでSvelteKitを使うのは少し怖いですね。 まずは小さなサービスから始めてみるのがオススメです。社内向けサポートツールなど内向きに閉じているサービスもチャレンジしやすくてオススメです。リスクを正しく認識していればチーム理解も得やすいと思います。 技術理解 SvelteKitでできることを知りましょう。 たいていのことはできると思いますが不得意なこともあります。 ゲームを作ることは難しいですし、エコシステムもNextに比べたらまだまだです。 Githubや ShowCase などをみて、すでに動いているサービスはどんな実装なのか見てみると勉強になります。 またあくまでもアプリケーションフレームワークです。目的はアプリケーションを作ることでそのSvelteKitは便利な手段を提供しているにすぎません。 それを念頭にSvelteKitでよりよい開発をしていきましょう。 最後に SvelteKitを導入する上でこれら3つの理解を深めていくことが重要だと感じました。 SvelteKitは使い勝手がよく素晴らしいフレームワークです。本番環境で使うかどうかは置いといても触れておいて損はないです。 この記事を読んでSvelteKitを本番に導入した事例が増えれば幸いです。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
こんにちは、新卒2年目のRyommです! この記事は、 ニフティグループ Advent Calendar 2022 24日目の記事です。 クリスマスイブです!今日はSexyZoneがデビューから11年目にして初めて京セラドームで単独ライブを行った記念すべき日ですね!私も今日は京セラドームに来ています! 今回はSexyZoneから受け取ったエネルギーで作ったスタンプラリーbotを紹介します! はじめに ニフティではクラウド・ゴールデン・ジム(以下CGG)というクラウド人材を育てるための社内勉強会を開催しています。 私はこのCGGの運営として、より多くの人に参加してもらうことで社内全体の技術力の底上げにつなげるべく、いくつか参加のモチベーションとなるような仕掛けを準備しました。 その一環として、ジムから連想してスタンプラリーを作ることにしました。 スタンプラリーの要件 スタンプラリーの要件は以下の通りです。 勉強会への出欠と、勉強会で行うクイズの結果をもとにスタンプのランクが変わるようにしたい 誰でも参照できて、誰がどんなスキルを持っているかの指標になるようにしたい 簡単に参照できるようにするため、Slackでbotとして呼び出したい @hogebot command [target] のような形式で問い合わせると、対象の画像が返却されてslack上のプレビューで見られるようにしたい 管理画面を作る余力はないので、データの管理はGoogle SpreadSheetで行いたい できたもの helpコマンド 検索結果が1つだけのとき 検索結果が複数あるとき 検索結果に合致するデータがないとき 有効なコマンドがないとき 作ってみた 概要は以下の通りです。 bot応答部分の骨組みを Lambda + API gateway + Slack App で作成 画像合成部分を Google SpreadSheet + GAS ( + S3 )で作成 合成した画像を S3 にアップロードしてURLを SpreadSheet に保持する部分を作成 作成したURLと名前などをセットにして DynamoDB にアップロードする部分を作成 botが受け取った値を用いて DynamoDB を検索して該当のスタンプラリーカードURLを返却する部分を作成 構成図 1. bot応答部分の骨組みを Lambda + API gateway + Slack Appで作成 まずは基礎となるbot部分を作ります。 ここではSlack botにメンションをつけてメッセージを送ると、Lambda側でメッセージを受け取ることができ、メッセージに応じて何かしらの返信をするようにします。 Slack App作成 Permissionを設定する app_mentions:read メンションされたメッセージを読み込むために必要 channels:history 公開チャンネルでメンションされたメッセージを読み込む chat:write チャットに書き込むために必要 im:history DMのメッセージを読み込むために必要(現状実装していないので今は使っていない) users:read データ投入時に名前からslack名を取得するために必要 users:read:email データ投入時にemailを使って照合するために必要 Scopesの設定 発行されたトークンを後述のlambdaの環境変数に設定するため、コピーしておきます。 API Gateway作成 slackAPIから Content-Type が application/x-www-form-urlencoded でイベントが送られてくるので、マッピングテンプレートを仕込んでおきます。 右上の統合リクエストを開く マッピングテンプレートのリクエスト本文のパススルーは「 リクエストの Content-Type ヘッダーに一致するテンプレートがない場合 」を選択し、 application/x-www-form-urlencoded マッピングテンプレートを作成し、 こちらの記事 にあるコードを貼り付けます。 Lambda作成 ひとまず @cgg-stamp-rally hello とメッセージを送ると「何?」と返すようにします。 import json import os import logging import urllib.request logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(data, context): # challenge # slack api との連携に必要 if ('challenge' in data): return { "statusCode": 200, "body": json.dumps({'challenge': data['challenge']}) } # slack event event = data["event"] # メンション時 if (event["type"] == "app_mention"): # helloコマンド if ("hello" in event["text"]): send_slack("何?") return { "statusCode": 200 } def send_slack(message): url = 'https://slack.com/api/chat.postMessage' token = os.environ['SLACK_TOKEN'] channel = "#specific_channnel" headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json; charset=utf-8' } method = 'POST' data = { "channel": channel, "text": message } json_data = json.dumps(data).encode("utf-8") req = urllib.request.Request(url=url, data=json_data, headers=headers, method=method) res = urllib.request.urlopen(req, timeout=5) tokenなど機密情報はLambdaの環境変数に設定しています。 challenge Slack Appで連携する際に、challengeパラメータを受け取って疎通確認を行います。 We’ll send HTTP POST requests to this URL when events occur. As soon as you enter a URL, we’ll send a request with a challenge  parameter, and your endpoint must respond with the challenge value.  Learn more. slack api Slack App Event Subscriptions設定 Slack Appを開き、 Add features and functionality の Event Subscriptions の Request URL に先ほど作成したAPIのエンドポイントを貼り付けて疎通確認します。 Subscribe to bot eventsに app_mention 、 message.channels を設定します。 message.im は不要・・・ 疎通確認 ここまででSlackとの疎通ができるようになりました! Lambdaを作り込む 最終的にコマンドで検索できるようにしたいので、検索ワードの抽出ができるようにしています。 また、helpコマンドと、指定のコマンドがない時はhelpコマンドを呼ぶように誘導するメッセージを出すようにしました。 botがメッセージを送るチャンネルも固定にしていたところを、メンションが呼び出されたチャンネルに投稿するように変更しました。 import json import os import logging import urllib.request logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(data, context): # challenge # slack api との連携に必要 if ('challenge' in data): return { "statusCode": 200, "body": json.dumps({'challenge': data['challenge']}) } # slack event event = data["event"] # メンション時 if (event["type"] == "app_mention"): # helpコマンド if ("help" in event["text"]): msg = '''`@CGGスタンプラリー help`:このメッセージを表示する `@CGGスタンプラリー hello`:何?と返す `@CGGスタンプラリー data`: 渡ってくるデータを返す(開発用) `@CGGスタンプラリー ref [検索文字]`:氏名・slack名で検索(複数指定でAND検索) ''' send_slack(event["channel"], msg) # helloコマンド elif ("hello" in event["text"]): send_slack(event["channel"], "何?") # どんなデータが渡ってくるか確認するため elif("data" in event["text"]): send_slack(event["channel"], data) # refコマンド elif ("ref" in event["text"]): # 検索文字を抽出 search_words = event["text"].split() # ワードからbotメンションを削除 del search_words[0] # ワードからrefコマンド文字列を削除 search_words.remove("ref") send_slack(event["channel"], search_words) # その他のコマンド else: send_slack(event["channel"], "意味がわかりません!helpでコマンドを確認してください!") return { "statusCode": 200 } def send_slack(channel, message): url = 'https://slack.com/api/chat.postMessage' token = os.environ['SLACK_TOKEN'] headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json; charset=utf-8' } method = 'POST' data = { "channel": channel, "text": message } json_data = json.dumps(data).encode("utf-8") req = urllib.request.Request(url=url, data=json_data, headers=headers, method=method) res = urllib.request.urlopen(req, timeout=5) 2. 画像合成部分をGoogle SpreadSheet+GAS(+S3)で作成 スタンプラリーは出欠と理解度確認クイズのスコアを基にスタンプの色を出し分けるようにしたいです。 そのため、それぞれの入力に対応してバッジをセットするGASを仕掛けてありますが、ここでは詳細説明を省略します。 画像合成は、大まかに以下の手順で進めます。 ベースになるhtmlを作成 管理シートのデータを基に名前やスタンプ画像を埋め込む html2canvas を用いてcanvasに変換 toDataURL("image/png") を用いてPNG画像に変換 管理シートを作成する 管理シートのカラムは以下の通りです。 name:実名 slack_name:slackの名前 image-url:合成した画像のURL #0attend:#0(講義のナンバリング)の出欠 meetの出席レポートから入力する #0score:#0のクイズのスコア #0badge:#0attendと#0scoreの値を加味して振り分けられたバッジ S3を作成し、CORS設定を行う スタンプの画像はS3でホストしておきます。 社内だけでなくグループ会社の方も使う予定のため、画像が閲覧できるようにパブリックに公開できる設定にしています。また、独自ドメインをあえて設定するような用途ではないため、CloudFrontは挟んでいません。 次に、CORSに Access-Control-Allow-Origin を設定します。 CORS設定 curlで叩いてheaderにキチンと含まれているか確認します。 ※ Access-Control-Allow-Origin: * にしてるので呼び出す側のoriginはなんでもいいです。 curl -i {S3のURL}/{画像名}.png -H "Origin:{GASの呼び出す側のURL}" --head HTTP/1.0 200 Connection established HTTP/1.1 200 OK . . . Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, HEAD Access-Control-Expose-Headers: Access-Control-Allow-Origin . . . バッジ画像リスト スプレッドシートに新しいシートを作成し、バッジのURLを参照できるようにしておきます。 メニューを作る 毎度GASを開いて実行するのは面倒なので、以下のようにワンクリックで呼び出せるように準備しておきます。 スプレッドシートのメニュー スクリプトファイルを作成し、メニューを追加します。関数を作成したら順次こちらに追加していきます。 // メニューに追加 function onOpen() { const sp = SpreadsheetApp.getActiveSpreadsheet() const myMenu = [ { name: 'メニューが動くか確認', functionName: 'testMenu_' } ] sp.addMenu('自動化ツール', myMenu) } function testMenu_() { Browser.msgBox('メニュー動く') } 土台となるhtmlを作成 スタンプラリーの土台のhtml ローカルでサクッと組みます。 <?=変数名 ?> とすることでHTML生成時に動的に値を埋め込むことができます。大体できたら、GASにindex.htmlで作成します。 GASでwebページをホストする 毎度デプロイで表示させてもいいですが、ちょっと面倒なのでスプレッドシートの右側に表示されるようにします。 先ほど作ったindex.htmlを基に HtmlService.createTemplateFromFile("index") でHtmlテンプレートオブジェクトを作成し、テンプレートの変数部分に値を挿入します。その後 evaluateメソッドを実行してHtmlOutputオブジェクトを生成します。 SpreadsheetApp.getUi().showSidebar(htmlOutput) でサイドバーに生成したHtmlOutputオブジェクトを表示させています。 HTMLサービスやClassUIについて詳しくは公式ドキュメントをお読みください。 https://developers.google.com/apps-script/reference/base/ui https://developers.google.com/apps-script/reference/html/html-service.html 引数の c と columns は、1行分のデータが入った配列とカラム名です。 この関数を各行に対してfor文で回すことで一気に画像生成ができる算段です。 繰り返す時は処理中の行番号とslack名をPropertiesServiceに入れています。 function createHtml_(c, columns) { const html = HtmlService.createTemplateFromFile("index") // 各行の内容を取得してhtmlを生成 for (const [i, v] of columns.entries()){ Logger.log(v) switch (v) { case 'name': html.NAME = c[i] break case '#1badge': html.BADGE_1 = c[i] break case '#2badge': html.BADGE_2 = c[i] break case '#3badge': html.BADGE_3 = c[i] break . . . default: break } } const htmlOutput = html.evaluate() SpreadsheetApp.getUi().showSidebar(htmlOutput) } 画像に変換する htmlは作成できたので、作成後 html2canvas を用いてcanvasに変換し、 toDataURL() でpng形式に変換します。テンプレートのinde.htmlを編集し、以下のようなコードになっています。 <!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.5/dist/html2canvas.min.js"></script> <style> * { margin: 0; padding: 0; line-height: 1; font-size: inherit; font-weight: normal; } .wrapper { width: 400px; padding: 1em; background-color: #e2e5ea; } header { text-align: center; margin-bottom: 0.5em; } header h1 { font-weight: bold; line-height: 2em; } .slack-profile { margin: 0 auto; } .slack-profile h2 { font-size: 1.5em; line-height: 3em; text-align: center; word-break: keep-all; } .stamp-rally table { margin: 0 auto; border-spacing: 1em 2em; } .stamp-rally table td { position: relative; width: 80px; height: 80px; border-radius: 0.3em; background-color: #fff; } .stamp-rally table td span { position: absolute; top: 105%; left: 0; width: 100%; height: 1em; text-align: center; word-break: keep-all; font-size: 0.6em; color: #5e6062; } .stamp-rally table td img.badge { width: 80%; margin: 10%; } </style> </head> <body> <div class="wrapper" id="stampCard"> <header> <h1>CGG 2022 スタンプラリー</h1> </header> <main> <div class="slack-profile"> <h2><?=NAME ?></h2> </div> <div class="stamp-rally"> <table> <tr> <td> <img class="badge" src="<?=BADGE_1 ?>" crossorigin="anonymous" /> <span class="description">THE AWS</span> </td> <td> <img class="badge" src="<?=BADGE_2 ?>" crossorigin="anonymous" /> <span class="description">設計GD</span> </td> <td> <span class="description">フロントエンド</span> <img class="badge" src="<?=BADGE_3 ?>" crossorigin="anonymous" /> </td> <td> <img class="badge" src="<?=BADGE_4 ?>" crossorigin="anonymous" /> <span class="description">暴れん坊コンテナ</span></td> </tr> <tr> <td><img class="badge" src="<?=BADGE_5 ?>" crossorigin="anonymous" /><span class="description">サーバーレス</span></td> <td><img class="badge" src="<?=BADGE_6 ?>" crossorigin="anonymous" /><span class="description">CI/CD</span></td> <td> <img class="badge" src="<?=BADGE_7 ?>" crossorigin="anonymous" /> <span class="description">オブザーバビリティ</span> </td> </tr> <tr> <td> <img class="badge" src="<?=BADGE_gd ?>" crossorigin="anonymous" /> <span class="description">障害対応WS</span></td> <td><img class="badge" src="<?=BADGE_quiz ?>" crossorigin="anonymous" /><span class="description">トリビアクイズ</span></td> <td> <img class="badge" src="<?=BADGE_lt ?>" crossorigin="anonymous" /> <span class="description">LT</span></td> </tr> </table> </div> </main> </div> </body> <script> window.onload = function() { const stampCard = document.querySelector("#stampCard") html2canvas(stampCard, {scale:2, allowTaint: true, useCORS: true, taintTest: true, logging: true}).then((ele) => { google.script.run.saveImage(ele.toDataURL("image/png")) }) } </script> </html> html2canvas を使用して指定したオブジェクトをcanvasに変換 <script src="<https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.5/dist/html2canvas.min.js>"></script> toDataURL を使ってcanvasをpng形式に変換 google.script.run.saveImage(png画像) は後述のS3に保存する場面で作成する関数 <script> window.onload = function() { const stampCard = document.querySelector("#stampCard") html2canvas(stampCard, {scale:2, allowTaint: true, useCORS: true, taintTest: true, logging: true}).then((ele) => { google.script.run.saveImage(ele.toDataURL("image/png")) }) } </script> CORSについて GASのhtmlに埋め込んだ画像はCORSに引っかかり、png変換時に出力されません。 その場合、まず画像URLのヘッダーに Access-Control-Allow-Origin を設定します。 自分でホストしている画像であれば、 Access-Control-Allow-Origin: * をヘッダーに含める Slackのプロフィール画像など、ヘッダーを弄れない場合は出力できないので諦める また、html2canvas実行時の useCORS を有効にします。 html2canvas(stampCard, {scale:2, allowTaint: true, useCORS: true, taintTest: true, logging: true}).then((ele) => { google.script.run.saveImage(ele.toDataURL("image/png")) }) さらに、HTML内の 全てのimgタグ に crossorigin="anonymous" を含めます。 <img class="badge" src="<?=BADGE_1 ?>" crossorigin="anonymous" /> ヘッダーに設定した Access-Control-Allow-Origin が消滅するときは以下の現象が起きていると考えられます。私の場合、 crossorigin="anonymous" を全てのimgタグに設定するとエラーが消えました。 S3の画像URLを使用している場合、かつ、Chromeでブラウザのcacheが有効になっている場合、2度目のアクセス時に Access-Control-Allow-Origin Headerの付いていないキャッシュしたレスポンスをChromeが返すためCORSエラーが発生してしまう模様 特定の条件でのみS3の画像URLでCORSエラーが発生する問題をなんとかする – Qiita 3. 合成した画像をS3にアップロードしてURLをSpreadSheetに保持する部分を作成 GASで作成した画像をDriveに保存してリンクを共有するとslack上で画像プレビューされないため、S3にアップロードします。 IAM S3FullAccessを指定し、IAMロールを作成したらアクセスキーとシークレットキーはPropertyServiceに保存しておきます。 S3にアップロードするライブラリ追加 aws-sdk-js を使用するので、GASにライブラリを追加します。 スクリプトID 1Qx-smYQLJ2B6ae7Pncbf_8QdFaNm0f-br4pbDg0DXsJ9mZJPdFcIEkw_ ただし、このライブラリは画像をアップロードする際に改造が必要になります。 GASプロジェクト内にファイルを新規作成し、それぞれ以下のファイルをGASプロジェクト内にコピペします。 https://github.com/eschultink/S3-for-Google-Apps-Script/blob/master/S3.gs https://github.com/eschultink/S3-for-Google-Apps-Script/blob/master/S3Request.gs コピペしたファイルを、以下の記事を参考に修正します。 https://note.com/marina1017/n/n431f0bb4e342 S3にアップロードし、アップロードした画像のURLをスプレッドシートに記録する関数作成 rowNum はfor文で createHtml_() を呼び出す際にPropertyServiceに保存した処理中の行番号です。 function saveImage(img) { // 情報を格納する行 const row = PropertiesService.getDocumentProperties().getProperty("rowNum") // 作成した画像のアップロード先S3の設定 const S3_ACCESS_KEY = PropertiesService.getScriptProperties().getProperty("S3_ACCESS_KEY") const S3_SECRET_ACCESS_KEY = PropertiesService.getScriptProperties().getProperty("S3_SECRET_ACCESS_KEY") const s3 = getInstance(S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY) const S3_BUCKET_NAME = "cgg-stamp-rally" // ファイル名を組み立てる const today = new Date() const name = PropertiesService.getDocumentProperties().getProperty("slackName").trim() const filename = `${("0" + today.getFullYear()).slice(-2)}${("0" + (today.getMonth() + 1)).slice(-2)}${("0" + today.getDate()).slice(-2)}${("0" + today.getHours()).slice(-2)}${("0" + today.getMinutes()).slice(-2)}-${name}` const a = img.replace('data:image/png;base64,', '') const decodedImg = Utilities.base64Decode(a) const imgblob = Utilities.newBlob(decodedImg, "image/png", `${filename}.png`) s3.putObject(S3_BUCKET_NAME, `${filename}`, imgblob, {logRequests: true}) const url = `{S3のURL}/${filename}` const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID') const s = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('2022'); s.getRange(Number(row), 4).setValue(url) } 画像をBlobオブジェクトにする必要があるため、data URI schemeのメディアタイプとエンコード方式を削除してからデコードし、メディアタイプとファイル名を指定してBlobを作成しています。 const a = img.replace('data:image/png;base64,', '') const decodedImg = Utilities.base64Decode(a) const imgblob = Utilities.newBlob(decodedImg, "image/png", `${filename}.png`) これをhtml中から呼び出しています。 処理中の行番号とスプレッドシートで持っているslack名をPropertyServiceに入れたのは、 saveImage() のようにhtmlテンプレート内部で呼び出す関数に変数を渡すのが大変なためです。 4. 作成したURLと名前などをセットにしてDynamoDBにアップロードする部分を作成 DynamoDBを使うためにライブラリ追加 GASからDynamoDBに直接データ投入を行いたいですが、認証を突破することが大変なので aws-apps-scripts を使います。メンテ状況を見て不安になりますが、動きました。 GASプロジェクト内にファイルを新規作成し、以下のaws.js内のコードをそのままコピペします。 https://github.com/smithy545/aws-apps-scripts/blob/master/aws.js IAM 権限は以下の2つ AmazonDynamoDBFullAccess batch-submit-policy アクセスキーとシークレットキーをGASのPropertyServiceに設定しておきます。 DynamoDBにデータを登録していく関数作成 DynamoDBを検索することを考えると同じ人のデータは更新していく方が望ましいため、updateメソッドを使います。 本来はDynamoDBに一括でアイテム登録した方がいいですが、ここではfor文で回しています。 function updateDynamodb_() { const DYNAMODB_ACCESS_KEY = PropertiesService.getScriptProperties().getProperty('DYNAMODB_ACCESS_KEY') const DYNAMODB_SECRET_ACCESS_KEY = PropertiesService.getScriptProperties().getProperty('DYNAMODB_SECRET_ACCESS_KEY') AWS.init(DYNAMODB_ACCESS_KEY, DYNAMODB_SECRET_ACCESS_KEY) const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID') const s = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('2022'); const data = s.getRange(2, 1, s.getLastRow(), s.getLastColumn()).getValues() const table = "cgg-stamp-rally" for (const [i, c] of data.entries()) { if (c[1]) { const item = { slackName: {S: c[1]}, name:{S: c[0]}, stampCardUrl: {S: c[3]} } const res = AWS.request( 'dynamodb', 'ap-northeast-1', 'DynamoDB_20120810.UpdateItem', {}, 'POST', { TableName: table, Item: item, Key: { 'slackName': item.slackName }, ExpressionAttributeNames: { '#n': 'name', '#url': 'stampCardUrl' }, ExpressionAttributeValues: { ':newName': item.name, ':newUrl': item.stampCardUrl }, UpdateExpression: 'SET #n = :newName, #url = :newUrl' }, { 'Content-Type': 'application/x-amz-json-1.0' }, ) const code = res.getResponseCode() const text = res.getContentText() if (code < 200 || code >= 300) throw Error(`AWS.request failed: ${code} - ${text}`) Logger.log(`OK: ${table} - ${JSON.stringify(item)}`) } } } 5. botが受け取った値を用いてDynamoDBを検索して該当のスタンプラリーカードURLを返却する部分を作成 lambdaのbot部分に戻り、検索条件に合ったスタンプラリーのURLを返却するようにします。 検索条件に合致するスタンプラリー画像URLを探す関数作成 複数の検索ワードが渡された場合、AND検索するようにします。 検索結果が複数ある場合や、データが見つからなかったときは文言を変えています。 def get_card(targets): dynamodb = boto3.resource('dynamodb') table = dynamodb.Table('cgg-stamp-rally') # 検索文字列をFilterExpressionに指定できる形に整形 fe = None for target in targets: if fe is None: fe = Attr('slackName').contains(target) | \ Attr('name').contains(target) else: fe = fe & Attr('slackName').contains(target) | \ Attr('name').contains(target) res = table.scan( FilterExpression = fe ) items = res['Items'] # 最後まで読み込む while 'LastEvaluatedKey' in res: res = table.scan( FilterExpression = fe, ExclusiveStartKey=resp['LastEvaluatedKey'] ) items.extend(res['Items']) if len(items) == 0: return '条件に合致するデータがありません' # 同姓同名の場合を考慮 if len(items) > 1: prospective_targets = [] for item in items: prospective_targets.append(item['slackName']+':'+item['stampCardUrl']) return "対象ユーザーの候補が複数あります\n" + "\n".join(prospective_targets) target_card = [] for item in items: target_card.append(item['stampCardUrl']) return "\n".join(target_card) FilterExpression で条件を指定してscan 結果がページネーションされている場合を考慮し、 LastEvaluatedKey でページを最後まで読み込む 最後にSlackから @cgg-stamp-rally ref hoge とコマンドを送り、対象のスタンプラリーが返ってくることを確認したら完成です! @cgg-stamp-rally ref hoge おわりに スタンプラリー制作はGAS上で行う画像合成部分が一番大変でした。 かなり荒削りな実装ですが、CGG開催期間中のみ利用する想定なので今の状態で運用しています。 スタンプラリーはかなり反響が高く、さらに副次的な効果としてスタンプラリーきっかけで始めたクイズも理解度が上がると好評となっています。 このクイズは社内研修で行われたジョブローテーション先の サービスインフラチーム の「伝授」の仕組みから着想を得て、勉強会に応用してみました。 CGGはグループを横断して行われる大規模な勉強会のため、テーマであるクラウドサービス以外にも様々な刺激を受けてもらえたらと考え試行錯誤しています。 今後の展望としては、まだまだ改善の余地はたくさんあるシステムなため開発環境の整備が完了次第、社内オープンソース化をして扱いやすくし、社内勉強会においてスタンプラリーが定着してくれたらいいなと考えています。 というわけで、以上がRyommサンタからのクリスマスプレゼントでした! 明日は、 14kw さんの Notionのなんか書く です。お楽しみに! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
この記事は、 ニフティグループ Advent Calendar 2022 21日目の記事です。 はじめに 会員システムグループ 第二開発チームの川上です。普段はニフティ会員向けiOS/Androidアプリの開発や運用を担当しています。 私のチームはスクラムで開発しており、GitHub Projects(classic)でタスク管理しています。スプリント内の進捗を管理する上でプランニングポーカーでつけたポイントを可視化したいという話がありました。そこで、定期的に自動更新するバーンダウンチャートをGAS(Google Apps Script)で簡易的に用意してみました。 本記事ではこのバーンダウンチャート作成で行った実装について紹介します。 要件 現時点までの残りポイントについて折れ線が表示される スプリント終了日までの予測ポイントについて折れ線が表示される 予測線は残日数の割合で計算 休日はポイント消費しない 定期的に自動更新する 構成 リソース管理や運用の手間を少なくするため、GASでタスクデータを取得して Looker Studio に表示しています。 前提 GitHubのIssueにはチーム独自の運用として下記が設定されており、一部実装はこの内容を前提としています。 Title タスクのタイトル 「:」後にポイントを記載 例) 「〇〇のテストを作成する:3」、「〇〇のインターフェースを追加する:2」 Milestones スプリントを設定 例)「Sprint5」、「Sprint11」 Labels チケットの種類を設定 「PR」「Epic」以外がポイント集計対象のタスク 例)「android」、「ios」、「PR」、「Epic」 実装 1. スプレッドシートを用意 スプレッドシートを新規作成してシートを追加して4つ用意します。 タスク一覧シート GitHubから取得した情報を保存しておくシート Sprint集計シート タスク一覧をスプリントごとに集計したシート 実際にLooker Studioから参照してバーンダウンチャート化する 設定用シート スプリント期間などの情報が記載されたシート 一時計算用シート GASから一時的に書き込む空のシート 2. GitHub API v4でデータ取得してスプレッドシートに収集する GitHub API v4でデータを取得するために、下記の関数を用意します。 function fetchGithubTasks() { const graphql_query = ` query { \ search(type: ISSUE, query: "is:issue org:organization_name project:project_name", last: 100) { \ issueCount \ nodes { ... on Issue { \ id \ milestone { title } \ number \ title \ closed \ closedAt \ createdAt \ author { login } \ assignees(first: 100){ nodes { login } } \ labels(first: 100){ nodes { name } } \ } \ } \ } \ } `; // スクリプトプロパティに登録されたトークンを取得 const token = PropertiesService.getScriptProperties().getProperty("GITHUB_ACCESS_TOKEN"); const option = { method: "post", contentType: "application/json", headers: { Authorization: "bearer " + token }, payload: JSON.stringify({ query: graphql_query }) }; return UrlFetchApp.fetch("https://api.github.com/graphql", option); } 「graphql_query」の文字列で指定している「organization_name」と「project_name」は環境にあった文字列に置き換えてください。また、定期実行されるまでの期間に100件以上更新されることがなかったため、一回の実行でIssueの取得件数は更新日時が新しい順に100件としています。 この関数を利用してデータを取得し、データ変換とスプレッドシートへの書き込みを行います。 const SPREAD_SHEET_ID = "スプレッドシートのID" const TASK_SHEAT_NAME = "タスク一覧シート" const TMP_SHEAT_NAME = "一時計算用のシート" const ignore_labels = [ "Epic", "PR", ] // タスク一覧を取得してスプレッドシートに書き込む関数 function updateTasks() { // GitHubからタスクを取得 const response = fetchGithubTasks() // レスポンスをタスク形式に変換して、一部のlabelに該当するタスクを除去 const result = JSON.parse(response) const sbis = result.data.search.nodes .map(x => convertToTask(x)) .filter(x => !ignore_labels.includes(x.label)) // keyとvalueを分離 const keys = Object.keys(sbis[0]) const records = sbis.map(x => Object.values(x)) // スプレッドシートに書き込み const sheet = SpreadsheetApp.openById(SPREAD_SHEET_ID).getSheetByName(TASK_SHEAT_NAME) const tmpSheet = SpreadsheetApp.openById(SPREAD_SHEET_ID).getSheetByName(TMP_SHEAT_NAME) // 取得したデータからスプレッドシートを更新 records.forEach(x => { // idから行番号を取得 const row = getRow(tmpSheet, TASK_SHEAT_NAME, x[0]) // IDが存在しない場合は新規追加 if (row == null) { sheet.appendRow(x) return } // 存在する場合は置き換え sheet.getRange(row, 1, 1, x.length).setValues([x]) }) } // スプレッドシートのQUERY関数でIDを検索する関数 // データが多くなったときに線形探索より高速 function getRow(tmpSheet, targetSheetName, id) { tmpSheet.getRange(1,1).setValue(`=QUERY({${targetSheetName}!A:A, ARRAYFORMULA(ROW(${targetSheetName}!A:A))},"WHERE Col1 = '${id}'")`) const row = tmpSheet.getRange(1, 2).getValue() return row != "" ? row : null } // GitHubから取得したデータを整形する関数 function convertToTask(item) { const title = item.title.split(':', 2)[0] const point = parseInt(item.title.split(':', 2)[1] ?? 0) const closedAtJST = item.closedAt ? Utilities.formatDate(new Date(item.closedAt), "JST", "yyyy-MM-dd HH:mm:ss") : undefined const createdAtJST = item.createdAt ? Utilities.formatDate(new Date(item.createdAt), "JST", "yyyy-MM-dd HH:mm:ss") : undefined return { id: item.id, title: title, point: point, closed: item.closed, closedAt: closedAtJST, author: item.author.login, assignee: item.assignees.nodes[0]?.login ?? "", label: item.labels.nodes[0]?.name ?? "", milestone: item.milestone?.title ?? "", createdAt: createdAtJST, } } GASのトリガーにupdateTasks関数を定期実行するように設定します。実行後は次のようなデータがスプレッドシートに書き込まれます。 3. Sprint用のデータに変換する タスク一覧シートにデータ取得できましたが、Looker Studioでバーンダウンチャートのようなグラフを表示するにはこのデータを元に値の加工が必要です。Looker Studio上でも値の加工はできますが、データソースにスプレッドシートを使う場合は複雑な加工ができません。そのため、スプレッドシート側の別シート(Sprint集計シート)で加工を行います。 また、タスク一覧シートの変更を即時にSprint集計シートに反映する処理が必要です。ただ、変更したデータの取得→加工→反映を愚直に実装するのは少し手間がかかるため、GASからはセルにスプレッドシート関数の文字列を書き込むことで実現します。 下記のコードは設定シートに記載された更新日を過ぎたら、次のスプリント日数分の行を追加して、各セルにスプレッドシート関数を埋め込んでいます。(実装を妥協しているので、シートのヘッダーが変わったら崩れてしまいます…) const CONFIG_SHEAT_NAME = "設定用シート" const ACTIVITY_SHEAT_NAME = "Sprint集計シート" // 設定用シートを読み込む関数 function readConfig(sheet) { const rows = sheet.getDataRange().getValues() // Configデータをマップに読み込み const config = {} rows.forEach( (x, i) => config[x[0]] = { value: x[1], index: i } ) return config } // 休日判定用の関数 function isHoliday(date) { // 土日 const day = date.getDay() if (day === 0 || day === 6) return true // 祝日 const id = 'ja.japanese#holiday@group.v.calendar.google.com' const cal = CalendarApp.getCalendarById(id) const events = cal.getEventsForDay(date) if (events.length) return true // その他休日 const otherHoliday = [ '12/28', '12/29', '12/30', '12/31', '01/01', '01/02', '01/03', ]; const fd = Utilities.formatDate(date, 'JST', 'MM/dd') return otherHoliday.some(value => value === fd) } // 次回のスプリントデータをスプレッドシートに書き込む関数 function nextSprint() { const configSheet = SpreadsheetApp.openById(SPREAD_SHEET_ID).getSheetByName(CONFIG_SHEAT_NAME) config = readConfig(configSheet) const today = new Date() // 更新日前は何もしない if (today < config.next_sprint_update_date.value) { return } // 設定シートのスプリント番号を更新、次回更新日を設定 const nextSprintNumber = config.current_sprint_number.value + 1 const nextUpdateTime = new Date(config.next_sprint_update_date.value.getTime()); nextUpdateTime.setDate(nextUpdateTime.getDate() + 7 * config.sprint_week_span.value) configSheet.getRange(config.current_sprint_number.index+1, 1+1).setValue(nextSprintNumber) configSheet.getRange(config.next_sprint_update_date.index+1, 1+1).setValue(Utilities.formatDate(nextUpdateTime, "JST", "yyyy-MM-dd HH:mm:ss")) const activitySheet = SpreadsheetApp.openById(SPREAD_SHEET_ID).getSheetByName(ACTIVITY_SHEAT_NAME) const lastRowNumber = activitySheet.getLastRow() const sprintStartDate = new Date(config.next_sprint_update_date.value.getTime()); sprintStartDate.setDate(sprintStartDate.getDate() + 1) // 前処理(日割計算のため、スプリントの実働日数を計算しておく) let working_day_num = 0 for (var d = new Date(sprintStartDate.getTime()); d <= nextUpdateTime; d.setDate(d.getDate() + 1)) { working_day_num = !isHoliday(d) ? working_day_num + 1 : working_day_num } // DailyActivityシートに次のスプリント分のデータを追加する let index = 0 let sprint_elapsed_day = -1 for (var d = new Date(sprintStartDate.getTime()); d <= nextUpdateTime; d.setDate(d.getDate() + 1)) { const currentRowNumber = lastRowNumber + index + 1 // 営業日判定 const is_business_day = !isHoliday(d) // スプリント経過日数(初日を0とする) sprint_elapsed_day = !isHoliday(d) ? sprint_elapsed_day + 1 : sprint_elapsed_day // 特定のmilestoneのうちdの日付に作成されたポイント const today_created_point = `=SUMIFS(${TASK_SHEAT_NAME}!$C:$C,${TASK_SHEAT_NAME}!$J:$J,">="&$A${currentRowNumber}, ${TASK_SHEAT_NAME}!$J:$J,"<"&($A${currentRowNumber}+1), ${TASK_SHEAT_NAME}!$I:$I,"="&$B${currentRowNumber})` // 特定のmilestoneのうちdの日付に完了したポイント const today_closed_point = `=SUMIFS(${TASK_SHEAT_NAME}!$C:$C,${TASK_SHEAT_NAME}!$E:$E,">="&$A${currentRowNumber}, ${TASK_SHEAT_NAME}!$E:$E,"<"&($A${currentRowNumber}+1), ${TASK_SHEAT_NAME}!$I:$I,"="&$B${currentRowNumber})` // 特定のmilestoneのうちdの日付まで作成されたポイント合計 const total_created_point = `=SUMIFS(${TASK_SHEAT_NAME}!$C:$C, ${TASK_SHEAT_NAME}!$J:$J,"<"&($A${currentRowNumber}+1), ${TASK_SHEAT_NAME}!$I:$I,"="&$B${currentRowNumber})` // 特定のmilestoneのうちdの日付まで完了したポイント合計 const total_closed_point = `=SUM($F${lastRowNumber+1}:$F${currentRowNumber})` // 予測線用の残りポイント(残日数の割合で計算) const focast_remaining_point = index == 0 ? `=$G${currentRowNumber}` : `=MAX(($I${currentRowNumber-1}+$E${currentRowNumber})-ROUNDUP(($I${currentRowNumber-1}+$E${currentRowNumber})/(${working_day_num}-$D${currentRowNumber})) * ($D${currentRowNumber}-$D${currentRowNumber-1}), 0)` // 残りポイント(未来の日付は空白を入力) const remaining_point = `=IF(TODAY()+1>A${currentRowNumber},MAX($G${currentRowNumber}-$H${currentRowNumber}, 0), "")` // スプレッドシートに書き込み activitySheet.appendRow([ Utilities.formatDate(d, "JST", "yyyy-MM-dd"), `Sprint${nextSprintNumber}`, is_business_day, sprint_elapsed_day, today_created_point, today_closed_point, total_created_point, total_closed_point, focast_remaining_point, remaining_point, ]) index += 1 } } 収集処理と同様にGASのトリガーにnextSprint関数を定期実行するように設定します。実行すると次のようなデータを書き込みます。 4. Looker Studioで表示する Sprint集計シート(下記の図ではdaily_activity)をLooker Studioのデータソースに追加して、折れ線グラフを作成します。フィルタ機能でmilestoneを設定することによりSprint単位に表示を絞ることができます。 設定を完了すると次のようなグラフが表示されます。 おわりに 今回はGAS + Looker Studioで簡易的なバーンダウンチャートを作る方法を紹介しました。バーンダウンチャートがあることで視覚的に進捗が把握しやすくなります。そして、スプリントゴールに間に合うかどうかを早めに見極めて、プロダクトオーナーへの相談や作業自体の見直しがしやすくなります。もしバーンダウンチャートを利用していない場合はぜひ導入を検討してみてください。 明日は、 @penpenpen さんの お金をかけずに学ぶRust です。お楽しみに! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
はじめに こんにちは。セキュリティチームの添野隼矢と申します。 近年、サイバー攻撃によるセキュリティ被害やApache Log4jの脆弱性の件などで、脆弱性やシークレット情報をスキャンすることが重要になってきています。 本記事では、脆弱性やシークレット情報のスキャンを手軽に実行できるツール「Trivy」について紹介したいと思います。 「Trivy」とは Trivyとは、コンテナイメージやファイルシステム、RemoteのGit Repository等の脆弱性やシークレット情報をスキャンできるツールです。 Trivyは他の脆弱性スキャンツールと比べ、バイナリを配置するだけで利用可能になる等、導入が容易で、実行もワンライナーで実行することができます。 また、TrivyはGitHub Actions、JenkinsなどのCIにも簡単に組み込めるように作られています。 Trivyがスキャンできるターゲット Container Image Filesystem Git Repository (remote) Virtual Machine Image Kubernetes AWS 最近、AMIやEBSスナップショットのスキャンに対応しました。( Trivy Now Scans Amazon Machine Images (AMIs) ) 参考: https://github.com/aquasecurity/trivy#installation Trivyのスキャン内容 使用しているOSのソフトウェアの依存関係(SBOM) 既知の脆弱性(CVE) IaC の問題と設定ミス 機密情報とシークレット情報 ソフトウェア ライセンス 参考: https://github.com/aquasecurity/trivy#installation Trivyの脆弱性スキャンについて Trivyの脆弱性スキャンは、trivy-dbと呼ばれているツールで作成されている脆弱性DBを参照して行われます。 6時間おきに、脆弱性DBが更新されていくため、最新の脆弱性情報で脆弱性スキャンをかけることができます。 Update interval Every 6 hours 引用元:https://github.com/aquasecurity/trivy-db 上記の脆弱性DBに更新があった際、スキャンコマンド初回実行時に以下のようなコマンドが流れ、自動で最新の脆弱性情報を取り込んでくれます。 INFO Need to update DB INFO DB Repository: ghcr.io/aquasecurity/trivy-db INFO Downloading DB... 35.54 MiB / 35.54 MiB [---------------------------------------------------] 100.00% 2.33 MiB p/s 15s Trivy実際に使ってみる 初めにTrivyをインストールします。 インストール方法は以下の公式に従って、インストールをします。 https://github.com/aquasecurity/trivy#installation コンテナイメージスキャン コンテナイメージをスキャンする際は、 trivy image [image名] でスキャンすることができます。 試しにベースイメージ Alpine Linux 3.4のPythonイメージに対してスキャンしてみます。 ※出力結果は長いため、記載は一部のみにしています。 $ trivy image python:3.4-alpine python:3.4-alpine (alpine 3.9.2) Total: 37 (UNKNOWN: 0, LOW: 4, MEDIUM: 16, HIGH: 13, CRITICAL: 4) ┌──────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐ │ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ ├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ │ expat │ CVE-2018-20843 │ HIGH │ 2.2.6-r0 │ 2.2.7-r0 │ expat: large number of colons in input makes parser consume │ │ │ │ │ │ │ high amount... │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2018-20843 │ │ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤ │ │ CVE-2019-15903 │ │ │ 2.2.7-r1 │ expat: heap-based buffer over-read via crafted XML input │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-15903 │ ├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ │ libbz2 │ CVE-2019-12900 │ CRITICAL │ 1.0.6-r6 │ 1.0.6-r7 │ bzip2: out-of-bounds write in function BZ2_decompress │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-12900 │ ├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ │ libcrypto1.1 │ CVE-2019-1543 │ HIGH │ 1.1.1a-r1 │ 1.1.1b-r1 │ openssl: ChaCha20-Poly1305 with long nonces │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-1543 │ │ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤ 出力結果を確認すると37件の脆弱性があることがわかります。 37件の脆弱性の内訳にCRITICAL、HIGH、MEDIUM、LOW、UNKNOWNが書かれていると思います。 こちらは、共通脆弱性評価システムCVSS(Common Vulnerability Scoring System)によって評価された脆弱性のスコアをもとに設定されているものです。 現在、最新版のCVSSバージョン3での各レベルのスコアは以下の通りです。 CRITICAL(9.0~10.0)、HIGH(7.0~8.9)、MEDIUM(4.0~6.9)、LOW(0.1~3.9)、UNKNOWN(未確認) スコアの算出方法など詳しくは こちら をご覧ください。 --severity オプションを使用することで、上記の出力結果から脆弱性のレベルで絞ることもできます。 実行例(CRITICAL,HIGHで絞ってみた例) $ trivy image --severity CRITICAL,HIGH python:3.4-alpine python:3.4-alpine (alpine 3.9.2) Total: 17 (HIGH: 13, CRITICAL: 4) ┌──────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐ │ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ ├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ │ expat │ CVE-2018-20843 │ HIGH │ 2.2.6-r0 │ 2.2.7-r0 │ expat: large number of colons in input makes parser consume │ │ │ │ │ │ │ high amount... │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2018-20843 │ │ ├────────────────┤ │ ├───────────────┼──────────────────────────────────────────────────────────────┤ │ │ CVE-2019-15903 │ │ │ 2.2.7-r1 │ expat: heap-based buffer over-read via crafted XML input │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-15903 │ ├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ │ libbz2 │ CVE-2019-12900 │ CRITICAL │ 1.0.6-r6 │ 1.0.6-r7 │ bzip2: out-of-bounds write in function BZ2_decompress │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-12900 │ ├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ ファイルシステムスキャン 次にファイルシステムをスキャンする際は、 trivy fs または trivy rootfs でスキャンすることができます。 trivy fs と trivy rootfs の違い fsコマンド ローカルプロジェクトに対するスキャン 参考: Filesystem roofsコマンド ホストマシン、仮想マシンイメージ、展開されたコンテナイメージのファイルシステムなどに対するスキャン 参考: Rootfs 試しにDjangoをインストールしたPythonの仮想環境のプロジェクトを用意してみました。 $ cat Pipfile [[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [packages] django = "==4.1.4" [dev-packages] [requires] python_version = "3.8" 上記のプロジェクトに対して、スキャンしてみます。 ※実行すると、実行時刻が実行結果のINFOの左列に出ますが、ここでは消しています。 $ trivy fs /path/to/project/ INFO Vulnerability scanning is enabled INFO Secret scanning is enabled INFO If your scanning is slow, please try '--security-checks vuln' to disable secret scanning INFO Please see also https://aquasecurity.github.io/trivy/v0.35/docs/secret/scanning/#recommendation for faster secret detection INFO Number of language-specific files: 1 INFO Detecting pipenv vulnerabilities... 上記のようなコマンドが出力された後に、なにも脆弱性情報が出なかった場合は、スキャンで脆弱性が見当たらなかったということになります。 また単一ファイル(Pipfile.lockなど)に対してもスキャンすることが可能です。 $ trivy fs /path/to/project/Pipfile.lock INFO Vulnerability scanning is enabled INFO Secret scanning is enabled INFO If your scanning is slow, please try '--security-checks vuln' to disable secret scanning INFO Please see also https://aquasecurity.github.io/trivy/v0.35/docs/secret/scanning/#recommendation for faster secret detection INFO Number of language-specific files: 1 INFO Detecting pipenv vulnerabilities... ここで一時的にDjangoのバージョンを4.0.5(脆弱性が発見されているバージョン)に落としてみて、再度スキャンをしてみます。 $ pipenv install django==4.0.5 $ cat Pipfile [[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [packages] django = "==4.0.5" [dev-packages] [requires] python_version = "3.8" $ trivy fs /path/to/project/Pipfile.lock INFO Vulnerability scanning is enabled INFO Secret scanning is enabled INFO If your scanning is slow, please try '--security-checks vuln' to disable secret scanning INFO Please see also https://aquasecurity.github.io/trivy/v0.35/docs/secret/scanning/#recommendation for faster secret detection INFO Number of language-specific files: 1 INFO Detecting pipenv vulnerabilities... Pipfile.lock (pipenv) Total: 3 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 2, CRITICAL: 1) ┌─────────┬────────────────┬──────────┬───────────────────┬──────────────────────┬─────────────────────────────────────────────────────────────┐ │ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ ├─────────┼────────────────┼──────────┼───────────────────┼──────────────────────┼─────────────────────────────────────────────────────────────┤ │ django │ CVE-2022-34265 │ CRITICAL │ 4.0.5 │ 3.2.14, 4.0.6 │ python-django: Potential SQL injection via Trunc(kind) and │ │ │ │ │ │ │ Extract(lookup_name) arguments │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-34265 │ │ ├────────────────┼──────────┤ ├──────────────────────┼─────────────────────────────────────────────────────────────┤ │ │ CVE-2022-36359 │ HIGH │ │ 3.2.15, 4.0.7 │ An issue was discovered in the HTTP FileResponse class in │ │ │ │ │ │ │ Django 3.2... │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-36359 │ │ ├────────────────┤ │ ├──────────────────────┼─────────────────────────────────────────────────────────────┤ │ │ CVE-2022-41323 │ │ │ 3.2.16, 4.0.8, 4.1.2 │ python-django: Potential denial-of-service vulnerability in │ │ │ │ │ │ │ internationalized URLs │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-41323 │ └─────────┴────────────────┴──────────┴───────────────────┴──────────────────────┴───────────────────────────────────────────────────────────── 4.0.5にバージョンを下げてスキャンしたところ、3件の脆弱性が検知されました。 3件の脆弱性が検知されることの確認が終わりましたので、バージョンを元に戻します。 $ pipenv install django==4.1.4 終わりに Trivyには、ライセンスチェックやシークレット情報のチェック、出力形式の指定、また記事の最初の方で触れたAWSのAMIやEBSスナップショットのスキャンなど、本記事で紹介していない部分がまだまだあります。 今後も引き続き紹介していければと思います。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
この記事は、 ニフティグループ Advent Calendar 2022  22日目の記事です。 こんにちは!ニフティ株式会社の上原です。 個人的にRustという言語にハマっており、社内でもRustを学ぶ勉強会を主催しています。 プログラミング言語を学習する際にはまずは書籍を買って勉強される方も多いと思うのですが、お金がかかってしまいます。 Rustは入門者向けのWeb上の資料が充実しているため、書籍を買わずとも学習することができます。 ここでは無料で見られる教材についていくつかご紹介します。 教材 Tour of Rust Rustで出てくる概念の説明とソースコードが例示され、実際にコードを手元で実行することができる教材です。 私はいきなりThe Rust Programming Language(後述)から始めてしまったのですが、まずはここから始める方が良かったかなぁと思いました。 ローカルにRustの開発環境を整えることなく、ブラウザ一つあれば気軽に学習を進めることができることから、まずはTour of Rustからやるのがいいと思います。 Tour of Rustの変数についてのページ。概念の説明とコード例が示され、コードを実行することもできる。 https://tourofrust.com/00_ja.html The Rust Programming Language 公式から出ているRustの入門書で、よくTRPLと呼ばれています。 入門書と言われてはいますがカバーしている範囲は幅広く、この教材だけでRustのなんたるかは理解できるかと思います。 その分、インプットする量は多いのですがコマンドラインツールやWebサーバを作る回もあるので、実際に手を動かしながら学ぶこともできます。 また、メモリやHTTPなどのRust以外の話にも触れられており、参考になります。 難点としては、とても丁寧に文章が書かれていて、結局何を言いたいのかわからなくなってしまうことがあったり、日本語訳がところどころおかしな場所もあったりするところです。 そういう時は他のネットの記事を読んでみたり、原文の英語で読むと理解が進むかもしれません。 大体11章までやればRustの基本的な部分は抑えられると思います。 ちなみに社内勉強会でもTRPLを使っており、全て終わるのに1年かかるくらい濃密な内容となっています。 Ther Rust Programming Languageのページの一例。懇切丁寧に説明してくれる。 コード例に現れるferrisという蟹のキャラクターが可愛い。 https://doc.rust-jp.rs/book-ja/ Rust By Example TRPLと異なりこちらはコード中心の教材になります。例示されたコードを通してRustの各概念を学んでいきます。 TRPLで勉強した部分を簡単におさらいしたいときに使っています。 Rust By Exampleの所有権とムーブについて説明しているページ。コード例を見て学ぶことができる。 https://doc.rust-jp.rs/rust-by-example-ja/ rustlings Rustの文法練習問題集です。 そのままではコンパイルできないコードが幾つか用意されており、コンパイルできるようにソースコードを修正していく中でRustの諸概念を習得することができます。 これだけだと他の言語の教材にもあるかもしれませんが、rustlingsというコマンドをインストールしてやると進捗状況や正解かどうかが で表示されるのでゲーム感覚で楽しく学ぶことができるのでおすすめです。 TRPLの章を終わらせるごとにrustlingsで力試しすると理解がさらに深まります。 始め方は、rustlingsというディレクトリができてもいいディレクトリで以下のコマンドを実行します。 $ curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash エディタでrust-analyzerによる補完を効かせたい方は以下のコマンドを実行しておきましょう。 $ cd rustlings $ rustlings lsp rustlings watchでスタートです $ cd rustlings $ rustlings watch rustlingsをしている様子。rustlings watchを叩けばあとは指示通り進めていくだけ。 https://github.com/rust-lang/rustlings まとめ 現在はネットでも十分に情報を集め、無料でプログラミング言語を学べる時代になりました。 2023年を迎えるにあたり新しいことを始めるのに、Rustの学習を一つ候補に入れていただき、この記事を参考にいただければ幸いです。 明日は @kinari321 さんです!楽しみですね。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
こんにちわ。11/22にニフティ初のオンラインイベント「NIFTY Tech Day 2022」を開催しました。皆様ご覧いただけましたでしょうか? 今回は、当日の内容について紹介します。 概要 サービス開始から35年、ニフティは常に技術者やIT技術と共にありました。 高い好奇心で新しい技術トレンドをいち早く導入し、システムやサービスへの活用を常に模索しています。ニフティが創業時から変わらないのは、好奇心と挑戦し続けること。 この”NIFTY Tech Day”を通じて、私たちの今と未来をお伝えします。 公式サイト 今回のタイムテーブルとセッション紹介を見ることができます。 また謎解きページもあるのでチャレンジしてみてください! ↑ロゴとサイトはテクニカル・SF・近未来感をイメージして社内の制作チームが作成 セッション 各セッションごとに紹介文と動画を載せていますので見逃した方もう一度見たい方はぜひチェックしてください! 最近のインターネット動向と未来のネットワークについて ニフティのビジネスパートナー NTTコミュニケーションズ株式会社の森信様 をお招きした特別セッションです。最近のインターネット動向、そしてまったく新しい未来の技術までご紹介いただきました。 スクラムのハードルの越え方〜リリース数を1.5倍にするまで〜 デブサミウーマン 登壇者による「スクラムのハードルの越え方」についてのセッションです。 社内8チームに対しスクラム導入を支援してきた中でわかったことについて紹介しました。 複数プロダクトを抱えて行うスクラム開発のこれまでとこれから 持っているプロダクトが複数あってPOも複数必要。スクラム開発のアンチパターンのようにも見えますがコミュニケーションで解決できました。 それまでの道のりについて紹介しました。 クリーンアーキテクチャはこの3年間で私たちのチームに何をもたらしたのか ニフティのオプションサービスの設計開発にクリーンアーキテクチャを導入しました。私たちだからこそ語れるクリーンアーキテクチャのあれこれを実体験を交えて紹介しました。 なぜニフティはLeSSを選んだのか。 @nifty MAX光における大規模スクラム開発体験談 ニフティの新規接続サービス @nifty MAX光 は大規模スクラムLeSSで作られました。開発の裏側についてパネルディスカッション形式で語りました。 AWS/GCPとSlackを駆使したオフィスの固定電話廃止への取り組み オフィスの固定電話を廃止するため、AWS/GCPとSlackを駆使して、サーバレスな自動電話音声応答システムを開発した話を若手エンジニアがカジュアルに紹介しました。 オンライン会議を盛り上げる!音声リアクションツール「もじこえ」の開発 ニフティには技術を学べる環境、便利ツールを作ってみるという文化があります。 作成したオンライン会議のリアクション不足を解決するツールを例にツールを作るときの考え方や構成について紹介しました。 安心・安全な@niftyメールサービスの裏側 ニフティでは @nifty メールサービス を提供しています。 本セッションでは、お客様に安心してサービスをご利用いただくための取り組みについて紹介しました。 セシール事業におけるモノリシックアーキテクチャとの向き合い方 「ビジネスを加速させたい」 その思いを実現するためには、さまざまな課題があります。 本セッションでは、そのような課題に対して、セシールがどう向き合っていったかを紹介しました。 CI/CDを導入して変わったモバイルアプリ開発体制 ニフティのグループ会社であるニフティライフスタイル株式会社のセッションです。 CI/CDツールの導入による「モバイルアプリ開発の効率化」について紹介しました。 深層自然言語処理によるニュース記事要約の手法と実装 ニフティのスペシャリスト制度である N1! の機械学習エンジニアが自然言語処理について解説。 本セッションではニフティニュースにおける記事要約や構築した深層自然言語処理モデル、AWSを用いた実際のサービス実装について紹介しました。 ニフティエンジニア徹底分析 休憩中に流していたニフティエンジニアのアンケート結果を少しだけお見せします。 開発・運用どちらも担当している人が多いんです! ニフティではサービスによって様々なプログラミング言語が使用されています。 最近では社内でRustの勉強会も行われていました! ほとんどのニフティエンジニアは新しい技術に「 関心があり 」と回答しました。 導入した結果を紹介しているセッションもありますのでぜひチェックしてみてください! 様々な特徴が出ました。仕事に対する姿勢を表したワードが目立ちますね。 こんなアンケートもありました! (ちなみに私はきのこ派です。) 技術者交流会 技術者交流会とは「技術者同士で喋る」ことを目的とした懇親会です。 社内のエンジニアとTech Day 2022にご参加いただいた方とオンラインで技術者交流会を実施しました。 ニフティでは、技術者との交流を推進しており、その活動の一環として開催が決定しました。 Tech Day 2022の登壇者のAsk the Speakerが行われたり、技術に関連する話で盛り上がったりと参加された方からは好評でした。 ↑Remoで作成した会場の様子 まとめ NIFTY Tech Day 2022 では、ニフティのサービス・技術を紹介してきました。 配信を通して少しでもニフティの魅力を知ってもらえますと幸いです。 またクロージングで話がありましたが、来年秋に NIFTY Tech Day 2023 を開催いたします。 そちらもぜひご期待ください! ニフティグループでは一緒に働く仲間を募集中です ニフティ株式会社 新卒採用、キャリア採用を実施しています。ぜひ リクルートサイト をご覧ください。 ニフティエンジニアが業務で学んだことやイベント情報を エンジニアブログ にて発信しています! ニフティライフスタイル株式会社 想像以上を、みつけよう。 をコーポレートメッセージに、“一人ひとり”のライフスタイルを便利で豊かにするため、ニフティライフスタイルのエンジニアは日々開発をしています。 採用情報 や ニフティライフスタイル Tech Blog をご覧ください。 株式会社セシール セシールでは「ECシステムの新規開発」「基幹システム開発・運用」「インフラ設計構築~運用」についてキャリア採用を行っています。 詳しくは キャリア採用募集要項 をご覧ください。 ニフティエンジニアのTwitterアカウントを作りました NIFTY Tech Talkのことや、ニフティのエンジニアの活動を発信していきます。 https://twitter.com/NIFTYDevelopers
アバター
この記事は、 ニフティグループ Advent Calendar 2022 18日目の記事です。 はじめに 基幹システムグループ サービスインフラチームの南川です。 普段はユーザーサインアップやシングルサインオン、顧客管理システム等の開発や運用を担当しています。 今回は、 Amazon CloudWatch Logs に出力するログの形式について説明します。 Amazon CloudWatch Logs Amazon CloudWatch Logs は、 AWS リソースや AWS 上で実行するアプリケーションからのログファイルをモニタリング、保存、アクセスできるサービスです。 CloudWatch Logs に出力するログ形式 結論から言うと JSON 形式 にしておくと、検索や分析する際に楽です。 今回はチャットツールのログを例として、ログが JSON 形式でない場合と JSON 形式である場合でどのような違いがあるかを取り上げます。 例:チャットツールのログ (JSON 形式でない場合) まず、ログとして出力されるデータは以下のようになっています。 先頭にログのレベル、その後ろにログのメッセージが記載されています。 [INFO] Taro posted "hoge" from 1.2.3.4 [INFO] Jiro posted "fuga" from 5.6.7.8 [INFO] Taro posted "piyo" from 1.2.3.4 [INFO] Jiro posted "hello" from 9.10.11.12 [ERROR] Saburo couldn't post from 1.1.1.1 投稿成功時 (1-4行目) INFO レベルで「<ユーザー名> posted “<本文>” from <IPアドレス>」 エラー発生時 (5行目) ERROR レベルで「<ユーザー名> <エラーメッセージ> from <IPアドレス>」 CloudWatch Logs にこれらのログを出力すると以下のようになります。 CloudWatch Logs に出力されたこれらのログは、 CloudWatch Logs Insights を用いて検索・分析することができます。 CloudWatch Logs Insights でログを検索、分析する CloudWatch Logs Insights では、クエリを使ってログデータを検索・分析・データの抽出ができます。 それでは、先ほどの CloudWatch Logs に出力したログに対して、 CloudWatch Logs Insights でデータを抽出してみます。 例:ログイベントを取得する まずは、簡単な例として最新20件のログイベントのタイムスタンプとメッセージを新しい順で取得するクエリを実行します。クエリとその実行結果は以下の通りです。 fields @timestamp, @message | sort @timestamp desc | limit 20 下部の実行結果にはCloudWatch Logsに出力されたログのタイムスタンプとメッセージが表示されています。クエリは複数のコマンドで構成されており、それぞれのコマンドがパイプ文字 ( | ) で区切られています。クエリ内で使えるコマンドについては、 CloudWatch Logs Insights のクエリ構文 を参照してください。今回はこのクエリで使われているコマンドについて簡単に説明します。 fields @timestamp, @message 1行目の fields コマンドは、クエリ結果の特定のフィールドを表示するコマンドです。例のクエリでは @timestamp と @message の値をクエリ結果に表示するようにしています。このコマンドで指定できるフィールドは サポートされるログと検出されるフィールド を参照してください。 sort @timestamp desc 2行目の sort コマンドは、特定のフィールドについてソートするコマンドです。例のクエリでは @timestamp について降順 (desc) ソートしています。 limit 20 3行目の limit コマンドは、クエリで返すログイベントの上限数を指定するコマンドです。例のクエリでは検索結果 (ログイベントの数) を20件まで返すように指定しています。 例:投稿に成功したログイベントの投稿者名とメッセージとIPアドレスを取得する 次に、投稿に成功したイベントを抽出し、そのイベントの投稿者名とメッセージとIPアドレスを表示するクエリを実行します。クエリとその実行結果は以下の通りです。 投稿者名、メッセージ、IPアドレスはそれぞれ userName, body, ipAddress フィールドに格納されています。 fields @message | parse @message "[*] *" as loggingType, loggingMessage | filter loggingType = "INFO" | filter loggingMessage like /\w+ posted ".*" from \d+\.\d+\.\d+\.\d+/ | parse loggingMessage "* posted \"*\" from *" as userName, body, ipAddress | display @message, loggingType, loggingMessage, userName, body, ipAddress parse @message "[*] *" as loggingType, loggingMessage parse loggingMessage "* posted \"*\" from *" as userName, body, ipAddress 2,5行目の parse コマンドは、フィールドの値からでデータを抽出し、クエリ (後続のコマンド) で使える一時的なフィールドを作成するコマンドです。例の2行目のクエリでは、 @message において "[*] *" の各 * に該当する値が as の後ろの各フィールド (1つ目の * の箇所は loggingType 、2つ目の * の箇所は loggingMessage) に格納されます。例えば、 @message が 「 [INFO] Taro posted "hoge" from 1.2.3.4 」 の場合、 loggingType は 「 INFO 」 、 loggingMessage は 「 Taro posted "hoge" from 1.2.3.4 」 となります。 filter loggingType = "INFO" filter loggingMessage like /\w+ posted ".*" from \d+\.\d+\.\d+\.\d+/ 3,4行目の filter コマンドは、1つ以上の条件を満たすイベントを取得するコマンドです。例のクエリでは、 loggingType の値が “INFO” であるイベント(3行目)と、 loggingMessage の値が 「 \w+ posted ".*" from \d+\.\d+\.\d+\.\d+ 」 というパターンの正規表現にマッチしているイベント(4行目)を取得しています(4行目の filter コマンドが無くても投稿成功イベントを抽出できなくはないですが、正規表現でも判定できる例として追加しています)。 display @message, loggingType, loggingMessage, userName, body, ipAddress 5行目の display コマンドは、クエリ結果の特定のフィールドを表示するコマンドです。例のクエリではカンマ区切りで列挙されたフィールド (@message, loggingType, loggingMessage, userName, body, ipAddress) の値を表示しています。 このように、ログを構造化 (JSON 形式で記述) していない場合、ログから投稿文、投稿者名、IPアドレスを抽出するために5,6行程度のクエリを書く必要があります。 ログを JSON 形式で構造化する 先ほどのログを JSON 形式で構造化してみます。構造化した一例は以下の通りです。 {"level":"INFO","user_name":"Taro","body":"hoge","ip_address":"1.2.3.4"} {"level":"INFO","user_name":"Jiro","body":"fuga","ip_address":"5.6.7.8"} {"level":"INFO","user_name":"Taro","body":"piyo","ip_address":"1.2.3.4"} {"level":"INFO","user_name":"Jiro","body":"hello","ip_address":"9.10.11.12"} {"level":"ERROR","user_name":"Saburo","body":"couldn't post","ip_address":"1.1.1.1"} CloudWatch Logs にこれらのログを出力すると以下のようになります。 この構造化されたログから、先ほどと同様の投稿者名とメッセージとIPアドレスを取得するクエリを書くと以下のようになります。投稿者名、メッセージ、IPアドレスはそれぞれ user_name, body, ip_address フィールドに格納されています。 fields @message, level, user_name, body, ip_address | filter level = "INFO" ログの本文 (@message) が JSON 形式である場合、 parse コマンド不要で JSON フィールドの値を、キー名を指定して参照することができます。これにより、構造化されていない時に比べ、クエリの行数を削減することができました。 また、ネストが深く複雑な JSON でも、ドット表記を使用して JSON フィールドにアクセスすることも可能です。 参考: サポートされるログと検出されるフィールド – Amazon CloudWatch Logs 構造化ログのデメリット ログを JSON 形式で出力する場合、構造化されていない時に比べ、ログのサイズが大きくなる傾向があります。また、 CloudWatch Logs では、ログの取り込み、保管、分析によって 1 GB ごとに料金が発生します。つまり、 JSON 形式でログ出力すると、 JSON形式で出力されていない時に比べて、 CloudWatch Logs のコストがかかることがあります。 JSON 形式でログを出力する際は、すべてのデータをログ出力するのではなく、必要なデータを取捨選択するなどの工夫が必要です。 おわりに 今回は CloudWatch Logs のログ形式について書きました。CloudWatch Logs に格納するログは JSON 形式 (構造化ログ) にしたほうが、 Logs Insights のクエリが簡潔になり、検索・分析・データ抽出が楽になります。しかし、ログサイズの肥大化に伴い、コストが増えることもあるので、不必要なデータは出力しないなどの工夫が必要です。皆さんも AWS 上にアプリケーションをデプロイする際は、ログ形式を JSON にすることを検討してみてください。 明日は、 yt_glaceon さんの担当です。 お楽しみに! 参考 Amazon CloudWatch Logs とは – Amazon CloudWatch Logs CloudWatch Logs Insights を使用したログデータの分析 – Amazon CloudWatch Logs サポートされるログと検出されるフィールド – Amazon CloudWatch Logs CloudWatch Logs Insights のクエリ構文 – Amazon CloudWatch Logs Amazon CloudWatch Pricing – Amazon Web Services (AWS) CloudWatch の料金を理解して今後の料金を削減する We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
この記事は、 ニフティグループ Advent Calendar 2022 17日目の記事です。 こんにちは、会員システムグループのたけろいどです。アドベントカレンダーも中盤になってきました。一年が早い… 概要 本記事ではエンジニアチームとデザインチームの連携について詳しく書いていきます。 ニフティでサービス開発する際はエンジニアチーム・デザインチーム・企画チームが協力してサービスを作り上げています。 しかし協力といっても当時は頻繁にコミュニケーションをとれていませんでした。とくにエンジニアチームとデザインチームのコミュニケーションは少なかったです。 その状態を改善するために行なっていることを紹介します! いままでの流れ まずはいままでのサービス開発の流れを紹介します。 従来は企画チームがデザイナーチームに依頼してデザイン作成を行い、その後にエンジニアチームがデザインを見ながら開発をする流れでした。 図にするとこうなります。これを便宜上、旧フローと呼びます 旧フローのメリット 企画チームが進捗把握しやすい エンジニア視点では1対1の関係でフローがわかりやすい 旧フローのデメリット 小さな修正でも企画を通すためリリースまでのスピードが落ちる 高機能なサービス開発には不向き HTML・CSS・JSでできることは数年前の比ではありません。リッチなWEBページを作ることも多くなり、確認事項はどんどん増えていました。その状況下で旧フローだと認識の齟齬が発生しやすくサービス提供までのスピードが落ちてしまうのは自明でした。 これからの流れ そこでそのフローに手を加えることにしました。 デザイナー・エンジニアが一緒になって作る体制を整え、そこにデザイン依頼を投げてもらう形です。ここでは新フローと呼びます。 新フローのメリット デザインにエンジニア視点が入り、振る舞いについて理解が深まる デザイナーの意図を理解した上で開発に取り組める 新フローのデメリット 企画が進捗把握しづらい デザイナーとエンジニアの関心ごとが異なりコミュニケーションがうまくいかない 他にも細かな課題は残されていますが、基本的にはいままでのコミュニケーション不足からなるものです。次のセクションではデメリットとどう向き合っているか書いていきます。 デメリット向き合い方 主にデザイナー・エンジニアの会話をふやすことを目標にしています。企画とはスクラム開発を通じて進捗確認など行なっています。それぞれ詳しく書いていきます。 スクラム開発 スクラム開発はスプリントレビューがあるため進捗を細かい単位で確認できます。またスプリントレビューはコミュニケーションの場でもあります。作ったものを見ながらレビューするため互いの認識を合わせるにとても役に立ちます。 特にサービス開発始めは文言や動作などで細かな齟齬が多発しがちです。それをタスク化し修正を都度行っていけるスクラム開発はエンジニアとしても助かっています。 スクラム開発をしていくことでコミュニケーションが増え、より良いサービス開発ができています。 フロントエンド知見共有会 ニフティではフロントエンド知見共有会という社内勉強会を毎週開催しています。フロントエンドに興味のある人が集まり、自由にLT・雑談をするという会です。当初はエンジニアが集まりフロントエンドについてエンジニア同士でお話しをしていました。 この場にデザイナーの方を招くことで知見を共有してもらい互いのことを知ってもらうことができています。先日はカラーユニバーサルの話題でした。ニフティでもカラーユニバーサルを意識していることやそれをデザインに落とし込む時の注意点などを教えてもらいました。 互いの価値観について知る場としてとても有効に働いています。 デザインシステム輪読会 現在は企画段階ですがデザインシステム輪読会を開こうと考えてます。 こちら の本を輪読しデザイナー・エンジニアともに同じ知識をつけていこうという狙いです。 またデザインシステムは効率の良いデザインを作成できるだけでなく会社のブランディングを高めることもできます。こういった活動をデザイナー・エンジニア共にしてコミュニケーションを深めていきたいと思ってます。 デザインシステム輪読会の活動は別のブログで書いていこうかなと思います。(次回予告 まとめ サービス開発でのエンジニアがどうデザイナー・企画と連携しフロントエンド改善に向かっているのか書きました。 まずは密にコミュニケーションをとり、互いにリスペクトし合える環境を整えていっています。まだまだ課題は山積みですがお客様目線のサービスを素早く提供できるように頑張っています。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering 明日は、 @mh36 さんの記事です。 Amazon Cloud WatchLogsのお話しみたいです。楽しみです!
アバター
この記事は、 ニフティグループ Advent Calendar 2022  16日目の記事です。 初めに 最近、アニメを見る欲が復活した会員システムグループの2年目社員の関です。 数ヶ月前に以下のブログ記事を書きました。 主催した社内勉強会の課題でアクセシビリティ的に優れているTODOリストの課題を出した話 上記はアクセシビリティ的に良いTODOリストを勉強会の課題として出したという記事でした。 その勉強会の最終課題としてアクセシビリティに優れたツールバーの課題を出したので今回はその話をしようと思います。 勉強会については前回のブログ記事に記述してあるのでそちらを確認ください。 勉強会の 最終課題の概要 早速、勉強会の最終課題について説明します。 課題の概要は以下になります。 課題 アクセシブルなツールバーとリッチテキストボックスを作成する 要件 以下の要件を満たすツールバーとテキストボックスを作成してください 選択した文字の色を変えるボタン(任意の色で良い) 選択した文字を太文字に変更するボタン 選択した文字をコピーするボタン 文字をペーストするボタン 選択した文字をカットするボタン イメージ その他 React、Vue、Svelte、Solid.jsなどのライブラリは使っても良いが、React-modalなどのライブラリは使わずに自分で実装を行ってください 上記が課題の概要となります。 このようなツールバーはGitHubのIssueのテキストボックスやNotionなど多数のウェブアプリケーションで使用されています。 一方で、さまざまなユーザに使いやすいように作るにはさまざまな工夫が必要になります。 それではこの課題をどのようにアクセシブルに作成するかを見ていきます。 アクセシビリティの観点から気をつけるべき点 さて上記の課題に対して、どのようなことを気をつければ良いのかを以下で述べます。 Toolbarとして認識されるようにHTMLを作成する ツールバーはHTML要素としては存在していません。そのため、WAI-ARIAなどの技術を使用してToolbarとして認識されるようにHTMLを作成する必要があります。 具体的には以下を満たすように作成すると良いです。 ToolbarのRole要素が入っている 以下のように role 要素を使用して記述を行うことで、ブラウザにツールバーであることを認知してもらい、それを使用者に伝えることができます。 <div class="toolbar" role="toolbar" aria-controls="textarea-sent"> ... </div> Toolbarには aria-label によってそのツールバーの意味が付与されている そのツールが何をするものなのか、支援ツールを介して使用者に伝えることができます。 例えば、以下の例ではtoolGroupごとにラベルを指定しています。 <div class="toolGroup" :aria-label="スタイルの変更をする"> ... </div> Toolbarに aria-controls で操作対象が指定されている そのツールバーがどのコンテンツを操作するものなのか、ブラウザに伝えるとそれが支援技術(VoiceOverなど)に伝わり、多くのユーザにも使いやすいツールバーになります。 <div class="toolbar" role="toolbar" aria-controls="textarea-sent"> ... </div> <div ... id="textarea-sent" ... > </div> toolbar内のボタンは button 要素で正しく記述されている button要素を使用することで、そのツール要素がボタン操作できることをブラウザに伝えることができます。こうすることで、さまざまなユーザがボタンの認識がしやすくなり、ツールバーを使いやすくなります。 <button class="tool tooltip" :area-pressed="isPressed" :area-disabled="isDisabled" @click="props.onClick(isDisabled)"> ... </button> 以上のようにツールバーを作成することで、支援技術がツールバーをツールバーとして認識するようにHTMLを作成することができます。 操作対象などを指定することで、音声でWebページを操作するユーザやキーボード操作するユーザにもツールバーの操作がしやすいようにすることが可能になります。 キーボード操作 HTMLとWAI-ARIAを使用してWebページを構築するだけではなく、JavaScriptなどを使ってキーボード操作しやすいようにするのも重要です。 キーボード操作を行う一定数のユーザはTabで操作を行いますが、ツールバー要素全てをTabでフォーカス可能にすると辿り着きたい要素まで時間がかかってしまいます。 そのため、矢印でツールバーの要素を操作できるようにし、タブではツールバーの前後の要素に移動するようになどを実装するとキーボードで操作するユーザにも使いやすい作りになります。 実際に作成する操作は以下になります。 HOMEを押すとToolbarの一番初めのToolに移動する Endを押すとToolbarの最後のToolに移動する 左矢印を押すと左のToolに移動する 右矢印を押すと右のToolに移動する ツールバーに戻ると前にフォーカスがあった場所にフォーカスされる 最初の場合は左で最後に、最後の場合は右で最初にフォーカスが移る 実際の実装は以下のようになります。 const changeTool = (event) => { const elements = document.getElementsByClassName('tooltip') const index = [].findIndex.call(elements, e => e === event.target) const moveFocus = (nowIndex:number, afterIndex: number) => { opacity.value = 1 changeTabindex(nowIndex, - 1); elements[afterIndex].focus() changeTabindex(afterIndex, 0); } switch(event.key){ case "ArrowLeft": // 矢印キーが押されたら、フォーカスとtabindexを変更する。 if(!elements[index - 1]) { moveFocus(index, elements.length - 1) } else { moveFocus(index, index - 1) } break; case "ArrowRight": if(!elements[index + 1]) { moveFocus(index, 0) }else { moveFocus(index, index + 1) } break; case "Home": moveFocus(index, 0) break; case "End": moveFocus(index, elements.length - 1) break; case "Escape": console.log("event", opacity.value) opacity.value = 0 break; } } // tabindexを変更する関数。 const changeTabindex = (index:number, tabindex:number) => { let indexTmp = index; for(let tool of toolBarList.value){ if(indexTmp < tool.tooltipsGroupList.length){ let tooltmp = tool.tooltipsGroupList[indexTmp] tooltmp.tabindex = tabindex tool.tooltipsGroupList.splice(indexTmp, 1, tooltmp); return } indexTmp -= tool.tooltipsGroupList.length; } } changeTool でイベントを受け取り、イベントの内容に合わせて moveFocus 関数を使用してフォーカスを移動します。 moveFocus 関数内では changeTabindex という関数を読んでおり、 changeTabindex 関数がDOM要素の tabindex を変更することでフォーカスを受け取れる対象を操作しています。 tabindexとは tabindexは要素が入力フォーカスを持てることとキーボードナビゲーションに加わるかどうかを指定できるHTMLのグローバル属性です https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/tabindex こうすることで、ユーザがツールバーの外に移動した後にツールバーに戻ってきたときに、前操作していたボタンにフォーカスが当たるようになります。 音声 で伝える 音声を使用してウェブ操作をするユーザがツールバーを使用するために、アラートなどを使用して操作の内容を伝えることは重要です。 例えば以下の実装のようにすることでカット操作などをユーザに伝えることが可能です。 <!-- roleとしてdivをtextarea要素にする --> <div role="textarea" ... > </div> <!-- アラートするために以下のようなspan要素を透明で作成して、アラートされるようにする --> <span role="alert" aria-live="assertive" class="alert">{{alertText}}</span> // JavaScriptなどでは以下のようにalertTextを書き換えてアラートする alertText.value = `${selectText.value}をカットしました` 様々な色覚特性に適応した配色にする 筆者である私は色弱持ちですが、色のコントラストがはっきりしていないと色が正しく認識できないことが本当に多いです。 私のような色覚に障害がある方々のために色のコントラストをはっきりさせることはとても重要です。 Googleの開発者ツールを使用すると以下のようにコントラストの確認が可能です。 操作の可不可 を伝える ユーザが操作に対して可能か不可能かを認知できるように、ツールバーにDisable属性を設けて押せないようにすると親切です。また、CSSでそのことが見分けがつくように色の変更を行います。 isDisabledなどを用意してHTMLのプロパティを書き換え、操作が可能かどうかを切り替えます。また、こうすることで音声で操作をしているユーザにも操作が不可能かどうかを伝えることができます。 以下はVueで行う例です。属性要素を状態として保持しておきます。 const tooltipsListOfFixStyle = ref([ { ... //以下のように属性を用意します areaPropaties: { isPressed: false, isDisabled: true } }, ... ]) 次に以下のような関数を用意して操作します。 const disableFunc = async () => { const text = await navigator.clipboard.readText() if(selectText.value === '') { const templist = tooltipsListOfCCP.value.map((item)=>{ if(item.iconName === "paste" && text !== '') return item item.areaPropaties.isDisabled = true return item }) const tempFixList = tooltipsListOfFixStyle.value.map((item) => { item.areaPropaties.isDisabled = true return item }) tooltipsListOfCCP.value = templist tooltipsListOfFixStyle.value = tempFixList }else{ const templist = tooltipsListOfCCP.value.map((item)=>{ if(item.iconName === "paste" && text === '') return item item.areaPropaties.isDisabled = false return item }) const tempFixList = tooltipsListOfFixStyle.value.map((item) => { item.areaPropaties.isDisabled = false return item }) tooltipsListOfCCP.value = templist tooltipsListOfFixStyle.value = tempFixList } } 上記の関数では クリップボードにテキストがない場合はペーストボタンをdisableにする 文字が選択されていないときはカットとコピーボタンをdisableにする という二つの処理を行なっています。 tooltipの表示 ツールバーの要素はアイコンで作成されていますが、アイコンだけだとユーザの文化の違いや慣れの違いなどから意味がわからない可能性があります。そのため、操作名を表示するようにすることでツールバーのボタン操作が文字でわかるようにすると良いです。 ツールチップは以下のように span 要素などでHTMLで記述し、CSSでフォーカス時などに表示されるようにします。 <button class="tool tooltip" :tabindex="buttonTabIndex" :area-pressed="isPressed" :area-disabled="isDisabled" @click="props.onClick(isDisabled)"> <slot></slot> </button> <!--以下にツールチップのテキストを入れる--> <span class="tooltip-text">{{tooltipText}}</span> .tooltip-text { ... } /* ホバー時にツールチップの非表示を解除 */ .tool:hover + .tooltip-text { opacity: 1; visibility: visible; } .tool:focus + .tooltip-text { opacity: v-bind(opacity); visibility: visible; } まとめ アクセスフルなツールバーとリッチテキストボックスを作成するときに気をつけなければいけない点を紹介しました。 これ以外にも スマートフォンや低スペックなPCなどのユーザの機器の違い 言語の違い 文化の違い などなど、使うユーザによって、もっと考慮する点はあるように思えます。 これからもアクセシビリティについてもっと学んで、全てのユーザが使いやすいシステムを構築できるようなエンジニアになれるように頑張っていきたいです。 明日は、 @takenokoroid さんの記事です。 お楽しみに!! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
この記事は、 ニフティグループ Advent Calendar 2022 14日目の記事です。 今回は、モブプロ支援ツールの「mob」について紹介していきたいと思います! モブプロとは? モブプログラミング(モブプロ)とは、複数人の開発メンバー(モブ)がコミュニケーションを取りながら実装を進め、実際にコードを書く「タイピスト(ドライバー)」と、「その他のモブ(ナビゲーター)」に分かれて行うソフトウェア開発手法です。また、タイピストは一定時間で交代することが良いとされています。 モブプロのメリットとしては以下のようなものが挙げられます。 一人で考え込まず、参加者と知識を共有し合うことで、素早く問題を解決できる 複数人で確認し合いながら作業を進めるため、レビューの時間を短縮できる 個人でそれぞれ持つ知見を他の参加者に共有でき、全員が共通認識を持って作業することができる。誰か一人がプロジェクトを抜けてしまっても、他の人がカバーすることができる コミュニケーションが促進される 反対に、デメリットは以下です。 大勢でのモブプロの場合、発言する回数が少なくなり集中が続かない 全員で一つのタスクについて作業をするため、リソース効率は落ちる 全員の予定を合わせるのが難しい モブプロの概要やメリット・デメリットを踏まえた上で、モブプロ支援ツールである「mob」について触れていきたいと思います。 mobとは? リポジトリ → https://github.com/remotemobprogramming/mob mobとはモブプロをリモートで実施する上で便利に使えるツールです。 実装されているコマンド群を使って、以下のようなことができます! タイマーを使った時間管理 Git経由で次の人へソースコードを渡すことができる(Git Handover) モブプロ用にブランチを作成し、そこにcommitしていくので、ブランチをクリーンに保つことができる なお、mobはGitHub上でOSSになっており、Go言語で実装されているようです。 インストール 基本的には以下のコマンドを使うことでインストールできます。 # works for macOS, linux, and even on windows in git bash curl -sL install.mob.sh | sh macOSの場合はHomebrewを使ってもインストールできます。 brew install remotemobprogramming/brew/mob 実際に使ってみる 必要なコマンドは mob start 、 mob next 、 mob done のみです! mob start まず、 mob start でモブプログラミングを開始します。 mob-programming-test というリポジトリの main ブランチでモブプロすることを想定します。 mob-programming-test (main)$ mob start git fetch origin --prune git merge FETCH_HEAD --ff-only > starting new session from origin/main git checkout -B mob/main origin/main git push --no-verify --set-upstream origin mob/main > you are on wip branch 'mob/main' (base branch 'main') > It's now 19:12. Happy collaborating! :) git branch コマンドを見てみます。すると、 main ブランチから mob/main というブランチが新たに作成され、 checkout していることがわかります。モブプロ中はこちらで作業していきます。   mob-programming-test (mob/main)$ git branch main* mob/main ちなみに、 mob start はタイマーを設定することができます。以下のコマンドでは 10 と設定しました。現在時刻が19:57なので、20:07までの10分間のタイマーが設定されました。 mob-programming-test (main)$ mob start 10 git fetch origin --prune git merge FETCH_HEAD --ff-only > starting new session from origin/main git checkout -B mob/main origin/main git push --no-verify --set-upstream origin mob/main > you are on wip branch 'mob/main' (base branch 'main') > It's now 19:57. 10 min timer ends at approx. 20:07. Happy collaborating! :) 10分経過すると、Macの場合はAppleScript ( osascript )を使って通知されるようになっています。さらに say コマンドを使って  mob next と読み上げてくれます。 mob next mob next は、タイピストが実施した全ての作業をコミットにまとめてリモートブランチに push し、次のタイピストへGitを経由してソースコードを渡します。 作業の例として sample.txt を生成します。 mob-programming-test (mob/main)$ echo 'Hello, World' > sample.txt その後 mob next でタイピストを交代します。 mob-programming-test (mob/main)$ mob next git add --all git commit --message mob next [ci-skip] [ci skip] [skip ci] lastFile:sample.txt --no-verify sample.txt | 1 + 1 file changed, 1 insertion(+) dd58204b0377d5fc732183df97d4c16a82c51c2b git push --no-verify origin mob/main 次の人はモブプロが始まったブランチ(この場合はmainブランチ)で mob start を実行します。 mob/main ブランチに checkout し、前のタイピストが実施したところまでのコミットをローカルに反映します。 mob-programming-test (main)$ mob start git fetch origin --prune git merge FETCH_HEAD --ff-only > joining existing session from origin/mob/main git checkout -B mob/main origin/mob/main git branch --set-upstream-to=origin/mob/main mob/main > you are on wip branch 'mob/main' (base branch 'main')dd58204 9 minutes ago <k0825> > It's now 19:34. Happy collaborating! :) mob done 何度か mob start 、 mob next を駆使し、モブプロで実装する機能が完成したとします。 mob done コマンドを実行し、派生元のブランチにマージしていきます! ここでは main ブランチへマージします。 mob-programming-test (mob/main)$ mob done git fetch origin --prune git add --all git commit --message mob next [ci-skip] [ci skip] [skip ci] lastFile:fizz.txt --no-verify fizz.txt | 1 + 1 file changed, 1 insertion(+) 2df8c40f6badd01c6813d7f44d63bc01758d8f0c git push --no-verify origin mob/main git checkout main git merge origin/main --ff-only git merge --squash --ff mob/main git merge --squash --ff mob/main git branch -D mob/main git push --no-verify origin --delete mob/main fizz.txt | 1 + sample.txt | 1 + 2 files changed, 2 insertions(+) To finish, use git commit あとは git commit し、リモートブランチへ push するだけです! mob-programming-test (main)$ git commit [main b292073] Squashed commit of the following: 2 files changed, 2 insertions(+) create mode 100644 fizz.txt create mode 100644 sample.txt mob-programming-test (main)$ git push origin HEAD Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 8 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (4/4), 497 bytes | 497.00 KiB/s, done. Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 To <https://github.com/k0825/mob-programming-test.git> 9e30517..b292073 HEAD -> main まとめ mobコマンドはリモートでモブプロをするときに便利! それぞれのコマンドはGit Handoverで実現されているのでソースコードの受け渡しが簡単にできる! mob start : モブプロを開始する mob next : タイピストの交代 mob done : モブプロを終了する リモートでモブプロを実施するときに使ってみてください! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering   明日は、 @spicy_laichi さんの記事です。お楽しみに!
アバター