TECH PLAY

ニフティ株式会社

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

487

iwillblog . こんにちは、Ryuseiです。普段はマイ ニフティというiOS/Androidアプリの開発をしています。 先日、弊社エンジニア3名でiOSDC Japan 2024にリアル参加してきました!(私はDay 1とDay2の一部に参加させていただきました。) 宣言通りブログを書きます! iOSDCとは iOSDC Japan 2024はiOS関連技術をコアのテーマとしたソフトウェア技術者のためのカンファレンスです。 https://iosdc.jp/2024/ iOSDC Japanは今年で9回目の開催らしいですが、過去最高人数の参加者になったみたいですね。 私自身は今回が初参加となります! ブース 今回はスタンプラリーがビンゴになっていて、各ブースでスタンプを押してもらって縦横斜めが揃うとクレープかたこ焼きの引換券がゲットできるというものでした。 全ての協賛ブースを制覇してクレープとたこ焼きの引換券をそれぞれ8枚と全制覇特典のiOSDCパーカーをゲットしました! スパイダープラスさんのブースでは風速1.8m/sを目指すゲームで、なんとピッタリ賞をいただきました!!ありがとうございます! セッション 4レーン同時進行で進んでおり、見たいセッションだらけでどこに行こうか迷って大変でした!! iOSの隠されたAPIを解明し、開発効率を向上させる方法 by noppe 開発時に便利そうなAPIがたくさんで実際に使ってみたいと思いました。 UIDebuggingInformationOverlay がまさしく自分が求めていたものでした。 Ditto SDK 紹介: インターネットなしで快適なデータ同期 by 近藤峻輔 dittoというプロトコルは聞いたことありましたがこのセッションを見て活用場面がたくさんありそうだなと思いました。 FlutterのSDKもあるとのことなので実際に使ってみたいです。 Mergeable Libraryで高速なアプリ起動を実現しよう! by giginet 今までふわっとして浅い理解だったFrameworkについてちょっと詳しくなりました! Mergeable Libraryが登場した背景から説明されていたので自分でも理解できました。 月間4.5億回再生を超える大規模サービスTVer iOSアプリのリアーキテクチャ戦略 by 小森 英明 実例の発表はすごく勉強になります! モジュールごとにリリースするのが参考になりました。 LT バドワイザーを片手にたこ焼きを食べながらのLT最高でした! 登壇者の皆さんが上手すぎて面白く、あっという間に終わってしまいました。 LTに完璧なセット 懇親会 さまざまな働き方をしてる方々とお話しさせていただきとても刺激になりました。 お話ししてくださった皆様ありがとうございます! おわりに 今回初参加でしたが、他カンファレンスよりも盛り上がりがすごく驚きました。 また幕間の動画など細かなところから凝っている点にも驚かされました。 セッションからは新たな技術や、より深い知識を得られてとても勉強になりました。 リアル参加することで熱量を直で感じモチベーションが上がっているのを実感しています。 そして、クレープとたこ焼きとバドワイザー最高でした!! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
こんにちは。ニフティ株式会社の村山です。 先日 Python のフォーマッタである Black のアップデートを行った際にアップデート前後でフォーマット結果に差分が出たため原因を調べたときのお話です。結論から言ってしまえば、行長判定において日本語が二文字分としてカウントされるようになっていました。 別に大きく困ったわけではないですが小ネタ共有程度に。 Black 23.3.0 から Preview 機能として、行の長さの計算時に Unicode の East Asian Width を加味して計算する機能が追加されました。( リリースノート ) これまでは行の長さは len() によって計算されており、ひらがなもアルファベットも等しく1文字として扱われていました。 East Asian Width は Unicode のプロパティであり、詳細は UAX #11 に記載されていますが、かなり色々無視して大幅にざっくり言うと全角で表示されるような文字は2文字分としてカウントされるということです。 24.1.0 以降ではこれが標準の挙動になっていたみたいです。リリースノートには記載がありませんが、該当箇所を 23.12.1 と 24.1.0 で見比べると Preview かどうかで処理を分ける分岐がなくなっています。 というわけなので、ソース内で日本語を利用している行については、これまでにフォーマッタを素通りしていた行が 24.1.0 以降から突然改行されるようになることがあるみたいでした。 簡単な動作検証もしてみました。 フォーマット前: s = "" # 89文字 s = s + "1234567890123456789012345678901234567890123456789012345678901234567890123456789" # 88文字 s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" # 88文字(最後が全角) s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" Black 23.12.1: s = "" # 89文字 s = ( s + "1234567890123456789012345678901234567890123456789012345678901234567890123456789" ) # 88文字 s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" # 88文字(最後が全角) s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" Black 23.12.1( --preview 指定)、24.1.0: s = "" # 89文字 s = ( s + "1234567890123456789012345678901234567890123456789012345678901234567890123456789" ) # 88文字 s = s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" # 88文字(最後が全角) s = ( s + "123456789012345678901234567890123456789012345678901234567890123456789012345678" ) デフォルトの最大行長 88 文字できっちりコードを書かれている方などは今回の変更で割とコードが読みやすくなるのではないでしょうか。私が普段扱っているプロダクトは 132 文字と長めに設定されていたので個人的にはあんまり影響がありませんでしたが… なお、Python のリンタ兼フォーマッタとして普及してきている Ruff においても line-length の計算には East Asian Width を考慮することとなっているようです( ドキュメント )。古いバージョンの Black から Ruff に移行する場合にもこういったことが起こるかもしれません。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに おはようございます。IWSです 私達のチームではECRへのイメージのビルド、プッシュにGitHub Actionsを使っているのですが、ついでにECSのタスク定義も新しいのを作ってくれると便利なタイミングがあるかな〜とおもったので試しに作ってみました。せっかくなのでどういうのを作ったか書き残しておこうかなと思います。 タスク定義を作る あらかじめビルドしてECRにプッシュするところまでは出来ている状態で作っていきます。 タスク定義の作成には AWS CLI の aws ecs register-task-definition を使います。 オプションを1つずつ指定していっても使えますが、大変なのでタスク定義のJSONファイルを渡す方法を使いましょう。 既存のタスク定義のJSONを取得する タスク定義のJSONファイルを1から用意するのは面倒くさいので、今回は元々あるタスク定義を持ってきて必要な部分だけを変えて作ろうかと思います。まずは既存のタスク定義を取得するところからはじめましょう。 タスク定義の取得には aws ecs describe-task-definition --task-definition <タスク定義の名前> を使います。 コマンドを叩くとこのようなJSONが取得できるはずです。 { "taskDefinition": { "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:task-definition/<タスク定義名>:xx", "containerDefinitions": [ { "name": "name", "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<タグ>", "cpu": 0, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp", "name": "80-tcp" } ], "essential": true, "environment": [ { "name": "ENV", "value": "development" } ], "mountPoints": [], "volumesFrom": [], "dockerSecurityOptions": [], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "hoge", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "hoge" }, "secretOptions": [] }, "systemControls": [] } ], "family": "<タスク定義名>", "taskRoleArn": "<taskRoleArn>", "executionRoleArn": "<executionRoleArn>", "networkMode": "awsvpc", "revision": xx, "volumes": [], "status": "ACTIVE", "requiresAttributes": [ { "name": "com.amazonaws.ecs.capability.logging-driver.awslogs" }, { "name": "ecs.capability.execution-role-awslogs" }, { "name": "com.amazonaws.ecs.capability.ecr-auth" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19" }, { "name": "com.amazonaws.ecs.capability.task-iam-role" }, { "name": "ecs.capability.execution-role-ecr-pull" }, { "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18" }, { "name": "ecs.capability.task-eni" } ], "placementConstraints": [], "compatibilities": [ "EC2", "FARGATE" ], "requiresCompatibilities": [ "FARGATE" ], "cpu": "256", "memory": "1024", "registeredAt": "2024-06-27T17:21:10.983000+09:00", "registeredBy": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/Administrator/hogefuga" }, "tags": [] } これをそのまま使えたら楽なのですが、残念ながらこのままだと使えないので少し整えてあげる必要があります。 JSONを整える やることは taskDefinitionの中身を取り出す taskDefinitionArn を削除 revision を削除 status を削除 requiresAttributes を削除 compatibilities を削除 registeredAt を削除 registeredBy を削除 です。 GitHub Actions ではデフォルトで使える jq コマンドで整形すると簡単です。 # 取得したタスク定義JSONからいらない部分を削除 jq '.taskDefinition | del (.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)' > task-def.json jqコマンドで整形後のタスク定義JSON { "containerDefinitions": [ { "name": "name", "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<タグ>", "cpu": 0, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp", "name": "80-tcp" } ], "essential": true, "environment": [ { "name": "ENV", "value": "development" } ], "mountPoints": [], "volumesFrom": [], "dockerSecurityOptions": [], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "hoge", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "hoge" }, "secretOptions": [] }, "systemControls": [] } ], "family": "<タスク定義名>", "taskRoleArn": "<taskRoleArn>", "executionRoleArn": "<executionRoleArn>", "networkMode": "awsvpc", "volumes": [], "placementConstraints": [], "requiresCompatibilities": [ "FARGATE" ], "cpu": "256", "memory": "1024" } これでタスク定義が作れるJSONになりました! イメージを変える ここまでで aws ecs register-task-definition に渡せるJSONファイルが用意できました。ですが、中身の設定は当然前のタスク定義のままです。そのため、このままタスク定義を作成しても前と同じものが出来てしまうだけになります。 今度は中身の設定を変えていきましょう! 今回は使用するイメージ( containerDefinitions[0].image )のタグの部分だけを変更します。 containerDefinitions[0].image はこうなっています "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<タグ>" : の後がタグになっているので split を使って古いタグを取得して置き換えてしまいましょう # imageのタグ部分だけを取得する PREVIOUS_IMAGE_TAG=$(cat task-def.json | jq -r '.containerDefinitions[0].image | split(":")[1]') 古いタグが取得できたらあとは新しく設定するタグと置き換えるだけです。 # $PREVIOUS_IMAGE_TAG(古いタグ) を $IMAGE_TAG(新しいタグ)で置き換え cat task-def.json | jq --arg PREVIOUS_IMAGE_TAG "$PREVIOUS_IMAGE_TAG" --arg IMAGE_TAG "$IMAGE_TAG" '.containerDefinitions[0].image |= sub($PREVIOUS_IMAGE_TAG; $IMAGE_TAG)' > new-task-def.json jq の sub() を使うことで対象の部分を置き換えることができます。jqはなんでもできますね…… "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<古いタグ>" ↓ "image": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/<ECR名>:<新しいタグ>" これで新しいタスク定義のJSONファイルができました! タスク定義を作成 ここまできたらあとは完成したJSONファイルを使ってタスク定義を作るだけです。 # タスク定義を作成 aws ecs register-task-definition --cli-input-json fileb://new-task-def.json --cli-input-json を使うことでパラメーターをJSONファイルで渡すことが出来ます。 このコマンドが成功すればタスク定義にあたらしいリビジョン番号が増え、設定に新しいイメージタグが書かれているはずです。 どうでしょう?タスク定義は出来ましたか? あとはこのタスク定義を使って aws ecs update-service なりしてあげれば、新しいタスク定義を使用したECSタスクが立ち上がってくると思います。 最後に自分の作ったコードを一部載せておくので良ければ参考にしてください。 タスク定義作成部分コード 事前にAWS credentialsやイメージのビルドなどをしてください。 - name: Task Definition Update id: update-task-definition run: | # Create a new revision of the task definition aws ecs describe-task-definition --task-definition $TASK_DEF_NAME | jq '.taskDefinition | del (.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)' > task-def.json PREVIOUS_IMAGE_TAG=$(cat task-def.json | jq -r '.containerDefinitions[0].image | split(":")[1]') cat task-def.json | jq --arg PREVIOUS_IMAGE_TAG "$PREVIOUS_IMAGE_TAG" --arg IMAGE_TAG "$IMAGE_TAG" '.containerDefinitions[0].image |= sub($PREVIOUS_IMAGE_TAG; $IMAGE_TAG)' > new-task-def.json aws ecs register-task-definition --cli-input-json fileb://new-task-def.json - name: ECS Update id: update-ecs run: | # Update the ECS service with the latest task definition. aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --desired-count $DESIRE_COUNT --task-definition $TASK_DEF_NAME --force-new-deployment まとめ 今回はGitHub Actionsでのタスク定義の作成について書きました。 最近はGitHub Actionsを使ってAWSのリソースを作ったりイメージをPushしたりいろいろなことをやったりしています。今回もそのなかで試しにやってみたことを記事にしてみました。いつか何かしらの参考になればうれしいです。 ここまでお読みいただきありがとうございました! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24新卒リレーブログ」の記事です。 はじめに こんにちは。 初めまして、新卒1年目のけにです。 現在OJT期間で 課金システムチーム に所属しており、社内向けのAPIの開発に携わっています。そのAPIに対して負荷テストを行うにあたり、チーム内で負荷テストツールとしてk6を採用するという話になりました。 k6は簡単にテストシナリオを書けるツールでありながら、非常に強力な負荷テストを実行することができます。本記事では、そのk6について基本的な使い方からinfluxDBとGrafanaを用いた結果の可視化方法まで、調べた内容について自分の理解の整理も含めて記事にしたいと思います。 負荷テストを行う理由 現在のAPI開発において、ユーザー数の急激な増加や予期しないトラフィックの集中が起こることを想定し、それら状況下においてもシステムが安定して機能することが求められます。そのため、パフォーマンスの検証、限界値の測定、リソース使用状況の把握など、対象のシステムの品質や信頼性、リソースの最適化などを目的とする負荷テストは非常に重要な役割を持ちます。 今回私達のチームでは、開発したAPIが実用に耐えうる構成となっているのかについて実際のアクセス数を元に確認することを主な目的として、スパイクテストとストレステストを行いました。 想定される範囲で急激な負荷の上昇が起きたとしても想定通りの速度が出せるか 想定される以上の高負荷がかかった場合でもAPIは正常に動き十分なレスポンス速度を出すことができるか 長時間負荷がかかった場合にどのような挙動をするのか などです。また、負荷テストを実行した際に CPUやメモリなどのリソースは現在の構成で適切なものになっているか API / サーバーの設定は適切なものになっているか についても確認を行いました。 これらにより、システムの限界値や潜在的な問題点を事前に把握し必要な対策を講じることで、システムの品質と信頼性を向上させユーザー満足度を高めることに繋がります。 ツールの選定 負荷テストを行うことができるツールは様々あります。 それらのうち、無料で利用できる7つを比較してみます。 ツール名 公式 シナリオ記述言語 結果確認 方法 詳細 k6 k6.io , github JavaScript grafana / CUI / Cloud Golangで開発され、高負荷環境でもスムーズに動作、拡張性が高い Apache Bench apache.org CLI CUI 単一のURLへのリクエストを生成するツールのため、シナリオベースでWebアプリケーションをテストすることには向いていない Apache JMeter jmeter.apache.org , github GUI / Java GUI 拡張性が高く、結果可視化方法が豊富 Tsung tsung , github XML tsung-recorder Erlang言語で開発され、データベースやメッセージングシステムのテストを行える Gatling gatling.io , github Scala .html JavaVM上で動作 Vegeta vegeta , github Golang .html / plotコマンド 同時接続数やリクエストレートの詳細な制御が可能 Locust locust.io , github Python webサーバ スケーラブルで、分散実行が可能 負荷テストツールの比較 私達のチームでは、主に Python を使用していることから Locust と、今後チーム内に導入するにあたって学習コストが低い JavaScript で記述でき、現在も盛んに開発が行われている k6 が選択肢に上がりました。それらの内、OSSでアップデートが盛んに行われているため新しい技術に対応可能である点から、 k6 を採用することにしました。 実行環境 準備 今回は以下のGrafanaが用意している k6のリポジトリ を使用します。 k6で行った負荷テスト結果をinfluxDBに保存し、Grafanaを用いて可視化します。 # clone git clone https://github.com/grafana/xk6-output-influxdb.git エンドポイントについては、今回は こちら を使います。 これは、 k6 の実験用HTTP、 WebSocket API のコレクションとなっています。 このうち https://test-api.k6.io/public/crocodiles/{id} に対して負荷テストを行います。 注意点として、これは共有テスト環境であるため高負荷テストは避ける必要があります。 APIの挙動 curlコマンドを用いて一度APIを叩いてみます。 叩くことができるidの一覧は以下の結果のとおりです。 curl https://test-api.k6.io/public/crocodiles/ [ { "id": 1, "name": "Bert", "sex": "M", "date_of_birth": "2010-06-27", "age": 14 }, { "id": 2, "name": "Ed", "sex": "M", "date_of_birth": "1995-02-27", "age": 29 }, { "id": 3, "name": "Lyle the Crocodile", "sex": "M", "date_of_birth": "1985-03-03", "age": 39 }, { "id": 4, "name": "Solomon", "sex": "M", "date_of_birth": "1993-12-25", "age": 30 }, { "id": 5, "name": "The gharial", "sex": "F", "date_of_birth": "2004-06-28", "age": 20 }, { "id": 6, "name": "Sang Buaya", "sex": "F", "date_of_birth": "2006-01-28", "age": 18 }, { "id": 7, "name": "Sobek", "sex": "F", "date_of_birth": "1854-09-02", "age": 169 }, { "id": 8, "name": "Curious George", "sex": "M", "date_of_birth": "1981-01-03", "age": 43 } ] それを元に対象のAPIを叩いてみます。返却値は以下のようになっています。 curl https://test-api.k6.io/public/crocodiles/1/ { "id": 1, "name": "Bert", "sex": "M", "date_of_birth": "2010-06-27", "age": 14 } プロキシ設定の変更 本リポジトリでは、 k6 → influxdb → grafana の順にデータが送られます。 コンテナ間の通信が発生するため、 ~/.docker/config.json にproxyが設定されていると503エラーとなる可能性があります。そのため、設定をしている人は修正が必要になります。 シナリオ シナリオの選定 条件毎に、必要なシナリオに応じてパターンを考える必要があります。 特にGrafanaでは、負荷テストの種類を6つ挙げています。( 参考 テスト名 時間 内容 Smoke test 数分、数秒    最小限の負荷でシステムの機能を検証し、基準となるパフォーマンス値を収集するテスト Average-load test 中(5〜60分) 標準的な負荷下でシステムがどのように動作するかを評価するテスト Stress test 中(5〜60分) トラフィックのピーク時の負荷でシステムがどのように機能するかを調べるテスト Spike test 長時間 突然の使用状況の急増に対してシステムが耐えて機能するかどうかを検証するテスト Breakpoint test 数分 システムの限界を計測するために行うテスト Soak test 必要な限り 数時間から数日の長期間行う平均負荷テスト 負荷テストのシナリオ例 シナリオの実装 k6 テスト スクリプトでは、 load_test.js 内の options オブジェクトを使用してシナリオを構成できます。 各シナリオには一意の名前を付け、 executor タイプとその構成を指定する必要があります。 export const options = { scenarios: { spike: { executor: "ramping-vus", startVUs: 0, stages: [ { duration: "5m", target: 100 }, { duration: "2m", target: 0 }, ], }, stress: { executor: "constant-vus", vus: 50, duration: "10m", }, }, }; 上記は、スパイクテストとストレステストの例です。 スパイクテスト ramping-vus executor を使用 仮想ユーザーの秒間アクセス数を5分掛けて 0 から 100 まで徐々に増やし、2分掛けて 0 まで減らしている ストレステスト constant-vus executor を使用 10 分間、秒間50回の仮想ユーザーからのアクセスを保っている シナリオを複数指定している場合、上から順に実行されます。 executorには他にも種類があり、それぞれ記述方法が異なります。 参考: https://k6.io/docs/using-k6/scenarios/executors/#all-executors 負荷テストの実行 今回は上記のうち、ストレステストとスパイクテストを行ってみます。 実行コード import http from 'k6/http'; // スパイクテストの設定 export const options = { scenarios: { // スパイクテスト // 短時間で一気に負荷が上昇した場合の負荷テスト spike: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '2s', target: 10 }, { duration: '1m', target: 0 }, ], }, // ストレステスト // 短時間で一気に負荷が上昇した場合の負荷テスト stress: { executor: "constant-vus", vus: 50, duration: "10m", }, }, }; export default function () { // APIリクエストを送信 const id = Math.floor(Math.random() * 8); const response = http.get(`https://test-api.k6.io/public/crocodiles/${id}`, { tags: { "id": id, "all": "all" } // Grafana表示用にタグを設定 }); // レスポンスをチェック console.log(`VU: ${__VU}, Iteration: ${__ITER}, ID: ${id}, Status: ${response.status}`); } # コンテナの起動 ## k6以外を立ち上げる docker compose up influxdb grafana -d # テスト実行 docker compose run --rm -T k6 run - < samples/load_test.js 起動画面 結果の確認 1. k6 実行結果 確認できるメトリクスは以下の通りになります。 メトリクス名 説明 data_received 受信したデータ量 data_sent 送信されたデータ量 http_req_blocked リクエストを開始するまでにブロックされた時間 http_req_connecting リモートホストとのTCPコネクションの確立にかかった時間 http_req_duration リクエストを送ってから帰ってくるまでの時間 ( http_req_sending + http_req_waiting + http_req_receiving ) http_req_failed リクエストに失敗した数 http_req_receiving リモートホストからの応答データ受信にかかった時間 http_req_sending リモートホストへのデータ送信にかかった時間 http_req_tls_handshaking リモートホストとのTLSセッションのハンドシェイクにかかった時間 http_req_waiting リモートホストからの応答待ち時間 http_reqs 生成した HTTP リクエストの合計数 iteration_duration 1回のイテレーションの実行に要した時間 iterations virtual usersが実行したデフォルト関数の回数 vus virtual users、同時にアクセスする仮想ユーザー数 vus_max 仮想ユーザーの最大可能数 確認できるメトリクスの一覧 特に確認する必要があるのは http_req_duration , http_reqs , http_req_failed の3つです。 リクエストにかかった時間、リクエストの数、失敗した数です。 この結果から、このAPIのレスポンス速度は約200ms程度であり、ほとんどの場合に於いてその速度を出していることがわかります。 平均値 最小値 中央値 最大値 90パーセン タイル 95パーセン タイル 成功数 失敗数 203.49ms 176.55ms 200.83ms 1.21s 216.69ms 223.42ms 3112回 0回 実行結果 2. grafana コンテナを立ち上げた際に立ち上がっているため、そちらにアクセスします。 アクセス先: http://localhost:3000/dashboards これでは結果が分かりにくいため、凡例とクエリを修正します。 修正先: grafana/dashboards/xk6-output-influxdb-dashboard.json ︎ 修正コード { "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "gnetId": null, "graphTooltip": 1, "iteration": 1677877089957, "links": [], "liveNow": false, "panels": [ { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 }, "id": 2, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "http_reqs")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r["_field"] == "value")n |> group(columns: ["_measurement"])n |> aggregateWindow(every: v.windowPeriod, fn: sum, createEmpty: false)n |> yield(name: "sum")", "refId": "EACH" } ], "title": "Requests Made", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 }, "id": 12, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "http_req_failed")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r["_field"] == "value")n |> group(columns: ["_measurement"])n |> aggregateWindow(every: v.windowPeriod, fn: sum, createEmpty: false)n |> yield(name: "sum")", "refId": "EACH" } ], "title": "HTTP Failures", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 }, "id": 13, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [ "max" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "http_reqs")n |> filter(fn: (r) => r["_field"] == "value")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> group()n |> aggregateWindow(every: v.windowPeriod, fn: sum, createEmpty: false)", "refId": "EACH" } ], "title": "Peak RPS", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ms" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 12, "y": 0 }, "id": 14, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "http_req_duration")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> group()n |> quantile(q: 0.95, method: "exact_mean")", "refId": "EACH" } ], "title": "P95 Response Time", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "decbytes" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 16, "y": 0 }, "id": 15, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "data_received")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> group()n |> sum()", "refId": "EACH" } ], "title": "Data Received", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "decbytes" }, "overrides": [] }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 0 }, "id": 16, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.2.6", "targets": [ { "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "data_sent")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> group()n |> sum()", "refId": "EACH" } ], "title": "Data Sent", "type": "stat" }, { "datasource": null, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "left", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "EACH", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "displayName": "${__field.labels.id}", "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byFrameRefID", "options": "VUS" }, "properties": [ { "id": "displayName", "value": "Active VUs" }, { "id": "custom.lineStyle", "value": { "dash": [ 10, 10 ], "fill": "dash" } }, { "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } } ] }, { "matcher": { "id": "byFrameRefID", "options": "RPS" }, "properties": [ { "id": "displayName", "value": "RPS" } ] }, { "matcher": { "id": "byFrameRefID", "options": "EACH" }, "properties": [ { "id": "unit", "value": "ms" }, { "id": "custom.axisPlacement", "value": "right" } ] }, { "matcher": { "id": "byFrameRefID", "options": "ALL" }, "properties": [ { "id": "unit", "value": "ms" }, { "id": "displayName", "value": "ALL" } ] } ] }, "gridPos": { "h": 11, "w": 24, "x": 0, "y": 4 }, "id": 11, "options": { "legend": { "calcs": [ "min", "mean", "max", "lastNotNull" ], "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "pluginVersion": "8.3.1", "targets": [ { "hide": false, "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "http_req_duration")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> filter(fn: (r) => r.status == "200")n |> group(columns: ["all"], mode:"by")n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)n |> yield(name: "mean")n", "refId": "ALL" }, { "hide": false, "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r._measurement == "http_req_duration")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r._field == "value")n |> filter(fn: (r) => r.status == "200")n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)n |> yield(name: "mean")", "refId": "EACH" }, { "hide": false, "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "vus")", "refId": "VUS" }, { "hide": false, "query": "from(bucket: v.defaultBucket)n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)n |> filter(fn: (r) => r["_measurement"] == "http_reqs")n |> filter(fn: (r) => r["testid"] =~ /${testid:regex}/)n |> filter(fn: (r) => r["_field"] == "value")n |> group(columns: ["_measurement"])n |> aggregateWindow(every: 1s, fn: sum, createEmpty: false)n |> yield(name: "sum")", "refId": "RPS" } ], "type": "timeseries" } ], "refresh": false, "schemaVersion": 32, "style": "dark", "tags": [], "templating": { "list": [ { "allValue": null, "current": { "selected": false, "text": "All", "value": "$__all" }, "datasource": null, "definition": "import "influxdata/influxdb/schema"nschema.tagValues(bucket: v.defaultBucket, tag: "testid")", "description": null, "error": null, "hide": 0, "includeAll": true, "label": null, "multi": false, "name": "testid", "options": [], "query": "import "influxdata/influxdb/schema"nschema.tagValues(bucket: v.defaultBucket, tag: "testid")", "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "type": "query" } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "K6 Test Results", "uid": "4sk8QaJVx", "version": 1 } 修正後結果 凡例をID毎に設定したことで、各IDのレスポンス速度が可視化できるようになりました。 凡例名 概要 ALL 全ID平均の統計値 (クエリを追加) 各ID (タグで変更可能) (凡例を修正) 対象IDの統計値 Active VUs (Virtual Users) 動いたVirtual Users の数 RPS (Requests Per Second) 1秒あたりのリクエスト数 設定した凡例 修正前 修正後 全APIのレスポンスの平均速度 別途クエリを作成し表示しています。 毎秒毎に平均を算出したことで、全IDの平均値の推移を可視化するようにしました。 このAPIでは、レスポンス速度の平均が197msであることがわかり、ほぼ一定の速度を出せていることがわかります。 VUs (Virtual Users) このグラフは一秒間に何回のアクセスがあったかを表しています。 シナリオの通り、開始2秒でアクセス数が20まで跳ね上がり、その後2分をかけて徐々に数が減っている様相が見て取れます。 RPS (response per second) 直前の一秒間に帰ってきたリクエストの数を返しています。 時間経過で徐々に減少していることが見て取れ、想定通りの挙動をしていることがわかります。 3. データのインポート コンテナを立ち上げた際に立ち上がっているため、そちらにアクセスします。 csv形式でデータを保存する場合の手順は以下の通りです。 influxDB ( http://localhost:8086/ ) にアクセスする Data → Buckets → demo に遷移する フィルター設定をする ( demo / http_req_duration を選択) (その他フィルターは場合により設定) submit を押す 下図赤枠のボタンを押す おわりに 今回はinfluxdb, grafanaを用いたk6の使い方についてまとめてみました。k6は簡単に使えるツールでありながら、非常に強力な負荷テストを実行することができます。influxDBとGrafanaを組み合わせることで、テスト結果を視覚的に分析することも可能になります。 他にも、使用しているサーバーのCPU使用率やRAM使用率の計測や、発生したエラーの調査などのアプローチが必要になります。 はじめにも触れている通り、そういった手順が品質や信頼性の担保に繋がります。システムのパフォーマンスを把握し、潜在的な問題を早期に発見するために非常に重要です。そのため負荷テストは単なる技術的な作業ではなく、ユーザー体験の向上やビジネスの成功に直結する取り組みとなります。 今回紹介した手法を参考に、皆さんのプロジェクトでより安定したシステム運用につなげていただければ幸いです。 次回は、後藤さんです。どんな記事になるか楽しみですね! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
ニフティ社内で使われているGitHub Enterpriseの管理者をしている石川です。 今年の4月にself-hosted runnerとしてCodeBuildを利用できるようになる嬉しいアップデートがありましたね。 CodeBuild-hosted GitHub Actions runnerは、4月時点ではリポジトリとCodeBuildプロジェクトが1対1で利用する方法しか取れませんでしたが、6月のアップデートでOrganizationやEnterpriseレベルでひとつのCodeBuildをRunnerとして指定することができるようになりました! AWS CodeBuild now supports organization and global GitHub webhooks – AWS ということで今回はOrganizationレベルでのRunner設定を試してみます。 CodeBuildの設定 Runnerの利用制限 GitHub Actionsの設定 Jobを実行するまでのセットアップも課金対象 リポジトリごとにビルド費用を算出できるか まとめ CodeBuildの設定 Terraformだと以下のようなコードで設定できます。 resource "aws_codebuild_project" "main" { ... source { type = "GITHUB" location = "CODEBUILD_DEFAULT_WEBHOOK_SOURCE_LOCATION" } environment { compute_type = "BUILD_LAMBDA_1GB" image = "aws/codebuild/amazonlinux-aarch64-lambda-standard:nodejs20" image_pull_credentials_type = "CODEBUILD" type = "ARM_LAMBDA_CONTAINER" } ... } resource "aws_codebuild_webhook" "main" { project_name = aws_codebuild_project.main.name build_type = "BUILD" filter_group { filter { pattern = "WORKFLOW_JOB_QUEUED" type = "EVENT" } } scope_configuration { name = "your-organization-name" scope = "GITHUB_ORGANIZATION" } } CFnでも作れるとよかったのですが、まだ対応していないようです。 Global webhooks and GitHub Enterprise webhooks are not supported by AWS CloudFormation. Filter GitHub organization webhook events (AWS CloudFormation) – AWS CodeBuild デプロイ前に注意が必要なのが、事前にGitHub認証情報をCodeBuildに手動で登録する必要があります。上記コードでは除いてしまってますが、PATやOAuthを使いSecret Managerに認証情報を格納した場合、CodeBuildのサービスロールにシークレットを取得する権限も必要となります。 参考: GitHub and GitHub Enterprise Server access in CodeBuild – AWS CodeBuild CodeBuildのプロジェクトが作成されると同時にOrganizationにWebhookが登録されます。 管理者視点だとちょっと困ったことがあって、Webhooks一覧を見てもどのAWSアカウントのCodeBuildが呼ばれるのかここからだと識別できません。 GitHub認証情報管理の問題もありますし、Organization共通のRunnerは専用のAWSアカウントで管理した方がいいかもしれません。 Runnerの利用制限 Organization全体で使えるようなRunnerとして登録しつつも、利用方法に制限を加えたい場合があります。その場合はWebhookのFilterを使って制限を行います。 参考: aws_codebuild_webhook | Resources | hashicorp/aws | Terraform | Terraform Registry filter_group { filter { pattern = "WORKFLOW_JOB_QUEUED" type = "EVENT" } filter { pattern = "your-workflow-name" type = "WORKFLOW_NAME" } } 上記の例だと特定のワークフロー名で実行されたGitHub Actionのときのみ利用できるRunnerとなります。ほかにもいろいろな条件でFilterを作成することができます。 Terraformに限っては、まだ REPOSITORY_NAME を使っての制限は作れないようなので、ここはアップデートを待ちましょう。 参考: [Enhancement]: Add REPOSITORY_NAME event type filter to aws_codebuild_webhook resource · Issue #38868 · hashicorp/terraform-provider-aws GitHub Actionsの設定 組織レベルでもリポジトリレベルでも変わりません。 WorkflowのYAMLに以下のフォーマットでRunnerを指定すれば動作します。 組織内のリポジトリであれば、これだけで利用できるはかなりお手軽ですね。 runs-on: codebuild-<project-name>-${{ github.run_id }}-${{ github.run_attempt }} 参考: Tutorial: Configure a CodeBuild-hosted GitHub Actions runner – AWS CodeBuild Jobを実行するまでのセットアップも課金対象 CodeBuildで実行する場合、毎回Runnerのセットアップと登録処理が行われます(buildspecの指定はできないため、どんなイメージを用意しても必ず実行される)。これが20〜30秒かかっていて、数秒で終わるJobだろうとこの時間が追加で実行時間として加算されます。 GitHub Actionsよりも格段に安いですが、Job実行時間以外も計算に入れておかないといけない点に注意が必要です。 [Container] 2024/08/21 02:13:32.738199 YAML location is /tmp/codebuild/readonly/buildspec.yml [Container] 2024/08/21 02:13:32.738443 Processing environment variables [Container] 2024/08/21 02:13:33.141679 Moving to directory /tmp/codebuild/output/src653/src/1c4cfb37_4d30_4c6c_bb71_0197866a72cd [Container] 2024/08/21 02:13:33.315509 Ignoring BUILD phase commands for self-hosted runner build. [Container] 2024/08/21 02:13:33.315542 Checking if docker is running. Running command: docker version [Container] 2024/08/21 02:13:33.317171 Warning: Docker not installed. GHA self-hosted runner build triggered by /actions/runs/10482304118/job/29033188127 Creating GHA self-hosted runner workspace folder: actions-runner Downloading GHA self-hosted runner binary % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 25 136M 25 35.1M 0 0 46.6M 0 0:00:02 --:--:-- 0:00:02 46.5M 65 136M 65 90.1M 0 0 51.1M 0 0:00:02 0:00:01 0:00:01 51.1M 100 136M 100 136M 0 0 51.9M 0 0:00:02 0:00:02 --:--:-- 51.9M Configuring GHA self-hosted runner -------------------------------------------------------------------------------- | ____ _ _ _ _ _ _ _ _ | | / ___(_) |_| | | |_ _| |__ / \ ___| |_(_) ___ _ __ ___ | | | | _| | __| |_| | | | | '_ \ / _ \ / __| __| |/ _ \| '_ \/ __| | | | |_| | | |_| _ | |_| | |_) | / ___ \ (__| |_| | (_) | | | \__ \ | | \____|_|\__|_| |_|\__,_|_.__/ /_/ \_\___|\__|_|\___/|_| |_|___/ | | | | Self-hosted runner registration | | | -------------------------------------------------------------------------------- # Authentication √ Connected to GitHub # Runner Registration √ Runner successfully added √ Runner connection is good # Runner settings √ Settings Saved. Running GHA self-hosted runner binary √ Connected to GitHub Current runner version: '2.319.1' 2024-08-21 02:14:00Z: Listening for Jobs 2024-08-21 02:14:04Z: Running job: schedule_task 2024-08-21 02:14:17Z: Job schedule_task completed with result: Succeeded √ Removed .credentials √ Removed .runner Runner listener exit with 0 return code, stop the service, no retry needed. Exiting runner... リポジトリごとにビルド費用を算出できるか CUR/CUR2 で確認しましたが、CodeBuildのビルドステータスにあるイニシエータの情報はありませんでした。CloudWatch Logsに出力できるCodeBuildのログからもリポジトリの情報は標準では出力されておらず。残念ながらCURやCodeBuildのログから算出することはできませんでした。 なので、ちょっと手をかけて算出する必要があります。 ざっくり分かればいい 該当のRunnerが使われているWorkflowを特定 Actions Usage Metricsから実行時間を確認 参考: Viewing usage metrics for GitHub Actions – GitHub Enterprise Cloud Docs 詳細に算出したい EventBridgeには欲しい情報が揃っていたので、それをS3に保存してAthenaで集計 EventBridge → Firehose or Lambda → S3 ← Athena 参考: Build notifications sample for CodeBuild – AWS CodeBuild まとめ 組織レベルのRunnerを作るのは簡単 GitHub Organization ownerの認証情報管理とRunnerの置き場はちゃんと考える必要がある リポジトリごとの利用量や費用算出は一手間かければできる CI/CDを組織全体で共有管理している場合は、とても嬉しいアップデートですね。 一方各アカウントでCI/CDを管理している場合は、Organization owner権限が必要なポイントがあり、複数リポジトリで利用したいから使いたいという用途では少々使いにくい面もあります。 GitHub認証情報をどう管理していくかという問題とも紐づいているため、そこも考慮して利用を検討していくといいのではないかと思います。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに terraform で Amazon EventBridge → Amazon CloudWatch Logs の構成を作るときの例を紹介します。若干詰まった部分も書いているので、参考にしていただけると幸いです。 実装例 例えば、Amazon ECSの停止理由をAmazon CloudWatch Logs に残したい場合は以下の構成になります。 Amazon ECS → Amazon EventBridge → Amazon CloudWatch Logs これをTerraformで宣言すると、以下の様になります。 # ECS stopped tasks resource "aws_cloudwatch_event_rule" "ecs_stopped_tasks_event_rule" { name = "ECSStoppedTasksEvent" description = "Triggered when an Amazon ECS Task is stopped" event_pattern = jsonencode({ source = ["aws.ecs"] "detail-type" = ["ECS Task State Change"] detail = { desiredStatus = ["STOPPED"] lastStatus = ["STOPPED"] } }) state = "ENABLED" } resource "aws_cloudwatch_event_target" "ecs_stopped_tasks_event_target" { target_id = "ECSStoppedTasks" rule = aws_cloudwatch_event_rule.ecs_stopped_tasks_event_rule.name arn = "${aws_cloudwatch_log_group.ecs_stopped_tasks_event.arn}:*" } # ECS stoppped task resource "aws_cloudwatch_log_group" "ecs_stopped_tasks_event" { name = "/aws/events/ECSStoppedTasksEvent" retention_in_days = 90 } # ECS Scheduled tasks resource policy resource "aws_cloudwatch_log_resource_policy" "log_event_policy" { policy_name = "LogEventsPolicy" policy_document = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Principal = { Service = [ "events.amazonaws.com", "delivery.logs.amazonaws.com" ] }, Action = [ "logs:CreateLogStream", "logs:PutLogEvents" ], Resource = ["${aws_cloudwatch_log_group.ecs_stopped_tasks_event.arn}:*"] } ] }) } # 異常終了時のアラーム設定 resource "aws_cloudwatch_log_metric_filter" "task_failed_log_metric_filter" { name = "TaskFailedLogMetricFilter" log_group_name = aws_cloudwatch_log_group.ecs_stopped_tasks_event.name pattern = "failed" metric_transformation { name = "FailedLogCount" namespace = "TaskLogMetrics" value = "1" } } resource "aws_cloudwatch_metric_alarm" "task_failed_log_alarm" { alarm_name = "TaskFailedLogAlarm" comparison_operator = "GreaterThanOrEqualToThreshold" evaluation_periods = "1" metric_name = aws_cloudwatch_log_metric_filter.task_failed_log_metric_filter.metric_transformation[0].name namespace = aws_cloudwatch_log_metric_filter.task_failed_log_metric_filter.metric_transformation[0].namespace period = "60" statistic = "Sum" threshold = "1" alarm_description = "Alarm when there are task failed log entries" actions_enabled = true alarm_actions = [ aws_sns_topic.unpaid_notificate.arn(任意の通知先) ] } 上記のコードを見てみると、次のような見慣れないリソースが登場します。 aws_cloudwatch_log_resource_policy この設定、実はコンソールから設定・確認できないパラメータになっています。 AWS CLI からは以下の様に確認することができます。 aws logs describe-resource-policies --no-cli-pager { "resourcePolicies": [ { "policyName": "AWSLogDeliveryWrite20150319", "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"AWSLogDeliveryWrite\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"delivery.logs.amazonaws.com\"},\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Resource\":\"arn:aws:logs:ap-northeast-1:786813063316:log-group:/aws/api_gw/unpaid-api:log-stream:*\",\"Condition\":{\"StringEquals\":{\"aws:SourceAccount\":\"AWSAccountID\"},\"ArnLike\":{\"aws:SourceArn\":\"arn:aws:logs:ap-northeast-1:AWSAccountID:*\"}}}]}", "lastUpdatedTime": 1712559794739 }, { "policyName": "LogEventsPolicy", "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"delivery.logs.amazonaws.com\",\"events.amazonaws.com\"]},\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Resource\":\"arn:aws:logs:ap-northeast-1:AWSAccountID:log-group:/aws/events/ECSStoppedTasksEvent:*\"}]}", "lastUpdatedTime": 1720579910972 } ] } リソースポリシーは「〇〇からのアクセスは許可する」というルールです。 コンソールからAmazon EventBridgeを作成すると自動で作られるようですが、Terraformで宣言する場合は明示的に宣言してあげる必要があります。 おわりに 今回はTerraformを利用する場合のAmazon EventBridgeとAmazon CloudWatch Logsを連携する方法について紹介しました。 Terraformを利用して構築する場合、ご参考になれば幸いです。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24新卒リレーブログ」の記事です。 はじめに こんにちは。初めまして、新卒1年目の塚崎です。 現在、ジョブローテの1期目として、第一開発チーム( https://engineering.nifty.co.jp/blog/26940 )でとあるサイトをリニューアルするプロジェクトを進めています。このリニューアルではフロントエンドでNext.jsを採用し、バックエンドではGraphQLを採用しています。今回のリニューアルで私は主にフロントエンドの実装を担当しているのですが、GraphQLについても学んだので、今回はGraphQLを使ったAPIサーバーの実装について記事にしたいと思います。 GraphQLとは? GraphQL( https://graphql.org/ )とは、Meta社によって開発されたWeb APIのクエリ言語です。GraphQLでは、クライアントが必要なデータだけを指定し、サーバーから取得することができるため、REST APIの課題であったデータの過剰取得を防ぎ、効率良くデータの取得が行えます。また、REST APIでは複数のエンドポイントからデータの取得を行うのに対して、GraphQLでは単一のエンドポイントから一度のリクエストで全てのデータを取得することができます。他にもスキーマと呼ばれる型やクエリを定義する仕組みによって、安全に開発ができることも大きなメリットです。 特徴 柔軟なデータ取得 クライアント側で必要なデータだけを指定して取得可能 単一のエンドポイント 単一のエンドポイントで全てのデータ操作が可能 強力な型システム スキーマを定義することで型の安全性が保証される 階層的な構造 データの関係性をクエリの構造に反映できる リアルタイム機能 サブスクリプション機能を利用し、リアルタイムにデータを取得できる バージョン管理が不要 新しいフィールドの追加を容易に行うことができ、バージョン管理が不要 GraphQLサーバーの実装 実際にGraphQLサーバーを実装し、データの取得までをやってみます。 まずは任意の場所でGraphQL用のディレクトリ( graphql-server-example )を作成します。 mkdir graphql-server-example cd graphql-server-example npmで初期化します。 npm init --yes && npm pkg set type="module" 依存ライブラリである@apollo/serverとgraphqlをインストールします。 npm install @apollo/server graphql package.json を開き、 scripts に "start": "node index.js" を追加します。 スキーマの作成 今回は例としてアーティスト情報を返すAPIサーバーを構築してみます。 まず index.js を作成し、以下のようにスキーマ(APIの型やクエリを定義するもの)を書きます。 typeDefs はスキーマを定義する変数です。VSCodeでは以下の拡張機能を追加し、テンプレートリテラルの開始に #graphql と書くことでシンタックスハイライトが機能するようになります。 今回はID、アーティスト名、ジャンルの3つをアーティストオブジェクトとして定義しました。またクエリとして全てのアーティストオブジェクトを取得するクエリも定義しています。 拡張機能(GraphQL: Syntax Highlighting) https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; const typeDefs = `#graphql type Artist { id: String name: String genres: [String] } type Query { allArtists: [Artist] } `; データの作成 次にサーバーから返すデータを作成します。 通常、クライアントに返すデータはデータベースと接続し、そこから取得を行いますが、今回はデータを配列でハードコーディングすることで擬似的に用意します。 const artists = [ { id: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Bring Me The Horizon", genres: ["Metalcore", "Alternative Metal"], }, { id: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "In Flames", genres: ["Alternative Metal"], }, { id: "2400debb-45aa-4467-991f-64063e7753aa", name: "Dream Theater", genres: ["Progressive Metal"], }, ]; リゾルバの作成 ここまででスキーマを定義し、データも用意することができました。あとはサーバーからフロントへデータを返す処理を追加すれば良さそうです。 データを返すリゾルバを定義します。リゾルバとは、データベースなどからデータを取得し、スキーマで定義された型に合わせてデータを返す処理を担当します。 今回、スキーマで定義した allArtists は全てのアーティストオブジェクトを返すクエリのため、用意した配列のデータをそのまま返す処理を追加しています。 実際にはデータベースからデータを取得した際に、それがそのまま返せるケースは少ないので、リゾルバでスキーマと合うように整形してあげる必要があります。 const resolvers = { Query: { allArtists: () => artists, }, }; サーバーの起動 最後にApollo Serverを初期化する処理を追加してあげましょう。 const server = new ApolloServer({ typeDefs, resolvers, }); const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, }); console.log(`Server ready at: ${url}`); サーバーを起動します。 npm start サーバーを起動するとターミナルに以下が出力されていると思うので、リンクをブラウザで開きます。 Server ready at: http://localhost:4000/ リンクを開くと以下のような画面に遷移します。 この画面ではクエリを実行したり、クエリのレスポンス結果を確認したりすることができます。 それでは実際にクエリを実行してみましょう。 実行結果です。クエリで指定したフィールドの値を取得することができました。 { "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ] } ] } } フィールドの追加 次にアーティスト数を取得するフィールドを追加してみましょう。 const typeDefs = `#graphql type Artist { id: String name: String genres: [String] } type Query { allArtists: [Artist], # 以下を追加 totalArtists: Int } `; フィールド( totalArtists )を追加したので、それに対応するリゾルバを作成する必要があります。今回は、アーティスト数を返すリゾルバとしたので、単純にアーティストの配列の長さを返します。 const resolvers = { Query: { allArtists: () => artists, // 以下を追加 totalArtists: () => artists.length, }, }; サーバーを再起動し、変更を反映させます。 リンクを開き画面の左側を見ると、フィールドとして totalArtists が追加されていることが分かります。 クエリを実行してみましょう。 実行結果です。全てのアーティスト情報とアーティスト数を取得することができました。このようにクエリには複数のフィールドを追加して、実行することもできます。こういったケースでは、REST APIでは2つのエンドポイントに対してリクエストを送信する必要がありますが、GraphQLでは1回のリクエストで複数のデータを取得することができます。 { "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ] } ], "totalArtists": 3 } } 最後に各アーティストがリリースしたアルバム情報を取得できるように実装を追加してみます。 アルバムオブジェクトをスキーマとして定義します。またアーティストオブジェクトにリリースしたアルバム情報をリストで返すフィールド( releasedAlbums )も追加してあげましょう。 const typeDefs = `#graphql type Artist { id: String name: String genres: [String] # 追加 releasedAlbums: [Album] } # 追加 type Album { artistId: String name: String } type Query { allArtists: [Artist], totalArtists: Int } `; アルバム情報のデータを用意します。 const albums = [ { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Post Human: Nex Gen", }, { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "That's the Spirit", }, { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Sempiternal", }, { artistId: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "Foregone", }, { artistId: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "Whoracle", }, { artistId: "2400debb-45aa-4467-991f-64063e7753aa", name: "Images and Words", }, ]; リゾルバを追加します。配列の filter メソッドを使用することで、アーティストがリリースしたアルバム情報をリストで取得しています。 const resolvers = { Query: { allArtists: () => artists, totalArtists: () => artists.length, }, // 以下を追加 Artist: { releasedAlbums: (artist) => { return albums.filter((album) => album.artistId === artist.id); }, }, }; サーバーを再起動し、変更を反映させます。 クエリを実行してみます。 以下のようにデータを取得することができたと思います。 { "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ], "releasedAlbums": [ { "name": "Post Human: Nex Gen" }, { "name": "That's the Spirit" }, { "name": "Sempiternal" } ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ], "releasedAlbums": [ { "name": "Foregone" }, { "name": "Whoracle" } ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ], "releasedAlbums": [ { "name": "Images and Words" } ] } ], "totalArtists": 3 } } まとめ 今回は、とあるプロジェクトのリニューアルでGraphQLについて学び、APIサーバーの実装をやってみました。実際にプロジェクトに導入し、実装を進める中で必要なデータだけを簡単に取得できる点や事前にスキーマを固めることでバックエンドの開発を待たずにフロントエンドの実装が進められる点がメリットとして実感できました。しかし、今回学んだ内容はGraphQLの基礎的な内容になるため、キャッシュやスキーマファーストといったパフォーマンスや設計に関する部分もこれから学んでいく必要があると感じました。ニフティでは、書籍購入費用補助制度やUdemyなども使えるので、それらを活用してこれからも勉強していきたいです。 次回は、けにさんです。どんな記事になるか楽しみですね! 参考資料 初めてのGraphQL ― Webサービスを作って学ぶ新世代API Introduction to GraphQL | GraphQL( https://graphql.org/learn/) Introduction to Apollo Server | Apollo GraphQL Docs( https://www.apollographql.com/docs/apollo-server/ ) ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに こんにちは! 今回は、6/20,21のAWS Summit Japan 2024にて開催されたGameDayに初出場し、入賞したので感想などをまとめていきたいと思います! また、先日NIFTY Tech Talkに登壇し、この記事には書ききれなかった詳しい内容も語っています。YouTubeにて録画が公開されており、資料も残っているのでチェックしてみてください! AWS GameDayはネタバレ厳禁なので、公式のブログにて公開されている情報から逸脱しない範囲でまとめていきます https://aws.amazon.com/jp/blogs/news/aws-gameday-at-aws-summit-japan-2024/ AWS GameDayとは AWS GameDay  は、ある課題に対して AWS サービスで解決するための対応力や実装スキルを試すことができる実践形式のワークショップです。 3~4 名でチームを結成し、待ち受けるさまざまなトラブルやクエストをクリアしながら最終ミッションの達成を目指します。 各クエストをクリアするごとにポイントが付与され、最も多くのポイントを獲得したチームが勝者となります。 ゲーミフィケーションされた安全な環境で、楽しみながらさまざまなことを学ぶことができる機会を得られるワークショップです。 https://aws.amazon.com/jp/blogs/news/aws-gameday-at-aws-summit-japan-2024/ 要約すると以下のようになります。 AWSサービスを使って問題を解決する実践的なワークショップ チームで協力し、様々な課題をクリアしてポイントを獲得 ゲーム感覚で楽しみながらAWSのスキルを学べる AWS GameDayを通してさまざまなAWSのサービスを、チームで協力してゲーム感覚で楽しく学ぶことができます。 参加方法 予約必須! 予約制なのでAWSの公式サイトから予約します。 予約フォームが公開されるタイミングですぐ埋まってしまうので運要素が結構強いです。 また、この時に取れなくてもキャンセルが出ることがあるので、たまにサイトの様子を見るとワンチャンスあるかもしれません。 予約が取れなくても諦めない! 当日枠がある可能性があるので、会場で並んで参加することができます。 こちらの告知も公式サイトに出るのでチェックしましょう! 今回のテーマ 今回のテーマはFrugality Fest(節約祭り)ということで、コスト削減がテーマでした。 https://aws.amazon.com/jp/blogs/news/aws-gameday-at-aws-summit-japan-2024/ コスト削減は色々な企業が課題として持っていますし、当社でも近年課題として挙げられているので、学びを業務に活かしやすいテーマだなぁと思いました。 ゲーム中に思ったこと コミュニケーション大事 最初は知らない人同士でチームを組むため、コミュニケーションが取りづらい面がありました。 しかし、わからないことを積極的に発言し、ペアやモブで解決していくことで、徐々にチームワークが形成されていきました。 自分のスキル不足を他のメンバーが補い、逆に他の人が詰まった時は一緒に解決することができました。 GameDayにおいて、コミュニケーションはとても重要だと思いました。 わからないAWSサービスでのタスクを取りやすい タスクの解決方法が詳細に記載されており、非常に分かりやすい構成になっています。 また、リソースの説明や公式ドキュメントへのリンクが提供されているため、不慣れなサービスでも取り組みやすくなっています。 各タスクには入力欄があり、指定された情報(例:作成したリソースのARN)を入力することで課題が解決されます。 入力内容が正しければ成功、間違っていればエラーが表示されるため、視覚的に進捗が確認できます。 さらに、タスクの難易度に応じてポイントが付与されるシステムは非常に魅力的で、次々とタスクをこなしたくなる意欲が湧きます。成功時の達成感は格別でした!! 未知の体験 / 知らなかったことを学べる AWS GameDayは、普段の業務では触れる機会が少ないAWSのリソースや機能について実践的に学ぶことができる貴重な機会でした! この経験を通じて得た新しい知識や経験は、実際のプロジェクトや日常の業務に応用するきっかけとなりました。 また、チームでの協力を通じて他のメンバーの知識や経験からも学ぶことができ、より幅広い視点でAWSの活用方法を考えられるようになりました。 結果 3位でした! 正直あまりスコアボードを見ずに、がむしゃらに目の前のタスクを解いていました。 最初は10位くらいからのスタートで、徐々にランクを上げていき、最後の方は4位のチームと接戦で、ほぼ神頼み状態でした。 10位から3位とのことでかなり健闘したなぁと思いました。 メダルゲット! ちなみに、チーム名「GOOD TASTE」の名前の由来は、同じチームメンバーだった当社のrubihikoが当日着ていたTシャツからきています笑 入賞チームの写真が公式サイトに載っているので、ぜひ「GOOD TASTE Tシャツ」をチェックしてみてください! https://aws.amazon.com/jp/blogs/news/aws-gameday-at-aws-summit-japan-2024/ おわりに AWS Summit Japan自体が初参加でしたが、GameDayでほぼ丸一日拘束されていたので、ブースやセッションにはあまり参加できませんでした。 ですが、GameDayはそれでも参加する価値はあったと思っています。 GameDayは本当に楽しく学べる良い機会だと感じました。 チームで協力し、実践的な課題解決を通じて様々な学びを得ることができました。 この経験を今後の業務に活かし、さらなるスキルアップにつなげていきたいと思います。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに こんにちは。NIFTY engineering ブログ運用チームです。 ブログ運用チームでは、ニフティのエンジニアに関する情報を広く世間に発信する活動を行っています。 この度、NIFTY engineering ブログの月間アクティブユーザー数(MAU)が20,000を突破しました! おかげさまでMAUは順調に増加し、右肩上がりの成長を続けています。 MAUの推移 20,000MAUを記念して、最近のブログ運用チームの取り組みをご紹介します。 10,000MAU達成時の記事は こちら をご覧ください。 X(旧Twitter)自動投稿機能の作成 NIFTY engineering ブログを更新すると、X(旧Twitter)に自動で更新情報がポストされるようになっています。 こちらの自動投稿機能をブログ運用チームで作成しました! ブログを更新しました! 今回の記事は「【リレーブログ企画第二弾】24新卒リレーブログをやります!」です。 https://t.co/xm4nQtxtbw #nifty_dev #リレーブログ #新卒 — NIFTY Developers (@NIFTYDevelopers) July 19, 2024 自動でポストされた投稿 実現方法については以下の記事にまとめていますので、ぜひご覧ください。 LambdaでX APIを呼び出してみた API GatewayとLambdaでX投稿するAPIを作ってみた ZapierでX投稿するAPIを呼び出して結果をSlackに通知してみた PickUpコーナーの設置 NIFTY engineering のトップページに、PickUpコーナーを設けました。 NIFTY engineeringトップページのPickUpコーナー ブログのタグに「PickUp」を設定することで、こちらのコーナーに表示されるようになっています。 みなさんに見ていただきたい記事を集めていますので、ぜひチェックしてください! 過去記事のX(旧Twitter)投稿 過去にブログ投稿された記事の中から、あるテーマに沿った記事をX(旧Twitter)で紹介する取り組みをしています。 直近では資格勉強に関する記事を再ポストしました! スレッドの中でニフティの「資格取得支援制度」「資格手当」についても触れていますので、ぜひチェックしください。 【資格勉強に関する記事をピックアップ】(1/7) 過去にブログ投稿された記事の中から、資格勉強に関する記事をピックアップしてみました。 スレッド投稿で紹介いたしますので、ぜひチェックしてください! #nifty_dev #資格 #資格勉強 #応用情報技術者試験 #情報処理安全確保支援士試験 … — NIFTY Developers (@NIFTYDevelopers) June 10, 2024 資格勉強に関する記事を紹介したポスト 社内向けレポートのブログ化 ニフティでは、社内に向けて共有したいレポートやメモを書くスペースがあるのですが、その中からブログ化できそうなものをブログ運用チーム内で選定し、代筆する作業を行っています。 また、選定時の参考になるよう「いいね」ボタンを作成したり、代筆時に作業者によるムラが起きないよう、代筆時のガイドラインを作成する等、運用しやすくなる工夫も行っています。 社内向けレポートと「いいね」ボタン リレーブログの企画 ニフティではイベントとして毎年アドベントカレンダーを実施しており、アウトプットの恒例行事となっています。 ニフティグループ Advent Calendar 2023 このようなイベントをクリスマス以外にも実施したいと思い、リレーブログを開催することにしました! 現在は以下のリレーブログを開催中です。記事は随時更新されていきますのでお楽しみに! 【リレーブログ企画第一弾】チーム紹介リレーブログをやります! 【リレーブログ企画第二弾】24新卒リレーブログをやります! 記事公開通知や週次ブログランキングをSlackにポスト アドベントカレンダーについての記事 でもご紹介しましたが、新着記事のSlack通知やブログランキングの発表を、アドベントカレンダー実施時のみではなく、通年で実施しています。 新着記事のSlack通知 特に、ブログランキングの発表は社内から好評を得ており、社員のモチベーションアップにつながっています。 四半期中に公開された記事のランキングについてはSlackで通知し、全体のランキングはLooker Studioで確認できるようにしています。 四半期中に公開された記事のランキング 全体のランキング おわりに 今回は、NIFTY engineering ブログ運用チームの最近の活動についてご紹介しました。 また、今後の活動としては以下を計画しています。 ①話題になっているテーマの観測とブログ執筆 以前、社内でブログ執筆に関するアンケートを実施したところ「ブログを書きたい気持ちはあるが、ネタが思いつかない」という意見が多く寄せられました。 そこで、IT業界で話題になっているテーマを観測し、社内から執筆者を募集する取り組みを開始しようと考えています。 この取り組みのトライアルとして、ブログ運用チームメンバーがClaudeの新機能「Artifacts」に関する記事を執筆しました! Claudeの新機能「Artifacts」を使って簡易Todoアプリを作って遊ぶ ②「WP Sync for Notion」を活用したブログ記事のNotion連携 当ブログではWordPressを使用していますが、WordPressの操作に慣れていない社員が多く、別のプラットフォームにできないかという意見が多く寄せられていました。 そこで「WP Sync for Notion」というWordPressとNotionを連携させるためのプラグインを使用し、普段から使用しているNotionで記事の執筆ができるよう、準備を進めています。 今後も様々な取り組みを実施し、社内外から愛されるブログになるよう努めていきます。 今後もNIFTY engineering ブログをよろしくお願いいたします! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
こんにちは!今回ドメイン駆動設計(DDD)についての社内勉強会を開催したので、運営責任者の佐藤、発起人の松尾、ドメインエキスパート役の大里の3名でその様子や学びをブログで紹介したいと思います。 開催に至った背景 昨今のソフトウェア開発において、複雑なビジネスロジックを持つアプリケーションを開発する機会が増えてきています。そんな中、DDDは複雑なドメインを効果的にモデリングし、ソフトウェアに落とし込むための設計手法として注目を集めています。 今回の勉強会では、DDDの第一人者であるログラス松岡 幸一郎さんを講師としてお招きし、DDDの基本概念から実践的な適用方法まで、幅広く学ぶ機会を得ることができました。松岡さんは長年DDDに取り組んでこられた経験から、わかりやすく要点を押さえた説明をしてくださいました。 当日の様子 第1回 第1回のテーマは「モデリング実践講座」。ニフティの実際に提供しているサービスを題材に松岡さんにライブでモデリングを実施していただきました。今回選定したのは「 @niftyつなぎモバイル 」です。「 @niftyつなぎモバイル 」は、例えば引越しや事業者変更などの際に、光回線が開通するまでの期間でもすぐにネットが使用でき、しかも@nifty光や@nifty MAX光をご契約のお客様はタダという、とてもうれしいサービスです。回線が開通後もそのまま継続利用(有料)は可能です。 「 @niftyつなぎモバイル 」の料金プランは以下の3つとなります。 今回はこの「 @niftyつなぎモバイル 」の料金が決定されるロジックについて、ドメイン駆動設計を使ってモデリングを行いました。これだけでワクワクしませんか? 実況チャンネルも準備して、いよいよスタートです。参加者はこの時点で87名! 松岡さんの「こんばんわ」の挨拶に次々と反応するニフティのエンジニアたち。 DDD勉強会に対する期待度も上がります。 さて、講義のほうは松岡さんおすすめのsudoモデリングを使用して進みます。 ドメインエキスパートとして松岡さんのヒアリングに回答していくのは、「 @niftyつなぎモバイル 」のシステム担当者の大里さんです。 次々に魔法のように情報を絡め取って、図に落としていきます。このあたりに全く無駄がなく、そして必要な情報を聞き出していくファシリテーション力はさすがです。 それぞれの状態がどのタイミングで変化するのか、またその際の補足情報を吹き出しのコメントで追記していきます。 できあがった図は以下です。詳細はお見せできませんが、雰囲気は掴んでいただけたでしょうか? トリガーとなるきっかけや関連する人やシステムも多くて複雑ですが、松岡さんのおかげでエンジニアだけでなく非エンジニアでも理解できる状態になりました。 「外部講師の勉強会で過去1よかった」という声もあがるほど、大盛況で第1回目の勉強会は幕を閉じました。 第2回 1回目でモデリングした内容から、実際のコードに落とし込む方法をライブ形式で学んでいきました。 DDDの概念としてドメインモデルや値オブジェクトを学んでも実際のコードに落とし込むとなるとどうすればいいのか・・・と手が止まってしまいがちです。今回の勉強会では、松岡さんのモデリングからコーディングまでの様子を見せていただくことで、机上の空論ではない実践的なDDDの手法を学ぶことができました。 (オフライン参加の様子) 感想:ドメインエキスパートを経験して 言葉での説明が難しかった箇所が図に表されることによって、受講者全員に仕様が共有できたことが体感できました。 slackでの実況で実際にコードを読んだことがないエンジニアの方々にサービスの仕様自体への質問や疑問点が多く挙げられたため、sudoモデリングでサービス関係者の認識を合わせることによる有用性を感じることができました。 知っているサービスでsudoモデリングを説明していただけたため、Youtubeやブログでご説明していただくよりも複雑なサービスでのsudoモデリングの書き起こし方を理解することができました。特にsudoモデリングと主な4つのモデルの違いや図に起こすほどでもない些細な情報の扱い方を理解することができました。 主な4つのモデルの違いについてはsudoモデリングの目的の一つはサービスの共通理解をするというところであるため、しっかりとルールに則ったユースケース図やドメインモデル図などではなくてもよいということでした。ユースケース図やドメインモデル図を作ったことがなかったのですが、sudoモデリングは複雑なサービスを図に起こすことのきっかけにしやすいと思いました。 最後に 今回の勉強会はシステム部門全体に声をかけ、2回合わせると延べ参加人数が150名を超える大規模な開催となりました。社内勉強会でこれほどの規模は運営メンバーとしても初めてということもあり、当日まで様々な心配は消えませんでしたが、終わってみると参加者の満足度が5点満点中4.8点という非常に高い評価をいただくことができました。 松岡さんのDDDに対する熱意や教え導いていただいた力がとても素晴らしかったこと、そして何よりも保守性が高く、またお客様にとって価値の高いサービスを提供したいと思うニフティのエンジニアにとって学びの大きかった勉強会となったと思います。ありがとうございました。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「チーム紹介」の記事です。 はじめまして!私は会員システムグループの第二開発チームでマネージャーをしております、松尾です。 会員システムグループは主にWEBサービス系の開発部隊で、中でも第二開発チームは各サブチームがそれぞれのプロダクトを持っているのが特徴です。 サブチームの紹介は後ほど詳しくしますので、まずは私の自己紹介をさせてください。 私は20数年前、新卒でニフティに入社しました。当時キーボードも打てない私でしたが、SEという響きに憧れて開発職を希望しました。その私が今日までずっとWEBエンジニアをやってこれたのはひとえに、会社の仲間たちと自分たちのサービスを作り、たくさんのお客様に使っていただける喜びを知ってしまったからだと思います。 では続いて、私たちの誇るプロダクトとサブチームの紹介をしていきたいと思います。 1. ポイントサブチーム 私たちのプロダクト https://lifemedia.jp/ このチームのプロダクトは、ニフティが運営するポイントサイト「ニフティポイントクラブ」です。ニフティポイントクラブは、運営実績20年以上、累計会員数350万人以上のサイトで、買い物やアンケート、ゲームなど、日常生活で利用するだけでポイントを獲得できます。 私たちのミッションは、このニフティポイントクラブをポイントを貯めやすく、ポイントを使いやすいサイトにすることで、お客様に少しでもお得を感じていただき、ニフティに愛着を持っていただくことです! チームの課題 今チームが抱えている課題は、スピード感をもって価値を提供していきたいという思いを阻む、レガシーなシステムです。 2023年から約1年がかりで、サイトを構成するメインのフレームワークであるRuby on Railsのバージョンアップや、クラウド環境をAWSに移行するプロジェクトを実施してきました。データベースは旧クラウド環境に残し、フロントとバックエンドのみAWS環境に移行したことで性能問題が発生しましたが、知恵と技術を結集させ、以前より表示速度をあげることに成功しました。 そして今現在、今度はデータベースの移行および脱オラクルを計画しています。またそれに合わせてC言語でできている古いツール群が刷新されますので、プロジェクト終了時には、さらに速度が向上し使いやすいサイトになる予定です!ぜひご期待ください。 メンバー どんな時も前向きで笑顔を絶やさない細野さん 【執筆ブログ】 中途入社1年目から見たニフティとポイント開発チームの紹介 チーム1のムードメーカー、関くん 【執筆ブログ】 主催した社内勉強会の課題でアクセシビリティ的に優れているTODOリストの課題を出した話 いつもマイペースで癒し系の西根さん 【執筆ブログ】 デイリースクラムの見学に行ってデイリーを改善したい! ニフティポイントクラブのドメイン知識の宝庫、Sさん イラストを書くこと、音楽を聞くことが大好き、Kくん 【執筆ブログ】 「Zapier」を用いたSlackでのChatOps 協力会社さんにも参画してもらいつつ、お互い助け合いの精神で各々の得意分野を発揮しながら明るく元気に開発しています。 2. マイニフティサブチーム 私たちのプロダクト https://csoption.nifty.com/myapp/ チームのプロダクトビジョンは「マイニフティアプリがあることで、安心・便利・お得にインターネットを活用できる」ことです。ニフティの会員が回線の開通から契約情報の更新、トラブルのサポートなど知りたい情報をすぐに知り、手続きが簡単にできるなど、お客様のお困りごとに寄り添って解決し、より長くニフティを利用したくなるアプリを目指しています! チームの特徴 ニフティ内で唯一のアプリ開発チームです。 iOS (Swift)、 Android (Kotlin)を使ったアプリ開発のみならず、サーバーサイド(Go)から社内ツールまで開発する全方位のエンジニア軍団が集結! 比較的新しいプロダクトのため、データドリブン開発をはじめ、コンテナ・サーバレスが100%、IaC・CI/CD、Clean Architecture採用など、ニフティの中でも最先端をいくモダン開発を実現できているチームです。今は新しい施策に対応する傍ら、自動テストのカバレッジをあげて品質向上へ取り組んでいます。 メンバー ファシリ力ピカイチ、ニフティの元祖スクラムマスター西野さん 【執筆ブログ】 パターンでわかる効果的なレトロスペクティブ インフラなら俺にまかせろ、いつも穏やかな川上くん 【執筆ブログ】 ISUCON13へ参戦するまでにやったこと 知識とホスピタリティの塊、参謀タイプの村松くん 【執筆ブログ】 URLProtocolでAPI Mockを作成してみる Flutter導入を虎視眈々と狙っている、オンライン配信ならお任せ柴田くん 【執筆ブログ】 Flutterコントリビューターになりました プルリク数チームNo.1、Android開発といえばLinさん 【執筆ブログ】 Gemini AIアプリを作りましょう 個性豊かなメンバーで、毎朝のデイリースクラムの前の15分間の雑談タイムでは話題が尽きることなく大盛り上がり。そのあとは業務に集中とメリハリのしっかり効いたチームです。 後編に続く 残りの2つのチームは後編でご紹介したいと思います。バトンを会員システムグループ長の小松さんにお渡しします。こちらもお楽しみに。 オプションサブチーム 採用マーケティング ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24新卒リレーブログ」の記事です。 はじめまして。 新卒1年目でジョブローテーション中の山本です。 今回、24卒のブログリレー企画で、テーマは「 自由 」ということなので、私が先日 Vision AI Expo 2024 に参加した際に知った「 roboflow 」という画像認識ツールの体験記について執筆させていただきました。 AIの知識がまったくない、、、という方でも簡単に画像認識ができるツールとなっており、大学で画像認識のAIを作成していた身としてもとても衝撃を受けたツールなので、少しでも興味のある方はぜひ参考にしていただければと思います。  ▼目次 roboflowの概要 roboflowとは なぜroboflowが人気なのか 実際にroboflowを使ってみた モデルの選択 デモで動作確認 画像の選択 検出の詳細設定 結果の確認 モデルの利用方法 実際に実行してみた まとめ roboflowの概要 roboflowとは そもそもroboflowってなんぞやって話ですが、 公式によると Everything you need to build and deploy computer vision models Roboflow: Computer vision tools for developers and enterprises とのこと。 なんかすごそうですね、、、 まあざっくりいうと、このroboflowで画像認識に必要なタスクが全てできてしまうすごいツールっていうことです。 画像認識AIモデルの作成から他ユーザとのモデルの共有、作成したモデルの品質管理などが行えるツールとなっています。 このroboflowですが、アメリカでは「AIモデル構築プラットフォーム」としてのデファクトスタンダードとなっているそうで、2022年時点で 60,000ユーザ を獲得し、多くの研究・開発に利用されているようです。 なぜroboflowが人気なのか ではなぜroboflowがここまで人気なのでしょうか? 簡単にまとめると以下の4点が人気の理由です。 使いやすさ 専門知識が少なくても、GUIで高度な画像認識プロジェクトを実現できる。 効率性 データの前処理からモデルのデプロイまでを1つのプラットフォームで完結できる。 柔軟性 様々な画像認識タスク(物体検出、セグメンテーション、分類など)に対応している。 コミュニティ 豊富な事前トレーニング済みモデルやデータセットが利用・拡張可能。 これらの要因が組み合わさることで、roboflowは幅広いユーザーにとって魅力的なプラットフォームとなっています。初心者から専門家まで、様々なニーズや要求に応えられる柔軟性と機能性を備えているため、画像認識プロジェクトにおける強力なツールとして人気を集めているようです。 AIブームが続く中で、こういったユーザが最新技術を容易に活用できる環境を提供してくれるというのはありがたいですね! 実際にroboflowを使ってみた では、実際にroboflowを使って、いろいろな物体の検出をやっていきます。 https://roboflow.com/ 上記のページにアクセスすると、roboflowユーザが作成したAIモデルを検索することができます。 (※初回のみアカウント登録が必要になるため、Googleアカウントなどお好きな方法で登録してください) モデルの選択 まずは今回使用するモデルを選んでいきたいと思います。 特定の物体(人や車)のみを検出するモデルなどもありますが、今回はいろいろな物体を検出してみたいので、 COCO Dataset という汎用的なモデルを選択しました。 このモデルではコップや人間、猫など80種類の様々な物体検出に対応しています。対応している物体の種類に関してはOverview画面右側のCLASSESという箇所から確認することができるので、用途に合ったモデルを選択します。 デモで動作確認 roboflowではWebサイト上でどのように検出できるのかデモを行うことができる機能があるので、まずはこのモデルを試してみたいと思います。 画面左側のタブから「 Model 」を選択するとモデルの詳細画面に遷移します。 すると、さっそく画面中央に検出の結果が表示されます。 画像の選択 他の画像で試してみたい場合は、左の「 Samples from Test Set 」の箇所からすでに用意されている別の画像を選択するか、「 Upload Image or a Video File 」の箇所から写真や動画をアップロードすることでも確認することができます。 また、「 Try With Webcam 」でウェブカメラの映像からリアルタイムに検出を行うことができます。 検出の詳細設定 より細かい設定をしたい場合は 右上の「 Confidence Threshold」 と「 Overlap Threshold 」を変更することで指定できます。 Confidence Threshold(信頼度閾値): これは、AIが物体を検出したときの確信度を表します。 例:猫の検出 高い設定:AIが「100%確実に猫だ」と判断したものだけを検出します。 低い設定:「猫らしきもの」も検出します。例えば、猫に似た模様や形のものも含まれる可能性があります。 調整により、検出の精度と範囲のバランスを取ることができます。 Overlap Threshold(重複閾値): これは、複数の検出結果をどう扱うかを決める基準です。 例:1匹の猫を複数回検出した場合 AIが同じ猫を少しずれた位置で2回検出することがあります。 この閾値は、「どれくらい重なっていれば、それらを1つの検出結果としてまとめるか」を決めます。 これにより、同じ物体の重複検出を防ぎ、より正確な結果を得ることができます。 つまり、Confidence Thresholdは「検出の確信度」を、Overlap Thresholdは「検出結果をどうまとめるか」を制御する役割を果たします。これらの設定を適切に調整することで、より精度の高い物体検出が可能になります。 結果の確認 画面中央の画像から検出結果を確認することもできますが、 PythonやJavaScriptなどでAPIを利用し検出する場合は画像の右下にあるようなJSON形式で結果が取得できます。 モデルの利用方法 実際に自身のプログラムでモデルを利用して検出をしていきます。 画面下のほうにスクロールすると「Code Snippets」という箇所があり、言語別に具体的な使いかたが説明されています。 PythonのHosted APIの箇所を見ると以下のコマンドでライブラリを読み込むことができ、 pip install inference-sdk 以下のコードでAPIを利用して検出ができると書いてあります。 from inference_sdk import InferenceHTTPClient CLIENT = InferenceHTTPClient( api_url="https://detect.roboflow.com", api_key="vOwAVfDFTbgLcWxJrx5g" ) result = CLIENT.infer(your_image.jpg, model_id="coco/24") 実際に実行してみた 以下のコードを実際に実行して結果が取得できるか試してみました。 from inference_sdk import InferenceHTTPClient import pprint CLIENT = InferenceHTTPClient( api_url="https://detect.roboflow.com", api_key="vOwAVfDFTbgLcWxJrx5g" ) result = CLIENT.infer("input.jpg", model_id="coco/24") # 結果出力 pprint.pprint(result) 使用した画像はこちらです↓ 結果は以下のようになりました。 {'image': {'height': 4096, 'width': 3072}, 'inference_id': '61284435-9a72-4432-b283-2210f3e73318', 'predictions': [{'class': 'cat', 'class_id': 15, 'confidence': 0.9505542516708374, 'detection_id': 'f4b4a946-a391-4173-9cda-9782db7788bc', 'height': 2112.0, 'width': 1788.0, 'x': 1910.0, 'y': 2472.0}], 'time': 0.6150403930000721} 結果を見ると、 'class': 'cat' となっているので、しっかりと猫が1匹検出されているのがわかります。 ただ、JSONで結果を取得するだけではわかりづらいのでコードを少し変更して、結果を画像に描画してみました。(OpenCVを利用) 正しく検出できていそうです!  ▼描画に使用したコード from inference_sdk import InferenceHTTPClient, InferenceConfiguration import pprint import cv2 import sys # 信頼度の閾値 CONFIDENCE = 0.5 # 画像パスを実行コマンドから取得 image_path = sys.argv[1] if len(sys.argv) > 1 else "input.jpg" # 詳細設定 custom_configuration = InferenceConfiguration( # モデルの信頼度の閾値 confidence_threshold=CONFIDENCE, # クライアントのダウンサイジングを無効にする client_downsizing_disabled=True ) # InferenceHTTPClientの初期化 CLIENT = InferenceHTTPClient( api_url="https://detect.roboflow.com", api_key="vOwAVfDFTbgLcWxJrx5g" ) # 座標系の修正 def convert_coordinates(prediction, image_width, image_height): """ roboflowとcv2の座標系を変換する関数 Args: prediction (dict): 座標を含む予測結果の辞書。 image_width (int): 画像の幅。 image_height (int): 画像の高さ。 Returns: tuple: 変換された座標 (x, y, width, height)。 """ x = prediction["x"] - prediction["width"] / 2 y = prediction["y"] - prediction["height"] / 2 width = prediction["width"] height = prediction["height"] # 座標を整数に変換し、画像の範囲内に収める x = max(0, min(int(x), image_width - 1)) y = max(0, min(int(y), image_height - 1)) width = max(1, min(int(width), image_width - x)) height = max(1, min(int(height), image_height - y)) return x, y, width, height # 推論の実行 with CLIENT.use_configuration(custom_configuration): # 画像の読み込み image = cv2.imread(image_path) # ファイルが存在しない場合にエラーを出力 if image is None: print("Error: Image file not found.") sys.exit(1) # 推論の実行 result = CLIENT.infer(image, model_id="coco/24") # 結果の出力 pprint.pprint(result) # クラス・バウンディングボックスの描画 for prediction in result["predictions"]: x, y, width, height = convert_coordinates(prediction, image.shape[1], image.shape[0]) class_name = prediction["class"] confidence = int(prediction["confidence"] * 100) put_text = class_name + " " + str(confidence) + "%" cv2.rectangle(image, (x, y), (x + width, y + height), (0, 255, 0), 10) cv2.putText(image, put_text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 5, (0, 255, 0), 10) # 描画結果を保存 cv2.imwrite("result.jpg", image) ちなみに、上記のコードではconfidence(信頼度)を指定できるようにしているので、閾値を0.2に変更してみると、以下の画像のように結果が変わりました。 ピントが合わずにボケている人物の箇所もしっかりとpersonと認識されています。 これは信頼度が低い(ボケていて人間かどうか微妙なライン)の箇所もconfidenceの閾値を下げることで検出結果に含まれるようになったということです。 以下に他の画像の検出結果もまとめました。 このモデルが対応している物体に関しては正確に検出ができていますね! まとめ このようにroboflowを利用することで少ないコードで画像認識ができてしまいます。 今回は様々な物体の検出に対応したモデルを利用しましたが、人の顔を検出するモデルを利用して顔の箇所にモザイクを入れたり、来客数を継続的に計測し販売戦略を立てたりなど、アイデア次第では様々な活用が期待できますね。 また、roboflowにはまだまだ多くの機能があります。 今回使用した COCO Dataset はハムスターの検出に対応していませんが、追加でハムスターを検出させたい場合、自身で作成したワークスペースに、 COCO Dataset のモデルをインポートして、そこにハムスターのデータを追加することでモデルを拡張することが可能です。 新たなデータを追加する際にも自動でアノテーションをしてくれる機能などもあります。 他にも動画から写真を切り出して、自動でアノテーションをしてくれたり、作成したモデルの品質管理をしてくれたりなど魅力的な機能が多く備わっています。 今後、AI技術がますます身近になっていく中で、roboflowのような手軽にAIモデルに触れることのできるツールの存在は、イノベーションを加速し、さらなる発展に寄与していくと思います。もしこの記事で少しでも興味を持っていただけた方は、ぜひビジネスや研究、個人利用など様々な場面でroboflowの活用を検討してみてください! 次回は、塚崎さんです。 どんな記事になるのかワクワクですね♪ ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
SREチームの島です。 8月3日、4日に開催された SRE NEXT 2024 にて、「FourKeysを導入したが生産性向上には至らなかった理由」というタイトルで登壇させていただきました。 登壇した理由 今回このテーマで登壇した理由は、私自身の経験を共有することでFourKeysの導入を考えている方々の参考になればと考えたからです。 私たちはFourKeysを用いた仮説検証を実施し、その過程で指標への解像度は確かに上がりました。しかし、開発生産性の向上には結びつかなかったという貴重な経験をしました。 FourKeysは注目を集めている概念ですが、単に導入すれば生産性が向上するわけではありません。私たちが直面した課題や、得られた教訓を共有することで、みなさまのFourKeys導入や開発プロセス改善の一助となることを願っています。 NIFTY Tech Talk開催のお知らせ 登壇で十分に触れられなかった内容については、8月27日に開催予定の「NIFTY Tech Talk #21」にてお話しする予定です。興味のある方はぜひご参加ください。 NIFTY Tech Talk #21 〜SRE関係イベント登壇者のAfter Talk〜 登壇の様子 Photo by SRE NEXT Staff Ask the Speakerにもお越しいただき、ありがとうございました。 最後に 約1年半前にSREチームに加わり、昨年初めてSRE NEXTに参加しました。 当時はまだ分からないことだらけで日々苦戦していた私にとって、初めて参加したSRE NEXTは華々しく輝いて見えました。 登壇者も参加者も目を輝かせて楽しそうに発表したり話を聴いたりしている姿が刺激的で、今でも鮮明に記憶しています。 そのような憧れの舞台に今年立つことができ、とても光栄でした。 SRE NEXT運営のみなさま、視聴いただいたみなさま、本当にありがとうございました! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに こんにちは。ニフティ株式会社の添野隼矢です。 最近、開発業務でSvelteKitに触れる機会がありました。そのシンプルさと学習コストの低さから、未経験者の私でもすぐに開発に活かせることを実感しました。 今回は、そのSvelteKitのエラーハンドリングについてご紹介します。 基本的なエラーハンドリング まずSvelteKitには、エラーをスローするための方法が以下の2つの方法があります。 error(SvelteKit 1.x系ではthrowが必要です。) throw new Error これらの違いについて、説明していきます。 error こちらは、アプリケーション内で発生することが予想されるエラーに使用されます。 例えば、以下のエラーの場合です。 403(アクセス権限がない場合などのエラーコード) 404(ユーザーが存在しない場合やリソースが見つからない場合などのエラーコード) 409(クライアントからのリクエストがサーバー上の現在の状態と競合しているときに発生するエラーコード) 500(サーバー側に発生した問題でリクエストが処理されなかったことを示すエラーコード) 利用例は以下のようになります。 import { error } from '@sveltejs/kit'; export const load = async () => { const user = await getUser(); if (!user) { throw error(404, { message: 'ユーザ情報が見つかりませんでした', code: 'USER_NOT_FOUND' }); } const permissions = await getUserPermissions(); if (!permissions.includes('ACCESS_RESOURCE')) { throw error(403, { message: 'アクセス権限がありません', code: 'FORBIDDEN' }); } return { user, permissions }; }; throw new Error こちらは、想定外エラーが起こった場合やプログラムのバグの際に使用するものです。 こちらは標準のJavaScriptのエラーハンドリング方法のため、throwが必要です。 利用例は以下のようになります。 export const load = async () => { try { const data = await fetchData(); return { data }; } catch (e) { throw new Error('データの取得に失敗しました'); } }; カスタムエラーページの作成 SvelteKitでは、 +error.svelte ファイルを作成して、カスタムエラーページを作成することができます。 カスタムエラーページを作成することによって、ユーザーフレンドリーな体験を提供できます。 試しに先程の403と404のerrorのカスタムエラーページを作成すると以下のようになります。 <script lang="ts"> const dict = { USER_NOT_FOUND: 'ユーザ情報を再度ご確認いただき、ログインしてください。', FORBIDDEN: 'アクセス権限がありません。' }; export function load({ error, status }) { return { props: { error, status } }; } </script> <svelte:head> <title>{status} - エラー</title> </svelte:head> <main> <h1>{status}</h1> <p>{@html dict[error.code]}</p> </main> 上記の例では、エラーメッセージを辞書型で管理し、対応するエラーメッセージを表示しています。 これにより、エラーコードごとに異なるメッセージを簡単に設定できます。 <p>{@html dict[error.code]}</p> の部分は、この場合には以下が表示されます。 エラーコードがUSER_NOT_FOUNDの場合、辞書に基づいて「ユーザ情報を再度ご確認いただき、ログインしてください。」と表示されます。 エラーコードがFORBIDDENの場合、辞書に基づいて「アクセス権限がありません。」と表示されます。 ※上記以外のエラーコードの場合、辞書に定義されていないため、undefinedが表示される可能性があります。 まとめ SvelteKitのエラーハンドリングは非常に柔軟で、単純なエラーから複雑なエラーページのカスタマイズまで対応できます。 適切なエラーハンドリングを実装することで、ユーザーエクスペリエンスを向上させ、問題のデバッグを容易にすることができます。 詳細な情報については、 公式ドキュメント を参照してください。 カスタムエラーページを簡単に作成したい場合は、ぜひSvelteKitを使ってみてください。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
この記事は、リレーブログ企画「24新卒リレーブログ」の記事です。 はじめに こんにちは。新卒1年目のkeyliumです。開発生産性に関心があります。 シェルの入力でも普段使っているエディターのように、行・単語ごとのジャンプや削除などをしてみたいと思ったことはありませんか? エディターでの快適な操作性をシェルでも再現できると、生産性がぐっと向上します。そんな便利な設定を可能にするのが、 .inputrc ファイルです。 筆者の環境 OS: Windows Terminal: Windows Terminal   Tera Termを使用している場合、一部のキーバインドや cat -v が期待通りに動作しないことがあります。 Shell: Bash (GitBash) .inputrcとは? .inputrc ファイルは、GNU Readlineライブラリの設定ファイルです。Readlineは、Bashを始めとする多くのシェルやコマンドラインツールで使われており、コマンドラインでの入力編集を制御しています。 このファイルをカスタマイズすることで、自分好みのキーバインディングや入力方法を設定することができます。 デフォルトの設定 まずは、デフォルトでどのようなキーバインディングが設定されているのかを見てみましょう。 デフォルトのBashシェルはEmacsスタイルのキーバインディングを使用しています。以下に、いくつかの主要なデフォルト設定を紹介します: Ctrl + A : 行の先頭に移動 Ctrl + E : 行の末尾に移動 Ctrl + K : カーソルから行の末尾まで削除 Ctrl + U : カーソルから行の先頭まで削除 Ctrl + W : カーソルの前の単語を削除 Ctrl + Backspace : カーソルの前の単語を削除 Ctrl + Delete : カーソルの後の単語を削除 Ctrl + L : 画面をクリアにする Alt + F : 次の単語に移動 Alt + B : 前の単語に移動 これらのデフォルト設定を確認するために、シェルを開いて実際に試してみてください。 おすすめの キーバインディング # PageUp キーで履歴検索 "\e[5~": history-search-backward # PageDown キーで履歴検索 "\e[6~": history-search-forward # Ctrl + P で前のコマンドを表示する (上キーでも可) "\C-p": previous-history # Ctrl + N で次のコマンドを表示する (下キーでも可) "\C-n": next-history # Ctrl+R でコマンド履歴を逆方向から検索 "\C-r": reverse-search-history # Alt + R で行を元に戻す "\er": revert-line history-search-backward デフォルトキーバインディング: PageUp 動作 : 現在のコマンドラインに部分的に入力されたテキストに一致する履歴のコマンドを、逆方向に検索します。 使用例 : 例えば、コマンドラインに「echo」と入力し、その状態で history-search-backward を実行すると、履歴の中で「echo」 で始まる コマンドを逆方向に検索して表示します。 利点 : 一部入力されたコマンドに基づいて履歴を絞り込みたい場合に便利です。 history-search-forward デフォルトキーバインディング: PageDown 動作 : history-search-backward の逆の操作です。 previous-history デフォルトキーバインディング: Ctrl + P   ↑ 動作 : 現在のコマンドラインの入力に関係なく、単に履歴の中で1つ前のコマンドを表示します。 使用例 : コマンドラインに何も入力されていない状態で previous-history を実行すると、履歴の中で直前のコマンドが表示されます。 利点 : コマンド履歴を一つずつ遡って確認したい場合に便利です。 next-history デフォルトキーバインディング: Ctrl + N   ↓ 動作 : previous-history の逆の操作です。 reverse-search-history デフォルトキーバインディング: Ctrl + R 動作 : インクリメンタルサーチを使用して、コマンド履歴を逆方向に検索します。検索中にリアルタイムで結果が更新されます。 使用例 : Ctrl+Rを押すと、 (reverse-i-search) プロンプトが表示され、キーワードを入力するたびに、履歴から一致するコマンドが順次表示されます。 利点 : インタラクティブに履歴を検索できるため、目的のコマンドを素早く見つけることができます。 character-search デフォルトキーバインディング: Ctrl + [ 動作 : カーソルから順方向に特定の文字を検索し、最初に見つかった位置にカーソルを移動します。 使用例 : Ctrl+[ を押して、特定の文字を入力すると、カーソルから順方向にその文字を検索します。 利点 : コマンドライン内の特定の文字にすばやく移動したい場合に便利です。 character-search-backward デフォルトキーバインディング: Alt + Ctrl + [ 動作 : character-search-backward  の逆の動作です。 revert-line デフォルトキーバインディング: Alt + R 動作 : カーソルのある行を元の状態に戻します。元の状態とは、行が最初に表示された時の内容です。コマンドを編集してしまった場合に、編集前の状態に戻すことができます。 使用例 : 例えば、 history-search-backward などで現在の行に入力されたコマンド を変更してしまったが、元のコマンドに戻したい場合に使用します。 kill-whole-line デフォルトキーバインディング: これはデフォルトでは設定されていません。次の章で設定します。 動作 : カーソルのある行全体を削除します。 使用例 : 例えば、現在の行に長いコマンドが入力されていて、その行全体を削除したい場合に使用します。 .inputrcを使ってカスタマイズする デフォルト設定を理解したら、自分のニーズに合わせて .inputrc ファイルをカスタマイズしてみましょう。 .inputrc ファイルはホームディレクトリに置き、以下のような形式で設定を記述します。この設定は私が使っている設定です。: # コメントは#で始めます # Alt + qで行削除 "\eq": kill-whole-line # ↑↓キーで履歴検索(PageUp, PageDownが押しづらい人向け) "\e[A": previous-history "\e[B": next-history # TABキーで、補完する単語を可能な補完リストの 1 つの一致に置き換えます。 # menu-completeを繰り返し実行すると、可能な補完リストを順に進み、各一致を順番に挿入します。 TAB: menu-complete # Shift + TAB で逆方向にmenu-complete "\e[Z": menu-complete-backward # 補完で大文字小文字を無視する set completion-ignore-case on .inputrc ファイルの変更を適用するには3つの方法があります。以下のどれかを実行しましょう。 シェルを再起動する 以下のコマンドを実行する: bind -f ~/.inputrc C-x C-r を押す(Ctrl + xを押してからCtrl + rを押す) これで .inputrc に書いた機能が有効になったはずです。 履歴に関する環境変数 履歴を参照することが多いため、履歴に関する環境変数を紹介します。以下の設定は .bashrc に書きます。この設定は私が使っている設定です。 # 重複する連続コマンドと、先頭に半角スペースが入っているコマンドを履歴に記録しない export HISTCONTROL=ignoredups:ignorespace # 履歴に保存するコマンドの最大数を設定(デフォルトは500) export HISTSIZE=10000 # 履歴ファイルに保存するコマンドの最大数を設定(デフォルトは500) export HISTFILESIZE=10000 # 履歴に保存しないコマンドのパターンを指定. # 複数ある場合は":"で区切る export HISTIGNORE="ls" # 履歴に保存されるコマンドのタイムスタンプの形式を指定 export HISTTIMEFORMAT="%F %T " HISTCONTROL HISTCONTROL は、シェル(特に Bash)でコマンド履歴の動作を制御するための環境変数です。 HISTCONTROL を使用すると、コマンド履歴に保存されるコマンドの種類を制御できます。以下に、 HISTCONTROL の主なオプションを説明します。 HISTCONTROL のオプション ignoredups 重複する連続したコマンドを履歴に保存しません。 例: ls コマンドを2回連続で実行すると、1回分だけ履歴に保存されます。 ignorespace 空白で始まるコマンドを履歴に保存しません。 例: ls (スペースが前にある ls )は履歴に保存されません。 ignoreboth ignoredups と ignorespace の両方を適用します。 重複する連続コマンドと、空白で始まるコマンドを履歴に保存しません。 erasedups 履歴全体から重複するコマンドを削除します。最新のものだけが保存されます。 例: 履歴に ls が複数回含まれている場合、最新の ls だけが残ります。 HISTSIZEとHISTFILESIZE HISTSIZE 履歴に保存するコマンドの最大数を設定します。 この設定は、 シェルがメモリ内で保持する履歴の数 を制御します。 シェルセッション中に使用され、シェルを閉じるとクリアされます(履歴ファイルに保存されない場合)。 HISTFILESIZE 履歴ファイルに保存するコマンドの最大数 を設定します。 これは、シェルセッションが終了するときに .bash_history などの履歴ファイルに保存されるコマンドの数を制御します。 次のシェルセッションで履歴を読み込むために使用されます。 HISTTIMEFORMAT HISTTIMEFORMAT 履歴に保存されるコマンドのタイムスタンプの形式を指定します。 フォーマットは date コマンドの形式と同じです。 export HISTTIMEFORMAT="%F %T " という設定を行うと、履歴にタイムスタンプが追加されます。 history コマンドを使うと YYYY-MM-DD HH:MM:SS の形式で表示されます。 Q&A Q: キーがどの制御文字に対応するかを知る方法は? A: cat コマンドを使う 最も簡単な方法の一つは、シェルで cat コマンドを使うことです。このコマンドは、入力されたキーシーケンスをそのまま表示してくれます。 シェルを開き、 cat -v コマンドを実行します: 次に、調べたいキーを押します。例えば、Ctrlキーを押しながら左矢印キーを押し、Enterを押すと、 ^[OD が出力されるはずです。これは、Ctrl + 左矢印キーが \eOD に対応することを意味します。 ^[ は \e に対応し、 OD がそのまま制御文字として使用されます。 終了するには、Ctrl + D を押します。 機能しない機能 forward-search-history (C-s) 履歴を最初の方から検索する機能。実行するとフリーズする。C-cで抜けられます。 set mark (C-@) 矩形選択を行う機能。これも機能しません。 参考資料 Readline – ArchWiki https://wiki.archlinux.jp/index.php/Readline readline(3) — Arch manual pages https://man.archlinux.org/man/readline.3 history コマンドに日時を付与する – Qiita https://qiita.com/bezeklik/items/56a597acc2eb568860d7   最後に シェルの入力設定をカスタマイズすることで、コマンドラインでの作業効率を大幅に向上させることができます。 .inputrc ファイルを利用して、自分好みのキーバインドや動作を設定することで、コマンドライン操作がより直感的で便利になるでしょう。 シェルのカスタマイズを通じて、日々の作業がよりスムーズに、そして効率的に進むことを願っています。 以上、.inputrc の設定 -Readlineのキーバインディングを知って開発生産性を向上させる-でした。 ありがとうございました。 次回は山本さんです。 楽しみにしています。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
イベント概要 NIFTY Tech Talkは、ニフティ株式会社の社員が主催するトークイベントです。 本イベントでは、ニフティグループの社員が業務を通じて学んだことを発信しています! テーマ 2024年6月20日、21日にAWS社の大規模イベント AWS Summit Japan 2024 が開催されました。AWS Summit Japan(以前はTokyo)には毎年ニフティのエンジニアが多く参加しています。 NIFTY Tech Talk #20では、AWS Summit Japan内で行われた実践的なチーム学習演習イベントであるAWS GameDayの入賞チームメンバーから、AWS GameDayの楽しさや学び、あるいは攻略法について語ってもらいます。 ご参考 AWS GameDay @ AWS Summit Japan 2024 結果発表!! ※ AWS GameDayはその性質上内容のネタバレは厳禁とされてますため、ネタバレを避けた範囲でのトークとなります。ご承知おきください 開催概要 コンパスにて開催しました https://nifty.connpass.com/event/325653/ 資料 こんな方におすすめ AWS GameDayに興味があるが参加したことがない方 AWSを使っているがAWS Summit Japanに参加したことがまだない方 参加したことはあるが、楽しみ方をより知りたいという方 AWS GameDayとは https://aws.amazon.com/jp/gameday/ ある課題に対して AWS サービスで解決するための対応力や実装スキルを試すことができる実践形式のワークショップ 3~4 名でチームを結成 さまざまなクエストをクリアしていく! 最も多くのポイントを獲得したチームが勝者 学びについて 知らなかったことを学べる 普段扱っていない領域を体験できる チームメンバーとの協力・コニュニケーション 他社のエンジニアと知り合いに 課題解決力が付く 実践形式のワークショップで色々学びがあります。普段触ったことのないリソースや機能について体験できるかもしれません。 そこで得た学びはサービスに適用できるきっかけになるかもしれません! 心構え 事前準備 がんばって予約を取る 予約が取れなくても、当日並ぶことで入れるかも!? 資格は無くても大丈夫 グループワーク コミュニケーション取って課題解決しよう 仲良くなるのが大事 モブプロ 後日 イベントレポートを書こう 仕事に適用できるか検討してみよう まとめ AWS Summitの中でも特に大きなコンテンツであるAWS GameDay、参加することで様々な学びがありました。 ここでしか得られない学びと、活かしていきたいと思います。 AWS GameDayは他の予定より優先して参加する価値あり! 見れなかったセッションは後でアーカイブを確認しよう AWS Summitはお祭り、楽しもう ノベルティ ここでしか体験できないイベントが盛り沢山 We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も常時受け付けています。 カジュアル面談 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering
アバター
はじめに 最近の尋常ではない暑さに、ついに日傘を買った宮本です。 今回は、Astroでのコンポーネントテストについて紹介します。ただし、あくまでExperimentalな機能を使ったものであり、今後変更される可能性があるのでご注意を。 これまでコンポーネント単体をレンダリングする手段のなかったAstroですが、ついに今年の5月にリリースされた 4.9 にて、コンポーネント単体をレンダリングできるContainer APIがExperimental機能として追加されました。あくまでExperimentalということもあり、軽く触った感じだとやや機能が足りない部分を感じましたが、それでもコンポーネントテストが不可能だったこれまでに比べると大きな進歩です。 それはそれとして、この記事を書こうと思いながらもサボっていたら、いつの間にか4.12までAstroのバージョンが進んでいました。リリース頻度がとても早いです。 環境 Astro: v4.12.3 Vitest: 2.0.3 node-html-parser: 6.1.13 今回の記事は、Astro公式で提供されている、 Vitestを利用したテストテンプレート を元にファイルを追加して作成しました。手元ですぐに再現できるので、気になった方はぜひ試してみてください。 npm create astro@latest -- --template with-vitest コンポーネントテスト 次のようなCardコンポーネントをテストします。 --- // src/components/Card.astro type Props = { title: string; }; const { title } = Astro.props; --- <div> <h3> {title} </h3> <div class="card-body"> <slot /> </div> </div> Cardコンポーネントにはslotとpropsがあるため、テストではこれが正常に記述されているかを確認してみます。 リリース時の4.9.0だとpropsが指定できませんが、4.9.2からはpropsの指定も可能になっています。また、そのほかにもページルーティング時に利用するparamsなども指定可能です。レンダリング時に指定可能なオプションは 公式ドキュメント から確認できます。 // src/components/Card.spec.astro import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import { expect, test } from 'vitest'; import Card from './Card.astro'; test('Card with slots and props', async () => { const container = await AstroContainer.create(); // resultにはコンポーネントをHTMLとして出力した文字列が挿入される const result = await container.renderToString(Card, { props: { title: 'タイトル' }, slots: { default: 'コンテンツ' } }); expect(result).toContain('タイトル'); expect(result).toContain('コンテンツ'); }); このとき、resultにはコンポーネントをhtmlとして出力した場合の文字列が挿入されています。そのため実施しているのは、あくまで文字列の比較テストであるという点に注意が必要です。 上記のテストでresultに入る文字列は以下のようなものです(data-astro-source-fileの内容は変更してます)。テストの途中でconsole.logなどを使って出力するとわかりやすいです。 <!DOCTYPE html><div data-astro-source-file="~/vitest-astro/src/components/Card.astro" data-astro-source-loc="8:6"> <h3 data-astro-source-file="~/vitest-astro/src/components/Card.astro" data-astro-source-loc="9:7"> タイトル </h3> コンテンツ </div> 見てわかる通り、出力されるhtml文字列はローカルのdevサーバを立ち上げた場合と同じものです。そのため、data属性として data-astro-source-file や data-astro-source-loc が自動的に含まれたタグが出力されています。本番環境と完全に同じではないため、注意が必要です。 これを削除したい場合は、 vitest.config.ts にてテスト時に 開発ツールバー機能をオフにする ことで表示されなくなります。 /// <reference types="vitest" /> import { getViteConfig } from "astro/config"; export default getViteConfig( { test: {}, }, { // 開発時のツールバー機能を無効化 devToolbar: { enabled: false }, } ); <!DOCTYPE html><div> <h3 data-testid="title"> タイトル </h3> <div class="card-body" data-testid="body"> コンテンツ </div> </div> また、それ以上に文字列の比較になってしまうため、繰り返しなどを使っていて同じ文字列が複数表示されている場合に、コンポーネントの想定通りの場所にpropsやslotで挿入されているか確実とは言えない点も厳しいです。 そこで、文字列として出力されたhtmlをパーサーを使って解釈し、もう少し精度の高いテストをしてみます。今回はパーサーとして node-html-parser を利用したテストに変更してみました。 yarn add -D node-html-parser // src/components/Card.spec.astro import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import { expect, test } from 'vitest'; import Card from './Card.astro'; import { parse } from 'node-html-parser'; test('Card with slots and props', async () => { const container = await AstroContainer.create(); // resultにはコンポーネントをHTMLとして出力した文字列が挿入される const result = await container.renderToString(Card, { props: { title: 'タイトル' }, slots: { default: 'コンテンツ' } }); const root = parse(result); const title = root.querySelector('h3'); const body = root.querySelector('.card-body'); expect(title?.innerText).toContain('タイトル'); expect(body?.innerText).toContain('コンテンツ'); }); パーサーを用いてhtml文字列を解析することで、より厳密にテストすることができるようになったと思います。もちろんdata-testidのようなテスト用のデータ属性を与えてテストすることも可能です。 注意点 いささかしつこいくらいの注意点になりますが、前提として現在のContainer APIはあくまでExperimental機能であり、今後変更される可能性があります。 RFC 内でも、小さな機能としてリリースしてからフィードバックをもとに改善していきたいと記述されています。 テストから話は離れますが、このContainer APIをもとに AstroのStorybook対応 をするような展望もあるようなので、それに合わせて変更がある可能性もあります。(むしろ、テスト以上にStorybook対応を楽しみにしていた人も多い気がしています。が、これも今のContainer APIですぐに対応するのは難しい模様……。残念です) また、それ以外にもいくつか現状だとテストできないことがあります。 クライアント側で動作するscriptタグ(とstyleタグ)は、html文字列には含まれません。is:inlineを指定すれば含まれますが、これをテストするのはなかなか厳しいと思います。 クライアント側で動作するロジックをテストしたい場合は、関数として別ファイルで定義し、コンポーネント側ではscriptタグ内でimportして利用するのが現実的だと思います。 // src/lib/math.tsで定義して別途テストする export const add = (a:number,b:number) => a+b; <script> // コンポーネント側ではimportして利用 import {add} from "../lib/math.ts"; console.log(add(1, 2)); </script> もっとも、それでもテストし辛いような複雑なDOM操作やクラス追加などを実施するロジックがある場合は、そもそも部分的にReactやSvelteで作成したコンポーネントを用いるのが良いかもしれません。目的に応じてコンポーネントをAstro以外で作成することができるのもAstroの良い点です。 一方で、Astroコンポーネント内でReactやSvelteを利用している場合は、また 別途設定が必要 になるため注意が必要です。 さいごに 今回はAstroでのコンポーネントテストについて紹介しました。 もともとAstroにはコンポーネント単体を独立してレンダリングする機能がなく、そのためコンポーネントテストができないという課題がありましたが、Container APIの追加によって実現できることが増加しました。 まだできることも少ないですが、Astroは半月に1回ほどのペースで頻繁に機能追加を伴うリリースが行われているため、今後がより楽しみです。 参考 Astro 4.9 | Astro Astro Container API (experimental) | Docs data-astro-source-file added when devToolbar disabled · Issue #9324 · withastro/astro node-html-parser – npm roadmap/proposals/0048-container-api.md at rfc/container-api · withastro/roadmap Support for Astro components · Issue #18356 · storybookjs/storybook ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も随時受付中! ニフティに興味をお持ちの方は キャリア登録をぜひお願いいたします! キャリア登録 connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに こんにちは。会員向けアプリ「 マイ ニフティ 」の開発運用をしている村松です。 現在、マイ ニフティでは現在iOS・Androidアプリにユニットテストを追加しています。 その中で、iOSアプリでのAPIと通信する部分のユニットテストをサードパーティのMockライブラリなどを使わずに簡単に書くことができたので、紹介したいと思います。 AppleのWWDC18のセッションの「 Testing Tips & Tricks 」で紹介されている、URLProtocolを利用したMockのやり方を参考にしています。 背景 マイ ニフティ iOSアプリでは、通信処理では Alamofire を利用しています。APIからデータ取得する部分のユニットテストを追加する際にどのようにしていくか、調査すると、以下のような手段がありました。 Mockライブラリを使う DockerなどでAPIサーバーを立てる しかし、通信処理で複雑なことはしていないため、Mockライブラリを追加するほどではなく、また、DockerなどでAPIサーバーを立てるのは管理コストやテスト実行時間の点で、採用しにくいなと感じていました。 そんな中、URLProtocolを利用する方法だと、自分たちで簡単にMockを作成できそうだったので、採用してみました。 通信処理のMockの実装 URLSessionはURLProtocolのサブクラスで実際の処理を行うため、通信処理のMockをURLProtocolのサブクラスで実装し、URLSessionで利用するようにすれば、Mockを実現することができます。 URLProtocol のサブクラスで実装すべきメソッドは以下の4つあり、startLoading で通信処理のMockを実装します。 canInit canonicalRequest startLoading stopLoading public class LoginAPIMockURLProtocol: URLProtocol { override open class func canInit(with request: URLRequest) -> Bool { return true } override open class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } // 通信開始時に呼ばれるメソッド // Mock処理を実装する override open func startLoading() { } // 通信停止時に呼ばれるメソッド override open func stopLoading() { } } Mockの実装は求められるテスト粒度に応じて、実装していく流れになり、細かくやれば、 HTTPメソッドが正しいか リクエストパラメーターが正しいか APIで想定されているエラーを出し分ける などといったMockを実装することができます。 URLProtocolの実装ではURLProtocolClientのclientプロパティで、クライアント側に通信結果を伝えます。 通信成功時は urlProtocol(_:didReceive:cacheStoragePolicy:) urlProtocol(_:didLoad:) urlProtocolDidFinishLoading(_:) を呼び出します。 client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: responseData) client?.urlProtocolDidFinishLoading(self) 通信失敗時は urlProtocol(_:didFailWithError:) を呼び出します。 self.client?.urlProtocol( self, didFailWithError: APIMockError( errorDomain: "APIMockError.LoginAPIMockProtocol", errorUserInfo: [NSLocalizedDescriptionKey: "HTTPメソッドがPOSTではありません"] ) ) 以上を踏まえて、実装したコードが以下のコードとなります。 import Foundation import XCTest struct APIMockError: CustomNSError { var errorDomain: String var errorCode = 1 var errorUserInfo: [String: Any] } public class LoginAPIMockURLProtocol: URLProtocol { override open class func canInit(with request: URLRequest) -> Bool { return true } override open class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } // 通信開始時に呼ばれるメソッド // Mock処理を実装する override open func startLoading() { guard let originalRequest = task?.originalRequest else { self.client?.urlProtocol( self, didFailWithError: APIMockError( errorDomain: "APIMockError.LoginAPIMockProtocol", errorUserInfo: [NSLocalizedDescriptionKey: "リクエストを取得できませんでした"] ) ) return } guard let method = originalRequest.httpMethod, method == "POST" else { self.client?.urlProtocol( self, didFailWithError: APIMockError( errorDomain: "APIMockError.LoginAPIMockProtocol", errorUserInfo: [NSLocalizedDescriptionKey: "HTTPメソッドがPOSTではありません"] ) ) XCTFail("HTTP Method is not POST") return } guard let httpBody = originalRequest.httpBody else { self.client?.urlProtocol( self, didFailWithError: APIMockError( errorDomain: "APIMockError.LoginAPIMockProtocol", errorUserInfo: [NSLocalizedDescriptionKey: "リクエストボディが空です"] ) ) XCTFail("Request Body is empty") return } // リクエストJSONのデコード let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase jsonDecoder.dateDecodingStrategy = .iso8601 let loginUser: LoginUserAPIModel do { loginUser = try jsonDecoder.decode(LoginUserAPIModel.self, from: httpBody) } catch { self.client?.urlProtocol(self, didFailWithError: error) XCTFail("Request JSON is Invalid") return } let response: HTTPURLResponse var responseData: Data let jsonEncoder = JSONEncoder() jsonEncoder.keyEncodingStrategy = .convertToSnakeCase // 引数でMockの処理を変更している switch loginUser.password { case "200": // 正常の場合 let token = TokenAPIModel(accessToken: "accessToken") response = HTTPURLResponse( url: originalRequest.url!, statusCode: 200, httpVersion: "HTTP/2", headerFields: ["Content-Type": "application/json"] )! guard let data = try? jsonEncoder.encode(token) else { self.client?.urlProtocol( self, didFailWithError: APIMockError( errorDomain: "APIMockError.LoginAPIMockProtocol", errorUserInfo: [NSLocalizedDescriptionKey: "TokenAPIModel構造体のエンコードに失敗しました"] ) ) return } responseData = data case "401": // エラーの場合 let userAPIErrorResponse = APIErrorResponse(error: "401 error") response = HTTPURLResponse( url: originalRequest.url!, statusCode: 401, httpVersion: "HTTP/2", headerFields: ["Content-Type": "application/json"] )! guard let data = try? jsonEncoder.encode(userAPIErrorResponse) else { self.client?.urlProtocol( self, didFailWithError: APIMockError( errorDomain: "APIMockError.LoginAPIMockProtocol", errorUserInfo: [NSLocalizedDescriptionKey: "APIErrorResponse構造体のエンコードに失敗しました"] ) ) return } responseData = data default: self.client?.urlProtocol( self, didFailWithError: APIMockError( errorDomain: "APIMockError.LoginAPIMockProtocol", errorUserInfo: [NSLocalizedDescriptionKey: "レスポンス処理が設定されていないパスワードです"] ) ) return } // レスポンス client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: responseData) client?.urlProtocolDidFinishLoading(self) } // 通信停止時に呼ばれるメソッド override open func stopLoading() { } } 通信処理をMockに差し替えて、テストを作成 Mock自体の実装は完了したので、次は通信処理をMockに差し替えます。 通信処理はURLSessionで行われるので、URLSessionの設定である URLSessionConfiguration でMockに差し替える設定をします。 URLSessionConfiguration には protocolClasses という配列のプロパティが存在し、このプロパティにURLProtocolのサブクラスを設定することで、Mockに処理を差し替えることができます。 テストの setUpWithError でURLSessionConfigurationの設定をして、APIと通信するテスト対象の関数にURLSessionを渡すことで、Mockを利用したテストを実現できました。 override func setUpWithError() throws { let configuration = URLSessionConfiguration.af.default configuration.protocolClasses[0] = [LoginAPIMockURLProtocol.self] self.loginAPI = LoginAPI(session: Session(configuration: configuration)) } 以下のコードが今回のテスト対象です。 import Foundation import Combine import Alamofire struct LoginAPIModel: Codable { let username: String let password: String } struct TokenAPIModel: Codable, Equatable { let accessToken: String } struct APIError: Error { let statusCode: Int let errorResponse: APIErrorResponse } struct APIErrorResponse: Codable, Equatable { let error: String } class LoginAPI { private let session: Session init(session: Session = AF) { self.session = session } func login(username: String, pw: String) -> Future<TokenAPIModel, APIError> { let loginUser = LoginAPIModel(username: username, password: pw) let jsonEncoder = JSONEncoder() jsonEncoder.keyEncodingStrategy = .convertToSnakeCase let parameterEncoder = JSONParameterEncoder(encoder: jsonEncoder) let headers: HTTPHeaders = [.contentType("application/json"), .accept("application/json")] let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase return Future { promise in self.session .request( loginAPIURL, method: .post, parameters: loginUser, encoder: parameterEncoder, headers: headers ) .validate() .responseDecodable(of: TokenAPIModel.self, decoder: jsonDecoder) { response in switch response.result { case .success(let userToken): promise(.success(userToken)) case .failure: guard let statusCode = response.response?.statusCode else { promise(.failure(APIError( statusCode: 0, errorResponse: APIErrorResponse(error: "network error") ))) return } guard let errorResponse = try? jsonDecoder.decode(UserAPIErrorResponse.self, from: response.data!) else { promise(.failure(APIError( statusCode: statusCode, errorResponse: APIErrorResponse(error: "json decode error") ))) return } promise(.failure(APIError( statusCode: statusCode, errorResponse: errorResponse ))) } } } } } テストコードは以下になります。 import XCTest import Combine import Alamofire final class LoginAPITests: XCTestCase { private var loginAPI: LoginAPI! private var cancellables: Set<AnyCancellable> = [] // テストのセットアップで通信処理をMockに差し替え override func setUpWithError() throws { let configuration = URLSessionConfiguration.af.default configuration.protocolClasses[0] = [LoginAPIMockURLProtocol.self] self.loginAPI = LoginAPI(session: Session(configuration: configuration)) } override func tearDownWithError() throws { } func testLoginAPI() throws { let expectation = XCTestExpectation() expectation.expectedFulfillmentCount = 2 loginAPI.login(username: "username", pw: "200") .sink(receiveCompletion: { completion in switch completion { case .finished: break case .failure: XCTFail("LoginAPI 200 Test Fail") } }, receiveValue: { token in XCTAssertEqual( token, TokenAPIModel(accessToken: "accessToken"), "LoginAPI 200 Test is not Expected") expectation.fulfill() }).store(in: &cancellables) loginAPI.login(username: "username", pw: "401") .sink(receiveCompletion: { completion in switch completion { case .finished: break case let .failure(error): XCTAssertEqual( error, APIError( statusCode: 401, errorResponse: APIErrorResponse(error: "401 error") ), "LoginAPI 401 Test is not Expected" ) expectation.fulfill() } }, receiveValue: { _ in XCTFail("LoginAPI 401 Test Fail") }).store(in: &cancellables) wait(for: [expectation], timeout: 1) } } おわりに 実際に、Mockを簡単に実装できました。状況に応じて、Mockを細かく実装できるのが嬉しいですね。簡単にできるので、利用してみてください。 ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに こんにちは。キャリア採用で入社した入社1年目の西村です。 キャリア採用者は入社直後は、社内ルールや業務知識など、多くの情報を短期間で吸収しなければなりません。 企業ミッションや社内ルールは、キャリア採用者向けのプログラムや社内資料で理解を深められますが、技術面での対応は不足している部分を把握し、積極的に学習を進める必要があります。 学習リソースの活用 オープンな情報については、書籍、IT系勉強会、学習系ウェブサイト、動画など様々なリソースが活用できます。 新たな分野の学習では、専門家がまとめた資料を理解することで、体系的な知識を効率的に得られます。 自己啓発支援プログラム ニフティには自己成長を促す福利厚生があります。一例として下記があります。 書籍購入支援 特定資格の取得者への報奨金・手当支給 社内勉強会・LT大会の開催 Udemyの提供 これらの制度は継続的な学習と成長をサポートしてくれます。 上記以外の自己啓発に関するプログラムや働き方に関する福利厚生については、 下記をご参照ください。 福利厚生 | 採用情報 | ニフティ株式会社 (nifty.co.jp) AWS資格取得への挑戦 私の場合、オープンな技術においてAWSの関連知識が不足していると認識し、効率的な学習方法としてAWS認定資格の取得を目指しました。 「資格取得支援制度」と「Udemy」のAWS関連教材を活用し、計画的に学習を進めました。 資格取得のメリット 資格取得の最大のメリットは、最新情報を得る機会になることです。 IT分野の資格は定期的に内容が更新されるため、比較的新しい技術動向を学ぶことができます。 資格取得を通じて体系的な学習と知識の確認ができます。 まとめ 技術面での学習は入社半年で下記2つのAWS認定資格を取得し、知識の再構築ができました。 また目的であった業務への理解が深まり、新たな学習のモチベーションにもつながりました。 AWS Certified Solutions Architect (SAA) AWS Certified Cloud Practitioner (CLF) 資格取得以外にも、ハンズオンやチュートリアルを利用して実際に手を動かす事や、Todoアプリのような簡易的なシステムを作ってみる方法など、様々な学習方法があります。 状況に合った方法で、新たな知識を深めていきましょう! ニフティでは、 さまざまなプロダクトへ挑戦する エンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトより お気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering connpassでニフティグループに 参加いただくと イベントの お知らせが届きます! connpassで ニフティグループに参加する
アバター
はじめに こんにちは。ニフティ株式会社の佐々木です。 今回はTipsとして、VSCodeでGitHub Copilotを使い複数ファイルを認識させる方法についてご紹介します。 前提 GitHub CopilotとGitHub Copilot Chatの違い GitHub Copilotは、コード補完やコード生成を支援するAIツールです。一方、GitHub Copilot Chatは、チャット形式でユーザーと対話しながら、よりインタラクティブにコーディング支援を行うツールです。両者の主な違いは、ユーザーインターフェースと対話の方法にあります。 GitHub Copilotについての詳しい説明は こちら をご参照ください GitHub Copilotの仕様 認識範囲について GitHub Copilotは、基本的にアクティブなファイルのみを認識します。しかし、実際にVSCodeで開発していると、単一のファイルだけでなく複数のファイルを認識して欲しい場合がよくあります。 複数のファイルにまたがるコードの依存関係や構造をCopilotに認識してもらうことで、より適切な提案をしてもらえるので、結果的に、より質の高い回答を得ることにつながると思います。 複数ファイルを認識させる方法 では、GitHub Copilotで複数ファイルを認識させるにはどうすればよいでしょうか? 結論から言うと、 #file: や @workspace を付与して複数ファイルを認識してもらった上で、通常通りチャットで質問を投げる形でOKです。 #file:の活用 一例として、TypeScriptをベースにしたプロジェクトで、複数ファイルを認識させた上で、改善点を出力してもらうための質問を投げかけてみます。 #file:index.tsx #file:README.md #file:_variables.scss #file:Header.tsx #file:utils.ts これらのファイルをすべて含めてみた時の改善点を教えて下さい このように「参照済みのファイル」として複数ファイルにまたがるコード内を検索し、適切な回答が返ってきました。 @workspaceの活用 代表的な例として、 @workspace を付与することで、特定のファイルに限定せずともワークスペース全体を読み込んだ上で質問を投げることもできるようです。 例えば、コードの構造を把握するために以下のような質問文を投げてみます。 @workspace ワークスペース全体のコードの構成について教えて下さい このように、主要なディレクトリやファイルごとに大まかな構成の説明が返ってきました。 また、やや曖昧な表現で質問しても、ワークスペース内の必要なファイルを辿りつつ、具体的なコードの説明や修正までいい感じにやってくれました。 @workspace ワークスペース全体のCSSを確認し、必要があればリファクタリングしてください その他のコマンドについて 今回ご紹介しきれなかったもの以外にも、GitHub Copilotには「コードの提案、コードの説明、単体テストの生成、コマンドラインに関する質問」など様々なケースを想定したコマンドが用意されています。 質問の投げ方や詳しいコマンドの仕様については以下の公式ドキュメントから確認できるので、良く使いそうな機能について押さえておくと良いかもしれません。 https://docs.github.com/en/copilot/using-github-copilot/prompt-engineering-for-github-copilot https://docs.github.com/en/copilot/using-github-copilot/asking-github-copilot-questions-in-your-ide まとめ 今回はGitHub Copilotに複数ファイルを認識させる方法をTipsとして紹介しました。 GitHub Copilotは非常に便利で強力ですが、こちらが期待した動作をしない場合もあり、まだ痒いところに手が届かないことも多いのかなと思います。 今後、AIによるコード支援はさらに進化していくと予想されるので、ぜひ、この記事で紹介した方法を試して自分なりのGitHub Copilotの活用法を見つけてください。 また、この記事を書くにあたって以下の記事を参考にさせていただきました。 【C#】Visual Studio で GitHub Copilot に複数のファイルやコードを認識させる方法 – Qiita We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 カジュアル面談も受け付けています! カジュアル面談 Tech TalkやMeetUpも開催しております! こちらもお気軽にご応募ください! Event – NIFTY engineering ニフティ株式会社 – connpass
アバター