TECH PLAY

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

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

1226

こんにちは、広野です。 これまでいろいろと React アプリを AWS リソースと連携させるための仕組みをブログ記事にしてきましたが、絶対に使うであろうユースケースを書いていませんでした。以下、順を追って説明したいと思います。 本記事はアーキテクチャ編です。今後、 環境編 、 UI 編 を続編記事として用意します。 背景 こんな状況、ありませんか? アプリにログイン認証はある。(今回は Amazon Cognito 使用) アプリの動的コンテンツ (UI 画面) は AWS Amplify や Amazon S3 で配信されている。 静的コンテンツは Amazon S3 にあり、アプリのログイン済みユーザーにのみ閲覧を許可したい。ただし、全てのコンテンツが認証が必要なわけではなく、パブリックに公開してよいファイルもある。 セキュリティやレスポンスを考慮し、Amazon S3 には Amazon CloudFront をかぶせてアクセスさせたい。 この状況ですと、以下に紹介するアーキテクチャが有用であると考えます。 アーキテクチャ ざっくり全体像は以下になります。Amazon CloudFront に AWS Lambda@Edge 関数を実装することになります。 本記事では、動的コンテンツ置き場の Amplify Hosting や Amazon Cognito については基本言及せず、図の上半分の Amazon CloudFront、AWS Lambda@Edge 関数、および React コードにフォーカスします。 React アプリにログインをすると、Amazon Cognito ユーザープールからトークンをもらえます。アプリから Amazon S3 にある静的コンテンツへのリクエストを行うときにトークンを含め、間に存在する Amazon CloudFront 上で Lambda@Edge 関数によりトークンの正当性をチェックさせるという仕組みです。 以下の AWS 公式ドキュメントやブログ記事のように React アプリログイン機能を AWS Amplify で作成し、その状態になっていることが前提です。 Authenticate existing React application users by using Amazon Cognito and AWS Amplify UI - AWS Prescriptive Guidance Set up user authentication for a React web application by using Amazon Cognito and AWS Amplify. docs.aws.amazon.com AWS Amplify + AWS CDK で人生初のアプリケーションを作ってみた (第1回) AWS Amplify + AWS CDK で人生初のアプリケーションを作ってみました企画の第1回です。本当は序/破/急にしようかと考えてましたが構成的に断念しました。IT業界に従事して約10年になりますが、一度もアプリケーションを作ったことがなく、どのように動いているのかもまだふんわりとしか理解できていませんが、先輩方の記事や生成AIのおかげで何とか形になりました。 blog.usize-tech.com 2025.01.14   アーキテクチャ図の上半分に少し入り込んで説明します。 静的コンテンツへのリクエストを行うとき、そのリクエストの Authorization ヘッダーにトークンを含めます。この動きは一般的にはデフォルトでは実施してくれないので、アプリ側で作り込む必要があります。 UI 編 の記事で説明します。 リクエストは Amazon CloudFront が受け取ります。リクエストをそのまま Amazon S3 に流すのではなく、Lambda@Edge 関数でインターセプトします。 Lambda@Edge 関数は以下の処理をします。 リクエストが CORS プリフライトチェック (OPTIONS メソッド) であれば S3 静的コンテンツにアクセス不要なので、CloudFront に 204 No Content のレスポンスを戻すと、CloudFront がアプリに戻してくれます。このとき、CORS で必要なレスポンスヘッダー Access-Control-Allow-Origin が必要ですが、Amazon CloudFront でレスポンスヘッダーをオーバーライドする設定にしておけば Lambda@Edge 関数側で何もすることはありません。 パブリックに公開可能 (認証不要) な静的コンテンツ (例えば public という名前のフォルダ内にあるとします) へのアクセスであれば、トークンのチェックはせずにそのまま Amazon S3 にリクエストを流します。以降、Amazon S3 からのレスポンスが Amazon CloudFront 経由でアプリに戻ります。 その他のリクエストはトークンのチェックが必要です。Authorization ヘッダーにあるトークンの正当性を Lambda@Edge 関数内でチェックし、正当であれば Amazon S3 にリクエストを流します。不正であった場合は Amazon CloudFront に 403 Forbidden のレスポンスを戻すと、CloudFront がアプリにそれを戻してくれます。 ざっくり恐縮ですがこの方式で Amazon S3 へのアクセスを Amazon Cognito ユーザーのみに制限をかけることができます。 これを実装することによるアプリへのレスポンス影響が心配だったのですが、体感的には許容範囲でした。   続編記事 React アプリで Amazon Cognito 認証済みユーザーにのみ Amazon S3 静的コンテンツへのアクセスを許可したい -環境編- Amazon Cognito でユーザー認証する React アプリで、Amazon CloudFront 経由の Amazon S3 へのアクセス制御を実装する方法を紹介します。本記事は環境編です。 blog.usize-tech.com 2026.01.13 React アプリで Amazon Cognito 認証済みユーザーにのみ Amazon S3 静的コンテンツへのアクセスを許可したい -UI編- Amazon Cognito でユーザー認証する React アプリで、Amazon CloudFront 経由の Amazon S3 へのアクセス制御を実装する方法を紹介します。本記事は UI 編です。 blog.usize-tech.com 2026.01.13   まとめ いかがでしたでしょうか? まだアーキテクチャ編は概要レベルなので、こんな構成で動くよね、という理解をしていただければと思います。具体的な実装面ではハマりどころがあったので、注意点として続編記事で紹介します。 本記事が皆様のお役に立てれば幸いです。
アバター
こんにちは、SCSK株式会社 中村です。 先月、SCSKのパートナー企業様との合同の勉強会に参加してきました。趣旨としては互いの若手エンジニアの交流・共同作業を通じたGoogle Cloudに対する知見深化や、生成AIを活用したアプリデプロイの学習です。 勉強会の中でGoogle Cloud + Geminiを使ったアプリ開発を体験し、開発手法の新時代を見たと感じていますのでそちらを中心に活動の内容をご紹介できればと思います。 勉強会の概要 参加者の構成 勉強会は弊社メンバー10名、パートナー様メンバー10名の20名+講師陣という体制で開催され、 参加メンバーは両社のメンバーが均等に割り振られる形で5名1組となり、計4組でワークショップを行いました。 参加者のスキルは日ごろから業務でGoogle Cloudを利用している方から、私のようにオンプレしか経験したことのない人まで様々でしたが 比較的Google Cloudスキルの浅いメンバーが中心となっています。チームごとに課題が出題され最終的に発表を行います。 課題内容   テーマ   スキルマップ作成 AI アプリ 目的 : 経歴書からスキルマップを作成する インプットデータ : 経歴書 想定ユースケース 1:中途採用での簡易的なスキル把握 経歴書を読み込まずとも、 どんなスキルを持つ要員か簡単にわかる ようにする。 採用に直接かかわらないが社内のほかメンバへ、 簡単に要員情報を把握 してもらう。 ITの技術スキルはもちろん、管理スキルや過去の実績などもどこまでスキルマップ化するか? 2:自身の経歴書の書き方の参考 対外的にアピールできるような経歴書の書き方となっているかという 自身のスキル の確認 。 自己評価と異なるスキルマップが出てきた場合、改善できないか検討するなどに利用。 入りたいと思っている会社の募集要項と照らし合わせての評価等も面白いか。 課題はチームにより異なりますが、私のチームは上記内容の課題でした。 インプットデータとなる履歴書と、ベースとなる超簡易的なスキルマップ作成アプリは運営側で用意してくれています。 ベースとなるアプリはユースケースに沿った出力を行わないので、私たちは ユースケースに沿うように Google Cloudと生成AIを使ってアプリを改修していく ということが活動の主となります。 活動を始めるにあたって ここからはどのように学習を進めていったかをポイントに絞ってご紹介したいと思います。 まず、今回の勉強会の中で私たちが利用したGoogle Cloudのサービスは以下の通りです。 サービス名 説明 Cloud SQL Google Cloudが提供するフルマネージドのリレーショナルデータベースです。 今回はスキルマップの検索結果を履歴として保存するため利用します。 (しかし結果として履歴機能はほぼ使わない形となりました。) Cloud Run Google Cloud上でコンテナ化されたアプリをインフラの管理無しに実行できるサービスです。 今回作成するスキルマップ作成アプリをデプロイしたり、改修するために使います。 Cloud Load Balancing Google Cloudが提供するネットワークの負荷分散を行うサービスです。 構成が寂しいこともあり、実際の業務システムで作る構成を想定して用意。(無くてもいい) 今回は単一のアプリのため負荷分散の機能は利用しません。 Cloud Storage Google Cloudが提供するストレージサービスです。 インプットデータとなる履歴書の生データや、生成したスキルマップを格納するためのストレージです。 Secret Manager APIキー、証明書、パスワードといった秘密情報を安全に管理するためのサービスです。 デプロイ時にCloud SQLのIDとパスワードを直打ちしないためにSecret Managerに登録して、そこからIDとパスワードを取得する形にします。 Gemini 今回のアプリ開発の肝となる生成AI。バージョンは2.5フラッシュ版を利用しています。 ほぼ全てのアプリの改修をこのGeminiに担ってもらいます。 事前準備 ①VPCネットワークの作成 初めにVPCネットワークをGoogle Cloudのコンソール上から作成します。 コンソール左側のナビゲーションメニューから「VPCネットワーク」を選択し、「+VPCネットワークを作成」のメニューを押下します。 ネットワーク名、サブネット名、リージョン、IPv4範囲を決めてVPCを作成します。その他の項目はデフォルトで大丈夫です。 作成後のネットワーク名やサブネット名は次で使います。 ②Cloud SQLの作成 次にCloud SQLをGoogle Cloudのコンソール上から作成します。 簡易的な機能のみを利用するためEnterpriseエディション、シングルゾーンの料金体系が安い構成で作成します。 細かい設定は運営側で用意された資料を元に設定を行います。(赤枠のような値となるように設定します。) オンプレ環境しか経験したことのない自分にとっては GUIで設定項目を決めるだけで約30分ほどでSQLサーバが作れてしまう というだけで既に驚きです。 SQLの作成が完了したら、作成したSQLを選択して「ユーザ」と「データベース」を新たに作成します。 ”postgres”という名前のものはデフォルトで存在するものですので、追加で任意のユーザとデータベースを作成します。 ③Secret Managerの登録 Secret Managerを開いて、「SQL_USER」「SQL_PASSWORD」という名前でそれぞれ先ほど作成したCloud SQLのIDとパスワードの値を設定します。 ④作業ディレクトリの作成 Google Cloud管理画面からCloud Shellを立ち上げてホームディレクトリ直下に作業用のフォルダを作成します。 Cloud ShellはGoogle Cloudで利用できるブラウザベースのコマンドラインです。 $ mkdir cloud-run-app 作成したディレクトリの中に、運営側で用意してもらった簡易スキルマップ作成アプリの資材を格納します。 下記の資材についてはCloud Shell起動後geminiを立ち上げて、「PythonでCloud RunのWebアプリを作成したい」と言うと ほぼ同じ構成の資材を作ってくれます(中身は全然違いますが) 資材の中身は改修を行うなかでどんどん変わっていくので最初からgeminiに用意してもらう方法でも問題ありません。 ※geminiの起動方法は後述します。 ■資材一覧 資材名 説明 Dockerfile コンテナイメージをビルドするためのファイル main.py スキルマップ作成アプリの本体となるPythonファイル (SQLサーバのIPアドレスやdb名は自分が作成したものに書き換えます) requirements.txt DockerfileがPythonパッケージをインストールするためのリスト templates(フォルダ)    →history.html スキルマップ作成アプリにて質問履歴を表示するための設定ファイル  →index.html スキルマップ作成アプリの表示構成を決める設定ファイル 初期スキルマップの確認 現状のスキルマップがどのようなものか確認するために用意されたアプリのデプロイを行います。 ※[cloud-run-app]は任意のアプリ名、[vpc][subnet]は自身がVPCネットワークを作成した際の名称を指定するようにしてください。 $ cd cloud-run-app $ $ gcloud run deploy [cloud-run-app] --region asia-northeast1 --network=[vpc] --subnet=[subnet] --vpc-egress=private-ranges-only --port 8080 --set-secrets SQL_USER=SQL_USER:latest,SQL_PASSWORD=SQL_PASSWORD:latest デプロイのコマンドを実行するとソースコードの場所を確認されることがありますが 最初にcdでソースファイルが格納されている場所に移動済みですのでそのまま何も入力せずにEnterを押してもらって大丈夫です。 (チームの別のメンバーは何も聞かれずにデプロイ完了したりすることもあるみたいで、ここら辺の違いはよく分かりませんでした) デプロイが無事完了すると、作成されたURLがプロンプト内に表示されます。 デプロイが完了したら、Cloud Runから「サービス」を選択するとリストの中に先ほどデプロイした[cloud-run-app]の名前のアプリがあると思いますので選択します。開いた画面にURLが記載されていると思いますので、そちらをクリックするとデプロイしたアプリを開くことが出来ます。 初期状態のスキルマップ作成アプリ 上の図が運営側で用意してくれたスキルマップ作成アプリです。(※表示されている氏名や経歴は架空の社員を想定したものです。) 簡素な作りで、 年齢の表記の仕方もバラバラだったりするのでここら辺の体裁も整えたい ですね スキルマップを作成したい人のチェックボックスにチェックを入れてページ下部にあるスキルマップ作成のボタンを押下するとExcel形式のスキルマップが作成されます。 上記が初期状態のスキルマップです。 スキル項目について、チェックボックスで選択した人がどれくらいのレベル感なのかをGeminiが◎、〇、△、-といった記号で判断して入力してくれています。しかし これだけではなぜ〇なのか、なぜ△なのかという根拠が分かりません 。 インプットデータの履歴書には 保有する資格なども記載があるため、そちらもレベル感を主張する材料として出力できるとよい です。 といった具合に色々改良の余地がありそうなので、これを Geminiで改良していきます 。  アプリの改修をGeminiでやってみる 必要な準備ができたので、ここからはGeminiを使ってスキルマップ作成アプリを改修していきます。 Cloud Shellにてアプリ資材を配置しているフォルダに移動してGeminiを起動します。 Geminiの起動は任意の場所で”gemini”と打つだけ です。簡単! $ cd cloud-run-app $ $ gemini 起動すると上記のような画面になります。 対話式のアプリ改修 さっそくGeminiにアプリの改修を依頼してみます。 試しに出力されるスキルマップについて、一人ひとりシートを分けて結果を出力することが可能か聞いてみました。 いかがでしょうか? 私は 「スキルマップを出力する人が複数選択されている場合、一人ひとりシートを分けて結果を出力することは可能ですか?」とだけ質問 しただけなのですが、 Geminiは自分でフォルダ内のmain.pyの中身を確認して、改善案を2つ提示 してくれました。また、 案②のほうが見やすいという意見をこちらに伝えた上で案②に変更しますか?と提案 してくれました。非常に優秀な営業マンのようです。 特に懸念はないためGeminiの提案通り案②への変更で進めます。 本当に変更していいのか?という最終確認が入るので、「1. Yes,allow once」を選択し、Enterを押下します。 そうして、 ものの5分ほどで修正が完了 しました。 従来のアプリ改修では考えられないスピード です。 そもそも 自分でツールを編集するということすらやらなくても変更が完了 出来てしまいました。 ただ、これだけではファイルの中身を変えただけなので再度デプロイしてあげる必要があります。 デプロイが10分ほど時間がかかりますが、それを考慮してもとても短い時間 ですね。 再デプロイを終えて、改めてCloud Runからアプリを起動、適当に人を選択してファイルを出力してみます。 そして出力されたファイルがこちら。 赤枠の通りしっかり 個人ごとにシートが分かれて結果を出力してくれています ね。 もしこれを自分でやろうものなら、pythonのコマンドを調べてコードの追記位置を検討して実装してみて問題があればエラー箇所を特定して、、と修正を完了するために 私であれば 2人/日はかかってしまうであろうものがGeminiによって5~10分で完了してしまいました 。 最終的な成果物 その後のスキルマップの改修については基本的には上記で紹介したようにやりたいことをGeminiに伝えて、 Geminiが提案してくれた内容が要望通りのものであれば承諾して改修を進めることの繰り返しです。 そんな感じで進めて、成果物となるスキルマップの改修に加えて、生成アプリも最終的には以下のような感じに変更しました。 ~変更点~ ・名前をクリックすると履歴書の生データが表示されるようにリンクを設定 ・年齢の表記方法が”〇歳”と統一されるように修正 ・ページの配色をかわいい感じに変更(Geminiにかわいくして。とお願いしたら上記の青ベースになりました。) ・スキルレポートの生成を開始したら進捗率が表示されるように修正 ・スキルレポートの生成時間が短縮されるように修正(5~10分ほど→20秒ほどに短縮) システム構成図 最終的なアプリの構成図は以下の通りです。 Cloud RunのフロントにCLB(Cloud Load Balancing)を配置していますが、勉強会の最後に発表する成果物の資料として Cloud RunとCloud StorageとGeminiだけだと構成図的にも寂しいなとなったので、 実際の業務システムで作る構成を想定してCLBを追加しました。それ以外は前述した通りの処理の流れとなっています。 さいごに 以上がGoogle CloudとGeminiを使ったアプリ開発体験の紹介となります。 アプリケーションエンジニアの方ではなくとも、自分でバッチやマクロなどを作った経験のある方であれば Geminiを使った対話式のアプリ開発が、従来の方法とは一線を画すものであることは何となくイメージしてもらえたのではないでしょうか? ただ、 Geminiによる改修がいつも上手くいくかというと上手くいかないことも多い です。 そんな時でもCloud Runの[オブザーバビリティ]>[ログ]というところに大抵エラー出力などが出ていますので、 その エラーの記述を丸々コピーして、これもGeminiに投げて原因を聞くとほとんどのことは解消してくれました 。 他にはほぼ 全ての変更をGeminiに任せているので、エラーが解消できなかった時などは本当にどこが悪いのかという検討が自分でつけられなくなってしまうことがありました 。そうすると結局Geminiを使って更なる改修を加えるか、前のバージョンに切り戻すことが必要になってきます。 とはいえ、やはり開発経験がない人間でも対話ベースで作りたいアプリが作れるというのは非常に大きな魅力です。 さらにAIによる開発スピードの高速化というのも今回の勉強会で感じた大きなポイントでした。 今後求められるエンジニア像というものは、 AIとの協調をベースとしたスキルセットに変化していくであろうし、私たちも変化しなければいけない ということを強く感じています。 私自身も自己研鑽としてクラウドや生成AIについて学習して 時代にあったエンジニアとして活躍できるように頑張っていきたいと思います。 (とりあえず初めの一歩としてGoogle Cloud Leaderの資格を取得しました。) 最後までお読みいただき、ありがとうございました。
アバター
こんにちは、SCSKの谷です。 これまでに3大クラウドの各サービスをMackerelのクラウドインテグレーションを利用して監視を実装する記事を投稿してきました。 AWS: Mackerel で AWS のサーバーレスサービスを監視してみた – TechHarmony Azure: MackerelでAzure環境を監視してみた! – TechHarmony Google Cloud: MackerelでGoogle Cloudを監視してみた – TechHarmony 最後に総まとめということで、3大クラウドのMackerel監視(クラウドインテグレーション)を比較していきます! そもそもMackerelのクラウドインテグレーションとは? これまでの記事の中で各クラウドインテグレーションを利用して監視を実装していましたが、そもそもMackerelのクラウドインテグレーションについて説明していなかったので、簡単に説明させていただきます。 Mackerelのクラウドインテグレーションとは、AWS/Azure/Google Cloudと連携し、それぞれのクラウド環境を「Mackerel上のホスト」として一元監視できる機能です。 ■基本構造 Mackerel側から各クラウドの監視サービスのAPIを利用してホストの情報及びメトリクスを取得 ・ポーリング間隔:5分 ■利用方法 MackerelからAPIを使用するために、それぞれのクラウドに合わせた認証方法を利用します。 AWS:IAMロールで連携 + ポリシー付与 Azure:サービスプリンシパル設定 + ロール割当 Google Cloud:サービスアカウント設定 + ロール割当   クラウドインテグレーションで監視可能なクラウドサービス比較 ここでは、Mackerelのクラウドインテグレーションで監視可能なサービスについて比較していきます。 合計サービス比較 現在Mackerelのクラウドインテグレーションで監視可能なクラウドサービスの合計はそれぞれ以下の通りです。   AWS Azure Google Cloud 合計サービス数 27 11 3 クラウドインテグレーションで監視できるサービスの数を比べると、クラウド利用者が多い AWS が、他のクラウドを大きく引き離していることがわかります。 サービスカテゴリ比較 各クラウドサービスのカテゴリごとの監視可能サービスの対応状況は以下の通りです。 カテゴリ分けはある程度各クラウドサービスのカテゴリ分けに沿っています。よく利用されるサービスのカテゴリについては太字にしています。 カテゴリ AWS Azure Google Cloud コンピューティング ◎ ◎ 〇 コンテナ 〇 × × データベース ◎ ◎ 〇 ネットワーク ◎ 〇 × ストレージ 〇 〇 × 分析 ◎ × × アプリケーション統合 〇 × × ビジネスアプリケーション 〇 × × セキュリティ 〇 × × 開発者ツール 〇 × × 管理 〇 × × 上記以外のカテゴリ × × × ※3つ以上対応している場合は◎ AWSは監視可能なサービス数が非常に多く、ほぼすべてのカテゴリで1つ以上のサービスに対応しています。さらに、利用頻度が高いカテゴリでは3つ以上のサービスを監視できるものもあり、幅広い選択肢を提供しています。特筆すべきは、AWSのみコンテナ関連のサービス監視に対応している点です。 一方、Azureはコンピューティング系とデータベース系で3つ以上のサービスに対応しているものの、基本的にはよく利用されるサービスに限定されています。 Google Cloudはさらにシンプルで、コンピューティング系とデータベース系のみ監視可能であり、最低限のサービス対応にとどまっています。   クラウドインテグレーションで取得可能なメトリクス ここでは、Mackerelのクラウドインテグレーションを利用して、仮想サーバー系サービスで取得できるメトリクス数と項目を比較していきます。 メトリクス数 現在Mackerelのクラウドインテグレーションで取得可能な仮想マシンサービスのメトリクス数合計はそれぞれ以下の通りです。 比較として、各クラウドの監視サービスで取得可能なメトリクス数も記載しています。   AWS Azure Google Cloud Mackerel 21 11 22 クラウド監視サービス 29 64 179 Mackerelでは、CloudWatchなどのクラウド監視サービスからすべてのメトリクスを取得するのではなく、利用頻度の高いメトリクスに絞って収集しているように見えます。特にGoogle CloudやAzureの場合、提供されるメトリクス数が非常に多いため、Mackerel側であらかじめ選定されたメトリクスのみが管理画面に表示されることで、ユーザーは膨大なメトリクスの中から必要なものを探す手間がなく、直感的でわかりやすい監視設定が可能になっていると感じました。 メトリクス項目 現在Mackerelのクラウドインテグレーションで取得可能な仮想マシンサービスのメトリクス項目はそれぞれ以下の通りです。 ■共通するメトリクス   AWS Azure Google Cloud CPU CPUUtilization CPUCreditUsage CPUCreditBalance CPUSurplusCreditBalance CPUSurplusCreditsCharged Percentage CPU CPU Credits Remaining CPU Credits Consumed utilization Disk DiskReadOps DiskWriteOps DiskReadBytes DiskWriteBytes Disk Read Operations/Sec Disk Write Operations/Sec Disk Read Bytes Disk Write Bytes read_bytes_count write_bytes_count read_ops_count write_ops_count throttled_read_bytes_count throttled_write_bytes_count throttled_read_ops_count throttled_write_ops_count Network NetworkIn NetworkOut NetworkPacketsIn NetworkPacketsOut Network In Network Out Network In Total Network Out Total received_bytes_count sent_bytes_count received_packets_count sent_packets_count   ■クラウド個別のメトリクス クラウド 分類 メトリクス 説明 AWS Status StatusCheckFailed_Instance StatusCheckFailed_System StatusCheckFailed EC2インスタンスやシステムの正常性を監視するためのステータスチェック結果 Disk (EBS) EBSReadOps EBSWriteOps EBSReadBytes EBSWriteBytes EBSIOBalance% EBSByteBalance% AWS EBS(Elastic Block Store)のI/O性能とバランスを監視するメトリクス群 Azure VM Availability Metric VmAvailabilityMetric Azure VM の稼働可否を示すメトリクス Google Cloud Uptime instance/uptime Compute Engineが起動してからの累積稼働時間を示すメトリクス ミラーリング パケット Mirroring bytes Mirroring packets Mirroring packets dropped ミラーリングされたトラフィック量とパケット数、そしてドロップされたパケット数を示すメトリクス Firewall dropped_bytes_count dropped_packets_count Google Cloud の VPC ファイアウォールでドロップされたトラフィック量(バイト数)とパケット数を示すメトリクス ■共通して取得しているカテゴリ どのクラウドでも、CPU・Disk・Network関連のメトリクスは必ず取得しています。 どのクラウドでもクラウドインテグレーションを使用した場合メモリのメトリクスを取得することができず、仮想マシンにエージェントを入れる必要があります。Mackerelとしてはメモリのメトリクスはエージェントから取得する思想なのかなと感じました。 ■クラウドごとの特色 共通メトリクス以外は、それぞれのクラウド特有の指標が追加されています。 AWS:EBSのI/O性能やステータスチェック(可用性) Azure:VMの可用性を示す VmAvailabilityMetric Google Cloud:ファイアウォールのドロップ数やパケットミラーリングなど、ネットワークセキュリティ関連のメトリクス クラウドインテグレーションの設定手順 最後に各クラウドのMackerelクラウドインテグレーションの設定手順を比較していきます。 詳しい導入手順については、各ブログで紹介していますので、そちらをご覧ください。 設定手順の難易度 今回は以下の指標で導入手順の難易度を比較しています。 設定方法の種類 複数の設定パターン(GUI、CLI)が用意されているかどうか 手順数(GUI) GUIで設定する場合、完了までに必要なステップ数はどれくらいか 設定手順の種類 複数の手順で設定可能か 所要時間(GUI) GUIで設定した場合、初期設定にどれくらい時間がかかるか 上記をまとめた結果は以下の通りです。   AWS Azure Google Cloud 設定方法 △ GUI 〇 GUI、CLI 〇 GUI、CLI 手順数(GUI) 〇 13 △ 16 △ 15 設定手順の種類 〇 ・IAMロール(推奨) ・Access Key IDと Secret Access Key 〇 ・Azure CLI 2.0 ・Azure Portal 〇 ・Cloud SDK ・Cloud Console 所要時間(GUI) 〇 3分2秒 × 7分2秒 △ 4分46秒 今回、AWS・Azure・Google Cloudのクラウドインテグレーション設定を実際に試してみました。その結果、設定のしやすさや所要時間に違いがあり、次のような印象を持ちました。 ・AWS 最もスムーズに設定できました。手順数も少なく、公式ドキュメントを参照しながら進めても特に詰まる箇所はありませんでした。 全体的に直感的で、短時間で設定完了できる印象です。 ・Azure 設定にやや時間がかかりました。公式ドキュメントで指定されている項目の場所をAzureポータル上で探すのに手間取ったことと、手順自体が多かったことです。その分、設定完了までに時間がかかる印象でした。 (慣れてないだけかもしれませんが。。。) ・Google Cloud 設定の大部分はスムーズでしたが、APIライブラリで必要なAPIを有効化する作業に少し手間取りました。公式ドキュメントに記載されて いるAPI名と、Google Cloud Console上で表示される名前が異なる場合があり、確認に時間がかかりました。 ただ、それ以外の手順は問題なく進められました。 総じて、クラウドインテグレーションの設定は比較的簡単に行えると感じました。 AWS・Azure・Google Cloudそれぞれに特徴はありますが、公式ドキュメントを参照すれば、どのクラウドでも大きな障壁なく設定を完了できます。 おわりに 記事は以上になります。いかがでしたでしょうか? 詳細が気になった方は 公式ヘルプ をご確認ください。 今回は AWS、Azure、Google Cloud における Mackerelのクラウドインテグレーションを比較し、 「監視可能なサービス」、「取得できるメトリクス」、「設定手順」といったポイントを整理しました。 整理したことによりMackerelのクラウドインテグレーションでも、クラウドごとに細かな違いがあることが改めて分かりました。 Mackerelクラウドインテグレーションのメリットは、各クラウド上のリソースをMackerelのホストとして一元管理できることです。 これにより、複数クラウドを利用していても、統一された監視基盤を構築できます。 現状クラウドサービスによって利用できるレベルに差があるので、将来的にはどのクラウドサービスでも同じレベルでクラウドインテグレーションを利用できるようになると、さらに便利になると感じました。 これまで、部署内で Mackerelのクラウドインテグレーションに関する記事を投稿してきましたが、ひとまず今回の記事で一区切りとなります。最後までお読みいただき、ありがとうございました! 今後も、新しい気づきや皆さまに役立つ情報があれば、随時記事を更新していきます。 次回の投稿をぜひお楽しみに!
アバター
こんにちは。 今更ながら Amazon WorkSpaces で SAML 連携を実施する機会があり、いろいろ触ってみましたので、 今回は Microsoft Entra ID を利用した SAML 連携設定を紹介したいと思います。 構成イメージ 今回設定するSAML認証のイメージ図です。 Entra IDをIAM IDプロバイダーとして登録し、WorkSpacesディレクトリの多要素認証方法として指定することで、多要素認証でEntra ID認証を利用することができるようになります。 図の中では省略していますが、AD認証にはEC2で構築した検証用ADサーバを利用するため、AD Connectorを作成して連携先ADに検証用ADサーバを指定しています。また、WorkSpacesはPersonal(個人)を利用します。 本記事では、検証用AD/AD Connector/WorkSpacesの基本的な設定が実施された環境に対して、Entra IDを用いた多要素認証設定を追加設定する手順を説明します。   設定手順 今回設定する手順は以下の通りです。 AWSとAzureのコンソールを行き来するので、分かりやすいように()内に操作するコンソールを書いています。 ①エンタープライズアプリケーション作成 (Azure) ②SAML基本設定/フェデレーションメタデータXMLダウンロード(Azure) ③SAML ID プロバイダー作成 (AWS) ④IAMロール作成/インラインポリシー設定(AWS) ⑤SAML認証応答のアサーション設定 (Azure) ⑥フェデレーションのリレー状態構成 (Azure) ⑦アプリケーションへのユーザー割り当て/リンクコピー(Azure) ⑧WorkSpacesディレクトリでSAML2.0統合を有効化 (AWS) 以上の設定を順を追って説明していきます。 ①エンタープライズアプリケーション作成 (Azure) まずはEntra IDにSAML認証用のエンタープライズアプリケーションを追加していきます。 Azure管理コンソールでEntra IDの画面を開き、エンタープライズアプリケーションを新規作成します。 アプリケーションの新規作成画面で「独自アプリケーションの追加」を選択し、任意のアプリ名を入力します。 「アプリケーションでどのような操作を行いたいですか?」の箇所については、一番下の「ギャラリー以外」を選択して アプリケーションを作成します。 ②SAML基本設定/フェデレーションメタデータXMLダウンロード(Azure) エンタープライズアプリケーションの作成が完了したら、作成したアプリケーションの「シングルサインオン」画面を開きます。 デフォルトで「無効」となっているシングルサインオン方式を「SAML」に設定します。 続いて、以下のURLからAWS提供のメタデータXMLをダウンロードします。 https://signin.aws.amazon.com/static/saml-metadata.xml ダウンロードしたメタデータXMLをアップロードして、「保存」を押下します。 SAML設定画面に戻ったら、「SAML証明書」欄のダウンロードリンクから、メタデータXMLファイルをダウンロードします。 ここでダウンロードしたXMLファイルを用いて、AWS側のIDプロバイダー設定を実施します。 ③SAML ID プロバイダー作成 (AWS) 続いてはAWS側の設定となります。 AWSマネジメントコンソールを開いて、IAM>IDプロバイダを開きます。 「プロバイダを追加」を押下して以下の通り設定します。 ・プロバイダのタイプ:SAML ・プロバイダ名:任意 ・メタデータドキュメント:手順②でダウンロードしたメタデータXMLファイルを選択 設定出来たらIDプロバイダを作成します。 ④IAMロール作成/インラインポリシー設定(AWS) 続いて、SAML認証で利用するIAMロールを作成します。 IAM>ロールを開き、「ロールを作成」を押下します。 作成画面が開いたら、以下の通り設定し、「次へ」を押下します。 ・信頼されたエンティティタイプ:SAML2.0フェデレーション ・SAML2.0ベースのプロバイダー:手順③で作成したIDプロバイダ ・許可されるアクセス:プログラムによるアクセスのみを許可する ・属性:SAML:sub_type ・値:persistent その他の設定はデフォルトのまま、任意のロール名で一度ロールを作成します。 ロールが作成出来たら、ロールの信頼関係設定で、信頼ポリシーのActionに”sts:TagSession”を追加し、保存します。 ロールの設定が完了したら、最後に以下の通りインラインポリシーを追加します。 ポリシー内の対象リージョン/アカウントID/ディレクトリIDは、それぞれご自身が利用する予定のものに置き換えてください。 { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "workspaces:Stream", "Resource": "arn:aws:workspaces:<対象リージョン>:<対象AWSアカウントID>:directory/<WorkSpacesディレクトリID>", "Condition": { "StringEquals": { "workspaces:userId": "${saml:sub}" } } } ] } ⑤SAML認証応答のアサーション設定 (Azure) さて、ここまできたらAzure管理コンソールに戻って、作成したアプリケーションへAWS側の情報を追加していきます。 手順①で作成したアプリケーションの「シングルサインオン」画面を開き、「属性とクレーム」を編集します。 編集画面が開いたら、属性を以下の通り編集します。 ※デフォルトで設定されている属性/クレームは削除してください。 クレーム名 名前識別子の形式 値 一意のユーザー識別子 (名前 ID) 永続的 user.mailnickname [nameid-format:persistent] https://aws.amazon.com/SAML/Attributes/Role – (デフォルト) ④で作成したロールのARN,③で作成したプロバイダのARN (arn:~,arn:~のようにカンマ区切りで記載する) https://aws.amazon.com/SAML/Attributes/RoleSessionName  – (デフォルト) user.mailnickname https://aws.amazon.com/SAML/Attributes/PrincipalTag:Email  – (デフォルト) user.mail ⑥フェデレーションのリレー状態構成 (Azure) アプリケーションの「シングルサインオン」画面に戻り、SAML基本設定を編集します。 編集画面が開いたら、リレー状態に利用リージョンのリレーステートURLを記載します。 https://relay-state-region-endpoint/sso-idp?registrationCode=registration-code 各リージョンのリレーステートエンドポイントは以下のAWSドキュメントを参照してください。 https://docs.aws.amazon.com/ja_jp/workspaces/latest/adminguide/setting-up-saml.html#configure-relay-state ⑦アプリケーションへのユーザー割り当て/リンクコピー(Azure) SAML認証を利用するEntra IDユーザー(もしくはグループ)を作成したアプリケーションに割り当てていきます。 ※ここで割り当てたユーザー/グループのみSAML認証を突破できます。 (割り当ててていないユーザーの場合、認証時にエラーになります。) 作成したアプリケーションの「ユーザーとグループ」画面を開き、「ユーザーまたはグループの追加」からWorkSpacesを利用するユーザー/グループを追加します。 ユーザ/グループの追加が完了したら、以下のURLへアクセスします。 https://myapps.microsoft.com/ アクセスしたら、作成したアプリケーションのリンクをコピーして、メモ帳等に控えておきます。   ⑧WorkSpacesディレクトリでSAML2.0統合を有効化 (AWS) 最後にWorkSpacesのディレクトリ設定でSAML2.0統合を有効化していきます。 AWSマネジメントコンソールでWorkSpaces画面を開き、SAML連携を実施するディレクトリを開きます。 ディレクトリ画面が開いたら、認証設定を編集してきます。 認証設定画面が開いたら、「SAML 2.0 アイデンティティプロバイダーの編集」を押下します。 「SAML 2.0 認証の有効化」にチェックを入れ、ユーザーアクセスURLに以下のURLを入力します。 https://myapps.microsoft.com/signin/<手順⑦でコピーしたアプリケーションのリンクのsignin以降> 記述イメージ:https://myapps.microsoft.com/signin/******?tenantId=******* URLの入力ができたら設定を保存します。 以上で、AWS/Entra IDのSAML連携設定およびWorkSpacesディレクトリの多要素認証設定は完了です。 動作確認 多要素認証の設定が完了したので、動作確認をしてみます。 動作確認前にADユーザーおよびWorkSpacesワークスペースを作成してください。 ただし、Entra IDを多要素認証で利用する場合、ADユーザーのユーザー名およびメールアドレスがEntra ID側のユーザーと一致している必要がありますので、ご注意ください。 まずはクライアントPCにインストールしたWorkSpacesクライアントを起動します。 すると、ID/パスワードの入力画面ではなく、「サインインを続行」のボタンが表示されるため、押下します。 ボタンを押下するとEntra IDのログイン画面へリダイレクトされるため、認証情報を入力します。 認証に成功するとWorkSpacesクライアントが再度起動します。 起動後、パスワードの入力画面が表示されたら、パスワードを入力します。 ※この際、Entra ID認証で利用したユーザー名が自動で入力されます。 パスワードを入力してサインインすると、無事デスクトップ画面が表示されました。   さいごに ここまで、Entra IDのSAML認証をAmazon WorkSpacesの多要素認証として利用する手順を説明しましたが、いかがでしょうか? 最後に、試してみた感想と注意すべき点について述べて終わりたいと思います。 ・Azure(Entra ID)と組み合わせることでWorkSpacesのセキュリティを向上させることができる ⇒セキュリティ向上はもちろん、他のクラウドサービスと連携できることがAWSの強みだと再実感しました。 ・仕様上の落とし穴に注意 ⇒Entra IDのユーザー名/メールアドレスとOS認証で利用するADユーザーのユーザー名/メールアドレスが一致している必要があるため、Entra IDとAD間の同期/連携を行っていない場合は注意が必要です。 以上です。ここまで読んでいただきありがとうございました。
アバター
こんにちは、広野です。 件名の件で地味に悩んだので、書き残します。AWS 公式ドキュメントで明確な答えを見つけられず、生成 AI に助けてもらいました。 やりたかったこと セキュリティ対策で、Amazon API Gateway HTTP API の特定のレスポンスヘッダーを任意の値にオーバーライドしたかったのですが、 AWS CloudFormation による設定の書き方 がわからず。 REST API だと以下のドキュメントに書いてあります。 API Gateway での REST API パラメータマッピングの例 - Amazon API Gateway Amazon API Gateway で API メソッドリクエストからメソッドレスポンスパラメータへのデータマッピングを設定する docs.aws.amazon.com   HTTP API だと具体的な記述方法が見当たりませんでした。CloudFormation のリファレンスの方にも無く。 API Gateway で HTTP API の API リクエストとレスポンスを変換する - Amazon API Gateway Amazon API Gateway HTTP API の API リクエストとレスポンスを変更するためのパラメータマッピングを設定する方法について説明します。 docs.aws.amazon.com   AWS CloudFormation テンプレート抜粋 結局、以下のように書くことで設定できました。インラインでコメントします。 統合 (Integration) の部分にオーバーライドしたいヘッダーの内容を書きます。 # ------------------------------------------------------------# # API Gateway # ------------------------------------------------------------# HttpApi: Type: AWS::ApiGatewayV2::Api Properties: Name: sample-send-function Description: HTTP API Gateway to send xxxxx ProtocolType: HTTP CorsConfiguration: AllowCredentials: false AllowHeaders: - "*" AllowMethods: - POST - OPTIONS AllowOrigins: - !Sub https://xxxxx.xxx ExposeHeaders: - "*" MaxAge: 600 DisableExecuteApiEndpoint: false IpAddressType: dualstack Tags: Cost: xxxxx HttpApiIntegration: Type: AWS::ApiGatewayV2::Integration Properties: ApiId: !Ref HttpApi IntegrationMethod: POST IntegrationType: AWS_PROXY IntegrationUri: !GetAtt Lambda.Arn # 関連付ける Lambda 関数の ARN CredentialsArn: !GetAtt ApiGatewayLambdaInvocationRole.Arn # Lambda 関数を invoke するロール PayloadFormatVersion: 2.0 TimeoutInMillis: 5000 ### ここにレスポンスヘッダーを書く ### ResponseParameters: "200": # HTTP レスポンスステータスごとに書かないといけないので完璧ではない。 ResponseParameters: # 二重に ResponseParameters が登場するが、これで正しい。 - Destination: "append:header.X-Content-Type-Options" # Destination が変更方法とパラメータ名。 Source: nosniff # Source が値。わかりにくい!! - Destination: "overwrite:header.Cache-Control" Source: "no-store, max-age=0" - Destination: "overwrite:header.Content-Security-Policy" Source: "frame-ancestors 'none';" - Destination: "overwrite:header.X-Frame-Options" Source: DENY - Destination: "overwrite:header.Strict-Transport-Security" Source: "max-age=31536000; includeSubDomains" HttpApiRoute: Type: AWS::ApiGatewayV2::Route Properties: ApiId: !Ref HttpApi RouteKey: POST /send Target: !Sub integrations/${HttpApiIntegration} HttpApiStage: Type: AWS::ApiGatewayV2::Stage Properties: ApiId: !Ref HttpApi AutoDeploy: true StageName: $default 注意事項 書いたコメントと重複しますが、以下、ハマり箇所でした。 HTTP レスポンスステータス単位で書かないといけない。エラーステータスのときも書くとなると、うーん。きちんとやろうとすると Amazon CloudFront をかぶせた方が正解なんでしょうね。 ResponseParameters の項目が二重に出てくるのですが、それで正しいです。気持ち悪いですが・・・。 Destination が変更方法とパラメータ名、Source が値、というのが非常にわかりにくいです。このネーミングではやってみないとわからないです。 X-Content-Type-Options ヘッダーについては、overwrite (上書き) ではなく append (追加) にしないとエラーになりました。   結果 AWS マネジメントコンソールで設定を確認したところ、以下のように反映されました。API の統合のメニューにあります。   呼び出し元アプリ (ブラウザ) の方にも、設定したレスポンスヘッダーが返されていることが確認できました。めでたし、めでたし。   まとめ いかがでしたでしょうか。 今回は小ネタでしたが、同じことでハマる人がいるかもしれないと思い書いておきました。 本記事が皆様のお役に立てれば幸いです。
アバター
皆さんはじめまして!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で実装してみました。 皆さんのお役に立てれば幸いです。
アバター