TECH PLAY

ニフティ株式会社

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

517

この記事は、 ニフティグループ 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 さんの記事です。お楽しみに!
この記事は、 ニフティグループ Advent Calendar 2022 13日目の記事です。 はじめに 最近ChatGPTにハマっている柴田です。普段はFlutterの開発を個人的に楽しんでいます。今回は、FlutterでHero Animationsを実装してみます。 Hero とは FlutterのHero Animationsとは、アプリらしいシームレスなアニメーションのことで、Androidで言うshared element transitions、iOSでいうSwiftUI2.0で追加されたmatchedGeometryEffectのようなものです。 遷移先の画面に遷移元と共通または似ている要素がある場合にHero Animationsを使用することでシームレスに画面遷移することができます。 Flutterでは、標準で用意されているMaterial LibraryのHero Widgetを使うことで簡単に実装することができます。 詳細はこちら https://docs.flutter.dev/development/ui/animations/hero-animations 実装する カードをタップすると詳細が表示されるUIを実装します。 Heroを使用しない実装 Heroを使う前のコードと動作例です。 Card( child: InkWell( splashColor: Colors.blue.withAlpha(30), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => DescriptionScreen(cardContent: cardContent), ), ); }, child: Column( children: [ Container( height: 150, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5)), image: DecorationImage( fit: BoxFit.cover, image: Image.asset(cardContent.imageAssets).image, ), ), ), ListTile( title:Text( cardContent.title, style: Theme.of(context).textTheme.titleMedium, ), ), ], ), ), ); Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Image.asset(cardContent.imageAssets), Padding( padding: const EdgeInsets.all(30), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( cardContent.title, style: Theme.of(context).textTheme.displayMedium, ), const SizedBox(height: 20), Text( cardContent.description, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ], ), Heroを使用した実装 このコードにHero Animationsを実装していきます。実装と言ってもImageやTextをHero Widgetでラップするだけです。 Hero Widgetが同一のTagが設定されているWidget間でいい感じにアニメーションを計算してくれます。 Hero Widgetを使って実装したコードと動作例です。 Card( child: InkWell( splashColor: Colors.blue.withAlpha(30), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => DescriptionScreen(cardContent: cardContent), ), ); }, child: Column( children: [ Hero( tag: cardContent.imageAssets, child: Container( height: 150, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5)), image: DecorationImage( fit: BoxFit.cover, image: Image.asset(cardContent.imageAssets).image, )), ), ), ListTile( title: Hero( tag: cardContent.title, child: Text( cardContent.title, style: Theme.of(context).textTheme.titleMedium, ), ), ), ], ), ), ); Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Hero( tag: cardContent.imageAssets, child: Image.asset(cardContent.imageAssets), ), Padding( padding: const EdgeInsets.all(30), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Hero( tag: cardContent.title, child: Text( cardContent.title, style: Theme.of(context).textTheme.displayMedium, ), ), const SizedBox(height: 20), Text( cardContent.description, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ], ), 注意 HeroでラップするTextにはstyleを付ける必要があります。 Styleを指定していないと、下の図のようにHero Animationsの処理が上手く行われず、アニメーション時に文字がはみ出してしまいます。 今回実装した全体のコードはこちらで公開しています https://github.com/ShibataRyusei/flutter-hero-demo 余談 今回の動作例はiOSですが、もちろんAndroid、Web、macOSでも動作しました。(Windows, Linuxは未検証) 一度の実装で複数のOSに書き出せるのは、何度体験しても感動します。 終わりに 今回は、FlutterでHero Animationsを実装してみました。ImageやTextなどのWidgetをHero Widgetでラップするだけでアプリらしいアニメーションが簡単に実装できました。このようなWidgetsが標準で用意されている点からFlutterのDX(Developer Experience)の高さを感じ、ストレスフリーに開発ができました。 みなさんもFlutterで快適な開発体験を!! 明日(14日目)は、 ike-chan さんです。お楽しみに! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
この記事は、 ニフティグループ Advent Calendar 2022 11日目の記事です。 はじめに 会員システムグループ N1!Machine Learning Product Engineerの中村です。 ニフティでは11/22にNIFTY Tech Dayというイベントを開催しました。 この時に、ニフティニュースにおける深層自然言語処理によるニュース記事要約について発表をさせていただきましたが、この時に行った転移学習(ファインチューニング)について技術的な解説をしていきます。 モデルの転移学習について 本記事の大まかな実装は sonoisa さんの記事を参考に構築しています。 https://qiita.com/sonoisa/items/a9af64ff641f0bbfed44 言語モデルの転移学習 現在の言語モデルは非常に大規模であり、全く学習されていない状態(スクラッチ)から改めて学習させることは現実的ではありません。そこで考案されたのが転移学習という手法です。 転移学習とは、既に学習された大規模な言語モデルを元に、新しいタスクについての学習を行う手法です。この方法は、新しいタスクの学習データが少ない場合に特に有効です。また、既に学習されたモデルの学習済みの知識を引き継ぐことで、学習がスムーズに進むため、学習時間の短縮が期待されます。 プロンプト学習 https://arxiv.org/abs/1910.10683 今回のニュース記事要約ではT5というモデルの学習を行いますが、この学習にはプロンプトを用いて学習を行います。現在の深層学習モデルは非常に大規模であり、1つのモデルで複数のタスクを実行可能です。プロンプトと呼ばれる接頭辞を頭に付けて学習を行うことで、文章に対して何を行いたいかを指定し、それに合わせたタスクを実行するように学習します。 実装と学習の実行 ここではニフティニュースにおける記事データと要約データを用いて、T5モデルの転移学習を行います。 データセットの準備 ノーマライズ処理 まず、neologdの正規化処理を改変した処理を用いて、正規化処理を定義します # https://github.com/neologd/mecab-ipadic-neologd/wiki/Regexp.ja から引用・一部改変 from __future__ import unicode_literals import re import unicodedata def unicode_normalize(cls, s): pt = re.compile('([{}]+)'.format(cls)) def norm(c): return unicodedata.normalize('NFKC', c) if pt.match(c) else c s = ''.join(norm(x) for x in re.split(pt, s)) s = re.sub('-', '-', s) return s def remove_extra_spaces(s): s = re.sub('[  ]+', ' ', s) blocks = ''.join(('\u4E00-\u9FFF', # CJK UNIFIED IDEOGRAPHS '\u3040-\u309F', # HIRAGANA '\u30A0-\u30FF', # KATAKANA '\u3000-\u303F', # CJK SYMBOLS AND PUNCTUATION '\uFF00-\uFFEF' # HALFWIDTH AND FULLWIDTH FORMS )) basic_latin = '\u0000-\u007F' def remove_space_between(cls1, cls2, s): p = re.compile('([{}]) ([{}])'.format(cls1, cls2)) while p.search(s): s = p.sub(r'\1\2', s) return s s = remove_space_between(blocks, blocks, s) s = remove_space_between(blocks, basic_latin, s) s = remove_space_between(basic_latin, blocks, s) return s def normalize_neologd(s): s = s.strip() s = unicode_normalize('0-9A-Za-z。-゚', s) def maketrans(f, t): return {ord(x): ord(y) for x, y in zip(f, t)} s = re.sub('[˗֊‐‑‒–⁃⁻₋−]+', '-', s) # normalize hyphens s = re.sub('[﹣-ー—―─━ー]+', 'ー', s) # normalize choonpus s = re.sub('[~∼∾〜 ~]+', '〜', s) # normalize tildes (modified by Isao Sonobe) s = s.translate( maketrans('!"#$%&\'()*+,-./:;<=>?@[¥]^_`{|}~。、・「」', '!”#$%&’()*+,-./:;<=>?@[¥]^_`{|}〜。、・「」')) s = remove_extra_spaces(s) s = unicode_normalize('!”#$%&’()*+,-./:;<>?@[¥]^_`{|}〜', s) # keep =,・,「,」 s = re.sub('[’]', '\'', s) s = re.sub('[”]', '"', s) return s そのほかにタブ文字やタグの除去、エスケープシーケンスの復号化などを行い、データセットを整備します。 参考元の処理ではlower処理やスペースの削除などが含まれていますが、例えば「iPhone」などの商品名や、空白によって意図的に区切られている部分が消滅するなどの現象が起こったため、本記事ではその部分の処理を行わないように実装しています。 ニフティニュースでは短文と長文の2種類のタイトルの他、3行要約も作成しており、そのデータにそれぞれプロンプトを付与し学習させます。 import re import numpy as np import pickle from tqdm import tqdm tag_regex = re.compile(r"<[^>]*?>") def normalize_text(text): text = text.replace("\t", " ") text = normalize_neologd(text) text = tag_regex.sub("", text) text = text.replace(""", "\"").replace("&", "&").replace("<", "<").replace(">", ">").replace(" ", " ") return text all_data = [] count = 0 for index, data in news_data.iterrows(): if data['body'] is None or data['body'] is np.nan or not data['body']: continue normalized_body = normalize_text(data['body']) all_data.append({"text": "keyword: " + normalized_body,"response": normalize_text(data['keyword_str_1']),}) all_data.append({"text": "keyword: " + normalized_body,"response": normalize_text(data['keyword_str_2']),}) all_data.append({"text": "keyword: " + normalized_body,"response": normalize_text(data['keyword_str_3']),}) all_data.append({"text": "topics_title: " + normalized_body,"response": normalize_text(data['topics_article_title']),}) all_data.append({"text": "title: " + normalized_body,"response": normalize_text(data['title']),}) if data['long_title'] is not None and data['long_title'] is not np.nan: all_data.append({"text": "long_title: " + normalized_body,"response": normalize_text(data['long_title']),}) if data['summary_1'] is not None and data['summary_1'] is not np.nan: all_data.append({"text": "summary_1: " + normalized_body,"response": normalize_text(data['summary_1']),}) if data['summary_2'] is not None and data['summary_2'] is not np.nan: all_data.append({"text": "summary_2: " + normalized_body,"response": normalize_text(data['summary_2']),}) if data['summary_3'] is not None and data['summary_3'] is not np.nan: all_data.append({"text": "summary_3: " + normalized_body,"response": normalize_text(data['summary_3']),}) プロンプトも含めた本文であるtextと、それに対応する応答であるresponseを、all_dataという配列に含めた状態になりました。 データセットの分割 データをtrain/validation/testの3つに分割します。 import random from tqdm import tqdm random.seed(1234) random.shuffle(all_data) def to_line(data): text = data["text"] response = data["response"] assert len(text) > 0 and len(response) > 0 return f"{text}\t{response}\n" data_size = len(all_data) train_ratio, val_ratio, test_ratio = 0.95, 0.03, 0.02 with open(f"data/train.tsv", "w", encoding="utf-8") as f_train, \ open(f"data/val.tsv", "w", encoding="utf-8") as f_val, \ open(f"data/test.tsv", "w", encoding="utf-8") as f_test: for i, data in tqdm(enumerate(all_data)): line = to_line(data) if i < train_ratio * data_size: f_train.write(line) elif i < (train_ratio + val_ratio) * data_size: f_val.write(line) else: f_test.write(line) 確認してみると、ランダムにデータが分割されたことがわかります。 学習の実行 モデルの定義などは参考元と同じため、ここでは割愛します。 https://qiita.com/sonoisa/items/a9af64ff641f0bbfed44 事前学習モデルには megagonlabs/t5-base-japanese-web を使用します。 A100(80GB)時の設定について Google ColaboratoryなどでGPUにA100を使う場合、プリインストールされているtorchではCUDAが対応していないというエラーが起きます。その場合には、以下のコマンドで対応するtorchなどをインストールします。 !pip install -qU transformers[ja] pytorch_lightning sentencepiece torch==1.10.0+cu111 torchvision==0.11.1+cu111 torchaudio torchtext -f https://download.pytorch.org/whl/torch_stable.html その他ハイパーパラメータ ハイパーパラメータについては以下のように設定します。 モデルのチェックポイント周りの定義を行っておくと、もしも途中で学習が終了する(PCが止まる、Google Colaboratoryのセッションが切れてしまう)自体に陥っても、学習を途中から始めることが可能なため設定しておくことをおすすめします。 (どうしても時間がかかってしまうような、このような大規模モデルの学習では非常に便利だと感じました) # 学習に用いるハイパーパラメータを設定する args_dict.update({ "max_input_length": 1024, # 入力文の最大トークン数 "max_target_length": 64, # 出力文の最大トークン数 "train_batch_size": 8, "eval_batch_size": 8, "num_train_epochs": 2, }) args = argparse.Namespace(**args_dict) train_params = dict( accumulate_grad_batches=args.gradient_accumulation_steps, gpus=args.n_gpu, max_epochs=args.num_train_epochs, precision= 16 if args.fp_16 else 32, amp_backend='apex', amp_level=args.opt_level, gradient_clip_val=args.max_grad_norm, default_root_dir=f"{MODEL_SAVE_DIR}/checkpoint", ) 以下を実行して、転移学習を行います。 # 転移学習の実行 model = T5FineTuner(args) trainer = pl.Trainer(**train_params) trainer.fit(model) # 最終エポックのモデルを保存 model.tokenizer.save_pretrained(MODEL_DIR) model.model.save_pretrained(MODEL_DIR) 推論処理 以下のコードを実行することで推論処理を行います。 article_body = "本文" MAX_SOURCE_LENGTH = 1024 # 入力される記事本文の最大トークン数 MAX_TARGET_LENGTH = 64 # 生成される出力の最大トークン数 import re import pickle from tqdm import tqdm tag_regex = re.compile(r"<[^>]*?>") def normalize_text(text): text = text.replace("\t", " ") text = normalize_neologd(text) text = tag_regex.sub("", text) return text def preprocess_body(text): return normalize_text(text.replace("\n", " ")) # 推論モード設定 trained_model.eval() # 前処理とトークナイズを行う preprocessed_body = preprocess_body(article_body) inputs = ["title: " + preprocessed_body, "long_title: " + preprocessed_body, "topics_title: " + preprocessed_body, "summary_1: " + preprocessed_body, "summary_2: " + preprocessed_body, "summary_3: " + preprocessed_body] batch = tokenizer.batch_encode_plus( inputs, max_length=MAX_SOURCE_LENGTH, truncation=True, padding="longest", return_tensors="pt") input_ids = batch['input_ids'] input_mask = batch['attention_mask'] if USE_GPU: input_ids = input_ids.cuda() input_mask = input_mask.cuda() # 生成処理を行う outputs = trained_model.generate( input_ids=input_ids, attention_mask=input_mask, max_length=MAX_TARGET_LENGTH, temperature=1.0, # 生成にランダム性を入れる温度パラメータ num_beams=10, # ビームサーチの探索幅 diversity_penalty=1.0, # 生成結果の多様性を生み出すためのペナルティ num_beam_groups=10, # ビームサーチのグループ数 num_return_sequences=1, # 生成する文の数 repetition_penalty=1.5, # 同じ文の繰り返し(モード崩壊)へのペナルティ ) # 生成されたトークン列を文字列に変換する generated = [tokenizer.decode(ids, skip_special_tokens=True, clean_up_tokenization_spaces=False) for ids in outputs] # 生成された文字列を表示する for i, generated_str in enumerate(generated): if i == 0: print(f"title: {generated_str}") elif i == 1: print(f"long_title: {generated_str}") elif i == 2: print(f"topics_title: {generated_str}") else: print(f"summary {i-2}: {generated_str}") # 前処理とトークナイズを行う の部分でプロンプトを与えつつ、トークナイズを行います。 # 生成処理を行う の生成処理ではハイパーパラメータを変更することで、処理時間や精度のトレードオフ関係を調整します。 実際の推論 実際の推論は以下のようになります。(NIFTY Tech Day 2022より) 全てにおいて成功するわけではなく、以下のように失敗する例もあります。 おわりに 今回はNIFTY Tech Day 2022では話せなかった細かい転移学習の手法などについて書いてきました。 深層学習周りの自然言語の歴史や、クラウド上のアーキテクチャに関してもNIFTY Tech Day 2022でお話したので、 興味がある方はぜひ御覧ください。 最近だとChatGPTのような流暢な対話型のAI技術も登場し、いよいよ自然言語処理は人間に近い存在になりつつありますね。 実際に深層自然言語処理による要約生成などに挑戦してどのような苦労があったのか?についてもまたどこかの機会に発表したいと思います。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering この記事は、 ニフティグループ Advent Calendar 2022 11日目の記事です。 明日は ニフティライフスタイル のsaikeiさんです。次回もお楽しみに!
この記事は、 ニフティグループ Advent Calendar 2022  10日目の記事です。 はじめに こんにちは!インフラシステムグループの仲上と申します。 今回は、Slackでインタラクティブなリマインダーアプリを作成したので、紹介したいと思います。 背景 突然ですが、皆さんはSlackを使っていますか?Slackには優秀なリマインダー機能があり、繰り返し設定や日付時間指定、リマインド相手など細かいところまで設定することができます。 しかし、こちらの機能はコマンドの構文を覚えるのが大変です。特に繰り返し系のコマンドは少し複雑なので、覚えるまで苦労します。(英語ができれば自然と書けると思いますが、筆者は英語が全くできません!) また、忙しいタスクの中で、コマンドを打ってリマインダーをセットするのは大変ですよね。 そこで、Slackのショートカットから呼び出せるインタラクティブなアプリを作ってみました。 インタラクティブとは 「 対話 」または「 双方向 」といった 意味 で、ユーザーがパソコンの画面を見ながら、対話をするような 形式 で 操作 する 形態 を指す。 https://kotobank.jp/word/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96-901 ここでは、modal(Slackのショートカットをクリックしたときに表示されるポップアップのこと)への入力内容に反応して自動で入力項目が書き換わっていることから「インタラクティブ」と呼ばせていただきました。 成果物 アプリには以下の機能があります 単発のリマインドセット 繰り返しのリマインドセット 毎日、毎週、毎月、毎年から選択 投稿先指定 構成 Slackは公式ライブラリとして、 Slack bolt を公開しています。こちらのライブラリがとても便利なので、今回はNode.js + Slack bolt の構成でAWSのlambdaにデプロイしました。 デプロイには、Serverless Frameworkを用いています。 modal(ショートカットをクリックしたときに表示される画面)は Slack Block Kit を用いて作成しました。 費用については、リクエストに応じて課金されるので、個人で使用する分にはほとんどかかりません。(1月当たり数百円程度) 作り方 コードはすべて こちらのリポジトリ に公開しています。 準備物 Node.js(自分のローカルは v16.17.0) AWSアカウント AWS CLI のインストールとプロファイル設定 手順 1.Slackアプリの設定 2.環境構築系 3.Serverless Frameworkの整備 4.Lambda関数の準備 5.環境変数の設定 6.デプロイ 7.イベント通知先のURLを設定 より詳細な手順に関しては、以前別の社員が こちらの記事 で紹介しているので、今回はコードとコマンドのみ紹介させていただきます。 1.Slackアプリの設定 display_information: name: reminder_maker features: bot_user: display_name: reminder_maker always_online: false shortcuts: - name: リマインダーセット type: global callback_id: socket-mode-shortcut description: リマインドコマンド生成 oauth_config: scopes: bot: - channels:history - chat:write - reminders:write - reminders:read - commands settings: event_subscriptions: request_url: https://example.com # イベント送信先。後で変えるので一旦適当 bot_events: - message.channels interactivity: is_enabled: true request_url: https://example.com # イベント送信先。後で変えるので一旦適当 org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false 2.環境構築系 Serverless Framework の設定 $ npm init (npm initの設定はすべて何も入力せずEnterでOK) $ npm i -g serverless # Serverless Frameworkインストール ライブラリの準備 $ npm install @slack/bolt 3.Serverless Frameworkの整備 service: serverless-bolt-js frameworkVersion: '3' provider: name: aws runtime: nodejs14.x region: ap-northeast-1 environment: SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET} SLACK_BOT_TOKEN: ${env:SLACK_BOT_TOKEN} functions: slack: handler: app.handler events: - http: path: slack/events method: post plugins: - serverless-offline 4.Lambda関数の準備 今回コードが長くなってしまったので、ピックアップして紹介します。 全文はGitHubのリポジトリを見てください。 この辺りはNode.js + Lambda でインタラクティブアプリを作るための初期化です。 // Initialize your custom receiver const awsLambdaReceiver = new AwsLambdaReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET, }); // Initializes your app with your bot token and the AWS Lambda ready receiver const app = new App({ token: process.env.SLACK_BOT_TOKEN, usr_token: process.env.SLACK_USER_TOKEN, receiver: awsLambdaReceiver, }); ショートカットが押された時の動作を定義しています。 app.shortcut('socket-mode-shortcut', async ({ shortcut, ack, context, logger }) => { ~ } 送信先の選択肢が変更された時のmodal更新を行っています。 Slack boltでは、app.actionにactionIDを渡してあげることでボタンが押された時の動作を定義することができます。 app.action('actionId-3', async ({ ack, context, logger, body, payload }) => { ~ } 送信ボタンが押された時の制御をしています。 app.view('modal-id', async ({ ack, body, view, client, logger, payload, context }) => { ~ } 最後にLambda関数のイベント処理を書いて完成です。 module.exports.handler = async (event, context, callback) => { const handler = await awsLambdaReceiver.start(); return handler(event, context, callback); }; 5.環境変数の設定 $ export SLACK_SIGNING_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx $ export SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 6.デプロイ $ sls deploy 7.イベント通知先のURLを設定 デプロイ後に表示されたURLをここに書きます。 動作確認 すべての設定が完了すると、ショートカットが選択できるようになっているはずです。 ここをクリックすると、最初に紹介したmodalが表示され、リマインドが登録できると思います。 まとめ 今回は、Slackのリマインド設定を楽にするために、インタラクティブなアプリを作成してみました。 Slack boltに初めて触ってみましたが、イベントの制御が非常に優秀でとても使いやすかったです。また、今回のように動きを持ったアプリのほうが、使っていて楽しいなと感じました。 普段コードをあまり書かないため、今回書いたコードはとても長くなってしまいました。modalのブロック部分はほかのjsonファイルに保存して、適宜呼び出すような形にしたほうが良いかもしれません。 また、現状はリマインドの登録機能のみ実装されているので、削除機能やリスト表示機能なども追加したいと考えています。 皆さんもぜひ使ってみてください! 次回予告 明日は「 @ibukinakamura 」さんの「Google Colaboratoryで自然言語処理モデルT5をチューニングする」です!お楽しみに! 参考 https://slack.dev/bolt-js/ja-jp/tutorial/getting-started https://slack.dev/bolt-js/ja-jp/deployments/aws-lambda https://api.slack.com/block-kit 【初心者でもできる!】SlackのTimesチャンネルの開設を通知しよう! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
NIFTY Tech Talkは、ニフティ株式会社の社員が主催するトークイベントです。 本イベントでは、ニフティグループの社員が業務を通じて学んだことを発信しています! 第8回目のテーマは「ニフティのデータ分析を語る会」です。 ニフティのデータ分析部隊のメンバーから実際の業務で培われたノウハウについて、LTと質疑応答を交えたディスカッション形式で語っていただきます。 概要 日程:12月20日(火)12:00〜13:00 配信方法:YouTube Live 視聴環境:インターネット接続が可能なPC/スマートフォン 参加方法 YouTube Liveにて配信いたします。 connpass にて登録をお願いいたします。 YouTube LiveのURLは決定後、connpass内の参加者への情報欄に記載いたします。 こんな方におすすめ データ分析系の業務に関わるエンジニア ニフティの現場で使われているデータ基盤やデータ分析の技術について興味がある方 ニフティの技術や風土に興味がある方 タイムテーブル 時間 コンテンツ 12:00 – 12:05 オープニング+会社紹介 12:05 – 12:10 登壇者紹介 12:10 – 12:25 データ基盤について 12:25 – 12:40 Tableau、TableauServerの社内の活用事例 12:40 – 12:55 ChatGPTで賑わう自然言語処理技術、ニフティのデータサイエンスについて 12:55 – 13:00 まとめ+クロージング テーマ ニフティでデータ分析・データ基盤の業務に携わっている方々に登壇していただき、LT形式で発表していただきます。 データ基盤について Tableau、TableauServerの社内の活用事例 ChatGPTで賑わう自然言語処理技術、ニフティのデータサイエンスについて 登壇者プロフィール 松下 俊平(ファシリテータ) 管理会計・データ分析チームで購買データやお客様の声のデータ分析をしています。 分析でよく使うのはSQL、Tableauです。 黒羽 孝夫(登壇者) 継続的にデータ活用・分析を実現するために、データ基盤を続けているニフティN1!データアーキテクト。 いかに楽しく働くかってことを考えてます。 打矢 光(登壇者) 『データエンジニアの方に整えて貰ったデータ基盤』と『ビジネス的なドメイン知識』・『分析技術』を掛け合わせて、 データ分析起点での、①社内各所からの分析相談 ②問題提起や解決提案 などを行っています。 瀬川 雄太(登壇者) ニフティN1!データサイエンティストの瀬川です。 データセンター管理やクラウド開発を学んだと思ったら、気づけばコンテナ開発や機械学習をしていたエンジニアの端くれです。 今は経営管理部門に異動してビジネスを学び、データを活かして主力商品のマーケティング推進に携わっています。 ニフティグループでは一緒に働く仲間を募集中です 新卒採用、キャリア採用を実施しています。ぜひ リクルートサイト をご覧ください。 ニフティエンジニアが業務で学んだことやイベント情報を エンジニアブログ にて発信しています! ニフティエンジニアの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/ ) で公開されています。
この記事は、 ニフティグループ Advent Calendar 2022  8日目の記事です。 はじめに こんにちは!会員システムグループの渡邊です。普段はニフティトップページの開発運用を担当しています。 今回はニフティトップページでも採用しているNext.jsを使ったサイトの表示速度を改善する方法について紹介します。 実行環境 MacBook Pro(M1、2021) macOS v12.6.1 Next.js v13.0.6 next/image 画像ファイルはウェブページ全体のバイト数の半分を占めると言われるほど、ページを読み込むときに負荷がかかる部分です。 こういった問題を解決するために画像サイズを縮小させるTinyPNGなどのサイトを使ったり、軽量な画像フォーマットに変換するという作業を行うことがあると思います。 Next.jsでは、next/imageと呼ばれるコンポーネントを呼び出すことで、軽量な画像フォーマットに自動で変換する仕組みを備えています。 使い方は単純で以下のようにコンポーネントを呼び出し、必要なプロパティを渡してあげるだけです。 以下の例では、1920×1080の画像を読み込み500×250リサイズしています。 import Image from 'next/image' import styles from '../styles/Home.module.css' export default function Home() { return ( <div className={styles.container}> <h1>画像テスト</h1> <div>next/image</div> <Image src="/tree.jpg" alt="tree" width={500} height={250} /> <div>imgタグ</div> <img src="/tree.jpg" alt="tree" width={500} height={250} /> </div> ) } 仕組みとしてはImageコンポーネントで読み込んだ画像をGoogleが開発している次世代画像フォーマットのwebpに変換します。 テスト用にnext/imageとimgタグを使って2枚の画像を表示してみると以下のようになります。特に劣化は感じることはなく、サイズを見ると1/10以下に削減できています。 https://nextjs.org/docs/api-reference/next/image next/dynamic 次にES2020から追加された新機能の動的インポートをNext.jsでも行えるようにする機能です。 トグルやモーダルなどの初回ロード時では不要な処理をユーザーが操作したときに都度読み込みを行うようにするものです。 一部の処理を動的インポートにすることで初回ロード時に読み込む量を減らすことができます。 静的インポートと動的インポートの書き方の違いは以下のようになります。 https://nextjs.org/docs/advanced-features/dynamic-import // static import import List from './list' import List2 from './list2' // dynamic import import dynamic from 'next/dynamic'; const List = dynamic(() => import('./list')) const List2 = dynamic(() => import('./list2')) next/script 外部スクリプト読み込みは、パフォーマンスに大きな影響を与える原因の一つです。 外部スクリプトはこちらから手を入れることができず、そのまま読み込むしかないため、パフォーマンス改善の中では一番苦しめられる部分だと個人的には思います。 こういった問題を改善するために登場したのがnext/scriptです。 next/scriptは、外部スクリプトの実行順序を制御することができ、strategyというプロパティを使用することで自動的に優先順位をつけることができます。 書き方は以下のようにScriptコンポーネントにsrcとstrategyプロパティを渡してあげるだけです。 <Script src="<https://example.com/script.js>" strategy="beforeInteractive" /> strategyには3つのプロパティが用意されています。 beforeInteractive ページが表示される前に読み込みが開始する バンドルされたJavaScriptを読み込むより前に実行することができる 最優先で読み込まれるので、実行速度が求められる広告スクリプトなどで有用 afterInteractive プロパティを何も指定していないと選ばれるもの ページが表示された直後に読み込みが開始される タグマネージャーやGAなどのスクリプトに有用 lazyOnload ページが表示されてアイドル状態になった後に読み込みが開始される すぐに読み込む必要がなく、ページが完全に表示されてから使われるようなチャットボットやソーシャルメディアウィジェットなどで有用 以上のことから beforeInteractive > afterInteractive > lazyOnloadの順番でスクリプトが読み込まれるということになります。 https://nextjs.org/docs/api-reference/next/script バンドルサイズを確認する 最後にバンドルされたファイルに含まれる各パッケージの容量を可視化するツールを使って、アプリケーションのどこの読み込みに時間がかかっているか可視化してみます。 webpack-bundle-analyzerをインストールします。 yarn add -D @next/bundle-analyzer next.config.jsに以下の設定を追加します。 const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer({}); 最後にコマンドを実行します。 ANALYZE=true yarn build 今回はライブラリによる読み込みサイズを見るために日付ライブラリを複数導入し、同じ処理を実行してみました。 日付ライブラリでメジャーなday.js、date-fns、luxonを入れて検証しました。 3つ比較してみましたが、luxonが圧倒的に重い処理をしていることがひと目で分かります。同じような処理をしているライブラリでもここまでの差が生まれるので、慎重に選ばないといけないことがわかります。 加えてコンポーネント単位での確認もできるので、どの機能に原因があるかの特定が容易にできるのも良い点です。 まとめ 以上がニフティトップで採用しているパフォーマンス改善の一部になります。どれもNext.jsを採用しているサイトならすぐに実施できることなので、積極的に活用することをおすすめします。 パフォーマンス改善において大事なのは可視化できることなので、原因を自動で特定し、数値化するLightHouse ciの導入もおすすめです。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering 明日は、 nakanowai さんの「AWS re:Invent 2022で発表されたこと」のまとめ記事です。 お楽しみに!
この記事は、 ニフティグループ Advent Calendar 2022 5日目の記事です。 はじめに 会員システムグループ SREチームの浅見( @rubihiko )です。 インシデント管理、リスク分析の重要性の高まりから色々調べていたところ、以下の記事の記事をみて良いと思ったので試してみました。 その SLO は現実的ですか?SRE 的なリスク分析手法 Googleでは 稼働準備レビュー(PRR) 時に リスクを優先順位付けして明確化 することを推奨しているようで、ニフティでも Production Readiness Checklist を作って運用し始めているので、取り入れていきたいなと思っています。 用語の説明 平均検出時間(MTTD:Mean Time To Detect) 問題が発生し、修復する人(やシステム)が検知するまでの平均時間です 平均修復時間(MTTR:Mean Time To Repair) 人(やシステム)が問題を検知して、それを修復するためにかかった平均時間です 平均障害間隔(MTBF:Mean Time Between Failures) 障害を復旧させサービスが使えるようになってから、次の障害が発生するまでの平均時間です MTTD と MTTR と MTBFの関係 その SLO は現実的ですか?SRE 的なリスク分析手法 年間の損失 以下の数式でリスクに対しての年間の損失が導けます (MTTD + MTTR) * (365.25 / MTBF) * percent of affected users MTTD(分): 5分 MTTR(分): 120分 MTBF(日): 365日 (5 + 120) * (365.25 / 365) * 100% = 125分 年間で約125分停止するリスクがあるとわかります リスク分析 MTTD, MTTR, MTBFの関係がわかりました。 これにユーザーインパクトを加えると損失を導くことができました。 この計算式を使い、リスク分析へと進んでいきます。 今回は、サンプル事例とデータを使って見ていきたいと思います。 記事の中に、リスク分析のためのテンプレートがあるためこちらをコピーして使います。 このテンプレートはいくつかのシートで構成されており、リスクの洗い出し、要素の洗い出し、リスク許容の判断をすることが出来ます。 CRE Risk Analysis Template – Public ※ ここでは、テンプレートに書いてある用語を以下のよう置き換えて考えています ETTD = MTTD ETTR = MTTR ETTF = MTBF ETTD Estimated Time To Detection – how long it would take to detect and notify a human (or robot) that the risk has occurred; aka MTTD. ETTR Estimated Time To Resolution – how long it would take to fix the incident once the human (or robot) has been notified; aka MTTR. ETTF Estimated Time To Failure – estimated frequency between instances of this risk; aka MTBF. リスクカタログ リスクの洗い出しを行います。 過去にあった障害をベースに考えるのがよいと思います。 リスクカタログ このリスクカタログにある、 「新規機能リリースの影響で一部機能が利用不可」 の項目の場合の詳細としては、「リリースをしたが、ある一定数のユーザーに対してエラーとなっており、それに気がついたのは1日後だった。気がついた後10分で切り戻しを行い解消した。この障害は60日毎に発生してた。」としたものです。 つまり表に入力する値としては以下のようになります。 一定数のユーザーに対してエラー: impact 2% 気がついたのは1日後: MTTD 1440(min) 気がついた後10分で切り戻しを行い解消: MTTR 10(min) この障害は60日毎に発生してた: MTBF 60(days) 損失の計算のとおり、年間における障害の時間が算出されます。 年間の平均障害数: incidents/year 6.09(回) 年間の平均障害時間: bad mins/year 177(min) リスクファクター リスク全体に影響を及ぼすような要素を洗い出します リスクカタログで挙げたリストに対して、影響を与える要素を追加することになります。 追加する観点は、MTTD, MTTR, MTBFどれを使っても構いません。 リスクファクター 例えば、 「運用過負荷のため、障害検知時間(MTTD)がインシデント毎に+30分増加」 などは、通常の業務が忙しすぎたりしてアラートに気が付かない、他に優先度が高いタスクがありアラートを受け取れない、などが課題としてあり、全てのリスクに対して、MTTRが+30分増加するというものです。 また、 「運用手順書が不足しているため、障害復旧時間(MTTR)がインシデント毎に+10分増加」 はアラートに気が付き対処を開始したが、復旧のための運用手順書が不足しているので、通常よりMTTRが+10分増加してしまうというものです。 このように、全ての障害に影響を与える要素について、リスク(bad mins/year)として追加しているわけです。 リスクスタックランク これまで洗い出したリスクのカタログ・要素を並び替えます このテンプレートを使えば以下のよう自動で並び替えをしてくれます。 ここからは、SLOに対して、どの程度リスクを許容できるか確認するフェーズになります。 リスクスタックランク SLO 99.5% なら全てのリスクを許容してもエラー予算は余りますが・・・ SLO 99.5%の可用性は、年間43.8時間の停止を許容する目標となりますが、ユーザーに見せているサービスとしては低いので良くないように思えます。 社内で利用しているツールや実験的なサービスならありかもしれません。 リスクスタックランク: SLO 99.5% 99.9%, 99.99% を求める場合は当然許容できるリスクが減ります SLO 99.9%, 99.99%の場合はどうなるか見てみます。 SLOが厳しくなるほど許容できるリスクが少なくなっているのが下の図から分かると思います。 リスクを許容しすぎるとエラー予算がなくなってしまい、SLOを維持できなくなってしまいます。 これは、ユーザーへの信頼性低下へと繋がります。 ここでは、下から適当にリスク許容としていますが、実際にはSLOに基づいた適切なリスク許容をする必要があります。 このようにリスクを数値化したことで、データドリブンのアプローチが可能になったのはすごいことだと思います。 リスクスタックランク: SLO 99.99% リスクの許容 分析したリスクは、プロダクト開発にも影響する大きい要素です。 ビジネスサイド(PO)とエンジニアサイドで議論して決める必要があります。 リスクを許容した上で施策を進めるのか、それとも時間をとって改善するのか。 このようなデータがあることでプロジェクトの進行判断の材料になります。 まとめ リスクを数値化したことで潜在的なリスクを明るみにして、優先度付けできました。 テンプレが整っているので、Production Readiness Checklistにも容易に組み込めるのでとても良い考えだと思います。 リスクを可視化するには MTTD, MTTR, MTBF, ユーザーインパクトを使う MTTD: アラートが上がってから検知するまでの平均時間 MTTR: アラートを受け取ってから対応を完了するまでの平均時間 MTBF: サービスが正常になった点から次に障害発生するまでの平均時間 MTTD, MTTR をできるだけ短くすることが信頼性向上につながる MTBF をできるだけ長い期間にすることは信頼性向上につながる リスクを分析すること リスクやリスクを増加させる要素を洗い出し、数値化し、分析する リスクを評価し許容を検討する ユーザーインパクト、ビジネスインパクト、現実的な問題など話し合って決める SLOに応じてリスクは許容できることを理解する SLO 99.99%: ほぼ許容出来ない SLO 99.90%: 一部許容できる SLO 99.50%: ほとんど許容できる 明日は、 supreme3854 さんの tkinterで残業代計算 です。 お楽しみに! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
この記事は、 ニフティグループ Advent Calendar 2022 4日目の記事です。 はじめに こんにちは。ニフティ 会員システムグループ シニアエンジニアの伊達です。 AWS上で稼働するアプリケーションの開発をするにあたってIaC(Infrastructure as Code)を実践することは一般的になっています。ただ、そのツールにはいくつか候補があるでしょう。ニフティではTerraformを使うことが多くCDKは今のところ少数派です。 今回はCDKを使うにあたってのちょっとしたTipsを共有します(特に設定値に関するものをいくつか用意しました)。とはいえ、まだまだCDK初級者ですので、 @NIFTYDevelopers へ読者諸賢のTipsも教えていただけると嬉しいです。 なお、伊達はTerraformを通らずにCloudFormationとCDKを使い始めたため、それTerraformでも普通にできるよというものがあると思いますが目を瞑っていただけますと幸いです。 また、本記事はCDK v2を前提とし、CDKのコードの言語はTypeScriptを使っています。 CDKとは AWS CDKの特徴は既存のプログラミング言語を使ってAWS上でシステムを構築できる点です。 2022年12月現在ではTypeScript、Java、Python、C#、Go言語で記述ができます。開発者はアプリケーションのコードを書くのと同じようにIDEの恩恵を受けながらAWSのリソースのプロビジョニングをすることができます。 この記事ではCDKそのものの解説などはしません。詳しくはAWSの公式ページやGitHubを参照ください。 オープンソースの開発フレームワーク – AWS クラウド開発キット – Amazon Web Services https://github.com/aws/aws-cdk Tipsその1 Contextで設定値を与える CDKにはContextという仕組みがあり、CDKのStackなどにkey-value形式のデータを渡すことができます。cdk.jsonのcontext内がデフォルト値となります。 例えば cdk initしたばかりのcdk.jsonは以下のようになっています。 { "app": "npx ts-node --prefer-ts-exts bin/tmp.ts", "watch": { "include": [ "**" ], "exclude": [ "README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "tsconfig.json", "package*.json", "yarn.lock", "node_modules", "test" ] }, "context": { "@aws-cdk/aws-lambda:recognizeLayerVersion": true, "@aws-cdk/core:checkSecretUsage": true, "@aws-cdk/core:target-partitions": [ "aws", "aws-cn" ], "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, "@aws-cdk/aws-iam:minimizePolicies": true, "@aws-cdk/core:validateSnapshotRemovalPolicy": true, "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, "@aws-cdk/core:enablePartitionLiterals": true, "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, "@aws-cdk/aws-iam:standardizedServicePrincipals": true, "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true } } ここに値を追加することでStack内などで参照することができます。例えば、既存のVPCがあり、それを参照したいという場合には以下のように cdk.jsonに記述します。 ... "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, "vpc_id": "vpc-xxxxxxxxxxxxxxxx" } コード内では以下のようにして参照します。 import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class ExampleAppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // vpcIdの値は"vpc-xxxxxxxxxxxxxxxx" const vpcId = scope.node.tryGetContext('vpc_id'); ... } } Tipsその2 環境を分ける まず、devlepment、staging、productionなど稼働環境を複数持つ場合には、AWSアカウント自体をわけることをおすすめします。同じアカウント内に複数の環境を作るとリソースの重複などを避ける手間があることと、誤った環境にデプロイするなどのオペミスが起きやすくなります。 その上で環境ごとに設定を分けるには、cdk.json内に”stage”といったキーでデータを追加します。 ... "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, "stage": "development" } これは以下のように参照できます。 import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class ExampleAppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // stageの値は"development" const stage = scope.node.tryGetContext('stage'); ... } } 例えばグローバルでユニークな必要があるドメイン名やS3のバケット名を以下のようにしたいとします。development環境にはprefixをつけるパターンです。 環境 ドメイン名 S3バケット名 development dev- app.example.com dev- nifty-engineering-example-bucket production app.example.com nifty-engineering-example-bucket cdk.json内ではstage、domain、bucket_nameを設定します(stageがdevelopmentでほかがproduction用の値なのが不格好ではありますが……)。 ... "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, "stage": "development", "domain": "app.example.com", "bucket_name": "nifty-engineering-example-bucket" } CDKのコードではstageを参照してドメイン名やバケット名を組み立てます。 import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as route53 from 'aws-cdk-lib/aws-route53'; import * as s3 from 'aws-cdk-lib/aws-s3'; export class ExampleAppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const stage = scope.node.tryGetContext('stage'); let domainPrefix = ''; if (stage == 'development') { domainPrefix = 'dev-'; } const domain = [domainPrefix, scope.node.tryGetContext('domain')].join(''); // 既存のRoute53ホストゾーンを参照 const hostedZone = route53.HostedZone.fromLookup(this, 'ExampleAppHostedZone', { domainName: domain, }); const bucketName = [domainPrefix, scope.node.tryGetContext('bucket_name')].join(''); const bucket = new s3.Bucket(this, 'ExampleAppBucket', { bucketName: bucketName, }); } } Contextはcdkコマンド実行時に上書き指定ができます。以下のようにすることで、コード内で参照される値を変えることができるため、環境ごとに異なる設定でデプロイができます。 $ cdk synth # 指定がないときにはcdk.jsonの値なので stage: "development" $ cdk synth --context stage=development # stage: "development" $ cdk synth -c stage=production # stage: "production" Tipsその3 さらに環境ごとの設定をする 先ほどの書き方の場合には、 if (stage == 'development') { としてましたので、developmentではないときにはproductionという扱いでした。prefixをロジックで追加できるのは良いですが、cdkコマンド実行時にスペルミスすると惨事になりそうです。また、他システムのAPI Keyなど環境ごとに値が全く異なるものもあるでしょう。 以下のように環境ごとの設定をcdk.jsonに記載します。 ... "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, "stage": "development", "development": { "domain": "app.example.com", "bucket_name": "nifty-engineering-example-bucket", "foo_system_api_key": "ABCDEF012345" }, "production": { "domain": "dev-app.example.com", "bucket_name": "dev-nifty-engineering-example-bucket", "foo_system_api_key": "XYZABC789012" } } その2で記載したコードは以下のようになります。こちらのほうがだいぶスッキリしますね。 import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as route53 from 'aws-cdk-lib/aws-route53'; import * as s3 from 'aws-cdk-lib/aws-s3'; export class ExampleAppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const stage = scope.node.tryGetContext('stage'); const settings = scope.node.tryGetContext(stage); const hostedZone = route53.HostedZone.fromLookup(this, 'ExampleAppHostedZone', { domainName: settings.domain, }); const bucket = new s3.Bucket(this, 'ExampleAppBucket', { bucketName: settings.bucket_name, }); } } Tipsその4 リソースにタグを設定する コスト分析のためにコスト配分タグを使っていると思います。 CDKではStack内のリソースにまとめてタグを設定できます。 以下のようにタグを付けたいとします。 タグ名 値 application example application system example system 例によってcdk.jsonに以下のように書きます。 ... "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, "application": "example application", "system": "example system" } 以下のようにしてタグを設定できます。scopeはStackでもConstructでも指定できます。 Tags.of(scope).add(key, value) ExampleAppStack内のリソースすべてに同じタグを設定するには以下のように書きます。 import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class ExampleAppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); cdk.Tags.of(scope).add('application', scope.node.tryGetContext('application')); cdk.Tags.of(scope).add('system', scope.node.tryGetContext('system')); ... } } Tipsその5 リソースにタグを設定する#2 常日頃から活発に開発をしているアプリケーションであれば良いですが、中には一度リリースした後にはほとんど触らないようなものもあります。1年後に手を入れることになり「ドキュメントやレポジトリはどこだっけ……」と調べて回るようなことになりがちです。 タグで各リソースにドキュメントやレポジトリのURLをつけておくと便利です。 タグ名 値 document https://notion.so/barcorporation/xxxxxxxxxx repository https://github.com/barcorporation/example-application タグのキーと値が上記の場合には、cdk.jsonには以下のように記述します。 ... "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, "document": "https://notion.so/barcorporation/xxxxxxxxxx", "repository": "https://github.com/barcorporation/example-application", } そして、CDKのStackのコードに以下のように書けば、Stack内の各リソースにドキュメントとレポジトリのURLがタグ付けされ、AWS管理コンソールから調査を始めたときにドキュメントに辿り着けるようになります。 import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class ExampleAppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); cdk.Tags.of(scope).add('document', scope.node.tryGetContext('document')); cdk.Tags.of(scope).add('repository', scope.node.tryGetContext('repository')); } } We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering 明日は、 @rubihiko さんのSREでのリスク検討に関する記事です。 お楽しみに!
はじめに この記事は、 ニフティグループ Advent Calendar 2022 7日目の記事です。 どうも!IWSです!普段はお客様が初めて@niftyをご利用になる際の無料ID会員登録システムや、サービスをご利用になる際のログインシステムの開発・運用を担当しています。 そして、それとは別にオンライン会議イベントサポートチーム(通称:オンサポチーム)としても活動をさせていただいています。その活動の一環として、ニフティで毎月行われているTechイベント「NIFTY Tech Talk」の配信サポートもしています。 この記事ではオンサポチームのお仕事、とりわけNIFTY Tech Talkの舞台裏のようなものを書いていこうかなと思います! NIFTY Tech Talk NIFTY Tech Talk はテーマに対してLTやディスカッションの形式で話すニフティのテックイベントです。過去にはスクラムマスター対談やコスト管理術、レガシー脱却など様々なテーマで開催されました。 最初はZoomで NIFTY Tech Talkは、元々は配信にZoomを使用していました。#1から#3までですね。 ZoomにはミーティングをそのままYouTubeに配信する機能があります。当初はこの機能を使い登壇者の方にZoomに集まって話してもらいYouTubeに配信するという方式でやっていました。 しかし、Zoomではワイプとスライドのレイアウトが難しいという問題がありました。 たとえば、下の画像は NIFTY Tech Talk #1 の配信画面の切り抜きです。登壇者の方のカメラワイプがスライドに重なってしまい、QRコードが何のものなのか分からなくなっています。せっかく作ったスライドが隠れてしまうのはもったいないですね…… ワイプでスライドの大事なところが隠れてしまった StreamYard!! 「もっときれいなレイアウトで配信ができないものか」 と、そこに彗星のごとく現れたのが StreamYard !! StreamYardはYouTubeやTwitchなどのPFへの配信ができるライブサービスです。画面の装飾やレイアウト変更、BGMを再生したりコメントを拾ったり……などなど配信をサポートする様々な機能があります。 中でも特にありがたいのがバックヤードの機能です。名前の通り配信の舞台裏といった機能で事前に必要なものをバックヤードに集めておくことで配信への出し入れをスムーズに行えるようになります。 ここがバックヤード! 例えば、スライドを途中で別のものに交代するような場合、事前にバックヤードに集めておくことでワンクリックでスライドを切り替えることができます。バックヤードにはそれ以外にも画面共有や動画などいろいろなものを準備しておけるので様々な状況に対応できると思います。 レイアウトはどうなった? 問題だったレイアウト部分は StreamYard では8種類から選ぶことができます。中でも使い勝手が良かったのはこのレイアウトですね。スライドを大きく見せつつワイプもしっかり出す一番オーソドックスなものではないでしょうか。 NIFTY Tech Talk #5 ちなみに、画面の装飾もStreamYardで行っています。右上にあるニフティとニフティライフスタイルのロゴマークは自作した画像をオーバレイという機能を使って出したものです。簡単なものならこれだけで出せます。 ただ、大きさや位置など細かい調整はStreamYard側ではできないので作るときに気をつけないといけないというのはありました。このロゴマークも実は16:9の透過画像で作成しています。 StreamYard側での調整ができないので「いざ作ってみたらスライドに被ってしまった……」といった事があるのは少し悲しいところ。かゆいところに手が届かない…… お主にはこの共催権限を与えようぞ…… StreamYardの権限には すべての操作ができる「管理者」 配信の開始などの一部操作以外のことができる「共催」 必要最低限の権限だけを持った「ゲスト」 の3パターンがあります。 基本的に登壇者はゲスト権限があればこと足りるのですが、自分の番になったときにスライドを配信に追加する。ということはできない(バックヤードから配信への追加は共催以上の権限が必要)ため、そこの管理だけは管理者がやる必要があります。 第4回では登壇者にゲスト権限を与えて管理者の人力でスライド管理を行っていました。ただ、いくら数人分だけとはいえ、タイムテーブルとにらめっこしながらスライドを上げ下げするのはかなり大変だったため、第5回以降では登壇者の方に共催の権限を付けるようにしています。 共催であればバックヤードから自分やスライドをいつでも配信に追加することができるため、自分の番になったら各自で追加し話してもらう、といったスタイルです。登壇者の方の誤操作でレイアウトが変わったり違うものが追加されたりといった危険性はありますがそこは飲んでいます。タイムテーブルとにらめっこするよりは……という感じですね。 最後に いろいろ話してきましたが、シンプルでスライドの切り替えがしやすく使いやすい、というのがStreamYardの1番のいいところではないでしょうか。NIFTY Tech Talkのように毎回登壇者が変わるイベントではそのたびに使い方を説明するのも大変なのでシンプルで使いやすいというのはそれだけで助かります。 実際にStreamYardで登壇してくれた方からは「スライドをシームレスに切り替えられるのはめちゃ使いやすかった」、「事前にバックヤードで画面共有が出来ていたので、画面共有時のあたふたが無くてよかった」などの声をもらっており、StreamYardを使い始めてよかったのではないかと思っています。 同時に「(共催権限が付いていて)誰でもいじれちゃうので、誤操作怖いなみたいな気持ちになりました!」というような声ももらいました。このあたりは課題としてうまく解決していきたいですね! さて、結局StreamYardのお話がメインになってしまいましたがこの記事はここで終わりにしようかなと思います。 第7回開催まできたNIFTY Tech Talkですが、これからもオンサポメンバーとして、 第8回、第9回……と良いものができるよう新しいやり方や面白いやり方をまだまだ模索して頑張っていきます! ぜひ NIFTY Developers のチャンネル登録や高評価のほどよろしくおねがいします! YouTube : https://www.youtube.com/channel/UCKn__vHAD4yqAzeOUzb_0sQ Twitter : https://twitter.com/NIFTYDevelopers We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 NIFTY Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering 明日は、 @D_W さんのNext.jsに関する記事です。 お楽しみに!
この記事は、 ニフティグループ Advent Calendar 2022 3日目の記事です。 はじめに こんにちは。ニフティ会員システムグループのたけろいどです。 今回は業務改善の一環としてGoogle拡張機能を作ってみました。そのとき躓いたポイントを書いていきます。 背景 みなさんはGoogle拡張機能を作ったことはありますか? 低コストで行える業務改善の手段の一つとしてGoogle拡張機能はとてもよい選択肢です。 私も面倒だなと感じていた作業があったのでGoogle拡張機能をつかって作業の簡略化を図りました。そこで得た知見をみなさんに共有出来たらと思います。 今回はフレームワークにSvelteを使用しています。Svelteについては過去に記事で触れています。 そちら をご覧ください。 また、躓きポイントはサービス固有のものではなくGoogle拡張機能やSvelteの全般的なものを紹介するつもりです。誰かの助けになれば幸いです。 躓き SvelteとCRXJSの相性が悪い 当初 CRXJS を使用して開発環境を整えようとしました。CRXJSとはGoogle拡張機能開発時のVite環境でHMRが可能になるなど開発のサポートをしてくれるライブラリです。 開発体験があがるためモチベの維持や素早いデプロイなど様々なメリットなどあったのですが、インストール時Svelteと依存関係でコンフリクトが起こりました。 ドキュメント にスタートガイドもなく、Svelteのバージョンを3.49.0からいくつか下げるなど行ったのですがうまくいきませんでした。 時間を無駄に浪費していたため、HMRの導入をあきらめ気合でビルドする方向で舵取りしました。 悔しい躓きの一つです ClipBoard APIを動かすのは大変 ブラウザ上でコピペを行うには2種類の方法があります。それがClipBoard APIとexecCommandです。execCommandはすでに非推奨となっておりClipBoard APIを使うのが通常です。しかし拡張機能上ではClipBoard APIを動かすことは困難です。Permission関係なのかGoogle拡張機能の対応が遅れているのか、これだという原因は不明です。 回避方法はありますが こちら の方法少々コストが高く、スマートな方法ではありませんでした。 今回は個人で使うもの&他のGoogle拡張機能のコードでもexecCommandが使用されていたため、execCommandで実装しました。 execCommandやClipBoard APIの仕様や使い方はMDNを参考にしました。 コンポーネントの再レンダリングについて SvelteはRefなどの変数に更新があればコンポーネントを自動的かつ部分的に再レンダリングしてくれます。 この際即時に再レンダリングするわけではなく、ほかに更新するものがないか一旦確認してから再レンダリングします。 つまり再レンダリング後の値を参照するにはすこしだけ待たなければいけません。そこで tick() が役に立ちます。詳細は参考に置いておきます。 値が参照できないなどの問題があった場合は tick() を使うことも検討してみましょう。 まとめ 以上3点の躓きでした。 SvelteとGoogle拡張機能の文献や記事はまだ少なく手探りな状態ですが、知的好奇心をくすぐられて楽しかったです。 いくつか妥協はしましたが、無事Google拡張機能も完成し目的も達成できたのでよしとします。 ありがとうございました。 参考 クリップボードとのやりとり tickについて We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering この記事は、 ニフティグループ Advent Calendar 2022 3日目の記事です。 次回もおたのしみに!
概要 この記事は、 ニフティグループ Advent Calendar 2022 2日目の記事です。 こんにちは。ニフティ トップページチームの添野、山田、佐々木、宮本、碇川、渡辺です。今回は共同執筆記事です。 ニフティで開催した障害対応ロールプレイングについてご紹介します。 障害対応ロールプレイングとは、「実際にシステムに障害が起こったことを想定し、擬似的な障害を起こすことで、実際の障害対応をスムーズに行えるようにしよう」というコンセプトでニフティで実施しているイベントです。 詳細内容 担当 開発サイド 障害対応:碇川 補佐兼議事録:佐々木 運営:添野、山田、宮本 企画サイド 関係者への障害告知:渡辺 ロールプレイングで扱った障害 今回の障害対応ロールプレイングで扱った障害について軽くご紹介します。 ニフティトップページでは、ログインしたユーザーのメールやニフティポイントなどの情報を表示しています。各サービスを回らなくても、ニフティトップページにログインするだけで手軽に確認することができてしまうなんて便利ですね! しかし、そんな便利な機能が使えなくなると、ユーザーの方々に大変ご迷惑をおかけしてしまいます。できる限り問題が発生しないよう心がけ、ユニットテストなども実施して日々対策していますが、残念ながら障害とはいつか思わぬ原因で発生してしまうもの。そしていざ障害が発生したとき、あたふたしてしまうだけでは問題です。 ということで 、今回のロールプレイングでは、ユーザーの皆様がよく使う新着メール通数の表示機能に障害を起こすことに決めました。重要な機能にあえて障害を起こすことで、いざというときスムーズに対応できるよう経験を積むことができます。 余談ですが、障害対応ロールプレイングのために、本番環境や開発環境からも完全に独立したロールプレイング専用の環境も用意しました。これなら憂いなく壊し放題です。もちろん、本番と同じアラートやサービス監視用ダッシュボードも完備しています。これなら完璧ですね。 障害!新着メール通数が表示されない! ターゲットが決まったところで、次にどのように障害を発生させるかについてです。ニフティトップページで利用しているAPIは、どれもうっかり外部に公開しようものなら、第三者からの不正アクセスされ放題になってしまいます。これを防ぐため、複数の方法を組み合わせAPIへのアクセスを限定しています。 そして、認証もその仕組みの一つです。つまり、”うっかり運用ミスで認証情報を誤ったものに書き換えた”りすれば、それだけでAPIへのリクエストが失敗するようになり、障害が発生してしまいます。人間のうっかりミスで発生する障害……気をつけてはいても起こってしまいそうです。 障害の原因としてはあっさりしたものですが、もちろん実際の障害対応者にはいろいろと考えることがあります。単にニフティトップページ上の表示では、新着メール通数が表示されないだけです。APIからデータを取得していることを理解していても、どこで問題が発生しているのかはすぐにはわかりません。認証以外でも、たとえば以下のようないろいろな原因を考えることができます。 リファクタリングした際にAPIのリクエスト形式が変わってしまった リクエスト先を開発環境のAPIにしてしまった ニフティトップページ開発チームでは管理していないAPI側での障害 クラウドプラットフォーム自体の障害 etc. アラートの種類や吐き出されるログ、監視用ダッシュボード、その他ドキュメントなども用いて、障害の原因を特定していきます。 ロールプレイングの流れ 当日は、事前に打ち合わせをしてお客様問い合わせから障害が発覚したというシナリオでスタートしました。 お問合せとほぼ同時にアラートも発生させています。ですが、実際の障害だと流石にアラートと同時にお問合せまでは発生しないため、もう少し実際の障害と近いような状況を設定する余地があったかもしれません。この辺りはよりリアルなロールプレイをするための反省点でした。 障害の発生を検知した時点で、開発チームが対応に当たります。ロールプレイでは事前に障害対応者と補佐兼議事録役は決めていたので、初動はスムーズです。   障害対応で行う作業について、議事録役がSlackに記録していく しかし、ここでちょっとした問題が発生します。今回想定しているのは「メール通数が表示されない」という障害であり、データの大元を辿ればそれはトップ開発チームの管轄外のAPIです。そのため、もちろん原因としては大元のAPIに何かが発生したということも考えることができます。もちろん原因がトップページ側にある可能性もあり(今回はそのパターンです)、調査は行っていきますが並行してAPIを管理しているチームに問い合わせを行うのは自然なことだと思います。 が、そのことを完全にロールプレイングの運営側は失念してました……。運営3人いたのに、完全に、素で忘れていました。 そこで、急遽運営が一人外部のチーム役として、対応するようにしました。ロールプレイのシナリオを考える段階で、関係者としてどんな人が出てくるか?という考慮が漏れていると、せっかくの障害対応ロールプレイングもどんどん実際の障害と乖離してしまうため、注意が必要です。 こうして想定から漏れていた外部のチームとのやりとりも行いつつ、障害を解消しようと対応に当たっていただきました。単純な障害の解消以外にも、ユーザーの方に向けたお知らせのための正確な情報収集や、企画サイドとのリアルタイムな意思疎通など、なかなか普段の業務では行わない作業も多いです。 また、実際に障害調査を行う過程を経ることで、どのようなドキュメントが不足しているかについても浮き彫りになります。さらにドキュメントとして用意していても、いざというときまだシステムに不慣れなメンバーでもすぐに参照できる場所にあるか?といったこともわかります。とりあえず必要だからとドキュメントを用意しても、その存在自体が共有できていなかったり、わかりやすい場所になく探すために時間がかかってしまっては宝の持ち腐れになってしまいます。普段のシステム運用時から障害発生時の備えはしていますが、その備えが適切かという点について確認することができるのが良いところです。 ロールプレイの終了後には、運営から発生させた障害原因についての詳しい解説と、障害の特定方法についての解説を行いました。今回のロールプレイを通し見つかった問題点を改善しつつ、より安定したシステム運用を行えるようにしていけるようにしていこうと思います。 所感 ロールプレイの参加者と運営から所感を書いていただいています。 参加者目線 佐々木 今回は補佐兼議事録役として参加しました。 ロールプレイングでは一部サービスが動かないというケースの想定でしたが、ドメイン知識やシステム構成の知識が背景として求められるなと実感しました。すでにあるドキュメントやシステムを理解していないと直ぐに答えにはたどり着けないものもあるので、すぐに情報を得られるように普段からチームで整理しておくことも対策になると感じました。 最終解決までは至らなかったですが、今回のように役割分担を決めておくと実際の障害対応時には動きやすくなると思うので活かしていきたいと思います。 渡辺 企画側の立場で障害時の対応を把握するために開発と一緒に参加しました。 対応するエンジニアがどれだけシステムを把握しているかで初動が変わってくるのがよくわかりました。 全員で同じ知識を持つというのも、難しいことではありますが、みなさんに力を付けていただき、企画側はお客様への周知や関係者への情報提供に努め、不測の事態が起こっても障害に対処できる対応力をつけてゆければと思います。 定期的にこのような機会をもって、企画・開発メンバー全員で対応できるようにしようと思います。 碇川 今回私がメインで障害の対応を行ったのですが、自分が持っている情報だけではうまく解決できない点もあり、補佐役の佐々木さんには頼りきりになってしまったことが反省点です。この障害対応ロールプレイングでどのような立ち回りをすればよいかを理解することができたんじゃないかと思います。 今回得た知見や反省点を活かし、実際の障害時はメンバーとしてうまく立ち回っていきたいです。 運営目線 添野 今回は起こりうるシナリオを想定した上で、開催をしましたが、準備を進める中でも不足している情報がちらほらあり、見直す良いきっかけになりました。またロールプレイングの最中では、参加者の動き方を見て、障害への向き合い方が人それぞれだということを改めて感じました。実際の障害時や次回ロールプレイングを開催する際には今回得た知見を活かしていきたいと感じました。 宮本 普段はいかに安定してシステムを運用するかを考えていますが、「ロールプレイングとしてどんな障害を起こそうか?」というのを考えるのは正直結構楽しかったです。他にもいくつか対象とする障害の案が出ていたので、今後も色々試していきたいと思います。ぜひこの記事を読んでいる方々も、どんな障害が起こりそうか考えてみてください。 一方で、実施してはみたいものの、なかなかロールプレイングとして起こし辛い障害(クラウド自体の障害等)もあります。そうした障害にも対応していけるよう、日々対策は怠らないようにしていきたいです。 山田 ロールプレイング実施に当たっては「こう対応するだろうな」という想定シナリオを用意していましたが、実際はその通りに進まない部分が多く苦労しました。運営側は障害前後の状況などを現実に起こりうる形で作っていく必要があり、そのためには普段の運用をより広く理解していなければならないため、大変ですが学びが多かったとも思っています。 本番に近い形での障害を起こす、という部分でまだ再現が難しいものも多いため、カオスエンジニアリングツールなども取り入れて改善していきたいと思います。 まとめ 今回はニフティで開催した障害対応ロールプレイングについて紹介しました。 このように実際の障害対応を意識したチーム体制で動くことで、本番の対応でもスムーズに動ける対応力が身につきますし、「障害に強いシステムへの改善」という目線でも気づきを得るきっかけにつながると思います。 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering 明日は、 @takenokoroid さんのSvelteに関する記事です。 お楽しみに!