every Tech Blog

株式会社エブリーのTech Blogです。

Amazon Q in QuickSightで計算フィールドを作成してみる

はじめに

こんにちは、開発部でデータサイエンティストをしている蜜澤です。

ついに東京リージョンでAmazon Q in QuickSightがGAしました!
データストーリー、シナリオ、トピックなど自然言語でデータ分析を行う機能が追加されましたが、このあたりの機能の解説は以前の弊社のブログや、他の方が記事にされているので、そちらをご覧いただければと思います。
今回はAmazon Q in QuickSightを計算フィールドを作成する際に使用することで、ビジュアル作成をより効率化させられないかの検証をしたいと思います。

使用する模擬データ

検証には以下のような、レシピ動画サイトの特定のレシピの日毎のインプレッション数とクリック数を集計したという想定の模擬データを使用します。

それぞれのカラムの定義は以下の通りです。

  • date:日付(2025-01-01~2024-04-30)
  • recipe:レシピ名(ハンバーグ)
  • click:レシピをクリックした回数(1~10の整数の乱数)
  • impression:レシピが表示された回数(100~200の整数の乱数)

やってみる

まずは、CTRを作成できるか試してみます。
CTR = click / impresseion
を期待しています。

計算フィールド追加を選択し、「計算を作成」をクリックします。

テキストフォールドが出てくるので、そこに作成したい計算式を入力します。
今回は「CTR」と入力。
計算式を書いたら、「作成」をクリックします。

2〜3秒ほどで計算式が作成されました。
期待している通りの計算式ができたので、「挿入」をクリックします。

計算式が挿入されたので、最後に「保存」をクリックして、計算フィールドの作成完了です。

自然言語で、計算フィールの計算式を作成することができました。
CTRの定義を与えなくても作成できるのは良いなと思いました!
しかし、これくらい簡単な式ならQを使わずに、自分で作った方が早いと思うので、もう少し複雑な計算式を作成してみます。

日、週、月といった日付の粒度をパラメータで指定することで、日付の集計単位が変わるような計算フィールドを作成します。
下準備として、DateGranularityという文字列のパラメータを作成します。
このパラメータで日、週、月を指定します。

先ほど作成したCTR計算を、分子と分母が粒度の変更に伴って合計されるように変更します。

日本語でプロンプトを書いたらあまり良い出力を得られなかったので、英語でプロンプトを書きました。
うまくいかなかったプロンプトは記事の最後の方で紹介します。
何パターンか試しましたが、weekの時にWKにならずDDになってしまうのは、改善しなかったので、今回は諦めて手動で直します。

得られた結果を挿入して、day、week、monthを日、週、月に、粒度が週の時のDDをWKに置き換えて、保存します。

粒度を週にするとevent_dateが週の始め(日曜日)になり、月にするとyyyy-mmで月の形式になる日付計算フィールドが完成しました。

最後にもう少し複雑な計算フィールドに挑戦します。
StartDateとEndDateというパラメータを作成し、粒度に合わせて開始日と終了日を調整してくれる日付のフィルターを計算フィールドで作成します。
粒度で月を選択している状態で、StartDaeが2025-01-15、EndDateが2025-02-07と入力された場合に、2025-01-01~2025-02-28のデータが表示されるといった想定になります。

以下のようにプロンプトを入力しました。

DAY、WEEK、MONTHを日、週、月に置き換えて保存します。

粒度を日にすると開始日と終了日の間の日付フィルターが1になり、週にすると開始日と終了日を含む日の週始めから週終わりまでの日付フィルターが1になっているので、期待通りのものができていそうです。
最後にフィルターで、日付フィルターが1の場合のみ表示するようにすれば、粒度に合わせて開始日と終了日を調整してくれる日付のフィルターの完成です!

このフィルターを自作した時はかなりの時間がかかったので、一瞬でできて感動しました!

うまくいかなった例

うまく計算フィールドが作成できなかったプロンプトも紹介します。

簡潔に日本語でプロンプトを入力したら、エラーが出ました。

日本語でももう少し詳しくプロンプトを書いたら、出力を得ることはできました。
しかし、週がではなく、年が設定されました。

日本語のプロンプトに対する出力の精度の向上は今後に期待です!

さいごに

今回はAmazon Q in QuickSightを使用して計算フィールドを作成する検証を行いました。
簡単な計算フィールドの作成はQを使用しない方が早いと思いますが、少し複雑な計算フィールドを作成する時や、やりたいことは決まっているが式がパッとは思いつかない時には、開発を効率化できる機能だと感じました。
最後まで読んでいただきありがとうございました!

Goで実装するUnicode文字数カウントが実はわりと難しい的な話

開発2部の内原です。文字コードの話は大好物です。

一般的に、アプリケーションの開発において文字数カウントは非常に身近な機能です。パラメータ取得時やフォーム入力時など、様々な場面で文字数計算を実装する機会があります。

しかし、Unicode文字、特に絵文字や結合文字などが混在するテキスト処理において、「正しい文字数カウント」は意外に複雑な問題です。

この記事では、Go言語でのUnicode文字数カウントに焦点を当てて、実装時に注意すべき点を述べます。

文字数カウントの罠

まず、以下のコードについて考えます。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "Hello👍🏿" // 6文字?

    fmt.Printf("バイト数: %d\n", len(s))                    // バイト数: 13
    fmt.Printf("ルーン数: %d\n", utf8.RuneCountInString(s)) // ルーン数: 7
}

一見6文字のように見える文字列ですが、実際にカウントすると異なる値が返却されます。これはどういう状況でしょうか?

この差異は、例えば以下のような場面で問題が発生し得ます。

  • フォーム入力時
    • 文字数制限を超過して入力できる
  • データベース投入時
    • カラム文字数の制限に抵触しDBエラーが発生する
  • UI表示
    • 文字数カウンターの表示がユーザーの感覚と合わなくなる

文字数カウント手法

Unicode文字を正しく扱うために、用途に応じて適切なカウント手法を選択する必要があります。

1. byte数カウント len()

UTF-8のバイト列として参照します。

func countBytes(s string) int {
    return len(s)
}

// 例
fmt.Println(countBytes("Hello"))  // 5
fmt.Println(countBytes("こんにちは")) // 15 (ひらながはUTF-8では3バイト)
fmt.Println(countBytes("café"))   // 5 (é[U+00E9]はUTF-8では2バイト)

2. rune数カウント utf8.RuneCountInString()

Go言語の内部コードである rune 単位で、 rune 1つがUnicodeのコードポイント1つに対応します。

func countRunes(s string) int {
    return utf8.RuneCountInString(s)
}

// 例
fmt.Println(countRunes("Hello"))  // 5
fmt.Println(countRunes("こんにちは")) // 5
fmt.Println(countRunes("café"))   // 4

3. 正規化後カウント

Unicodeでは、一見同じ見た目でも異なるUnicode表現になるものが存在します。結合文字(Combining Character)という文字数としてカウントしないコードポイントが存在します。

例えば以下の文字は一見同じ文字に見えます(環境によっては別物に見えるかも知れません)が、コードポイントしては別物で、また文字数も異なります。

  • が [U+304C]
  • が [U+304B U+3099]

これを一つの表現に統一するのが正規化で、正規化してからカウントします。

import "golang.org/x/text/unicode/norm"

func countNormalizedRunes(s string) int {
    normalized := norm.NFC.String(s)
    return utf8.RuneCountInString(normalized)
}

// 例
s1 := "が" // [U+304C]
s2 := "が" // [U+304B U+3099]

fmt.Println(s1 == s2)                            // false
fmt.Println(countRunes(s1), countRunes(s2))      // 1 2
fmt.Println(countNormalizedRunes(s1))            // 1
fmt.Println(countNormalizedRunes(s2))            // 1

正規化形式の選択

Unicodeには4つの正規化形式があります。最終的に獲得したい形式に対応した正規化方法を選ぶ必要があります。

名前 説明 特徴
NFC 正規化(合成) 視覚的に同じなら同じコードにまとめる
NFD 正規化(分解) 組み合わせ可能な文字を分解する
NFKC 互換正規化(合成) 表示上同じ意味の文字を1つにまとめる(全角→半角など)
NFKD 互換正規化(分解) 分解かつ互換性も加味
func compareNormalizationForms() {
    s1 := "が"
    fmt.Printf("NFD: %s(%U)→%s(%U)\n", s1, []rune(s1), norm.NFD.String(s1), []rune(norm.NFD.String(s1))) // NFD: が([U+304C])→が([U+304B U+3099])
    s2 := "が"
    fmt.Printf("NFC: %s(%U)→%s(%U)\n", s2, []rune(s2), norm.NFC.String(s2), []rune(norm.NFC.String(s2))) // NFC: が([U+304B U+3099])→が([U+304C])
    s3 := "Ⅳ"
    fmt.Printf("NFKC: %s(%U)→%s(%U)\n", s3, []rune(s3), norm.NFKC.String(s3), []rune(norm.NFKC.String(s3))) // NFKC: Ⅳ([U+2163])→IV([U+0049 U+0056])
    s4 := "A1"
    fmt.Printf("NFKC: %s(%U)→%s(%U)\n", s4, []rune(s4), norm.NFKC.String(s4), []rune(norm.NFKC.String(s4))) // NFKC: A1([U+FF21 U+FF11])→A1([U+0041 U+0031])
    s5 := "㎝"
    fmt.Printf("NFKD: %s(%U)→%s(%U)\n", s5, []rune(s5), norm.NFKD.String(s5), []rune(norm.NFKD.String(s5))) // NFKD: ㎝([U+339D])→cm([U+0063 U+006D])
}

4. 書記素クラスタ数(ユーザー知覚文字数)

Unicodeにはゼロ幅接合子(Zero Width Joiner)やModifierなど、文字数としてはカウントしない種類のコードポイントも存在します。

これらのコードポイントを用いると、特定のコードポイントと組み合わせて別の文字表現ができるようになります。これにより必要となるコードポイント数が減らすことができます。絵文字における肌の色を変更したり、🇯🇵をJ+Pのように表現する、といった用途に使われます。

最終的に、環境に依るところはありますがここで表示される文字列の表現がユーザーが実際に認識する文字数と言えます。

ただ、この算出を自力で実装するのはかなり大変なので、公開されているライブラリに頼るほうがよいと思います。

注意:最後の2つの文字がブログの仕様で複数文字に分割して表示されていますが、本来は 🏳️‍🌈 と 👨‍👩‍👧‍👦 です。

import "github.com/rivo/uniseg"

func countGraphemes(s string) int {
    gr := uniseg.NewGraphemes(s)
    count := 0
    for gr.Next() {
        count++
    }
    return count
}

// 例
fmt.Println(countGraphemes("Hello"))     // 5
fmt.Println(countGraphemes("こんにちは"))    // 5
fmt.Println(countGraphemes("café"))      // 4
fmt.Println(countGraphemes("😀"))      // 1 [U+1F600] (絵文字は1文字として認識)
fmt.Println(countGraphemes("🇯🇵"))     // 1 [U+1F1EF U+1F1F5](Regional Indicator Symbolsは1文字として認識)

fmt.Println(countGraphemes("🏳️<200d>🌈"))     // 1 [U+1F3F3 U+FE0F U+1F308] (白旗 + 異体字セレクタ + 虹 = レインボーフラッグは1文字として認識)
fmt.Println(countGraphemes("👨<200d>👩<200d>👧<200d>👦")) // 1 [U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466] (家族絵文字も1文字)

まとめ

文字数のカウントという一見簡単そうで実はいろいろとややこしい問題について記事を書いてみました。

Go言語でUnicodeを扱う場合、ある程度までは言語としてのサポートを受けられますが、そもそもUnicodeの仕様としてコードポイントと人間が認識する文字数には齟齬があるため、これらの差分を埋めるためにはどうしても実装時に考慮が必要となります。

やりたいことに応じた適切なカウント手法を採用するように心がけましょう。

日本CTO協会 新卒合同研修2025 に参加しました!(後編)

はじめに

前半の記事では、新卒合同研修の第1回から第3回までの内容をご紹介しました。 前半の記事もぜひご覧ください!

tech.every.tv

後半となる本記事では、第4回から第6回の講義についてお伝えします!

後半の研修は、より実践的な内容が盛りだくさんでした。実際にサーバーを解体してハードウェアの仕組みを学んだり、ISUCON形式でパフォーマンスチューニングに挑戦したり、最新の生成AI技術について学んだりと、エンジニアとして知っておきたい幅広い知識を身につけることができました。

それでは、各回の内容を見ていきましょう!

6/4開催 4回目 サーバー解体研修

こんにちは、開発1部 デリッシュキッチンAWG PUの黒髙です。私の方からはGMOぺパボ株式会社様に主催していただいた「サーバー解体研修」について紹介します。

GMOインターネットグループ株式会社様の会場提供により、GMO Yoursで開催されました。

サーバー基礎講座

前半は、デル・テクノロジーズ株式会社の福田さんに「サーバー基礎講座」と題して、大きく以下三つの内容を講義していただきました。

  • サーバーの基礎 - パソコンが「個人」で使うことを想定しているのに対し、サーバーは「みんな」で使うことを前提としており、24時間365日の連続稼働や高い信頼性が求められる、といったお話でした。
  • サーバーの構造 - CPU, DRAM, SSD/HDD, NICといった各構成部品の説明。データの正確性を保つECCメモリや、ストレージの冗長化を実現するRAID構成など、サーバーならではの部品についても学びました。
  • サーバーならではの付加機能 - PowerEdgeサーバーのリモート管理ツールであるiDRAC(integrated Dell Remote Access Controller)について解説いただきました。

私が特に興味深かったのは、サーバーがいかに「停止しないこと」を重視して設計されているかという点です。電源や冷却ファンの冗長化、リモートでの管理機能など、すべてがサービスの継続性を支えるための工夫であり、普段利用しているPCとの設計思想の根本的な違いを理解することができました。

このように事前にハードウェアの知識をインプットしたことで、より一層「サーバーの内部を見てみたい」という気持ちが高まりました。

サーバー解体作業

続いて、研修の目玉である後半のサーバー解体についてレポートしていきます。 会場にはサーバーが複数台用意されており、任意のチームに分かれてそれぞれ解体を進めていきました。

こちらが私たちのチームで解体していくサーバーになります。データセンターのラックに効率的に搭載するため、一般的なタワー型PCよりも横向きに細長い「ラックマウント型」と呼ばれる形状をしています。この一台で軽自動車が買えるほどの値段がすると聞いた時は、大変驚きました。

まずは天板を外します。CPU、メモリ、電源ユニットなど、基本的なパーツ構成は通常のPCと同じですが、各パーツの信頼性や拡張性は大きく異なっていました。例えば、電源ユニットは二重化されており、片方が故障してもサーバーは停止しません。また、冷却ファンが多数搭載されている様子からも、高性能なパーツを安定して動かす強力な冷却性能が見てとれます。

各チームのサーバーはそれぞれ構成が異なっており、私たちのものは特にメモリの搭載量が多いモデルでした。GPUを搭載しているチームもあったようで、少し羨ましかったのを覚えています。

解体作業に特定のマニュアルはなく、各メンバーが「これは何だろう?」と興味の赴くままに、ときには協力し合いながら進めていきました。 その結果、以下のように多くのパーツを取り出すことができました。

工具を使わない範囲での解体だったため限界はありましたが、裏を返せば、交換頻度の高いメモリやストレージなどのパーツは、専門的な技術者でなくても簡単に扱えるよう設計されていることもわかりました。

復元作業とメンテナンス性

さらに、ここから元の状態に戻す「復元作業」も行います。 目に入ったものを次々と取り出していく解体作業よりも、パーツを取り付ける順序などを考慮する必要があるため、少し身構えました。 しかし、実際にはそこまで難しくありませんでした。各パーツは所定の場所にしか収まらないように工夫されており、メンバーと確認しながら作業を進めることで、意外とすぐに元の状態に戻すことができました。

この一連の作業を通して、サーバーがいかにメンテナンス性を考慮して設計されているかを実感しました。実際に、このサーバーのストレージ(HDD/SSD)や電源はホットプラグ(サーバーの電源を入れたまま部品の交換ができる仕組み)に対応しているというお話を聞き、まさに常時稼働が求められるサーバーならではの特徴だと感じました。

HDDの解体

懇親会では、特別にHDDの解体もさせていただきました。HDDは、プラッタ(データを記録する円盤)とヘッド(データを読み書きする部分)の隙間が数ナノメートルと非常に狭く、空気中の微細な塵が付着するだけでも壊れてしまうため、このHDDが再び動くことはありません。普段目にすることのできない精密な内部構造に、思わず見入ってしまいました。

普段触れることのできないハードウェアにたくさん触れることができ、とても貴重な体験でした!

まとめ

AWSなどクラウドサービスの利用が当たり前になり、私たちにとってサーバーはより抽象的な存在になりつつあります。しかし、物理的なサーバーの仕組みを理解することは、クラウド上で発生するパフォーマンスの問題や障害に対して、より深いレベルでの洞察を与えてくれます。 そのため、今回得られたハードウェアの知識は、インフラの設計や障害対応など、より低いレイヤーを扱う際に必ず役に立つと感じました。

もはやクラウドを採用するのが当たり前のようになっている昨今ですが、サービスの特性やコスト、セキュリティ要件といった運用のユースケースに応じて柔軟なインフラ選択が求められます。その中で、自社でサーバーを持つオンプレミスも、必要に応じて選択肢に入れるべきだと感じました。

6/11開催 5回目 ISUCON研修

開発1部 デリッシュキッチンMS DRMの鈴木です。私からはISUCON研修の紹介をします。

ISUCONとは、「いい感じに スピードアップ コンテスト」の略称で、お題となるWebサービスを決められたレギュレーションの中でどれだけ高速化できるかを競う大会のことです。

isucon.net

今回のISUCON研修では、本番のISUCONのようにチームで与えられた問題に取り組む形式で行われました。

事前準備

私はISUCON初挑戦だったのですが、事前にISUCON出場経験のある先輩にISUCONでよく使われるツールやテクニックに関する知見を共有して頂きました。

ツールとしては以下のようなものがあり、研修当日に活用しました。

  • pt-query-digest
    • slowqueryの解析に使うツール
    • mysqlのログを解析してsqlが遅い順に表示される
  • alp
    • NGINXのアクセスログの解析に使うツール
    • 遅いエンドポイント順に表示される

研修当日

Step 0: 準備 - Gitでのコード管理

本格的な改修を始める前に、まずEC2インスタンス上にあったコードをGit管理下に置き、GitHubリポジトリにプッシュしました。これにより、変更履歴の追跡や、問題発生時の切り戻しが容易になり、安心して作業を進めるための基盤が整いました。

Step 1: N+1クエリの特定と解消 - DB負荷の最大の原因を叩く

課題

アプリケーションの動作を分析したところ、トップページを表示するだけでデータベースに大量のクエリが発行されていることが判明しました。これは、投稿の一覧を表示するループ処理の中で、投稿ごと・コメントごとに個別のSQLクエリを発行してしまう、典型的な「N+1クエリ問題」でした。

分析

問題となっていたのは、app.gomakePosts関数です。以下のように、forループの中で都度DBアクセスが発生していました。

// 問題のあったコード(抜粋)
for _, p := range results {
    // ループごとにコメント数やユーザー情報を取得していた
    db.Get(&p.CommentCount, "SELECT COUNT(*) FROM `comments` WHERE `post_id` = ?", p.ID)
    db.Select(&comments, "SELECT * FROM `comments` WHERE `post_id` = ?", p.ID)
    // ...
}

解決策

ループの外側で、必要なデータを一度にまとめて取得する方式に変更しました。

  1. 表示対象となる投稿のIDをすべて集める。
  2. SQLのIN句を使い、関連するコメント情報やユーザー情報を一括で取得する。
  3. 取得したデータをGoのmapに格納する。
  4. ループの中ではDBにアクセスせず、mapからデータを参照して構造を組み立てる。

この修正により、発行されるクエリ数は、投稿数に関わらず一定(数回)にまで激減しました。

Step 2: データベースインデックスの追加 - 検索速度の向上

課題

N+1問題を解決した後、次にpt-query-digestを使ってスロークエリログを分析したところ、特定のクエリが依然として遅いことがわかりました。特にORDER BY created_at DESCによるソート処理がテーブル全体をスキャン(フルテーブルスキャン)しており、大きなボトルネックとなっていました。

解決策

パフォーマンスを改善するため、以下のインデックスをデータベースに追加しました。

-- トップページの投稿一覧表示を高速化
CREATE INDEX idx_posts_created_at ON posts (created_at);

-- ユーザーページの投稿一覧表示を高速化
CREATE INDEX idx_posts_user_id_created_at ON posts (user_id, created_at);

-- コメント一覧の取得を高速化
CREATE INDEX idx_comments_post_id_created_at ON comments (post_id, created_at);

これにより、検索やソートがインデックスを使って効率的に行われるようになり、クエリの実行速度が大幅に向上しました。

Step 3: 高コストな外部コマンド呼び出しの排除 - CPU負荷の削減

課題

CPU使用率を調査する中で、ユーザー登録やログイン時に呼ばれるパスワードのハッシュ化処理がボトルネックとなっている可能性が浮上しました。コードを確認すると、exec.Commandを使って外部のopensslコマンドを呼び出していました。

// 修正前の digest 関数
func digest(src string) string {
    out, err := exec.Command("/bin/bash", "-c", `... | openssl dgst -sha512 | ...`).Output()
    // ...
}

外部プロセスの起動は非常に高コストな処理です。

解決策

この処理を、Goの標準ライブラリcrypto/sha512を使って、Goのプロセス内で完結するように書き換えました。

// 修正後の digest 関数
import (
    "crypto/sha512"
    "encoding/hex"
)

func digest(src string) string {
    hasher := sha512.New()
    hasher.Write([]byte(src))
    return hex.EncodeToString(hasher.Sum(nil))
}

この修正により、プロセス起動のオーバーヘッドがなくなり、CPU負荷を大幅に削減できました。

Step 4: ページネーションによるデータ取得の効率化

課題

主要なボトルネックを解消していくと、それまで隠れていた新たな問題が見えてきました。トップページでは20件の投稿を表示しているにもかかわらず、SQLでは全件の投稿データを取得しており、無駄な処理が行われていました。

解決策

トップページの投稿を取得するクエリにLIMIT 20を追加しました。

-- 修正前
SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC

-- 修正後
SELECT `id`, `user_id`, `body`, `mime`, `created_at` FROM `posts` ORDER BY `created_at` DESC LIMIT 20

この単純な修正により、DBからアプリケーションへのデータ転送量が削減され、メモリ使用量とDB負荷の両方が改善されました。

(なお、あとになって気づくのですが、削除済みユーザーのフィルタリングによる表示数不足を補うために、少し多くLIMIT 30くらいにしないといけません。)


しかしながら、残り1時間を切ったところで問題が発生しました。用意されたデータを誤って削除してしまったようです。 運営の皆さんに助けを求めたところ、新しいEC2を立てていただきました。皆さん、データバックアップは取っておきましょう、、、

終了時刻が刻一刻と迫ってくる中、なんとかスコアを伸ばそうと、手早く「データベースインデックスの追加 」、「ページネーションによるデータ取得の効率化」を行いました。なお、ページネーションでは、余裕を持ってLIMIT 50にしておきました。


まとめ

最終的に、私たちのチーム「アポヒ」は21350点43チーム中17位という結果になりました。1時間弱の格闘ではこの点数が限界でした、、

データバックアップを取っていなかったことが一番の反省点ですが、同時に実践形式で学ぶことも多く有意義な研修でした。ぜひ本家ISUCONにリベンジしたいと思います。

6/18開催 6回目 生成AI活用

開発1部 デリッシュキッチンMS DRMの惟高です。 私からは生成AI活用の紹介をします。

本研修は、大きく2つのテーマで構成されていました。 1. マイクロソフトの変遷とAIエージェントの概念 2. AI時代におけるソフトウェアエンジニアの役割の変化

AIの進化

AIの進化は、私たちがテクノロジーと関わる方法を大きく変えようとしています。

これまで、エンジニアのコーディングをアシストするCopilotのようなAIツールがありましたが、 マイクロソフトでは、2025年をAIエージェント元年と位置づけ、自然言語であらゆる操作や管理を可能にする新たな挑戦が進められています。

AIエージェントとは、ユーザーがタスクを割り振ると自律的に完遂する「代理人」のような存在です。 マイクロソフトは複雑な業務を自律的にこなすエージェントの提供を始めており、将来的にはエージェント同士が連携するマルチエージェントの世界が構想されています。

エンジニアの役割の変化

AIが発展する以前のエンジニアは、顧客課題の発見から設計、コーディング、チューニング、システムの監視に至るまで、人間主導型であらゆる工程をこなす必要がありました。

しかし、AIの登場によりこれまで人間が行っていた多くの作業をAIに任せられるようになりました。 今後は、AIに対して適切な指示を出し、その出力を評価し、最終的により良いアウトプットを形成する役割が重要とのことでした。

誰もがフルスタックエンジニアになれる可能性を秘めている一方で、これまでのエンジニアのようにあらゆるタスクを人手のみでこなすのではなく、AIを活用して全体の調和を取りながらプロジェクトを推進するような姿勢に変わらなければ、エンジニアとして生き残るのが難しいと感じました。

開発スタイルの変化

GitHubの統計によると、AIを利用した開発者数は2024年の2割未満から2028年には9割近くに増加すると予測されています。 つまり、数年後には社会全体でAIを使って効率化していこうというムーブメントが起こる可能性があるということです。

現状のAIによる開発がコーディングに限られているのに対して、将来的には企画・設計・実装・テスト・運用といったソフトウェア開発のライフサイクル全体に活用されることで、開発スタイルが大きく変わると考えられています。

コーディングスタイルの変化では、現在のCopilotによるAI支援(ペアプログラミング)から、AIが自律的に作業を行うチームの一員(ピアプログラミング)へと進み、さらに2030年には、AIがほぼ完全に自律的にソースコードを実装する時代が到来すると予測されています。

この未来では、エンジニアの主な役割は「要件定義」と「AIが作成したものがその要件を満たしているかの確認」となり、プロダクトマネージャーのような、サービス・製品の方向性を定める役割が重要視されます。

人間がやり続けること

AIがコードを生成する時代においても、そのコードの正しさを理解するためのプログラミングの基礎知識は依然として重要とのことでした。 しかし、AIがコードを書けてしまうため、今後は実践的な力よりも知識として知っておくことが求められるようになるそうです。

それよりも重要になってくるのが、AIに意図を正確に伝えるための「プロンプトエンジニアリング」の能力です。 AIは推論の中でハルシネーションを起こす可能性もあるため、目標を明確に宣言し、自然言語で要件や意思を的確に記述するスキルが不可欠だと説明いただきました。

また、AIは限界値テストや倫理的な判断が苦手なため、最終的なテストは依然として人間が行うべきとのことでした。 企業倫理やガバナンス、国のポリシーなど、人間の価値観に基づく判断は、今後も人間の重要な役割として残ると考えられています。

まとめ

本研修を通して、ここから数年の内にAIが自律的に実装する時代が来ると再認識しました。(もうすでに来ているのかもしれません...)

AIは私が考えているよりも身近なものになり、今後は「息をするようにAIを使う」ことが重要になってくると感じました。

またAIに対しては、何をして欲しいかといった指示を的確に伝える能力が必要になり、言語化能力がより必要になってくると感じました。

今後はAIを使い倒し、少しでも効率よく開発ができるように試行錯誤していきたいと思います。

おわりに

全6回の新卒合同研修を通じて、本当にたくさんのことを学ぶことができました。

技術的な知識はもちろんですが、何より良かったのは他社の新卒エンジニアの皆さんと出会えたことです。同じような悩みを抱えていたり、違う視点でものを見ていたり、刺激をもらうことがたくさんありました。研修後も連絡を取り合う仲間ができたのは、この研修の大きな収穫の一つです。

また、各分野の第一線で活躍されている講師の方々から直接学べたのも貴重な経験でした。最新技術のトレンドや現場での実践的な情報など、参考書では学べない生きた知識を得ることができました。

この研修で得た知識や経験、そして仲間たちとのつながりを大切にしながら、これからもエンジニアとして成長していきたいと思います。

最後に、このような素晴らしい機会を提供してくださった日本CTO協会の皆様、各回の講師の皆様、そしてスポンサーをしていただいた企業様に心から感謝いたします。ありがとうございました!

日本CTO協会 新卒合同研修2025 に参加しました!(前編)

はじめに

こんにちは、株式会社エブリーの2025年新卒エンジニアです。 私たちは、2025年5月から6月にかけて開催された日本CTO協会主催の新卒合同研修に参加しました。本記事では、研修の概要や各回の講義内容、そして実際に参加して得られた学びや気づきについてご紹介します。

2024年の新卒合同研修参加レポートも是非ご覧ください!

tech.every.tv

新卒合同研修とは

日本CTO協会が主催する新卒合同研修は、会社の枠を超えて新卒エンジニアが業界全体で成長できる場をつくることを目指しています。背景には、エンジニア不足や、スタートアップ・中小企業にとって新卒育成のコストが大きいといった、業界共通の課題があります。
そこで、さまざまな企業や専門家が協力し、最新技術や実践的なスキル、キャリア形成、クラウド、サーバー解体、ISUCON、生成AIなど、幅広いテーマで講義やハンズオンが行われました。
2025年は下記のような日程で開催されました。

研修回 テーマ・内容 講師/スポンサー
第1回 Google Cloudのスペシャリストと学ぶ!BigQuery & Gemini グーグル・クラウド・ジャパン合同会社
第2回 CTOから新卒に向けた講話、生成AI時代のソフトウェアエンジニアとしての働き方の期待値 日本CTO協会 / 株式会社LayerX、株式会社Progate
第3回
(初学者・中級者向け)
AWS JumpStart アマゾンウェブサービスジャパン合同会社
第3回
(上級者向け)
AWSサービスを使いISUCONで高得点を出そう! アマゾンウェブサービスジャパン合同会社 / 日本CTO協会 若手エンジニアコミュニティ有志
第4回 サーバー解体研修 GMOペパボ株式会社
第5回 日本CTO協会ISUCON新卒研修+解説(※事前課題あり・クリア必須) 株式会社PR TIMES / ピクシブ株式会社
第6回 生成AIに関する講義 日本マイクロソフト株式会社

研修はオフラインで開催され、他社の新卒エンジニアと交流したり、コミュニティを広げたりする機会もたくさんありました。
この取り組みは、エンジニアとしてのキャリアのスタートを後押しし、業界全体の成長につなげることを目指しています。
詳しくはこちらをご覧ください。

cto-a.org

ここからは、それぞれが印象に残った講義を1つずつピックアップし、内容や学びについてご紹介していきます。

5/14開催 1回目 Google Cloud のスペシャリストと学ぶ! BigQuery & Gemini

開発1部 デリッシュキッチンMS SPの谷口です。私からはグーグル・クラウド・ジャパン合同会社にて開催された第1回研修「Google Cloud のスペシャリストと学ぶ! BigQuery & Gemini」について紹介します。

この研修では、Google Cloudのスペシャリストの方々からBigQueryやGeminiについて直接学ぶ貴重な機会をいただきました。特にGoogle Cloudのデータ分析・AI関連サービスを中心に、現代のデータ活用における最新のアプローチについて詳しく教えていただきました。また、後半ではNotebookLMの活用方法についてグループディスカッションを行い、参加者同士で様々なアイデアを共有する時間がありました。

BigQueryの進化とデータ活用の新時代

フルマネージドなデータウェアハウス

Google CloudのBigQueryはサーバーレスアーキテクチャで構築されたフルマネージドなデータウェアハウスサービスです。

従来のデータ分析ではインフラの管理やスケーリングに多くのリソースを割く必要がありました。しかし、BigQueryを活用することで、これらの運用負荷から解放され、データ分析そのものに集中できるようになるという話をまずしていただきました。

Gemini in BigQuery

自然言語でのデータ分析

研修で最も驚いたのは、Gemini in BigQueryの自然言語でクエリを記述できる革新的な機能でした。

これまで複雑なSQLを書く必要があった分析作業が、日本語での質問形式で実行できるようになります。例えば「先月のユーザー登録数の推移を教えて」といった自然な問いかけから、適切なSQLクエリが自動生成されます。

分析の民主化

この機能により、SQL知識が限定的なビジネスサイドのメンバーでも、直接データウェアハウスに問い合わせを行うことができるようになります。

私はこれを聞いて、真の意味での「分析の民主化」だと感じました。組織全体でのデータ活用レベルの底上げに大きく貢献するはずだと思います。

NotebookLMセッション

実践的なAI活用事例

NotebookLMは、Googleが開発したAIを活用したリサーチアシスタントで、アイデアの洗練と整理をサポートしてくれます。アップロードした文書やデータを基に、質問応答や要約、分析などを行うことができます。

NotebookLM活用事例のセッションでは、参加者同士で様々な活用事例を共有することができました。

文書の要約、質問応答、アイデア生成など、日常業務で即座に活用できる具体的な使い方を学ぶことができ、非常に実践的な内容でした。

グループディスカッションでの活用アイデア

セッションでは、グループに分かれてNotebookLMの活用方法についてアイデアを出し合うセクションがありました。

その中で特に興味深かったのは、会議の文字起こしとNotebookLMを組み合わせた活用方法というアイデアです。

会議内容を文字起こしした後、その内容をNotebookLMに追加することで、会議中に出てきた専門用語や不明な点について後から詳しくNotebookLMに質問することができるのではないか、という提案がグループから出ました。

今後の活用に向けて

データドリブンな意思決定の加速

今回学んだGoogle Cloudのサービスを活用することで、データに基づいた意思決定のスピードを大幅に向上させることができると思います。

特にGemini in BigQueryの自然言語クエリ機能により、データ分析から洞察の獲得、そして施策の立案までのサイクルを劇的に短縮できる可能性があります。

チーム全体でのデータ活用レベル向上

自然言語インターフェースの活用により、これまでデータ分析に関わることが少なかった私たちのチームメンバーも積極的にデータを活用できるようになると期待しています。

これにより、組織全体でのデータリテラシー向上と、より多角的な視点からの分析が可能になるのではないでしょうか。

まとめ

今回のGoogle Cloud入門セッションを通じて、私はデータ分析とAI活用の新しい可能性を学ぶことができました。

特に印象的だったのは、技術的な障壁を下げることで「すべてのエンジニア」「すべてのメンバー」がデータとAIを活用できる環境が整いつつあることでした。

これらの技術を積極的に取り入れることで、デリッシュキッチンサービスの更なる発展に貢献していきたいと思います。

5/21開催 2回目 エンジニアの働き方・キャリア

開発1部 デリッシュキッチンAWG PUの岩﨑です。 私からはエンジニアの働き方とキャリアについて紹介します。

この研修は、大きく2つのテーマで構成されていました。

登壇者 所属 役職 タイトル
島津 真人 株式会社Progate CTO AI時代の新卒エンジニアに必要な変化と学習
松本 勇気 株式会社LayerX CTO キャリアの考え方、フォロワーシップ

AI時代の新卒エンジニアに必要な変化と学習

まず株式会社Progate CTO 島津さんのセッションでは、AI時代における新卒エンジニアの変化と学習についてお話しいただきました。

ソフトウェアプロダクト開発の変化

従来のソフトウェアプロダクト開発は、「企画→ 設計→ 実装→ 評価」というフローで進み、プログラミングはその中の道具の1つでした。特に「ジュニアエンジニア(=新卒)」は、先輩から与えられたタスクに基づいて実装をこなすことが、2024年頃までは一般的でした。

しかし2025年現在ではこの状況は大きく変化しており、Devin、Cursor、Claude Codeといった生成AIツールが登場したことで簡単なタスクであればAIが実装できるようになっています。

これにより「簡単な仕事はどんどん捌けて、なんらかの理由で難しい仕事がどんどん残る」という状況が発生し、結果として新卒エンジニアのオンボーディングに適した「簡単なタスク」が減少するという課題が生まれています。

生成AIと仕事をしていく上で考えること

ここで生成AIが進化するたびに話題としてあげられる「生成AIがいれば人間は代替されてしまうのか?」という問いに対し、島津さんは明確にNOと答えています。

これまでの仕事の一部がAIによって代替されたとしても、次に人間がやるべきことが必ず出てくるとおっしゃっていました。

こういった時代の流れにおけるエンジニアの心構えについて、島津さんより何点かお伝えいただいた中で個人的に最も印象に残ったお話を紹介します。

AI時代のエンジニアの心構え

現状AIはまさに過渡期であり状況は常に変化しているため、それに追いつき、やり方を更新し続ける必要があります。

これは従来も求められていましたが、変化の大きさが格段に増している点が新しい部分です。

そして世の中でAIの活用法に答えが出ていないため、以下のサイクルを継続的に回して知見をアップデートしていく必要があります。

  1. 局所最適を無限に積み重ねる:自分自身でたくさん試行錯誤を繰り返すこと。
  2. 知見を共有し、アップデートする:得られた知見をチームや組織、コミュニティーに還元すること。
  3. 技術の進歩に合わせて1と2を繰り返す

新しい技術が出てきたらとにかく試す。

それをまずは個人で試行錯誤してチームや組織内でブラッシュアップするサイクルが回るようになれば、弊社が掲げるAIで開発スピードを10倍にすることも実現できるのではないかと感じました。

キャリアの考え方、フォロワーシップ

株式会社LayerX CTO 松本さんのセッションでは、キャリアの考え方とフォロワーシップについてお話しいただきました。

キャリアの考え方

松本さんはキャリアを後悔しないためのポイントとして以下の3つをあげています。

  1. 投資家的思考
  2. コミュニティ
  3. 最初の10年間の使い方

それぞれ首が取れるほど頷ける内容なのですべて紹介したいところですが、ここでは投資家的思考を取り上げます。

投資家的にキャリアを考える

ここではキャリアを資産としてとらえ、自身のキャリアを単なる仕事の連続ではなく、人生という資産を最大化するための「投資活動」と捉えます。

「資産」とはお金だけでなく、信用・信頼、健康・体力、名声、知識・経験、時間など多様な要素が含まれます。

投資家的思考とは、私たち一人ひとりが自身の「資産」を守り、その資産を使って自分の仮説・学びたい方向に向けて効率よく投資することでより大きな資産を獲得していく「投資家」である、という考え方です。

リスクとリターン

このような考え方をする上で、どんな意思決定にもリスクとリターンが存在することを意識する必要があります。

自分が目的とするリターンを得るために、手元にどのような選択肢があり、どのようなリスクがあるのかを整理することで、最適な選択をすることができます。

一般的にリスクとリターンは比例しますが、知識量を増やすことで同じリターンをより小さなリスクで得ることが可能になります。

探索と学び

そしてその知識を増やし、不確実性を低下させるためには継続的な学習サイクルが不可欠です。

「仮説立て→ 行動→ 振り返り→ 学習・知識化」のサイクルを回すことで、より精度の高い状況理解と不確実性の低下につながります。

バランスシートとポートフォリオ

とはいえ、やりたいことすべてに投資することはできません。

自身の持つ投資可能な資産(お金、時間、体力、信用など)をどのように配分するかといったバランスシートを作成し、余分な資産(特に時間)に対してどこにどれだけ使うかを決めることで、キャリアの方向性が明確になります。

レバレッジ

そして時にはレバレッジをかけ、リスクをとってより大きく投資をすることも必要です。

手元の資産を特に知識や経験といったストックされる方向へ投資し、運用効率が高まる手段を追いかけることでより大きなリターンを得ることができます。

例として、以下のような項目が挙げられていました。

  • お金で時間や知識・経験を買う(家事のアウトソースや有料ツールの活用)
  • チームで動く(人にレバレッジをかける)
  • 信用のレバレッジ(スキルが不足していても信頼があれば任せてもらえる)

こういったレバレッジをかけることで、キャリアのスピードを加速させることができます。

しかしリスクを取りすぎると失敗する可能性もあるため、自身の取りたいリスク度合いに応じて調整が必要です。

まとめ

AI時代のキャリアを考える上での指針となるお話を島津さんから聞くことができて本当によかったなと思っています。

また松本さんに発表いただいた資料は以前より拝見しており、こちらも私自身のキャリア指針に大きな影響を与えてくれているので今回の研修で直接お話を伺うことができてとても嬉しかったです。

AI時代の波に乗り遅れないようなキャリア戦略を考え続けたいと思います!

5/27・28開催 3回目 AWS JumpStart

開発1部 デリッシュキッチンMS DRMの江﨑と開発1部デリッシュキッチンAWGヘルシカの赤川です!私たちからは、AWS JumpStartについて紹介します!

AWS JumpStart 2025は、AWS初学者のエンジニアを対象とした実践的な研修プログラムです。 このプログラムのゴールは、一般的なアーキテクチャの理解、AWSコアサービスの概要とその選定基準の把握、そしてAWSアーキテクチャ図作成の流れを学ぶことです。

タイムスケジュール

2日間のスケジュールは以下の通りです。1日目は座学とAWS環境の構築を行うハンズオン、2日目は1日目の内容を踏まえたより実践的なアーキテクチャ検討を行いました。

1日目

まず1日目の内容について紹介します。

講義

講義では「アーキテクティングのコツ」について解説いただきました。Webサービス構築におけるフェーズごとのAWSサービス選定の基礎や、システムスケール時の課題と対策について学びました。 講義の内容を簡潔にまとめると以下のようになります。

フェーズ1: プロトタイプ(〜100人)

構成: Route 53 → EC2 → RDS

特徴: シンプル・安価、可用性は重視しない

フェーズ2: 一般公開(100人〜10,000人)

構成: Route 53 → ALB → EC2(マルチAZ) → RDS(マルチAZ)

改善点: - Application Load Balancerによる負荷分散 - 複数AZでの冗長化 - Auto Scalingの導入

フェーズ3: 大規模化(10,000人〜1,000,000人)

構成: CloudFront → ALB → Auto Scaling Group → Aurora + リードレプリカ + ElastiCache

改善点: - CloudFront: 静的コンテンツのCDN配信 - Aurora: 高性能DB、リードレプリカで読み取り負荷分散 - ElastiCache: インメモリキャッシュでDB負荷軽減

フェーズ4: 超大規模(1,000,000人以上)

特徴: 計測・分析・改善サイクルの重視

改善点: - アプリケーション最適化 - DBシャーディング、NoSQL活用 - マイクロサービス化 - Infrastructure as Code、CI/CD導入

システム構築時は、最初から過度に作り込まず、要件を満たすシンプルな設計から始めること、そして計測・分析・改善のサイクルを継続することが重要であると学びました。

ハンズオン

ハンズオンでは、実際にAWS環境の構築を体験しました。2〜3人のチームで、AWS環境を操作するドライバーと、手順書を確認しながら指示を出すナビゲーターをローテーションしながら進める、モブプログラミング形式で実施されました。

前半のハンズオンでは、以下のようなシンプルなアーキテクチャを構築しました。実際に手を動かすことで、AWS環境構築の理解が深まりました。

後半のハンズオンでは、より実践的なアーキテクチャを構築しました。ALBによる負荷分散や、DBのフェイルオーバー機能による可用性の担保など、前半よりも高度な構成となっています。 また、アーキテクチャを構築して終わりではなく、ECSタスクを停止させた際の挙動なども確認し、実際に障害が発生した場合にどのように可用性が保たれるかを体験できました。

まとめ

AWS JumpStart 1日目を通して、単なるAWSサービスの学習にとどまらず、「なぜこのサービスを選定するのか」「どのようにアーキテクチャを設計すべきか」といったアーキテクティングの思考プロセスの基礎を学ぶことができました。 また、モブプログラミング形式で学ぶことで、チーム内で活発に意見交換し、教え合うことで理解をより深めることができました。ここで学んだことを土台に、今後の業務でも要件に合ったアーキテクチャを考えていきたいと思います。

2日目

2日目はより実践的な内容で、与えられたお題に沿ったアーキテクチャを設計する課題に取り組みました。

アーキテクチャの検討(個人)

まずは個人でアーキテクチャを検討する時間が設けられました。与えられたお題に対して、以下のような構成を考えました。

  • マルチAZ構成で冗長化
  • フロントエンド:React(TypeScript)プロジェクトをECS Fargateで実行、CDNのためにCloudFrontを使用、静的コンテンツはS3から配信
  • バックエンド:Java/Spring BootアプリケーションをECS Fargateで実行、時間に応じてオートスケーリング
  • データベース:Aurora、レプリカも追加
  • ロードバランサー:Application Load Balancer(ALB)
  • ログ:CloudWatchとKinesis経由で、S3に保存
  • DNS:Route 53

1日目で学んだ内容をもとに、基本的なWebサービスのアーキテクチャに必要なことは網羅することができたと思います!

アーキテクチャの検討(グループ)

まず、メンバーそれぞれが考えたアーキテクチャを共有し、特徴と改善すべき点を共有していきました。 その後各アーキテクチャの良いところを集めて、以下の要素が追加されました。 - キャッシュ:ElastiCache - キュー:SQS - 外部との接続:NAT Gateway - 認証:Cognito

これで、我々のアーキテクチャ図は完成しました! NAT Gatewayを経由した外部APIとの連携を丁寧に描けたのが良かったと思っています!

発表会

発表では3チームが選ばれ、それぞれのアーキテクチャについて説明をしていました。 私のチームは選ばれませんでしたが、弊社メンバーのいるチームが選ばれたのでそのアーキテクチャを載せておきます!

個人的には、普段の定例会議でダッシュボードを見る機会が多いので、ダッシュボード作成の機構があるのが良いなと思いました!

懇親会

夜の懇親会では、はじめに24新卒の先輩からAWSに関するLTが行われました。 その中で、シナジーマーケティング株式会社の木山さんからAWSのコスト管理に関するお話をしていただきました。 この話を聞いて、早速個人開発のプロジェクトにコストアラートを設定しておきました!

その後は、一緒にアーキテクチャを作ったチームメンバーや他の会社の人たちとたくさん交流することができました! その時に知り合ったメンバーと二次会に行ったり、輪読会を始めたりと、同期の繋がりの良さを改めて実感しました。

まとめ

この研修を通じて、実際のアーキテクト業務を体験することができました。 実際のビジネス要件を技術的にどう実現するかなどを深く考えるのはとても楽しかったです。 さらに面白かったのは、完成したアーキテクチャがグループによって全く違ったことです。 中にはサーバーを立てずに、全てをLambdaでサーバーレスに処理するという挑戦的なアーキテクチャもありました。

今後は、「使ったことある」AWSサービスを増やしていきたいです! 実務ではもちろん、個人開発でも色々なAWSサービスを試してみたいと思います!

おわりに

ここまで、2025年新卒合同研修の前半(第1回〜第3回)についてご紹介しました。 後半では、第4回から第6回の研修内容についてお伝えします。 ぜひ、後半の記事もご覧ください!

tech.every.tv

WAF でセキュリティを強化してチームの QOL を向上させる

WAF でセキュリティを強化してチームの QOL を向上させる

目次

はじめに

こんにちは。 開発本部開発1部トモニテ開発部所属の庄司(@ktanonymous)です。

先日、トモニテで WAF (Web Application Firewall) を導入しました。 WAF の導入により、これまで以上に安心感を持ってサービス運用に向き合えるようになったと感じています。
本記事では、WAF 導入の背景から、実際に調査・検討した内容、そして導入後の運用についてまとめていきます。

WAF 導入の背景

サービス運営をしている中で、攻撃を受けるというのはよくあることだと思います。 トモニテでも攻撃と思われるアクセスを検知することがありますが、直近ではそのようなアクセスが過去に比べても多い状況が続いていました。
以前には、トモニテで SQL インジェクション攻撃を受けたことに関するブログも公開していますので、そちらもご覧ください。

SQL インジェクションのような攻撃に対しては、アプリケーション側のバリデーションの徹底などによる対応をしていましたが、 DoS 攻撃のように大量のリクエストを送りつけることでシステム負荷を高めるような攻撃に対しては、 社内 Slack でアラートが発報され、都度状況確認するという対応をしていました。
システムの過負荷を検知した時にアラートを発報する監視体制が取れているのは良いのですが、平日・休日・昼夜問わず対応を迫られてしまう状況は、対応負荷も高く健全ではないと感じていました。
そこで、大量アクセスによる過負荷が続いたことも踏まえ、WAF の導入を決定しました。
(なお、トモニテではインフラに AWS を利用しているため、AWS WAF を導入しました。)

WAF 導入にあたり調査・検討したこと

WAF を導入するにあたっては、運用・金額コストをできるだけ抑えるミニマム構成を前提として、以下の観点で調査・検討を進めました。

ログの運用

WAF を導入する上で、最小コストで運用できるログは何かを考えました。
結論から述べてしまうと、追加コストなく設定できるサンプルリクエストおよび CloudWatch Metrics を利用することにしました。 S3 にログを保存することも考えましたが、そのためには Kinesis Data Firehose を利用する必要があるため、 シンプルに導入できる構成を採用することにしました。 これらは、Terraform で WAF のルールのリソースを作成する際に以下のように visibility_config というフィールドを定義するだけで設定できます。

visibility_config {
  cloudwatch_metrics_enabled = true
  metric_name = "metric_name"
  sampled_requests_enabled = true
}

WAF のサンプルリクエストでは、実際のリクエストの一部がランダムにピックアップされます。 あらゆるリクエストのサンプリングだけでは WAF のルールで検知されたリクエストを確認するのは難しいですが、サンプルリクエストはメトリクス別にフィルタリングして確認することができるので、WAF がどのようなリクエストを検知したのかを十分に確認することができます。

WAF のサンプルリクエスト
WAF のサンプルリクエスト

Sampled requests の各項目について

  • Metric name
    • visibility_config で設定した metric_name
  • Source IP
    • 該当のルールで検知されたリクエストの送信元 IP アドレス
  • URI
    • 該当のルールで検知されたリクエストの URI
  • Rule Inside rule group
    • 該当のルールが所属するルールグループ
  • Action
    • 該当のルールで検知されたリクエストに対して実行されたアクション
  • Time
    • 該当のルールで検知されたリクエストの送信時刻

Slack 通知の実現

WAF で攻撃と思われるリクエストを検知した時および攻撃が止んだ時に Slack へ通知することで 即座に状況を把握できるようにもしたいと考えました。
そこで、追加のコストを掛けずに通知システムを実現するため、以下のような構成を採用しました。

WAF アラームの通知システム
WAF アラームの通知システム

この構成では、WAF のルールの検知状況を CloudWatch Alarm で監視します。 今回の監視対象は、レートベースによる IP アドレスのブロックルールになります。 WAF によってブロックされたということは攻撃と判断されていることになるため、アラームの閾値は 1 件としています。
これにより、攻撃発生時に Slack へ通知されるようになります。 ただし、これだけでは平時のアラームの状態が「データ不足」と認識されてしまうので、 0 件の状態を「正常」であると認識させるために、noBreaching の設定をしておきます1。 この設定を追加しておくことで、攻撃が止みブロックルールの検知数が 0 件に戻った時に、 アラームが「正常」状態に戻り、攻撃が止まったことを Slack へ通知することができるようになります。
(アラームは状態変化によるものなので複数の攻撃を通知することはできませんが、 全ての攻撃が終わったタイミングで「正常」に遷移するため、状況把握という観点では十分だと考えています。)

最終的には、以下のような Terraform コードを実装することで実現しました。

resource "aws_cloudwatch_metric_alarm" "resource_name" {
  alarm_name          = "alarm_name"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = "BlockedRequests"
  namespace           = "AWS/WAFV2"
  period              = 300
  statistic           = "Sum"
  threshold           = 1
  alarm_description   = "過剰なリクエストのIPアドレスをブロックしました"
  alarm_actions       = [
    var.sns_topic_arn,
  ]
  ok_actions          = [
    var.sns_topic_arn,
  ]
  treat_missing_data = "notBreaching"  # 0 件の状態を「正常」であると認識させるための設定
  dimensions          = {
    WebACL = var.web_acl_name,
    Rule   = var.rule_name,
    Region = var.region,
  }
}

WAF 導入時の考慮点

WAF を導入する際、攻撃を防ぐために大量リクエストをブロックしたくなりますが、 いきなりブロックするルールを設定してしまうと、ブロックしてはいけないリクエストまでブロックされてしまう可能性があります。 そのため、まずはカウントのルールを設定してから様子を見てからブロックのルールに変更しました。

また、実際にリソースを作成する前に、開発環境で動作確認を行いました。 作成する予定のリソースと同じ構成のものを開発環境に作成し、 簡単なスクリプトで大量リクエストが発生する状況を再現することで、WAF が意図通りに動作するかを確認しました。

おわりに

今回の記事では、トモニテにおける WAF 導入の背景から、調査・検討内容、実際の運用までを紹介しました。

今回の対応のおかげで、安心感を高めることはできたかと思います。 WAF を導入しただけでセキュリティが完璧になるわけではありませんが、 今後もサービスの安全性向上に向けて、アプリケーション・インフラ両面からセキュリティ対策をおろそかにしないようにしていきたいと思います。

今回の記事が、少しでも皆さんのお役に立てれば幸いです。 最後まで読んでいただき、ありがとうございました。