TECH PLAY

SCSKクラウドソリューション

SCSKクラウドソリューション の技術ブログ

1141

皆さんはじめまして!SCSKのタカギです。普段はAzureの専門部隊でエンジニアをしています。 先日発表されたゾーン冗長(プレビュー提供)について、ポイントをまとめます。 なお、2026年1月8日時点の情報に基づきます。 参考: NAT ゲートウェイと可用性ゾーン – Azure NAT Gateway | Microsoft Learn   Azure NAT Gatewayとは Azure NAT Gatewayは、フルマネージドのネットワーク アドレス変換サービスです。 簡単に言うと、Azureのプライベート ネットワークからのインターネット アウトバウンドを可能にするサービスです。 参考: Azure NAT Gateway とは | Microsoft Learn   Azure NAT Gatewayの構成例 Azureのサービスでゾーン冗長を構成するには、そのリソースのデプロイの種類が「ゾーン冗長リソース」か「ゾーン固有リソース」かを把握する必要があります。 それを踏まえて、構成例で使用するサービスのデプロイの種類は以下です。※デプロイの種類に関する細かい説明は機会があれば。 # サービス名 デプロイの種類 1 Azure NAT Gateway ゾーン固有 2 Virtual Machines(VM) ゾーン固有 3 Virtual Network(VNet) ゾーン冗長 4 Subnet ゾーン冗長   上記を踏まえ、Azure NAT Gatewayの構成例を以下に示します。 (図1)基本構成(VM 3台の例)   これまでのゾーン冗長構成 これまで、Azure NAT Gatewayでゾーン冗長構成を取るには、以下のようなやや複雑な構成が必要でした。 (図2)従来のゾーン冗長構成(NAT Gatewayをゾーンごとに用意) ※コストや要件によっては(例:VMが2台構成など)、Zone1と2のみでも可用性向上が見込めます ポイントは、Azure NAT Gatewayをゾーンごとに用意し、それぞれに対応するサブネットを分けていることです。 というのも、 Azure NAT Gatewayはゾーン固有 、かつ 1つのサブネットに紐づけられるAzure NAT Gatewayは1つだけ という制約があったからです。 絵で描くだけなら簡単なのですが、ルーティングやNSGなど、設計の検討事項が増えるのも悩ましいところです。   StandardV2 NAT Gatewayのゾーン冗長構成例 今回プレビューとして公開された「StandardV2 NAT Gateway」を使えば、以下のような構成を取ることができます。 (図3)StandardV2を用いたゾーン冗長構成 どうでしょうか。かなりシンプルになったと思いませんか? 可用性がネックになり、Azure NAT Gatewayの利用を見送っていた人にとっては朗報ではないでしょうか。   まとめ Azure NAT Gatewayがゾーン冗長構成になることで、Azureインフラがシンプルになることが伝わったかと思います。 まだプレビュー段階のため、GA後の費用や制約は確認が必要ですが、エンタープライズ環境におけるインターネット アウトバウンド構成の有力候補に近づいたと言えそうです。 今後のアップデートが楽しみですね。ここまで読んでいただきありがとうございました。
アバター
SCSK いわいです。 前回はRaspberry Pi 5で気温/気圧/湿度センサーを使って測定し、 Webで表示、DBに取得データを検索するシステムを構築しました。 今回は測定したデータからAIを使って気温/気圧/湿度をリアルタイム予測してみます。 今回は前回セットアップした環境をそのまま流用します。 Raspberry Piで気温/気圧/湿度計測 結果をWebサーバで見てみよう Raspberry Pi 5で気温/気圧/湿度センサーを使って測定し、Webで表示するシステムを構築したいと思います。DBに取得データを格納し、あとから検索できるといろいろ便利です。 blog.usize-tech.com 2025.12.08   過去データから現在値を予測す る 過去データから現在値を予測します。これには機械学習結果からの推論(教師あり学習)を使用します。 温度/湿度/気圧の予測のために「線形回帰」「非線形回帰」「LSTM(Long Short Term Memory)」の3つを試してみます。 「線形回帰」はデータの関係性が直線(線形)の場合で表せる場合に使われます。 ⇒グラフにした時にだいたいまっすぐな線で表せる 例えば商品の売上と広告費用などが該当します。 「非線形回帰」は「データの関係性が曲線(非線形)」で表せる場合に使われます。 ⇒グラフにした時に曲がった線で表せる 例えば投げたボールの高さと時間の関係が該当します。 「LSTM」は時系列データや文章など、時間の流れや順番が大事なデータをうまく使える機械学習のモデルです。 ⇒過去と今の情報を組み合わせて考えられる仕組み 例えば株価の予測、文章の意味理解が該当します。 普通の再帰型ニューラルネットワークは「昔のこと」をすぐ忘れてしまいますが、LSTMは 「長い・短い記憶をうまく使い分けできる」ので、長い文章や長期的な傾向も扱えるようです。 なんだか今回のテーマに合致しそうな気配です。 ざっくりまとめると以下になります。 方式 得意なデータ 値の関係 過去の情報との関係 例 線形回帰 数値 & シンプル 直線 考慮しない 商品の売上と広告費用 非線形回帰 (RF予測) 数値 & 複雑 曲線 考慮しない 投げたボールの高さと経過時間 LSTM 時系列・文章・音声等 複雑 + 順番 重視する 株価予測、文書生成 これらの3つの方式を実装してどの予測値が実測値に近いか確認してみます。   システムのイメージ 前回作成したFlaskアプリケーションに機能を追加します。今回はWeb画面に測定結果と予測値を表示します。 蓄積した測定結果から予測モデルを作成して、予測モデルを使ってリアルタイムで現在の温度/湿度/気圧を予測してみます。 イメージはこんなカンジで。 今回のシステムで導入する機能と各ライブラリの説明は以下のとおりです。 機能 ライブラリ 説明 Webサーバ Flask 軽量なWebフレームワーク。センサー値や予測結果をWebアプリとしてブラウザに表示。 センサー通信 Smbus2 ラズパイとI2C通信する。BME280と通信するために利用。   bme280 Bosch製の温湿度・気圧センサー BME280用のPythonライブラリ。データ取得する。 データ保存 sqlite3 軽量な組み込み型データベースSQLiteを操作するためのライブラリ。計測データをローカルDBに保存・検索するために利用。 時刻処理 datetime 計測時刻の記録に利用。ローカルDBに保存するtimestampを生成。 ファイル管理 (new) os OSレベルの操作。LSTMモデル/Scalerファイル(ディープラーニング結果ファイル)の存在確認に利用。   joblib Pythonオブジェクトを高速に保存・読み込みするためのライブラリ。学習済みScalerを保存・読み込みするために利用。 数値処理 (new) numpy 数値計算ライブラリ。線形回帰やLSTMに渡すデータを配列に整形するために利用。 機械学習 (new) scikit-learn 線形回帰、非線形回帰をつかった予測のために利用。   tensorflow LSTM予測のために利用。 前回導入済みのライブラリに加え、ファイル管理用ライブラリ(joblib)、数値処理ライブラリ(numpy)、機械学習用ライブラリ(scikit-learn、tensorflow)を追加します。ファイル管理用ライブラリであるosはデフォルトでインストールされています。   過去のデータからLSTMモデルを作成する LSTMモデルを作成するために高速演算用ライブラリのnumpy、機械学習ライブラリのscikit-learnとtensorflow、ファイル生成用ライブラリjoblibをインストールします。 sudo pip install numpy –break-system-packages  sudo pip install tensorflow –break-system-packages sudo pip install scikit-learn –break-system-packages sudo pip install joblib –break-system-packages ローカルに測定結果を蓄積しているDBファイルを元にLSTMモデルとScalerファイルを生成します。 Scalerファイルとは学習時のデータの最大値/最小値、標準偏差等を求めて、それぞれのデータをを0~1の数値に 置き換えるための定義ファイルとのこと。 この定義ファイルがないとそもそもどんな情報を元に学習した結果なのかわからず、 予測もできないため、学習時と予測時には同じ定義ファイルを使う必要があります。 import sqlite3 import numpy as np import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from sklearn.preprocessing import MinMaxScaler import joblib # スケーラー保存用 # ====== 設定 ====== DB_FILE = "bme280_data.db" TREND_WINDOW = 10 # LSTM の timesteps MODEL_FILE = "bme280_lstm_model.keras" SCALER_FILE = "bme280_scaler.save" # ====== SQLite からデータ取得 ====== def load_data(): with sqlite3.connect(DB_FILE) as conn: c = conn.cursor() c.execute(""" SELECT temperature, humidity, pressure FROM measurements ORDER BY timestamp ASC """) rows = c.fetchall() data = np.array(rows, dtype=np.float32) return data # shape = (n_samples, 3) # ====== LSTM 用系列データ作成 ====== def create_sequences(data, window_size): X, y = [], [] for i in range(len(data) - window_size): X.append(data[i:i + window_size]) y.append(data[i + window_size]) return np.array(X), np.array(y) # ====== メイン処理 ====== def main(): # --- データロード --- data = load_data() if len(data) <= TREND_WINDOW: raise ValueError("データ数が TREND_WINDOW 以下です") # --- 正規化 --- scaler = MinMaxScaler() data_scaled = scaler.fit_transform(data) # --- 系列化 --- X, y = create_sequences(data_scaled, TREND_WINDOW) print("X shape:", X.shape) # (samples, timesteps, features) print("y shape:", y.shape) # (samples, features) # --- LSTM モデル --- model = Sequential([ LSTM(32, input_shape=(X.shape[1], X.shape[2])), Dense(X.shape[2]) # temperature, humidity, pressure ]) model.compile( optimizer="adam", loss="mse" ) model.summary() # --- 学習 --- model.fit( X, y, epochs=100, batch_size=16, verbose=1 ) # --- 保存(.keras 形式) --- model.save(MODEL_FILE) joblib.dump(scaler, SCALER_FILE) print("✅ モデル保存:", MODEL_FILE) print("✅ スケーラー保存:", SCALER_FILE) # ====== 実行 ====== if __name__ == "__main__": main() 予測用に直近10件のデータを測定し、学習結果から次の1件のデータを予測するLSTMモデルを作成しています。 これで温度/湿度/気圧予測の準備ができました。   Pythonスクリプト作成/実行 今回もChatGPTを利用してPythonスクリプトを作りました。 前回の構成に過去のデータから現在の気温/湿度/気圧を線形予測、RF予測、LSTM予測した結果を 表示する機能を追加しています。 線形予測は直近5000件のデータから現在の各値を予測、RF予測は過去の測定値からランダムな特徴やデータの一部を選定、 50パターンの決定木 = forestを生成して、その平均値から各値を予測するように設計しています。 Raspberry PiでWebサーバを起動します。   実行結果 上から「実測値」、「線形予測値」、「RF予測値」、「LSTM予測値」を表示しています。 線形予測はだいぶ外れた値、RF予測は実測値にかなり近い値、LSTM予測は若干ずれた値となりました。 現実では温度/湿度/気温には以下の傾向があります。 温度:平坦⇒微増/微減⇒平坦 湿度:ジグザグ 気圧:ほぼ一定+揺れ この現象に対してそれぞれのリアルタイム予測はざっくりと以下のように動きます。 線形予測:微増/微減したら次も微増/微減するはず ⇒そもそも現実と合致してないが傾向はわかる RF予測:大体前と同じぐらいの値では? ⇒ほぼ正解 LSTM予測:過去の値からみてちょっと変えたほうがそれっぽい? ⇒賢すぎてノイズが発生することもあるがクセは覚えられる 今回のケースではそれぞれの予測は得意な分野があることがわかりました。 線形予測は「傾向予測、急激な変化を検出する」、RF予測は「リアルタイム予測、直近予測をする」、 LSTM予測は「周期的な予測、1時間後の予測をする」が得意なようです。 勉強になりました。
アバター
こんにちは。SCSKの井上です。 New Relicで柔軟にデータを分析したいけれど、分析方法に迷う方も多いのではないでしょうか。本記事では、New Relicで収集したデータの分析手法を解説します。 はじめに New Relicでデータを収集後、データを分析しなければ価値を最大限に発揮ができません。New Relicには NRQL(New Relic Query Language:通称ヌルクル)と呼ばれるデータ検索するためのNew Relic独自の言語 があります。「独自言語を学ぶのは大変そう…」と思うかもしれませんが、SQLの知識がある方ならすぐに理解できます。SQLを知らない方でも、基本構造を押さえれば柔軟な分析が可能です。本記事では、データ分析の可能性を広げるため、NRQLの基本概念と活用方法を解説していきます。個々のNRQL文については別の記事にて紹介します。   NRQLでできること NRQLは、New Relicに送信したデータを検索・分析するためのクエリ言語です。NRQLを使うことで、以下のような分析ができます。 パフォーマンス分析 トランザクションの平均レスポンスタイムや最大値、分布を確認し、ボトルネックを特定 エラーモニタリング エラー率やエラーメッセージの頻度を集計し、異常発生の兆候を把握 リソース使用状況の把握 CPU、メモリ、ディスクの使用率やトラフィックを監視し、キャパシティ計画やスケーリングの判断材料とする ビジネス指標の分析 国別のアクセス数やアクセス時間帯などのビジネスデータを集計   NRQLを始めましょう:データの言語 | New Relic Documentation Learn how to query your New Relic data with NRQL, our SQL-like query language. docs.newrelic.com   NRQLの基本構造 NRQLは、テレメトリデータを分析するためのクエリ言語です。SELECTで取得する値を決め、FROMで対象イベントを指定し、WHEREで条件を絞ります。オプションとしてFACETやTIMESERIESでグループ化や時系列分析をすることができます。どんな文法ルールがあり、調べ方があるのかを解説していきます。 NRQLの文法ルール NRQLを書く際には、いくつかの基本ルール(お作法)があります。これらを理解しておくことで、NRQLが正しく動作し、効率的にデータ分析ができます。まずは、NRQLのお作法を確認してみます。 ルール項目 内容 必須句 SELECT と FROM は必須。他の句(WHERE、FACET、LIMITなど)はオプション。 クエリ開始位置 クエリは SELECT または FROM からクエリ文の開始可能。一部SHOWコマンドも使用可。 クエリ文字列のサイズ 最大 4KB未満。超えるとエラー表示 (New Relicでは、クエリが長すぎると処理できない)。 大文字・小文字の扱い イベントタイプ名と属性名は 区別される。NRQL句や関数は 区別されない。 イベントタイプ名:FROMで指定する対象データ。属性名:FROMで指定した個々のデータ。 文字列の指定方法 シングルクォート ‘ ‘ を使用 カスタムイベント・属性名 英数字、コロン(:)、アンダースコア(_)、ピリオド(.)を使用可能。左記以外でスペースや特殊文字が含まれる場合は バッククォート(``)で囲む 。 データ型の型変換 型強制はサポートされない。 データは 収集時の型のまま扱う必要 。 文字列を数値に変換したり、数値を文字列に変換することは不可。   NRQLを始めましょう:データの言語 | New Relic Documentation Learn how to query your New Relic data with NRQL, our SQL-like query language. docs.newrelic.com   NRQLの基本構文 NRQLの基本構文は、FROM に対象のイベントタイプまたは Metric を記述します。SELECT にイベントタイプ内の属性や関数、またはメトリクス名+関数を指定します。 FROM は「どのデータ(イベントタイプまたはメトリクス)から取り出すか」、SELECT は「どの値を取り出すか」を決めます。 例えば、イベントタイプ Transaction には appName や duration などの属性があります。FROM Transaction SELECT duration と記述するとトランザクションの処理時間を返します。メトリクスの場合は時間の経過とともに変化する数値を分析できます。 FROM Metric SELECT average(aws.ec2.CPUUtilization) と記述するとAWS EC2のCPU使用率を集計します。 メトリクスの場合の構文はFROM Metricとして固定で使用します 。いずれもSELECTから始めるかFROMから始めるか指定はありません。 NRQLで時間指定して分析したい場合、UTC時刻で書く必要があります。 SINCE ‘2026-01-01 00:00:00’ UNTIL ‘2026-01-01 12:00:00’とした場合は、JSTでは2026-01-01 09:00~21:00に相当します。-9時間を意識して書く必要があります。   NRQLの句 NRQLでは、クエリを書くときに「どのデータを対象にするか」「どんな条件で絞り込むか」「どのような形式で結果を返すか」を指定します。この役割を担うのが「句」です。代表的な句は次のとおりです。 句 説明 使用例 WHERE 条件を指定してデータを絞り込み WHERE appName = ‘MyApp’ FACET 属性ごとにグループ化(SQLのGROUP BYに相当) FACET appName LIMIT 返す結果の件数を制御 LIMIT 100 SINCE / UNTIL 時間範囲を指定 SINCE 1 day ago UNTIL now TIMESERIES 時系列データを返す(グラフ化に利用) TIMESERIES 1 minute COMPARE WITH 過去の期間と比較 COMPARE WITH 1 week ago WITH TIMEZONE タイムゾーンを指定。SINCE/UNTILの固定時間には使えない。データを時間単位でグループ化して集計する際に使用。 WITH TIMEZONE ‘Asia/Tokyo’ AS 別名を付ける SELECT count(*) AS ‘CPU使用率’   NRQLの関数 取得したデータを「どのように集計・計算するか」を指定するのが「関数」です。関数を使うことで、件数の集計、平均値の算出、最大値や最小値の取得など、データを分析するための処理ができます。代表的な関数は次のとおりです。 関数 説明 使用例 count() 件数を数える SELECT count(*) FROM Transaction average() 平均値を計算 SELECT average(duration) max() 最大値を取得 SELECT max(duration) min() 最小値を取得 SELECT min(duration) sum() 合計値を計算 SELECT sum(duration) percentage() 割合を計算 SELECT percentage(count(*), WHERE error IS TRUE) rate() 単位時間あたりの発生率を計算 SELECT rate(count(*), 1 minute) latest() 最新の値を取得 SELECT latest(duration) percentile() パーセンタイル値を取得 SELECT percentile(duration, 95)   NRQLリファレンス | New Relic Documentation A detailed reference list of clauses and functions in NRQL, the New Relic query language. docs.newrelic.com   NRQLのデータタイプ アカウントごとに有効化している機能が異なるため、一概にどのデータタイプが使えるか言えません。例えばInfrastructureエージェントを導入している場合は、Systemsampleが使えます。APMエージェントを利用していない場合は、Transactionなどを利用することができません。対象のアカウントでどのデータタイプを使用できるかは以下のSHOW EVENT TYPESコマンドを実行することで確認できます。または、データエクスプローラを使用して検索することができます。   New Relic でテレメトリ データを最適化する | New Relic Documentation Our data ingest governance guide helps you get optimal value for the telemetry data you're reporting to New Relic. docs.newrelic.com   NRQLのあいまい検索 NRQLのあいまい検索は以下があります。NRQLでは、完全一致だけでなく、部分一致やパターンマッチングを行うための演算子が用意されています。これらを利用することで、ログやイベントデータから特定のキーワードやパターンを柔軟に抽出できます。 演算子 ワイルドカード 意味 例 LIKE % 任意の文字列(0文字以上) appName LIKE ‘MyApp%’ → MyAppで始まる名前   データを探して、絞り込んで、理解するために NRQLのクエリはNew Relicのプラットフォーム上で実行されます。ここでは、どのようなデータがどのような形式で格納されているのか、クエリを実行したい場合にどうやって操作したらよいのかを解説してきます。検索対象はすでに収集されたデータに対して行われるため、負荷の高いクエリを実行しても、監視しているホストやサービスには影響しません。ただし、New Relicのプラットフォーム側には負荷がかかるため、複雑なクエリや大量のデータを扱う場合は、実行制限や実行時間が長くなる可能性があります。 New Relicデータ辞書 New Relicで扱うデータの構造や属性を理解するためのリファレンスです。NRQLを記述する際、どんなデータが格納されているかを把握していないとクエリを書くことはできません。NRQLの基本構造は『イベントタイプ(Event Type)と、その中に含まれる属性(Attributes)』という関係になっています。項目数が多いのでよく目にする項目について以下の表にまとめました。詳細は公式サイトをご確認ください。 データソース名 代表的なイベントタイプ(データ種類) 主な属性例(データ項目) アカウント関連 NrUsage productLine, usageAmount アラート NrAiIncident conditionName, policyName APM Transaction appName, duration, error.message ブラウザエージェント PageView appName, duration, countryCode 分散トレーシング Span traceId, duration, service.name インフラ SystemSample cpuPercent Kubernetes K8sContainerSample cpuUsedCores メトリック Metric metricName 外形監視 SyntheticCheck monitorName, result   New Relic data dictionary | New Relic Documentation New Relic data dictionary docs.newrelic.com   クエリビルダー(Query Builder) NRQLを実行するための画面のひとつにクエリビルダーがあります。複雑なNRQLを手動で書かなくても、 GUIを使って自動補完機能でクエリを作成 できます。また、クエリ結果をグラフ化してダッシュボードに追加したり、その結果を基にアラート条件を設定することも可能です。過去に実行したクエリ(検索や処理の内容)は履歴として保存されているので、再度同じクエリを実行したい場合でも、履歴から呼び出せるため、再入力する必要はありません。   クエリビルダーの操作手順 1.左下部の「Query your data」をクリックします。 2.以下の画面にてNRQLを記載していきます。 3.自動補完機能があるため、入力補完機能を使ってリストから選択しながらNRQLを作成できます。作成後、「Run」をクリックすることで、該当のグラフが表示されます。       クエリビルダーの概要 | New Relic Documentation The New Relic query builder lets you run queries of your data, build charts and other visualizations, and share charts. docs.newrelic.com   データエクスプローラー(Data Explorer) どんなデータが保存されているのかを確認したい場合に、GUI上で視覚的に確認できる場所がデータエクスプローラーです。 NRQLの書き方がわからなくても、クリック操作でNRQLを作成 することができます。以下、5つのデータタイプに分かれています。 データタイプ 特徴 利用シーン Events アプリやサービスのイベントデータ(例: Transaction, PageView) アプリのレスポンス時間、エラー率、ユーザー操作分析など Metrics システムやサービスのメトリクス(CPU、メモリ、ネットワーク) インフラ監視、リソース使用率分析など Timeslices 時間単位で集計されたデータ TimeslicesはNew Relic Oneに統合する前に使われているため、現在はTIMESERIESを使うのが一般的。 過去のダッシュボードやAPI利用する場合に使用を想定 Logs アプリやシステムのログメッセージ エラーログ検索、トラブルシューティングなど lookups IDを名前に変換して見やすくする 視認性を高めるために、データの表示名を変更したいとき   データエクスプローラの操作手順 実際にデータエクスプローラを使ってNRQLの構文を作成します。今回は、メモリの空き容量の平均を調べる例を基に進めます。そのためには、メモリの空き容量がどのデータのイベントタイプに含まれているかを、事前にNew Relicのデータ辞書で確認する必要があります。ここでは、データ辞書にアクセスしてどのイベントタイプに格納されているか確認済という前提で手順を解説します。 1.左下部の「Query your data」をクリックし、以下の画面から「DATA EXPLORER」をクリックします。 2.表示するデータの期間をタイムピッカーから選択後、データタイプを選択します。ここではEventsを例に進めます。 3.New Relicデータ辞書からメモリの空き容量を格納しているデータ名を検索後、該当の項目をクリックします。 4.該当の項目でメモリ空き容量に該当する属性の「・・・」をクリックし、関数を選択します。 5.選択後、自動的にNRQLが表示されますので、「Run」を実行すると、データが表示されます。 【補足】FACET hostnameを補記して、「Run」を実行すると、ホスト別に表示されます。   データエクスプローラーの概要 | New Relic Documentation An introduction to the New Relic data explorer for browsing and visualizing your data. docs.newrelic.com   NRQLの活用 この記事では、NRQLの活用方法を2つ紹介します。 NRQLの実行結果を日常的に確認する方法:ダッシュボードに追加して表示 既存NRQLをカスタマイズする方法:New Relicの既存テンプレートや自動生成されたグラフからNRQLを真似る ダッシュボードに追加する NRQLからデータ分析した結果を定常的に確認したい場合、ダッシュボードに追加して閲覧することができます。ここでは、NRQLから作成したデータをダッシュボードに追加する方法を解説します。 1.クエリビルダーを開き、NRQLを記載し、「Run」を実行後、右下部の「Add to dashboard」をクリックします。 2.「Widget title」にグラフの名前をつけます。すでにダッシュボードがある場合は、どのダッシュボードに追加するか一覧が表示されます。ここでは「Create a new dashboard」をクリックします。 3.ダッシュボードの名前を入力し、「Copy」をクリックします。 4.左メニューから「Dash boards」をクリックし、先ほど作成したダッシュボード名をクリックします。 5.NRQLで作成したグラフが表示されます。都度NRQLを実行しなくてもダッシュボード上で情報が確認することができます。       自動生成されるNRQLを使ってカスタマイズ ダッシュボードの既存テンプレートを使えば、観測したいデータをすぐに表示できます。また、アラート設定では、New Relicが提示するNRQLを活用して、効率的にカスタムクエリを作成できます。ここでは自動生成されるNRQLの見方レベルまでの解説としています。 New Relicのダッシュボードテンプレートには、推奨されるメトリクスがあらかじめ集約されています。その中から必要なチャートを選び、NRQLをコピーすることで、オリジナルのダッシュボードを簡単に作成できます。このデータが欲しいけれどNRQLがわからないという場合でも、一からクエリを作成する必要がなく、NRQL作成のハードルが下がります。 レスポンスタイムやCPU/メモリなどのメトリクス情報をNRQLで分析したい場合、アラート設定の過程でNRQLも表示されます。特定のホストのみのデータを分析したい場合などに、参考となります。   コメントアウトの仕方 NRQLクエリの意味を明確にするために、説明を残す方法は3パターンあります。メンテナンス性を高め、属人化を防ぐためにも、クエリの意図や目的を記録しておくことが重要です。 -- (ダッシュ2つ) この記号の右側にある同じ行のテキストをコメントとして扱います。 // (スラッシュ2つ) この記号の右側にある同じ行のテキストをコメントとして扱います。 /* ... */ (スラッシュとアスタリスク) この記号の間にあるテキストをコメントとして扱います。複数行にわたって記述できます。   NRQLリファレンス | New Relic Documentation A detailed reference list of clauses and functions in NRQL, the New Relic query language. docs.newrelic.com   主要クラウドのメトリクス クラウド環境では、EC2やKubernetesなど多様なリソースが稼働しています。これらのメトリクスを一元的に分析することで、パフォーマンス最適化やコスト削減が可能です。New RelicのNRQLを使えば、クラウド連携で送られたテレメトリデータ(メトリクス、イベントやログ等)を柔軟にクエリし、ダッシュボード化できます。 プラットフォーム 命名規則・表現形式 New Relicへの取り込み方式 AWS – aws.<namespace>.<metricName> に変換 ( / → . に変換し、元の大文字小文字を保持) – CloudWatch Metric Streams (推奨) :リアルタイムストリーミング – APIポーリング:サービス毎に間隔は異なり、AWS側で制限を受ける可能性あり              AWSサービス固有のAPIレート制限 | New Relic Documentation Azure – azure.<resourceType>.<metricName> に変換(”Microsoft. → azure.” “/ → . “に変換。空白削除、元の大文字小文字維持)  – ポーリングベースでAzure Monitor APIを使用 – 取得頻度や範囲はサービスごとに間隔は異なり、Azure側で制限を受ける可能性あり              Azure統合のポーリング間隔 | New Relic Documentation GCP – gcp.<service>.<metricName> に変換 – GCP Monitoring APIをポーリングして取得 – 取得頻度や範囲はサービスごとに異なる              GCP統合のためのポーリングインターバル | New Relic Documentation   メトリックAPIの制限と制限された属性 | New Relic Documentation Rate limits and restricted keywords for the New Relic Metric API, and what to do if you reach their limits. docs.newrelic.com   利用可能なメトリクスは、以下からご確認いただけます。 AWSインテグレーションのメトリクス | New Relic Documentation AWSインテグレーションのメトリクス docs.newrelic.com Azure統合メトリクス | New Relic Documentation Azure統合メトリクス docs.newrelic.com GCP統合メトリクス | New Relic Documentation GCP統合メトリクス docs.newrelic.com   NRQLの利用制限 無制限にクエリを実行できると、 New Relicのサービスを使っている全世界のユーザーのパフォーマンスに悪影響を与える可能性があります。そのため、一定の利用制限を設けることで、誰もが公平に利用できるようリソース配分を実現しています。 制限項目 内容 備考 クエリ実行時間 (結果が返されるまでの最大許容時間。この時間を超えるとタイムアウト) – Dataプラン: 最大 1分 – Data Plusプラン: 最大 10分 NerdGraph API経由の場合、デフォルトタイムアウトは 5秒 API経由のクエリ数 1アカウントあたり 1分間に最大3,000クエリ UIからの実行には適用されない 同時実行クエリ数 複雑なクエリ(FACETやTIMESERIES含む)は最大5件程度推奨 長時間並列実行は制限に達する可能性あり 制限確認方法 Limits UIで現在の制限状況を確認可能 超過時はクエリが拒否される場合あり 制限確認方法については以下の記事をご参照ください。 【New Relic】New Relicによるデータ収集の仕組み この記事では、エージェントを導入する前に、New Relicで収集されるデータの内容と、その構造について解説します。あわせてセキュリティ面にも触れています。New Relicのデータ収集の仕組みを理解する際の参考になれば幸いです。 blog.usize-tech.com 2025.11.06   Rate limits with NRQL | New Relic Documentation An explanation of rate limits for NRQL, the New Relic query language docs.newrelic.com   さいごに この記事では、New Relicに送信したデータをどのように検索・分析できるかを解説しました。NRQLを使いこなすことで、柔軟なダッシュボードやアラートを作成でき、データ活用の幅が広がります。私自身、まだNRQLを使いこなしているわけではありませんが、AIにNRQL構文を質問しながらトライ&エラーを重ね、クエリの結果が表示されたときの達成感を糧にスキルを磨いています。今後はNRQLでよく使う構文例をご紹介します。 SCSKはNew Relicのライセンス販売だけではなく、導入から導入後のサポートまで伴走的に導入支援を実施しています。くわしくは以下をご参照のほどよろしくお願いいたします。
アバター
クラウド環境におけるディザスタリカバリ(DR)の重要性は年々高まっています。特に、リージョン障害や大規模障害に備えた仕組みは、事業継続計画(BCP)の観点から必須です。AWS Elastic Disaster Recovery(以下、DRS)は、AWSが提供するDRサービスで、オンプレミスやAWS内のシステムを別リージョンに迅速に復旧できる仕組みを提供します。 今回、DRSを用いてEC2インスタンスのレプリケーションとフェイルオーバーを検証しました。 AWS Elastic Disaster Recoveryとは AWS DRS(AWS Elastic Disaster Recovery)は、AWSが提供するディザスタリカバリ(DR)サービスで、オンプレミスやクラウド上のシステムをAWSにレプリケーションし、障害時に迅速に復旧できるようにする仕組みです。 主な特徴は以下の通りです。 シンプルな構成 専用エージェントをインストールするだけでレプリケーション開始 迅速なフェイルオーバー 数分でAWS上にシステムを起動可能。 RPO(Recovery Point Objective)とRTO(Recovery Time Objective)を短縮。 同一IPでの復旧が可能 プライベートIPを維持できるため、アプリケーション再設定不要 コスト効率 通常時はレプリケーションのみで、AWS上に最小限のリソースを保持。 災害発生時に必要なインスタンスを起動するため、コスト効率が高い。 ブロックレベルのレプリケーション ソース環境(オンプレミスや他クラウド)のサーバーをAWSにリアルタイムで複製。 OS、アプリケーション、データを含む完全なシステムを対象。 ちなみに、なぜ EDR ではなく、 DRS と略されるのか気になったのですが、FAQに答えがありました。 Disaster Recovery – AWS Elastic Disaster Recovery FAQs – Amazon Web Services なぜAWSのエラスティック災害復旧は「AWS DRS」と略されるのでしょうか? AWS Elastic Disaster Recovery の略称は AWS DRS です。DRS は「Disaster Recovery Service(ディザスタリカバリーサービス)」を意味します。この名称が選ばれた理由は、EDR という略称がすでに別の意味で広く使われているためです(EDR は「Endpoint Detection and Response」を指します)。 構成イメージ ソースリージョン:東京(ap-northeast-1) ターゲットリージョン:バージニア(us-east-1) 対象インスタンス:Windows Server 2022 ネットワーク構成:VPC、サブネット、セキュリティグループは事前に準備 以下のサイトを参考にしました。 – サポートWindows https://docs.aws.amazon.com/drs/latest/userguide/Supported-Operating-Systems-Windows.html – Agentインストール Windows への AWS レプリケーションエージェントのインストール https://docs.aws.amazon.com/drs/latest/userguide/windows-agent.html https://aws-elastic-disaster-recovery-us-east-1.s3.us-east-1.amazonaws.com/latest/windows/AwsReplicationWindowsInstaller.exe – 通信要件 https://docs.aws.amazon.com/drs/latest/userguide/Network-diagrams.html#Network-diagrams-onprem-vpn https://dev.classmethod.jp/articles/drs-onpremises-network/ 検証してみた AWSDRS設定 まずはDR先のバージニアリージョンでAWSDRSの設定をします。 「設定と初期化」ボタンから設定に進みます。 DRSの設定はあとから変更できるので、最初はデフォルト設定のままでも大丈夫です。 まず、レプリケーションサーバを配置するサブネットと、レプリケーションサーバのインスタンスタイプを指定します。 インスタンスタイプはデフォルトのt3.smallが推奨のようなので、そのままにしています。   次にボリュームとセキュリティグループを指定します。 こちらもデフォルト設定が推奨されているようなので、このままにします。   次にレプリケーションの設定です。 レプリケーションをプライベート接続にしたり、ネットワーク帯域幅の調整、バックアップ保持日数などの設定ができます。 今回はデフォルトままにします。   次にDRS起動設定です。 リカバリ実行時のリカバリサーバの起動設定になります。 今回は復旧前後のサーバIPを固定したいので、「プライベートIPをコピー」をオンにしました。   最後にデフォルトのEC2起動テンプレートを設定します。 あくまでデフォルト設定であり、ソースサーバ(DR元サーバ)ごとに設定できます。 リカバリサーバのデプロイ先サブネットを指定し、そのほかはデフォルト設定としました。      「確認と初期化」画面ではサマリが表示されるので、内容を確認し「設定と初期化」ボタンを押します。   以上でAWSDRSの初期設定は完了です。   ソースサーバの準備 次にDR対象となるEC2(Windows)を起動し、Agentをインストールします。 EC2にはマネージドポリシーの「AWSElasticDisasterRecoveryEc2InstancePolicy」をアタッチしました。 下記からインストーラーをダウンロードします。 Installing the AWS Replication Agent on Windows – AWS Elastic Disaster Recovery DR先のリージョン用のインストーラーが必要なため、バージニアリージョンを指定します。 https://aws-elastic-disaster-recovery-us-east-1.s3.us-east-1.amazonaws.com/latest/windows/AwsReplicationWindowsInstaller.exe aws-elastic-disaster-recovery-us-east-1.s3.us-east-1.amazonaws.com   Windowsに管理者権限でサインインし、ダウンロードしたインストーラーを「管理者として実行」で起動します。 コマンドプロンプトが起動します。 「AWSElasticDisasterRecoveryEc2InstancePolicy」をアタッチしない場合は、アクセスキーの入力を求められますが、 今回はアタッチしたためリージョンのみ入力しました。レプリケーション先のリージョンを入力します。 その後、レプリケーションするディスクを選択するか聞かれますが、すべてのディスクが対象なので、何も入力せずエンターキーを押下します。 インストールが成功すると「… successfully installed.」が表示されるので、エンターキーで画面を閉じます。   AWSDRSコンソールを確認するとソースサーバとして登録され、同期が開始されていました。   EC2コンソールから「AWS Elastic Disaster Recovery Replication Server」という名前でレプリケーションサーバが起動していることも確認できます。   これで準備は完了しました。 次回リカバリを実行してみたいと思います。
アバター
SCSKの畑です。 前回の投稿 に引き続き、3回目として非同期処理部分のフロントエンド実装についてピックアップして説明していきます。 フロントエンドにおける非同期処理実装の方針について まず前提として、同期処理で実装していた部分を非同期処理に変更しても、画面の遷移/見せ方自体は基本的に同期処理時と同一にする必要があります。例えば、テーブルデータの取得処理を非同期にただ変更するだけだと、データ取得が完了しない内にテーブルデータの表示画面に遷移してしまうため、単純に想像すると取得が完了するまで空の表が表示されることになってしまいます。また、おそらく実際には何らかのエラーが発生してしまう可能性が高いです(同期処理である以上、テーブルデータが取得されている前提で表示画面のロジックが組まれているため) テーブルの更新差分計算処理など、画面遷移後も引き続きバックグラウンドで処理を継続できるようなものはこの限りではありませんが、全体の割合としては少なかったです。 よって、前回の投稿でも言及した通り AppSync の Subscription を使用する方針としました。非同期処理の進捗状況や完了をプッシュ通知としてリアルタイムで受け取れる方が実装上の都合も良いためです。また昨年度の投稿でも記載している通り、テーブルのステータス(編集状態)を画面上でリアルタイム反映する機能などに Subscription を使用した実績がある点も理由の一つでした。 Web アプリケーションにおける排他制御の実装例(第三回) 作成中の Web アプリケーションにおいて排他制御を実装するための重要なステータスの扱いについて、実装上の考慮点や工夫をまとめました。 blog.usize-tech.com 2025.01.16 ただし、今回非同期に変更する各処理・画面ごとに Subscription を使用するように実装を変更するというのは効率があまりよろしくないため、非同期処理の進捗状況を表示するインジケータ(いわゆるロード画面)を Nuxt.js の component として実装して、各画面から共通して使用できるようにしました。そのあたりの話について次のセクションにて説明していきます。 なお、前回に引き続き広野さんのエントリもそのものズバリな内容であるため再掲します。 AWS AppSync を使って React アプリからキックした非同期ジョブの結果をプッシュ通知で受け取る 非同期ジョブを実行した後、結果をどう受け取るか?というのは開発者として作り込み甲斐のあるテーマです。今回は React アプリが非同期ジョブを実行した後に、AWS AppSync 経由でジョブ完了のプッシュ通知を受け取る仕組みを紹介します。 blog.usize-tech.com 2022.12.01 非同期処理進捗状況表示用の共通 component の実装例 ちょっと悩みましたが部分的に切り出して説明するのも難しいので、実装例をそのまま載せてしまおうかと思います。 <template> <UProgress v-model="currentWipValue" :max="TaskStep" :color="getIndicatorColor()"> <template v-for="(step, index) in TaskStep" :key="index" #[`step-${index}`]="{ step }"> <span v-if="currentErrorMessage" class="text-red-500"> <UIcon :name="getIconName(index)"/> {{ getStepText(step) }} </span> <span v-else> <UIcon :name="getIconName(index)"/> {{ getStepText(step) }} </span> </template> </UProgress> </template> <script setup lang="ts"> import * as subscriptions from "@/src/graphql/subscriptions"; import * as models from "@/src/API"; // Propsの定義 interface Props { task_id: string task_step: string[] } // Emitの定義 const emit = defineEmits<{ completed: [info: models.AsyncTask] failed: [error: string] }>() const props = defineProps<Props>() const { addErrorInfo } = useErrorInfo() const client = useNuxtApp().$Amplify.GraphQL.client const TaskStep = toRef(props, 'task_step') const currentWipValue = ref<number>(0) const currentErrorMessage = ref<string>('') const currentStatus = ref<models.AsyncTaskStatus | null>(null) const taskSubscription = ref<any>(null) const getIconName = (index: number) => { if (currentStatus.value === models.AsyncTaskStatus.FAILED) { return 'material-symbols:error' } else if (currentStatus.value === models.AsyncTaskStatus.COMPLETED) { return 'material-symbols:check' } else { return 'svg-spinners:90-ring-with-bg' } } const getStepText = (step: string) => { if (currentErrorMessage.value) { return currentErrorMessage.value } return step } const getIndicatorColor = () => { if (currentErrorMessage.value) { return 'error' } return 'primary' } // 非同期処理の進捗状況のサブスクライブ const subscribeAsyncTask = async () => { try { taskSubscription.value = client .graphql({ query: subscriptions.onUpdateAsyncTask, variables: { id: props.task_id } }) .subscribe({ next: (data: any) => { const asyncTask = data.data.onUpdateAsyncTask if (asyncTask) { currentWipValue.value = asyncTask.session_info.wip_value || 0 currentErrorMessage.value = asyncTask.err_msg || '' currentStatus.value = asyncTask.status // タスクが完了またはエラー状態になった場合 if (currentStatus.value === models.AsyncTaskStatus.COMPLETED) { emit('completed', asyncTask) //unsubscribeAsyncTask() } else if (currentStatus.value === models.AsyncTaskStatus.FAILED) { emit('failed', currentErrorMessage.value || '非同期実行処理が何らかの原因で失敗しました。') unsubscribeAsyncTask() } } }, error: (error: any) => { emit('failed', error.message || '非同期実行処理サブスクライブ時に何らかのエラーが発生しました。') unsubscribeAsyncTask() } }) } catch (error: any) { emit('failed', error.message || '何らかのエラーが発生しました。') } } // 非同期処理の進捗状況のアンサブスクライブ const unsubscribeAsyncTask = () => { if (taskSubscription.value) { taskSubscription.value.unsubscribe() taskSubscription.value = null } } onMounted(async() => { if (props.task_id) { await subscribeAsyncTask() } }) onUnmounted(() => { try { unsubscribeAsyncTask() } catch (error: any) { addErrorInfo(error) } }) </script> 内容についてもかいつまんで説明します。 このコンポーネントを他のページ(画面)から mount した時点で、特定の非同期処理のステータスを subscribe する ページ(画面)ごとに実行する非同期処理の種類自体は異なるため、このコンポーネント内に非同期処理の実行は含まない ページ側で非同期処理を実行した後に返り値として得た ID をprops 経由で本コンポーネントに渡すことで、非同期処理ステータス管理用のテーブルの該当行の subscribe を実現 非同期処理の種類に応じてインジケータのラベルに示す内容(文面)やステップ数が異なるため、同じく props 経由で本コンポーネントに渡す subscribe により更新を検知した場合はインジケータの進捗状況を更新の上、ステータスが完了またはエラーの場合は呼び出し元ページの対応するメソッドを emit 経由で実行して後続処理を進める 完了の場合はコンポーネント側で unsubscribe していないが、呼び出し元ページ側で後続処理が必要&引き続きインジケータにその進捗状況を表示し続けたいケースがあるため 共通 component 呼び出し元ページの実装例 こちらは呼び出し元のページによって実装が大きく異なるので、対象コンポーネント呼び出し部分のみ抜粋します。例えば最新のテーブルデータを Redshift から取得する場合の実装はこんな感じです。AsyncTaskProgress が今回実装例として示したコンポーネント名です。 <div v-if="AsyncTaskID_loadtabledata" class="my-4 max-w-4xl"> <AsyncTaskProgress :task_id="AsyncTaskID_loadtabledata" :task_step="['初期化', 'Redshift上の最新データを取得', 'Redshift上の最新データとの比較', 'Redshift上の最新データをS3に反映', 'S3上のデータをロード']" @completed="onLoadTableDataCompleted" @failed="onLoadTableDataFailed" /> </div> :task_id に最新のテーブルデータを Redshift から取得する非同期処理の ID を渡す :task_step にタスクのステップ数とラベルを定義した文字列型の配列を渡す @completed 及び @failed で、共通 component からemit 経由で実行する呼び出し元ページのメソッドを指定する なお、最新のテーブルデータを Redshift から取得する非同期処理が完了した時点でテーブルデータを画面上に表示するために、この AsyncTaskProgress コンポーネントは非同期処理の実行中のみ mount(画面に表示)する必要があります。このため、本コンポーネントの外側の div タグの v-if の条件句として AsyncTaskID_loadtabledata を指定の上、処理完了後に同変数を undefined で初期化する実装としています。 まとめ 要件変更(扱うデータ量の長大化)によるアプリケーションの設計・実装変更に伴う、特定処理の改修(同期処理⇒非同期処理)について、フロントエンド/バックエンドそれぞれの観点から2回に渡ってまとめました。全体通しての振り返りは第1回の投稿でまとめてしまった感があるのでここであまり書くことがないのですが、全体方針が決まってからの改修自体はフロントエンド/バックエンド共にそこそこ効率良く実施できたかと思います。 扱うデータ量の長大化によるアプリケーションの改修は今回説明した以外にも主にフロントエンド側で幾つか発生したので、そのあたりの説明についても今後別エントリにて触れていく予定です。 本記事がどなたかの役に立てば幸いです。
アバター
SCSKの畑です。 前回の投稿 に引き続き、今回は非同期処理部分のバックエンド実装についてピックアップして説明していきます。 バックエンドにおける非同期処理実装の方針について 前回の投稿で説明した通り、密結合・同期処理前提の実装を、疎結合・非同期処理前提の実装に変更する必要がありました。この内、密結合を疎結合に変更する過程については、非同期処理として分割すべき処理単位を頑張って中身を見ながら分割していく・・くらいしか極論書くことがないので割愛します。 一方、非同期処理への変更については処理ロジックそのものを大きく変更する必要はありません。例えば前回の投稿で取り上げたテーブルの更新差分計算処理についても、その計算処理が非同期で実行されるというだけで計算ロジック自体には手を入れる必要がないためです。あくまで要件としてはそのような特定の処理を非同期実行することにあり、かつ既存の実装(Lambda)が既に存在することから、特定の処理を非同期で実行するための共通インターフェース/ラッパーを実装した上で、非同期処理として実行する Lambda の入出力仕様をそれに合わせる形で修正していく方針が最も効率的と判断しました。 お客さん環境における AWS リソースの追加・変更のための申請等にリードタイムが必要なこともあり、一連の申請に要するリードタイムが最も短い(=既存 AWS リソースの追加・変更が最も少ない)方針にすべきという観点からも有力でした。この方針に従うと、AppSync のデータソースを1つ、Lambda を数個作成する申請を上げるだけで済んだので。 ※なお、AppSync の他リソースについては変更可能な IAM 権限を頂けているので大丈夫でした。そのあたりの顛末は以下のエントリを御覧ください。 AWS AppSync における特定 API 配下のリソースのみに編集権限を付与する AWS AppSync において、特定の API 配下のリソースのみに編集権限を付与するような IAM ポリシーの設定について試行錯誤した内容についてまとめました。 blog.usize-tech.com 2024.12.24 非同期実行のための共通インターフェース/ラッパーの実装 上記方針に基づき、以下のような流れで実装を進めていきました。 DynamoDB 上に非同期実行ステータス管理用のテーブルを作成 非同期実行用の共通インターフェース/ラッパーとしての AppSync クエリの作成 2.で作成した AppSync クエリのデータソースとなる Lambda の作成 非同期実行対象の Lamdba について、主に入出力仕様を1.及び2.の実装に対応する形で変更 以下、順番に説明します。 1.DynamoDB上に非同期実行ステータス管理用のテーブルを作成 さて、実際に特定の処理を非同期実行するにあたり、その実行ステータスを何かしらのデータストアで管理する必要があります。フロントエンド(画面)側でそのステータスを取得した上で、画面のロジックや通知などに反映する必要があるためです。このあたりの話は、以前の投稿でも取り上げさせて頂いた通り、広野さんの以下エントリに「プッシュ通知」として詳説されていますので適宜ご参照頂ければと思います。 AWS AppSync を使って React アプリからキックした非同期ジョブの結果をプッシュ通知で受け取る 非同期ジョブを実行した後、結果をどう受け取るか?というのは開発者として作り込み甲斐のあるテーマです。今回は React アプリが非同期ジョブを実行した後に、AWS AppSync 経由でジョブ完了のプッシュ通知を受け取る仕組みを紹介します。 blog.usize-tech.com 2022.12.01 データストア自体は極論何を使用しても良いと思いますが、Amplify 及び AppSync との親和性や画面へのプッシュ通知機能(Subscription)の使用を考慮して素直にDynamoDBのテーブルを使用しました。以下 Amplify によるスキーマ定義例となります。 type AsyncTask @model ( queries: { list: "listAsyncTasks", get: null }, mutations: { create : "createAsyncTask", update: "updateAsyncTask", delete: null }, subscriptions: { onCreate: null, onUpdate: null, onDelete: null }, ) @auth(rules: [ {allow: public, provider: apiKey}, {allow: private, provider: iam}, ]) { id: ID! @primaryKey status: AsyncTaskStatus! session_info: AWSJSON err_msg: String ttl: AWSTimestamp! createdAt: AWSDateTime updatedAt: AWSDateTime } enum AsyncTaskStatus { PENDING PROCESSING COMPLETED FAILED POSTPROCESSING } type Subscription { onUpdateAsyncTask(id: ID!): AsyncTask @aws_subscribe(mutations: ["updateAsyncTask"]) @aws_api_key @aws_iam } 内容についてもポイントだけかいつまんで説明します。 使用する想定のない query/mutation/subscription は明示的にnullを設定して Amplify による自動作成を抑止 特に本機能における Subscription は特定の行(=特定のID)のみを対象とできれば良いため、@model の定義には含めずSubscription として個別に定義 非同期実行したタスク(Lambda)の実行単位でユニークなIDを発行し、そのステータスを status 列で管理 非同期実行したタスク(Lambda)の処理結果は原則 S3 上に出力する前提とするが、タスクの実行中や実行後に同期的に渡す必要のある情報については session_info 列を使用してやり取り、例えば以下のような目的で使用 非同期実行しているタスクの処理状況を画面にリアルタイムに通知するための情報を格納 非同期実行の呼び出し元で S3 上に出力される処理結果のパスを同定できない場合、そのパスを格納 本テーブルの情報はあくまでテンポラリで永続的に保持する必要がないため、DynamoDB の TTL 機能を使用して自動的に削除、そのためのタイムスタンプ情報として ttl 列を使用 なお、残念ながら Amplify gen1 におけるスキーマ定義に TTL 設定は含められないため、別途 DynamoDB 側で定義する必要あり。Amplify gen2 では別の仕組みでできるらしい?  createdAt・updatedAt 列は Subscription との兼ね合いで明示的に定義 Amplify が自動で気を利かせて作成してくれる Subscription を使用する分には明示的な定義は不要なようですが、今回は明示的に ID 列をキーとした Subscription を定義しているため、明示的にスキーマ定義に含める必要がありました ちなみに、Amplify でテーブルのスキーマに createdAt・updatedAt 列を定義していなくても、Amplify が自動的に作成した Mutation を使って更新すると同情報が含まれるようになっています 2.非同期実行用の共通インターフェース/ラッパーとしての AppSync クエリの作成 平たく言うと「非同期実行対象となる他の Lambda を非同期実行するための AppSync クエリを作成する」という話なので、Amplify による AppSync のスキーマ定義も以下実装例のようにシンプルですが、ちょっとだけ工夫してます。 type ExecuteAsyncTaskResult { exec_result: Boolean! id: ID } input ExecuteAsyncTaskInput { type: AsyncTaskType! args: AWSJSON! } enum AsyncTaskType { <非同期実行対象のタスク名を定義> } type Query { ExecuteAsyncTask(input: ExecuteAsyncTaskInput!): ExecuteAsyncTaskResult @function(name: "<データソースのLambda関数名>") @aws_api_key @aws_iam } 引数  type 引数において、AsyncTaskType として非同期実行対象のタスク名を enum で定義 このタスク名と非同期実行したい Lambda 関数名の1:1の対応関係をコンフィグとして AppSync に対応する Lambda 関数に持たせることで、環境ごとの Lambda 関数名の差異を吸収 一方、非同期実行対象の各タスク(Lambda)ごとに必要となる引数は異なることから、それは引数 args により AWSJSON 形式で指定の上、対象の Lambda 関数にそのまま渡す 返り値 非同期実行の成否(=本 AppSync クエリの実行成否)自体を exec_result で返却 一方、非同期実行されたタスク(Lambda)の実行ステータスを呼び出し元から確認・取得できる必要があるため、そのキーとなる ID を合わせて返却 この ID が1.で作成した DynamoDB テーブルにおける ID 列の値に対応します 3. 2.で作成した AppSync クエリのデータソースとなる Lambda の作成 同様にデータソースとして定義している Lambda 関数の実装例も載せてみました。 import os import json import time import boto3 import requests import logging import traceback from lambdautility import common from aws_requests_auth.aws_auth import AWSRequestsAuth # GraphQL mutations mutation_createAsyncTask = """ mutation CreateAsyncTask($input: CreateAsyncTaskInput!) { createAsyncTask(input: $input) { id status ttl } } """ mutation_updateAsyncTask = """ mutation UpdateAsyncTask($input: UpdateAsyncTaskInput!) { updateAsyncTask(input: $input) { id status ttl } } """ logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(event, context): try: # AppSync経由で渡される引数の取得 task_type = event['arguments']['input']['type'] args = event['arguments']['input']['args'] # 共通レイヤーから環境依存の変数を初期化 config_dict = common.get_env_config(os.environ.get('env_name')) APPSYNC_HOST = config_dict.get('appsync', 'host') APPSYNC_ENDPOINT = config_dict.get('appsync', 'endpoint') HEADERS = {'Content-Type': 'application/json'} TTL_SECONDS = config_dict.get('dynamodb', 'ttl') TARGET_LAMBDA_NAME = config_dict.get('async_lambda', task_type) if not TARGET_LAMBDA_NAME: raiseException(f'Lambda function not found for task type: {task_type}') # AppSync接続情報初期化 session = boto3.session.Session() credentials = session.get_credentials() auth = AWSRequestsAuth( aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=APPSYNC_HOST, aws_region='ap-northeast-1', aws_service='appsync' ) # TTL初期化 ttl = int(time.time()) + int(TTL_SECONDS) # AsyncTaskレコード作成 create_task_input = { 'status': 'PENDING', 'ttl': ttl } payload = { 'query': mutation_createAsyncTask, 'variables': {'input': create_task_input} } result_appsync = requests.post(APPSYNC_ENDPOINT, auth=auth, json=payload, headers=HEADERS).json() if 'errors' in result_appsync: raiseException(f'GraphQL error: {result_appsync["errors"]}') # レスポンスから非同期実行のIDを取得 task_id = result_appsync['data']['createAsyncTask']['id'] # 対象のLambda関数を非同期実行 lambda_client = boto3.client('lambda') invoke_payload = { 'src_id': task_id, 'async_flag': True, 'args': args } lambda_client.invoke( FunctionName=TARGET_LAMBDA_NAME, InvocationType='Event', # 非同期実行 Payload=json.dumps(invoke_payload) ) # Lambda関数実行後、ステータスをPROCESSINGに更新 update_input = { 'id': task_id, 'status': 'PROCESSING', 'ttl': ttl } update_payload = { 'query': mutation_updateAsyncTask, 'variables': {'input': update_input} } requests.post(APPSYNC_ENDPOINT, auth=auth, json=update_payload, headers=HEADERS) # ExecuteAsyncTaskの返り値(正常時) return { 'exec_result': True, 'id': task_id } except Exception as e: logger.error(f'Error: {str(e)}') logger.error(traceback.format_exc()) # ExecuteAsyncTaskの返り値(異常時) return { 'exec_result': False, 'id': None } こちらは前段(1. 及び 2.)で作成した Amplify・AppSync の定義に沿って作成している以上、他に説明できることがほとんどないのですが、実装における留意点をいくつか挙げてみます。 type 引数で渡される AsyncType に対応する Lambda 関数名は S3 上に配置したコンフィグファイルで定義の上、Lambda 関数の共通レイヤー内で読み込むような実装としている  対象のタスク(Lamdba)を非同期実行する前に、非同期実行ステータス管理用テーブルにレコードを登録して ID を取得 この ID をフロントエンド/バックエンド双方で使用して対象タスクのステータス管理・更新を実施します また、レコード登録時のタイミングで合わせて TTL も設定しておけば、(本ユースケースにおいては)処理失敗時などに対象レコードの掃除(削除)を実施する必要がなくなります 4.非同期実行対象の Lamdba について、主に入出力仕様を1.及び2.の実装に対応する形で変更 このステップにおける必要な対応は変更対象の Lambda の中身次第なので詳細は割愛しますが、変更にあたり共通して留意すべきポイントは以下2点かなと思います。特に2点目は Lambda から普通に DynamoDB を更新することもできてしまう分、ちょっとした落とし穴かなと。 非同期実行する Lambda 内でステータス管理用 DynamoDB テーブルを更新する際は、呼び出し時に引数として渡している ID を使用すること Subscription でプッシュ通知を受け取るために同テーブルは AppSync の mutation 経由で行う必要があること まとめ 改めて内容をまとめてみると思ったよりシンプルでした。逆に言うと各論になってしまう部分については相対的により多くの工数がかかってしまったところになりますが・・次回はフロントエンド側の実装変更について説明する予定です。 本記事がどなたかの役に立てば幸いです。
アバター
SCSKの畑です。期せずして昨年と同じく年明けからの投稿となりますがよろしくお願いします。 まずは昨年度の投稿で主に言及していた Redshift データメンテナンス用の Web アプリケーションについて、今年度も引き続き携わっている中で主に実施していた取り組みについて数回に渡って記載していきたいと思います。 アーキテクチャ概要 一年ぶりの投稿となるので載せておきます。今回はアーキテクチャの変更や改修を伴う内容ではないのですが、AppSync や Lambda が関連する話題となります。要するにバックエンド API の部分ですね。 背景 昨年度本アプリケーションをリリースしてお客さんに使い始めてもらったのですが、細かい不具合などはありつつも有難いことに全体的には好評頂き、それに伴いアプリケーションの機能拡張や他アプリケーションでカバーしている機能も移管・集約することで、より本アプリケーションを活用していきたいという主旨のご要望を頂いていました。もちろんその方向性自体は大変ありがたい話で、今年度はそれらの要望への対応を中心に取り組んでいました。 一方で、アプリケーションの機能拡張や他機能の移管・集約に伴い、本アプリケーション実装時点での要件からの変更も当然起こり得ます。その中で今回特にネックとなったのが 本アプリケーションで扱うデータサイズの長大化 です。 元々、過去の投稿で言及している通り、本アプリケーションでは主にマスタテーブルのデータ参照や更新をターゲットにしていました。もちろん同じマスタテーブルでもサイズの大小はありますが、こちらも以下のエントリで言及している通りちょっとした工夫もしつつ最終的にこのアーキテクチャで十分に捌けると判断しました。 AWS AppSync + AWS Lambda 構成におけるペイロードサイズ低減の試み AWS AppSync + AWS Lambda 構成において、リクエスト/レスポンスペイロードサイズを低減する試みの内容を、それに至った背景・原因と合わせて記載しています。 blog.usize-tech.com 2025.01.20 ところが、今回の機能拡張に伴いより大きなデータサイズを持つテーブルを本アプリケーションで扱う必要が出てきてしまいました。具体的には以下のような内容です。 履歴管理テーブル 特定マスタ(機種情報など)の履歴や、ETL/ELT 処理失敗時のエラー履歴を蓄積。 後者は最終的に ETL/ELT 処理時のデータ変換マスタとしても活用。 今年度における ETL/ELT 処理改善取り組みの一環として実装。 ダッシュボード/レポート表示結果検算用テーブル 最終的なダッシュボード/レポートの表示結果が正しいかどうかをエンドユーザ側でチェックするための補足情報。 元々は BI ツール上で暫定的に表示していたが、合わせてマスタの確認・修正が必要になることが多いため機能移管したいという要望より実装。 もちろん、トランザクションテーブルやマートテーブルなどより巨大なデータを持つテーブルと比較すると十分現実的なラインではあるのですが、本アプリケーションのアーキテクチャやアプリケーションの作りを踏まえて考えるとやはり厳しい部分が出てきてしまいます。それが、先述した過去の投稿でも言及している AppSync のレスポンスタイム及びペイロードサイズに関する以下の制約です。 ペイロードサイズ: 5MB レスポンスタイム: 30秒 言わずもがな、扱うデータサイズが増大することによりどちらの制約にも抵触する可能性が高まりますが、実際に上記種類のテーブルを本アプリケーションから扱えるか試してみたところ見事に引っ掛かってしまったため、いよいよ腰を据えての対策が必要となりました。 ちなみに、扱うデータ量が増大するとフロントエンド(画面)側の処理にも影響することは自明ですが、そちらに関連する内容は別のエントリで取り上げたいと思います。パフォーマンス改善に苦心した処理が幾つかあったので・・ 対策として実施したアプリケーションの設計・実装変更 大きく以下2点の変更を実施することで、AppSync の制約を回避することができました。なお、これらの変更を具体的にどう実装に落とし込んだかについては次回以降でかいつまんで説明していこうと思います。 「密結合かつ同期処理」から「疎結合かつ非同期処理」への変更 さて、対策が必要と書いたのですが厳密にはちょっと語弊がありまして、本アプリケーションのバックエンド API が AppSync の上記制約に引っ掛かるような作りをしているために対策が必要になった、というのが正確な表現となります。例えば、以前の投稿で取り上げたテーブルの更新差分を導出する処理では、AppSync のデータソースとして Lambda を使用の上、以下のような実装としていました。 テーブルデータの差分比較を pandas で実施する Redshift テーブルデータのメンテナンスアプリケーションにおいて、テーブルデータの差分比較機能を実装したので内容について記載してみます。バックエンド側の話です。 blog.usize-tech.com 2025.03.26 アプリケーションから更新差分取得用の AppSync API を実行 AppSync のデータソースである Lambda 内で更新差分を計算し、計算結果を AppSync に返す AppSync API の返り値として差分計算結果を取得 これだけ見ると特に違和感ないというか普通の処理の流れだと思うのですが、その一方で対象テーブルのデータサイズや更新量が増大した場合は、2.の処理により時間を要したり、3.でアプリケーションに返す差分計算結果のサイズが大きくなることで、結果的に AppSync の上記制約に抵触してしまいます。つまり、この更新差分導出の実装が密結合かつ同期処理前提になっていたことが、今回対策が必要となってしまった要因でした。よって、この処理を AppSync の上記制約をなるべく受けないようにするには 差分計算処理と差分取得処理をそれぞれ別の AppSync API(Lambda)として実装する 本アプリケーションにおいて差分取得処理は最終的に AppSync API(Lambda)を使用する必要性がなくなったため削除(理由は後述) 差分計算処理は非同期処理で実行することを前提とする この変更により AppSync のレスポンスタイム制約を回避可能 非同期処理となる以上、AppSync API の返り値として差分計算結果は取得できないため、DynamoDB や S3 上などアプリケーションからアクセスできる別の領域に結果を出力 差分計算処理の完了待機や、処理完了後の差分取得処理はアプリケーションのロジックとして実装 のように、疎結合かつ非同期処理前提に変更する必要がありました。他のバックエンド API も概ね似たような実装となっていたため、結果的にバックエンド API におけるテーブル関連の各処理(Redshift からテーブルデータを取得/Redshift へ更新/更新差分計算など)は一律で上記のような変更が必要となり、最終的に結構な工数を要してしまうことになりました。。。 特に、テーブルデータを取得する処理についてはアプリケーションにおけるテーブルの状態遷移管理との兼ね合いもありロジックが複雑で、「アプリケーションから読取/書込するテーブルデータの更新処理(アプリケーション上のバージョン管理含む)」と、「アプリケーション上で表示するためのテーブルデータの取得処理」を分割すること自体が大変でした。複雑なあまり実装時点で疎結合で実装することも考えていただけに、そのまま踏み切っていれば今回の一連の対応に要する工数がもう少し低減できたかもしれません。 S3 上にアプリケーション上で扱うデータを保持し、S3 署名付き URL を通して読み書きする方式に変更 上記対応により AppSync のレスポンスタイム制約は回避することができましたが、ペイロードの制約については回避できません。先程の更新差分の導出処理に例えると、計算処理を非同期化しても計算結果を取得する処理は同期処理でないと意味を成さない(=AppSync API の返り値として計算結果を取得できる必要がある)からです。つまりここに AppSync や Lambda を介するような作りにしてしまうと、その時点でペイロードの制約がついて回ることになります。 この対策としてはシンプルで、S3 上にアプリケーション上で扱う諸情報(例えばテーブルデータや更新差分情報など)を保持・永続化するようにした上で、アプリケーションからは S3 署名付き URL を介して読み書きするような方式に変更することで解決しました。AppSync や Lambda などのペイロードの制約を回避する方法としてはメジャーなものの一つかと思います。 実装上も Nuxt(Vue)の場合は fetch を使って S3 署名付き URL を叩けば良かったので変更難易度は総じて低かったです。S3 上で保持していなかったデータの配置場所の設計や、S3 署名付き URL 自体をフロントエンド/バックエンドどちら側で生成するのか、フロントエンド側の場合は対象の S3 URL をどのように導出するのかなどの細かな課題はありましたが、いずれも特に問題なく解決することができました。 まとめ・所感 本件について一応補足すると、実質的にアプリケーションの要件が昨年度のリリース時点から変更されたのが大元の原因であるため、そもそもの実装が極端に間違っていたとは考えていません。疎結合・同期処理前提とすることで、アプリケーション側のロジックが相対的にシンプルになるというメリットもありますし、昨年度のアプリケーション開発においては工数・期間の問題との兼ね合いもありました。 また、StepFunctions のステートマシンで作成している ETL ジョブを本アプリケーションから実行する機能も昨年度時点でリリースしていましたが、こちらについてはその性質上非同期処理として最初から実装していました。よって(相当に言い訳がましいですが)非同期処理として実装するという選択肢自体も昨年度の開発時から持てていたかなとは思います。 一方で、AppSync/Lambda のペイロードやレスポンスタイムへの制約の懸念が開発時点であったことも確かで、今回のようにその懸念が再燃した場合を考慮して事前に以下のような手を打てなかったのか、という点は大いに反省すべきところと感じました。 バックエンドAPIの機能単位としては疎結合ベースで実装すべきだった  疎結合で実装した機能単位の一部を非同期化すること自体は、AppSync の機能(subscription)も相まってそこまで難しくないため S3 署名付き URL を介したデータのやり取りについては最初から実装すべきだった 先述の通り大して難しくなかった上、ペイロード制約を回避するメジャーな方法であったため(事前調査不足) そのあたりの設計・実装の勘所についてはまだまだ未熟であると今回の対応を通して痛感したところで、今後も引き続き精進していければと感じました。本記事がどなたかの役に立てば幸いです。
アバター
こんにちは。SCSKの谷です。 AWSマネジメントコンソールから Amazon S3 のフォルダを作成した場合、実態はフォルダではなくオブジェクトとして取り扱われることをご存じでしょうか。 本記事では、S3のフォルダがオブジェクトとして扱われてしまう場合とそうでない場合について、検証・解説していきたいと思います! はじめに結論 AWS公式ドキュメントによると AWS公式のドキュメントに以下の記載がありました。(以下引用) 重要 Amazon S3 コンソールでフォルダを作成すると、S3 は 0 バイトのオブジェクトを作成します。このオブジェクトキーには、指定したフォルダ名と末尾のスラッシュ ( / ) 文字が設定されます。例えば、Amazon S3 コンソールで、バケットに  photos  という名前のフォルダを作成した場合、Amazon S3 コンソールは  photos/  キーを使用して 0 バイトのオブジェクトを作成します。コンソールは、フォルダの考え方をサポートするために、このオブジェクトを作成します。 引用: https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/using-folders.html 上記の通り、コンソール画面からフォルダを作成すると、そのフォルダ自体が0バイトのオブジェクトとして扱われるようです。 一方、AWS CLIなどでS3に「folder/test/test.txt」のようなファイルをアップロードする場合(例えば”aws s3 cp test.txt s3://<バケット名>/folder/test/test.txt”のようなコマンド)、プレフィックスとして指定した「folder/test/」はオブジェクトとしては扱われないようです。 フォルダがオブジェクトになる場合とならない場合について、いろいろなパターンで検証していきます!   検証 コンソールからS3フォルダを作成してオブジェクトを配置した場合 まずはS3のフォルダがオブジェクトとして扱われる場合についてです。 S3のコンソールよりフォルダの作成を行いました。作成したフォルダは「folder/test/」です。 上記フォルダ内に「test.txt」ファイルをアップロードし、CLIコマンド”s3 ls”で対象のS3ファイルの中を確認してみます。 結果として、「folder/test/test.txt」オブジェクトに加えて、「folder/」と「folder/test/」の末尾が”/”でフォルダとして扱われるものについても容量0のオブジェクトとして表示されていることが確認できました。 ※「–recursive」オプションをつけることで各フォルダの中も再帰的に表示しています。 ※「folder/」などの左にある数字がそのオブジェクトの容量を表しています。 CLIからプレフィックス指定してオブジェクトを配置した場合 次にCLIからプレフィックスを指定して、コマンドでオブジェクトを配置した場合についてみていきます。 今回指定するプレフィックスは「cli_folder/test/」とし、以下コマンドで「cli_test.txt」ファイルを先ほどと同様のS3に配置します。 aws s3 cp cli_test.txt s3://s3b-tani-test/cli_folder/test/cli_test.txt 同様にs3 lsで対象S3の中を見てみます。 先ほどとは異なり「cli_folder/test/cli_test.txt」オブジェクトのみ追加されています。 「cli_folder/」や「cli_folder/test/」がフォルダとして(容量0のオブジェクトとして)追加されることはありませんでした。 上記の検証より、コンソールでフォルダを作成した場合に限りフォルダが容量0のオブジェクトとして作成されてしまうようです。 CLIからオブジェクトを配置した場合はプレフィックスとして扱われ、フォルダとして扱われるわけではないことが確認できました。 <おまけ>CLIから末尾”/”のオブジェクトをS3に配置した場合 以下コマンドでCLIからS3に対して末尾”/”のオブジェクトを作成してみます。 (–keyオプションでオブジェクトの名前を指定できます。今回は末尾に”/”のついたオブジェクトを作成します。) aws s3api put-object --bucket <バケット名> --key 'cli_dir/' この場合、作成したオブジェクトがどのように扱われるかを、同様に”s3 ls”コマンドで確認してみます。 CLIから末尾”/”のオブジェクトを作成した場合もコンソールからフォルダを作成した場合と同様に、容量0のオブジェクトとして扱われるようです。 コンソールで作成したときと同様の扱いになることから、コンソール上ではフォルダとして表示されていました! 末尾”/”のオブジェクトがフォルダ扱いになることに関してはAWS公式のドキュメントにも記載がありました。 また、末尾にスラッシュ文字 ( / ) を含む名前が付いている既存のオブジェクトは、Amazon S3 コンソールにフォルダとして表示されます。例えば、キー名  examplekeyname/  のオブジェクトは、Amazon S3 コンソールのフォルダとして表示され、オブジェクトとしては表示されません。それ以外の場合は、他のオブジェクトと同様に動作し、AWS Command Line Interface (AWS CLI)、AWS SDK、または REST API を使用して表示および操作できます。さらに、キー名の末尾がスラッシュ文字 ( / ) のオブジェクトは、Amazon S3 コンソールを使用してアップロードすることができません。ただし、名前の末尾がスラッシュ ( / ) 文字のオブジェクトは、AWS Command Line Interface (AWS CLI)、AWS SDK、または REST API を使用してアップロードできます。 引用: https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/using-folders.html   <おまけのおまけ>–bodyオプションをつけた場合 –bodyオプションをつけることで、作成するオブジェクトの中身を指定することができます。 ◆以下コマンド例 aws s3api put-object --bucket <バケット名> --key 'cli_dir2/' --body cli_test.txt 上記コマンドを実行した場合の結果について確認してきます。 “s3 ls”コマンドで確認したところ、末尾に”/”がついていてフォルダのように見えるにも関わらず、35バイトの容量を持ったものとして表示されていました。 今までと違い0バイトのフォルダではないのはどういうことかと思いコンソールで確認してみると、「cli_der2/」というフォルダは作成されていましたが、そのフォルダ内にファイル名「/」のファイルが作成されていました。 「/」という容量のあるオブジェクトが保存されていたので、「cli_dir2/」に容量があったのも理解はできます。 しかし、フォルダなのかオブジェクトなのか非常に紛らわしいため、実際の運用では末尾に”/”を付けたオブジェクトを作成することはやめたほうがよさそうですね。 ちなみに「/」というファイルをS3からダウンロードして中身を確認してみたところ、–bodyオプションで指定したファイルと同様の内容が記載されていたので、アップロード時にファイルが壊れたりということもなさそうです。   まとめ コンソールで作成したS3フォルダがオブジェクトとして扱われる件について調査・検証しました。 公式ドキュメントに記載のある通り、コンソールからフォルダを作成した場合は容量0のオブジェクトとして扱われ、CLIからプレフィックス指定した場合はフォルダがオブジェクトとして扱われないことが確認できました。 また、”s3api put-object”コマンドを使用して末尾が”/”のオブジェクトを作成した場合、コンソールからフォルダを作成したときと同様に容量0のオブジェクトとなることが分かりました。 S3のオブジェクトをSDKで参照するLambdaスクリプト実装時などに、フォルダがオブジェクトとして扱われていることを考慮してスクリプト実装する必要があるかもしれないですね。
アバター
こんにちは、広野です。 MP4 録画した自分の PC の操作画面を、そのままではサイズが大きいのでアニメーション GIF にしたいと思いました。ネット上の無料サービスはあるのですが、セキュリティ的に心配があったので AWS Step Functions と AWS Elemental MediaConvert を使って変換ジョブをつくりました。 つくったもの PC 操作の録画 MP4 を GIF に変換したものです。 画質は 640 x 360 px、10 fps にしているので粗いですが、パラメータ次第で綺麗にできると思います。その分サイズは大きくなりますが。   仕様 MP4 ファイルを Amazon S3 バケットに配置したら、自動で AWS Step Functions ステートマシンが呼び出される。 ステートマシンは、AWS Elemental MediaConvert を呼び出し、S3 上の MP4 ファイルを GIF に変換し S3 に保存する。 最後に完了したことを Amazon SNS で通知する。 アーキテクチャ Amazon S3 のイベント通知により、Amazon EventBridge ルールが発火します。 Amazon EventBridge ルールは AWS Step Functions ステートマシンを呼び出します。 ステートマシンの中の AWS Elemental MediaConvert CreateJob API を実行し、Amazon S3 バケットにある MP4 を GIF に変換します。AWS Lambda 関数は一切使用していません。 変換した GIF は Amazon S3 バケットに保存されます。 CreateJob の結果を Amazon SNS を使用してユーザーに通知します。 Amazon SNS の通知は以下のように届きます。とりあえず完了したことがわかるだけの簡易な出来です。   ハマりポイント この構成を作成しようと思ったときは楽勝ーと思っていたのですが意外とハマり、半日かかってしまいました。 今回、CreateJob のところで以下の「タスクが完了するまで待機」の設定を有効にしました。この設定はそのタスクの完了 (コールバック) を待ってくれる設定なのですが、IAM ロールは特殊な設定をしないといけないようです。通常、この設定を OFF にすると、タスクの実行命令後は次のタスクに移ってしまいます。(非同期) 「タスクが完了するまで待機」の設定は、裏で AWS 管理の EventBridge を使用するようです。そのため、それにアクセスするための IAM ロールが必要です。 Step Functions を使用して AWS Elemental MediaConvertジョブを作成する - AWS Step Functions Step Functions を と統合AWS Elemental MediaConvertして MediaConvert ジョブを作成する方法について説明します。 docs.aws.amazon.com   今回、私は当初 MediaConvert に関する権限に関してはすべて AWS マネージドポリシーを使用して網羅していたつもりだったのですが、EventBridge に関するロールが抜け落ちていました。これになかなか気付けずハマりました。   AWS CloudFormation テンプレート 実際にデプロイしたときに使用した AWS CloudFormation テンプレートを貼り付けます。詳細な設定はこちらをご覧ください。 AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a Step Functions state machine. It provides converting MP4 to GIF. The IAM role MediaConvert_Default_Role must be created in your AWS account before creating a job. Please refer https://docs.aws.amazon.com/mediaconvert/latest/ug/creating-the-iam-role-in-mediaconvert-configured.html for details. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. use lower case only. (e.g. example) Default: example MaxLength: 10 MinLength: 1 SubName: Type: String Description: System sub name. use lower case only. (e.g. prod or dev) Default: dev MaxLength: 10 MinLength: 1 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "General Configuration" Parameters: - SystemName - SubName Resources: # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# S3Bucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${SystemName}-${SubName}-mp4-gif-conv LifecycleConfiguration: Rules: - Id: AutoDelete Status: Enabled ExpirationInDays: 14 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true NotificationConfiguration: EventBridgeConfiguration: EventBridgeEnabled: true Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} # ------------------------------------------------------------# # Elemental MediaConvert # ------------------------------------------------------------# MediaConvertQueue: Type: AWS::MediaConvert::Queue Properties: Description: !Sub For ${SystemName}-${SubName} Name: !Sub ${SystemName}-${SubName} PricingPlan: ON_DEMAND Status: ACTIVE Tags: Cost: !Sub ${SystemName}-${SubName} MediaConvertJobTemplate: Type: AWS::MediaConvert::JobTemplate Properties: Name: !Sub ${SystemName}-${SubName}-mp4-animated-gif-640-360 Description: !Sub The transcoding configuration for ${SystemName}-${SubName} (MP4 to Animated GIF) Category: !Ref SystemName AccelerationSettings: Mode: DISABLED Priority: 0 Queue: !GetAtt MediaConvertQueue.Arn SettingsJson: Inputs: - VideoSelector: ColorSpace: FOLLOW SampleRange: FOLLOW Rotate: DEGREE_0 EmbeddedTimecodeOverride: NONE AlphaBehavior: DISCARD PadVideo: DISABLED FilterEnable: AUTO PsiControl: IGNORE_PSI FilterStrength: 0 DeblockFilter: DISABLED DenoiseFilter: DISABLED InputScanType: AUTO TimecodeSource: ZEROBASED OutputGroups: - CustomName: GIF_output Name: File Group Outputs: - ContainerSettings: Container: GIF Extension: gif NameModifier: _animated VideoDescription: Width: 640 Height: 360 ScalingBehavior: DEFAULT Sharpness: 50 CodecSettings: Codec: GIF GifSettings: FramerateControl: SPECIFIED FramerateNumerator: 10 FramerateDenominator: 1 FramerateConversionAlgorithm: DUPLICATE_DROP OutputGroupSettings: Type: FILE_GROUP_SETTINGS FileGroupSettings: Destination: !Sub s3://${S3Bucket}/output/ DestinationSettings: S3Settings: StorageClass: STANDARD TimecodeConfig: Source: ZEROBASED StatusUpdateInterval: SECONDS_60 Tags: Cost: !Sub ${SystemName}-${SubName} DependsOn: - MediaConvertQueue # ------------------------------------------------------------# # Step Functions State Machine # ------------------------------------------------------------# StateMachineMp4GifConv: Type: AWS::StepFunctions::StateMachine Properties: StateMachineName: !Sub ${SystemName}-${SubName}-mp4-gif-conv StateMachineType: STANDARD DefinitionSubstitutions: DsSystemName: !Ref SystemName DsSubName: !Ref SubName DSRegion: !Ref AWS::Region DsAwsAccountId: !Ref AWS::AccountId DsMediaConvertJobTemplateArn: !GetAtt MediaConvertJobTemplate.Arn DsMediaConvertQueueArn: !GetAtt MediaConvertQueue.Arn DsSnsTopicArn: !GetAtt SNSTopic.TopicArn DefinitionString: |- { "StartAt": "CreateJob", "States": { "CreateJob": { "Type": "Task", "Resource": "arn:aws:states:::mediaconvert:createJob.sync", "Arguments": { "JobTemplate": "${DsMediaConvertJobTemplateArn}", "Queue": "${DsMediaConvertQueueArn}", "Role": "arn:aws:iam::${DsAwsAccountId}:role/service-role/MediaConvert_Default_Role", "Settings": { "Inputs": [ { "FileInput": "{% 's3://' & $states.input.detail.bucket.name & '/' & $states.input.detail.object.key %}" } ] }, "Tags": { "Cost": "${DsSystemName}-${DsSubName}" } }, "Next": "SnsPublish", "QueryLanguage": "JSONata", "TimeoutSeconds": 600 }, "SnsPublish": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "QueryLanguage": "JSONata", "Arguments": { "TopicArn": "${DsSnsTopicArn}", "Message": { "Input": "{% $states.input.Job.Settings.Inputs[0].FileInput %}", "Status": "{% $states.input.Job.Status %}", "Messages": "{% $states.input.Job.Messages %}" } }, "TimeoutSeconds": 30, "End": true } }, "TimeoutSeconds": 660, "Comment": "For converting a MP4 media to animated GIF" } LoggingConfiguration: Destinations: - CloudWatchLogsLogGroup: LogGroupArn: !GetAtt LogGroupStateMachineMp4GifConv.Arn IncludeExecutionData: true Level: ERROR RoleArn: !GetAtt StateMachineMp4GifConvRole.Arn TracingConfiguration: Enabled: false Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} DependsOn: - LogGroupStateMachineMp4GifConv - StateMachineMp4GifConvRole - MediaConvertJobTemplate # ------------------------------------------------------------# # Step Functions State Machine LogGroup (CloudWatch Logs) # ------------------------------------------------------------# LogGroupStateMachineMp4GifConv: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/vendedlogs/states/${SystemName}-${SubName}-mp4-gif-conv RetentionInDays: 365 Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} # ------------------------------------------------------------# # Step Functions State Machine Execution Role (IAM) # ------------------------------------------------------------# StateMachineMp4GifConvRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${SystemName}-${SubName}-StateMachineMp4GifConvRole Description: This role allows State Machines to invoke specified AWS resources. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: states.amazonaws.com Action: - sts:AssumeRole Path: /service-role/ ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess - arn:aws:iam::aws:policy/AWSElementalMediaConvertFullAccess Policies: - PolicyName: !Sub ${SystemName}-${SubName}-StateMachineMp4GifConvPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "s3:GetObject" Resource: - !Sub "${S3Bucket.Arn}/input/*" - Effect: Allow Action: - "s3:PutObject" Resource: - !Sub "${S3Bucket.Arn}/output/*" - Effect: Allow Action: - "events:PutTargets" - "events:PutRule" - "events:DescribeRule" Resource: - !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/StepFunctionsGetEventsForMediaConvertJobRule" - Effect: Allow Action: - "sns:Publish" Resource: - !GetAtt SNSTopic.TopicArn DependsOn: - S3Bucket - SNSTopic # ------------------------------------------------------------# # EventBridge Rule for starting State Machine # ------------------------------------------------------------# EventBridgeRuleStartSfn: Type: AWS::Events::Rule Properties: Name: !Sub ${SystemName}-${SubName}-mp4-gif-conv-start-sfn Description: !Sub This rule starts mp4 gif converter Sfn for ${SystemName}-${SubName}. The trigger is the S3 event notifications. EventBusName: !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/default" EventPattern: source: - "aws.s3" detail-type: - "Object Created" detail: bucket: name: - !Ref S3Bucket object: key: - wildcard: "input/*.mp4" State: ENABLED Targets: - Arn: !GetAtt StateMachineMp4GifConv.Arn Id: !Sub ${SystemName}-${SubName}-mp4-gif-conv-start-sfn RoleArn: !GetAtt EventBridgeRuleSfnRole.Arn DependsOn: - EventBridgeRuleSfnRole # ------------------------------------------------------------# # EventBridge Rule Invoke Step Functions State Machine Role (IAM) # ------------------------------------------------------------# EventBridgeRuleSfnRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${SystemName}-${SubName}-EventBridgeSfnRole Description: !Sub This role allows EventBridge to invoke mp4 gif converter Sfn for ${SystemName}-${SubName}. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: !Sub ${SystemName}-${SubName}-EventBridgeSfnPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "states:StartExecution" Resource: - !GetAtt StateMachineMp4GifConv.Arn DependsOn: - StateMachineMp4GifConv # ------------------------------------------------------------# # SNS Topic # ------------------------------------------------------------# SNSTopic: Type: AWS::SNS::Topic Properties: TracingConfig: PassThrough DisplayName: !Sub ${SystemName}-${SubName}-mp4-gif-conv FifoTopic: false Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName}   AWS Elemental MediaConvert は、前提として MediaConvert に割り当てる IAM ロールが必要になります。以下のドキュメント参考に作成が必要ですが、今回は雑に広い権限を作成をしています。以前は権限決め打ちで、かつ AWS アカウント共通で持たないといけなかった記憶がありますが、今は細かく定義できるようになっています。 Setting up IAM permissions - MediaConvert Set up an AWS Identity and Access Management (IAM) role to use with AWS Elemental MediaConvert. docs.aws.amazon.com AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a MediaConvert_Default_Role in your AWS account. It is needed when you create MediaConvert jobs. Resources: # ------------------------------------------------------------# # Elemental MediaConvert Default Role (IAM) # ------------------------------------------------------------# MediaConvertDefaultRole: Type: AWS::IAM::Role Properties: RoleName: MediaConvert_Default_Role Description: This role allows MediaConvert to execute jobs. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - mediaconvert.amazonaws.com Action: - sts:AssumeRole Path: /service-role/ ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonS3FullAccess - arn:aws:iam::aws:policy/AmazonAPIGatewayInvokeFullAccess   まとめ いかがでしたでしょうか? インターネット上では多くの方が似たようなハマりをしていますが、AWS Elemental MediaConvert に関する情報は少なかったので今回の件を私の方で書かせていただきました。 本記事が皆様のお役に立てれば幸いです。
アバター
こんにちは、広野です。 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。 Amazon S3 Vectors は、プレビュー時の 40 倍の規模で一般提供されています。 - AWS AWS の新機能についてさらに詳しく知るには、 Amazon S3 Vectors は、プレビュー時の 40 倍の規模で一般提供されています。 aws.amazon.com 以前は RAG 用のベクトルデータベースとして Amazon OpenSearch Service や Amazon Aurora など高額なデータベースサービスを使用しなければならなかったのですが、Amazon S3 ベースで安価に気軽に RAG 環境を作成できるようになったので嬉しいです。 それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。 内容が多いので、記事を 3つに分けます。本記事は UI 編です。 つくったもの こんな感じの RAG チャットボットです。 画面から問い合わせすると、回答が文字列細切れのストリームで返ってきます。それらをアプリ側で結合して、順次画面に表示しています。 回答の途中で、AI が回答生成の根拠にしたドキュメントの情報が送られてくることがあるので、あればそのドキュメント名を表示します。一般的には親切にドキュメントへのリンクも付いていると思うのですが、今回は簡略化のため省略しました。 このサンプル環境では、AWS が提供している AWS サービス別資料 の PDF を独自ドキュメントとして読み込ませ、回答生成に使用させています。 前回の記事 本記事はアーキテクチャ編、実装編の続編記事です。以下の記事をお読みの上、本記事にお戻りください。 React で Amazon Bedrock Knowledge Bases ベースの簡易 RAG チャットボットをつくる [2026年1月版] アーキテクチャ概要編 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。それを受けて、以前作成した RAG チャットボットをアレンジしてみました。 blog.usize-tech.com 2026.01.06 React で Amazon Bedrock Knowledge Bases ベースの簡易 RAG チャットボットをつくる [2026年1月版] 実装編 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。本記事は実装編です。 blog.usize-tech.com 2026.01.06   アーキテクチャ 図の左側半分、アプリ UI 基盤は以下の記事と全く同じです。お手数ですが内容についてはこちらをご覧ください。 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] アーキテクチャ概要編 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (初回) はアーキテクチャ概要編です。 blog.usize-tech.com 2025.07.30 図の右側半分、Lambda 関数から Bedrock ナレッジベースに問い合わせるところを今回新たに作成しています。 まず、RAG に必要な独自ドキュメントを用意し、ドキュメント用 S3 バケットに保存します。 S3 Vectors を使用して、Vector バケットとインデックスを作成します。 これら S3 リソースを Amazon Bedrock Knowledge Bases でナレッジベースとして関連付けます。 それができると、ドキュメント用 S3 バケットのドキュメント内容をベクトルデータに変換して S3 Vectors に保存してくれます。これを埋め込み (Embedding) と言います。埋め込みに使用する AI モデルは Amazon Titan Text Embeddings V2 を使用します。 Bedrock ナレッジベースが完成すると、それを使用して回答を生成する LLM を指定します。今回は Amazon Nova 2 Lite を使用します。Lambda 関数内でパラメータとして指定して、プロンプトとともに問い合わせることになります。 フロントエンドの UI は React で開発します。   UI について 開発環境 React 19.2.3 vite 7.3.0 ビルドツール @mui/material 7.3.6 UI モジュール @aws-amplify/ui-react 6.13.2 UI モジュール 画面イメージ 画面は一例です。途中、長すぎたのでカットしています。 少し React の画面パーツ的な意味で分類します。以下のような動きをします。 React の State で言うと、以下のようになります。 会話履歴: conversation 直近の回答: streaming 直近の質問: prompt アーキテクチャ概要編で紹介した通り、直近の回答部分 (streaming) が以下のデータ集計・変換処理を経て画面描画されています。 細かい説明は難しいので、次項でコードをまんま紹介しますが、以下の記事と重複する部分は割愛します。 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] 実装編3 React 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (4回目) は React アプリ開発編です。 blog.usize-tech.com 2025.08.16   React コード import { useState, useEffect, useRef } from "react"; import { Container, Grid, Box, Paper, Typography, TextField, Button, Avatar } from "@mui/material"; import SendIcon from '@mui/icons-material/Send'; import { blue, grey } from '@mui/material/colors'; import { v4 as uuidv4 } from "uuid"; import { events } from "aws-amplify/data"; import { inquireRagSr, Markdown } from "./Functions.jsx"; import Header from "../Header.jsx"; import Menu from "./Menu.jsx"; const RagSr = (props) => { //定数定義 const groups = props.groups; const sub = props.sub; const idtoken = props.idtoken; const imgUrl = import.meta.env.VITE_IMG_URL; //変数定義 const appsyncSessionIdRef = useRef(); //AppSync Events チャンネル用セッションID const bedrockSessionIdRef = useRef(null); //Bedrock Knowledge Bases 用セッションID const channelRef = useRef(); const streamingRefMap = useRef(new Map()); //state定義 const [prompt, setPrompt] = useState(""); const [conversation, setConversation] = useState([]); const [streaming, setStreaming] = useState({ text: "", refs: [] }); //RAGへの問い合わせ送信関数 const putRagSr = () => { if (streaming.text) { setConversation(prev => [ ...prev, { role:"ai", text: streaming.text, ref: streaming.refs }, { role:"user", text: prompt, ref: [] } ]); streamingRefMap.current.clear(); setStreaming({ text:"", refs:[] }); } else { setConversation(prev => [...prev, { role:"user", text: prompt, ref: [] }]); } inquireRagSr(prompt, appsyncSessionIdRef.current, bedrockSessionIdRef.current, idtoken); //プロンプト欄をクリア setPrompt(""); }; //URI整形関数 const normalizeLabel = (uri) => { try { return decodeURIComponent(uri.split("/").pop()); } catch { return uri; } }; //サブスクリプション開始関数 const startSubscription = async () => { const appsyncSessionId = appsyncSessionIdRef.current; if (channelRef.current) await channelRef.current.close(); const channel = await events.connect(`rag-stream-response/${sub}/${appsyncSessionId}`); channel.subscribe({ //ここで、AppSync Events からレスポンスストリームを受け取ったときの挙動を場合分けして定義している //動作を把握するため、各ケースにおいて console.log でログを表示している next: (data) => { //Bedrock Knowledge base の session id 取得 if (data.event.type === "bedrock_session") { console.log("=== Session received ==="); console.log(data.event.bedrock_session_id); bedrockSessionIdRef.current = data.event.bedrock_session_id; return; } //問い合わせに対する回答メッセージ (chunkされている) if (data.event.type === "text") { console.log("=== Message received ==="); setStreaming(s => ({ ...s, text: s.text + data.event.message })); return; } //回答に関する関連ドキュメント (citation) if (data.event.type === "citation") { console.log("=== Citation received ==="); console.log(data.event.citation); //citationに格納されるドキュメント名を取り出し、streamingRefMap に格納する //同じドキュメント名が届いたときは格納しないようチェックしている data.event.citation.forEach(ref => { const uri = ref.location?.s3Location?.uri; if (!uri) return; if (!streamingRefMap.current.has(uri)) { streamingRefMap.current.set(uri, { id: uri, label: normalizeLabel(uri) }); setStreaming(s => ({ ...s, refs: Array.from(streamingRefMap.current.values()) })); } }); } }, error: (err) => console.error("Subscription error:", err), complete: () => console.log("Subscription closed") }); channelRef.current = channel; }; //セッションIDのリセット、サブスクリプション再接続関数 const resetSession = async () => { appsyncSessionIdRef.current = uuidv4(); bedrockSessionIdRef.current = null; setPrompt(""); streamingRefMap.current.clear(); setStreaming({ text:"", refs:[] }); setConversation([]); await startSubscription(); }; //画面表示時 useEffect(() => { //画面表示時に最上部にスクロール window.scrollTo(0, 0); //Bedrockからのレスポンスサブスクライブ関数実行 appsyncSessionIdRef.current = uuidv4(); bedrockSessionIdRef.current = null; startSubscription(); //アンマウント時にチャンネルを閉じる return () => { if (channelRef.current) channelRef.current.close(); }; }, []); //Chatbot UI 会話部分 const renderMessage = (msg, idx) => ( <Box key={idx} sx={{display: "flex", justifyContent: msg.role === "user" ? "flex-end" : "flex-start", mb: 1, width: "100%"}}> {msg.role === "ai" && (<Avatar src={`${imgUrl}/images/ai_chat_icon.svg`} alt="AI" sx={{ mr: 2, mt: 2 }}/>)} <Paper elevation={2} sx={{p:2,my:1,maxWidth:"100%",bgcolor:msg.role === "user" ? blue[100] : grey[100]}}> <Markdown>{msg.text}</Markdown> {msg.ref.length > 0 && ( <> <h4>参考ドキュメント</h4> <ul style={{ paddingLeft: 20, margin: 0 }}> {msg.ref.map(s => ( <li key={s.id}>{s.label}</li> ))} </ul> </> )} </Paper> {msg.role === "user" && (<Avatar src={`${imgUrl}/images/human_chat_icon.svg`} alt="User" sx={{ml:2,mt:2}}/>)} </Box> ); return ( <> {/* Header */} <Header groups={groups} signOut={props.signOut} /> <Container maxWidth="lg" sx={{mt:2}}> <Grid container spacing={4}> {/* Menu Pane */} <Grid size={{xs:12,md:4}} order={{xs:2,md:1}}> {/* Sidebar */} <Menu /> </Grid> {/* Contents Pane IMPORTANT */} <Grid size={{xs:12,md:8}} order={{xs:1,md:2}} my={2}> <main> <Grid container spacing={2}> {/* Heading */} <Grid size={{xs:12}}> <Typography id="bedrocksrtop" variant="h5" component="h1" mb={3} gutterBottom>Amazon Bedrock RAG Stream Response テスト</Typography> </Grid> <Grid size={{xs:12}}> {/* Chatbot */} <Paper sx={{p:2,mb:2,width:"100%"}}> {/* あいさつ文(固定) */} {renderMessage({ role: "ai", text: "こんにちは。何かお困りですか?", ref: []}, -1)} {/* 会話履歴 */} {conversation.map((msg, idx) => renderMessage(msg, idx))} {/* 直近のレスポンス */} {streaming.text && renderMessage({ role:"ai", text: streaming.text, ref: streaming.refs }, "stream")} </Paper> {/* 入力エリア */} <Box sx={{display:"flex",gap:1}}> <TextField fullWidth multiline value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Type message here..." sx={{ flexGrow: 1 }} /> <Button variant="contained" size="small" onClick={putRagSr} disabled={!prompt} startIcon={<SendIcon />} sx={{ whiteSpace: "nowrap", flexShrink: 0 }}>送信</Button> </Box> {/* クリアボタン */} {(streaming.text || conversation.length > 0) && ( <Box sx={{ display: "flex", justifyContent: "flex-end", mt: 2 }}> <Button variant="contained" size="small" onClick={resetSession}>問い合わせをクリアする</Button> </Box> )} </Grid> </Grid> </main> </Grid> </Grid> </Container> </> ); }; export default RagSr;   バックエンドの Python コード 実装編の AWS CloudFormation に組み込まれていますが、以下のコードにより AWS AppSync Events チャンネルを介してアプリにストリームレスポンスを返します。 import os import json import boto3 import urllib.request from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest # common objects and valiables session = boto3.session.Session() bedrock_agent = boto3.client('bedrock-agent-runtime') endpoint = os.environ['APPSYNC_API_ENDPOINT'] model_arn = os.environ['MODEL_ARN'] knowledge_base_id = os.environ['KNOWLEDGE_BASE_ID'] region = os.environ['REGION'] service = 'appsync' headers = {'Content-Type': 'application/json'} # AppSync publish message function def publish_appsync_message(sub, appsync_session_id, payload, credentials): body = json.dumps({ "channel": f"rag-stream-response/{sub}/{appsync_session_id}", "events": [ json.dumps(payload) ] }).encode("utf-8") aws_request = AWSRequest( method='POST', url=endpoint, data=body, headers=headers ) SigV4Auth(credentials, service, region).add_auth(aws_request) req = urllib.request.Request( url=endpoint, data=aws_request.body, method='POST' ) for k, v in aws_request.headers.items(): req.add_header(k, v) with urllib.request.urlopen(req) as res: return res.read().decode('utf-8') # handler def lambda_handler(event, context): try: credentials = session.get_credentials().get_frozen_credentials() # API Gateway からのインプットを取得 prompt = event['body']['prompt'] appsync_session_id = event['body']['appsyncSessionId'] bedrock_session_id = event['body'].get('bedrockSessionId') sub = event['sub'] # Amazon Bedrock Knowledge Bases への問い合わせパラメータ作成 request = { "input": { "text": prompt }, "retrieveAndGenerateConfiguration": { "type": "KNOWLEDGE_BASE", "knowledgeBaseConfiguration": { "knowledgeBaseId": knowledge_base_id, "modelArn": model_arn, "generationConfiguration": { "inferenceConfig": { "textInferenceConfig": { "maxTokens": 10000, "temperature": 0.5, "topP": 0.9 } }, "performanceConfig": { "latency": "standard" } } } } } # Bedrock sessionId は存在するときのみ渡す (継続会話時のみ) if bedrock_session_id: request["sessionId"] = bedrock_session_id # Bedrock Knowledge Bases への問い合わせ response = bedrock_agent.retrieve_and_generate_stream(**request) # Bedrock sessionId if "sessionId" in response: publish_appsync_message( sub, appsync_session_id, { "type": "bedrock_session", "bedrock_session_id": response["sessionId"] }, credentials ) for chunk in response["stream"]: payload = None # Generated text if "output" in chunk and "text" in chunk["output"]: payload = { "type": "text", "message": chunk["output"]["text"] } print({"t": chunk["output"]["text"]}) # Citation elif "citation" in chunk: payload = { "type": "citation", "citation": chunk['citation']['retrievedReferences'] } print({"c": chunk['citation']['retrievedReferences']}) # Continue if not payload: continue # Publish AppSync publish_appsync_message(sub, appsync_session_id, payload, credentials) except Exception as e: print(str(e)) raise   まとめ いかがでしたでしょうか? Amazon Bedrock Knowledge Bases で RAG チャットボットを開発するときの UI 開発例を紹介しました。細かい説明はないので、コードの不明点は生成 AI に聞いてもらえると理解が進むと思います。 本記事が皆様のお役に立てれば幸いです。
アバター
こんにちは、広野です。 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。 Amazon S3 Vectors は、プレビュー時の 40 倍の規模で一般提供されています。 - AWS AWS の新機能についてさらに詳しく知るには、 Amazon S3 Vectors は、プレビュー時の 40 倍の規模で一般提供されています。 aws.amazon.com 以前は RAG 用のベクトルデータベースとして Amazon OpenSearch Service や Amazon Aurora など高額なデータベースサービスを使用しなければならなかったのですが、Amazon S3 ベースで安価に気軽に RAG 環境を作成できるようになったので嬉しいです。 それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。 内容が多いので、記事を 3つに分けます。本記事は実装編です。 つくったもの こんな感じの RAG チャットボットです。 画面から問い合わせすると、回答が文字列細切れのストリームで返ってきます。それらをアプリ側で結合して、順次画面に表示しています。 回答の途中で、AI が回答生成の根拠にしたドキュメントの情報が送られてくることがあるので、あればそのドキュメント名を表示します。一般的には親切にドキュメントへのリンクも付いていると思うのですが、今回は簡略化のため省略しました。 このサンプル環境では、AWS が提供している AWS サービス別資料 の PDF を独自ドキュメントとして読み込ませ、回答生成に使用させています。 前回の記事 本記事はアーキテクチャ概要編の続編記事です。以下の記事をお読みの上、本記事にお戻りください。 React で Amazon Bedrock Knowledge Bases ベースの簡易 RAG チャットボットをつくる [2026年1月版] アーキテクチャ概要編 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。それを受けて、以前作成した RAG チャットボットをアレンジしてみました。 blog.usize-tech.com 2026.01.06   アーキテクチャ 図の左側半分、アプリ UI 基盤は以下の記事と全く同じです。お手数ですが内容についてはこちらをご覧ください。 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] アーキテクチャ概要編 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (初回) はアーキテクチャ概要編です。 blog.usize-tech.com 2025.07.30 図の右側半分、Lambda 関数から Bedrock ナレッジベースに問い合わせるところを今回新たに作成しています。 まず、RAG に必要な独自ドキュメントを用意し、ドキュメント用 S3 バケットに保存します。 S3 Vectors を使用して、Vector バケットとインデックスを作成します。 これら S3 リソースを Amazon Bedrock Knowledge Bases でナレッジベースとして関連付けます。 それができると、ドキュメント用 S3 バケットのドキュメント内容をベクトルデータに変換して S3 Vectors に保存してくれます。これを埋め込み (Embedding) と言います。埋め込みに使用する AI モデルは Amazon Titan Text Embeddings V2 を使用します。 Bedrock ナレッジベースが完成すると、それを使用して回答を生成する LLM を指定します。今回は Amazon Nova 2 Lite を使用します。Lambda 関数内でパラメータとして指定して、プロンプトとともに問い合わせることになります。 フロントエンドの UI は React で開発します。   実装について まず、この基盤のデプロイについては以下の Amazon Bedrock 生成 AI チャットボットの環境がデプロイできていることが前提になっています。そこで紹介されている基盤に機能追加したイメージです。 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] 実装編1 Amazon Cognito 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (2回目) は Amazon Cognito 実装編です。 blog.usize-tech.com 2025.08.15 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] 実装編2 API作成 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (3回目) は API 作成編です。 blog.usize-tech.com 2025.08.15 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] 実装編3 React 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (4回目) は React アプリ開発編です。 blog.usize-tech.com 2025.08.16   リソースは基本 AWS CloudFormation でデプロイしています。上記に続く、追加分のテンプレートをこちらに記載します。インラインで説明をコメントします。 AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a S3 vector bucket and index as a RAG Knowledge base. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. use lower case only. (e.g. example) Default: example MaxLength: 10 MinLength: 1 SubName: Type: String Description: System sub name. use lower case only. (e.g. prod or dev) Default: dev MaxLength: 10 MinLength: 1 DomainName: Type: String Description: Domain name for URL. xxxxx.xxx (e.g. example.com) Default: example.com AllowedPattern: "[^\\s@]+\\.[^\\s@]+" SubDomainName: Type: String Description: Sub domain name for URL. (e.g. example-prod or example-dev) Default: example-dev MaxLength: 20 MinLength: 1 Dimension: Type: Number Description: The dimensions of the vectors to be inserted into the vector index. The value depends on the embedding model. Default: 1024 MaxValue: 4096 MinValue: 1 EmbeddingModelId: Type: String Description: The embedding model ID. Default: amazon.titan-embed-text-v2:0 MaxLength: 100 MinLength: 1 LlmModelId: Type: String Description: The LLM model ID for the Knowledge base. Default: global.amazon.nova-2-lite-v1:0 MaxLength: 100 MinLength: 1 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "General Configuration" Parameters: - SystemName - SubName - Label: default: "Domain Configuration" Parameters: - DomainName - SubDomainName - Label: default: "Embedding Configuration" Parameters: - Dimension - EmbeddingModelId - Label: default: "Knowledge Base Configuration" Parameters: - LlmModelId Resources: # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# # ドキュメント保存用 S3 汎用バケット S3BucketKbDatasource: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${SystemName}-${SubName}-kbdatasource PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true CorsConfiguration: CorsRules: - AllowedHeaders: - "*" AllowedMethods: - "GET" - "HEAD" - "PUT" - "POST" - "DELETE" AllowedOrigins: - !Sub https://${SubDomainName}.${DomainName} ExposedHeaders: - last-modified - content-type - content-length - etag - x-amz-version-id - x-amz-request-id - x-amz-id-2 - x-amz-cf-id - x-amz-storage-class - date - access-control-expose-headers MaxAge: 3000 Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} S3BucketPolicyKbDatasource: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref S3BucketKbDatasource PolicyDocument: Version: "2012-10-17" Statement: - Action: "s3:*" Effect: Deny Resource: - !Sub "arn:aws:s3:::${S3BucketKbDatasource}" - !Sub "arn:aws:s3:::${S3BucketKbDatasource}/*" Condition: Bool: "aws:SecureTransport": "false" Principal: "*" DependsOn: - S3BucketKbDatasource # S3 Vector バケット S3VectorBucket: Type: AWS::S3Vectors::VectorBucket Properties: VectorBucketName: !Sub ${SystemName}-${SubName}-vectordb # S3 Vector バケットに関連付けるインデックス S3VectorBucketIndex: Type: AWS::S3Vectors::Index Properties: IndexName: !Sub ${SystemName}-${SubName}-vectordb-index DataType: float32 Dimension: !Ref Dimension DistanceMetric: cosine VectorBucketArn: !GetAtt S3VectorBucket.VectorBucketArn MetadataConfiguration: NonFilterableMetadataKeys: - AMAZON_BEDROCK_TEXT - AMAZON_BEDROCK_METADATA DependsOn: - S3VectorBucket # ------------------------------------------------------------# # Bedrock Knowledge Base # ------------------------------------------------------------# # ここで、各種 S3 を 1つの Knowledge Base として関連付ける BedrockKnowledgeBase: Type: AWS::Bedrock::KnowledgeBase Properties: Name: !Sub ${SystemName}-${SubName}-kb Description: !Sub RAG Knowledge Base for ${SystemName}-${SubName} KnowledgeBaseConfiguration: Type: VECTOR VectorKnowledgeBaseConfiguration: EmbeddingModelArn: !Sub arn:aws:bedrock:${AWS::Region}::foundation-model/${EmbeddingModelId} RoleArn: !GetAtt IAMRoleBedrockKb.Arn StorageConfiguration: Type: S3_VECTORS S3VectorsConfiguration: IndexArn: !GetAtt S3VectorBucketIndex.IndexArn VectorBucketArn: !GetAtt S3VectorBucket.VectorBucketArn Tags: Cost: !Sub ${SystemName}-${SubName} DependsOn: - IAMRoleBedrockKb # ドキュメント保存用 S3 バケットはここで DataSource として関連付けないと機能しない BedrockKnowledgeBaseDataSource: Type: AWS::Bedrock::DataSource Properties: Name: !Sub ${SystemName}-${SubName}-kb-datasource Description: !Sub RAG Knowledge Base Data Source for ${SystemName}-${SubName} KnowledgeBaseId: !Ref BedrockKnowledgeBase DataDeletionPolicy: RETAIN DataSourceConfiguration: Type: S3 S3Configuration: BucketArn: !GetAtt S3BucketKbDatasource.Arn DependsOn: - S3BucketKbDatasource - BedrockKnowledgeBase # ------------------------------------------------------------# # AppSync Events # ------------------------------------------------------------# AppSyncChannelNamespaceRagSR: Type: AWS::AppSync::ChannelNamespace Properties: Name: rag-stream-response ApiId: Fn::ImportValue: !Sub AppSyncApiId-${SystemName}-${SubName} CodeHandlers: | import { util } from '@aws-appsync/utils'; export function onSubscribe(ctx) { const requested = ctx.info.channel.path; if (!requested.startsWith(`/rag-stream-response/${ctx.identity.sub}`)) { util.unauthorized(); } } Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} # ------------------------------------------------------------# # API Gateway REST API # ------------------------------------------------------------# RestApiRagSR: Type: AWS::ApiGateway::RestApi Properties: Name: !Sub rag-sr-${SystemName}-${SubName} Description: !Sub REST API to call Lambda rag-stream-response-${SystemName}-${SubName} EndpointConfiguration: Types: - REGIONAL IpAddressType: dualstack Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} RestApiDeploymentRagSR: Type: AWS::ApiGateway::Deployment Properties: RestApiId: !Ref RestApiRagSR DependsOn: - RestApiMethodRagSRPost - RestApiMethodRagSROptions RestApiStageRagSR: Type: AWS::ApiGateway::Stage Properties: StageName: prod Description: production stage RestApiId: !Ref RestApiRagSR DeploymentId: !Ref RestApiDeploymentRagSR MethodSettings: - ResourcePath: "/*" HttpMethod: "*" LoggingLevel: INFO DataTraceEnabled : true TracingEnabled: false AccessLogSetting: DestinationArn: !GetAtt LogGroupRestApiRagSR.Arn Format: '{"requestId":"$context.requestId","status":"$context.status","sub":"$context.authorizer.claims.sub","email":"$context.authorizer.claims.email","resourcePath":"$context.resourcePath","requestTime":"$context.requestTime","sourceIp":"$context.identity.sourceIp","userAgent":"$context.identity.userAgent","apigatewayError":"$context.error.message","authorizerError":"$context.authorizer.error","integrationError":"$context.integration.error"}' Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} RestApiAuthorizerRagSR: Type: AWS::ApiGateway::Authorizer Properties: Name: !Sub restapi-authorizer-ragsr-${SystemName}-${SubName} RestApiId: !Ref RestApiRagSR Type: COGNITO_USER_POOLS ProviderARNs: - Fn::ImportValue: !Sub CognitoArn-${SystemName}-${SubName} AuthorizerResultTtlInSeconds: 300 IdentitySource: method.request.header.Authorization RestApiResourceRagSR: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref RestApiRagSR ParentId: !GetAtt RestApiRagSR.RootResourceId PathPart: ragsr RestApiMethodRagSRPost: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref RestApiRagSR ResourceId: !Ref RestApiResourceRagSR HttpMethod: POST AuthorizationType: COGNITO_USER_POOLS AuthorizerId: !Ref RestApiAuthorizerRagSR Integration: Type: AWS IntegrationHttpMethod: POST Credentials: Fn::ImportValue: !Sub ApigLambdaInvocationRoleArn-${SystemName}-${SubName} Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaRagSR.Arn}/invocations" PassthroughBehavior: NEVER RequestTemplates: application/json: | { "body": $input.json('$'), "sub": "$context.authorizer.claims.sub" } RequestParameters: integration.request.header.X-Amz-Invocation-Type: "'Event'" IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'" method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'" method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'" ResponseTemplates: application/json: '' StatusCode: '202' MethodResponses: - StatusCode: '202' ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true RestApiMethodRagSROptions: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref RestApiRagSR ResourceId: !Ref RestApiResourceRagSR HttpMethod: OPTIONS AuthorizationType: NONE Integration: Type: MOCK Credentials: Fn::ImportValue: !Sub ApigLambdaInvocationRoleArn-${SystemName}-${SubName} IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'" method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'" method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'" ResponseTemplates: application/json: '' StatusCode: '200' PassthroughBehavior: WHEN_NO_MATCH RequestTemplates: application/json: '{"statusCode": 200}' MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true StatusCode: '200' # ------------------------------------------------------------# # API Gateway LogGroup (CloudWatch Logs) # ------------------------------------------------------------# LogGroupRestApiRagSR: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/apigateway/${RestApiRagSR} RetentionInDays: 365 Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaRagSR: Type: AWS::Lambda::Function Properties: FunctionName: !Sub rag-sr-${SystemName}-${SubName} Description: !Sub Lambda Function to invoke Bedrock Knowledge Bases for ${SystemName}-${SubName} Architectures: - x86_64 Runtime: python3.14 Timeout: 300 MemorySize: 128 Environment: Variables: APPSYNC_API_ENDPOINT: Fn::ImportValue: !Sub AppSyncEventsEndpointHttp-${SystemName}-${SubName} MODEL_ARN: !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/${LlmModelId}" KNOWLEDGE_BASE_ID: !Ref BedrockKnowledgeBase REGION: !Ref AWS::Region Role: !GetAtt LambdaBedrockKbRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} Code: ZipFile: | import os import json import boto3 import urllib.request from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest # common objects and valiables session = boto3.session.Session() bedrock_agent = boto3.client('bedrock-agent-runtime') endpoint = os.environ['APPSYNC_API_ENDPOINT'] model_arn = os.environ['MODEL_ARN'] knowledge_base_id = os.environ['KNOWLEDGE_BASE_ID'] region = os.environ['REGION'] service = 'appsync' headers = {'Content-Type': 'application/json'} # AppSync publish message function def publish_appsync_message(sub, appsync_session_id, payload, credentials): body = json.dumps({ "channel": f"rag-stream-response/{sub}/{appsync_session_id}", "events": [ json.dumps(payload) ] }).encode("utf-8") aws_request = AWSRequest( method='POST', url=endpoint, data=body, headers=headers ) SigV4Auth(credentials, service, region).add_auth(aws_request) req = urllib.request.Request( url=endpoint, data=aws_request.body, method='POST' ) for k, v in aws_request.headers.items(): req.add_header(k, v) with urllib.request.urlopen(req) as res: return res.read().decode('utf-8') # handler def lambda_handler(event, context): try: credentials = session.get_credentials().get_frozen_credentials() # API Gateway からのインプットを取得 prompt = event['body']['prompt'] # セッション ID はアーキテクチャ概要編で説明した通り2種類ある appsync_session_id = event['body']['appsyncSessionId'] bedrock_session_id = event['body'].get('bedrockSessionId') sub = event['sub'] # Amazon Bedrock Knowledge Bases への問い合わせパラメータ作成 request = { "input": { "text": prompt }, "retrieveAndGenerateConfiguration": { "type": "KNOWLEDGE_BASE", "knowledgeBaseConfiguration": { "knowledgeBaseId": knowledge_base_id, "modelArn": model_arn, "generationConfiguration": { "inferenceConfig": { "textInferenceConfig": { "maxTokens": 10000, "temperature": 0.5, "topP": 0.9 } }, "performanceConfig": { "latency": "standard" } } } } } # Bedrock sessionId は存在するときのみ渡す (継続会話時のみ) if bedrock_session_id: request["sessionId"] = bedrock_session_id # Bedrock Knowledge Bases への問い合わせ response = bedrock_agent.retrieve_and_generate_stream(**request) # Bedrock sessionId がレスポンスにあれば、AppSync に送る if "sessionId" in response: publish_appsync_message( sub, appsync_session_id, { "type": "bedrock_session", "bedrock_session_id": response["sessionId"] }, credentials ) for chunk in response["stream"]: payload = None # Generated text: チャンク分けされた回答メッセージが入る if "output" in chunk and "text" in chunk["output"]: payload = { "type": "text", "message": chunk["output"]["text"] } print({"t": chunk["output"]["text"]}) # Citation: 参考ドキュメントの情報が入る elif "citation" in chunk: payload = { "type": "citation", "citation": chunk['citation']['retrievedReferences'] } print({"c": chunk['citation']['retrievedReferences']}) # Continue if not payload: continue # Publish AppSync publish_appsync_message(sub, appsync_session_id, payload, credentials) except Exception as e: print(str(e)) raise DependsOn: - LambdaBedrockKbRole - BedrockKnowledgeBase LambdaRagSREventInvokeConfig: Type: AWS::Lambda::EventInvokeConfig Properties: FunctionName: !GetAtt LambdaRagSR.Arn Qualifier: $LATEST MaximumRetryAttempts: 0 MaximumEventAgeInSeconds: 300 # ------------------------------------------------------------# # Lambda Bedrock Knowledge Bases Invocation Role (IAM) # ------------------------------------------------------------# LambdaBedrockKbRole: Type: AWS::IAM::Role Properties: RoleName: !Sub LambdaBedrockKbRole-${SystemName}-${SubName} Description: This role allows Lambda functions to invoke Bedrock Knowledge Bases and AppSync Events API. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess Policies: - PolicyName: !Sub LambdaBedrockKbPolicy-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "bedrock:InvokeModel" - "bedrock:InvokeModelWithResponseStream" - "bedrock:GetInferenceProfile" - "bedrock:ListInferenceProfiles" Resource: - !Sub "arn:aws:bedrock:*::foundation-model/*" - !Sub "arn:aws:bedrock:*:${AWS::AccountId}:inference-profile/*" - Effect: Allow Action: - "bedrock:RetrieveAndGenerate" - "bedrock:Retrieve" Resource: - !GetAtt BedrockKnowledgeBase.KnowledgeBaseArn - Effect: Allow Action: - "appsync:connect" Resource: - Fn::ImportValue: !Sub AppSyncApiArn-${SystemName}-${SubName} - Effect: Allow Action: - "appsync:publish" - "appsync:EventPublish" Resource: - Fn::Join: - "" - - Fn::ImportValue: !Sub AppSyncApiArn-${SystemName}-${SubName} - /channelNamespace/rag-stream-response # ------------------------------------------------------------# # IAM Role for Bedrock Knowledge Base # ------------------------------------------------------------# IAMRoleBedrockKb: Type: AWS::IAM::Role Properties: RoleName: !Sub BedrockKbRole-${SystemName}-${SubName} AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Principal: Service: - bedrock.amazonaws.com Condition: StringEquals: "aws:SourceAccount": !Sub ${AWS::AccountId} ArnLike: "aws:SourceArn": !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:knowledge-base/*" Policies: - PolicyName: !Sub BedrockKbPolicy-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "s3:GetObject" - "s3:ListBucket" Resource: - !GetAtt S3BucketKbDatasource.Arn - !Sub ${S3BucketKbDatasource.Arn}/* Condition: StringEquals: "aws:ResourceAccount": - !Ref AWS::AccountId - Effect: Allow Action: - "s3vectors:GetIndex" - "s3vectors:QueryVectors" - "s3vectors:PutVectors" - "s3vectors:GetVectors" - "s3vectors:DeleteVectors" Resource: - !GetAtt S3VectorBucketIndex.IndexArn Condition: StringEquals: "aws:ResourceAccount": - !Ref AWS::AccountId - Effect: Allow Action: - "bedrock:InvokeModel" Resource: - !Sub arn:aws:bedrock:${AWS::Region}::foundation-model/${EmbeddingModelId} DependsOn: - S3BucketKbDatasource - S3VectorBucketIndex # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: # S3 S3BucketKbDatasourceName: Value: !Ref S3BucketKbDatasource # API Gateway APIGatewayEndpointRagSR: Value: !Sub https://${RestApiRagSR}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${RestApiStageRagSR}/ragsr Export: Name: !Sub RestApiEndpointRagSR-${SystemName}-${SubName}   今回、API Gateway を環境に追加しているので、以下の通り AWS Amplify の CloudFormation テンプレートも変更が入っています。 AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates an Amplify environment. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. use lower case only. (e.g. example) Default: example MaxLength: 10 MinLength: 1 SubName: Type: String Description: System sub name. use lower case only. (e.g. prod or dev) Default: dev MaxLength: 10 MinLength: 1 DomainName: Type: String Description: Domain name for URL. xxxxx.xxx (e.g. example.com) Default: example.com MaxLength: 40 MinLength: 5 SubDomainName: Type: String Description: Sub domain name for URL. (e.g. example-prod or example-dev) Default: example-dev MaxLength: 20 MinLength: 1 NodejsVersion: Type: String Description: The Node.js version for build phase. (e.g. v22.21.1 as of 2025-12-24) Default: v22.21.1 MaxLength: 10 MinLength: 6 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "General Configuration" Parameters: - SystemName - SubName - Label: default: "Domain Configuration" Parameters: - DomainName - SubDomainName - Label: default: "Application Configuration" Parameters: - NodejsVersion Resources: # ------------------------------------------------------------# # Amplify Role (IAM) # ------------------------------------------------------------# AmplifyRole: Type: AWS::IAM::Role Properties: RoleName: !Sub AmplifyExecutionRole-${SystemName}-${SubName} Description: This role allows Amplify to pull source codes from CodeCommit. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - amplify.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub AmplifyExecutionPolicy-${SystemName}-${SubName} PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/amplify/*" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" - Effect: Allow Resource: Fn::ImportValue: !Sub CodeCommitRepoArn-${SystemName}-${SubName} Action: - "codecommit:GitPull" # ------------------------------------------------------------# # Amplify Console # ------------------------------------------------------------# AmplifyConsole: Type: AWS::Amplify::App Properties: Name: !Sub ${SystemName}-${SubName} Description: !Sub Web App environment for ${SystemName}-${SubName} Repository: Fn::ImportValue: !Sub CodeCommitRepoUrl-${SystemName}-${SubName} AutoBranchCreationConfig: EnableAutoBranchCreation: false EnableAutoBuild: true EnablePerformanceMode: false EnableBranchAutoDeletion: false Platform: WEB BuildSpec: |- version: 1 frontend: phases: preBuild: commands: - npm ci build: commands: - npm run build - echo "VITE_BASE=$VITE_BASE" >> .env - echo "VITE_REGION=$VITE_REGION" >> .env - echo "VITE_USERPOOLID=$VITE_USERPOOLID" >> .env - echo "VITE_USERPOOLWEBCLIENTID=$VITE_USERPOOLWEBCLIENTID" >> .env - echo "VITE_IDPOOLID=$VITE_IDPOOLID" >> .env - echo "VITE_RESTAPIENDPOINTBEDROCKSR=$VITE_RESTAPIENDPOINTBEDROCKSR" >> .env - echo "VITE_RESTAPIENDPOINTRAGSR=$VITE_RESTAPIENDPOINTRAGSR" >> .env - echo "VITE_APPSYNCEVENTSHTTPENDPOINT=$VITE_APPSYNCEVENTSHTTPENDPOINT" >> .env - echo "VITE_IMG_URL=$VITE_IMG_URL" >> .env - echo "VITE_SUBNAME=$VITE_SUBNAME" >> .env artifacts: baseDirectory: /dist files: - '**/*' cache: paths: - node_modules/**/* CustomHeaders: !Sub |- customHeaders: - pattern: '**' headers: - key: 'Strict-Transport-Security' value: 'max-age=31536000; includeSubDomains' - key: 'X-Frame-Options' value: 'DENY' - key: 'X-XSS-Protection' value: '1; mode=block' - key: 'X-Content-Type-Options' value: 'nosniff' - key: 'Content-Security-Policy' value: >- default-src 'self' *.${DomainName} ${DomainName}; img-src 'self' *.${DomainName} ${DomainName} data: blob:; style-src 'self' *.${DomainName} ${DomainName} 'unsafe-inline' fonts.googleapis.com; font-src 'self' *.${DomainName} ${DomainName} fonts.gstatic.com; script-src 'self' *.${DomainName} ${DomainName} cdn.jsdelivr.net; script-src-elem 'self' *.${DomainName} ${DomainName} cdn.jsdelivr.net; connect-src 'self' *.${AWS::Region}.amazonaws.com *.${DomainName} ${DomainName} wss:; media-src 'self' *.${DomainName} ${DomainName} data: blob:; worker-src 'self' *.${DomainName} ${DomainName} data: blob:; - key: 'Cache-Control' value: 'no-store' CustomRules: - Source: /<*> Status: 404-200 Target: /index.html - Source: Status: 200 Target: /index.html EnvironmentVariables: - Name: VITE_BASE Value: / - Name: VITE_REGION Value: !Ref AWS::Region - Name: VITE_USERPOOLID Value: Fn::ImportValue: !Sub CognitoUserPoolId-${SystemName}-${SubName} - Name: VITE_USERPOOLWEBCLIENTID Value: Fn::ImportValue: !Sub CognitoAppClientId-${SystemName}-${SubName} - Name: VITE_IDPOOLID Value: Fn::ImportValue: !Sub CognitoIdPoolId-${SystemName}-${SubName} - Name: VITE_RESTAPIENDPOINTBEDROCKSR Value: Fn::ImportValue: !Sub RestApiEndpointBedrockSR-${SystemName}-${SubName} - Name: VITE_RESTAPIENDPOINTRAGSR Value: Fn::ImportValue: !Sub RestApiEndpointRagSR-${SystemName}-${SubName} - Name: VITE_APPSYNCEVENTSHTTPENDPOINT Value: Fn::ImportValue: !Sub AppSyncEventsEndpointHttp-${SystemName}-${SubName} - Name: VITE_IMG_URL Value: !Sub https://${SubDomainName}-img.${DomainName} - Name: VITE_SUBNAME Value: !Ref SubName - Name: _LIVE_UPDATES Value: !Sub '[{"name":"Node.js version","pkg":"node","type":"nvm","version":"${NodejsVersion}"}]' IAMServiceRole: !GetAtt AmplifyRole.Arn Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} AmplifyBranchProd: Type: AWS::Amplify::Branch Properties: AppId: !GetAtt AmplifyConsole.AppId BranchName: main Description: production EnableAutoBuild: true EnablePerformanceMode: false AmplifyDomainProd: Type: AWS::Amplify::Domain Properties: AppId: !GetAtt AmplifyConsole.AppId DomainName: !Ref DomainName CertificateSettings: CertificateType: CUSTOM CustomCertificateArn: Fn::ImportValue: !Sub AcmCertificateArn-${SystemName}-${SubName} SubDomainSettings: - BranchName: !GetAtt AmplifyBranchProd.BranchName Prefix: !Sub ${SystemName}-${SubName} EnableAutoSubDomain: false   続編記事 UI 編で、React による UI の開発例を紹介します。 React で Amazon Bedrock Knowledge Bases ベースの簡易 RAG チャットボットをつくる [2026年1月版] UI編 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。本記事は UI 編です。 blog.usize-tech.com 2026.01.06   まとめ いかがでしたでしょうか? Amazon Bedrock Knowledge Bases で RAG チャットボットを開発するときのアーキテクチャを AWS CloudFormation でデプロイする一例を紹介しました。 本記事が皆様のお役に立てれば幸いです。
アバター
こんにちは、広野です。 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。 Amazon S3 Vectors は、プレビュー時の 40 倍の規模で一般提供されています。 - AWS AWS の新機能についてさらに詳しく知るには、 Amazon S3 Vectors は、プレビュー時の 40 倍の規模で一般提供されています。 aws.amazon.com 以前は RAG 用のベクトルデータベースとして Amazon OpenSearch Service や Amazon Aurora など高額なデータベースサービスを使用しなければならなかったのですが、Amazon S3 ベースで安価に気軽に RAG 環境を作成できるようになったので嬉しいです。 それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。 内容が多いので、記事を 3つに分けます。本記事はアーキテクチャ概要編です。 つくったもの こんな感じの RAG チャットボットです。 画面から問い合わせすると、回答が文字列細切れのストリームで返ってきます。それらをアプリ側で結合して、順次画面に表示しています。 回答の途中で、AI が回答生成の根拠にしたドキュメントの情報が送られてくることがあるので、あればそのドキュメント名を表示します。一般的には親切にドキュメントへのリンクも付いていると思うのですが、今回は簡略化のため省略しました。 このサンプル環境では、AWS が提供している AWS サービス別資料 の PDF を独自ドキュメントとして読み込ませ、回答生成に使用させています。 ふりかえり・過去の関連記事 だいぶ前になりますが、Agents for Amazon Bedrock を使用した RAG チャットボットを作成したことがありました。 Amazon Bedrock RAG 環境用 AWS CloudFormation テンプレート series 1 VPC 編 Agents for Amazon Bedrock を使用した最小構成の RAG 環境を構築する AWS CloudFormation テンプレートを紹介します。3部構成になっており、本記事は1つ目、VPC 編です。 blog.usize-tech.com 2024.08.01 当時は Agents for Amazon Bedrock 経由でないと Amazon Bedrock Knowledge Bases にアクセスできず、回答待ち時間が長かったです。 ベクトルデータベースは Amazon Aurora Serverless なので比較的安価でしたが、データベース構築のオーバーヘッドがありました。 現在は Agents for Amazon Bedrock なしで Amazon Bedrock Knowledge Bases に直接問い合わせができるようになっています。また、冒頭で紹介した通りベクトルデータベースとして Amazon S3 が使用できるようになりました。レスポンスもかなり速くなりました。 Amazon Bedrock の LLM に単純に問い合わせるだけのチャットボットは、昨年度の最新アーキテクチャで作成しておりました。このアーキテクチャをそのまま活用し、過去の RAG チャットボットを改善したものを作成します。 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] アーキテクチャ概要編 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (初回) はアーキテクチャ概要編です。 blog.usize-tech.com 2025.07.30   アーキテクチャ 図の左側半分、アプリ UI 基盤は以下の記事と全く同じです。お手数ですが内容についてはこちらをご覧ください。 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] アーキテクチャ概要編 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (初回) はアーキテクチャ概要編です。 blog.usize-tech.com 2025.07.30 図の右側半分、Lambda 関数から Bedrock ナレッジベースに問い合わせるところを今回新たに作成しています。 まず、RAG に必要な独自ドキュメントを用意し、ドキュメント用 S3 バケットに保存します。 S3 Vectors を使用して、Vector バケットとインデックスを作成します。 これら S3 リソースを Amazon Bedrock Knowledge Bases でナレッジベースとして関連付けます。 それができると、ドキュメント用 S3 バケットのドキュメント内容をベクトルデータに変換して S3 Vectors に保存してくれます。これを埋め込み (Embedding) と言います。埋め込みに使用する AI モデルは Amazon Titan Text Embeddings V2 を使用します。 Bedrock ナレッジベースが完成すると、それを使用して回答を生成する LLM を指定します。今回は Amazon Nova 2 Lite を使用します。Lambda 関数内でパラメータとして指定して、プロンプトとともに問い合わせることになります。 フロントエンドの UI は React で開発します。   細かい話 この RAG チャットボットを作成するにあたり、苦労、工夫した点を挙げます。かなり細かい話です。上記のざっくりとしたアーキテクチャ図では表せない補足事項です。 継続問い合わせ時のセッション管理 いわゆる生成 AI チャットボットは、初回問い合わせの回答以降は AI が前の会話内容を覚えている状態で回答を考えてくれることが一般的です。 Amazon Bedrock Knowledge Bases はネイティブにその機能を持っていて、問い合わせを過去の問い合わせと関連付けさせるために会話にセッション ID を関連付けます。セッション ID は初回問い合わせ時に生成されたレスポンスに含まれ、2回目以降の問い合わせ時には問い合わせのパラメータに含める必要があります。 retrieve_and_generate_stream - Boto3 1.42.21 documentation boto3.amazonaws.com ややこしいですが、Amazon Bedrock Knowledge Bases が問い合わせを管理するセッションと、AWS AppSync Events チャンネルをサブスクライブするためのセッションという 2つのセッションがあり、それぞれアプリ側とバックエンド側で共通認識を持つ必要があります。一連の問い合わせであれば 2つのセッション ID をそれぞれ同じものを使用する必要があり、問い合わせをリセットする (新たな新規問い合わせにする) ときには両方のセッション ID をリセットする必要があります。 参考ドキュメントの取り扱い 参考ドキュメントの情報 (citation と呼びます) は Amazon Bedrock Knowledge Bases が問い合わせに対する回答文を作成し chunk 分けして五月雨式に送信してくる途中に入ってきます。ドキュメント名は重複することがあります。回答文章内のいくつかの異なる文面にそれぞれ参考ドキュメントがあったとしても、それらが同じドキュメントである可能性があるからです。 そのため、生成された citation 情報の重複排除と、受け取ったら UI に表示する処理をリアルタイムに行う必要があります。これをバックエンド側で一時的にバッファして処理させようとしてもうまくいかなかったので、アプリ側で処理しています。バックエンド側はとにかくアプリに忠実に chunk を送りつけることに専念させています。 当初、回答文字列 (text) と参考ドキュメント (citation) を React 別々の変数 (React 的には State) に格納していましたが、それだと画面更新に競合が発生し、うまくいきませんでした。text と citation は同じ一連のレスポンスストリームとして送られてくるので、同じストリームによる画面更新は 1つの State (上の図では streaming) で管理するべきと考えました。   実装について まず、この基盤のデプロイについては以下の Amazon Bedrock 生成 AI チャットボットの環境がデプロイできていることが前提になっています。そこで紹介されている基盤に機能追加したイメージです。 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] 実装編1 Amazon Cognito 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (2回目) は Amazon Cognito 実装編です。 blog.usize-tech.com 2025.08.15 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] 実装編2 API作成 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (3回目) は API 作成編です。 blog.usize-tech.com 2025.08.15 React で Amazon Bedrock ベースの簡易生成 AI チャットボットをつくる [2025年7月版] 実装編3 React 生成 AI 界隈の技術の進化はすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。今回 (4回目) は React アプリ開発編です。 blog.usize-tech.com 2025.08.16   新たな RAG チャットボットの基盤については実装編、アプリ UI については UI 編の続編記事でそれぞれ紹介します。 React で Amazon Bedrock Knowledge Bases ベースの簡易 RAG チャットボットをつくる [2026年1月版] 実装編 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。本記事は実装編です。 blog.usize-tech.com 2026.01.06 React で Amazon Bedrock Knowledge Bases ベースの簡易 RAG チャットボットをつくる [2026年1月版] UI編 AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。本記事は UI 編です。 blog.usize-tech.com 2026.01.06   まとめ いかがでしたでしょうか? Amazon Bedrock Knowledge Bases で RAG チャットボットを開発するときのアーキテクチャを紹介しました。 本記事が皆様のお役に立てれば幸いです。
アバター
当記事は、 日常の運用業務(NW機器設定)の自動化 により、 運用コストの削減 および 運用品質の向上  を目標に 「Ansible」 を使用し、様々なNW機器設定を自動化してみようと 試みた記事です。 Ansibleは、OSS版(AWX)+OSS版(Ansible)を使用しております。   PaloAltoの「セキュリティポリシー」の登録/変更/削除を実施してみた 事前設定 Templateを作成し、インベントリーと認証情報を設定する。 インベントリー:対象機器(ホスト)の接続先を設定。 ※ホストには以下変数で接続先IPを指定 ansible_host: xxx.xxx.xxx.xxx 認証情報:対象機器へのログイン情報(ユーザ名/パスワード)を設定。 ユーザ名は  変数:ansible_user   に保持される パスワードは 変数:ansible_password に保持される 各設定値(送信元/宛先/サービス)は、以下の関連記事で登録された値を使用します。※参考にして下さい ・ Ansibleを使用してNW機器設定を自動化する(PaloAlto-アドレス編①) ・ Ansibleを使用してNW機器設定を自動化する(PaloAlto-アドレスグループ編①) ・ Ansibleを使用してNW機器設定を自動化する(PaloAlto-サービス編①) ・ Ansibleを使用してNW機器設定を自動化する(PaloAlto-サービスグループ編①)   Playbook作成(YAML) 使用モジュール paloaltonetworks.panos.panos_security_rule  を使用。 ※参考ページ: Ansible Galaxy galaxy.ansible.com   接続情報(provider)の設定 providerには、ip_address/username/password の指定が必要。 vars: provider:   ip_address: '{{ ansible_host }}'   ← インベントリーのホストで設定   username: '{{ ansible_user }}'    ← 認証情報で設定   password: '{{ ansible_password }}'  ← 認証情報で設定   セキュリティポリシーの登録 接続情報とポリシー(送信元/宛先/サービス)を指定して登録( state: ‘present’ )を行う。 - name: Security_Policy Present paloaltonetworks.panos.panos_security_rule: provider: '{{ provider }}'   ← 接続情報 rule_name: 'policy001' description: 'policy001' source_zone: 'trust'    ← 送信元ゾーン source_ip: 'test_address001'    ← 送信元アドレス destination_zone: 'trust'    ← 宛先ゾーン destination_ip: 'test_address002'    ← 宛先アドレス service: 'test_service001'    ← サービス action: 'allow' state: 'present' register: wk_result_data 実行結果:対象のセキュリティポリシーが登録された。 ※Ansibleの実行結果(diff)を抜粋 "before": "", "after" : "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<entry name=\"policy001\">\n\t<from>\n\t\t<member>trust</member>\n\t</from>\n\t<to>\n\t\t<member>trust</member>\n\t</to>\n\t<source>\n\t\t<member>test_address001</member>\n\t</source>\n\t<source-user>\n\t\t<member>any</member>\n\t</source-user>\n\t<hip-profiles>\n\t\t<member>any</member>\n\t</hip-profiles>\n\t<destination>\n\t\t<member>test_address002</member>\n\t</destination>\n\t<application>\n\t\t<member>any</member>\n\t</application>\n\t<service>\n\t\t<member>test_service001</member>\n\t</service>\n\t<category>\n\t\t<member>any</member>\n\t</category>\n\t<action>allow</action>\n\t<log-start>no</log-start>\n\t<log-end>yes</log-end>\n\t<description>policy001</description>\n\t<rule-type>universal</rule-type>\n\t<negate-source>no</negate-source>\n\t<negate-destination>no</negate-destination>\n\t<disabled>no</disabled>\n\t<option>\n\t\t<disable-server-response-inspection>no</disable-server-response-inspection>\n\t</option>\n</entry>\n"   セキュリティポリシーの登録 ※セキュリティポリシーが既に存在する場合 接続情報とポリシー(送信元/宛先/サービス)を指定して登録( state: ‘present’ )を行う。  ※セキュリティポリシーが既に存在する場合は、既存設定の置き換えとなる(state: ‘replaced’と同様) - name: Security_Policy Present paloaltonetworks.panos.panos_security_rule: provider: '{{ provider }}'   ← 接続情報 rule_name: 'policy001' description: 'policy001' source_zone: 'trust'    ← 送信元ゾーン source_ip: 'test_address001,test_addressgroup001'    ← 送信元アドレス destination_zone: 'trust'    ← 宛先ゾーン destination_ip: 'test_address002,test_addressgroup001'    ← 宛先アドレス service: 'test_service001,test_servicegroup001'    ← サービス action: 'allow' state: 'present' register: wk_result_data 実行結果:対象のセキュリティポリシーが登録された。 ※Ansibleの実行結果(diff)を抜粋 "before": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<entry name=\"policy001\">\n\t<from>\n\t\t<member>trust</member>\n\t</from>\n\t<to>\n\t\t<member>trust</member>\n\t</to>\n\t<source>\n\t\t<member>test_address001</member>\n\t</source>\n\t<source-user>\n\t\t<member>any</member>\n\t</source-user>\n\t<hip-profiles>\n\t\t<member>any</member>\n\t</hip-profiles>\n\t<destination>\n\t\t<member>test_address002</member>\n\t</destination>\n\t<application>\n\t\t<member>any</member>\n\t</application>\n\t<service>\n\t\t<member>test_service001</member>\n\t</service>\n\t<category>\n\t\t<member>any</member>\n\t</category>\n\t<action>allow</action>\n\t<log-start>no</log-start>\n\t<log-end>yes</log-end>\n\t<description>policy001</description>\n\t<rule-type>universal</rule-type>\n\t<negate-source>no</negate-source>\n\t<negate-destination>no</negate-destination>\n\t<disabled>no</disabled>\n\t<option>\n\t\t<disable-server-response-inspection>no</disable-server-response-inspection>\n\t</option>\n</entry>\n", "after" : "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<entry name=\"policy001\">\n\t<from>\n\t\t<member>trust</member>\n\t</from>\n\t<to>\n\t\t<member>trust</member>\n\t</to>\n\t<source>\n\t\t<member>test_address001</member>\n\t\t<member>test_addressgroup001</member>\n\t</source>\n\t<source-user>\n\t\t<member>any</member>\n\t</source-user>\n\t<hip-profiles>\n\t\t<member>any</member>\n\t</hip-profiles>\n\t<destination>\n\t\t<member>test_address002</member>\n\t\t<member>test_addressgroup001</member>\n\t</destination>\n\t<application>\n\t\t<member>any</member>\n\t</application>\n\t<service>\n\t\t<member>test_service001</member>\n\t\t<member>test_servicegroup001</member>\n\t</service>\n\t<category>\n\t\t<member>any</member>\n\t</category>\n\t<action>allow</action>\n\t<log-start>no</log-start>\n\t<log-end>yes</log-end>\n\t<description>policy001</description>\n\t<rule-type>universal</rule-type>\n\t<negate-source>no</negate-source>\n\t<negate-destination>no</negate-destination>\n\t<disabled>no</disabled>\n\t<option>\n\t\t<disable-server-response-inspection>no</disable-server-response-inspection>\n\t</option>\n</entry>\n"   セキュリティポリシーの変更 ※登録のつづき 接続情報とポリシー(送信元/宛先/サービス)を指定して変更( state: ‘replaced’ )を行う。  ※replacedの場合は、既存設定の置き換えとなる - name: Security_Policy Replaced paloaltonetworks.panos.panos_security_rule: provider: '{{ provider }}'   ← 接続情報 rule_name: 'policy001' description: 'policy001' source_zone: 'trust'    ← 送信元ゾーン source_ip: 'test_address001,test_addressgroup001'    ← 送信元アドレス destination_zone: 'trust'    ← 宛先ゾーン destination_ip: 'test_address002,test_addressgroup001'    ← 宛先アドレス service: 'test_service001,test_servicegroup001'    ← サービス action: 'allow' state: 'replaced' register: wk_result_data 実行結果:対象のセキュリティポリシーが登録された。 ※Ansibleの実行結果(diff)を抜粋 "before": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<entry name=\"policy001\">\n\t<from>\n\t\t<member>trust</member>\n\t</from>\n\t<to>\n\t\t<member>trust</member>\n\t</to>\n\t<source>\n\t\t<member>test_address001</member>\n\t</source>\n\t<source-user>\n\t\t<member>any</member>\n\t</source-user>\n\t<hip-profiles>\n\t\t<member>any</member>\n\t</hip-profiles>\n\t<destination>\n\t\t<member>test_address002</member>\n\t</destination>\n\t<application>\n\t\t<member>any</member>\n\t</application>\n\t<service>\n\t\t<member>test_service001</member>\n\t</service>\n\t<category>\n\t\t<member>any</member>\n\t</category>\n\t<action>allow</action>\n\t<log-start>no</log-start>\n\t<log-end>yes</log-end>\n\t<description>policy001</description>\n\t<rule-type>universal</rule-type>\n\t<negate-source>no</negate-source>\n\t<negate-destination>no</negate-destination>\n\t<disabled>no</disabled>\n\t<option>\n\t\t<disable-server-response-inspection>no</disable-server-response-inspection>\n\t</option>\n</entry>\n", "after" : "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<entry name=\"policy001\">\n\t<from>\n\t\t<member>trust</member>\n\t</from>\n\t<to>\n\t\t<member>trust</member>\n\t</to>\n\t<source>\n\t\t<member>test_address001</member>\n\t\t<member>test_addressgroup001</member>\n\t</source>\n\t<source-user>\n\t\t<member>any</member>\n\t</source-user>\n\t<hip-profiles>\n\t\t<member>any</member>\n\t</hip-profiles>\n\t<destination>\n\t\t<member>test_address002</member>\n\t\t<member>test_addressgroup001</member>\n\t</destination>\n\t<application>\n\t\t<member>any</member>\n\t</application>\n\t<service>\n\t\t<member>test_service001</member>\n\t\t<member>test_servicegroup001</member>\n\t</service>\n\t<category>\n\t\t<member>any</member>\n\t</category>\n\t<action>allow</action>\n\t<log-start>no</log-start>\n\t<log-end>yes</log-end>\n\t<description>policy001</description>\n\t<rule-type>universal</rule-type>\n\t<negate-source>no</negate-source>\n\t<negate-destination>no</negate-destination>\n\t<disabled>no</disabled>\n\t<option>\n\t\t<disable-server-response-inspection>no</disable-server-response-inspection>\n\t</option>\n</entry>\n"   接続情報とポリシー(送信元/宛先/サービス)を指定して変更( state: ‘merged’ )を行う。 - name: Security_Policy Merged paloaltonetworks.panos.panos_security_rule: provider: '{{ provider }}'   ← 接続情報 rule_name: 'policy001' description: 'policy001' source_zone: 'trust'    ← 送信元ゾーン source_ip: 'test_address003'    ← 送信元アドレス destination_zone: 'trust'    ← 宛先ゾーン destination_ip: 'test_address003'    ← 宛先アドレス service: 'test_service003'    ← サービス action: 'allow' state: 'merged' register: wk_result_data 実行結果:エラーとなり変更処理はできない。 ※変更は、state:present/replacedで実施する必要あり。。。要注意!!   "msg": "Failed update source_devices" ※msgの抜粋   セキュリティポリシーの削除 ※変更のつづき 接続情報とポリシーを指定して削除( state: ‘absent’ )を行う。 - name: Security_Policy Absent paloaltonetworks.panos.panos_security_rule: provider: '{{ provider }}'   ← 接続情報 rule_name: 'policy001' state: 'absent' register: wk_result_data 実行結果:対象のセキュリティポリシーが削除された。 ※Ansibleの実行結果(diff)を抜粋 "before": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<entry name=\"policy001\">\n\t<from>\n\t\t<member>trust</member>\n\t</from>\n\t<to>\n\t\t<member>trust</member>\n\t</to>\n\t<source>\n\t\t<member>test_address001</member>\n\t\t<member>test_addressgroup001</member>\n\t\t<member>test_address003</member>\n\t</source>\n\t<source-user>\n\t\t<member>any</member>\n\t</source-user>\n\t<hip-profiles>\n\t\t<member>any</member>\n\t</hip-profiles>\n\t<destination>\n\t\t<member>test_address002</member>\n\t\t<member>test_addressgroup001</member>\n\t\t<member>test_address003</member>\n\t</destination>\n\t<application>\n\t\t<member>any</member>\n\t</application>\n\t<service>\n\t\t<member>test_service001</member>\n\t\t<member>test_servicegroup001</member>\n\t\t<member>test_service003</member>\n\t</service>\n\t<category>\n\t\t<member>any</member>\n\t</category>\n\t<action>allow</action>\n\t<log-start>no</log-start>\n\t<log-end>yes</log-end>\n\t<description>policy001</description>\n\t<rule-type>universal</rule-type>\n\t<negate-source>no</negate-source>\n\t<negate-destination>no</negate-destination>\n\t<disabled>no</disabled>\n\t<option>\n\t\t<disable-server-response-inspection>no</disable-server-response-inspection>\n\t</option>\n</entry>\n", "after" : ""   最後に 「Ansible」の「paloaltonetworks.panos.panos_security_rule」を使用し、「セキュリティポリシー」の登録/変更/削除ができたことは良かった。何らかの変更申請の仕組みと連携することで、より 設定変更の自動化 が活用できるようになると考える。 現状 設定情報がベタ書きで使い勝手が悪いので、今後 設定内容をINPUTする仕組みを試みたいと思います。 また、引続き 他にも様々なNW機器設定を自動化してみようと思います。
アバター
あけましておめでとうございます。SCSKでZabbixの研究をしている小寺です。               Zabbixを導入する際、誰もが一度は悩むのが 「バックエンドデータベースに何を採用するか」 という問題です。特に代表的なOSSであるMySQLとPostgreSQLは、どちらも実績が豊富で選択に迷うところです。 今回は、最新のZabbix 7.0環境において、MySQLとPostgreSQLのパフォーマンスを徹底比較しました。デフォルト状態(チューニングなし)とチューニング後の挙動の違いに注目して検証結果をお届けします。          1. 検証環境の構成 今回の検証では、AWS上のEC2インスタンス(m5.large)を使用し、OSやZabbixのバージョンを統一して比較を行いました。 サーバー構成 項目 MySQL 環境 PostgreSQL 環境 OS RHEL 9.6 RHEL 9.6 Zabbix バージョン 7.0.22 7.0.22 DB バージョン MySQL 8.0.44 PostgreSQL 13.22 インスタンスタイプ m5.large (2 vCPU / 8GB RAM) m5.large (2 vCPU / 8GB RAM) ディスク (EBS) gp3 (3000 IOPS) gp3 (3000 IOPS) 監視負荷設定 中規模〜大規模環境を想定した負荷をかけています。 監視ホスト数: 751 ホスト 1秒あたりの監視項目数 (NVPS): 約 1,090 NVPS アイテム数(Zabbixエージェント): 38,500 アイテム アイテム数(SNMPポーリング): 33,500 アイテム 2. 検証の着目ポイント(評価指標) DBの性能差を測るため、以下の5つの指標をモニタリングしました。 ロードアベレージ (Load Average): システム全体の負荷。 Disk Utilization (ディスク使用率): ディスクのビジー率。 History Syncer の使用率: DBへのデータ書き込みプロセスの負荷。 Configuration Syncer の使用率: 監視設定の読み込みプロセスの負荷。 Housekeeper の実行時間: 不要データの削除にかかる時間。 3. 【検証結果】デフォルト状態(チューニングなし) インストール直後の初期設定状態で計測した結果です。 評価項目 MySQL 8.0 PostgreSQL 13 ロードアベレージ 0.99 0.42 Disk Utilization 28% 13% History Syncer 使用率 5.6% 3.1% Configuration Syncer 使用率 1.4% 1.2% Housekeeper 実行時間 351s (300万件) 84s (299万件)  MySQLはデフォルト状態だと本来の性能が出にくい という特徴が顕著に現れました。特にHousekeeper実行時の負荷がロードアベレージを押し上げています。対してPostgreSQLは、デフォルト設定でも削除処理が非常に高速に完了しています。 4. 【検証結果】チューニング実施後 チューニング適用後の結果です。 評価項目 MySQL 8.0 PostgreSQL 13 ロードアベレージ 0.60 0.39 Disk Utilization 15% 9.5% History Syncer 使用率 4.0% 3.0% Configuration Syncer 使用率 1.0% 1.0% Housekeeper 実行時間 152s (268万件)  77s (263万件)  チューニングにより、MySQLのHousekeeper時間は大幅に改善されました。ここで注目すべきは、 削除処理(Housekeeper)を除けば、MySQLもPostgreSQLもほぼ同等の性能である という点です。History SyncerやConfiguration Syncerの数値を見れば、データの読み書き自体のポテンシャルに大きな差はないことがわかります。 考察:なぜ処理速度に差が出たのか 削除処理における性能差について、それぞれのアーキテクチャから深掘りします。 PostgreSQL:高速さの裏にある「論理削除」と「バキューム」 PostgreSQLの DELETE が高速なのは、データを即座に物理削除するのではなく、削除フラグを立てるだけの 「論理削除(MVCCアーキテクチャ)」 を採用しているためです。ディスクI/Oを後回しにするため見かけ上の速度は非常に速くなります。 ただし、この方式では「不要領域(デッドタプル)」がディスクに蓄積されます。これを放置するとDBが肥大化し性能が低下するため、定期的に VACUUM(バキューム)処理 を行い、領域を再利用可能な状態にする運用設計が不可欠です。 MySQL:物理削除の負荷と「パーティショニング」による最適化 MySQL(InnoDB)の DELETE はデータを物理的に整理しながら削除するため、大量のデータを消す際はI/O負荷が高くなります。デフォルト状態で性能が出にくいと言われる要因の一つです。 これを解決するのが 「パーティショニング」 です。データを期間ごとに区切り、パーティションごと切り離す(DROPする)ことで、物理削除の負荷を回避し一瞬で処理を終えることができます。 なお、パーティショニングの設定は専門的な知識が必要ですが、 Zabbixの公式サポート契約 を締結されている場合、SCSKから最適なパーティショニングの設定方法や運用スクリプトをご案内することが可能です。大規模運用の際は、こうしたサポートの活用が非常に有効です。 まとめ 今回の比較検証をまとめます。 削除以外は互角の性能: MySQLもPostgreSQLも、監視データの書き込みや読み込み性能は非常に高く、拮抗しています。 PostgreSQLは「論理削除」: 削除は速いが、その後の VACUUM 管理を適切に行う運用設計が求められます。 MySQLは「パーティション」: デフォルトの DELETE には限界がありますが、パーティショニングによって弱点を克服できます。Zabbixサポート契約があれば、その設定方法の案内を受けられるため安心です。 結論: 「削除が速いからPostgreSQL」と単純に決めるのではなく、自組織の運用で「VACUUMの最適化」と「パーティショニングの導入(+サポート活用)」のどちらが適しているかを検討することが、安定稼働への近道です。 SCSKでは、Zabbixの導入支援から高度なチューニングまで幅広くサポートしています。DB選定やパーティショニング設定にお悩みの方は、ぜひお気軽にご相談ください!                  SCSK Plus サポート for Zabbix SCSK Plus サポート for Zabbix 世界で最も人気のあるオープンソース統合監視ツール「Zabbix」の導入構築から運用保守までSCSKが強力にサポートします。 www.scsk.jp ★YouTubeに、SCSK Zabbixチャンネルを開設しました!★ SCSK Zabbixチャンネル SCSK Zabbixチャンネルでは、Zabbixのトレンドや実際の導入事例を動画で解説。明日から使える実践的な操作ナレッジも提供しており、監視業務の効率化に役立つヒントが満載です。 最新のトピックについては、リンクの弊社HPもしくはXアカ... www.youtube.com ★X(旧Twitter)に、SCSK Zabbixアカウントを開設しました!★ https://x.com/SCSK_Zabbix x.com
アバター
今回は、AWS Systems Manager を使用した Amazon EC2 インスタンスの即時バックアップシステムを AWS CDK で実装する方法をまとめました。 同様にバックアップ機能を提供しているAWS Backupは優れたサービスですが、スケジュール出来る時間には幅があり、特定の時間にバックアップを開始するようスケジュールすることができません。 一方、今回のSSMバックアップシステムは以下の特徴があります。 柔軟なスケジュール設定  – 分単位での細かい実行スケジュール調整 カスタム処理の組み込み  – 独自の前後処理やチェック機能 詳細な制御 – 再起動有無の制御、AMI世代管理のカスタマイズ はじめに 今回は、AWS Systems Manager を使用して、EC2インスタンスの 即時バックアップ をAWS CDKで実装していきます。AWS Systems Managerを利用したバックアップは、緊急時の即座な対応から定期的な運用まで、AWS Backupでは実現できない柔軟性と即時性を備えています。 また、Sytems Managerを利用するためSSM Agentの死活監視についても実装をしていきます。 今回作成するリソース SNSトピック : バックアップ失敗通知とSSM Agent監視 IAMロール : SSM Automation、Lambda実行権限 Lambda関数 : 世代管理とSSM Agent監視 SSMドキュメント : カスタムバックアップドキュメント メンテナンスウィンドウ : スケジュール実行設定 EventBridge : 失敗検知と事前監視 アーキテクチャ概要   AWS CDK ソースコード SNS通知設定 const emailAddresses = [  // SNS通知先メーリングリスト 'xxxxxxxx@example.com', 'xxxxxxx@example.com', ]; const backupTopic = new sns.Topic(this, 'BackupTopic', {  // バックアップ失敗通知用のトピック topicName: 'sns-backup-alertnotification', displayName: 'Backup Alert Notifications' }); emailAddresses.forEach(email => { backupTopic.addSubscription( new subscriptions.EmailSubscription(email) ); }); backupTopic.addToResourcePolicy( // トピックポリシー追加1 new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'sns:GetTopicAttributes', 'sns:SetTopicAttributes', 'sns:AddPermission', 'sns:RemovePermission', 'sns:DeleteTopic', 'sns:Subscribe', 'sns:Publish', ], resources: [backupTopic.topicArn], principals: [new iam.AnyPrincipal()], conditions: { StringEquals: { 'aws:SourceOwner': cdk.Stack.of(this).account } } }) ); backupTopic.addToResourcePolicy( // トピックポリシー追加2 new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'sns:Publish', ], resources: [backupTopic.topicArn], principals: [new iam.ServicePrincipal('events.amazonaws.com')], }) ); ポイント: 複数の管理者への通知配信 アラーム発生時に通知するメールアドレスを指定 IAMロール設定 //=========================================== // Automationタスク用IAMロール作成 //=========================================== const ssmBackupRole = new iam.Role(this, 'SSMBackupRole', { roleName: 'SSMAutomationRole', assumedBy: new iam.CompositePrincipal( new iam.ServicePrincipal('ec2.amazonaws.com'), new iam.ServicePrincipal('ssm.amazonaws.com') ), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonSSMMaintenanceWindowRole'), // AWS管理ポリシー追加 iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore') ] }); ssmBackupRole.addManagedPolicy(              // IAMポリシー追加1 iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')   // 基本的な権限(logs:CreateLogGroup、logs:CreateLogStream、logs:PutLogEvents)を含むポリシー「AWSLambdaBasicExecutionRole」をアタッチ ); new iam.Policy(this, 'SSMBackupPolicy', {               // IAMポリシー追加2 policyName: 'iam-policy-for-ssm-backup', roles: [ssmBackupRole], statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "ec2:CreateTags", "ec2:CreateImage", "ec2:DescribeImages", "ec2:DescribeTags", "ec2:DescribeInstances", "ec2:DescribeInstanceStatus", "ec2:DescribeSnapshots", "ec2:StopInstances", "ec2:StartInstances" ], resources: ['*'] }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "lambda:InvokeFunction" ], resources: ["*"] }) ] }); //=========================================== // Lambda実行用IAMロール作成 //=========================================== // AMI世代管理(Lambda実行用)IAMロール作成 const lambdaBackupRole = new iam.Role(this, 'LambdaBackupRole', { roleName: 'LambdaBackupRole', assumedBy: new iam.CompositePrincipal( new iam.ServicePrincipal('lambda.amazonaws.com'), new iam.ServicePrincipal('ec2.amazonaws.com') ) }); lambdaBackupRole.addManagedPolicy(            // IAMポリシー追加1 iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole') // 基本的な権限(logs:CreateLogGroup、logs:CreateLogStream、logs:PutLogEvents)を含むポリシー「AWSLambdaBasicExecutionRole」をアタッチ ); new iam.Policy(this, 'LambdaBackupPolicy', {            // IAMポリシー追加2 policyName: 'iam-policy-for-lmd-ssm-backup', roles: [lambdaBackupRole], statements: [ new iam.PolicyStatement({ sid: 'LifeCycleOfAMIandSnapshot',               // AMIとスナップショットのライフサイクル管理用権限追加 effect: iam.Effect.ALLOW, actions: [ 'ec2:DescribeImages', 'ec2:DeregisterImage', 'ec2:DeleteSnapshot', 'ec2:DescribeSnapshots', 'ec2:DescribeTags' ], resources: ['*'] }) ] }); // SSMAgent監視(Lambda実行用)IAMロール作成 const ssmAgentCheckRole = new iam.Role(this, 'SSMAgentCheckRole', { roleName: 'LambdaSSMAgentCheckRole', assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), }); ssmAgentCheckRole.addManagedPolicy(           // IAMポリシー追加1 iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole') // 基本的な権限(logs:CreateLogGroup、logs:CreateLogStream、logs:PutLogEvents)を含むポリシー「AWSLambdaBasicExecutionRole」をアタッチ ); new iam.Policy(this, 'SSMAgentCheckPolicy', {            // IAMポリシー追加2 policyName: 'iam-policy-for-lmd-ssmagent-check', roles: [ssmAgentCheckRole], statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'ssm:DescribeInstanceInformation', 'sns:Publish' ], resources: ['*'] }) ] }); Lambda関数設定 //=========================================== // バックアップ(世代管理)用Lambda作成 //=========================================== const backupGenLambda = new lambda.Function(this, 'BackupGenLambda', { functionName: 'lmd-del-backup-gen', runtime: lambda.Runtime.PYTHON_3_13, handler: 'index.lambda_handler', code: lambda.Code.fromAsset( path.join(__dirname, 'Lambda/backup-gen') ), role: lambdaBackupRole, timeout: cdk.Duration.seconds(600), // 実行タイムアウト(秒):10分 memorySize: 128, description: 'SSMドキュメントを使用してバックアップ実行時に流れる世代管理用の関数' }); //=========================================== // SSM Agent監視用Lambda作成 //=========================================== const ssmAgentCheckLambda = new lambda.Function(this, 'SSMAgentCheckLambda', { functionName: 'lmd-ssmagentcheck-send-sns', runtime: lambda.Runtime.PYTHON_3_13, handler: 'index.lambda_handler', code: lambda.Code.fromAsset( path.join(__dirname, 'Lambda/ssmagentcheck') ), role: ssmAgentCheckRole, environment: { SNS_TOPIC_ARN: backupTopic.topicArn, }, timeout: cdk.Duration.seconds(600),  // 実行タイムアウト(秒):10分 memorySize: 128, description: 'SSMAgent疎通エラーのSNS通知' }); ポイント: 自動世代管理 : 古いバックアップの自動削除 事前監視 : バックアップ前のSSM Agent健全性チェック エラーハンドリング : 失敗時の適切な通知機能 ソースコードの配置パスは実際のパスにより変更してください。 SSMドキュメント設定 const backupDocument = new ssm.CfnDocument(this, 'BackupDocument', { name: 'SSM-BackupEC2Instance', documentType: 'Automation', // ドキュメント作成時に選択する:オートメーション documentFormat: 'YAML',  // ドキュメントフォーマット content: yaml.load(fs.readFileSync( path.join(__dirname, 'Document', 'backup-document.yaml'), 'utf8' )) }); バックアップドキュメントの主な機能: 柔軟な実行 : 即時実行とスケジュール実行の両対応 再起動制御 : タグによる再起動有無の選択 エラー処理 : 失敗時の自動復旧機能 世代管理 : Lambda連携による古いバックアップの自動削除 メンテナンスウィンドウ設定 // メンテナンスウィンドウの作成 const backupMaintenanceWindow = new ssm.CfnMaintenanceWindow(this, 'BackupMaintenanceWindow', { name: 'mw-ssm-backup', schedule: 'cron(30 03 ? * * *)',   // 実行スケジュール(JST: 毎日3:30) duration: 1,     // 実行可能時間:1時間 cutoff: 0,      // 終了1時間前までに新規タスク開始 allowUnassociatedTargets: false, // 関連付けられたターゲットのみ実行 scheduleTimezone: 'Asia/Tokyo'  // スケジュールのタイムゾーン }); // ターゲットの作成 const backupMaintenanceWindowTarget = new ssm.CfnMaintenanceWindowTarget(this, 'BackupMaintenanceWindowTarget', { windowId: backupMaintenanceWindow.ref, name: 'tg-ssm-backup', targets: [{     // ターゲットの指定方法(タグで指定) key: 'tag:BackupGroup', values: ['ssm-backup-group'] }], resourceType: 'INSTANCE',  // 対象リソース種別:EC2インスタンス ownerInformation: 'バックアップ対象インスタンス' }); // タスクの作成 const backupMaintenanceWindowTask = new ssm.CfnMaintenanceWindowTask(this, 'BackupMaintenanceWindowTask', { windowId: backupMaintenanceWindow.ref, taskArn: backupDocument.ref, // ドキュメントARN taskType: 'AUTOMATION', // タスクタイプ priority: 1,     // タスク優先度 maxConcurrency: '100', // 同時制御実行数:100ターゲット maxErrors: '100',   // エラーのしきい値:100エラー name: 'EC2Backup', targets: [{ key: 'WindowTargetIds', values: [backupMaintenanceWindowTarget.ref] }], serviceRoleArn: ssmBackupRole.roleArn, taskInvocationParameters: { maintenanceWindowAutomationParameters: { documentVersion: '$DEFAULT', // ドキュメントのバージョン:ランタイムのデフォルトバージョン parameters: {    // 入力パラメータ InstanceId: ['{{TARGET_ID}}'], // ターゲットインスタンスID } } } }); ポイント: 柔軟スケジュール : 分単位での細かい実行時間設定 タグベース制御 : 対象インスタンスの動的管理 EventBridge設定 const backupRule = new events.Rule(this, 'BackupEventRule', {   // バックアップ失敗時のルール ruleName: 'evtbg-rule-backup', eventPattern: { source: ['aws.ssm'], detailType: ['EC2 Automation Execution Status-change Notification'],   // Automationタスクの実行結果に発行されるイベント detail: { Status: ['Failed', 'TimedOut', 'Cancelled']        // AutiomationのステータスFailed,TimedOut,Cancelledを検知 } }, targets: [ new targets.SnsTopic(backupTopic) ] }); //=========================================== // SSM Agent監視用ルール //=========================================== const ssmMonitoringRule = new events.Rule(this, 'SSMMonitoringRule', {  // SSM Agent監視用のEventBridge ruleName: 'evtbg-rule-ssmagentcheck', schedule: events.Schedule.expression('cron(20 18 * * ? *)'),      // 3:20 JST バックアップ開始前に疎通確認 description: 'Triggers Lambda every day at 3:20 JST', targets: [ new targets.LambdaFunction(ssmAgentCheckLambda) ] }); } } ポイント: プロアクティブ監視 : バックアップ前のSSM Agent接続確認 予防的対応 : 事前のトラブル検知と通知 SSMAgent監視用Ruleの実行時間はバックアップ開始時間に合わせて変更が必要です Lambda関数の詳細 世代管理Lambda(即時・スケジュール両対応) import json import boto3 def lambda_handler(event, context): gen = int(event['gen']) client = boto3.client('ec2') images = client.describe_images( Filters=[ { 'Name': 'tag:Name', 'Values': [ event['ServerName'] + '*' ] }, { 'Name': 'tag:AutoBackup', 'Values': [ 'true' ] } ] )['Images'] #スナップショット一覧をソート images.sort(key=lambda x: x['CreationDate'], reverse=True) for i in range(gen, len(images), 1): print(images[i]['ImageId']) response = client.deregister_image( ImageId=images[i]['ImageId'] ) for device in images[i]['BlockDeviceMappings']: if device.get('Ebs') is not None: print(device['Ebs']['SnapshotId']) response = client.delete_snapshot( SnapshotId=device['Ebs']['SnapshotId'] ) return 'OK' SSM Agent監視Lambda import boto3 import os def lambda_handler(event, context): sns_topic_arn = os.environ.get('SNS_TOPIC_ARN', None) if not sns_topic_arn: print("SNS_TOPIC_ARN is not set.") return def get_managed_instances(): ssm_client = boto3.client('ssm') try: # ConnectionLostなインスタンスのみを取得 response = ssm_client.describe_instance_information( Filters=[ { 'Key': 'PingStatus', 'Values': ['ConnectionLost'] } ], MaxResults=50 # 必要に応じて調整 ) # インスタンス情報を取得した数をログに出力 instance_count = len(response['InstanceInformationList']) print(f"Number of instances with ConnectionLost: {instance_count}") # InstanceInformationListの内容を表示 instance_information_list = response['InstanceInformationList'] print("Instance Information List with ConnectionLost status:") for instance_info in instance_information_list: print(instance_info) # 各インスタンス情報を表示 # ConnectionLostのインスタンスIDを取得 managed_instances = [info['InstanceId'] for info in instance_information_list] except boto3.exceptions.Boto3Error as e: print(f"An error occurred while describing instances: {e}") managed_instances = [] return managed_instances def notify_connection_lost(instance_ids, sns_topic_arn): sns_client = boto3.client('sns') if not instance_ids: print("No instances with ConnectionLost status.") return for instance_id in instance_ids: message = f"Instance ID: {instance_id} has ConnectionLost status." print(message) try: sns_client.publish( TopicArn=sns_topic_arn, Message=message, Subject='SSM Managed Instance Connection Alert' ) except boto3.exceptions.Boto3Error as e: print(f"An error occurred while sending SNS notification for {instance_id}: {e}") # マネージドインスタンスの一覧を取得 connection_lost_instances = get_managed_instances() # 接続が失われたインスタンスについて通知 notify_connection_lost(connection_lost_instances, sns_topic_arn) SSM ドキュメントの詳細 schemaVersion: '0.3' description: SSM Standard EC2Backup parameters: InstanceId: description: (Required) InstanceIds to run command type: String mainSteps: ################################################################################### # 事前処理 ################################################################################### #InstanceのNameタグを取得 - name: Get_InstanceTag_EC2SV1Name action: aws:executeAwsApi nextStep: Get_InstanceTag_EC2BackupGen isEnd: false inputs: Filters: - Values: - '{{ InstanceId }}' Name: resource-id - Values: - Name Name: key Service: ec2 Api: DescribeTags outputs: - Type: String Name: value Selector: $.Tags[0].Value # EC2バックアップ世代数 - name: Get_InstanceTag_EC2BackupGen action: aws:executeAwsApi nextStep: Get_EC2StopStartTag isEnd: false inputs: Filters: - Values: - '{{ InstanceId }}' Name: resource-id - Values: - EC2BackupGen Name: key Service: ec2 Api: DescribeTags outputs: - Type: String Name: value Selector: $.Tags[0].Value # バックアップ再起動有無 - name: Get_EC2StopStartTag action: aws:executeAwsApi nextStep: CheckStopStartTag1 isEnd: false inputs: Filters: - Values: - '{{ InstanceId }}' Name: resource-id - Values: - BackupEC2StopStart Name: key Service: ec2 Api: DescribeTags outputs: - Type: String Name: value Selector: $.Tags[0].Value ################################################################################### # EC2停止 ################################################################################### - name: CheckStopStartTag1 action: aws:branch inputs: Choices: - NextStep: Run_EC2StopSV1 Variable: '{{ Get_EC2StopStartTag.value }}' StringEquals: 'true' Default: Get_BackupEC2SV1_AMI - name: Run_EC2StopSV1 action: aws:executeAutomation timeoutSeconds: '600' nextStep: Get_BackupEC2SV1_AMI isEnd: false onFailure: step:Run_EC2RestartSV1 inputs: RuntimeParameters: InstanceId: - '{{ InstanceId }}' DocumentName: AWS-StopEC2Instance ################################################################################### # バックアップ ################################################################################### # AMI作成 - name: Get_BackupEC2SV1_AMI action: aws:executeAwsApi nextStep: Get_BackupEC2SV1_AMI_SnapshotId isEnd: false onFailure: step:Run_EC2RestartSV1 inputs: Service: ec2 Api: CreateImage InstanceId: '{{ InstanceId }}' Name: '{{Get_InstanceTag_EC2SV1Name.value}}-{{global:DATE_TIME}}' NoReboot: true outputs: - Type: String Name: ImageId Selector: $.ImageId # スナップショット作成 - name: Get_BackupEC2SV1_AMI_SnapshotId action: aws:executeAwsApi nextStep: Get_BackupEC2SV1_AMITag isEnd: false onFailure: step:Run_Quit inputs: Filters: - Values: - '*{{ Get_BackupEC2SV1_AMI.ImageId }}*' Name: description Service: ec2 Api: DescribeSnapshots outputs: - Type: StringList Name: value Selector: $.Snapshots..SnapshotId # AMIにタグを付与 - name: Get_BackupEC2SV1_AMITag action: aws:createTags nextStep: Del_OldBackupEC2SV1_AMI isEnd: false onFailure: step:Run_Quit inputs: ResourceIds: - '{{ Get_BackupEC2SV1_AMI.ImageId }}' - '{{ Get_BackupEC2SV1_AMI_SnapshotId.value }}' ResourceType: EC2 Tags: - Value: '{{Get_InstanceTag_EC2SV1Name.value}}' Key: Name - Value: 'true' Key: AutoBackup ################################################################################### # 世代管理 ################################################################################### - name: Del_OldBackupEC2SV1_AMI action: aws:invokeLambdaFunction maxAttempts: 3 timeoutSeconds: '600' nextStep: CheckStopStartTag2 isEnd: false onFailure: step:Run_Quit inputs: FunctionName: lmd-del-backup-gen Payload: '{ "ServerName":"{{Get_InstanceTag_EC2SV1Name.value}}","gen":"{{Get_InstanceTag_EC2BackupGen.value}}"}' ################################################################################### # EC2起動 ################################################################################### - name: CheckStopStartTag2 action: aws:branch inputs: Choices: - NextStep: Run_EC2StartSV1_1 Variable: '{{ Get_EC2StopStartTag.value }}' StringEquals: 'true' Default: WaitForImageAvailable - name: Run_EC2StartSV1_1 action: aws:executeAutomation timeoutSeconds: '600' nextStep: WaitForImageAvailable isEnd: false onFailure: step:Run_EC2RestartSV1 inputs: RuntimeParameters: InstanceId: - '{{ InstanceId }}' DocumentName: AWS-StartEC2Instance - name: WaitForImageAvailable action: aws:waitForAwsResourceProperty timeoutSeconds: 10800 nextStep: Run_Quit isEnd: false onFailure: step:Run_Quit inputs: Service: ec2 Api: DescribeImages ImageIds: - '{{ Get_BackupEC2SV1_AMI.ImageId }}' PropertySelector: $.Images[0].State DesiredValues: - available - name: Run_Quit action: aws:executeAwsApi isEnd: true inputs: Service: sts Api: GetCallerIdentity ################################################################################### # 異常時処理 ################################################################################### - name: Run_EC2RestartSV1 action: aws:executeAutomation nextStep: Run_Restart_Quit isEnd: false onFailure: step:Sleep_EC2Restart inputs: RuntimeParameters: InstanceId: - '{{ InstanceId }}' DocumentName: AWS-RestartEC2Instance - name: Run_Restart_Quit action: aws:executeAwsApi isEnd: true inputs: Service: sts Api: GetCallerIdentity ################################################################################### # 異常処理に失敗した場合、1時間後に再起動 ################################################################################### - name: Sleep_EC2Restart action: 'aws:sleep' inputs: Duration: PT60M - name: Run_Retry_EC2RestartSV1 action: 'aws:executeAutomation' inputs: DocumentName: AWS-RestartEC2Instance RuntimeParameters: InstanceId: - '{{ InstanceId }}' - name: Run_Retry_Quit action: 'aws:executeAwsApi' isEnd: true inputs: Service: sts Api: GetCallerIdentity SSMドキュメント処理フロー 1. 事前処理 Nameタグ取得 : EC2インスタンスのNameタグを取得 バックアップ世代数取得 :  EC2BackupGen タグから保持する世代数を取得 停止/開始設定取得 :  BackupEC2StopStart タグでバックアップ時の停止/開始を確認 2. EC2停止(条件付き) BackupEC2StopStart タグが true の場合のみEC2インスタンスを停止 停止に失敗した場合は再起動処理へ 3. バックアップ実行 AMI作成 : インスタンスからAMI(Amazon Machine Image)を作成 スナップショット取得 : 作成したAMIに関連するスナップショットIDを取得 タグ付与 : AMIとスナップショットに Name と AutoBackup タグを付与 4. 世代管理 Lambda関数を呼び出して古いバックアップを削除 指定された世代数を超えるバックアップを自動削除 5. EC2起動(条件付き) 停止していた場合はEC2インスタンスを起動 AMIの作成完了を待機(最大3時間) 6. 異常時処理 バックアップ処理で異常が発生した場合、EC2インスタンスを再起動 再起動に失敗した場合は1時間後に再試行   前提条件・運用上の注意点 前提条件 SSM Agent : EC2インスタンスにSSM Agentがインストール・実行中であること IAMロール : インスタンスにSSM管理権限を付与されていること 必要タグ : バックアップ制御用タグの設定されていること ネットワーク : SSMエンドポイントへの通信が可能であること タグ設定例 BackupGroup: ssm-backup-group EC2BackupGen: 世代管理数 BackupEC2StopStart: true/false true: 再起動ありでバックアップ取得 false: 再起動なしでバックアップ取得   今回実装したコンストラクトファイルまとめ import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as fs from 'fs'; import * as path from 'path'; import * as yaml from 'js-yaml'; import * as sns from 'aws-cdk-lib/aws-sns'; import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; import * as events from 'aws-cdk-lib/aws-events'; import * as targets from 'aws-cdk-lib/aws-events-targets'; export interface SsmBackupConstructProps { } export class SsmBackupConstruct extends Construct { constructor(scope: Construct, id: string, props?: SsmBackupConstructProps) { super(scope, id); //=========================================== // バックアップ失敗通知用SNSトピック作成 //=========================================== const emailAddresses = [  // SNS通知先メーリングリスト 'xxxxxxxx@example.com', 'xxxxxxx@example.com', ]; const backupTopic = new sns.Topic(this, 'BackupTopic', {  // バックアップ失敗通知用のトピック topicName: 'sns-backup-alertnotification', displayName: 'Backup Alert Notifications' }); emailAddresses.forEach(email => { backupTopic.addSubscription( new subscriptions.EmailSubscription(email) ); }); backupTopic.addToResourcePolicy( // トピックポリシー追加1 new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'sns:GetTopicAttributes', 'sns:SetTopicAttributes', 'sns:AddPermission', 'sns:RemovePermission', 'sns:DeleteTopic', 'sns:Subscribe', 'sns:Publish', ], resources: [backupTopic.topicArn], principals: [new iam.AnyPrincipal()], conditions: { StringEquals: { 'aws:SourceOwner': cdk.Stack.of(this).account } } }) ); backupTopic.addToResourcePolicy( // トピックポリシー追加2 new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'sns:Publish', ], resources: [backupTopic.topicArn], principals: [new iam.ServicePrincipal('events.amazonaws.com')], }) ); //=========================================== // Automationタスク用IAMロール作成 //=========================================== const ssmBackupRole = new iam.Role(this, 'SSMBackupRole', { roleName: 'SSMAutomationRole', assumedBy: new iam.CompositePrincipal( new iam.ServicePrincipal('ec2.amazonaws.com'), new iam.ServicePrincipal('ssm.amazonaws.com') ), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonSSMMaintenanceWindowRole'), // AWS管理ポリシー追加 iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore') ] }); ssmBackupRole.addManagedPolicy(              // IAMポリシー追加1 iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')   // 基本的な権限(logs:CreateLogGroup、logs:CreateLogStream、logs:PutLogEvents)を含むポリシー「AWSLambdaBasicExecutionRole」をアタッチ ); new iam.Policy(this, 'SSMBackupPolicy', {               // IAMポリシー追加2 policyName: 'iam-policy-for-ssm-backup', roles: [ssmBackupRole], statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "ec2:CreateTags", "ec2:CreateImage", "ec2:DescribeImages", "ec2:DescribeTags", "ec2:DescribeInstances", "ec2:DescribeInstanceStatus", "ec2:DescribeSnapshots", "ec2:StopInstances", "ec2:StartInstances" ], resources: ['*'] }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "lambda:InvokeFunction" ], resources: ["*"] }) ] }); //=========================================== // Lambda実行用IAMロール作成 //=========================================== // AMI世代管理(Lambda実行用)IAMロール作成 const lambdaBackupRole = new iam.Role(this, 'LambdaBackupRole', { roleName: 'LambdaBackupRole', assumedBy: new iam.CompositePrincipal( new iam.ServicePrincipal('lambda.amazonaws.com'), new iam.ServicePrincipal('ec2.amazonaws.com') ) }); lambdaBackupRole.addManagedPolicy(            // IAMポリシー追加1 iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole') // 基本的な権限(logs:CreateLogGroup、logs:CreateLogStream、logs:PutLogEvents)を含むポリシー「AWSLambdaBasicExecutionRole」をアタッチ ); new iam.Policy(this, 'LambdaBackupPolicy', {            // IAMポリシー追加2 policyName: 'iam-policy-for-lmd-ssm-backup', roles: [lambdaBackupRole], statements: [ new iam.PolicyStatement({ sid: 'LifeCycleOfAMIandSnapshot',               // AMIとスナップショットのライフサイクル管理用権限追加 effect: iam.Effect.ALLOW, actions: [ 'ec2:DescribeImages', 'ec2:DeregisterImage', 'ec2:DeleteSnapshot', 'ec2:DescribeSnapshots', 'ec2:DescribeTags' ], resources: ['*'] }) ] }); // SSMAgent監視(Lambda実行用)IAMロール作成 const ssmAgentCheckRole = new iam.Role(this, 'SSMAgentCheckRole', { roleName: 'LambdaSSMAgentCheckRole', assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), }); ssmAgentCheckRole.addManagedPolicy(           // IAMポリシー追加1 iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole') // 基本的な権限(logs:CreateLogGroup、logs:CreateLogStream、logs:PutLogEvents)を含むポリシー「AWSLambdaBasicExecutionRole」をアタッチ ); new iam.Policy(this, 'SSMAgentCheckPolicy', {            // IAMポリシー追加2 policyName: 'iam-policy-for-lmd-ssmagent-check', roles: [ssmAgentCheckRole], statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'ssm:DescribeInstanceInformation', 'sns:Publish' ], resources: ['*'] }) ] }); //=========================================== // バックアップ(世代管理)用Lambda作成 //=========================================== const backupGenLambda = new lambda.Function(this, 'BackupGenLambda', { functionName: 'lmd-del-backup-gen', runtime: lambda.Runtime.PYTHON_3_13, handler: 'index.lambda_handler', code: lambda.Code.fromAsset( path.join(__dirname, 'Lambda/backup-gen') ), role: lambdaBackupRole, timeout: cdk.Duration.seconds(600), // 実行タイムアウト(秒):10分 memorySize: 128, description: 'SSMドキュメントを使用してバックアップ実行時に流れる世代管理用の関数' }); //=========================================== // SSM Agent監視用Lambda作成 //=========================================== const ssmAgentCheckLambda = new lambda.Function(this, 'SSMAgentCheckLambda', { functionName: 'lmd-ssmagentcheck-send-sns', runtime: lambda.Runtime.PYTHON_3_13, handler: 'index.lambda_handler', code: lambda.Code.fromAsset( path.join(__dirname, 'Lambda/ssmagentcheck') ), role: ssmAgentCheckRole, environment: { SNS_TOPIC_ARN: backupTopic.topicArn, }, timeout: cdk.Duration.seconds(600),  // 実行タイムアウト(秒):10分 memorySize: 128, description: 'SSMAgent疎通エラーのSNS通知' }); //=========================================== // バックアップ作成 //=========================================== // SSMドキュメントの作成 const backupDocument = new ssm.CfnDocument(this, 'BackupDocument', { name: 'SSM-BackupEC2Instance', documentType: 'Automation', // ドキュメント作成時に選択する:オートメーション documentFormat: 'YAML',  // ドキュメントフォーマット content: yaml.load(fs.readFileSync( path.join(__dirname, 'Document', 'backup-document.yaml'), 'utf8' )) }); // メンテナンスウィンドウの作成 const backupMaintenanceWindow = new ssm.CfnMaintenanceWindow(this, 'BackupMaintenanceWindow', { name: 'mw-ssm-backup', schedule: 'cron(30 03 ? * * *)',   // 実行スケジュール(JST: 毎日3:30) duration: 1,     // 実行可能時間:1時間 cutoff: 0,      // 終了1時間前までに新規タスク開始 allowUnassociatedTargets: false, // 関連付けられたターゲットのみ実行 scheduleTimezone: 'Asia/Tokyo'  // スケジュールのタイムゾーン }); // ターゲットの作成 const backupMaintenanceWindowTarget = new ssm.CfnMaintenanceWindowTarget(this, 'BackupMaintenanceWindowTarget', { windowId: backupMaintenanceWindow.ref, name: 'tg-ssm-backup', targets: [{     // ターゲットの指定方法(タグで指定) key: 'tag:BackupGroup', values: ['ssm-backup-group'] }], resourceType: 'INSTANCE',  // 対象リソース種別:EC2インスタンス ownerInformation: 'バックアップ対象インスタンス' }); // タスクの作成 const backupMaintenanceWindowTask = new ssm.CfnMaintenanceWindowTask(this, 'BackupMaintenanceWindowTask', { windowId: backupMaintenanceWindow.ref, taskArn: backupDocument.ref, // ドキュメントARN taskType: 'AUTOMATION', // タスクタイプ priority: 1,     // タスク優先度 maxConcurrency: '100', // 同時制御実行数:100ターゲット maxErrors: '100',   // エラーのしきい値:100エラー name: 'EC2Backup', targets: [{ key: 'WindowTargetIds', values: [backupMaintenanceWindowTarget.ref] }], serviceRoleArn: ssmBackupRole.roleArn, taskInvocationParameters: { maintenanceWindowAutomationParameters: { documentVersion: '$DEFAULT', // ドキュメントのバージョン:ランタイムのデフォルトバージョン parameters: {    // 入力パラメータ InstanceId: ['{{TARGET_ID}}'], // ターゲットインスタンスID } } } }); //=========================================== // バックアップ失敗検知 //=========================================== const backupRule = new events.Rule(this, 'BackupEventRule', {   // バックアップ失敗時のルール ruleName: 'evtbg-rule-backup', eventPattern: { source: ['aws.ssm'], detailType: ['EC2 Automation Execution Status-change Notification'],   // Automationタスクの実行結果に発行されるイベント detail: { Status: ['Failed', 'TimedOut', 'Cancelled']        // AutiomationのステータスFailed,TimedOut,Cancelledを検知 } }, targets: [ new targets.SnsTopic(backupTopic) ] }); //=========================================== // SSM Agent監視用ルール //=========================================== const ssmMonitoringRule = new events.Rule(this, 'SSMMonitoringRule', {  // SSM Agent監視用のEventBridge ruleName: 'evtbg-rule-ssmagentcheck', schedule: events.Schedule.expression('cron(20 18 * * ? *)'),      // 3:20 JST バックアップ開始前に疎通確認 description: 'Triggers Lambda every day at 3:20 JST', targets: [ new targets.LambdaFunction(ssmAgentCheckLambda) ] }); } } まとめ 今回はSSMドキュメントでのEC2のバックアップをAWS CDKで実装してみました。 皆さんのお役に立てれば幸いです。
アバター
今回は、Amazon EC2 Windows インスタンスのシステムリソース監視、状態監視、イベントログモニタリング、異常時のアラート通知までを Amazon CloudWatch を利用して一元的に AWS CDK で実装する方法をまとめました。 はじめに 今回は、Windows EC2インスタンスの包括的な監視システムをAWS CDKで実装していきます。システムリソース監視、死活監視、プロセス監視、Windowsイベントログ監視まで、運用に必要な監視項目を実装します。 また、今回は既に構築されているEC2のインスタンスIDを指定して監視設定を追加していきます。   今回作成するリソース SNSトピック : CloudWatchアラームの通知先 CloudWatchアラーム : システム/インスタンス死活監視、リソース使用率監視、プロセス監視 CloudWatch Logs : Windowsイベントログ収集とエラー監視 メトリクスフィルター : イベントログのエラーパターン検出   アーキテクチャ概要   AWS CDK ソースコード インスタンスIDインポート const ec2Instance = 'i-xxxxxxxxxxxxxxxxx' // 監視対象のインスタンスIDを指定 SNS通知設定 const emailAddresses = [ // SNS通知先メーリングリスト(通知先が複数ある場合アドレスを追加) 'xxxxxx@example.com', 'xxxxxxx@example.com', ]; // CloudWatchアラーム用トピック const alarmTopic = new sns.Topic(this, 'AlarmTopic', { topicName: 'clw-alertnotification', // トピック名 displayName: 'Cloudwatch Alert Notifications' // 表示名 }); // CloudWatchアラーム用サブスクリプション emailAddresses.forEach(email => { alarmTopic.addSubscription( new subscriptions.EmailSubscription(email) // プロトコル:EMAIL ); }); ポイント: 複数の管理者への通知配信 アラーム発生時に通知するメールアドレスを指定 CloudWatchアラーム設定 システム死活監視 const statusCheckFailedSystemAlarm = new cloudwatch.Alarm(this, 'StatusCheckFailedSystemAlarm', { alarmName: 'alarm-ec2-scfs', metric: new cloudwatch.Metric({ namespace: 'AWS/EC2', metricName: 'StatusCheckFailed_System', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, }, statistic: 'sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); statusCheckFailedSystemAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 statusCheckFailedSystemAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先   インスタンス死活監視 const statusCheckFailedInstanceAlarm = new cloudwatch.Alarm(this, 'StatusCheckFailedInstanceAlarm', { alarmName: 'alarm-ec2-scfi', metric: new cloudwatch.Metric({ namespace: 'AWS/EC2', metricName: 'StatusCheckFailed_Instance', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID }, statistic: 'sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); statusCheckFailedInstanceAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 statusCheckFailedInstanceAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先   CPU使用率監視 const cpuAlarm = new cloudwatch.Alarm(this, 'CpuAlarm', { alarmName: 'alarm-ec2-cpu', metric: new cloudwatch.Metric({ namespace: 'AWS/EC2', metricName: 'CPUUtilization', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID }, statistic: 'Average', // 統計:平均 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 90, // アラームの閾値(CPU使用率90%) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); cpuAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 cpuAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先   メモリ使用率監視 const memoryAlarm = new cloudwatch.Alarm(this, 'MemoryAlarm', { alarmName: 'alarm-ec2-mem', metric: new cloudwatch.Metric({ namespace: 'CWAgent', metricName: 'Memory % Committed Bytes In Use', // メトリクス名 dimensionsMap: { // 取得に必要な変数 InstanceId: ec2Instance, // 監視対象のEC2インスタンスID(config.jsonのappend_dimensionsで指定) objectname: 'Memory' // オブジェクト名(config.jsonのmetrics_collectedで指定) }, statistic: 'Average', // 統計:平均 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 90, // アラームの閾値(メモリ使用率90%) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); memoryAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 memoryAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先   ディスク使用率監視 const diskAlarmC = new cloudwatch.Alarm(this, 'DiskAlarmC', { alarmName: 'alarm-ec2-dsk-c', metric: new cloudwatch.Metric({ namespace: 'CWAgent', metricName: 'LogicalDisk % Free Space', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID(config.json側のappend_dimensionsで指定) instance: 'C:', // 対象ディスク objectname: 'LogicalDisk' // オブジェクト名(config.json側のmetrics_collectedで指定) }, statistic: 'Average', // 統計:平均 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 10, // アラームの閾値 evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, // アラーム条件: より小さい treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); diskAlarmC.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 diskAlarmC.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先   ディスク使用率監視(Dドライブ) const diskAlarmD = new cloudwatch.Alarm(this, 'diskDAlarmD', { alarmName: 'alarm-ec2-dsk-d', metric: new cloudwatch.Metric({ namespace: 'CWAgent', metricName: 'LogicalDisk % Free Space', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID(config.json側のappend_dimensionsで指定) instance: 'D:', // 対象ディスク objectname: 'LogicalDisk' // オブジェクト名(config.json側のmetrics_collectedで指定) }, statistic: 'Average', // 統計:平均 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 10, // アラームの閾値 evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, // アラーム条件: より小さい treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); diskAlarmD.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 diskAlarmD.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先   プロセス監視 const svchostServiceAlarm = new cloudwatch.Alarm(this, 'SVCHostServiceAlarm', { alarmName: 'alarm-svchost', metric: new cloudwatch.Metric({ namespace: 'CWAgent', metricName: 'procstat_lookup pid_count', // メトリクス名(プロセスに関連付けられたプロセスIDの数) dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID(config.json側のappend_dimensionsで指定) exe: 'svchost.exe', // 監視対象のプロセス名 pid_finder: 'native' // プロセスの検出方法:native(OSのネイティブAPIを使用) }, statistic: 'Min', // 統計: 最小 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値 evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件:閾値以下 treatMissingData: cloudwatch.TreatMissingData.BREACHING, // 欠落データを不正(しきい値を超えている)として処理 }); svchostServiceAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 svchostServiceAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先   イベントログ(システムログ)用のロググループのエラー監視 // イベントログ(システムログ)用のロググループのエラー監視アラーム const sytemLogErrorsAlarm = new cloudwatch.Alarm(this, 'SystemLogErrorsAlarm', { alarmName: 'os-eventlog-system', metric: new cloudwatch.Metric({ namespace: 'os-cloudwatchlogs', // カスタムメトリクスの名前空間(CloudwachLogsで作成した名前空間を指定) metricName: 'OS-Eventlog-System', // メトリクス名 statistic: 'Sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); sytemLogErrorsAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 ポイント: 名前空間 : システムロググループの名前空間を指定 メトリクス名 : システムロググループのメトリクス名を指定    イベントログ(アプリケーションログ)用のロググループのエラー監視 // イベントログ(アプリケーションログ)用のロググループのエラー監視アラーム const appLogErrorsAlarm = new cloudwatch.Alarm(this, 'ApplicationLogErrorsAlarm', { alarmName: 'os-eventlog-application', metric: new cloudwatch.Metric({ namespace: 'os-cloudwatchlogs', // カスタムメトリクスの名前空間(CloudwachLogsで作成した名前空間を指定) metricName: 'OS-Eventlog-Application', // メトリクス名 statistic: 'Sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); appLogErrorsAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 ポイント: 名前空間 : アプリケーションロググループの名前空間を指定 メトリクス名 : アプリケーションロググループのメトリクス名を指定   イベントログ(セキュリティ)用のロググループのエラー監視 // イベントログ(セキュリティログ)用のロググループのエラー監視アラーム const securityLogErrorsAlarm = new cloudwatch.Alarm(this, 'SecurityLogErrorsAlarm', { alarmName: 'os-eventlog-security', metric: new cloudwatch.Metric({ namespace: 'os-cloudwatchlogs', // カスタムメトリクスの名前空間(CloudwachLogsで作成した名前空間を指定) metricName: 'OS-Eventlog-Security', // メトリクス名 statistic: 'Sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); securityLogErrorsAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 ポイント: 名前空間 : イベントロググループの名前空間を指定 メトリクス名 : イベントロググループのメトリクス名を指定   CloudWatch Logs設定 ロググループとメトリクスフィルター // OSイベントログ(システムログ)用のロググループ const systemLogGroup = new logs.LogGroup(this, 'SystemLogGroup', { logGroupName: 'loggroup-os-eventlog-system', retention: logs.RetentionDays.ONE_MONTH, // 保持期間: 1ヵ月 removalPolicy: cdk.RemovalPolicy.DESTROY }); const systemErrorMetricFilter = new logs.MetricFilter(this, 'SystemErrorMetricFilter', { // イベントログ(システムログ)用メトリクスフィルター logGroup: systemLogGroup, filterName: 'filter-os-eventlog-system', // フィルター名 filterPattern: logs.FilterPattern.literal('?ERROR ?Error ?error ?FAIL ?Fail ?fail'), // フィルターパターン metricNamespace: 'os-cloudwatchlogs', // メトリクス名前空間 metricName: 'OS-Eventlog-System', // メトリクス名 metricValue: '1', // メトリクス値 defaultValue: 0 }); // OSイベントログ(アプリケーションログ)用のロググループ const applicationLogGroup = new logs.LogGroup(this, 'ApplicationLogGroup', { logGroupName: 'loggroup-os-eventlog-application', retention: logs.RetentionDays.ONE_MONTH, // 保持期間: 1ヵ月 removalPolicy: cdk.RemovalPolicy.DESTROY }); const applicationErrorMetricFilter = new logs.MetricFilter(this, 'ApplicationErrorMetricFilter', { // イベントログ(アプリケーションログ)用メトリクスフィルター logGroup: applicationLogGroup, filterName: 'filter-os-eventlog-application',        // フィルター名 filterPattern: logs.FilterPattern.literal('?ERROR ?Error ?error ?FAIL ?Fail ?fail'),    // フィルターパターン metricNamespace: 'os-cloudwatchlogs',      // メトリクス名前空間 metricName: 'OS-Eventlog-Application', // メトリクス名 metricValue: '1', // メトリクス値 defaultValue: 0 }); // OSイベントログ(セキュリティログ)用のロググループ const securityLogGroup = new logs.LogGroup(this, 'SecurityLogGroup', { logGroupName: 'loggroup-os-eventlog-security', retention: logs.RetentionDays.ONE_MONTH, // 保持期間: 1ヵ月 removalPolicy: cdk.RemovalPolicy.DESTROY }); const securityErrorMetricFilter = new logs.MetricFilter(this, 'SecurityErrorMetricFilter', { // イベントログ(セキュリティログ)用メトリクスフィルター logGroup: securityLogGroup, filterName: 'filter-os-eventlog-security', // フィルター名 filterPattern: logs.FilterPattern.literal('?ERROR ?Error ?error ?FAIL ?Fail ?fail'), // フィルターパターン metricNamespace: 'os-cloudwatchlogs', // メトリクス名前空間 metricName: 'OS-Eventlog-Security', // メトリクス名 metricValue: '1', // メトリクス値 defaultValue: 0 }); } } ポイント: ログ種別 : System、Application、Securityの3種類 メトリクスフィルター : ERROR/Error/error/FAIL/Fail/failパターンを検出 保持期間 : 1ヶ月(要件に応じて調整可能) 本番環境では :  removalPolicy: cdk.RemovalPolicy.RETAIN に変更 監視項目一覧 監視区分 監視項目 閾値 説明 死活監視 システムステータスチェック 失敗検知 AWS基盤レベルの問題検出 死活監視 インスタンスステータスチェック 失敗検知 インスタンスレベルの問題検出 リソース監視 CPU使用率 90%以上 プロセッサーの負荷監視 リソース監視 メモリ使用率 90%以上 メモリリソースの使用量監視 リソース監視 ディスク使用率(C:) 10%未満 システムドライブの空き容量 リソース監視 ディスク使用率(D:) 10%未満 データドライブの空き容量 プロセス監視 svchost.exe 1以下 重要システムプロセスの監視 イベントログ監視 システムログ エラー検出 システムレベルのエラー監視 イベントログ監視 アプリケーションログ エラー検出 アプリケーションエラー監視 イベントログ監視 セキュリティログ エラー検出 セキュリティ関連エラー監視   今回実装したコンストラクトファイルまとめ import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as sns from 'aws-cdk-lib/aws-sns'; import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as actions from 'aws-cdk-lib/aws-cloudwatch-actions'; import * as logs from 'aws-cdk-lib/aws-logs'; export interface EC2WinMonitoringConstructProps { // 必要に応じて追加のプロパティを定義 } export class EC2WinMonitoringConstruct extends Construct { constructor(scope: Construct, id: string, props?: EC2WinMonitoringConstructProps) { super(scope, id); //=========================================== // EC2インスタンス //=========================================== const ec2Instance = 'i-xxxxxxxxxxxxxxxxx' // 監視対象のインスタンスIDを指定 //=========================================== // SNS //=========================================== const emailAddresses = [ // SNS通知先メーリングリスト(通知先が複数ある場合アドレスを追加) 'xxxxxx@example.com', 'xxxxxxx@example.com', ]; // CloudWatchアラーム用トピック const alarmTopic = new sns.Topic(this, 'AlarmTopic', { topicName: 'clw-alertnotification', // トピック名 displayName: 'Cloudwatch Alert Notifications' // 表示名 }); // CloudWatchアラーム用サブスクリプション emailAddresses.forEach(email => { alarmTopic.addSubscription( new subscriptions.EmailSubscription(email) // プロトコル:EMAIL ); }); //=========================================== // CloudWatchアラーム //=========================================== // システム死活監視 const statusCheckFailedSystemAlarm = new cloudwatch.Alarm(this, 'StatusCheckFailedSystemAlarm', { alarmName: 'alarm-ec2-scfs', metric: new cloudwatch.Metric({ namespace: 'AWS/EC2', metricName: 'StatusCheckFailed_System', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, }, statistic: 'sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); statusCheckFailedSystemAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 statusCheckFailedSystemAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先 // インスタンス死活監視 const statusCheckFailedInstanceAlarm = new cloudwatch.Alarm(this, 'StatusCheckFailedInstanceAlarm', { alarmName: 'alarm-ec2-scfi', metric: new cloudwatch.Metric({ namespace: 'AWS/EC2', metricName: 'StatusCheckFailed_Instance', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID }, statistic: 'sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); statusCheckFailedInstanceAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 statusCheckFailedInstanceAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先 // CPU使用率アラーム const cpuAlarm = new cloudwatch.Alarm(this, 'CpuAlarm', { alarmName: 'alarm-ec2-cpu', metric: new cloudwatch.Metric({ namespace: 'AWS/EC2', metricName: 'CPUUtilization', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID }, statistic: 'Average', // 統計:平均 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 90, // アラームの閾値(CPU使用率90%) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); cpuAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 cpuAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先 // メモリ使用率アラーム const memoryAlarm = new cloudwatch.Alarm(this, 'MemoryAlarm', { alarmName: 'alarm-ec2-mem', metric: new cloudwatch.Metric({ namespace: 'CWAgent', metricName: 'Memory % Committed Bytes In Use', // メトリクス名 dimensionsMap: { // 取得に必要な変数 InstanceId: ec2Instance, // 監視対象のEC2インスタンスID(config.jsonのappend_dimensionsで指定) objectname: 'Memory' // オブジェクト名(config.jsonのmetrics_collectedで指定) }, statistic: 'Average', // 統計:平均 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 90, // アラームの閾値(メモリ使用率90%) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); memoryAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 memoryAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先 // ディスク使用率アラーム const diskAlarmC = new cloudwatch.Alarm(this, 'DiskAlarmC', { alarmName: 'alarm-ec2-dsk-c', metric: new cloudwatch.Metric({ namespace: 'CWAgent', metricName: 'LogicalDisk % Free Space', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID(config.json側のappend_dimensionsで指定) instance: 'C:', // 対象ディスク objectname: 'LogicalDisk' // オブジェクト名(config.json側のmetrics_collectedで指定) }, statistic: 'Average', // 統計:平均 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 10, // アラームの閾値 evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, // アラーム条件: より小さい treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); diskAlarmC.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 diskAlarmC.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先 const diskAlarmD = new cloudwatch.Alarm(this, 'diskDAlarmD', { alarmName: 'alarm-ec2-dsk-d', metric: new cloudwatch.Metric({ namespace: 'CWAgent', metricName: 'LogicalDisk % Free Space', // メトリクス名 dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID(config.json側のappend_dimensionsで指定) instance: 'D:', // 対象ディスク objectname: 'LogicalDisk' // オブジェクト名(config.json側のmetrics_collectedで指定) }, statistic: 'Average', // 統計:平均 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 10, // アラームの閾値 evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD, // アラーム条件: より小さい treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); diskAlarmD.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 diskAlarmD.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先 // プロセス監視アラーム const svchostServiceAlarm = new cloudwatch.Alarm(this, 'SVCHostServiceAlarm', { alarmName: 'alarm-svchost', metric: new cloudwatch.Metric({ namespace: 'CWAgent', metricName: 'procstat_lookup pid_count', // メトリクス名(プロセスに関連付けられたプロセスIDの数) dimensionsMap: { InstanceId: ec2Instance, // 監視対象のEC2インスタンスID(config.json側のappend_dimensionsで指定) exe: 'svchost.exe', // 監視対象のプロセス名 pid_finder: 'native' // プロセスの検出方法:native(OSのネイティブAPIを使用) }, statistic: 'Min', // 統計: 最小 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値 evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件:閾値以下 treatMissingData: cloudwatch.TreatMissingData.BREACHING, // 欠落データを不正(しきい値を超えている)として処理 }); svchostServiceAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 svchostServiceAlarm.addOkAction(new actions.SnsAction(alarmTopic)); // アラームがOK状態に戻った時のSNS通知先 // イベントログ(システムログ)用のロググループのエラー監視アラーム const sytemLogErrorsAlarm = new cloudwatch.Alarm(this, 'SystemLogErrorsAlarm', { alarmName: 'os-eventlog-system', metric: new cloudwatch.Metric({ namespace: 'os-cloudwatchlogs', // カスタムメトリクスの名前空間(CloudwachLogsで作成した名前空間を指定) metricName: 'OS-Eventlog-System', // メトリクス名 statistic: 'Sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); sytemLogErrorsAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 // イベントログ(アプリケーションログ)用のロググループのエラー監視アラーム const appLogErrorsAlarm = new cloudwatch.Alarm(this, 'ApplicationLogErrorsAlarm', { alarmName: 'os-eventlog-application', metric: new cloudwatch.Metric({ namespace: 'os-cloudwatchlogs', // カスタムメトリクスの名前空間(CloudwachLogsで作成した名前空間を指定) metricName: 'OS-Eventlog-Application', // メトリクス名 statistic: 'Sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); appLogErrorsAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 // イベントログ(セキュリティログ)用のロググループのエラー監視アラーム const securityLogErrorsAlarm = new cloudwatch.Alarm(this, 'SecurityLogErrorsAlarm', { alarmName: 'os-eventlog-security', metric: new cloudwatch.Metric({ namespace: 'os-cloudwatchlogs', // カスタムメトリクスの名前空間(CloudwachLogsで作成した名前空間を指定) metricName: 'OS-Eventlog-Security', // メトリクス名 statistic: 'Sum', // 統計:合計 period: cdk.Duration.minutes(5), // メトリクスの収集間隔(期間):5分 }), threshold: 1, // アラームの閾値(1以上=失敗あり) evaluationPeriods: 1, datapointsToAlarm: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, // アラーム条件: 閾値以上 treatMissingData: cloudwatch.TreatMissingData.MISSING, // 欠落データを見つかりませんとして処理 }); securityLogErrorsAlarm.addAlarmAction(new actions.SnsAction(alarmTopic)); // アラーム発生時のSNS通知先 //================================================ // CloudWatchLogs作成 //================================================ // OSイベントログ(システムログ)用のロググループ const systemLogGroup = new logs.LogGroup(this, 'SystemLogGroup', { logGroupName: 'loggroup-os-eventlog-system', retention: logs.RetentionDays.ONE_MONTH, // 保持期間: 1ヵ月 removalPolicy: cdk.RemovalPolicy.DESTROY }); const systemErrorMetricFilter = new logs.MetricFilter(this, 'SystemErrorMetricFilter', { // イベントログ(システムログ)用メトリクスフィルター logGroup: systemLogGroup, filterName: 'filter-os-eventlog-system', // フィルター名 filterPattern: logs.FilterPattern.literal('?ERROR ?Error ?error ?FAIL ?Fail ?fail'), // フィルターパターン metricNamespace: 'os-cloudwatchlogs', // メトリクス名前空間 metricName: 'OS-Eventlog-System', // メトリクス名 metricValue: '1', // メトリクス値 defaultValue: 0 }); // OSイベントログ(アプリケーションログ)用のロググループ const applicationLogGroup = new logs.LogGroup(this, 'ApplicationLogGroup', { logGroupName: 'loggroup-os-eventlog-application', retention: logs.RetentionDays.ONE_MONTH, // 保持期間: 1ヵ月 removalPolicy: cdk.RemovalPolicy.DESTROY }); const applicationErrorMetricFilter = new logs.MetricFilter(this, 'ApplicationErrorMetricFilter', { // イベントログ(アプリケーションログ)用メトリクスフィルター logGroup: applicationLogGroup, filterName: 'filter-os-eventlog-application',        // フィルター名 filterPattern: logs.FilterPattern.literal('?ERROR ?Error ?error ?FAIL ?Fail ?fail'),    // フィルターパターン metricNamespace: 'os-cloudwatchlogs',      // メトリクス名前空間 metricName: 'OS-Eventlog-Application', // メトリクス名 metricValue: '1', // メトリクス値 defaultValue: 0 }); // OSイベントログ(セキュリティログ)用のロググループ const securityLogGroup = new logs.LogGroup(this, 'SecurityLogGroup', { logGroupName: 'loggroup-os-eventlog-security', retention: logs.RetentionDays.ONE_MONTH, // 保持期間: 1ヵ月 removalPolicy: cdk.RemovalPolicy.DESTROY }); const securityErrorMetricFilter = new logs.MetricFilter(this, 'SecurityErrorMetricFilter', { // イベントログ(セキュリティログ)用メトリクスフィルター logGroup: securityLogGroup, filterName: 'filter-os-eventlog-security', // フィルター名 filterPattern: logs.FilterPattern.literal('?ERROR ?Error ?error ?FAIL ?Fail ?fail'), // フィルターパターン metricNamespace: 'os-cloudwatchlogs', // メトリクス名前空間 metricName: 'OS-Eventlog-Security', // メトリクス名 metricValue: '1', // メトリクス値 defaultValue: 0 }); } }   Cloud Watch Agent Jsonサンプル { "agent": { "metrics_collection_interval": 60, "run_as_user": "System" }, "logs": { "logs_collected": { "windows_events": { "collect_list": [ { "event_name": "System", "event_levels": [ "ERROR", "WARNING", "INFORMATION" ], "log_group_name": "loggroup-os-eventlog-system", "log_stream_name": "{local_hostname}_{instance_id}" }, { "event_name": "Application", "event_levels": [ "ERROR", "WARNING", "INFORMATION" ], "log_group_name": "loggroup-os-eventlog-application", "log_stream_name": "{local_hostname}_{instance_id}" }, { "event_name": "Security", "event_levels": [ "ERROR", "WARNING", "INFORMATION", "CRITICAL" ], "log_group_name": "loggroup-os-eventlog-security", "log_stream_name": "{local_hostname}_{instance_id}" } ] } } }, "metrics": { "namespace": "CWAgent", "metrics_collected": { "Memory": { "measurement": [ "% Committed Bytes In Use" ], "metrics_collection_interval": 60 }, "LogicalDisk": { "measurement": [ "% Free Space" ], "metrics_collection_interval": 60, "resources": [ "*" ] }, "Processor": { "measurement": [ "% Processor Time" ], "metrics_collection_interval": 60, "resources": [ "*" ] }, "procstat": [ { "exe": "svchost.exe", "measurement": [ "pid_count" ], "metrics_collection_interval": 60 } ] }, "append_dimensions": { "InstanceId": "${aws:InstanceId}" } } }   まとめ 今回は、Windows EC2インスタンスの包括的な監視システムをAWS CDKで実装しました。 本実装ではリソースはマネジメントコンソールで作成する場合でも監視をAWS CDKを用いて構築することが出来ます。 IaCとして管理することで、環境間での一貫した監視設定の展開や、監視ルールの変更履歴管理も可能になります。また、CloudWatch Agent設定ファイルと組み合わせることで、Windows固有のメトリクスやログを効率的に収集・監視できます。 皆さんのお役に立てれば幸いです。
アバター
こんにちは、広野です。 まとまった数の PowerPoint 資料 (PPTX ファイル) を PDF に変換したくて、AWS Lambda 関数をつくってみました。 記事を 環境構築編 と Lambda 関数編 (本記事) に分けて説明します。 やりたかったこと 多くの PowerPoint 資料 (PPTX) があり、それを RAG (Amazon Bedrock Knowledge Bases) に食わせたい。のですが、RAG が PPTX をソースデータファイルとしてサポートしておらず、一度 PDF に変換しないといけない事情がありました。簡単に変換できるよう、Amazon S3 バケットに置いたら自動変換してくれる処理をつくりました。PPTX – PDF 変換には LibreOffice を使用します。 Amazon S3 バケットにファイルを置くと、イベント通知が発行されます。ファイルは input フォルダに置きます。 Amazon EventBridge ルールで、input フォルダ内の .pptx ファイルであれば AWS Lambda 関数を呼び出します。 Lambda 関数は、EventBridge から当該 PPTX ファイルのメタデータを受け取っているので、それをもとに PPTX ファイルを取得します。 Lambda 関数内で、LibreOffice をヘッドレスで (No GUI で) 実行し、PPTX を PDF に変換します。 作成された PDF ファイルを Amazon S3 バケットの output フォルダに保存します。名前は元ファイル名の拡張子が .pdf に変わっただけのものです。 LibreOffice について LibreOffice はオープンソースの Office ソフトウェアです。Word, Excel, PowerPoint などの Microsoft 製品と互換性があります。そのため、PowerPoint のファイルを扱うことができます。 ホーム | LibreOffice - オフィススイートのルネサンス ja.libreoffice.org この LibreOffice はヘッドレス、つまりコマンドで操作することができ、PowerPoint を PDF 変換する機能を利用します。 環境について 環境については、以下の記事をご覧ください。 PowerPoint ファイルを PDF に自動変換する AWS Lambda 関数をつくる -環境構築編- まとまった数の PowerPoint 資料 (PPTX ファイル) を PDF に変換したくて、AWS Lambda 関数をつくってみました。 blog.usize-tech.com 2026.01.05 環境をご理解いただいた上で、本記事の Lambda 関数の説明をお読みいただくことをお勧めします。   Lambda 関数について Lambda 関数はコンテナ化するので、コンテナイメージの中に Python スクリプト (.py) が格納されています。LibreOffice 含む必要なモジュールがインストールされたコンテナイメージに。 必要なソースコードは以下の構成になっています。 Dockerfile ビルドフェーズで実行するコマンドが書かれています。主にコンテナイメージをビルドし、Amazon ECR に保存するのが目的です。 buildspec.yml ビルドフェーズでコンテナのベースイメージにモジュールをインストールしたり配置したりするコマンドが書かれています。 lambda_function.py Lambda 関数です。今回は Python で書かれており、Amazon EventBridge ルールから Amazon S3 オブジェクトのメタデータを受け取り、Amazon S3 へのファイル読み書きや LibreOffice の PDF 変換コマンドを実行します。 cfn_container_lambda.yml ビルドフェーズでデプロイされた Amazon ECR 内のコンテナイメージと、Lambda 関数を関連付けます。また、Amazon S3 バケットから発行されたイベント通知を受け取るための Amazon EventBridge ルールをデプロイします。 以下のように、AWS CodeCommit リポジトリにはこれらソースコードを特にフォルダー分けせず放り込んでいます。AWS CodeCommit リポジトリ内の main ブランチのソースコードが更新されると、 環境構築編 で構築した CI/CD パイプラインが動き出しコンテナ Lambda 関数が自動でデプロイされる仕組みです。 中のコードを紹介します。ところどころインラインでコメントします。 Dockerfile FROM public.ecr.aws/shelf/lambda-libreoffice-base:25.8-python3.14-x86_64 COPY lambda_function.py ${LAMBDA_TASK_ROOT} CMD [ "lambda_function.handler" ] ものすごくシンプルです。 元々は自分で Amazon Linux 2023 や Python 用のベースイメージを使用していろいろインストールして動くものを作ったのですが、後から有志の方が LibreOffice 用のベースイメージを公開してくれていることに気付きました。ほんとよく出来ているので、それを使わせてもらっています。 GitHub - shelfio/libreoffice-lambda-base-image Contribute to shelfio/libreoffice-lambda-base-image development by creating an account on GitHub. github.com この GitHub の中を覗くと、このベースイメージを作成するためのコマンドも書いてあり、もしフォントを追加したいなどあれば自分で加工したものを作れると思います。※フォント追加の必要性については後述します。 buildspec.yml version: 0.2 phases: pre_build: commands: - echo Logging in to Amazon ECR... - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com build: commands: - echo Building the Docker image... - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG post_build: commands: - echo Pushing the Docker image... - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG artifacts: files: - cfn_container_lambda.yml 環境変数多めです。どうしてもコマンド実行の際に環境特有の情報が必要になるので。それ以外は特別なことはしていません。 lambda_function.py import json import os import subprocess import boto3 s3 = boto3.client("s3") WORKDIR = "/tmp" def handler(event, context): print("Event:", json.dumps(event)) # EventBridge - S3 情報取得 bucket = event["detail"]["bucket"]["name"] key = event["detail"]["object"]["key"] filename = os.path.basename(key) local_pptx = f"{WORKDIR}/{filename}" # S3 - /tmp にダウンロード s3.download_file(bucket, key, local_pptx) # LibreOffice変換 subprocess.run([ 'libreoffice25.8', '--headless', '--invisible', '--nodefault', '--view', '--nolockcheck', '--nologo', '--norestore', '--convert-to', 'pdf', '--outdir', WORKDIR, local_pptx ], check=True) pdf_name = filename.replace(".pptx", ".pdf") local_pdf = f"{WORKDIR}/{pdf_name}" # 出力 S3 の output フォルダへ output_key = f"output/{pdf_name}" s3.upload_file(local_pdf, bucket, output_key) return { "status": "ok", "input": key, "output": output_key } こちらも特段特別なことはしていません。 Amazon EventBridge ルールから Amazon S3 の PPTX ファイルのメタデータを受け取り、それを元にファイルを取得。LibreOffice をヘッドレス実行して PDF 変換します。PDF を Amazon S3 バケットに戻す処理だけです。 cfn_container_lambda.yml インラインでコメントします。Lambda 関数の箱の設定です。中身はコンテナイメージになるのでここには記述されません。 AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a Lambda function on container and a relevant IAM role. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# # 以下のパラメータは自動で AWS CodePipeline から環境変数を受け取ります。 # デフォルト値は気にする必要はありませんが、定義を削除するとエラーになります。 Parameters: SystemName: Type: String Description: System name. use lower case only. (e.g. example) Default: example MaxLength: 10 MinLength: 1 SubName: Type: String Description: System sub name. use lower case only. (e.g. prod or dev) Default: dev MaxLength: 10 MinLength: 1 ImageTag: Type: String Default: xxxxxxxxxxxxxxxxxxxx MaxLength: 100 MinLength: 1 ImgRepoName: Type: String Default: xxxxxxxxxxxxxxxxxxxx MaxLength: 100 MinLength: 1 S3BucketDocs: Type: String Default: xxxxxxxxxxxxxxxxxxxx MaxLength: 200 MinLength: 1 Resources: # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# Lambda: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${SystemName}-${SubName}-pptx-pdf-conv Description: !Sub Lambda Function to convert pptx to pdf for ${SystemName}-${SubName} PackageType: Image Timeout: 60 # メモリは 1024 MB にしました。670 MB ほど使用していましたので。512 MB だと処理に時間がかかりました。 # 1024 MB で、1 MB の PPTX の処理が 20 秒ほどかかりました。 # 10 MB を超える PPTX ファイルだと 1024 MB メモリをフルに消費し、時間も 60 秒タイムアウトを超過してしまいました。 # 取り扱うファイルサイズによってメモリサイズとタイムアウトは調整する必要があります。 MemorySize: 1024 EphemeralStorage: Size: 512 Architectures: - x86_64 # 環境変数として HOME を /tmp として設定しないと LibreOffice の実行が失敗します。 Environment: Variables: HOME: "/tmp" Role: !GetAtt LambdaRole.Arn Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} # ここで、コンテナイメージを Lambda 関数にするように関連付けています。 Code: ImageUri: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ImgRepoName}:${ImageTag} DependsOn: - LambdaRole # ------------------------------------------------------------# # Lambda Role (IAM) # ------------------------------------------------------------# LambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub LambdaRole-pptx-pdf-conv-${SystemName}-${SubName} Description: This role allows Lambda functions to access S3 bucket. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess Policies: - PolicyName: !Sub LambdaPolicy-pptx-pdf-conv-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "s3:PutObject" - "s3:GetObject" Resource: - !Sub "arn:aws:s3:::${S3BucketDocs}/*" # ------------------------------------------------------------# # EventBridge Rule for starting Lambda function # ------------------------------------------------------------# EventBridgeRuleStartLambda: Type: AWS::Events::Rule Properties: Name: !Sub ${SystemName}-${SubName}-pptx-pdf-conv-start-lambda Description: !Sub This rule starts pptx pdf converter Lambda function for ${SystemName}-${SubName}. The trigger is the S3 event notifications. EventBusName: !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/default" EventPattern: source: - "aws.s3" detail-type: - "Object Created" detail: bucket: name: - !Ref S3BucketDocs object: key: - wildcard: "input/*.pptx" State: ENABLED Targets: - Arn: !GetAtt Lambda.Arn Id: !Sub ${SystemName}-${SubName}-pptx-pdf-conv-start-lambda RoleArn: !GetAtt EventBridgeRuleLambdaRole.Arn DependsOn: - EventBridgeRuleLambdaRole # ------------------------------------------------------------# # EventBridge Rule Invoke Lambda Role (IAM) # ------------------------------------------------------------# EventBridgeRuleLambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub EventBridgeLambdaRole-${SystemName}-${SubName} Description: !Sub This role allows EventBridge to invoke pptx pdf converter Lambda for ${SystemName}-${SubName}. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: !Sub EventBridgeLambdaPolicy-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "lambda:InvokeFunction" Resource: - !GetAtt Lambda.Arn DependsOn: - Lambda   変換した PDF  結局、この方法で PPTX を PDF に変換するとどうなるのか。PPTX と PDF のスクリーンショットを撮って比較してみました。 コンテナイメージに Noto Sans CJK フォントが入っていたので、日本語変換は問題ありません。しかし元々使用していたフォントと異なるので、ところどころにレイアウト崩れが起きてしまいました。見た目を気にする資料だとフォントを合わせないと実用的ではなさそうです。フォント以外は特段問題なさそうだと感じました。 PPTX PDF Noto Sans フォントの Noto は、No Tofu の意味です。環境にフォントが無いと文字化けした文字が四角形 (=豆腐) で表示されてしまうことがありましたが、もう豆腐は無くしたいという思いから、No Tofu -> Noto というフォントが作られたそうです。こんなところで日本語が使われていて面白いですね。   まとめ いかがでしたでしょうか。 本記事はコンテナ Lambda 関数の中身にフォーカスしていました。簡単でしたが LibreOffice の活用に触れられたと思います。アイデア次第で他の用途にも使えると思います。 本記事が皆様のお役に立てれば幸いです。
アバター
こんにちは、広野です。 まとまった数の PowerPoint 資料 (PPTX ファイル) を PDF に変換したくて、AWS Lambda 関数をつくってみました。 記事を環境構築編 (本記事) と Lambda 関数編 に分けて説明します。 やりたかったこと 多くの PowerPoint 資料 (PPTX) があり、それを RAG (Amazon Bedrock Knowledge Bases) に食わせたい。のですが、RAG が PPTX をソースデータファイルとしてサポートしておらず、一度 PDF に変換しないといけない事情がありました。簡単に変換できるよう、Amazon S3 バケットに置いたら自動変換してくれる処理をつくりました。PPTX – PDF 変換には LibreOffice を使用します。 Amazon S3 バケットにファイルを置くと、イベント通知が発行されます。ファイルは input フォルダに置きます。 Amazon EventBridge ルールで、input フォルダ内の .pptx ファイルであれば AWS Lambda 関数を呼び出します。 Lambda 関数は、EventBridge から当該 PPTX ファイルのメタデータを受け取っているので、それをもとに PPTX ファイルを取得します。 Lambda 関数内で、LibreOffice をヘッドレスで (No GUI で) 実行し、PPTX を PDF に変換します。 作成された PDF ファイルを Amazon S3 バケットの output フォルダに保存します。名前は元ファイル名の拡張子が .pdf に変わっただけのものです。 LibreOffice について LibreOffice はオープンソースの Office ソフトウェアです。Word, Excel, PowerPoint などの Microsoft 製品と互換性があります。そのため、PowerPoint のファイルを扱うことができます。 ホーム | LibreOffice - オフィススイートのルネサンス ja.libreoffice.org この LibreOffice はヘッドレス、つまりコマンドで操作することができ、PowerPoint を PDF 変換する機能を利用します。   環境について この実行環境は、大きく以下の 3つに分かれています。 Amazon S3 バケットに PPTX を保存し、PDF を受け取るインターフェースとしての Amazon S3 PPTX を PDF に変換する AWS Lambda 関数 -> Lambda 関数編の記事 で詳細を説明します。 AWS Lambda 関数をビルド、デプロイするための CI/CD 環境 全体像は以下の図のようになります。 図の右上の方に、インターフェースとしての Amazon S3 バケットがあります。 図の右下の方に、PPTX を PDF に変換する AWS Lambda 関数があります。Lambda 関数を呼び出すための Amazon EventBridge ルールとセットで、CI/CD パイプラインからデプロイされます。 ここで、なぜ CI/CD パイプラインを構築しているかというと。 LibreOffice の処理は通常の Lambda 関数にとっては重い処理になるので、コンテナ Lambda を使用することにしました。Docker コンテナイメージを作成する必要があり、イメージ置き場としての Amazon ECR、イメージをビルドする CI/CD パイプラインを構築しています。 ソースコードは大きく 2種類に分かれます。 ビルドフェーズで使用するコンテナイメージ構築用ファイル デプロイフェーズで使用する AWS CloudFormation テンプレート これらを開発者が AWS CodeCommit で管理しており、ソースコードが更新されると CI/CD パイプラインが動き出します。 ビルドフェーズでビルドされたコンテナイメージ (Lambda 関数の実体) は Amazon ECR に保存されます。そのままでは Lambda 関数として機能しないので、デプロイフェーズで AWS CloudFormation により Lambda 関数としてデプロイされます。   環境構築 (AWS CloudFormation) 上述の環境を AWS CloudFormation でデプロイしています。 Amazon S3 バケットに PPTX を保存し、PDF を受け取るインターフェースとしての Amazon S3 AWS Lambda 関数をビルド、デプロイするための CI/CD 環境 これができあがると、AWS CodeCommit でコンテナイメージを含む Lambda 関数コードを自由に開発、デプロイできます。 AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a CI/CD environment for a container Lambda function. It provides converting pptx to PDF. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. use lower case only. (e.g. example) Default: example MaxLength: 10 MinLength: 1 SubName: Type: String Description: System sub name. use lower case only. (e.g. prod or dev) Default: dev MaxLength: 10 MinLength: 1 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "General Configuration" Parameters: - SystemName - SubName Resources: # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# S3BucketDocs: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${SystemName}-${SubName}-pptx-pdf-conv-docs LifecycleConfiguration: Rules: - Id: AutoDelete Status: Enabled ExpirationInDays: 14 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true NotificationConfiguration: EventBridgeConfiguration: EventBridgeEnabled: true Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} S3BucketArtifact: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${SystemName}-${SubName}-pptx-pdf-conv-artifact LifecycleConfiguration: Rules: - Id: AutoDelete Status: Enabled ExpirationInDays: 14 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} S3BucketLogs: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${SystemName}-${SubName}-pptx-pdf-conv-logs LifecycleConfiguration: Rules: - Id: AutoDelete Status: Enabled ExpirationInDays: 365 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} # ------------------------------------------------------------# # ECR # ------------------------------------------------------------# EcrRepositoryContainerLambda: Type: AWS::ECR::Repository Properties: RepositoryName: !Sub ${SystemName}-${SubName}-pptx-pdf-conv EncryptionConfiguration: EncryptionType: AES256 ImageScanningConfiguration: ScanOnPush: true ImageTagMutability: IMMUTABLE LifecyclePolicy: LifecyclePolicyText: | { "rules": [ { "rulePriority": 1, "description": "Keep only 5 images, expire all others", "selection": { "tagStatus": "any", "countType": "imageCountMoreThan", "countNumber": 5 }, "action": { "type": "expire" } } ] } EmptyOnDelete: true Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} # ------------------------------------------------------------# # CodeCommit Repository # ------------------------------------------------------------# CodeCommitRepoContainerLambda: Type: AWS::CodeCommit::Repository Properties: RepositoryName: !Sub ${SystemName}-${SubName}-pptx-pdf-conv RepositoryDescription: !Sub pptx pdf converter for ${SystemName}-${SubName} Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} # ------------------------------------------------------------# # CodePipeline # ------------------------------------------------------------# CodePipelineContainerLambda: Type: AWS::CodePipeline::Pipeline Properties: Name: !Sub ${SystemName}-${SubName}-pptx-pdf-conv PipelineType: V2 ArtifactStore: Location: !Ref S3BucketArtifact Type: S3 RestartExecutionOnUpdate: false RoleArn: !GetAtt CodePipelineServiceRoleContainerLambda.Arn Stages: - Name: Source Actions: - Name: Source RunOrder: 1 ActionTypeId: Category: Source Owner: AWS Version: 1 Provider: CodeCommit Configuration: RepositoryName: !GetAtt CodeCommitRepoContainerLambda.Name BranchName: main PollForSourceChanges: false OutputArtifactFormat: CODEBUILD_CLONE_REF Namespace: SourceVariables OutputArtifacts: - Name: Source - Name: Build Actions: - Name: Build RunOrder: 1 Region: !Sub ${AWS::Region} ActionTypeId: Category: Build Owner: AWS Version: 1 Provider: CodeBuild Configuration: ProjectName: !Ref CodeBuildProjectContainerLambda BatchEnabled: false EnvironmentVariables: | [ { "name": "IMAGE_TAG", "type": "PLAINTEXT", "value": "#{codepipeline.PipelineExecutionId}" } ] Namespace: BuildVariables InputArtifacts: - Name: Source OutputArtifacts: - Name: Build - Name: Deploy Actions: - ActionTypeId: Category: Deploy Owner: AWS Provider: CloudFormation Version: 1 Configuration: StackName: !Sub ${SystemName}-${SubName}-pptx-pdf-conv-lambda Capabilities: CAPABILITY_NAMED_IAM RoleArn: !GetAtt CodePipelineDeployCreateUpdateRoleContainerLambda.Arn ActionMode: CREATE_UPDATE TemplatePath: Build::cfn_container_lambda.yml ParameterOverrides: !Sub '{"SystemName":"${SystemName}","SubName":"${SubName}","ImageTag":"#{codepipeline.PipelineExecutionId}","S3BucketDocs":"${S3BucketDocs}","ImgRepoName":"${EcrRepositoryContainerLambda}"}' InputArtifacts: - Name: Build Name: CreateOrUpdate RoleArn: !GetAtt CodePipelineDeployCreateUpdateActionRoleContainerLambda.Arn RunOrder: 1 Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} DependsOn: - CodePipelineServiceRoleContainerLambda - CodeBuildProjectContainerLambda - CodePipelineDeployCreateUpdateActionRoleContainerLambda - EcrRepositoryContainerLambda # ------------------------------------------------------------# # CodePipeline Service Role (IAM) # ------------------------------------------------------------# CodePipelineServiceRoleContainerLambda: Type: AWS::IAM::Role Properties: RoleName: !Sub CpServiceRoleContainerLambda-${SystemName}-${SubName} Description: This role allows CodePipeline to call each stages. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - codepipeline.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: !Sub CpServicePolicyContainerLambda-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "codecommit:CancelUploadArchive" - "codecommit:GetBranch" - "codecommit:GetCommit" - "codecommit:GetRepository" - "codecommit:GetUploadArchiveStatus" - "codecommit:UploadArchive" Resource: !GetAtt CodeCommitRepoContainerLambda.Arn - Effect: Allow Action: - "codebuild:BatchGetBuilds" - "codebuild:StartBuild" - "codebuild:BatchGetBuildBatches" - "codebuild:StartBuildBatch" Resource: "*" - Effect: Allow Action: - "cloudwatch:*" - "s3:*" Resource: "*" - Effect: Allow Action: - "lambda:InvokeFunction" - "lambda:ListFunctions" Resource: "*" - Effect: Allow Action: "sts:AssumeRole" Resource: - !GetAtt CodePipelineDeployCreateUpdateActionRoleContainerLambda.Arn DependsOn: - CodeCommitRepoContainerLambda - CodePipelineDeployCreateUpdateActionRoleContainerLambda # ------------------------------------------------------------# # CodePipeline Deploy Create Update Role (IAM) # ------------------------------------------------------------# CodePipelineDeployCreateUpdateRoleContainerLambda: Type: AWS::IAM::Role Properties: RoleName: !Sub CpCrUpdRoleContainerLambda-${SystemName}-${SubName} AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: cloudformation.amazonaws.com Version: "2012-10-17" Path: / Policies: - PolicyName: !Sub CpCrUpdPolicyContainerLambda-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Action: "*" Effect: Allow Resource: "*" # ------------------------------------------------------------# # CodePipeline Deploy Create Update Action Role (IAM) # ------------------------------------------------------------# CodePipelineDeployCreateUpdateActionRoleContainerLambda: Type: AWS::IAM::Role Properties: RoleName: !Sub CpCrUpdActionRoleContainerLambda-${SystemName}-${SubName} AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: AWS: Fn::Join: - "" - - "arn:" - Ref: AWS::Partition - ":iam::" - Ref: AWS::AccountId - :root Version: "2012-10-17" Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess Policies: - PolicyName: !Sub CpCrUpdPolicyContainerLambda-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Action: iam:PassRole Effect: Allow Resource: !GetAtt CodePipelineDeployCreateUpdateRoleContainerLambda.Arn - Action: - s3:GetBucket* - s3:GetObject* - s3:List* Effect: Allow Resource: - !Sub arn:aws:s3:::${S3BucketArtifact} - !Sub arn:aws:s3:::${S3BucketArtifact}/* DependsOn: - CodePipelineDeployCreateUpdateRoleContainerLambda - S3BucketArtifact # ------------------------------------------------------------# # EventBridge Rule for Starting CodePipeline # ------------------------------------------------------------# EventBridgeRuleStartCodePipelineContainerLambda: Type: AWS::Events::Rule Properties: Name: !Sub ${SystemName}-${SubName}-pptx-pdf-conv-start-codepipeline Description: !Sub This rule starts pptx pdf converter CodePipeline for ${SystemName}-${SubName}. The trigger is the source code change in CodeCommit. EventBusName: !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/default" EventPattern: source: - "aws.codecommit" detail-type: - "CodeCommit Repository State Change" resources: - !GetAtt CodeCommitRepoContainerLambda.Arn detail: event: - referenceCreated - referenceUpdated referenceType: - branch referenceName: - main RoleArn: !GetAtt EventBridgeRuleStartCpRoleContainerLambda.Arn State: ENABLED Targets: - Arn: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipelineContainerLambda}" Id: !Sub ${SystemName}-${SubName}-pptx-pdf-conv-start-codepipeline RoleArn: !GetAtt EventBridgeRuleStartCpRoleContainerLambda.Arn DependsOn: - EventBridgeRuleStartCpRoleContainerLambda # ------------------------------------------------------------# # EventBridge Rule Start CodePipeline Role (IAM) # ------------------------------------------------------------# EventBridgeRuleStartCpRoleContainerLambda: Type: AWS::IAM::Role Properties: RoleName: !Sub EventBridgeStartCpRoleContainerLambda-${SystemName}-${SubName} Description: !Sub This role allows EventBridge to start pptx pdf converter CodePipeline for ${SystemName}-${SubName}. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: !Sub EventBridgeStartCpPolicyContainerLambda-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "codepipeline:StartPipelineExecution" Resource: - !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipelineContainerLambda}" DependsOn: - CodePipelineContainerLambda # ------------------------------------------------------------# # CodeBuild Project # ------------------------------------------------------------# CodeBuildProjectContainerLambda: Type: AWS::CodeBuild::Project Properties: Name: !Sub ${SystemName}-${SubName}-pptx-pdf-conv Description: !Sub The build project for ${SystemName}-${SubName}-pptx-pdf-conv ResourceAccessRole: !GetAtt CodeBuildResourceAccessRoleContainerLambda.Arn ServiceRole: !GetAtt CodeBuildServiceRoleContainerLambda.Arn ConcurrentBuildLimit: 1 Visibility: PRIVATE Source: Type: CODEPIPELINE SourceVersion: refs/heads/main Environment: Type: LINUX_CONTAINER ComputeType: BUILD_GENERAL1_SMALL Image: "aws/codebuild/amazonlinux-x86_64-standard:5.0" ImagePullCredentialsType: CODEBUILD PrivilegedMode: true EnvironmentVariables: - Name: AWS_DEFAULT_REGION Type: PLAINTEXT Value: !Sub ${AWS::Region} - Name: AWS_ACCOUNT_ID Type: PLAINTEXT Value: !Sub ${AWS::AccountId} - Name: IMAGE_REPO_NAME Type: PLAINTEXT Value: !Ref EcrRepositoryContainerLambda TimeoutInMinutes: 30 QueuedTimeoutInMinutes: 60 Artifacts: Type: CODEPIPELINE Cache: Type: NO_CACHE LogsConfig: CloudWatchLogs: GroupName: !Sub /aws/codebuild/${SystemName}-${SubName}-pptx-pdf-conv Status: ENABLED S3Logs: EncryptionDisabled: true Location: !Sub arn:aws:s3:::${S3BucketLogs}/codebuildBuildlog Status: ENABLED Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} DependsOn: - EcrRepositoryContainerLambda - CodeBuildResourceAccessRoleContainerLambda - CodeBuildServiceRoleContainerLambda # ------------------------------------------------------------# # CodeBuild Resource Access Role (IAM) # ------------------------------------------------------------# CodeBuildResourceAccessRoleContainerLambda: Type: AWS::IAM::Role Properties: RoleName: !Sub CbResourceAccessRoleContainerLambda-${SystemName}-${SubName} Description: This role allows CodeBuild to access CloudWatch Logs and Amazon S3 artifacts for the project's builds. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - codebuild.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: !Sub CbResourceAccessPolicyContainerLambda-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${SystemName}-${SubName}-pptx-pdf-conv" - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${SystemName}-${SubName}-pptx-pdf-conv:*" - Effect: Allow Action: - "s3:PutObject" - "s3:GetObject" - "s3:GetObjectVersion" - "s3:GetBucketAcl" - "s3:GetBucketLocation" Resource: - !Sub arn:aws:s3:::${S3BucketLogs} - !Sub arn:aws:s3:::${S3BucketLogs}/* # ------------------------------------------------------------# # CodeBuild Service Role (IAM) # ------------------------------------------------------------# CodeBuildServiceRoleContainerLambda: Type: AWS::IAM::Role Properties: RoleName: !Sub CbServiceRoleContainerLambda-${SystemName}-${SubName} Description: This role allows CodeBuild to interact with dependant AWS services. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - codebuild.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser Policies: - PolicyName: !Sub CbServicePolicyContainerLambda-${SystemName}-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "codecommit:GitPull" Resource: !GetAtt CodeCommitRepoContainerLambda.Arn - Effect: Allow Action: - "ssm:GetParameters" Resource: - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SystemName}_${SubName}_*" - Effect: Allow Action: - "s3:*" Resource: - !Sub arn:aws:s3:::${S3BucketArtifact} - !Sub arn:aws:s3:::${S3BucketArtifact}/* - !Sub arn:aws:s3:::${S3BucketLogs} - !Sub arn:aws:s3:::${S3BucketLogs}/* - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${SystemName}-${SubName}-pptx-pdf-conv" - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${SystemName}-${SubName}-pptx-pdf-conv:*" - Effect: Allow Action: - "codebuild:CreateReportGroup" - "codebuild:CreateReport" - "codebuild:UpdateReport" - "codebuild:BatchPutTestCases" - "codebuild:BatchPutCodeCoverages" Resource: - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${SystemName}-${SubName}-pptx-pdf-conv*" DependsOn: - CodeCommitRepoContainerLambda - S3BucketArtifact - S3BucketLogs   関連記事 Lambda 関数の中身については、以下の記事で紹介しています。 PowerPoint ファイルを PDF に自動変換する AWS Lambda 関数をつくる -Lambda関数編- まとまった数の PowerPoint 資料 (PPTX ファイル) を PDF に変換したくて、AWS Lambda 関数をつくってみました。 blog.usize-tech.com 2026.01.05   まとめ いかがでしたでしょうか。 本記事はコンテナ Lambda 関数の CI/CD 環境構築にフォーカスしていましたので、他の用途にも使えると思います。 本記事が皆様のお役に立てれば幸いです。
アバター
近年、クラウドサービスの選択肢はますます多様化しており、さまざまなクラウドが活用されています。 世界のクラウドプロバイダーのシェア上位3社を見ると、AWSが29%、Microsoft Azureが20%、Google Cloudが13%となっており※、 Microsoft AzureやGoogle Cloud Platformも成長を続けていますが、依然としてAWSがトップの座を維持しています。 (※2025年第3四半期データ  Cloud Market Growth Rate Rises Again in Q3; Biggest Ever Sequential Increase | Synergy Research Group ) 実際、当チームでもクラウド案件の多くは引き続きAWSが中心です。 そこで本記事では、LifeKeeperによる可用性対応の観点から、AWSでよく採用される代表的な構成パターンについて紹介します。 AWS環境で高可用性設計を検討されている方の参考になれば幸いです。 AWS OSごとの基本構成 Amazon EC2で冗長化構成をとる場合のOSごとの基本構成は以下の通りです。(稼働系、待機系ノード間でデータ共有を行う場合を想定。) 基本的に他のクラウドやオンプレミスの仮想環境と大きく変わりません。 Windows環境の場合はWindows標準機能のWSFCを使用することでコストを抑えた高可用性を確保することができます。 Linux Windows LifeKeeper+DataKeeperの組み合わせ LifeKeeper+DataKeeperの組み合わせ WSFC+DataKeeperの組み合わせ ※DataKeeperによるデータレプリケーションは必須ではなく、LifeKeeperのみの構成も可能です。 ※図では省略しておりますが、可用性の観点から、各クラスターノードを別々のアベイラビリティゾーン(AZ)に配置することで、物理的な障害発生時にもシステム停止リスクを最小限に抑えることが可能です。 ルートテーブルシナリオ(仮想IPとルートテーブルによる制御) この構成はクラスターを同じVPC内のクライアントから接続される際によく用いられます。 クライアント(クラスタノードと通信するマシン)は仮想IPに向けて通信することでActiveノードに到達できます。 AWS環境でAZを跨ぐとサブネットも跨いでしまうので、オンプレミスのように仮想IPだけではクライアントは正しくActiveノードへ到達できません。 そこで、VPCのCIDR外の仮想IPをルートテーブルに登録し、転送先のActive/StandbyノードのENIをクラスターの切り替え時にLifeKeeperからAWS CLIを介して書き換えることで、クライアントは常にActiveノードに到達できます。 <概要図>     ➀VPCのCIDR外の仮想IPアドレス(図では10.1.0.10)を用意して、クライアントから仮想IPに向けて通信します。 ②ルートテーブルにはあらかじめ仮想IPのTaegetとして稼働系のENI(図では10.0.2.4)を指定しておくことで、 クライアントからの通信は稼働系へ到達します。 ③フェイルオーバー時には、LifeKeeperから自動的にAWS CLIが実行され、ルートテーブルの仮想IPのターゲットが待機系ENIに書き換えられます。以降はクライアントからの通信は待機系に到達します。 この構成で利用必須となるRecovery Kit: Recovery Kit for EC2、Recovery Kit for IP Address ※注意 :AWSの仕様上、クライアントはクラスタと同じVPCに存在している必要があります。 仮想IPはVPCのCIDR外で割り当てる必要があります。   ▼参考URL ・Linux AWS EC2リソースの作成(ルートテーブルシナリオ) – LifeKeeper for Linux LIVE – 10.0 Recovery Kit for EC2™ 管理ガイド – LifeKeeper for Linux LIVE – 10.0 ・Windows Recovery Kit for Amazon EC2™ 管理ガイド – LifeKeeper for Windows LIVE – 10.0 ルートテーブルシナリオにAWS Transit Gatewayを組み合わせた制御 ルートテーブルシナリオにAWS Transit Gatewayを組み合わせることで、VPC外(オンプレミスや別VPC)からもクライアント通信に対応できます。 例えば、JP1等の統合運用管理ツール、HULFTなどのファイル転送ソフトでクライアントがVPC外にいる場合、この構成が有効です。 <概要図> ※クライアントはVPC外となりますが、仮想IPを使用したクラスターへの通信経路はルートテーブルシナリオと同様になります。 この構成で利用必須となるRecovery Kit: Recovery Kit for EC2、Recovery Kit for IP Address ※注意 :Transit Gateway向きのルートテーブル設定を行っておく必要があります。   ▼参考URL ・Linux AWS Direct Connect クイックスタートガイド – LifeKeeper for Linux LIVE – 10.0 Recovery Kit for EC2™ 管理ガイド – LifeKeeper for Linux LIVE – 10.0 ・Windows AWS Direct Connect クイックスタートガイド – LifeKeeper for Windows LIVE – 10.0 Recovery Kit for Amazon EC2™ 管理ガイド – LifeKeeper for Windows LIVE – 10.0 Route53のAレコード書き換えによる制御 Transit Gatewayが使えない場合などに、DNSサービスのRoute53での名前解決を利用する構成です。 クライアントはRoute53により名前解決された実IPに向けて通信することで、Activeノードへ到達できます。 <概要図> ➀クライアントからホスト名でクラスターノードにアクセスし、Route53で名前解決を行い、 名前解決した実IPアドレスで稼働系ノードにアクセスします。 ②Route53のAレコードには、稼働系ノードのIPアドレス(図では10.0.2.4)を指定しておくことで、クライアントからの通信は稼働系へ到達します。 ③フェイルオーバ時には、LifeKeeperからAWS CLIを実行し、Route53のAレコードを待機系ノードのIPアドレス(図では10.0.4.4)へ書き換えることで、以降のクライアントからの通信は待機系に到達します。 この構成で利用必須となるRecovery Kit: Recovery Kit for Amazon Route 53™、Recovery Kit for IP Address ※注意 :クライアントはホスト名(FQDN)でアクセスできることが前提になります。 LifeKeeper( Recovery Kit for Amazon Route 53™ )に登録するホストゾーン名がパブリックホストゾーン、プライベートホストゾーンで複数存在している場合、 Recovery Kit for Amazon Route 53™によるリソース作成は現状不可となりますのでご注意ください。 (参考: 同名のホストゾーンは使えない!? Amazon Route 53リソース作成時の注意点 – TechHarmony ) ⇒上記問題発覚後、サイオステクノロジー社に改善提案を出したところ、今後改善予定で動いているとのこと。 (2025年12月時点リリース時期未定)   ▼参考URL ・Linux AWS Route53 シナリオ – LifeKeeper for Linux LIVE – 10.0 Recovery Kit for Amazon Route 53™ 管理ガイド – LifeKeeper for Linux LIVE – 10.0 AWS Route 53リソースの作成 – LifeKeeper for Linux LIVE – 10.0 ・Windows Recovery Kit for Amazon Route 53™ 管理ガイド – LifeKeeper for Windows LIVE – 10.0            インターネットに出られない環境からプロキシ経由でRoute53にアクセスする方法 | ビジネス継続とITについて考える NLBのヘルスチェックによる制御 NLB(Network Load Balancer)のヘルスチェックを利用した構成です。 セキュリティ要件により、前述のようなAWS CLIによる構成変更が実施できない、インターネットに接続していない環境でクライアントからDNS名でアクセスしたい場合にはこの構成をご検討下さい。 クライアントからはNLBのDNS名でアクセスし (AWS内部のRoute 53経由で解決)、NLBのヘルスチェックとLB Health Check Kitを組み合わせて、ヘルスプローブを受け取ったノードにトラフィックを転送することで接続先の切り替えを実現します。 <概要図> ➀クライアントはNLBのDNS名とアプリケーションのポート番号 で接続を試みます。(図ではXXXX-nlb1-YYYY.elb.region.amazonaws.comと1521) (DNS 名はAWS内部のRoute 53経由でNLBのサブネットのIPアドレスに変換されます) ②NLBには、プロトコルとポートに対してどのターゲットグループへ転送するかが登録されています。このとき、どのノードがヘルスプローブに応答するかを確認します。 ③アクティブノードではNLBのヘルスプローブに応答します。LifeKeeperによってヘルスプローブに応答するLB Health Checkリソースは常にひとつのインスタンスでのみアクティブになっているため、NLBのヘルスプローブに応答するのはアクティブノードだけです。つまり、NLBは常にアクティブノードだけにトラフィックを転送します。 ④NLBは、Clientからの接続要求を、アクティブノードに転送します。そのため最終的に接続要求は、宛先アドレスがNLBのアドレスからアクティブノードの実IPアドレス に書き換えられて、アクティブノードに到達します。 この構成で利用必須となるRecovery Kit: LB Health Check Kit、Recovery Kit for IP Address   ▼参考URL ・Linux AWS – Network Load Balancerの使用 – LifeKeeper for Linux LIVE – 10.0 AWS Network Load Balancerシナリオ – LifeKeeper for Linux LIVE – 10.0 ・Windows AWSでロードバランサーを使用した構成 – LifeKeeper for Windows LIVE – 10.0 さいごに 今回は代表的な構成についてご紹介しましたが、 その他にもクロスリージョン構成やAWS Outpostsで共有ディスクを冗長化させるなど様々な構成にも対応しています。 ▼参考URL ・AWS Elastic IPシナリオ構成: AWS Elastic IPシナリオ – LifeKeeper for Linux LIVE – 10.0 ・クロスリージョン構成: [Linux][Windows] AWSのクロスリージョン環境において仮想IPで通信できる構成をサポート – SIOS LifeKeeper/DataKeeper User Portal ・Amazon FSx for NetApp ONTAP利用構成: Amazon FSx for NetApp ONTAPのサポート開始について | ビジネス継続とITについて考える ・AWS Outposts ラック利用構成: AWS Outposts ラックでの可用性を高めるHAクラスター構成|ユースケース|サイオステクノロジー株式会社 この記事で紹介されていない構成につきましては弊社やサイオステクノロジー社にお問い合わせ頂くことを推奨いたします 本記事がAWS環境における冗長化の参考になりましたら幸いです!
アバター
こんにちは、高坂です。 前回の記事 では、Prisma Cloudのアラート解決状況をバブルチャートで可視化する試みについてご紹介しました。 バブルチャートでの可視化は、「どの領域で」「どれくらいの量と重要度のアラートが」「どの程度放置されているか」を直感的に把握する上で非常に有効でした。しかし、2次元のグラフであるバブルチャートでは、主に扱える変数が限られるという制約がありました。例えば、「アラートの種類」と「重要度」という2つの軸で状況を見ることはできても、そこに3つ目以上の要因を加えて、より多角的に分析することは困難でした。 そこで今回は、機械学習の手法の一つである「決定木」を使い、アラートの対応条件を分析する方法を試しました。決定木を用いることで、 Policy Type (種類)、 Policy Severity (重要度)、そして Alert Time (発生時期)といった複数の変数を同時に扱い、「チームの対応ルールがいつ、どのように変化したか」を解明することを目指します。 決定木とは 決定木は、機械学習のアルゴリズムの一種で、その名の通り、データを分類するためのルールを木のような構造(ツリー構造)で表現する手法です。 詳しい仕組みは以下の記事がわかりやすいです。 決定木の基礎 #機械学習入門 – Qiita 決定木を用いる最大のメリットは、その結果の分かりやすさにあります。他の高度な機械学習モデルが、時に「ブラックボックス」として振る舞うことがあるのに対し、決定木は人間が読んで解釈できる「if-then」形式のルールを出力します。 分析の前提と注意点 ただし、この決定木が万能というわけではないことを注釈しておきます。 決定木は、分析対象のデータ、今回の場合だとチームのアラート対応状況に、何らかの一貫したパターンや傾向(=暗黙のルール)が存在することを前提としています。 今回の決定木での分析を行えば、必ずしも明確な結果が保証されるわけではないということはご了承ください。 もし、チームの対応方針が定まっておらず完全に場当たり的であったり、担当者ごとに判断基準が大きく異なっていたりする場合、決定木は明確で解釈しやすいルールを見つけ出せない可能性があります。結果として、非常に複雑で、ビジネス的な意味を見出しにくいツリーが出力されるかもしれません。 可視化の準備:分析シナリオと仮想データの作成 今回の分析では「Policy Type」、「Policy Severity」、「Alert Time」、「Alert Status」のデータを使用していきます。 決定木がどのようなルールを見つけ出すのかを具体的に見ていくために、今回は「とある組織で行われたアラート対応」という仮想的なシナリオを用意し、それに基づいてダミーデータを用意しました。 このシナリオには、明確なアラート対応と「改善前」と「改善後」のフェーズが存在します。 【Phase 1】 対応ルール導入前(〜2025年7月31日まで) この組織のセキュリティ運用チームは、日々大量に発生するアラートへの対応に追われ、疲弊していました。明確なトリアージ基準はなく、対応は一部のベテラン担当者の経験と勘に頼っている状況でした。 この時期の暗黙的な対応ルールは、非常にシンプルでした。 Critical アラートだけは絶対に対応する:  これだけは経営層からも厳しく言われていたため、何があっても必ず解決していました。 それ以外( High 以下)は、ほぼ手付かず:  チームのリソースが足りず、ほとんどのアラートは未解決(Open)のまま放置されていました。 【Phase 2】 新ルール導入の時代(2025年8月1日以降) 2025年8月1日、チームに経験豊富な新しいマネージャーが着任し、アラート対応プロセスを抜本的に見直しました。アラートの重要度とタイプに基づいた、明確なトリアージルールを導入したのです。 新しいルールは以下の通りです。 Critical と High は、最優先で必ず解決する。 Medium と Low は、タイプによって対応を分ける。 config タイプ: 新しく導入された自動修復スクリプトの対象となり、ほぼ自動で解決されるようになりました。 それ以外のタイプ( iam ,  network ,  anomaly ):  手動での調査が必要なため優先度が低く、多くが未解決のままとなりました。 データ構成のマトリクス 上記のシナリオを、ダミーデータ生成のための具体的な構成表にまとめます。 期間 条件 Policy Severity Policy Type Alert Status  の確率分布 Phase 1 (〜2025/7/31) Severity が critical critical すべて resolved : 95% open : 5%   Severity が critical でない high ,  medium ,  low ,  informational すべて open : 90% resolved : 10% Phase 2 (2025/8/1〜) Severity が high 以上 critical ,  high すべて resolved : 98% open : 2%   Severity が medium 以下 medium ,  low ,  informational config resolved : 90% open : 10%   Severity が medium 以下 medium ,  low ,  informational iam ,  network ,  anomaly open : 85% resolved : 15% Pythonによる実装 今回の可視化分析には、前回同様Pythonを使用しました。 主な利用ライブラリは、データの加工には pandas 、モデルの学習には scikit-learn を利用しました。 処理の概要 ここではコードの全ての詳細には触れませんが、実装の主要なステップと、特に工夫したポイントをご紹介します。 Step1: データの前処理 まず、 pandas を使って生のアラートデータをモデルが学習できる形式に変換します。 今回の分析の鍵である Alert Time は、そのままでは機械学習モデルが扱えません。そこで、各アラートの発生日時を、分析期間の開始日からの経過日数( days_since_start )という数値に変換しました。これにより、決定木は「開始から何日目以降」といった時間的な分岐点を見つけ出せるようになります。 また、 Policy Type や Policy Severity といったカテゴリカルなデータも、モデルが理解できる数値形式(ダミー変数)に変換しています。 Step2: モデルの学習 次に、 scikit-learn の DecisionTreeClassifier を使って、準備したデータから決定木モデルを学習させます。 この際、前章で触れたポリシーの重要度を分析に反映させるために、重要度ごとに重みづけを実装しています。 fit メソッドを呼び出す際に、 critical や high のアラートに計算した「重み」を渡すことで、実際のセキュリティー運用を考慮した分析を目指しています。 Step3: 2つのアウトプット生成 学習が完了したら、その結果出力します。 今回の実装では通常の決定木とは別で、決定木の結果を生成AIに解釈してもらうためのルールテキストをJSONで生成します。 決定木の分析もAIにさせてみようという試みで、決定木の生成されたテキストを実際に生成AIに入力してみるところまでしてみようと思います。 結果の表示 以下結果です。 決定木 前述のダミーデータで決定木をさせると、以下の結果が得られました。 画像なので文字が見にくくて申し訳ないのですが、見方はざっくり以下です。 分岐条件: ノードの一番上に書かれているテキスト(例:Severity is not ‘Critical’)は、データを分割するための質問(分岐条件)です。 分岐の方向:  この条件を満たす場合(真 / True)は左下のノードへ、満たさない場合(偽 / False)は右下のノードへとデータが振り分けられます。 samples :  そのノードに到達したアラートの総数を示しています。 value :   samples の内訳です。 class_names が  ['Open', 'Resolved']  の順であるため、例えば  value = [315, 35]  は、Openが315件、Resolvedが35件含まれていることを意味します。 class :  そのノードで最も多数派となったクラスを示します。つまり、そのノードに分類されたアラートが、最終的にどちらに予測されるかを表しています。ツリーの末端(葉ノード)では、これが最終的な結論となります。 以上を踏まえると、決定木の結果はざっくり以下の様に分析できます。 まず最初に、ポリシー重要度が”Critical”(Severity is not ‘Critical’がFalse)だとほとんどのアラートが”Resolved”となっていることがわかります。 ポリシー重要度が”Critical”じゃないアラートについては、次にAlert Timeで条件分岐が起こっています。 “Critical”以外のアラートでは、2025/7/31以前は”Open”,以降は”Resolved”となっております。 さらに2025/7/31のアラートについては、”High”アラートは”Resolved”で、”High”(とCritical)以外のアラートについては”Config”タイプのものは”Resolved”となっております。 どうでしょう、最初にご紹介したシナリオを再現できているのではないでしょうか。 ルールテキスト ルールテキストは以下の様に出力されます。 これをChatGPTのような生成AIに入力し、「結果を要約して」とお願いすると以下の結果を得ました。 以下は、提示された決定木の分析結果の要約です。 1. 「Severity(重大度)」で最初に分岐 「Critical(重大)」かどうかで大きく傾向が分かれる。 2. Severityが「Critical」でない場合 【2025-07-31以前のアラート】 ほとんどが「Open」状態(解決されていない)。 「Alert Time」が細かく過去日付に分岐されるが、どの分岐も「Open」割合が高い(約70~98%)。 【2025-07-31以降のアラート】 「Severity」が「High」でない(それ以外): 「Type」が「Config」かどうかで差が出る。 「Config」タイプは、ほぼ「Resolved」。 それ以外は「Open」割合が高い。 「Severity」が「High」: 「Alert Time(警告発生日)」が新しいほど「Resolved」(解決済み)率が極めて高い(約97~100%)。 3. Severityが「Critical」の場合 時期に関係なく「Resolved」が多い傾向。 2025-08-09以降は100%解決済み。 それより前でも90%以上が解決済み。 要点まとめ Severityが高い(Critical / High)、かつアラートの発生日が新しければ「Resolved」(解決済み)が多い。 Severityが低く、発生日が古いほど「Open」(未解決)が多い。 Configタイプは比較的解決されやすい。 アラート発生直後や重大度の高いものは、解決対応が迅速に実施されている傾向。 この結果から、 「重大なアラートほど早急に解決されるが、古い・低重大度のアラートは未解決が多い」 と言えるでしょう。運用上は、低重大度のアラートや古い未解決案件のフォローアップが課題となりそうです。   まとめ 今回は機械学習の手法の一つである「決定木」を使い、アラートの対応条件を分析する方法を試しました。 決定木分析の結果、「Severity(重大度)」「Type(アラート種別)」「Alert Time(発生日)」という三つの変数が分析結果に現れました。前回作成したバブルチャートでは主に一度に二要素までの関係しか視覚化できませんでしたが、決定木による分析により、3変数以上の要因の組み合わせの分析が可能になり、データの奥行きや傾向把握の可能性が広がったかと思います。 また、今回は決定木分析の結果をテキストに出力することで、結果の解釈と要約の部分で生成AIの活用の可能性を示せたかと思います。 今回紹介した手法は一例であり、データの性質や分析目的によっては、さらに多様な可視化や分析手法が存在します。今後もPrisma Cloudから取得できるデータのさらなる活用方法について、試行錯誤し結果を発信していければと考えております。 また、当社では、Prisma Cloudを利用して複数クラウド環境の設定状況を自動でチェックし、設定ミスやコンプライアンス違反、異常行動などのリスクを診断するCSPMソリューションを販売しております。ご興味のある方はお気軽にお問い合わせください。リンクはこちら↓ マルチクラウド設定診断サービス with CSPM| SCSK株式会社 マルチクラウド環境のセキュリティ設定リスクを手軽に確認可能なスポット診断サービスです。独自の診断レポートが、運用上の設定ミスや設計不備、クラウド環境の仕様変更などで発生し得る問題を可視化し、セキュリティインシデントの早期発見に役立ちます。 www.scsk.jp
アバター