TECH PLAY

MNTSQ

MNTSQ の技術ブログ

å…š88ä»¶

MNTSQ プラットフォヌム郚の藀原です。 本蚘事では、PythonずLibreOfficeを組み合わせたオフィスファむルのpdf倉換に぀いお解説したす。 LibreOffice はオヌプン゜ヌスのオフィススむヌトです。 Microsoft Officeで䜜成した各皮ファむルdocxや、xslx、pptxを読み蟌み、線集できたす。 LibreOfficeにはこれらファむルをpdfで゚クスポヌトする機胜も存圚しおいたす。 GUIからの実行はもちろんCLIでも実行可胜です。 soffice --headless --convert-to pdf ファむル名.docx LibreOfficeを導入枈みの堎合はこのようなコマンド 1 を実行するこずで、docxなどをpdfに倉換できたす。 さお、 soffice コマンドでdocxファむルなどをpdfなどで倉換可胜なこずはここで瀺せたした。 りェブアプリケヌションなどでオフィスファむルをpdf倉換する堎合には、内郚で soffice コマンドを呌び出す圢で倉換できそうです。 ただし、この方匏では凊理のたびにプロセスを立ち䞊げるこずになりたす。 倧量のファむルを効率的に倉換しようず考えるず郜床プロセスを立ち䞊げるこずは非効率です。 そのための察策ずしお、LibreOfficeのプロセスを立ち䞊げた状態で倖郚プログラムからLibreOfficeの機胜を利甚するための仕組みが提䟛されおいたす。 それが、UNOUnified Network Objectsです。 UNOUnified Network Objectsに぀いお UNOUnified Network Objectsずは、LibreOfficeの内郚機胜に倖郚プログラムからアクセスするためのコンポヌネントです。 UNOは蚀語非䟝存でさたざたなプログラミング蚀語から利甚可胜です。 UNOぞのアクセス方法ずしおはむンプロセス方匏ず゜ケット接続方匏がありたす。むンプロセス方匏はLibreOfficeマクロからLibreOfficeの操䜜をするためのものです。゜ケット接続方匏は倖郚プロセス倖郚プログラムからTCPを䜿っお接続しおLibreOfficeの各皮操䜜をするための仕組みです。 次のようにするこずでTCP接続を介しおUNOを利甚できたす。 soffice --headless \ --accept="socket,host=localhost,port=2002;urp;"\ --norestore UNOを盎接操䜜するためのPythonパッケヌゞずしおは、 PyUNO が存圚したすが、オフィスファむルをpdfに倉換する目的ずしおは、too muchず蚀えそうです。 本蚘事ではUNOそのものの説明に぀いおは、このようにするず指定したTCPポヌトでLISTENさせるこずが可胜である旚に留めおおきたす。 以降は、UNOを䜿っおLibreOfficeの機胜を簡単に利甚するための仕組みずしおunoserverを玹介したす。 unoserverに぀いお unoconv/unoserver は、XML-RPC経由でUNOの仕組みを利甚できるようにするための、Pythonパッケヌゞです。 コンテナで動かす堎合の動䜜むメヌゞずしおは以䞋のようになっおいたす。 unoserverの動䜜むメヌゞ コンテナ内ではunoserverずheadlessなLibreOfficeを垞時立ち䞊げお凊理を埅ち受けおいたす 2 。 unoserverを簡䟿に利甚するため、たた、macOS等でも利甚する際の参考ずしおコンテナむメヌゞおよび、docker-compose.ymlを準備したした。 コヌドの党䜓像は Fufuhu/docker-unoserver にお公開しおいたす。 以䞋のようにコマンドを実行しおください。 docker run -d -p 2003:2003 fufuhu/unoserver コヌドからビルドしお利甚する堎合は以䞋の通り実行しおください。 git clone git@github.com:Fufuhu/docker-unoserver.git # dockerコマンドで利甚する堎合 docker build -t docker-unorsever . docker run -d -p 2003:2003 docker-unoserver # docker composeコマンドで利甚する堎合 docker compose up --build -d 動䜜確認ずしおPythonを䜿っおXML-RPCのリク゚ストを投げおみたす。 python -c " import xmlrpc.client; import json; print(json.dumps(xmlrpc.client.ServerProxy('http://localhost:2003').info()))" \ | jq { "unoserver": "3.6", "api": "3", "import_filters": { "HTML": "HTML (StarWriter)", 〜〜䞭略〜〜 "impress_svg_Export": "impress_svg_Export", "impress_tif_Export": "impress_tif_Export", "impress_webp_Export": "impress_webp_Export", "impress_wmf_Export": "impress_wmf_Export" } } このようにしお、汎甚のXML-RPCクラむアントを䜿っおunoserverを利甚するこずが可胜です。 ただし、unoserverの個別の機胜を利甚するには汎甚のクラむアントでは少々面倒です。 そこで、unoserverの提䟛するUnoClientを䜿った実装䟋を提瀺したす。 UnoClientのサンプル UnoClientはunoserverパッケヌゞに含たれおいるunoserver向けのXML-RPCクラむアントやバむナリデヌタの送受信などをラッピングした高レベルクラむアントです。 Fufuhu/docker-unoserver-client-sample にサンプルのコヌドを䜜成したした。 このリポゞトリのコヌドを git clone しお docker compose up --build -d でUNOサヌバヌずサンプルずなるりェブアプリケヌションが立ち䞊がりたす 3 。 すこし本題に入るたでが長くなりたすが、クラむアントりェブアプリケヌションのコヌドを眺めおみたしょう。 main.py の抜粋を芋おみたしょう。 from fastapi import FastAPI, Request, UploadFile from fastapi.responses import HTMLResponse, Response from fastapi.templating import Jinja2Templates from unoserver.client import UnoClient app = FastAPI() templates = Jinja2Templates(directory=Path(__file__).parent / "templates" ) UNOSERVER_HOST = os.getenv( "UNOSERVER_HOST" , "localhost" ) UNOSERVER_PORT = os.getenv( "UNOSERVER_PORT" , "2003" ) FastAPIを䜿ったアプリケヌションサヌバヌが立ち䞊がりたす。 UNOサヌバヌのホスト名ず埅受ポヌトを環境倉数で指定できたす。 たた、テンプレヌト゚ンゞンであるJinja2向けのテンプレヌトを栌玍しおいるディレクトリずしお templates ディレクトリを指定しおいたす。 docker-compose.ymlの該圓郚分を抜粋するず次のようになっおいたす。 services : unoserver : image : fufuhu/unoserver ports : - "2003:2003" app : build : . ports : - "8000:8000" depends_on : - unoserver environment : - UNOSERVER_HOST=unoserver - UNOSERVER_PORT=2003 次に main.py のindex関数を芋おみたしょう。 / にアクセスするず、 index.html ファむルをレンダリングしお返すようになっおいたす。 @ app.get ( "/" , response_class=HTMLResponse) async def index (request: Request): return templates.TemplateResponse(request, "index.html" ) index.htmlのbodyタグ以䞋の抜粋ずしおは以䞋のずおりです 4 。 < body > < h1 > PDF倉換ツヌル </ h1 > < form method = "post" action = "/convert" enctype = "multipart/form-data" > < p > 倉換したいファむルを遞択しおください </ p > < input type = "file" name = "file" required >< br > < button type = "submit" > 倉換 </ button > </ form > </ body > ファむルを遞択しお倉換ボタンをクリックするず /convert にファむルがPOSTされる圢ずなっおいたす。 ブラりザ䞊で http://localhost:8000 にアクセスするず次のような衚瀺になりたす。 むンデックス画面むメヌゞ このフォヌム内でファむルを指定しお倉換ボタンをクリックするず、 /convert にファむルがPOSTされる圢ずなっおいたす。 次に main.py の /convert に察応するコヌドを芋おみたしょう。 @ app.post ( "/convert" ) async def convert ( file : UploadFile): # アップロヌドされたファむルの内容をバむト列ずしお読み蟌む indata = await file .read() # 元のファむル名から拡匵子を陀いた郚分を取埗し、ダりンロヌド甚のPDFファむル名を生成する stem = Path( file .filename).stem if file .filename else "output" out_filename = f "{stem}.pdf" # unoserverに接続するクラむアントを䜜成するXMLRPC経由で通信 client = UnoClient(server=UNOSERVER_HOST, port=UNOSERVER_PORT) # ファむルのバむト列をunoserverに送信し、PDF圢匏に倉換する # 倉換結果はPDFのバむト列ずしお返される result = client.convert(indata=indata, convert_to= "pdf" ) # 倉換されたPDFをレスポンスずしお返す # Content-Dispositionヘッダヌにより、ブラりザがファむルを盎接ダりンロヌドする return Response( content=result, media_type= "application/pdf" , headers={ "Content-Disposition" : f 'attachment; filename="{out_filename}"' }, ) 内容ずしおはコヌド䞭のコメントに蚘茉の通りです。 リク゚ストからアップロヌドされたファむルのバむト列を取埗しお、UnoClientを䜿っおpdf倉換を実珟しおいたす。 ポむントは、 UnoClient です。 UnoClient を利甚するこずで、XML-RPCなどの凊理を隠蔜しお簡朔に蚘述できおいたす。 実際の倉換凊理郚分ずしおは以䞋の2行のみで実珟できたす。 client = UnoClient(server=UNOSERVER_HOST, port=UNOSERVER_PORT) result = client.convert(indata=indata, convert_to= "pdf" ) ここたでで、Python(unoserver, UnoClient)ずLibreOfficeを䜿ったオフィスファむルのpdf倉換に぀いお瀺したした。 今回提瀺のサンプルでは、リク゚ストを受けおその堎で倉換凊理を実行しお倉換埌のpdfファむルを返しおいたす。 巚倧なファむルを凊理する堎合、UXなどを考慮するず奜たしい実装ずしおはこの限りではない点には泚意した方が良いでしょう 5 。 たずめ LibreOfficeのGUI/CLIを䜿っおオフィスファむル(docx, xslx, pptxなど)をpdfに倉換できる 倧量のファむルを倉換する堎合はLibreOfficeのプロセスを垞駐させ、UNO(Unified Network Objects)を䜿っお倉換する方が効率的である UNOを容易に利甚するための仕組みずしお unoconv/unoserver が提䟛されおいる unoserverは蚀語非䟝存なXML-RPCを䜿ったファむルのpdf倉換を提䟛しおいる。ただし、Pythonの堎合はunoserverパッケヌゞの提䟛するUnoClientがあり、容易にpdf倉換を実珟できる 参考文献 https://ja.wikipedia.org/wiki/XML-RPC https://github.com/unoconv/unoserver https://docs.libreoffice.org/pyuno.html https://hub.docker.com/r/fufuhu/unoserver https://github.com/unoconv/unoserver https://github.com/Fufuhu/docker-unoserver https://github.com/Fufuhu/docker-unoserver-client-sample コマンド名が soffice ずなっおいるのはLibreOfficeの歎史的経緯に起因するものです。基本的にはlibreofficeコマンドでもaliasが貌られおいるこずがほずんどであり、同等の動䜜が可胜なはずです ↩ コンテナ内郚でunoserverのプロセスず、LibreOfficeのプロセス䞡方を管理するための仕組みずしおtiniを導入しおいたす。 ↩ Fufuhu/docker-unoserverから立ち䞊げたdockerコンテナやdocker composeずポヌト競合が発生するので、あらかじめ停止した䞊で実行しおください。 ↩ CSS指定郚分などは今回はメむンではないので䟋瀺からは陀倖しおいたす。 ↩ レスポンスで倉換枈みファむルを即時返华するのではなく、裏偎でむベント駆動で凊理したのちに倉換凊理完了通知ずダりンロヌドリンクを送るなどが考えられたす ↩
アバタヌ
はじめに 匊瀟では LLM を掻甚した機胜開発の芳枬基盀ずしお Langfuse をセルフホストで運甚しおいたす。Langfuse は LLM アプリケヌションのトレヌシングやプロンプト管理等に掻甚できるオヌプン゜ヌスの LLM ゚ンゞニアリングプラットフォヌムです。 さおこの皮のサヌビスには認蚌の課題が぀きたずいたす。Langfuse は暙準でメヌルアドレス / パスワヌドによるログむンが可胜ですが、匊瀟の方針ずしお開発組織内で利甚、管理しおいるアプリケヌションやサヌビス矀には IAM Identity Center を IdP ずする SSO を採甚しおいたす。 Redash や SendGrid ずいったサヌビスで既にこの方匏を採甚しおおり、Langfuse でも同様に IAM Identity Center 経由の SSO ログむンを実珟したいず考えたした。 ずころが Langfuse は IAM Identity Center を IdP ずしお盎接サポヌトしおいたせん。Langfuse の認蚌は NextAuth.js ベヌスであり、察応する認蚌方匏は OAuth / OIDC が䞭心です *1 。では IAM Identity Center の OIDC 機胜をそのたた䜿えるかずいうず、そう単玔ではありたせん。IAM Identity Center が OIDC を提䟛するにはアプリケヌション偎が trusted token issuer に察応しおいる必芁があり、Langfuse はこれに該圓しないため盎接利甚できたせん。 Cognito ず Langfuse の SSO 連携に぀いおは 既に優れた先䟋 がありたすが、ここに IAM Identity Center を加えた構成に぀いおの情報はあたり無いようです。ニッチな話題かもしれたせんが、同様の課題を抱えおいる方もいるのではず考え、本皿にお事䟋を玹介できればず思いたす。 構成 前述の事情を螏たえ、間に Amazon Cognito を挟む構成ずしたした。Cognito は SAML によっお IdP からの認蚌情報を受け取り、これを OIDC におアプリケヌション今回の堎合は Langfuseに提䟛する圹目を担いたす。IAM Identity Center は SAML による倖郚アプリケヌション今回の堎合は Cognitoのフェデレヌションをサポヌトしおいるので、これを組み合わせるず以䞋のような構成が実珟できたす。 IAM Identity Center --- (SAML) ---> Cognito --- (OIDC) ---> Langfuse 認蚌フロヌを時系列で描くず以䞋のようになりたす。 sequenceDiagram autonumber participant User as ナヌザヌ (Browser) participant LF as Langfuse participant Cog as Cognito participant IDC as IAM Identity Center User->>LF: SSO ログむンボタンをクリック LF->>User: Cognito のログむン URL ぞリダむレクト User->>Cog: 認蚌リク゚スト (OIDC) Cog->>User: IAM Identity Center ぞリダむレクト User->>IDC: SAML 認蚌リク゚スト IDC->>User: ログむン画面の衚瀺 / 認蚌の実斜 IDC->>User: SAML Assertion を発行 User->>Cog: SAML Assertion をポスト (ACS URL) Cog->>Cog: SAML Assertion の怜蚌 / ナヌザヌ情報の凊理 Cog->>User: Langfuse ぞリダむレクト (認可コヌド) User->>LF: 認可コヌドを送信 LF->>Cog: トヌクン亀換リク゚スト Cog->>LF: ID トヌクン / アクセストヌクン (OIDC) LF->>User: ログむン完了 芁するに Cognito が SAML -- OIDC 間の橋枡し圹を担う栌奜です。ナヌザヌ芖点では Langfuse のログむン画面で SSO ボタンを抌すず IAM Identity Center のログむン画面に遷移し、認蚌埌 Langfuse に戻っおきたす。 これを実珟する構成が以䞋のようになりたす。本皿の趣旚は Langfuse 利甚にかかる SSO ログむン化が䞻軞に぀き、Langfuse 本䜓の構成はかなり端折った図である点ご容赊ください *2 構成図。admin / langfuse / logging / security は AWS アカりント名の意 なお䞊図における logging / security 各アカりントは以䞋拙皿における member / security に察応するものです。䞻に SSO ログむン時の監査に䜿われる呚蟺芁玠であり、本皿では詳现を割愛したす。 蚭定の手順 蚭定は倧きく4぀のステップに分かれたす。 IAM Identity Center に SAML アプリケヌションを登録するadmin アカりント Cognito で SAML IdP ず OIDC クラむアントを構成するlangfuse アカりント Langfuse に OIDC 関連の環境倉数を蚭定するlangfuse アカりント SSO ログむンぞの䞀本化langfuse アカりント 以䞋、各ステップの内容を解説したす。 Step 1: IAM Identity Center ぞの SAML アプリケヌション登録 admin アカりントの IAM Identity Center に、Langfuse 甚のカスタム SAML アプリケヌション customer-managed application を Terraform で登録したす *3 。 aws_ssoadmin_application リ゜ヌスで application_provider_arn に custom-saml を指定し、IAM Identity Center のポヌタル䞊での可芖性を有効にしたす。 あわせお aws_ssoadmin_application_assignment で SSO ログむンを蚱可する察象を玐付けたす。グルヌプ単䜍での割り圓おも可胜ですが、匊瀟では職責や担圓職務に応じお柔軟にアクセス範囲を制埡したかったため、ナヌザヌ単䜍での割り圓おずしたした *4 。 続いおマネゞメントコン゜ヌルから Application metadata を蚭定したす。これは Cognito が SAML Assertion を正しく受け取るために必芁な蚭定です。 Application ACS URL : https://<Cognito ドメむン>.auth.<リヌゞョン>.amazoncognito.com/saml2/idpresponse Application SAML audience : urn:amazon:cognito:sp:<Cognito User Pool ID> ACS URL は Cognito User Pool のドメむン名から、SAML audience は Cognito User Pool の ID からそれぞれ導出されたす。Step 2 で Cognito User Pool を䜜成した埌に蚭定する、ずいう順序になりたす *5 。 同じくマネゞメントコン゜ヌルから Attribute mappings を蚭定したす。SAML Assertion に含めるナヌザヌ属性ず Cognito 偎の属性ずの察応を定矩するものです。 User attribute in the application Maps to this string value or user attribute in IAM Identity Center Format Subject倉曎䞍可 ${user:email} persistent email ${user:email} basic email_verified true unspecified name ${user.name} unspecified email_verified を固定倀 true ずしおいるのは匊瀟の運甚䞊 IAM Identity Center で管理されおいるナヌザヌのメヌルアドレスは怜蚌枈みであるず芋なしお差し支えないためです。これは以䞋の拙皿に詳现を譲りたすが、匊瀟の IAM Identity Center は IdP ずしお Entra ID を参照しおおり、Entra ID 䞊のナヌザ情報を信頌できるケヌスに該圓する為です。 最埌に、マネゞメントコン゜ヌルから SAML メタデヌタファむルをダりンロヌドしたす。 IAM Identity Center コン゜ヌルぞ移動 Application assignments → Applications を遞択 Customer managed タブぞ移動 察象の SAML アプリケヌションを遞択 IAM Identity Center metadata 内の IAM Identity Center SAML metadata file からダりンロヌド このメタデヌタ XML ファむルは次のステップで Cognito に枡す必芁がありたす。 Step 2: Cognito の構成 langfuse アカりント偎では Cognito User Pool を䜜成し、IAM Identity Center を SAML IdP ずしお登録した䞊で、Langfuse 向けの OIDC クラむアントを構成したす。 たず Step 1 でダりンロヌドした SAML メタデヌタ XML を S3 バケットに手動でアップロヌドし、Terraform からは data "aws_s3_object" で参照したす *6 。S3 バケット自䜓も Terraform で䜜成し、サヌバヌサむド暗号化ずパブリックアクセスブロックを蚭定しおいたす。 続いお、この SAML メタデヌタを䜿っお aws_iam_saml_provider ず aws_cognito_identity_provider の2぀を蚭定したす。前者は IAM レベルでの SAML プロバむダ登録、埌者は Cognito User Pool に察する SAML IdP の登録です。 aws_cognito_identity_provider では attribute_mapping で Step 1 の Attribute mappings ずの察応を指定したす。 なお provider_details には ignore_changes を蚭定しおいたす。Cognito は MetadataFile を解釈しお内郚的にいく぀かの属性を展開するため、内容に倉曎がなくおも terraform plan で差分が出続けたす。これを抑制するための措眮です。 次に aws_cognito_user_pool_client を蚭定したす。これは Langfuse から芋た OIDC クラむアントに盞圓するリ゜ヌスです。OAuth フロヌには Authorization Code Grant code を、スコヌプには openid / email / profile を指定したす。 callback_urls には Langfuse の OIDC コヌルバック URL https://<Langfuse のドメむン>/api/auth/callback/custom を蚭定したす。トヌクンの有効期間やクラむアントシヌクレットの生成など、必芁な蚭定を適宜入れたす。 最埌に、Cognito が払い出した Client ID / Client Secret / Issuer URL を aws_ssm_parameter SecureStringで SSM パラメヌタストアに栌玍し、Langfuse の ECS タスク定矩から参照できるようにしたす。 Step 3: Langfuse ぞの環境倉数蚭定 Langfuse は Custom OAuth Provider ずしおの蚭定を環境倉数で受け取りたす。ECS タスク定矩に以䞋の環境倉数を远加したした。 平文で枡すものずしお AUTH_CUSTOM_NAME 、 AUTH_CUSTOM_CHECKS 、 AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING の3぀がありたす。SSM パラメヌタストアから取埗する秘密情報ずしお AUTH_CUSTOM_CLIENT_ID 、 AUTH_CUSTOM_CLIENT_SECRET 、 AUTH_CUSTOM_ISSUER の3぀があり、これらは Step 2 で栌玍した倀を参照したす。 AUTH_CUSTOM_NAME は Langfuse のログむン画面に衚瀺される SSO ボタンのラベルです。わかりやすい名称を遞ぶず良いでしょう AUTH_CUSTOM_CHECKS には pkce を指定し PKCE (Proof Key for Code Exchange) を有効にしおいたす AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING を true にするず、同䞀メヌルアドレスの既存アカりントず SSO アカりントが玐付けられたす SSO 導入前のアカりントずの互換性のために有効にしおいたす これが無いず埌述のトラブルシュヌト時に Cognito 偎で察応する手法が取れず Langfuse 偎 DB を盎接觊っおアカりント管理をする手間が発生したす Step 4: SSO ログむンぞの䞀本化 SSO ログむンが安定皌動しおいるこずを確認した埌、環境倉数 AUTH_DISABLE_USERNAME_PASSWORD を true に蚭定しおメヌルアドレス / パスワヌドによるログむンを無効化したした。これにより Langfuse のログむン画面は SSO ボタンのみが衚瀺される状態になりたす。アカりント管理を IAM Identity Center に䞀元化でき、Langfuse 偎での個別のアカりント発行 / 削陀が䞍芁ずなりたす。 サンプルコヌド Step 1〜2 で䜿甚した Terraform コヌドの抜粋を以䞋に瀺したす。それぞれ collapsed になっおいたすので、クリックしお䞭身を参照ください。 admin アカりント: IAM Identity Center ぞの SAML アプリケヌション登録 # Identity Store からナヌザヌ情報を参照 data "aws_identitystore_user" "main" { for_each = toset (local.langfuse_users) identity_store_id = tolist (data.aws_ssoadmin_instances.main.identity_store_ids) [ 0 ] alternate_identifier { unique_attribute { attribute_path = "UserName" attribute_value = each.key } } } # カスタム SAML アプリケヌションの登録 resource "aws_ssoadmin_application" "langfuse" { name = "Langfuse" application_provider_arn = "arn:aws:sso::aws:applicationProvider/custom-saml" instance_arn = tolist (data.aws_ssoadmin_instances.main.arns) [ 0 ] portal_options { visibility = "ENABLED" sign_in_options { origin = "IDENTITY_CENTER" } } } # SSO ログむンを蚱可するナヌザヌの割り圓お resource "aws_ssoadmin_application_assignment" "langfuse" { for_each = toset (local.langfuse_users) # 職責に応じおフィルタしたナヌザヌリスト application_arn = aws_ssoadmin_application.langfuse.arn principal_id = data.aws_identitystore_user.main [ each.key ] .id principal_type = "USER" } langfuse アカりント: Cognito の構成 # ----- SAML メタデヌタの管理 ----- # メタデヌタ XML を栌玍する S3 バケット暗号化・パブリックアクセスブロックは別途蚭定 resource "aws_s3_bucket" "saml_metadata" { bucket = "mntsq-$ { var.env } -saml-metadata" } # IAM Identity Center からダりンロヌドしたメタデヌタ XML を参照 data "aws_s3_object" "saml_metadata" { bucket = aws_s3_bucket.saml_metadata.id key = "langfuse.xml" } # ----- SAML プロバむダ・Cognito User Pool ----- resource "aws_iam_saml_provider" "langfuse" { name = "langfuse" saml_metadata_document = data.aws_s3_object.saml_metadata.body } resource "aws_cognito_user_pool" "langfuse" { name = "langfuse" auto_verified_attributes = [ "email" ] } resource "aws_cognito_user_pool_domain" "langfuse" { user_pool_id = aws_cognito_user_pool.langfuse.id domain = "mntsq-$ { var.env } -langfuse" } # IAM Identity Center を SAML IdP ずしお登録 resource "aws_cognito_identity_provider" "langfuse" { user_pool_id = aws_cognito_user_pool.langfuse.id provider_name = "langfuse" provider_type = "SAML" provider_details = { "MetadataFile" = data.aws_s3_object.saml_metadata.body } attribute_mapping = { email = "email" email_verified = "email_verified" name = "name" } lifecycle { # Cognito が MetadataFile を解釈しお provider_details を展開するため、 # 毎回差分が出るのを抑制する ignore_changes = [ provider_details ] } } # ----- OIDC クラむアント (User Pool Client) ----- resource "aws_cognito_user_pool_client" "langfuse" { name = "langfuse" user_pool_id = aws_cognito_user_pool.langfuse.id allowed_oauth_flows_user_pool_client = true allowed_oauth_scopes = [ "openid" , "email" , "profile" ] allowed_oauth_flows = [ "code" ] supported_identity_providers = [ aws_cognito_identity_provider.langfuse.provider_name ] access_token_validity = 8 id_token_validity = 8 refresh_token_validity = 1 token_validity_units { access_token = "hours" id_token = "hours" refresh_token = "days" } callback_urls = [ "https://<Langfuse のドメむン>/api/auth/callback/custom" ] default_redirect_uri = "https://<Langfuse のドメむン>/api/auth/callback/custom" generate_secret = true } # ----- 認蚌情報の SSM パラメヌタストア栌玍 ----- resource "aws_ssm_parameter" "auth_id" { name = "/$ { var.env } /langfuse/AUTH_CUSTOM_CLIENT_ID" type = "SecureString" value = aws_cognito_user_pool_client.langfuse.id } resource "aws_ssm_parameter" "auth_secret" { name = "/$ { var.env } /langfuse/AUTH_CUSTOM_CLIENT_SECRET" type = "SecureString" value = aws_cognito_user_pool_client.langfuse.client_secret } resource "aws_ssm_parameter" "auth_issuer" { name = "/$ { var.env } /langfuse/AUTH_CUSTOM_ISSUER" type = "SecureString" value = "https://cognito-idp.$ { data.aws_region.current.name } .amazonaws.com/$ { aws_cognito_user_pool.langfuse.id } " } トラブルシュヌティング 運甚を開始しお以降、SSO ログむンが倱敗するケヌスに遭遇したした。ずくに初回ログむン時に顕著で、Cognito ず Langfuse ずの間でナヌザヌ情報のフェデレヌションがうたくいかない堎合に発生したす。 リトラむで解消するケヌスもありたすが、それでも解決しない堎合は Cognito 偎のナヌザヌを䞀旊削陀し、再床 Langfuse ぞ SSO ログむンしおもらう こずで通るようになりたす。Cognito User Pool 内のナヌザヌは IAM Identity Center から federate されたものなので、削陀しおも次回の SSO ログむン時に再䜜成されたす。 手順ずしおは以䞋の通りです。 Cognito User Pool のマネゞメントコン゜ヌルぞ移動 User management → Users でログむンに倱敗しおいるナヌザヌを特定 察象ナヌザヌを遞択し、たず Disable user access を実斜 その埌 Delete user を実行 ナヌザヌ削陀埌に改めお SSO ログむンを詊みおもらえば、Cognito 偎にナヌザヌが再䜜成されおログむンが成功するはずです。 おわりに IAM Identity Center を IdP ずしお Langfuse に SSO ログむンを実珟する構成に぀いお解説したした。ポむントは Cognito を SAML ず OIDC ずの翻蚳圹ずしお掻甚する 点です。 蚭定にあたっおは Terraform ずマネゞメントコン゜ヌルの手䜜業が混圚する点がやや煩雑です。ずくに IAM Identity Center 偎の Application metadata や Attribute mappings、SAML メタデヌタの取埗はコン゜ヌル操䜜が必芁であり、手順ずしお残しおおかないず再珟が難しくなりたす。 Cognito + Langfuse の SSO 構成 に぀いおは先䟋がありたすが、IAM Identity Center を加えた構成に぀いおはあたり情報が芋圓たりたせんでした。このパタヌンは Langfuse に限らず、OIDC のみをサポヌトするアプリケヌションに IAM Identity Center 経由の SSO を提䟛したい堎合に広く応甚できるものず考えおいたす。同様の課題を抱えおいる方の参考になれば幞いです。 文責MNTSQ 株匏䌚瀟 SRE 秋本 泚蚘この蚘事は文責者の過去蚘事ず匊瀟内のドキュメントをもずに Claude Opus 4.6 が䜜成した内容を9割皋床そのたた䜿甚しおいたす *1 : https://langfuse.com/self-hosting/security/authentication-and-sso を参照。Langfuse は Google / GitHub / Azure AD (Entra ID) / Okta / Auth0 / Custom OAuth Provider 等をサポヌトしおいたす *2 : 興味のある方向けECS で ClickHouse 含めお Langfuse が必芁ずする諞芁玠を動䜜させ、DB は Aurora PostgreSQL、キャッシュ関連は ElastiCache にお Valkey クラスタを組んで動䜜させおいたす *3 : 実際には党おの蚭定を Terraform でカバヌできるわけではないので、適宜 AWS マネゞメントコン゜ヌルからの蚭定䜜業も必芁になりたす *4 : この皮の柔軟さをグルヌプ単䜍で衚珟できなかったずいう内郚事情もありたす *5 : 厳密には Cognito User Pool の䜜成 → IAM Identity Center の Application metadata 蚭定 → Cognito 偎の残りの蚭定、ずいう順序を螏む必芁がありたす。Terraform で䞀括管理する堎合は初回適甚時にこの䟝存関係を意識する必芁がありたす。芁するに IAM Identity Center ず Cognito ずを行ったり来たりする必芁が2026幎2月時点で有りたす *6 : SAML メタデヌタはコヌド管理の察象ずしおいたせん。IAM Identity Center 偎で生成されるものであり、か぀ XML の内容が倧きいため S3 経由での参照ずしたした。メタデヌタの取埗手順は Step 1 に蚘茉の通りです
アバタヌ
はじめに Datadog には Notebook ずいうものがありたす。以前から存圚する機胜であり、目新しいものではありたせん。 詳现は公匏のドキュメントが詳しいのですが、Datadog で取り扱えるログやメトリックなどを埋め蟌める ドキュメンテヌション ツヌルずいう捉え方で然皋ズレは無いはずです。単にログやメトリックを远うずいう意味ではダッシュボヌド *1 も䜿え、 継続的な芳枬をする堎合は ダッシュボヌドのほうが断然よいです。 いっぜうでこういった課題感もあるず思いたす。すくなくずも私は垞々ありたす。 あのメトリックずこのメトリックずは䜕らか関連性のある動きをするはずなんだが、パッず远いづらいぞ 監芖やダッシュボヌドの蚭蚈をしたいのでメトリック状況を远いたいのだが、 メトリクス゚クスプロヌラヌ ではいたいち状況がわからないぞ 耇数のメトリックを束ねお蚈算したい *2 が、メトリクス ゚クスプロヌラ ヌで頑匵っおいたら蚳分からなくなっおしたった *3 メトリックを芋぀぀考察や芋解を添えおおきたい。別途存圚するドキュメント偎に Datadog ぞの誘導を蚭けるでもよいが、ひず぀の堎所で扱いたい こういった堎合、ダッシュボヌドを぀くるよりも Notebook を䜿うほうが手軜か぀確実な効果が埗られたす。 本項にお匊瀟で掻甚事䟋をあげ぀぀、Datadog Notebook は䟿利だよ、ずいう声を埮々たるものながら䞊げられればず思いたす。 事䟋1Elasticsearch → OpenSearch ぞの移行準備にかかる既存調査 匊瀟では珟圚怜玢基盀の曎改を怜蚎しおおり、珟状利甚しおいる Elasticsearch から Amazon OpenSearch ぞの移行を手段のひず぀ずしお考えおいたす。 この移行にあたり、移行先 OpenSearch ドメむン *4 のリ゜ヌス芋積もりをする必芁があり、その材料を既存 Elasticsearch の状態から埗ようず考えたした。 実際に䜿った Elaseticsearch 状況確認甚の Notebook黒塗り郚分が倚い点ご容赊ください 出すべき材料はおおむね以䞋のようになるはずです Elasticsearch ノヌド台数 各 Elasticsearch ノヌドのリ゜ヌス消費量 CPU RAM *5 ディスク 収容テナント数 収容シャヌド量 発生しおいるコスト これらメトリックは既に Datadog 䞊に蓄積されおいたす。もちろんこれらを䞀瞥するようなダッシュボヌドを䜜るこずも可胜ですが、今回は 定点監芖するこずを目的に確認したい蚳ではない 䞊べた数字を眺めながら議論をすすめるケヌスが倚々有った 既にあるドキュメントは詳现な考察ず怜蚎に䜿い、メトリックを䞊べた堎所は玔粋にメトリックから埗られる所感だけを扱うようにしたい需芁があった ずいうこずもあり、ずりわけ 1. を䞻芁な目的ずしお Notebook ぞ、いわば「曞き散らし」するこずにしたした。 継続的な芳察を目的ずせず䞀時的な調査ずしお Notebook ずいう手段をもっおおくこずで、こういった曞き散らしに数字的な意味を持たせるこずができたす。か぀䜓裁を気にせず玠早くたずめが仕䞊げられるので、議論ず結論出しを迅速に行うこずができたした。 事䟋2Datadog Cloud Cost ず Unit Economics もうひず぀の事䟋は、 Datadog Cloud Cost を掻甚した、䞀歩螏み蟌んだコスト分析での利甚です。 実際に䜿った Unit Economics 怜蚎甚 Notebook こちらも黒塗り郚分が倚い点ご容赊ください 匊瀟では先般 Datadog の Cloud Cost を䜿い始めおおり、 AWS / Google Cloud / Azure 各コストの俯瞰的な芳察ができるようになっおいたす。以䞋拙皿も参照ください。 䞀方で Cloud Cost を折角導入した以䞊、コストをただ芳察するのではなく、よりよい掞察を埗たい気持ちもありたす。前掲拙皿の通り Datadog メトリックずしお各 クラりド ベンダのコスト情報を適切な分解胜でもっお Datadog 内で扱える ずいう利点が Cloud Cost にはあり、各皮メトリックずコスト情報ずを掛け合わせたものが敎備できそうです。぀たり単に「いくらかかっおいるか」を眺めるだけでなく、その費甚察効果を 定量 的に評䟡できる䜙地がありそうです。そこで Unit Economics の抂念を取り入れ、 クラりド 利甚にかかるコストの経枈性を考察するこずにしたした。 SaaS における Unit Economics ずいえば、䞀般的には LTVLife Time Value顧客生涯䟡倀 CACCustomer Acquisition Cost顧客獲埗コスト の比率で語られたす。しかし、゚ンゞニアリングの偎面からこれを捉え盎すず 1テナントあたりの提䟛コスト (COGS) Cost Of Goods Sold売䞊原䟡 アクティブナヌザヌ以䞋 Active User を略しお AU ず衚蚘したすあたりのむンフラ費甚 ずいったものをいかに最適化し、粗利を最倧化するかずいう議論に単 玔化 できたす。 こうした指暙を远跡するなかで、特定の利甚パタヌンにおいお Unit Economics の悪化が芋られた堎合、それはそのパタヌンを支えるむンフラ的な アヌキテクチャ やリ゜ヌス配分に改善の䜙地がある、ずいう仮説が立ちたす。単なるコストの総額ではなく、ナニットあたりの効率を芋るこずで、゚ンゞニアリングずしお優先的に手を入れるべき箇所を特定しようずいう詊みです。 この分析を行うには、 クラりド コストのデヌタに加えお、Datadog 䞊の既存メトリックやログから生成したメトリックを突き合わせる必芁がありたす。この詊行錯誀においお、 Notebook が非垞に圹立ちたした。 扱うデヌタの怜蚎 たずは クラりド コストをテナントで割るか AU で割るかの怜蚎が必芁です。テナント単䜍で芋るのが前述のずおり筋ず圓初は考えたのですが、䞡者の実態や傟向に差異がある郜合、実際にはナヌザヌあたりで芋た堎合によりよい掞察が埗られるケヌスもあり、最終的には䞡者を状況によっお比范する぀たりテナント按分コストず AU 按分コストずを䞡方算出する手法をずるこずになりたした。 この詊行を玠早く繰り返す際に圹立ったのが Notebook で、耇数の蚈算結果を再び曞き散らし、実態を最も反映しおいそうな指暙が䜕かをグルグル考え回すこずができたした。 芳察 コスト芳察は長期間の芳察が䞍可欠です。日単䜍や週単䜍ではあたり意味のある情報は埗られたせん。定点芳察にはダッシュボヌド化が適したすが、そもそも今組み立おおいるデヌタが有意矩かどうかが刀っおいない段階でのダッシュボヌド化は早蚈でしょう。 たずは Notebook を サンドボックス ずしお䜿い、少ない敎備の手間で傟向芳察をはかるこずが可胜ずなりたした。 考察 デヌタが揃い、芳察が出来るようになったのち、必芁なのはその結果の考察です。これにも Notebook ぞの「曞き散らし」が䟿利でした。 デヌタの定矩はどういったものでこの結果は䜕を瀺すものず考えられるか等の情報がデヌタず同じ堎所にメモでき、䜕を䌝えればよいのだ等ず思い悩む必芁性が枛り、思い切った怜蚌ができるようにもなりたす。 たた怜蚎ず蚂正を繰り返しおいるず蚪れる「果たしおこれは䜕を意味するのだったか」ず倱念しお困惑するずいう事態も、曞き散らしによっお避けるこずができたす。 芁するに いきなり「正解」のダッシュボヌドを䜜るのではなく、 Notebook 䞊でこうした泥臭い怜蚌を重ねられたこずが、最終的に粟床の高い可芖化ぞの近道になりたした。 なお最終的に Notebook の内容はダッシュボヌドずしお新たに敎備するこずになりたした。有益さが刀った結果ですが、最初からダッシュボヌドにしおおけばよかったず思わないでもないです。 これは珟時点2026幎1月では Notebook からダッシュボヌドに移行できるような機胜が Datadog には無いためで、 Notebook 䞊に敎備した各皮メトリックをダッシュボヌドに気合で移す䜜業が必芁になりたした。今ずなっおは良い思い出です。 おわりに 今回の掻甚を通じお、以䞋のような嬉しさがありたした。 Elasticsearch 移行 怜蚌数倀的な裏付けを持っお机䞊での怜蚎ができるようになり、移行蚈画に際し十分な情報を提瀺するこずができた Notebook に適宜情報を远加しながら仮説怜蚌を繰り返すサむクルが回せたこずで、手戻りの少ない怜蚎が可胜ずなった Unit Economics 分析 仮説怜蚌が Notebook で玠早く行え、これが有益かどうかの怜蚎が手軜に行えるようになった たずは Notebook で「これで本圓に分析できるか」を確かめ、確信を持っおからダッシュボヌドずしお正匏に敎備する、ずいうフロヌを怜蚌できた Datadog Notebook は、監芖や o11y *6 ずいったメむン機胜に比べるず、正盎なずころ圱が薄い印象がありたす *7 。しかし、実際に䜿っおみるず「ダッシュボヌドを䜜るほど倧掛かりではないが、メトリクス ゚クスプロヌラ ヌよりは螏み蟌んだこずがしたい」ずいう堎面で倧倉䞁床よいツヌルです。 メトリクス ゚クスプロヌラ ヌでは耇数メトリックの取扱が勿論可胜ですが、どうしおもその堎限りの確認になりがちです。䞀方、 Notebook であれば耇数のメトリックを時系列や比率で比范した「論理構造」をそのたた残しおおけたす。これは぀たり グラフの偎に「なぜこの数字を芋たのか」ずいうコンテキストを蚘録できる 䞀時的な調査結果を、そのたたチヌム内ぞの共有レポヌトずしお転甚できる ダッシュボヌド化する前の「プロトタむプ」ずしお掻甚できる このように、䞀皮の「思考の跡地」ずしおの䟡倀が生たれるわけです。 ダッシュボヌド化する前の䞋曞きずしお、あるいは䞀時的な調査レポヌトずしお。䟿利な手段のひず぀ずしお Datadog Notebook があるずいう点、認知に貢献できたら幞いです。 MNTSQ 株匏䌚瀟 SRE 秋本 *1 : https://docs.datadoghq.com/dashboards/ *2 : 䟋 https://docs.datadoghq.com/monitors/guide/monitor-arithmetic-and-sparse-metrics/ *3 : 私的にこれが䞀番ありたす *4 : https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html の語法に則り「 ドメむン 」ず呌びたすが、デヌタノヌドおよび専甚マネヌゞドノヌドの集合䜓ずいう意味から「 クラスタ 」ず同䞀芖しおよいはずです *5 : Elasticsearch であるこずから実際には JVM ヒヌプメモリ量などの状況も重芁になりたす。今回このあたりを匕っ括めお "RAM" ず読んでいたす *6 : observability可芳枬性。本皿を読んでいる方には釈迊に説法感がありたすが念の為 *7 : これたで圚籍しおきたいく぀かの䌚瀟でわたしは Datadog Notebook を䜿っおきたしたが、いざ共有などするず「こんな機胜があったのか」ずいう反応に毎床なりたす
アバタヌ
前回のあらすじ スキヌマ 分離蚭蚈のDBテナント毎に独立した スキヌマ を持぀DBでサヌビス芏暡が拡倧するず、 スキヌマ 数の増加に由来するオヌバヌヘッドが無芖できないものになる 次はパラメヌタチュヌニングなどで䜕ずか延呜できないか詊しおみたい tech.mntsq.co.jp はじめに 前回の 負荷詊隓 によっお、匊瀟サヌビスは600テナントを超えたあたりから、デヌタベヌスの急激な性胜劣化を起こすリスクが高いこずが刀明したした。長期的には根本的な構成の芋盎しを行うずしお、パラメヌタチュヌニングなどでデッドラむンを埌ろにずらせるのであれば、それはそれでありがたいです。 よっお、今回の 負荷詊隓 の目的は、 チュヌニングによっお アヌキテクチャ 改善をどの皋床埌ろ倒しにできるかを怜蚎するこずでした過去圢 。 結論を申し䞊げるず、匊瀟のケヌスではパラメヌタチュヌニングによっお延呜を図れる芋蟌みは薄いこずが刀明したした。ただし、調査の過皋で、前回の結果の解釈の誀りを発芋したり、新たな条件で芋えおきた スキヌマ 分離の定性的な特性を発芋したりず、それなりに実りのある結果が埗られたので、再び報告させおいただきたいず思いたす。 前回のあらすじ はじめに 远加詊隓の結果報告 真のボトルネックはwait/io/table/sql/handlerだった パラメヌタチュヌニングに぀いお table_definition_cache & table_open_cache innodb_sync_array_size むンスタンスサむズを倧きくしおみる 䞀定高負荷䞋でのスキヌマ数によるパフォヌマンスの倉化 150スキヌマで性胜が劣化する理由 1200スキヌマで性胜が劣化する理由 負荷詊隓を行う䞊で泚意したいこず 本番環境ず詊隓環境の違いを考える 枬定の分散に぀いお おわりに 远加詊隓の結果報告 真の ボトルネック は wait/io/table/sql/handler だった wait/io/table/sql/handler は、ストレヌゞ゚ンゞン局における行レベルの操䜜読み取り、挿入、曎新、削陀などに察しお、 SQL 局が費やした埅機時間を蚈枬するむベントです。䞀般には物 理I /O、行レベルのロック埅ちなどが䞻原因になりたす。 前回の結果では wait/synch/mutex/innodb/dict_sys_mutex が ボトルネック になるず誀った結論を぀けおしたいたしたが、これは暖機運転りォヌムアップ䞍足による䞀時的な珟象でした。以䞋は暖機運転完了前埌の埅機時間の内蚳を、PeformanceInsiteの 平均アクティブセッション(AAS) のグラフで確認したものです。 暖機運転完了前埌の埅機時間の内蚳 これを芋るずvCPU数を倧幅にオヌバヌしお wait/synch/mutex/innodb/dict_sys_mutex の埅機むベントが支配的になっおいる時間巊偎: 暖機運転䞭ず、vCPU数以䞋の範囲内で wait/io/table/sql/handler が支配的になっおいる時間右偎: 暖機運転完了がハッキリず分かれおいるこずが確認できたす。 wait/synch/mutex/innodb/dict_sys_mutex は デヌタディクショナリ ぞのアクセス競合でしたが、必芁な メタデヌタ がすべおメモリに乗り切れば、基本的に競合は発生したせん。よっお、今回の枬定の条件䞋では、 wait/synch/mutex/innodb/dict_sys_mutex が発生しおいる堎合は暖機運転が十分でないず蚀えたす。 たた、このような wait/synch/mutex/innodb/dict_sys_mutex の倉化は、今回の枬定条件内では table_definition_cache テヌブル定矩の メタデヌタ を茉せるキャッシュなどのキャッシュサむズが十分に足りおいるこずを瀺唆しおいたす。 以降の枬定は暖機運転を十分に行い、この埅機むベントの倉化を確認しおから本枬定を行いたした。 なお、今回の怜蚌では最終的にこの wait/io/table/sql/handler の ボトルネック を解消するこずはできたせんでしたが、高䞊列・高QPSの負荷環境においおは、ストレヌゞ゚ンゞンが膚倧なリク゚ストを凊理する䞊で避けられない珟象であるず解釈しおいたす。 wait/io/table/sql/handler の埅機時間の内蚳を、ク゚リの皮別ごずに分けたものが以䞋の画像です。 負荷を構成しおいるク゚リの比率がそのたた埅機時間の比率になっおいたした 。したがっお、特定のスロヌク゚リがこれ発生させおいるわけではないこずがわかりたす。 `wait/io/table/ sql /handler`のク゚リ皮別ごずの内蚳 パラメヌタチュヌニングに぀いお チュヌニング むンスタンス サむズなどの倉曎も含むを詊みた項目に぀いお、端的に結果をたずめおいきたす。結論、 今回の枬定条件䞋では 、パフォヌマンスの改善はほずんどみられたせんでした。 table_definition_cache & table_open_cache table_definition_cache は、テヌブルの定矩情報 メタデヌタ を保持するグロヌバルなキャッシュです。 デヌタディクショナリ にアクセスする代わりにこのキャッシュを参照するこずで、テヌブルの定矩参照凊理を高速化するこずができたす。 table_open_cache は、各スレッドがテヌブルにアクセスする際に䜿甚するオブゞェクトを保持するキャッシュです。テヌブルを物理的にオヌプンする凊理は CPU コストが高いため、このキャッシュに保存されたオブゞェクトを再利甚するこずで、オヌバヌヘッドを劇的に䜎枛できたす。 前回の調査結果から、 メタデヌタ ぞのアクセス埅機が ボトルネック であるず予想しおいたため、キャッシュサむズを倧きくするこずで改善が芋蟌めるず思い、これらのパラメヌタに぀いお調査を行いたした。 チュヌニングが有効でなかった理由は、先ほどの暖機運転の議論でも述べた通り、今回の枬定においおはキャッシュサむズは最初から十分足りおいたためです。キャッシュヒット率も確認したしたが、99.555%ずほずんどキャッシュミスは発生しおいたせんでした。前回も述べたずおり、負荷は十数皮類のパタヌンのク゚リで䜜っおおり、参照テヌブルも数皮類にずどたりたす。そのため、 メモリの䜿い方は本番環境の方が圧倒的にハヌドであり、今回の枬定ではメモリぞの負荷や改善策を評䟡するこずができたせん。 もしも本番環境で 頻繁にキャッシュミスが発生しおいた堎合には、これらのパラメヌタのチュヌニングはパフォヌマンスを改善する䞊で非垞に有効 になりたす。 innodb_sync_array_size innodb_sync_array_size は、内郚同期甚の配列サむズを調敎するパラメヌタで、CPUコア数の倚い環境で倀を倧きくするず、耇数のスレッドが内郚ラッチを取り合う際の競合 wait/synch/mutex/innodb/sync_array_mutex を緩和し、スケヌラビリティを向䞊させるこずができたす。 このパラメヌタを増やすず、埅機䞭スレッドの倚いワヌクロヌドの同時実行性が高たるずいうのが通説なので、調査を行いたした。 チュヌニングが有効でなかった理由はシンプルで、 wait/synch/mutex/innodb/sync_array_mutex が発生しおいなかったので調敎する必芁がなかったずいうものです。 むンスタンス サむズを倧きくしおみる これたでの枬定では2xlargeを䜿甚しおいたしたが、これを4xlargeにスケヌルアップした際に性胜が改善するかを調査したした。クラむアント偎は24䞊列・各スレッドは500QPSの蚭定で負荷をかけたした。以䞋は枬定結果の䞀郚です。 むンスタンス TotalQPS QueryCount P99[ÎŒs] Avg [ÎŒs] r8g.2xlarge 8450 15,210,198 4.44E+03 2.36E+03 r8g.4xlarge 9233 16,618,969 3.15E+03 2.06E+03 4xlargeの方が党䜓的に性胜が改善しおいるのがわかりたす。しかし、2xlarge -> 4xlargeではCPU、メモリ共に2倍になっおいるにも関わらず、性胜の改善はQPS換算でせいぜい10皋床でした。これは費甚察効果ずしおは非垞に効率が悪いです。 この理由は、CPUを䜿甚しおいるセッションの内蚳で説明できたす。 2xlarge 4xlarge 䞡方ずも䞻芁な埅機は wait/io/table/sql/handler です。4xlargeの方はCPUが2倍になっおいるため瞊軞の瞮尺が異なりたすが、その絶察倀はほずんど倉わっおいないこずがわかりたす。2xlarge時点でCPUは ボトルネック ではなかったCPU数を超えたアクティブセッションが発生しおいなかったため、4xlargeに倉曎しおCPUが増えおも、目に芋えた性胜改善はできなかったず解釈できたす。逆に、このグラフでCPU数を超えお埅機しおいるセッションが倚くなった堎合は、CPUの数を増やすこずで倧きな性胜の改善が期埅できたす。 したがっお、 むンスタンス のスペックアップは、必ずしも限界を迎えた際の応急凊眮ずしお䜿えるずは限らない ず蚀えたす。 䞀定高負荷䞋での スキヌマ 数によるパフォヌマンスの倉化 Aurora MySQL のパフォヌマンスず スキヌマ 数の関係をより掘り䞋げお調べおみたした。前回ずは以䞋の点を倉曎し、150 ~ 1200 スキヌマ にお再枬定を行いたした。 クラむアントの負荷蚭定を固定する24䞊列 * 500QPS = 12000TotalQPS 十分な暖機運転を行いキャッシュに乗り切ったこずを確認しおから本枬定を行う 枬定結果を以䞋にたずめたす。かなり 盎感ずは異なる結果 になりたした。 高負荷で固定した際のパフォヌマンスの倉化 なんず、600 スキヌマ が最も スルヌプット が高く、 スキヌマ 数が増えた時だけではなく、少なくなった時にも性胜の劣化が芋お取れたす。 そしお150, 600, 1200 スキヌマ での枬定時のAASのグラフは以䞋のようになっおいたした。色は異なりたすが、支配的ずなっおいるのはすべお wait/io/table/sql/handler です。 150 スキヌマ 600 スキヌマ 1200 スキヌマ 600 スキヌマ での枬定で最も平均AASが少なくなっおいたす。たた、1200 スキヌマ に加えお、150 スキヌマ での枬定でも wait/io/table/sql/handler の埅機時間が増加しおいたす。 150 スキヌマ で性胜が劣化する理由 150 スキヌマ 構成では、600 スキヌマ 構成ず比范しおテヌブルあたりのアクセス密床が4倍になりたす。たずえ スキヌマ が分かれおいおも、 InnoDB 内郚の管理甚ハッシュテヌブルやラッチ 排他制埡 は むンスタンス 党䜓で共有、あるいは少数の パヌティション で管理されおいたす。 150 スキヌマ では管理察象が少ない分、 特定の管理 パヌティション にアクセスが集䞭する「 ホットスポット 」 が発生しやすく、内郚的な順番埅ちが頻発したす。この埮现な足止めが積み重なり、結果ずしおストレヌゞ゚ンゞン局の凊理時間である wait/io/table/sql/handler を匕き延ばしおいるず考えられたす。満遍なく䞀定の遅延が発生しおいるこずも、この説を裏付けたす。 150 スキヌマ 1200 スキヌマ で性胜が劣化する理由 䞀方、1200 スキヌマ 構成での劣化は、150 スキヌマ の時ずは逆に 「管理察象の膚倧さ」によるオヌバヌヘッド が原因であるず考えられたす。 今回の枬定ではキャッシュヒット率が99.5%を超えおおり、ディスクI/Oの圱響は無芖できたす。しかし、これほど倧量の スキヌマ が存圚するず、 InnoDB が高速化のために生成する「 アダプティブ ハッシュむンデックスAHI」などの管理デヌタ自䜓が巚倧化したす。この巚倧化したデヌタの䞭から目的のデヌタを探し出す際、たずえメモリ䞊であっおも管理 パヌティション の奪い合いが発生したす。この様子は wait/synch/sxlock/innodb/hash_table_locks の埅機時間(茶色)ずしお珟れおおり、これに比䟋しお wait/io/table/sql/handler が増加しおいるこずがわかりたす。 ぀たり、分散のメリットよりも、巚倧なリ゜ヌスを管理・怜玢するコストが䞊回っおしたった状態ず蚀えたす。 1200 スキヌマ 逆に蚀えば、適切な範囲内であれば スキヌマ を分離しお負荷を分散するこずパヌティショニングによっおパフォヌマンスが向䞊するケヌスもあるず蚀えたす。これは前回時点では認識しおいなかった スキヌマ 分離のメリットですね なお、今回の枬定はク゚リの皮類ずテヌブルを絞り、メモリ負荷が スキヌマ 数に察しお非垞に少なくなるような条件で行っおたす。実際に倧量 スキヌマ のテヌブルに察しお満遍なくアクセスが発生する堎合は、 wait/synch/mutex/innodb/dict_sys_mutex  デヌタディクショナリ ぞのアクセス競合などが支配的になるこずも十分に考えられたす。 負荷詊隓 を行う䞊で泚意したいこず 負荷詊隓 においお最も泚意しなければならないのは、 「埗られた数倀を絶察芖し、誀った解釈をしおしたうこず」 です。今回の枬定を通しお芋えおきた、詊隓蚭蚈ず結果評䟡における泚意点をたずめたす。 本番環境ず詊隓環境の違いを考える 前回、今回の 負荷詊隓 は、 䞊䜍のものずはいえ、䜿甚するク゚リやアクセスするテヌブルを絞った環境にお行ったものになりたす。たた、アクセスの䞊列数も24䞊列ず実際のワヌクロヌドに比べるず少ないものであり、1スレッドあたりの負荷を䞊げお総量を補っおいるずはいえ、本番環境を再珟しおいるずは蚀い難いです。 よっお、今回は以䞋のような点に泚意しお結果を評䟡しおいたす。 「キャッシュ䞍足」は評䟡できおも、「キャッシュ十分」ずは評䟡できない 負荷詊隓 はあくたでよく䜿われおいる䞀郚のク゚リ、テヌブルのみをサンプリングしおいる。䜿甚されないテヌブルはキャッシュに乗らない 結果は鵜呑みにするのではなく、定性的に再解釈する 䟋えば負荷の総量が同じでも、1䞊列でかける負荷ず100䞊列でかける堎合ではDB内郚の 排他制埡 は党く異なるため、結果の数倀をそのたた受け取っおはいけない。数倀をそのたた本番のキャパシティずしお解釈するのではなく、振る舞いの定性的な倉化を読み取るこずに䞻県を眮くべき 負荷詊隓 には、 条件蚭定によっお評䟡できるものず評䟡できないものがある ずいうこずです。たた、数倀をそのたたの状態で受け取るこずもミ スリヌド を生む危険がありたす。このような点に泚意しお詊隓蚭蚈、結果評䟡を行いたしょう。 枬定の分散に぀いお 以䞋は党く同じ負荷蚭定で、 数時間以内に 枬定した結果を比范したものです。サンプルは少ないですが、QPS換算で1%皋床の分散におさたっおいたす。 TotalQPS QueryCount P99[ÎŒs] Avg [ÎŒs] 8554 15,398,029 4.73E+03 2.34E+03 8459 15, 226 ,641 4.49E+03 2.37E+03 8460 15,228,216 4.32E+03 2.36E+03 察しおこちらは党く同じ負荷蚭定で、 日付を跚いで 枬定した結果を比范したものです。 TotalQPS QueryCount P99[ÎŒs] Avg [ÎŒs] 9118 16,412,667 4.60E+03 2.09E+03 8450 15,210,198 4.44E+03 2.36E+03 なんず、QPS換算で 7.3%皋床の差 が出おしたっおいたす。マネヌゞドサヌビスゆえの䞍可避な倖郚芁因 AWS の垯域や近隣 むンスタンス の負荷によるものだず思っおいたすが、この結果は、枬定そのものの分散よりも時間垯による分散の方が遥かに倧きなこずを瀺唆しおおり、 最䜎でも7%以䞊の分散 があるこずがわかりたす。 ぀たり、 この枬定では「5%皋床の性胜改善」を論じおも意味がない わけです。その数倀は誀差の範囲に埋もれおしたうからです。結果を数倀的に求めたい堎合は、枬定自䜓の誀差がどの皋床なのかを評䟡する必芁があり、それができない限りは 定量 的な評䟡は難しいずいうこずです。間違っおも、1回だけ枬定しお「こんな数倀が取れたした」ずいう結果の受け取り方はしないようにしたしょう。 おわりに 前回蚘事の内容ず合わせお本栌的な 負荷詊隓 を行い、圓初の目的であった珟行 アヌキテクチャ のデッドラむンを芋積もるずいう目的は無事達成できたした。しかし、目的達成以䞊に、その詊行錯誀の過皋から倚くのこずを孊ぶこずができたず思いたす。 「 スキヌマ 数が増えれば管理コストで遅くなる」ずいう䞀般論は知っおいおも、実際に手を動かしお枬定を行い、予想ず異なる振る舞いに悩み、考え抜いたこずで、より MySQL に関する理解は深たりたした。そしお、「掚論ず怜蚌を繰り返すこずで ブラックボックス を䞀぀ず぀明らかにしおいく」ずいうプロセスそのものが、゚ンゞニアにずっお䜕よりも倧切な経隓であり、自信にも぀ながるこずを再確認できたした。 本蚘事の詊行錯誀の過皋が、これから 負荷詊隓 に挑む皆様にずっお、䞀歩を螏み出すための参考になれば幞いです。 MNTSQ株匏䌚瀟 SRE 西宀
アバタヌ
はじめに 課題感 スプリットビュヌ DNS サンプルコヌド おわりに 参考 はじめに 小ネタです。蚘事タむトルが長いのですが、これは本皿の内容を1行で説明したものになりたす。 そしお OpenSearch に限らない䞀般的な話題ずしおは repost.aws ずいう優れた情報が既にありたす。本皿では Terraform コヌドの䟋瀺や背景に぀いおの解説を䞎えるこずで、付加䟡倀を䞎えんずするものになりたす。 課題感 OpenSearch を AWS 䞊のマネヌゞドサヌビスずしお扱う堎合、 OpenSearch ドメむン を倖郚からのアクセスが可胜なものずしお構築するか、 VPC 内に閉じたものずしお構築するかの2択がありたす *1 。埌者を遞ぶ堎合 OpenSearch ドメむン ぞのアクセスは察象の VPC 内からのみアクセスが可胜になりたす たた OpenSearch ドメむン には䜜成埌暙準で払い出される゚ンドポむントずは別にカスタム゚ンドポむントを蚭定するこずができたす。これは apex を含む任意 ドメむン を利甚者の所望のものにするこずができるものになりたす *2 さお OpenSearch は通垞 HTTPS による通信が匷制されおおり、これは暙準の゚ンドポむント / カスタム゚ンドポむント 䞡方で該圓したす。暙準の゚ンドポむントに぀いおは AWS がよしなに SSL 蚌明曞を蚭定しおくれたすが、カスタム゚ンドポむントに぀いおはナヌザ偎で SSL 蚌明曞を甚意しなくおはなりたせん。 手軜にこのあたりをやるには ACM で SSL 蚌明曞を払い出そうずいう話になり、より手軜にやろうずすればカスタム゚ンドポむント蚭定倀ずしおの ドメむン の存圚確認を DNS 怜蚌でやる *3 ずいう段取りになるでしょう。 ただし ACM による DNS 怜蚌は むンタヌネット経由で名前解決が可胜な DNS レコヌド に察しおのみ察応可胜です。 VPC に閉じる構成ずした OpenSearch ドメむン に察し蚭定するカスタム゚ンドポむントで䜿う ドメむン をむンタヌネットから名前解決可胜ずするメリットは無いでしょうから、これも VPC 内からのみ名前解決が可胜な Route 53 の private hosted zone で蚭定するこずになりたす *4 。 ぀たり カスタム゚ンドポむントずしお䜿甚する ドメむン はむンタヌネットから名前解決䞍可 になり、これで䜕が困るかずいうず ACM による DNS 怜蚌が䞍可 ずなりたす。 いたたでの議論をたずめるず以䞋のようになりたす。 課題感 スプリットビュヌ DNS これを解決するには本皿題名に掲げたスプリットビュヌ DNS *5 が有効でしょう。 AWS においおは Route 53 に関するドキュメントのうち こちら にあるずおり、Route 53 においお private / public の䞡 hosted zone を組み合わせるこずで実珟が可胜です。すなわち example.com ずいう public な hosted zone を Route 53 に远加する internal. example.com ずいう private な hosted zone を Route 53 に远加する これだけです。 opensearch.internal.example.com ずいう ドメむン を VPC 内に閉じた OpenSearch ドメむン のカスタム゚ンドポむントずしお䜿甚するこずを考えるず opensearch ずいうレコヌドで OpenSearch ゚ンドポむントを向く CNAME レコヌドを private 偎の Route 53 hosted zone internal.example.com に察応ぞ蚭定 opensearch.internal.example.com ずいう ドメむン に察し ACM 䞊で SSL 蚌明曞発行 2. の結果埗られた DNS 怜蚌甚レコヌドを public 偎の Route 53 hosted zone opensearch.internal.example.com に察応ぞ蚭定 ここたでに埗られた ACM 侊 SSL 蚌明曞の ARN ず ドメむン 名ずを OpenSearch ぞカスタム゚ンドポむントずしお蚭定 ずいう手筈を螏めば、 ACM で DNS 怜蚌が可胜な OpenSearch カスタム ドメむン 向けの SSL 蚌明曞が手に入り、か぀ VPC 内に限っお OpenSearch ドメむン におカスタム ドメむン を䜿っおの通信が可胜、ずいう状態にもっおゆくこずができたす *6 。 opensearch .internal. example.com な OpenSearch カスタム゚ンドポむントを䜿う図 サンプルコヌド 䞊蚘構想を AWS マネゞメントコン゜ヌルでやるのは少々骚です。実際に䜿甚しおいる Terraform コヌドに適宜修正を加えたサンプルコヌドを䟋瀺したす variable "hostzone" { type = object ( { external = string internal = string } ) default = { external = "example.com" internal = "internal.example.com" } } variable "custom_endpoint" { type = string nullable = true default = "opensearch.internal.example.com" } resource "aws_vpc" "main" { # 詳现略 } /* Route 53 関連 各 hosted zone 定矩ず OpenSearch カスタム゚ンドポむントずしお䜿うドメむンに察応する DNS レコヌドの蚭定を行う */ resource "aws_route53_zone" "external" { name = var.hostzone.external } resource "aws_route53_zone" "internal_split_dns" { name = var.hostzone.internal vpc { vpc_id = aws_vpc.main.id } } resource "aws_route53_record" "opensearch_custom_endpoint" { # 本皿の䟋では垞時 true だが var.custom_endpoint が無い堎合はレコヌド䜜成䞍芁なので条件分岐できるようにしおおく for_each = ( var.custom_endpoint != null ? { "main" = {} } : {} ) zone_id = aws_route53_zone.internal_split_dns.zone_id /* var.custom_endpoint の apex ドメむンは aws_route53_zone.internal_split_dns で指される Route 53 ゟヌンに䞀臎する必芁がある この理屈から圓該 apex zone に蚭定すべき OpenSearch ドメむン甚 DNS レコヌドの倀は var.custom_endpoint から apex zone 名を差っ匕けば埗られる */ name = replace (var.custom_endpoint, aws_route53_zone.internal_split_dns.name, "" ) type = "CNAME" ttl = 60 records = [ aws_opensearch_domain.main.endpoint, ] } /* ここから ACM 関連 蚌明曞払い出しず DNS 怜蚌のための各皮蚭定たでを行う */ resource "aws_acm_certificate" "internal_split_dns" { domain_name = var.custom_endpoint validation_method = "DNS" } resource "aws_route53_record" "internal_split_dns" { for_each = { for dvo in aws_acm_certificate.internal_split_dns.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type } } zone_id = aws_route53_zone.external.id allow_overwrite = true name = each.value.name records = [ each.value.record ] ttl = 600 # 適圓に倉曎可 type = each.value.type } resource "aws_acm_certificate_validation" "internal_split_dns" { certificate_arn = aws_acm_certificate.internal_split_dns.arn validation_record_fqdns = [ for record in aws_route53_record.internal_split_dns : record.fqdn ] timeouts { create = "15m" } } resource "aws_opensearch_domain" "main" { /* カスタム゚ンドポむント関連以倖の詳现は省略 実際に OpenSearch ドメむンを構築するには他にも必須項目がある See: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/opensearch_domain */ domain_endpoint_options { enforce_https = true custom_endpoint_enabled = var.custom_endpoint == null ? false : true custom_endpoint = var.custom_endpoint custom_endpoint_certificate_arn = aws_acm_certificate.internal_split_dns.arn } encrypt_at_rest { enabled = true } node_to_node_encryption { enabled = true } } おわりに 簡単ではありたすが OpenSearch 構成の tips に぀いお解説する項目ずなりたした。カスタム゚ンドポむントを䜿わない堎合、 OpenSearch のデフォルトでは https://search-<ドメむン名>-<ランダム文字列>.<リヌゞョン>.es.amazonaws.com のようになり *7 、か぀ VPC 内に閉じる堎合ではより長く https://vpc-search-<ドメむン名>-<ランダム文字列>.<リヌゞョン>.es.amazonaws.com ずなりたす。これが本皿の䟋でいえば https://opensearch.internal.example.com ずしお扱えるようになり、シンプルな゚ンドポむントで OpenSearch を䜿うこずが可胜になりたす。 甚途その他の事情で OpenSearch デフォルトのものではない゚ンドポむントを蚭定したくなり、たたその察象ずなる OpenSearch ドメむン が VPC 内からのみ利甚可胜なものであった堎合に、本皿の内容がお圹に立おば幞いです。だいぶニッチな話題ではありたすが、そういった需芁は決しおれロではないず考えおいたす。 MNTSQ 株匏䌚瀟 SRE 秋本 参考 文䞭で瀺した匕甚以倖のものずしお、本皿の話題に取り組む前の調査時に 【Route53】スプリットビューDNSの名前解決順序を整理してみた | DevelopersIO を拝読したした。 *1 : https://docs.aws.amazon.com/opensearch-service/latest/developerguide/vpc.html *2 : https://docs.aws.amazon.com/opensearch-service/latest/developerguide/customendpoint.html *3 : https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html *4 : OpenSearch / ACM / VPC ずお膳立おを AWS 前提ずしお進めおきたので、いきなり DNS 関係も Route 53 を䜿うこずずしおもさしお矛盟はしないず思っおいたす *5 : 本皿を曞く䞊でこの定矩が気になったので調べたずころ https://datatracker.ietf.org/doc/html/rfc8499 や https://datatracker.ietf.org/doc/html/rfc6950/ が有甚そうでした。本皿を読む䞊でこの内容を理解する必芁は党くありたせん。これはほが私的なメモです *6 : 盎䞋の図䞭 xN の衚蚘は https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html に拠りたす *7 : https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createupdatedomains.html
アバタヌ
はじめに 匊瀟では AWS を䞻軞ずしたむンフラ構成をもっおプロダクトを展開しおいたすが、䞀郚では AWS 以倖にも Azure および Google Cloud も掻甚しおいたす。それぞれの棲み分けは以䞋のようなものになりたす。 AWS ほずんど党お コンピュヌト / ネットワヌク / ストレヌゞ / DB / セキュリティ etc. Azure OCR 関連 Google CloudLLM を䜿う凊理および OCR 関連 利甚芏暡ずしおは AWS >> Google Cloud > Azure ずいったものになり、結果ずしおこれら クラりド ベンダヌの利甚にかかるコスト利甚料金の意。以䞋本皿では「コスト」を料金を指す意味でのみ甚いたす割合も AWS が支配的なものになりたす *1 。 これら クラりド ベンダヌを扱う䞊でネックになるこずずいえば勿論コストです。各ベンダヌそれぞれコスト確認のための優れたツヌルを提䟛しおいたすが、無論各ツヌルはそれぞれのベンダヌ内に閉じるものであり、䟋えば「今月 Google Cloud コストが普段ず比べお劙な増え方をしおいるが AWS 偎でも倉動しおいる圢跡は無いだろうか?」ずいった調査をする堎合、 AWS ず Google Cloud ずで行き぀戻り぀しながらコスト調査を行う必芁が出お来たす。 このあたりぞの察凊ずしお、匊瀟では Datadog にコスト情報を集玄する ずいう遞択肢をずるこずにしたした。この内容に぀いお解説したす。 構成 抂芁を以䞋に瀺したす。 図の耇雑さの違いが瀺すずおり、Datadog にコスト情報を連携させる戊略は各 クラりド ベンダヌによっお異なりたす。やりかたは䞀通りではないずいう前眮きをし぀぀、匊瀟では以䞋のような手法をずっおいたす。 AWS consolidated billing アカりントで Cost & Usage Report (CUR) を生成し、Datadog で CUR の内容を収集する AWS Organizations 管理䞋にある党 AWS アカりントのコスト情報を Datadog に連携する 匊瀟では䟋倖なく党 AWS アカりントが AWS Organizations 管理䞋にあるので、個別に連携を頑匵るよりも合理的 匊瀟では AWS Organizations 管理アカりントが consolidated billing アカりントを兌ねおおり、 適切な暩限制埡をする前提においお 管理アカりントず Datadog ずを連携するのみで事足りる Azureコストを远いたい サブスクリプション に察し cost export を蚭定し、その結果を Datadog で収集する Google CloudDatadog 連携甚のプロゞェクトでコスト情報を BigQuery + Cloud Storage で扱えるようにした䞊でその内容を Datadog で収集する プロダクト向けのワヌクロヌドを皌動させおいるプロゞェクトは同䞀の請求アカりントに玐付けるような構成ずしおいるので、远跡したいコスト *2 ずしおはこの請求アカりントに関するもの Datadog 連携甚のプロゞェクトもこの請求アカりントに玐付け埌述、必芁なコスト情報を単䞀プロゞェクトに集玄する栌奜ずした さも最初から蚭蚈したかのように曞いおいたすが、実際には殆どの芁玠を Datadog が提䟛するドキュメントに埓っおのものになりたす。以埌本皿でも基本的にはこの情報を前提にしたす。 https://docs.datadoghq.com/cloud_cost_management/setup/aws/ https://docs.datadoghq.com/cloud_cost_management/setup/azure/ https://docs.datadoghq.com/cloud_cost_management/setup/google_cloud/ なお Azure のみ単䞀の サブスクリプション を察象ずしおいたすが、これは以䞋背景によりたす。匊瀟事情である、ずいうのが芁旚です。 本番ワヌクロヌドを凊理する サブスクリプション が Azure コストの支配的な芁玠を占めおおり、それ以倖の サブスクリプション のコストを远う動機が薄い AWS / Google Cloud ずは異なり Azure はプロダクト甚途以倖にもコヌポレヌト甚途の芁玠があり、SRE で管理できおいない郚分がある 具䜓的には管理グルヌプ単䜍でのコスト監芖蚭定を行うのが難しかった プロダクト向けのワヌクロヌドを考える範囲においおは 1. の理由で特定の サブスクリプション を远跡できれば充分ずいう背景があった 蚭定 基本的には前述の Datadog が提䟛する蚭定手順に埓えばスムヌズに蚭定できたす。ずくにコスト関連のお膳立おが敎っおいれば Datadog ずの連携およびコスト関連の情報取埗はかなりスムヌズに蚭定できたす。 䞀方でコスト情報を適切に出力する為の䜜業で䞀郚難儀したこずがありたした。これは䜜業者が AWS の経隓に寄り過ぎおいるこずも䞀因ですが、ひょっずしたら同じようなハマり方をする方がいるかもしれたせん。恥をしのんで解説したす。 なお以䞋では実際に筆者が䜜業した過皋をなぞるかたちでドキュメントベヌスで解説したすが、実際には Cloud Cost のアカりント蚭定画面 からりィザヌドに埓っお䜜業をすすめるほうが盎感的だったりしたす。たた文䞭で瀺すリンクテキストのリンク先は特蚘のない限り Datadog 公匏ドキュメントです。 AWS 䜜業察象前掲抂芁図から抜粋 Datadog の公匏ドキュメントは こちら になりたす。やるこずはおおたかに Datadog ず AWS アカりントずの連携蚭定 AWS アカりント偎での Cost & Usage Report (CUR) の蚭定 Datadog 連携甚 IAM ロヌルで 2. の CUR が参照できるよう暩限の蚭定 Datadog 偎での Cloud Cost 蚭定 の4点です。 ドキュメントでは CloudFormation を䜿う方法ず手䜜業 (manual) で進める方法の2択がありたすが、匊瀟では既存蚭定を Terraform で IaC 化しおいるためにお本䜜業も Terraform で進めるべく、手䜜業での蚭定ずしたした。この堎合に参考にすべき手順は こちら になりたす。 1. ず 4. は䞊掲手順に埓うのみで倧䞈倫そうでした。 2. ず 3. *3 を Terraform で蚭定する堎合のサンプルコヌドは以䞋のようになりたす。 variable "datadog_external_id" { type = string description = "External ID provided by Datadog on AWS integration" default = "" } data "aws_caller_identity" "current" {} data "aws_iam_policy_document" "datadog_integration" { statement { actions = [ "s3:PutObject" , "s3:GetBucketPolicy" , ] resources = [ aws_s3_bucket.datadog_integration.arn, "$ { aws_s3_bucket.datadog_integration.arn } /*" , ] principals { type = "Service" identifiers = [ "billingreports.amazonaws.com" , "bcm-data-exports.amazonaws.com" , ] } condition { test = "StringLike" variable = "aws:SourceArn" values = [ "arn:aws:cur:us-east-1:$ { data.aws_caller_identity.current.account_id } :definition/*" , "arn:aws:bcm-data-exports:us-east-1:$ { data.aws_caller_identity.current.account_id } :export/*" , ] } condition { test = "StringLike" variable = "aws:SourceAccount" values = [ data.aws_caller_identity.current.account_id, ] } } } resource "aws_s3_bucket" "datadog_integration" { bucket = "mntsq-master-datadog-integration" } resource "aws_s3_bucket_policy" "datadog_integration" { bucket = aws_s3_bucket.datadog_integration.id policy = data.aws_iam_policy_document.datadog_integration.json } /* Datadog の Cloud Cost では CUR 2.0 に察応しおいない legacy CUR の堎合は以䞋リ゜ヌスを䜿っお定矩する */ resource "aws_cur_report_definition" "datadog_integration" { report_name = "datadog-integration" time_unit = "HOURLY" format = "Parquet" compression = "Parquet" additional_schema_elements = [ "RESOURCES" , "SPLIT_COST_ALLOCATION_DATA" , ] s3_bucket = aws_s3_bucket.datadog_integration.bucket s3_prefix = "cost_and_usage_report" s3_region = aws_s3_bucket.datadog_integration.region } data "aws_iam_policy_document" "datadog_aws_integration_assume_role" { statement { actions = [ "sts:AssumeRole" ] principals { type = "AWS" identifiers = [ "arn:aws:iam::464622532012:root" ] # See: https://docs.datadoghq.com/integrations/guide/aws-manual-setup/?tab=roledelegation } condition { test = "StringEquals" variable = "sts:ExternalId" values = [ var.datadog_external_id, ] } } } data "aws_iam_policy_document" "datadog_aws_integration" { statement { actions = [ /* Datadog ず AWS ずの連携 (integration) で䜕を連携するか= Datadog にどのような AWS の情報を流すかによっお倉わる 必芁に応じお定矩する。今回は詳现省略 */ ] effect = "Allow" resources = [ "*" ] } } data "aws_iam_policy_document" "cost_and_usage_report_export" { statement { effect = "Allow" actions = [ "s3:ListBucket" ] resources = [ aws_s3_bucket.datadog_integration.arn, ] } statement { effect = "Allow" actions = [ "s3:GetObject" ] resources = [ "$ { aws_s3_bucket.datadog_integration.arn } /cost_and_usage_report/*" , ] } statement { effect = "Allow" actions = [ "ce:Get*" ] resources = [ "*" ] } statement { effect = "Allow" actions = [ "cur:DescribeReportDefinitions" ] resources = [ "*" ] } statement { sid = "DDCloudCostListOrganizations" effect = "Allow" actions = [ "organizations:Describe*" , "organizations:List*" ] resources = [ "*" ] } } resource "aws_iam_policy" "datadog_aws_integration" { name = "DatadogAWSIntegrationPolicy" description = "Allow actions for Datadog Integration" policy = data.aws_iam_policy_document.datadog_aws_integration.json } resource "aws_iam_policy" "datadog_cost_and_usage_report_export" { name = "DatadogCurExportPolicy" description = "Allow actions required to export Cost and Usage Report to Datadog" policy = data.aws_iam_policy_document.cost_and_usage_report_export.json } resource "aws_iam_role" "datadog_aws_integration" { name = "DatadogAWSIntegrationRole" description = "Role to integrate AWS and Datadog" assume_role_policy = data.aws_iam_policy_document.datadog_aws_integration_assume_role.json } resource "aws_iam_role_policy_attachment" "datadog_aws_integration" { role = aws_iam_role.datadog_aws_integration.name policy_arn = aws_iam_policy.datadog_aws_integration.arn } # AWS マネヌゞドポリシ。Datadog が Resource Collection 時に芁求する data "aws_iam_policy" "security_audit" { name = "SecurityAudit" } resource "aws_iam_role_policy_attachment" "datadog_aws_integration_security_audit" { role = aws_iam_role.datadog_aws_integration.name policy_arn = data.aws_iam_policy.security_audit.arn } resource "aws_iam_role_policy_attachment" "datadog_aws_integration_cost_and_usage_report" { role = aws_iam_role.datadog_aws_integration.name policy_arn = aws_iam_policy.datadog_cost_and_usage_report_export.arn } Azure 䜜業察象前掲抂芁図から抜粋 Datadog の公匏ドキュメントは こちら になりたす。やるこずはおおたかに Datadog ず Azure サブスクリプション ずの連携蚭定 Azure サブスクリプション での cost export 蚭定 cost export 結果を収容する storage container を Datadog 連携甚 の app registration が参照できるよう蚭定 Datadog 偎での Cloud Cost 蚭定 の4点です。 Azure に぀いおは IaC 管理できおいない範囲 *4 に぀き、手䜜業で蚭定をすすめおゆく必芁がありたした。なお本察応をおこなうたで Azure ず Datadog ずを組み合わせお利甚するケヌスは匊瀟内においお存圚せず、その段階からの䜜業が必芁ずなった点、補蚘しおおきたす。 1. に぀いおは 手順 を通読するこずになりたす。匊瀟では "Quickstart (recommended)" によっお䜜業をすすめたした。 続く 2. ですが、 手順 に埓い䜜業をする前に storage account blob container を甚意しおおく必芁がありたす。Datadog ず連携した Azure サブスクリプション 内に storage account を䜜り、その䞭に blob container を蚭ける栌奜です。cost export の結果は最終的に blob container 内に栌玍されたす。䞊述二点が甚意できたら前掲手順に埓い cost export を蚭定したす。いく぀かフォヌマットを蚭定できたすが、匊瀟では AWS の CUR に合わせお Parquet を遞定したした。 3. に぀いおは 手順 に埓い玠盎に蚭定すれば倧䞈倫です。Azure の䞖界芳ではタブは 画面巊偎 にあるので、手順内で "tab" ず説明されるものは画面巊偎にありたすので泚意が必芁です *5 。 4. に぀いおは手順通りで割合簡単に蚭定が可胜なので詳现は省略したす。 Google Cloud 䜜業察象前掲抂芁図から抜粋 Datadog の公匏ドキュメントは こちら です。以䞋5点が必芁な䜜業ずなりたした。本察応をおこなうたで Google Cloud ず Datadog ずを組み合わせお利甚するケヌスは匊瀟内においお存圚せず、その段階からの䜜業が必芁ずなった点、補蚘しおおきたす。 Datadog 連携甚のプロゞェクトを甚意し、Datadog ずの連携蚭定を実斜 cost export 蚭定を察象プロゞェクト内の BigQuery から取り扱う蚭定を远加 甚意したプロゞェクトに察し請求アカりントからの cost export 蚭定を远加 BigQuery 出力先ずなる Cloud Storage バケット を察象プロゞェクト内に远加 Datadog 甹 service account から 4. で蚭定した Cloud Storage バケット が参照できるよう暩限を蚭定 Datadog 偎での Cloud Cost 蚭定 Google Cloud に぀いおも IaC 管理できおいない範囲 *6 に぀き、手䜜業で蚭定をおこないたす。 1. に぀いおは 手順 を通読し必芁な䜜業を行いたす。どのように Google Cloud プロゞェクトを蚭蚈するかは個々のケヌスによっお様々な方法論があるず思いたすが、匊瀟では玠盎に Datadog ず連携するためのプロゞェクトを新蚭する方向にしたした。この際プロゞェクトは適圓な請求アカりントに玐付けないず Datadog ずの連携蚭定で゚ラヌになるため、事前に請求アカりント向けの蚭定を Google Cloud 偎の手順 などを参照しお枈たせおおく必芁がありたす。 2. に぀いおは こちら が手順ずなりたすが、ここでは既に BigQuery デヌ タセット が存圚しおいるこずが前提になりたす。利甚するプロゞェクト内で事前に甚意しおおき、その䞊で 3. に臚みたす。cost export 蚭定を 請求アカりントから 実斜する必芁がありたす。 4. ず 5. は 手順 に埓い玠盎に䜜業したす。ここで手順内の "co-located" は BigQuery デヌ タセット ず Cloud Storage バケット ずでリヌゞョン蚭定を同䞀にせよずいう意味になる *7 ので、そのように蚭定したす。 6. においおは手順に埓えば䜜業は簡単にできたす。以䞋が必芁になるので適宜参照できるよう準備しお蚭定に臚みたす。 請求アカりント ID プロゞェクト ID cost export 甚の BigQuery デヌ タセット 名 BigQuery デヌ タセット 出力先の Cloud Storage バケット 名 なお BigQuery でコスト情報が取り扱えるようになる前に Cloud Cost 蚭定に臚むず゚ラヌになりたす。おおむね数時間皋床 *8 埅぀ず BigQuery でコスト情報が取り扱えるようになるので、それ以降で蚭定を詊みるずよいでしょう。 結果 党おの蚭定が枈むず Cloud Cost 抂芁画面 で蚭定した クラりド ベンダヌ暪断の状況が芳察できるようになりたす。backfill 的な䜜業をしない限りは基本的に Cloud Cost 利甚開始時点のコスト情報が連携されるので、これが真䟡を発揮するのは充分にコスト情報が蓄積されおからずなるでしょう。 Cloud Cost そのものの機胜を䜿いこなすには匊瀟ずしおもただ至っおいたせん。有効にしおから日が浅く、ただ充分なデヌタが溜たっおいないずいう偎面がありたす。 䞀方で Datadog メトリックずしお各 クラりド ベンダのコスト情報を適切な分解胜でもっお Datadog 内で扱える ずいうメリットは Cloud Cost そのものの機胜を差っ匕いおも享受でき、匊瀟ではひずたず AWS / Azure / Google Cloud 党䜓のコスト確認甚 䞊蚘のうち䞻芁なコストファクタヌずしお扱われるアカりント / プロゞェクトに絞ったコスト確認甚 プロダクト環境単䜍でアカりント / プロゞェクト / サブスクリプション を敎理した䞊で「環境」を暪断しおのコスト確認甚 ずいった区分で ダッシュ ボヌドを぀くり、定期的に傟向をみる、ずいった運甚をずっおいたす。 3. が少々解り蟛いのですが、芁するに本番環境ずしお扱われるお客様のワヌクロヌドを実際に捌いおいる AWS アカりント / Google Cloud プロゞェクト / Azure サブスクリプション のコストを俯瞰しお芳察するずいうものです。 おわりに Datadog の Cloud Cost を䜿甚し、瀟内で利甚しおいる各 クラりド ベンダヌのコストを集玄しお芳察する方法に぀いお取り扱いたした。費甚倉動の痕跡を぀ぶさに远う䞊で参照すべき情報が散逞しおいる状況は倚くの苊劎を䌎いたすが、ひず぀の堎所を確認しおおけば間に合う、ずいう状況は䞭々の 心理的 安党性が芋蟌めたす。Cloud Cost は SaaS コストや Datadog 自身のコストも察象に含めるこずが出来る *9 ので、さらなる芳枬範囲の拡匵をおこない粟床の高いコスト監芖䜓制を組むこずも可胜そうです。匊瀟でも Cloud Cost の利甚は開始したばかりずいった状況なので、たずはデヌタの蓄積をし぀぀、よりよい䜿い所を怜蚎できればず考えおいたす。 マルチ クラりド 構成におけるコスト監芖に課題感をお持ちの方の察応怜蚎の䞀助になれば幞いです。 MNTSQ 株匏䌚瀟 SRE 秋本 *1 : 詳现な数字は割愛したすが AWS の利甚コストは Google Cloud / Azure に比べ桁が1぀異なりたす *2 : もちろん「プロダクト向けのワヌクロヌド」を皌動させおいるプロゞェクトが察象です。ずいうのも匊瀟ではこれ以倖にもプロゞェクトが様々な甚途で存圚し、それらプロゞェクトは別の請求アカりントに玐付きたす。これら党おのコスト情報を集玄させるず話が倧きくなっおしたうので、今回はワヌクロヌド皌動環境に絞るものずしたした *3 : IAM ロヌル / ポリシ関連のコヌドがあるこずから掚枬できるずおり、実際には 1. に察応する範囲も含んでいたす *4 : Terraform でやりたい *5 : 本皿筆者はこれでけっこうハマりたした。実は CUI が䞻な生掻空間なので、 GUI 操䜜には苊手意識がありたす *6 : Terraform でやりたい2 *7 : 恥ずかしながらドキュメントから誘導される https://docs.cloud.google.com/bigquery/docs/exporting-data#data-locations を読んでも勝手が解らず少々ハマりたした。英語的なニュアンスが汲み取れればよかったのですが   *8 : だいたい Google Cloud 偎で蚭定完了しおから 4 -- 5 時間ずいったずころでした。状況による倉動がかなりあるず掚枬されるので、ひず぀の参考事䟋ずご理解ください *9 : https://docs.datadoghq.com/cloud_cost_management/
アバタヌ
はじめに ゜フトりェア゚ンゞニアの森山です。 CloudFront で API ず静的ファむルを別オリゞンで扱い Single Page Application以䞋 SPAを ホスティング する構成に぀いお解説したす。 プラむベヌト API を遮断する蚭定を远加する際に CloudFront のカスタム゚ラヌレスポンスで躓きたした。しかし CloudFront Functions を掻甚するこずで䞊手く切り抜けるこずができたした。 構成 リク ゚ス トのパスを元にCloudFrontでオリゞンぞのアクセスを振り分けおいたす。 S3でSPAのアセットを ホスティング し、ALB経由でECSでバック゚ンドの API ぞ疎通しおいたす。 ハマりポむント /api/private/* ぞのリク ゚ス トのみALBで遮断する蚭定を斜したした。しかし、䜕床 API コヌルしおも ステヌタスコヌド 200が返华され遮断できおいないように芋えたした。詊しにWAFで遮断する蚭定をしおも結果は同じく ステヌタスコヌド 200が返华されたした。 AWS の蚭定反映が遅延しおいるこずを疑いたしたがWAFには /api/private/* のリク ゚ス トはブロックしおいる アクセスログ が蚘録されおいたした。 原因 原因は以䞋のCloudFrontのカスタム゚ラヌレスポンスでした。 䞊蚘はオリゞンからの ステヌタスコヌド 403,404のレスポンスを ステヌタスコヌド 200に䞊曞きしindex.htmlを返华したす。 カスタム゚ラヌレスポンスを蚭定しおいるずCloudFrontがレスポンスを返华する 盎前に䞊曞き するのでALBでブロックしおもその埌に200に䞊曞きしたレスポンスが返华されたす。 䜙談ですが、動䜜確認の反省ポむントは HTTPメ゜ッド: HEAD の API を䜿っおいたこずです。GETで確認しおいればレスポンスがindex.htmlに䞊曞きされるこずの早期発芋に繋がったはずです。 カスタム゚ラヌレスポンスの意図 ではなぜこんな蚭定をしおいたのか。 䞀蚀で蚀うず / 以倖のリク ゚ス トで index.html を返华するためです。 これはフロント゚ンドがSPAであるこずが関係しおいたす。SPAは名の通り1぀のhtml今回はindex.htmlを起点に動䜜したす。画面遷移は JavaScript で実装されたrouterが担いたす。routerがURLを管理し、動的importが評䟡されるたびにブラりザがアセットのリク ゚ス トを飛ばしたす。 SPAの堎合はこの起点ずなるindex.htmlありきです。 圓たり前ですがindex.htmlがナヌザヌに届けられなければ、アプリケヌションの゚ントリヌポむントが無いので埌続のjsや css を読み蟌めたせん。 CloudFrontでSPAを ホスティング しおいる堎合、ルヌトペヌゞ以倖ぞの盎接アクセスで index.html が存圚しない状態が発生し埗たす。 䟋えば https://mntsq.com/hoge ずいうURLぞrouterを介さずにブラりザのURLバヌぞ盎接入力しおリク ゚ス ト、新芏タブで画面遷移、たたはリロヌド等です。 その堎合は、 GET: https://mntsq.com/hoge リク ゚ス トが CloudFront ぞ飛びたす。 しかしS3は /hoge ずいうオブゞェクトが無いので゚ラヌを返したす。非認蚌なら403、認蚌埌なら404です。結果ずしおブラりザに index.html が届けられなくなっおしたいたす。 これを回避するためにカスタム゚ラヌレスポンスを蚭定しおいたした。 しかし、カスタム゚ラヌレスポンスの察凊ではマルチオリゞン構成の堎合に課題がありたす。 カスタム゚ラヌレスポンスの課題 カスタム゚ラヌレスポンスはS3以倖のオリゞンぞのレスポンスも䞊曞きしおしたうこずです。 カスタム゚ラヌレスポンスはCloudFrontの ディストリビュヌション に察しお蚭定されたす。 CloudFrontにおいお ディストリビュヌション ずオリゞンの以䞋の関係性です。 ディストリビュヌション S3 オリゞン API オリゞン S3だけでなく API のレスポンスが403や404の堎合にも䞊曞きされおしたいたす。そうなるずクラむアント偎で API の゚ラヌハンドリングもできたせん。 解決策 CloudFront Functionsで リク ゚ス ト を䞊曞きしたす。 CloudFront Functionsはカスタム゚ラヌレスポンスず違い ディストリビュヌション 事ではなくビヘむビア毎に玐぀けるこずができたす。 ぀たり /* ぞのリク ゚ス トだけを䞊曞きし、 /api/* ぞのリク ゚ス トに察しおは䜕もしないずいう制埡ができたす。 凊理の分割ずしおは以䞋のようになりたす。 CloudFront FunctionsではCloudFrontのリク ゚ス トやレスポンスに JavaScript で軜量な凊理を挟むこずができたす。 シヌケンス図内の⑚ 必芁に応じおindex.htmlを芁求の箇所は、拡匵子の有無で識別できたした。 そのため、拡匵子の無いリク ゚ス トはindex.htmlのリク ゚ス トに䞊曞きするずいうCloudFront functionを以䞋のように実装したした。 /** * SPAの初回ロヌドリク゚ストに察しお/index.htmlを返す関数 * * SPAでは新芏タブやリロヌドでルヌト以倖のURLに初回アクセスするずindex.htmlが返らず画面がホワむトアりトするため、 * この関数は初回リク゚ストを/index.htmlに曞き換える。 */ function handler ( event ) { const request = event . request const uri = request . uri const pathTail = uri . substring ( uri . lastIndexOf ( "/" ) + 1 ) // 最埌の/以降のパス const hasExtension = pathTail . includes ( "." ) // パスに.が含たれおいるか(ファむル拡匵子があるか) if ( ! hasExtension ) { // 拡匵子無しは、新芏タブやリロヌドによるルヌトペヌゞ以倖の初回ロヌドリク゚スト // SPAにおける゚ントリヌポむントである/index.html に曞き換える request . uri = "/index.html" } return request } } CloudFront Functionsで泚意すべき点は JavaScript のランタむム環境です。 JavaScript ランタむム1.0ず2.0の2皮類ありたすがどちらも ECMAScript 5.1に準拠しおいたす。 倧芏暡で レむテンシヌ の圱響を受けやすい CDN カスタマむズのための軜量な関数を蚘述できるずありたす。( link ) この軜量さを実珟するためにランタむム環境ずしお䜿える機胜も厳遞されおいるようなので泚意が必芁です。( link ) 䞊蚘のCloufFront Functionsは JavaScript ランタむム2.0で動䜜しおいたす。 たずめ カスタム゚ラヌレスポンスからCloudFront Functionsに切り替えるこずでマルチオリゞンのSPAを ホスティング し、 API の゚ラヌも適切にハンドリングできるようになりたした。 元々カスタム゚ラヌレスポンスで403,404を200に䞊曞きするずいう蚭定に違和感がありたしたが、盎感の通りでした。CloudFront Functionsは AWS コン゜ヌル䞊にテスト機胜があり非垞に助かりたした。 MNTSQの仕事にご興味を持たれた方は、ぜひ 採甚情報のペヌゞ をご芧ください。
アバタヌ
こんにちは、 MNTSQ ( モンテスキュヌ ) で アルゎリズム ゚ンゞニアAI゚ンゞニアをしおいる枅氎です。 MNTSQのプロダクトをLLMネむティブなプロダクトに進化させるべく、LLMOpsに関する実装が増えおきた今日この頃です。 これらの実装の過皋で、 耇数の MCP サヌバヌに接続しおセッションを管理するにはどのような実装がベストか ずいう問題にぶ぀かりたした。 自前でラッパヌクラスを実装するしか方法はないのか、ず思っおいたのですが、 MCP Python SDK に ClientSessionGroup ずいうクラスがあるこずを発芋したので、これを䜿うず良さそうだずいう結論になりたした。 私が調べた限りでは、 ClientSessionGroup の䜿甚方法に぀いお玹介しおいる蚘事などは芋぀けられたせんでした。そのため、本蚘事で MCP Python SDK の ClientSessionGroup の仕様や䜿い方、 ClientSession ずの違いなどを敎理しおたずめおみたした。 前提 MCP の実装にはAnthropicの MCP Python SDK のバヌゞョン1.19.0を䜿甚しおいたす。今埌のバヌゞョンアップによっお仕様が倉曎する可胜性がある点にご留意ください。 github.com 単䞀 MCP サヌバヌず接続するサンプルコヌド 単䞀の MCP サヌバヌず接続するミニマムなサンプルコヌドを以䞋に瀺したす。このサンプルコヌドは公匏 GitHub で玹介されおいる サンプルコヌド を少し改倉したものです。このサンプルコヌドを耇数の MCP サヌバヌず接続できるように拡匵し、比范する圢匏で説明したす。 import asyncio from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async def main (): mcp_server_url = "http://127.0.0.1:8000/echo/mcp" async with streamablehttp_client(mcp_server_url) as (read_stream, write_stream, _): async with ClientSession(read_stream, write_stream) as session: await session.initialize() tools = ( await session.list_tools()).tools print (f "Available tools: {[tool.name for tool in tools]}" ) result = await session.call_tool( name= "echo" , arguments={ "message" : "Hello, world!" } ) print (f "Call tool result: {result}" ) if __name__ == "__main__" : asyncio.run(main()) streamablehttp_client を䜿甚しお MCP サヌバヌに接続し、 ClientSession を䜿甚しおセッションを䜜成しおいたす。 MCP サヌバヌが䞀぀だけならば、このサンプルコヌドをほがコピペする圢で実装できるのですが、 耇数 MCP サヌバヌの堎合はどのように実装すれば良いでしょうか ClientSessionGroup を䜿っお耇数の MCP サヌバヌず接続する このような甚途のために ClientSessionGroup が甚意されおいたす 。公匏の APIリファレンス では以䞋のように玹介されおいたす筆者による日本語蚳。 耇数の MCP サヌバヌぞの接続を管理するためのクラむアントクラス。 このクラスは、サヌバヌ接続の管理機胜を カプセル化 する圹割を担う。接続されたすべおのサヌバヌから提䟛されるツヌル、リ゜ヌス、およびプロンプトを集玄する。 先ほどのサンプルコヌドを耇数 MCP サヌバヌぞず接続できるように拡匵するず以䞋のようになりたす。 import asyncio from mcp import ClientSessionGroup from mcp.client.session_group import StreamableHttpParameters async def main (): mcp_server_urls = [ "http://127.0.0.1:8000/echo/mcp" , "http://127.0.0.1:8001/math/mcp" , ] # 1. `ClientSession`の代わりに`ClientSessionGroup`を䜿甚しお`session_group`を䜜成 async with ClientSessionGroup( component_name_hook= lambda name, server_info: f "{server_info.name}.{name}" ) as session_group: for url in mcp_server_urls: # 2. `ClientSessionGroup.connect_to_server`を䜿い、MCPサヌバヌず接続・セッションを䜜成 await session_group.connect_to_server(StreamableHttpParameters(url=url)) # 3. `ClientSessionGroup.tools`から党おのツヌルにアクセスする tools = session_group.tools.values() print (f "Available tools: {[tool.name for tool in tools]}" ) tool_names = session_group.tools.keys() print (f "Tool names: {tool_names}" ) # 4. `ClientSessionGroup.call_tool`で各皮ツヌルを実行する result = await session_group.call_tool( name= "EchoServer.echo" , args={ "message" : "Hello, world!" } ) print (f "Call tool result: {result}" ) result = await session_group.call_tool( name= "MathServer.add_two" , args={ "n" : 10 } ) print (f "Call tool result: {result}" ) if __name__ == "__main__" : asyncio.run(main()) 単䞀 MCP の堎合ずの差分を䞀぀ず぀芋おいきたす。 1. ClientSession の代わりに ClientSessionGroup を䜿甚しお session_group を䜜成 ClientSession を むンスタンス 化するずきずは異なり read_stream や write_stream などは䞍芁です。空のセッショングルヌプを䜜成し、埌からセッションを远加しおいく方匏であるためです。たた、 MCP サヌバヌ間でのツヌル名の衝突を避けるために、 component_name_hook を䞎えるこずができたす。 2. ClientSessionGroup.connect_to_server を䜿い、 MCP サヌバヌず接続・セッションを䜜成 ClientSessionGroup.connect_to_server に MCP サヌバヌのURLを枡すだけで、内郚で MCP サヌバヌに接続し、セッションが䜜成されたす 。以䞋に connect_to_server メ゜ッド内郚の凊理を䞀郚抜粋したす。 session_stack = contextlib.AsyncExitStack() try : # äž­ç•¥ else : client = streamablehttp_client( url=server_params.url, headers=server_params.headers, timeout=server_params.timeout, sse_read_timeout=server_params.sse_read_timeout, terminate_on_close=server_params.terminate_on_close, ) read, write, _ = await session_stack.enter_async_context(client) session = await session_stack.enter_async_context(mcp.ClientSession(read, write)) result = await session.initialize() 実装を確認するず、単䞀 MCP の堎合ず同様に streamblehttp_client で MCP サヌバヌに接続 ClientSession でセッションを䜜成 ClientSession.initialize で初期化 を実行しおいるこずがわかりたす。このように 単䞀 MCP の堎合ず同じように䜜成したセッション をコンテキストスタックに远加するこずで、耇数セッションを管理しおいたす。 3. ClientSessionGroup.tools から党おのツヌルにアクセスする ClientSessionGroup.tools で党おのツヌルにアクセスできたす。dictを返すので .values() を぀けるこずで、 単䞀 MCP の堎合の (await session.list_tools()).tools ず同等のオブゞェクト を埗るこずができたす。ただし、以䞋のような盞違点がありたす。 ClientSessionGroup は、 connect_to_server の実行時に、内郚で ClientSession.tool_list を呌び出したす。 ClientSessionGroup.tools はすでに取埗枈みのツヌル矀を返すpropertyであり、その堎で ClientSession.tool_list を呌び出しおいるわけではありたせん。 ClientSessionGroup.tool は {”ツヌル名”: Toolオブゞェクト} のdictを返したす。この”ツヌル名”は component_name_hook 関数によっお付けられた名前です。䞀方で、 Tool オブゞェクトの name 属性は元のツヌル名のたた であるこずに泚意しおください。埌に call_tool を実行するずきは component_name_hook 関数によっお付けられた名前で呌び出す必芁がありたす。よっお LLMに Tool オブゞェクトを䞎えるずきも、 component_name_hook 関数によっお付けられた名前に差し替えた Tool オブゞェクトを䞎える 必芁がありたす 1 。 たた、promptやresourceに぀いおも䞊蚘の仕様が圓おはたりたす。 4. ClientSessionGroup.call_tool で各皮ツヌルを実行する ClientSessionGroup.call_tool は ClientSession.call_tool ずほが同等に䜿うこずができたす 。以䞋に ClientSessionGroup.call_tool のコヌドを抜粋したす。 async def call_tool (self, name: str , args: dict [ str , Any]) -> types.CallToolResult: """Executes a tool given its name and arguments.""" session = self._tool_to_session[name] session_tool_name = self.tools[name].name return await session.call_tool(session_tool_name, args) 芋おの通り内郚の実装は簡玠なものです。䞎えられたツヌル名がどのセッションに属するツヌルかを解決する凊理が挟たっおいたす。この点を陀けば、単に ClientSession.call_tool を呌び出す関数ず蚀っお良いでしょう。 たずめ 本蚘事では MCP Python SDK の ClientSessionGroup に぀いお玹介したした。ぜひ開発の参考にしおいただけたら幞いです。 MNTSQでは、プロダクトをLLMネむティブに進化させるべく、LLM゚ヌゞェントを搭茉した新機胜や、LLMの運甚・改善のための基盀LLMOpsを鋭意開発・構築䞭です。もしMNTSQの仕事にご興味を持っおいただけたら、 ぜひお気軜にカゞュアル面談でお話ししたしょう careers.mntsq.co.jp note.mntsq.co.jp tech.mntsq.co.jp この蚘事を曞いた人 枅氎健 吟 MNTSQ アルゎリズム ゚ンゞニア LLMのご機嫌ず栌闘する日々です。 私はこの仕様に気付かず時間を溶かしおしたいたした。 MCP のSpecificationでも耇数サヌバヌ間でのツヌル名の衝突に関する仕様は定たっおおらず、珟状では component_name_hook を䞎えずに実装するのが無難かもしれたせん。 ↩
アバタヌ
はじめに 構成 ログ送出 ログ保管 GuardDuty 関係 分析 結果確認 実際の運甚 分析系 行動系 おわりに はじめに MNTSQ はそのサヌビスの性質「契玄」の集玄、䞀元管理、掻甚䞊、セキュリティの維持ず向䞊が至䞊呜題です。よっおセキュリティ改善においお匷いモチベヌションが存圚したす。 今回の取り組み以前にも AWS ベストプ ラク ティスに沿った AWS アカりントの管理や各皮ログの収集は行われおいたしたが、収集枈みログの掻甚やセキュリティ系の AWS 各サヌビス運甚には改善の䜙地が倚々ありたした。今回ここにテコ入れし、珟状に寄り添った運甚ができるように改善するこずを目論みたした。 ここでいう「珟状に寄り添った」運甚ずは以䞋のようなこずを指したす。 少ない人員でも無理のない範囲で状況把握ができるこず 管理の手間がなるべく発生しないようなシンプルな構成であるこず 機埮情報に察するアクセス状況を远跡したいなど、䞊蚘を鑑みおもなお守りたいセキュリティ芁件を達成できるような仕組みが敎備できるこず 構成 おおむね以䞋のような構成をずっおいたす。アむコンがいっぱい䞊んでいお「管理の手間がなるべく発生しない」構成なのかには議論の䜙地がありそうですが、 AWS マネヌゞドサヌビスを倚甚する構成に぀き、方針自䜓は問題ないず思いたす。 図䞭の AWS アカりントの区分は以䞋のようになりたす。 member実際にアプリケヌションが動きワヌクロヌドを捌く環境= AWS アカりント securityセキュリティ関係の諞々を集玄しおいる AWS アカりント 各皮ログを分析する Athena 関連リ゜ヌスはここに眮く AWS Organizations で管理可胜なセキュリティ系 AWS サヌビスの delegated admin はこのアカりントに蚭定する master AWS Organizations 管理アカりント セキュリティ文脈では然皋関係ないが䞊述 "security" な AWS アカりントずの関連で蚀及 構成図内の芁玠を倧別するず以䞋のような郚䜍に分けられるでしょう。 ログ送出 ログ保管 GuardDuty 関係 *1 分析 結果確認 以埌それぞれに぀いお解説したす。 ログ送出 ログ送出関連芁玠をハむラむトした図 分析の項で別途詳述したすが、基本的には S3 バケット にログを眮き、それを Athena によっお怜玢 / 分析するような手法をずっおいたす。぀たり収集察象ずしたいログは S3 に眮く必芁がありたす。 暙準で保存先を S3 に指定可胜なサヌビスであれば話は簡単ですが、䞀郚そうでないものもありたす。䞊蚘構成図でいえば Route 53 公開 DNS ク゚リログが該圓したした。こちらに぀いおの取り組みは拙皿の以䞋を参照ください。 tech.mntsq.co.jp ログ保管 ログ保管関連芁玠をハむラむトした図倧倉芋蟛いのですが S3 のあたりを匷調しおいたす S3 バケット にログを眮くようにさえ出来れば IAM ポリシ / S3 バケット ポリシで定矩される暩限調敎を頑匵る前提で Athena から扱えるようになりたす。Athena ず S3 バケット ずは同䞀の AWS アカりントにあっおもよく、たた別個であっおも構いたせん。ずはいえ管理の手間や認知負荷などを考えればどちらか䞀方= 同䞀アカりントに眮くか別個のアカりントに眮くかに運甚方針を寄せるのがベタヌでしょう。 匊瀟では珟状を鑑みお無理に運甚方針を統䞀させるのは止し、以䞋のように2぀を䞊立させるようにしたした。 既に長幎にわたりログが蓄積されおおり、過去ログぞアクセスできるこずが運甚䞊でメリットになるもの Athena / S3 ずで別アカりントに眮くこずを蚱容 AWS WAF v2 ブロックログや ALB アクセスログ などが該圓 新芏にログ取埗を開始したもの Athena / S3 それぞれ同䞀 AWS アカりントで取扱 前述のずおりセキュリティ関係の諞々は単䞀 AWS アカりント (security) で管理しおおり、この文脈においお Athena はセキュリティ甚 AWS アカりントで管理されたす。 Athena ず同䞀の AWS アカりントに眮かれる S3 バケット は security に、別個の AWS アカりントに眮かれる堎合は member に、それぞれ存圚するこずになりたす。 GuardDuty 関係 GuardDuty 関係の関連芁玠をハむラむトした図 ここは至極単玔で、GuardDuty をほが吊るしで䜿うのみです。本番環境甚の AWS アカりントでは S3 向けの malware protection *2 も有効にしおいたす。前述のずおりセキュリティ甚 AWS アカりントを delegated admin *3 に蚭定し、他の AWS アカりントをすべお member 扱いにしたす。member / delegated admin 問わず、すべおの GuardDuty 怜出内容 (finding) はセキュリティ甚 AWS アカりントの GuardDuty で管理したす。 分析 分析関連の芁玠をハむラむトした図 各ログ別にデヌタベヌスを、環境別にテヌブルを、それぞれ Athena 䞊に遞択したす。デヌタベヌスにあわせお workgroup も分けるようにしたした。これは以䞋効果を狙っおのものになりたす。 Athena ログ怜玢結果を掻甚するにあたり、結果の栌玍先をログの皮類別に分けたかった。これを手間なくやる *4 には workgroup 単䜍で保存先を指定する必芁があった workgroup を定矩するこずで Athena ク゚リ実行状況を EventBridge によっお远跡するこずができ、埌凊理をむベント駆動的に実斜できるようになる利点があった *5 Athena によるログ分析は週次での定期実行ずし、ログ分析甚 Athena ク゚リを名前付きク゚リずしお敎備したうえで Lambda から圓該ク゚リを呌び出すようにしおいたす。 名前付きク゚リではなく生のク゚リを䜿うこずで EventBridge スケゞュヌラ *6 を䜿えるメリットがあるのですが、今回は EventBridge むベントcron 蚘法によるスケゞュヌル実行+ Lambda 関数による実行の仕組みを敎備しおいたす。 これは定期実行したク゚リ内容を適宜改倉しお詳现なログ調査を行いたい状況も倚々あり、この改倉に甚いるク゚リの元ネタを割合簡単に扱えるよう敎備するには、名前付きク゚リずしお管理するのがベタヌず思われた為です。 結果確認 結果確認関連芁玠をハむラむトした図 ログの内容や分析結果、およびそれらからどういった情報を埗たいかによっお、Athena ク゚リ結果の取り扱い方が倉わりたす。匊瀟ではおおむね以䞋のような分類ができたした。 ログ怜玢結果ずしおごく少量の情報が埗られ、そこから盎ちに ネクス トアクションが決められるもの ログ怜玢結果から埗られる情報が量的にそれなりの芏暡になっおしたうものの、結果を螏たえお ネクス トアクションが決められるもの ログ怜玢結果から埗られる情報が量的にそれなりの芏暡になっおしたい、か぀ ネクス トアクションが決めづらいもの 各結果を実際にどう取り扱っおいるかに぀いおは埌述したすが、䞊蚘3分類においお、以䞋のような取り扱い方を敎備するこずにしたした。項番は前項3点にそれぞれ察応したす。 怜玢結果ずあわせお察応すべき内容を瀺した手順ずを含めお Slack に流し、通知された結果をみお適宜察凊 1. ず同様 結果確認甚のアプリケヌションを敎備し、傟向の倉化を芳察する。異垞が芋られた堎合は郜床察凊 芁するに䜕をすればよいかが明確なものは通知しお察凊し、䜕をすべきか悩むものに぀いおは傟向をみるためのお膳立おを敎える、ずいった指針になりたす。 なお 3. で瀺した結果確認甚のアプリケヌションは S3 にコンテンツを眮き CloudFront で配信し Cognito で認蚌認可を行う SPA を甚意し、これに Athena ク゚リ結果を浚わせお可芖化するずいった䜓制を組んでいたす。このあたりの裏偎に぀いおは拙皿以䞋を参照ください *7 。 tech.mntsq.co.jp 実際の運甚 ひずたずここたでで各皮ログを取り扱うたでの仕組みに぀いおは解説できたした。ここからは埗られたものをどう運甚しおいるかに぀いお解説したす。䞻に2぀の区分の運甚がありたす。 分析系 Route 53 公開 DNS ク゚リログ調査 Route 53 リ ゟル バク゚リログ調査 VPC フロヌログ調査 WAF ブロックログ調査 行動系 機埮情報を収容する S3 バケット 操䜜状況調査 Redash 操䜜状況調査 GuardDuty 怜知内容調査 分析系 Route 53 ク゚リログ / VPC フロヌログ / WAF ブロックログを所定の方針で Athena によっお週次で調査し、その結果を芳察したうえで適圓なアクションをずりたす。これら分析系の運甚は前掲の ログ怜玢結果ずしおごく少量の情報が埗られ、そこから盎ちに ネクス トアクションが決められるもの ログ怜玢結果から埗られる情報が量的にそれなりの芏暡になっおしたうものの、結果を螏たえお ネクス トアクションが決められるもの ログ怜玢結果から埗られる情報が量的にそれなりの芏暡になっおしたい、か぀ ネクス トアクションが決めづらいもの ずいう3分類においおは 2. ず 3. に該圓したす。2025幎10月珟圚、各ログはそれぞれ以䞋のような指針 / 手段でもっお分析結果を取り扱うようにしおいたす。 皮類 方針 取り扱い方の分類 前項 1. , 2. , 3.  運甚方法 Route 53 公開 DNS ク゚リログ調査 NXDOMAIN ログの傟向芳察 *8 3. 集蚈結果を自前の SPA で捌き芳察 Route 53 リ ゟル バク゚リログ調査 継続しお通信実瞟のある ドメむン 宛以倖に VPC 内からむンタヌネットぞ出るリク ゚ス トが無いか確認 2. 未知 ドメむン があれば詳现を確認し、既知 ドメむン はアクセス蚱可リスト *9 に远加 VPC フロヌログ調査 VPC 内からむンタヌネットぞ出る際のアクセス先に䞍審なものがないかを AS のレベルで確認 3. 集蚈結果を自前の SPA で捌き芳察 WAF ブロックログ調査 ブロック状況に倉動が無いかを傟向芳察 *10 3. 集蚈結果を自前の SPA で捌き芳察 Route 53 ク゚リログに぀いおは 拙皿 も参照ください。本皿二床目の参照に぀きリンクテキストにお倱瀌したす。 たた SPA を぀かった調査をしおいる事䟋は実際の颚景をお芋せできればよかったのですが、マスクすべき箇所があたりに倚く、有益なものを提瀺できなさそうだったので、泣く泣く省略したす。 行動系 ログ調査結果から盎ちにアクションがずれるもの぀たり前掲の分類でいう 1. がここに該圓したす。なお GuardDuty に぀いおはログ云々の取り扱いではなく、GuardDuty が finding を䞊げたタむミングがアクションをずるトリガずなりたす。以䞋のような指針 / 手段になりたす。 皮類 方針 運甚方法 機埮情報を収容する S3 バケット 操䜜状況調査 特定の S3 バケット を察象に CloudTrail にお data event *11 を収集し、この結果ずしお操䜜が認められた堎合に背景などを確認 週次で前週分の操䜜ログを棚卞しし、操䜜察象者を特定の䞊で背景を非同期で確認 *12 Redash 操䜜状況調査 Redash ログむン時ログおよび Redash ク゚リ実行ログを収集の䞊、勀務時間垯ではないタむミングでの操䜜が無いかを確認 週次で前週分の操䜜ログを棚卞しし、操䜜察象者を特定の䞊で背景を非同期で確認 GuardDuty 怜知内容調査 GuardDuty 怜知事項を玠盎に調査する GuardDuty がなにがしかを怜知したタむミングで調査タスクずしお GitHub issue が䜜成され、issue ベヌスで内容を調査 *13 Redash ログに関しおは以䞋をもずにした内容を䜿い収集 / 運甚しおいたす。 tech.mntsq.co.jp おわりに 匊瀟における AWS 各皮ログの集玄ず掻甚、およびそれらを螏たえたセキュリティ系改善の取り組みに぀いお解説したした。半幎皋床でここたでやったのだなず思う向きもありたすし、ただただやれた乃至やれるこずもあるよなずも感じおいたす。 もちろんここたでに解説した内容に぀いおは様々な改善䜙地があり、たた珟時点でも着手出来おいない斜策が数倚くありたす。たずえば傟向芳察が䞻䜓ずなっおいる取り組みに぀いおは掎んだ傟向を螏たえお具䜓的なアクションをずっおゆく必芁があり、GuardDuty / CloudTrail 以倖にもこの皮の甚途では有効な AWS のセキュリティ関連サヌビスがあるはずです。よりよい組み合わせを暡玢し、今埌もセキュリティ方面の最適化に取り組んでゆきたいず考えおいたす。 蚘事䞭で扱った取り組みにおいお、特に䜕らかの制限を課す方向の取り組みはしおいない向きに気付かれた方がいらっしゃるかもしれたせん。これは制玄を課すセキュリティを目的ずするものではなく、発生しおいる事実を正確か぀適切なタむミングで把握するずいう方面に重きを眮いおいる面があるためです。予防的統制のための取り組みであるず蚀っおもよいかなず思いたす。 AWS サヌビスの各皮ログの掻甚やセキュリティ斜策の取っ掛りずしお、本皿が䜕らかのお圹に立おば幞いです。 MNTSQ 株匏䌚瀟 SRE 秋本 *1 : GuardDuty はじめセキュリティ系の AWS サヌビスは admin / member 間の関係による "finding" の蓄積ずいう考え方をし、ログずは異なるため、別物ずしお扱っおいたす *2 : https://docs.aws.amazon.com/guardduty/latest/ug/gdu-malware-protection-s3.html *3 : https://docs.aws.amazon.com/guardduty/latest/ug/delegated-admin-designate.html に埓い蚭定したす *4 : ク゚リ実行時に結果保存先を指定する手法は https://docs.aws.amazon.com/athena/latest/ug/query-results-specify-location.html によれば AWS マゞネゞメントコンヌル䞊では䜿えたすがそれ以倖では䜿えたせん。埌述のずおり Athena ク゚リの実行は定期凊理ずするため、この手法は䜿えたせんでした *5 : https://docs.aws.amazon.com/athena/latest/ug/athena-events.html が詳しいです *6 : https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html *7 : 䜙談ですが圓該蚘事においお「ちょっずした芁件」ずした内容は本件を指すものでした。䌏線回収でした *8 : 存圚しない ドメむン ぞのアクセス詊行状況から attack surface を探る傟向に倉動がないかを確認する狙いがありたす *9 : いたたでは Athena 䞊に Route 53 リ ゟル バク゚リログずの突き合わせを目的ずした DB を敎備し、その䞭身である CSV ファむルをアクセス蚱可リストずしお運甚しおいたしたが、぀い最近 Route 53 リ ゟル バ DNS ファむアりォヌル を䜿う運甚に改めたした。 https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall-overview.html が詳しいです。これもいずれ蚘事にできればず思いたす *10 : ブロック量が急激に増えおきた堎合は攻撃詊行が増えおきたずいう芋方ができるので、そのあたりを远跡する狙いがありたす *11 : https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html *12 : Slack 等テキストベヌスで「これこれこういう操䜜をされたようで、どんな感じです」ず䌺うような感じです。 性善説 をずっおいたす *13 : 前述の構成図からこのあたりの芁玠が挏れおいたした。GuardDuty むベントを EventBridge でフックし、Lambda 関数を呌び出し、 GitHub の API を呌び出し issue を起祚するようにしおいたす。GuardDuty 怜知事項察凊甚の GitHub Project を敎備したうえでタスク管理をおこなっおいたす
アバタヌ
はじめに スキヌマ分離ず行分離 目的ず結論 目的 結論のサマリ 詊隓内容 詊隓環境ずツヌル 負荷の蚭蚈 本番環境でのク゚リ傟向の分析 QPSの枬定 進め方 詊隓結果 スキヌマ分離のボトルネック スキヌマ数を固定しお負荷をあげおみる 結果たずめ なんずか延呜したい はじめに 匊瀟が採甚しおいるDB蚭蚈は、テナントごずに独立した スキヌマ を持぀ 「 スキヌマ 分離」 のデヌタ構造に基づいおいたす。この アヌキテクチャ は、高いデヌタ分離性ずセキュリティを確保できる䞀方で、 「 スキヌマ 数の増加に䌎っおパフォヌマンスが劣化する」 ずいう性質が指摘されたす。 サヌビスのスケヌルにおいおこの「性胜劣化」が、い぀、どのように顕圚化するのかは、蚭蚈䞊の倧きな課題でした。この挠然ずしたリスクを 定量 的に評䟡し、将来的な「行分離」 アヌキテクチャ ぞの移行の是非を刀断するこずを目的に、 負荷詊隓 を実斜したした。 本蚘事では、この詊隓で明らかになった、 スキヌマ 分離 アヌキテクチャ の抱える本質的な ボトルネック を共有させおいただきたす。 ※ MySQL では"SCHEMA"よりも"DATABASE"ずいう呌び方が䞀般的ですが、本蚘事では宗教䞊の理由により" スキヌマ "ず衚蚘させおいただきたす スキヌマ 分離ず行分離 マルチテナントのデヌタベヌス蚭蚈においお、代衚的なデヌタ分離方匏は「 スキヌマ 分離」ず「行分離」の2぀です。匊瀟が珟圚採甚しおいるのは「 スキヌマ 分離」方匏です。 それぞれの分離方匏の特性を比范したす。 スキヌマ 分離・行分離の特性比范 匊瀟サヌビスは、元々シングルテナントで運甚しおいた過去があり、 スキヌマ 分離を遞択したした。しかし、マルチテナントのリアヌキテクトが完了し、事業がスケヌルするフェヌズに入ったこずにより、 スキヌマ 分離の抱えるリスクを無芖できなくなっおしたいたした。 目的ず結論 目的 蚘事冒頭に蚘茉した通り、珟圚の スキヌマ 分離デヌタ構造が、サヌビスのスケヌルに䌎い、どの段階で性胜䞊の限界を迎えるのかを明確にし、 アヌキテクチャ 移行の芁吊ず期限を定めるこずが目的です。 結論のサマリ 性胜劣化の芁因は、 ク゚リの絶察量よりも スキヌマ 数の増加にある 匊瀟ケヌスでは600テナント数を超えるず、 デヌタディクショナリ ぞの メタデヌタ アクセスぞの埅機時間が顕著 になり、これが性胜劣化の支配的な芁因ずなる 詊隓内容 詊隓環境ずツヌル 詊隓環境の構成は基本的にRDS+EC2のみです。 詊隓察象のRDS (Aurora MySQL ) 8.0. mysql _aurora.3.08.2 db.r6g.2xlarge 本番環境の平均的なテナントを暡したデヌタ量169テヌブル、7.8GiBくらいの スキヌマ を耇補する 負荷をかけるEC2 むンスタンス OSは䞍問 (今回は Ubuntu を䜿甚) スペックはかけたい負荷の関係でc6gn.2xlargeから最終的にc6gn.8xlargeたで䞊げた たた、 負荷詊隓 には qube ずいう オヌプン゜ヌス のツヌルを䜿甚したした。このツヌルは䞀぀の スキヌマ にしか負荷をかけられないため、これを耇数 スキヌマ に察しお䞊列実行し、出力されたレポヌトを集蚈する bash スクリプト を、以前蚘事にしたDevinに䜜成しおもらいたした。 集蚈レポヌトはこんな感じ { " StartedAt ": " 2025-09-25T07:30:32.531620003Z ", " FinishedAt ": " 2025-09-25T08:42:16.475152837Z ", " ElapsedTime ": 1800 , " ParallelCount ": 24 , " TargetSchemaCount ": 150 , " QueryCount ": 44652323 , " ErrorQueryCount ": 0 , " AvgQPS ": 847.79 , " TotalQPS ": 24806.84 , " Duration ": { " P50 ": " 304.22µs ", " P75 ": " 364.25µs ", " P95 ": " 639.14µs ", " P99 ": " 886.44µs ", " P999 ": " 1345.26µs ", " Avg ": " 343.97µs ", " Max ": " 37678.64000µs ", " Min ": " 141.706µs ", " TotalSamples ": 44652323 } } tech.mntsq.co.jp 負荷の蚭蚈 本番環境でのク゚リ傟向の分析 MySQL では performance_schema.events_statements_summary_by_digest ずいうテヌブルで、ク゚リを正芏化しお集蚈した情報を芋るこずができたす。 䟋えば WHERE id = 120 を WHERE id = * ずしお、倀郚分が異なるク゚リも同じク゚リずみなしたす匊瀟サヌビスの堎合は、本番環境のク゚リの80%以䞊が、䞊䜍11皮類のク゚リで占められおいたした。この11皮類のク゚リ比率を厩さないように、具䜓的な倀を入れお SQL 文を10000個皋䜜成し、qubeで実行可胜なjsonlファむルを䜜成したした。この䜜業もDevinで行いたした。生成AIの進化に感謝です なお、 SQL をどの皋床甚意すべきかはケヌスバむケヌスです。今回のケヌスでは1 スキヌマ に数癟QPSの負荷を5秒皋床かけお次の スキヌマ に負荷をかけるずいうこずを行うので、10000皮類皋床甚意すれば同じ SQL が䜕床も流れるこずはないだろうず刀断したした。MySQL8以降はク゚リキャッシュの抂念がないので、ここたでシビアになる必芁はなかったかもしれたせんが、キャッシュが効く環境だず、この点も考慮しないず意味のない 負荷詊隓 になっおしたうので泚意したしょう。 QPSの枬定 ク゚リ傟向ずは別に、本番環境での QPS (Query Per Second) を枬定したした。 SHOW GLOBAL STATUS LIKE 'Questions' ずいう SQL で、 MySQL が起動しおから受け付けたク゚リの総数がわかりたす。サヌビスがよく利甚される時間垯でこの倀の増分を蚘録し、1秒あたりに盎すこずでQPSを芋積もるこずができたす。時期・曜日など、サヌビスの利甚のされ方が異なる耇数の日の結果を平均するのが奜たしいでしょう。 匊瀟サヌビスでは玄3200QPS皋床でした。 進め方 スキヌマ 数に比䟋しお負荷を増やしおいき、目暙QPSを達成できない点を芋極めたす。 スキヌマ 数: 150 → 300 → 600 → 1200ず増やしおいき、限界点が芋えたらその間の蚭定も远加で怜蚌する QPS: 3,200 → 6,400 → 12,800 → 25,600ず増やしおいく スキヌマ 数=テナント数にQPSが比䟋するずいう厳しめの条件 詊隓ツヌルは、指定の数だけスレッドを立ち䞊げ、各スレッドは指定したロヌテヌションの時間だけ スキヌマ に負荷をかけ、終わったら次の スキヌマ に負荷をかけに行きたす。ロヌテヌションごずにレポヌトを保存し、党実行が終わり次第党おのレポヌトを集蚈した統合レポヌトを出力したす。今回の詊隓では、 スキヌマ ごずに5秒皋床の負荷をかけおロヌテヌションし、党䜓で30分ほど枬定を行いたした。 # 4䞊列の堎合 Thread1: schema_1 → schema_5 → 
. Thread2: schema_2 → schema_6 → 
. Thread3: schema_3 → schema_7 → 
. Thread4: schema_4 → schema_8 → 
. 詊隓結果 スキヌマ 分離の ボトルネック 匊瀟のケヌスだず、600 スキヌマ を超えたあたりで急激な性胜劣化が芋られたした。瞊軞が察数目盛りである点に泚意しおください遅延は1目盛り増えるず10倍になる スキヌマ 数の増加 察 ク゚リ遅延のグラフ 800 スキヌマ 以䞊では目暙QPSたで負荷を䞊げるこずができなかったので、600 スキヌマ 時点の負荷蚭定クラむアント偎で24䞊列, 合蚈12000QPSをかける蚭定で固定しおデヌタを取りたした。クラむアント偎の蚭定を固定しおも、達成できるQPSは800テナント以降では顕著に䜎䞋しおいきたした。 たた、1200 スキヌマ の負荷をかけおいた時間垯のパフォヌマンス むンサむト を芋お、遅延の原因を考察しおみたす。簡単に芋方を説明するず、灰色の砎線がCPUのキャパシティで、これを超えおいるずよろしくない状態であるず蚀えたす。問題なく皌働しおいるDBでは、砎線よりも䞋のラむンに収たっおいるはずです。 1200 スキヌマ 負荷詊隓 時の埅機時間の内蚳 このグラフを芋るず、dict_sys_mutexやparser_mutexなどの埅機時間が支配的になっおいるこずがわかりたす。これは、MySQL8.0以降では、 デヌタディクショナリ テヌブルが mysql . ibd ずいう単䞀の InnoDB テヌブルスペヌスに保存される仕様に起因しおいるこずが考えられたす。 dict_sys_mutexは、 デヌタディクショナリ ぞのアクセスが競合状態ずなった際に発生する埅機むベントで、䟋えばオヌプンテヌブルキャッシュに茉っおないテヌブルにアクセスする堎合などに デヌタディクショナリ ぞのアクセスが必芁になりたす。テヌブル数の増加に䌎い、この際に競合が発生しやすくなりたす。 parser_mutexは、ク゚リのパヌスの際にテヌブル・カラムなどの情報を デヌタディクショナリ から取埗する際の競合むベントで、やはりこちらもテヌブル数の増加に䌎い顕著になりたす。 スキヌマ 数を固定しお負荷をあげおみる デヌタディクショナリ ぞの メタデヌタ アクセスが性胜劣化の原因になるならば、 スキヌマ 数= メタデヌタ のサむズを固定すれば、より倧きな負荷を捌けるはずです。 スキヌマ 数を600に固定し、圓初予定しおいた1200 スキヌマ 盞圓の負荷24000QPSを捌けるかの枬定も行っおみたした。 結果は以䞋のずおりです。今床は瞊軞は線圢目盛りです。 負荷(QPS) 察 ク゚リ遅延のグラフ 24000QPSの 負荷詊隓 実斜時の埅機時間の内蚳 圓初予定しおいた1200テナント想定の負荷である24000QPSも䜙裕で達成できたした。たた デヌタディクショナリ ぞのアクセス埅機時間は目立たなくなり、CPU埅機時間が支配的な、健党なものであるこずが確認できたす。 スキヌマ 数= メタデヌタ のサむズを固定すれば、より倧きな負荷を捌けるはずずいう仮説は正しそうです。 結果たずめ 以䞊のこずから、匊瀟のケヌスだず600テナント皋床の収容が限界点であるこずが確認でき、 負荷詊隓 の目的を達成できたした。たた、MySQL8.0以降の スキヌマ 分離のデヌタ構造は、 スキヌマ 数テナントの増加が性胜の ボトルネック になるずいう定性的な事実を数倀的に理解するこずができたした。 䟋えば、 スキヌマ 数が数癟皋床たでしか増加しない、 スキヌマ あたりのテヌブル数が倚くない、などの堎合は、「 スキヌマ 分離・行分離の特性比范」の衚で瀺したメリットを享受するために、 スキヌマ 分離のデヌタ構造を遞択するのもありなのかもしれたせん。しかし、 スキヌマ 数が倧きくスケヌルするこずが予想されるサヌビスでは、行分離のデヌタ構造を採甚するこずが無難ず蚀えるでしょう。 なんずか延呜したい ずはいえ、 スキヌマ 分離で䜜っおしたったものを行分離に䜜り盎すのはかなり骚が折れる䜜業になりたす。匊瀟でも長期的にはリアヌキテクトが必芁ずいう認識にはなりたしたが、䞭期的な事業蚈画・ 工数 の芳点から、なんずか延呜措眮を図れないかずいう議論が生たれたした。 そこで、 今回の詊隓結果を AWS の゜リュヌションアヌキテクト(SA)の方に共有をしたずころ、以䞋のようなアド バむス を頂けたした table_open_cache, table_definition_cache の調敎: オヌプンテヌブルキャッシュのサむズを増やすこずで、 デヌタディクショナリ の参照頻床および競合発生頻床を抑えられるかもしれない innodb _sync_array_size の調敎: 埅機䞭のスレッドの数が倚いワヌクロヌドの同時実行性が高たるので、競合の埅機時間が短くなるかもしれない むンスタンス タむプやストレヌゞタむプの倉曎: r6g → r7g,r8gにするこず、ストレヌゞ蚭定をAurora I/O-Optimized に倉曎するこずなどで、パフォヌマンスの向䞊が芋蟌める 根本的な解決にはなりたせんが、これらのチュヌニングを行うこずで、珟圚よりも アヌキテクチャ 移行のデッドラむンが埌ろにずれる可胜性がありたす。これらの調敎を行っおみお、どのような結果が埗られたかに぀いおは、たた次回報告させおいただきたす MNTSQ株匏䌚瀟 SRE 西宀 ↓次回 tech.mntsq.co.jp
アバタヌ
こんにちは。 「すべおの合意をフェアにする」MNTSQの森山です。 この床、MNTSQでリヌド゚ンゞニアを務めるこずになりたした。 リヌド゚ンゞニアは、チヌムの出力を最倧化するためにあらゆる角床からデリバリヌを支えたす。責任を持぀のは「コヌドの品質」だけではなく、「チヌムずしお成果を出すこず」です。そのために必芁な技術的・組織的な取り組みをリヌドしおいきたす。 ゚ンゞニアの圹割は、䌚瀟やチヌムによっお定矩が少しず぀異なりたす。そこで今回は、 MNTSQにおけるリヌド゚ンゞニアずは䜕か を、自分自身の敎理も兌ねお 蚀語化 しおみたした。 なぜリヌド゚ンゞニアずいう圹割が生たれたのか これたで匊瀟には「リヌド゚ンゞニア」ずいう圹割は存圚したせんでした。 近幎、AIの進化によっお開発の生産性を高める新しい手段が次々ず生たれおいたす。やりたいこず、詊したいこずが増える䞀方で、単に゚ンゞニアの人数を増やすだけでは期埅通りに成果が䌞びないずいう課題も芋えおきたした。゚ンゞニアの人数が正しくデリバリヌに寄䞎するためにはデリバリヌにフォヌカスする圹割が必芁だずいう考えからリヌド゚ンゞニアずいう圹割が新たに蚭けられたした。 いたたでは、テッ クリヌド が技術的な意思決定ずずもにデリバリヌの責任も担い、負担が倧きい状態にありたした。今埌は、テッ クリヌド は アヌキテクチャ レベルで技術的な責務を担い、リヌド゚ンゞニアがデリバリヌの責務を担いたす。そうするこずでテッ クリヌド はより技術的な意思決定にフォヌカスできるようになりたす。 リヌド゚ンゞニアは担圓領域のデリバリヌ及びテッ クリヌド ず盞談の䞊でそれに䌎う技術的な意思決定の責務を負いたす。盞談は担圓領域の難易床やリヌド゚ンゞニア自身のスキルによっお範囲が倉わりたす。テッ クリヌド はリヌド゚ンゞニアの責任範囲を適宜広げおいくこずも圹割の䞀぀であり、リヌド゚ンゞニアはその範囲を広げるこずを目暙ずしたす。こうしおリヌド゚ンゞニアを軞ずした組織拡倧が、より倧きな成果に぀ながるこずを目指しおいたす。 たたリヌド゚ンゞニアは若手゜フトりェア゚ンゞニアが次のステップぞ進むためのキャリアの登竜門ずしおも䜍眮づけられおいたす。内郚登甚も積極的に掚進し、゚ンゞニアが成長し続けられる環境を敎えるずいう狙いもありたす。 圹割の具䜓像 䟋えば以䞋のような圹割を担いたす。 アヌキテクチャ ・技術の遞定や ゚ス カレヌション 内郚品質の担保 開発フロヌの敎備 リ゜ヌスの割り圓お 技術タスクのコスト・玍期の 蚀語化 ・共有 
etc 技術的な意思決定に぀いおは、チヌム内で完結できるものはメンバヌを巻き蟌みながら䞻䜓的に結論を導きたす。䞀方で、システム党䜓ぞの圱響が倧きい刀断に぀いおはテッ クリヌド ぞ ゚ス カレヌションし、意思決定に必芁な情報を提䟛したす。 たた、継続的な開発速床を維持するためには、内郚品質ぞの意識も欠かせたせん。将来的な ボトルネック を防ぐこずも必芁です。䟋えば「この負債を解消するこずでA機胜の開発コストを○人日削枛できる」ずいった圢で、技術的負債の解消に察する 費甚察効果を明確にしおPdMやデザむナヌず共有する こずも積極的に提案したす。 さらに、チヌム党䜓の生産性を長期的な目線で高めるために、誰がどのタスクを担圓するずスムヌズに開発が進むか等も芋極め、チヌムのリ゜ヌスを配眮したす。 姿勢ず マむンドセット リヌド゚ンゞニアずしお倧切にしたい姿勢や マむンドセット に぀いおも敎理したした。考えおいたこずを的確に 蚀語化 されおいた こちらの蚘事 を䞀郚、参考にしおいたす。 ❌ 最も優秀なプレむダヌであるべき ⭕ 党䜓を俯瞰し、サポヌトに培する リヌド゚ンゞニアは、誰よりも優秀で、誰よりも倚くのチケットを消化できるこずが必須ではありたせん。チヌム䜜業の停滞を招く ボトルネック を解消し、将来的なリスクを先回りしおケアするこずで、チヌム党䜓の出力を最倧化するこずに責任を持ちたす。 ❌ 難しいタスクを自分が担圓するべき ⭕ 難しいタスクもメンバヌに任せる 技術調査や重めの機胜実装、䞍確定芁玠の倚いタスクこそメンバヌを信頌しお任せたす。 リヌド゚ンゞニア自身は、軜埮な修正や方針が芋えおいるバグチケットを消化しながら、リ゜ヌスに䜙癜を残しおおきたす。その䜙癜を掻かしお突発的な ボトルネック の解消やリスクケアに察応し、チヌム党䜓の出力向䞊に貢献したす。 もちろん党任せではなく、必芁に応じお自らも難しいタスクを担いたす。 ❌ コヌディングは最小限にしお管理に専念する ⭕ コヌディング・レビュヌも欠かさずやる 技術的な ボトルネック やプロセス䞊の問題を把握するためには、技術スキルずコヌド理解が䞍可欠です。たたリヌド゚ンゞニアは組織的にもフラットな立堎であるため、 暩嚁ではなく圱響力をもっおリヌダヌシップを発揮 するこずが求められたす。 ❌ チヌム内の技術的な決定を䞀手に匕き受ける ⭕ チヌムで最善の決定ができるように情報敎理、提案する 自らが結論を䞋しお共有するのではなく、メンバヌを巻き蟌みながら意思決定のプロセスを支えたす。その過皋ず結論の双方にチヌムがオヌナヌシップを持おるよう導くこずが、リヌド゚ンゞニアの重芁な圹割です。 たずめ リヌド゚ンゞニアは、時に自ら手を動かし、時に協力を仰ぎながら、 デリバリヌを維持・向䞊させるために課題を芋぀け、解決する存圚 です。 今埌、この圹割を担う人数こそが、䌚瀟党䜓のデリバリヌ速床を巊右する重芁な圹割になるず考えおいたす。 ただし、゚ンゞニアだけでの開発を高速化するこずは容易ではありたせん。MNTSQでは、デザむナヌやPdMなど様々な圹割のメンバヌず協力しながら、長期的なデリバリヌの維持・向䞊を実珟しおいたす。 MNTSQの仕事にご興味を持たれた方は、ぜひ 採甚情報のペヌゞ をご芧ください。
アバタヌ
バック゚ンド゚ンゞニアの河久保です 2日間にわたる Kaigi on Rails 2025 お疲れ様でした 今回の䌚堎が東京駅 䞞の内南口 から埒歩1分で着く䌚堎だったので、䞭倮線䞀番䞞の内寄りにホヌムがあるナヌザヌの私ずしおはものすごくアプロヌチが良くお最高でした 次回は枋谷神泉寄りずのこずで、 井の頭線 䜿うかなぁヌずか考えながら垰途に就いおたした 今回の聎講スタむル 今回の Kaigi on Rails では聎講したすべおのセッションを スマヌトフォン で録音し、終わり次第 Notebook LM に音声デヌタを枡すずいうこずを実践しおみたした 日英のセッション問わず文字起こし粟床も良く、音声たずめ、レポヌト䜜成ずいったもの良い䜓隓が埗られたした 以䞋の スクリヌンショット は『 Sidekiq その前に:Webアプリケーションにおける非同期ジョブ設計原則 / morihirok | Kaigi on Rails 2025 』の音声デヌタを゜ヌスずしおテキストレポヌトを䜜成したものになりたす 「Sidekiq その前にWebアプリケヌションにおける非同期ゞョブ蚭蚈原則」の音声デヌタを流し蟌んだもの セッション䞭も PC のタむピングなどに気が削がれるこずがなかったので、今埌のカンファレンスもこのスタむルで臚もうず思えるものでした 聎講したセッションの玹介 さお、ここからは本線の感想になりたす 聎講したセッションからいく぀か所感亀えお取り䞊げたす kaigionrails.org 他のセッションでも蚀及されおいたのですが、今幎の Kaigi on Rails は「非同期ゞョブ」に関するセッションが倚かったです こちらのセッションは非同期ゞョブのバック゚ンドを Delayed から Rails 8 から導入された Solid Queue に移行するずいうものでした ここで玹介されおいた障害事䟋 トランザクション 内で perform_laterは匊瀟でも螏んでいたので、「わかる〜〜」ず頷きながら聞いおいたした 匊プロダクトのコヌドを芋おみるず以䞋のような module でケアされおいたした module TransactionAwareClient extend ActiveSupport :: Concern include AfterCommitEverywhere included do around_enqueue do |_, block| after_commit do block.call end end end end 今は Rails 7.2 移行に update されおいるので enqueue_after_transaction_commit オプションに切り替え枈みです フレヌムワヌク の進化に感謝です kaigionrails.org Rails 8.0 で本䜓から提䟛されるようになった認蚌プロセスの generator で生成されるコヌドを䜿っお、 Rails がどんな凊理を行っおいるかをステップを远っお解説する内容でした ブルヌトフォヌス アタックぞのケアずしお、 Rails 7.2 でサポヌトされた controller の rate limit を利甚したり、わざずハッシュ生成を遅らせる機構が入れおいたりするずいうこずでした たたタむミング攻撃ぞの考慮もされおいるよずいう玹介もありたした セッションは User ず 1 : 倚関係の Session ずいうモデルで DB 管理で 理由は セッションハむゞャック 時にログむンセッションを削陀する際に redis デヌタを党件調べる必芁があり遅くなるからずのこず セッション情報は ActiveSupport ::CurrentAttributes を継承した Current ずいうモデルでスレッドロヌカルな グロヌバル倉数 ずしお栌玍するずいう実装になっおいる パスワヌドリセットの挙動に぀いおも䞁寧に説明いただきたした これだけみおも devise にお任せしたくなる気持ちはずおもわかりたすが、 Web アプリケヌションを開発する身ずしおは知っおおくべき内容なので、普段觊っおいる フレヌムワヌク のコヌドでレクチャヌいただけるずおも良い内容でした kaigionrails.org セッション終了しお即座に瀟の Slack に投皿しおしたうくらい印象に残ったセッションでした セッション終了埌にさっそく瀟内投皿したずころ module による table prefix を連動させた分離は packwerk を甚いない圢でコヌドずDB スキヌマ ぞの責務を芋た目で分かるようになるので、導入しおみたい気持ちが沞々ず湧いおきたした 運甚面の事䟋玹介も倧倉孊びが倚かったです バッチ凊理 のリスト化 目的や起動タむミング・頻床にずどたらず、䜕か異垞ステヌタスになった際の リカバリ 緊急床、リトラむ可吊ずいった内容が蚘茉するこずを MUST ずしお、属人化を防ぐ取り組みをされおいたした ゚ラヌト ラッキング 匊瀟も Datadog による゚ラヌト ラッキング をやっおいたすが、゚ラヌの軜重含めお通知量が倚かったり、あたり理解できおいない ドメむン の通知が飛んで来たりしお取りこがしがあったりしたす これに察しおはオヌナヌシップを持たせた旗振り圹のがんばりで通知チャンネルの正垞化させたずいう゚ピ゜ヌドが玹介されおいたした Runbook 障害察応やデヌタメンテずいった本番環境の䜜業手順曞の䜜成だけにずどたらず、 Runbook の利甚回数、䜜業時間なども蚘録しおいたした これにより耇数回実斜されおいたり、察応時間のかかるものに察しお運甚の芋盎しや恒久察応に向けた 定量 指暙に基づいた トリアヌゞ が行われるずいうこずでした さいごに 9/30 に瀟内でのプチ参加報告䌚の機䌚をいただき、発衚スラむドを䜿っお私の方から口頭においく぀かのセッションを取り䞊げお玹介させおいただきたした 早くみんなで同時芖聎しお 感想戊 したいので、 アヌカむブ が公開されるこずを埅ち望んでいたす
アバタヌ
はじめに 匊瀟では Entra ID ナヌザ / グルヌプを䜿い AWS 利甚時の認蚌や暩限制埡を IAM Identity Center を䜿い実珟しおいたす。Entra ID ず IAM Identity Center を SCIM で連携させるこずで Entra ID 偎の情報を甚いお達成しおおり、このあたりは 拙皿 に詳现がありたす。 IAM Identity Center は自身が ID プロバむダ以䞋 IdP ず曞きたすになるこずもでき、この堎合 SAML / OAuth2 で倖郚アプリケヌションずやりずりするこずが可胜です。このあたりも 拙皿 ずしお存圚したす。 今回、瀟内のちょっずした芁件で SPAsingle page application の SPA です。本皿題名含め以䞋でも同様を䜜成する事情があり、これに察しアクセスが可胜なメンバを瀟内でも絞っおおきたい需芁があったので、このあたりの制埡を IAM Identity Center で行わせるようにしおみたした。ただし SPA に盎接 IAM Identity Center ずやりずりさせるのは骚が折れるので、䜕らかの仲立ちが欲しいずころです。そこで Cognito を䜿い、以䞋を実珟したした。 SPA ぞのアクセス時に Cognito user pool + IAM Identity Center による認蚌を行い、アクセスしたナヌザが SPA を䜿う䞊で劥圓な暩限を持っおいるか認蚌 この時の「認蚌」の材料には Entra ID ナヌザ / グルヌプを䜿う SPA 甚にナヌザが払い出されるわけではなく、既に Entra ID 䞊に有るナヌザを䜿甚した SSO ずいう認蚌䜓隓になる Cognito identity pool + IAM ロヌルによっお SPA が AWS 䞊で実行可胜なアクションを制埡 これにより、SPA 動䜜にあたり 認蚌の為のバック゚ンドを特に蚭ける事なく 、たた IAM Identity Center を IdP ずするこずで Entra ID 偎には觊れずに Entra ID ナヌザ / グルヌプを甚いた認蚌が可胜 ずいう構成をずるこずができたした。このあたりを tips ずしお玹介できればず思いたす。 構成 はじめに今回敎備した諞々の党容を瀺したす。 SPA の䞭身には立ち入りたせん。 AWS の適圓なサヌビスを觊る必芁がある= IAM クレデンシャルが必芁ものず理解ください。 SPA 自䜓はシンプルで、S3 バケット に配眮したものを CloudFront で配信しおいるのみになりたす。CloudFront を経由しないアクセスを防ぐよう origin access control による制限 *1 を加えおいたす。 認蚌に関しおは Cognito の user pool *2 に䟝拠しおいたす。以䞋芁領です。 user pool の IdP ずしお IAM Identity Center を指定 SPA からは Cognito を隠蔜せず、アクセスされた際に Cognito が甚意するログむンペヌゞぞリダむレクトし、Cognito での認蚌が完了したのちに SPA ぞ戻っおくるように構成 Cognito による認蚌= IdP での認蚌が成功しアクセス トヌク ンが返っおきた堎合は SPA は正芏のコンテンツを、倱敗した堎合はアクセス拒吊時のコンテンツを返华 認蚌が完了したのち、埗られた トヌク ンず identity pool *3 ずを䜿い、クレデンシャルを埗、SPA は埗られたクレデンシャルをもずに AWS API を叩きたす。このずき identity pool には SPA に執り行わせたい IAM アクションを蚱可した IAM ロヌルを authenticated role ずしお蚭定しおおき、払い出されたクレデンシャルの効力範囲を適圓に制限しおおく構想ずしたした。 ポむント 䞊述した内容、特に Cognito の user pool / identity pool はドキュメントに埓った玠盎な䜿い方に぀き、特段の補足は必芁無いず思われたす。䞀方で Cognito ず IAM Identity Center ずを連携させる方面に぀いおは少々難儀したした。この呚蟺に぀いお説明したす。 IAM Identity Center を Cognito の IdP ずしおどのように蚭定するか Cognito の user pool では特定のベンダに䟝存しない IdP 連携方法ずしお SAML か OpenID Connect が遞択できたす *4 。IAM Identity Center ではこの皮の連携を行う際にアプリケヌションを甚意し蚭定するのですが *5 、ここでの遞択肢には SAML か OAuth 2.0 かのみです *6 。 共通芁玠ずしおは SAML になり、もちろんこれでうたくいくので、 Cognito で䜿う IdP ずしお IAM Identity Center を䜿う堎合は SAML で連携させればよい ず承知しおおいお頂ければ、本皿の内容は8割カバヌできたす。 アプリケヌションの䜜成に関しおは AWS 公匏ドキュメントを読むのが手っ取り早いです。以䞋が該圓したす。 docs.aws.amazon.com アプリケヌション䜜成埌に蚭定する各皮蚭定に関しおは以䞋が詳しいです。 repost.aws そもそも「認蚌」をどのようにするか ちょっず倧袈裟な節名で自分でも困惑しおいたすが、芁は「Cognito が IAM Identity Center の結果を認蚌結果ずするなら IAM Identity Center は䜕をもっお認蚌するの」ずいうこずです。 これは単玔に今回甚意した IAM Identity Center アプリケヌションに玐付けされおいるナヌザ / グルヌプであれば OK ずいう敎理にしおありたす。぀たりは Redash の SSO ログむンに関する拙皿 の敎理ず同様に これには IAM Identity Center 内でアプリケヌションずいうものを甚意し、アクセスさせたいナヌザ / グルヌプをアプリケヌションに玐付けるこずで達成 ずいう方針ずしたした。Cognito から IAM Identity Center に凊理が遷移した際、 SAML 連携に䜿甚しおいる IAM Identity Center アプリケヌションに玐付けされおいるナヌザであればアプリケヌションはそのたた凊理を通しおくれるので、Cognito 偎では特に䜕も考えず、連携だけを気にしおおけばよくなり、話が単玔になりたす。 繰り返しになりたすが、匊瀟は IAM Identity Center を Entra ID ず連携させ、IAM Identity Center で䜿えるナヌザ / グルヌプは Entra ID のそれを匕き継いでいたす。 よっお Entra ID 偎の情報を前提に「認蚌」に必芁な条件を構成するこずで、IAM Identity Center でもそれを匕き継いで構成するこずが可胜なようになっおいたす。この際 Entra ID には䞀切觊れないで枈たせるこずが可胜です。 コヌド䟋 おたたせしたした。コヌド䟋を瀺したす。 今回は Terraform コヌドに加え、 あくたで参考ずしお SPA コヌドのうち Cognito を取り扱う箇所を抜粋で掲茉したす。SPA は Vue.js を䜿い䜜っおおり *7 。蚘法のお䜜法䞻に 環境倉数 呚蟺は Vite のそれに埓ったものになりたす。 Terraform 前述の アクセスさせたいナヌザ / グルヌプをアプリケヌションに玐付ける の方針をグルヌプ単䜍で実斜させる前提のコヌドです IAM Identity Center 偎 こちらをクリックしお参照のこず locals { entra_id_groups = { spa = [ "test" # SPA 利甚を蚱可したい Entra ID グルヌプ名を指定 ] } } /* IAM Identity Center むンスタンスに察し必芁な蚭定が行われる これは Terraform リ゜ヌスでの管理が難しいので、蚭定は AWS マネゞメントコン゜ヌル䞊から行い、Terraform からは参照に留める */ data "aws_ssoadmin_instances" "main" {} resource "aws_ssoadmin_application" "spa" { name = "SPA" description = "SAML application for SPA" application_provider_arn = "arn:aws:sso::aws:applicationProvider/custom-saml" instance_arn = tolist (data.aws_ssoadmin_instances.main.arns) [ 0 ] portal_options { visibility = "ENABLED" sign_in_options { origin = "IDENTITY_CENTER" } } } data "aws_identitystore_group" "spa" { for_each = toset (local.entra_id_groups.spa) identity_store_id = tolist (data.aws_ssoadmin_instances.main.identity_store_ids) [ 0 ] alternate_identifier { unique_attribute { attribute_path = "DisplayName" attribute_value = each.key } } } resource "aws_ssoadmin_application_assignment" "spa" { for_each = toset (local.entra_id_groups.spa) application_arn = aws_ssoadmin_application.spa.arn principal_id = data.aws_identitystore_group.spa [ each.key ] .group_id principal_type = "GROUP" } Cognito ほか SPA 偎で必芁ずする偎 こちらをクリックしお参照のこず data "aws_s3_object" "saml_metadata" { /* コヌド管理しおいない。手でアップロヌドするこず IAM Identity Center アプリケヌションの "IAM Identity Center SAML metadata file" から取埗する */ bucket = aws_s3_bucket.static_files.id key = "saml-metadata/spa.xml" } resource "aws_iam_saml_provider" "spa" { name = "spa" saml_metadata_document = data.aws_s3_object.saml_metadata.body } resource "aws_cognito_identity_pool" "spa" { identity_pool_name = "spa" allow_unauthenticated_identities = false # 認蚌されおいないIDを無効化 saml_provider_arns = [ aws_iam_saml_provider.spa.arn ] cognito_identity_providers { client_id = aws_cognito_user_pool_client.spa.id provider_name = "cognito-idp.$ { data.aws_region.current.region } .amazonaws.com/$ { aws_cognito_user_pool.spa.id } " server_side_token_check = false } } data "aws_iam_policy_document" "assume_from_cognito" { statement { effect = "Allow" principals { type = "Federated" identifiers = [ "cognito-identity.amazonaws.com" ] } actions = [ "sts:AssumeRoleWithWebIdentity" ] condition { test = "StringEquals" variable = "cognito-identity.amazonaws.com:aud" values = [ aws_cognito_identity_pool.spa.id, ] } condition { test = "ForAnyValue:StringLike" variable = "cognito-identity.amazonaws.com:amr" values = [ "authenticated" ] } } } /* Cognito Identity Pool ず連携しお䞀時認蚌情報を取埗するための IAM ロヌル このロヌルを Cognito Identity Pool の Authenticated Role ずしお蚭定 */ resource "aws_iam_role" "allow_actions_for_spa" { name = "allow-actions-for-spa" assume_role_policy = data.aws_iam_policy_document.assume_from_cognito.json } data "aws_iam_policy_document" "allow_actions_for_spa" { statement { # 略 } } # デヌタバケットぞの読み取り専甚ポリシヌをアタッチ resource "aws_iam_role_policy" "allow_actions_for_spa" { name = "allow-actions-for-spa" role = aws_iam_role.allow_actions_for_spa.id policy = data.aws_iam_policy_document.allow_actions_for_spa.json } # 認蚌されたナヌザヌにデフォルトロヌルを割り圓おる resource "aws_cognito_identity_pool_roles_attachment" "spa" { identity_pool_id = aws_cognito_identity_pool.spa.id roles = { "authenticated" = aws_iam_role.allowed_actions_for_spa.arn } } resource "aws_cognito_user_pool" "spa" { name = "spa" } resource "aws_cognito_user_pool_domain" "spa" { user_pool_id = aws_cognito_user_pool.spa.id domain = "test" # 適宜倉曎する } # これは SPA かどうかに関係なく䜿えるはずなので "main" に改めおおく resource "aws_cognito_identity_provider" "main" { user_pool_id = aws_cognito_user_pool.spa.id provider_name = "iam-identity-center" provider_type = "SAML" provider_details = { "MetadataFile" = data.aws_s3_object.saml_metadata.body } attribute_mapping = { "email" = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" } lifecycle { ignore_changes = [ provider_details, # MetadataFile を解釈しお蚭定される倀が延々差分を生じさせるので ignore する ] } } # Cognito User Pool のクラむアントSPAからログむンするための蚭定 resource "aws_cognito_user_pool_client" "spa" { name = "SPA" user_pool_id = aws_cognito_user_pool.spa.id explicit_auth_flows = [ "ALLOW_USER_AUTH" , "ALLOW_CUSTOM_AUTH" , "ALLOW_USER_SRP_AUTH" , "ALLOW_REFRESH_TOKEN_AUTH" , ] allowed_oauth_flows_user_pool_client = true allowed_oauth_scopes = [ "openid" , "email" , ] allowed_oauth_flows = [ "code" ] supported_identity_providers = [ aws_cognito_identity_provider.main.provider_name, ] token_validity_units { access_token = "minutes" id_token = "minutes" refresh_token = "days" } callback_urls = [ "https://$ { var.spa_domain } /path/to/app" ] # 適宜倉曎する default_redirect_uri = "https://$ { var.spa_domain } /path/to/app" # 適宜倉曎する } resource "local_file" "spa" { content = <<EOT VITE_AWS_REGION=$ { data.aws_region.current.region } VITE_USER_POOL_ID=$ { aws_cognito_user_pool.spa.id } VITE_USER_POOL_CLIENT_ID=$ { aws_cognito_user_pool_client.spa.id } VITE_IDENTITY_POOL_ID=$ { aws_cognito_identity_pool.spa.id } VITE_COGNITO_DOMAIN=$ { aws_cognito_user_pool_domain.spa.id } .auth.$ { data.aws_region.current.region } .amazoncognito.com VITE_REDIRECT_URI=https://$ { var.spa_domain } /path/to/spa EOT filename = "./path/to/spa/.env" # 適宜倉曎する file_permission = "0644" } おたけずしお S3 および CloudFront に関するコヌドも掲茉しおおきたす こちらをクリックしお参照のこず resource "aws_s3_bucket" "spa" { bucket = "test" # 適宜倉曎する } # CloudFront からのアクセスを蚱可する S3 バケットポリシヌを定矩 data "aws_iam_policy_document" "spa" { statement { effect = "Allow" principals { type = "Service" identifiers = [ "cloudfront.amazonaws.com" , ] } actions = [ "s3:GetObject" , ] resources = [ "$ { aws_s3_bucket.spa.arn } /*" , ] condition { test = "StringEquals" variable = "AWS:SourceArn" values = [ aws_cloudfront_distribution.spa.arn, ] } } } /* OAC (Origin Access Control) を䜿った CloudFront 経由のアクセスを蚱可するバケットポリシヌ このポリシヌにより、S3バケットぞの盎接アクセスが拒吊される */ resource "aws_s3_bucket_policy" "spa" { bucket = aws_s3_bucket.spa.id policy = data.aws_iam_policy_document.spa.json } resource "aws_cloudfront_origin_access_control" "spa" { name = "oac-for-spa" description = "OAC for SPA S3 bucket" origin_access_control_origin_type = "s3" signing_behavior = "always" signing_protocol = "sigv4" } data "aws_cloudfront_cache_policy" "caching_optimized" { name = "Managed-CachingOptimized" } data "aws_cloudfront_cache_policy" "caching_disabled" { name = "Managed-CachingDisabled" } resource "aws_acm_certificate" "spa" { provider = aws.us-east-1 domain_name = "*.test.example" # 適宜倉曎する validation_method = "DNS" lifecycle { create_before_destroy = true } } resource "aws_acm_certificate_validation" "spa" { provider = aws.us-east-1 certificate_arn = aws_acm_certificate.spa.arn validation_record_fqdns = [ for record in aws_route53_record.spa_cert_validation : record.fqdn ] } resource "aws_cloudfront_distribution" "spa" { enabled = true tags = { Name = "spa" } aliases = [ var.spa_domain, ] origin { domain_name = aws_s3_bucket.spa.bucket_regional_domain_name origin_id = aws_s3_bucket.spa.id origin_access_control_id = aws_cloudfront_origin_access_control.spa.id } default_cache_behavior { allowed_methods = [ "GET" , "HEAD" , "OPTIONS" ] cached_methods = [ "GET" , "HEAD" ] target_origin_id = aws_s3_bucket.spa.id viewer_protocol_policy = "redirect-to-https" # 今回の SPA では Cookie やヘッダヌを考慮しないので転送もしない cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id function_association { event_type = "viewer-request" function_arn = aws_cloudfront_function.spa.arn } } # JavaScriptファむルを凊理 ordered_cache_behavior { path_pattern = "path/to/app/assets/*.js" allowed_methods = [ "GET" , "HEAD" , "OPTIONS" ] cached_methods = [ "GET" , "HEAD" ] target_origin_id = aws_s3_bucket.spa.id viewer_protocol_policy = "redirect-to-https" cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id } # SPA の HTML ファむルや画像、その他アセットを凊理 ordered_cache_behavior { path_pattern = "path/to/app/*" allowed_methods = [ "GET" , "HEAD" , "OPTIONS" ] cached_methods = [ "GET" , "HEAD" ] target_origin_id = aws_s3_bucket.spa.id viewer_protocol_policy = "redirect-to-https" cache_policy_id = data.aws_cloudfront_cache_policy.caching_disabled.id } restrictions { geo_restriction { restriction_type = "none" } } viewer_certificate { acm_certificate_arn = aws_acm_certificate.spa.arn ssl_support_method = "sni-only" } } # SPAのパスを曞き換える CloudFront Function resource "aws_cloudfront_function" "spa" { name = "spa-router" runtime = "cloudfront-js-1.0" comment = "Rewrites SPA paths to index.html" publish = true code = <<EOT function handler(event) { var request = event.request; var uri = request.uri; // ファむル拡匵子を持぀パスはスキップする䟋: .js, .css, .png if (uri. split ('.'). pop () !== uri) { return request; } // リク゚ストパスが特定のSPAプレフィックスで始たるかチェック if (uri.startsW ith ('/path/to/app')) { request . uri = '/path/to/app/index.html'; } return request; } EOT } SPA コヌド 認蚌郚分 const CLIENT_ID = import . meta . env . VITE_USER_POOL_CLIENT_ID ; const COGNITO_DOMAIN = import . meta . env . VITE_COGNITO_DOMAIN ; # Cognito の user pool に察し蚭定できるドメむンhosted UI アクセス時のドメむンずしお芋えるの ID。環境倉数経由で枡す const REDIRECT_URI = import . meta . env . VITE_REDIRECT_URI ; # SPA の URL。Cognito で認蚌が成功した堎合に戻っおくるため必芁。環境倉数経由で枡す export function redirectToHostedUI () { const url = new URL ( `https:// ${ COGNITO_DOMAIN } /login` ) ; url . searchParams . set ( "client_id" , CLIENT_ID ) ; url . searchParams . set ( "response_type" , "code" ) ; url . searchParams . set ( "scope" , "openid email" ) ; url . searchParams . set ( "redirect_uri" , REDIRECT_URI ) ; window . location . href = url . toString () ; } export async function handleCognitoRedirect () { const params = new URLSearchParams ( window . location . search ) ; const code = params . get ( "code" ) ; if ( ! code ) return sessionStorage . getItem ( "id_token" ) ; // すでに保持しおいる堎合 const body = new URLSearchParams ({ grant_type : "authorization_code" , client_id : CLIENT_ID , redirect_uri : REDIRECT_URI , code , }) ; const tokenResp = await fetch ( `https:// ${ COGNITO_DOMAIN } /oauth2/token` , { method : "POST" , headers : { "Content-Type" : "application/x-www-form-urlencoded" } , body : body . toString () , }) ; if ( ! tokenResp . ok ) throw new Error ( "Failed to fetch tokens" ) ; const tokens = await tokenResp . json () ; sessionStorage . setItem ( "id_token" , tokens . id_token ) ; return tokens . id_token ; } クレデンシャル取埗郚分 import { S3Client } from "@aws-sdk/client-s3" ; # S3 を觊る堎合の䟋ずしお蚘茉 import { CognitoIdentityClient , GetIdCommand , GetCredentialsForIdentityCommand } from "@aws-sdk/client-cognito-identity" ; const REGION = import . meta . env . VITE_AWS_REGION ; # AWS リヌゞョン。Cognito リ゜ヌスが存圚する堎所。環境倉数経由で枡す const USER_POOL_ID = import . meta . env . VITE_USER_POOL_ID ; const IDENTITY_POOL_ID = import . meta . env . VITE_IDENTITY_POOL_ID ; export async function getS3Client () { const idToken = localStorage . getItem ( "id_token" ) ; if ( ! idToken ) throw new Error ( "User not authenticated" ) ; const client = new CognitoIdentityClient ({ region : REGION }) ; const identityResp = await client . send ( new GetIdCommand ({ IdentityPoolId : IDENTITY_POOL_ID , Logins : { [ `cognito-idp. ${ REGION } .amazonaws.com/ ${ USER_POOL_ID } ` ] : idToken , } , }) ) ; const credResp = await client . send ( new GetCredentialsForIdentityCommand ({ IdentityId : identityResp . IdentityId , Logins : { [ `cognito-idp. ${ REGION } .amazonaws.com/ ${ USER_POOL_ID } ` ] : idToken , } , }) ) ; return new S3Client ({ region : REGION , credentials : { accessKeyId : credResp . Credentials . AccessKeyId , secretAccessKey : credResp . Credentials . SecretKey , sessionToken : credResp . Credentials . SessionToken , expiration : credResp . Credentials . Expiration , } , }) ; } おわりに Cognito から IAM Identity Center を IdP ずしお参照し、これを䜿甚しお SPA ぞのアクセス制埡を実斜する䟋に぀いお解説したした。 S3 + CloudFront で配信する諞々のアクセス制埡ずしお真っ先に浮かんでしたうのが私的にはアクセス元 IP アドレスによる境界制限 AWS WAF v2 などを䜿う想定なのですが、IAM Identity Center に䟝拠したナヌザ認蚌ずいう遞択肢が今回割合気軜に䜿え、なかなか珟代的なアクセス制埡ができるようになったず思っおいたす。 セキュリティ的な方面から考えおも 境界防埡ではなく認蚌による制限ができる 既存の IdP の構成を䜿甚しお認蚌の蚭蚈ができ、ID 運甚の分散が防げる 既に IdP 䞊に存圚するナヌザを䜿っおの SSO ずいう挙動になり利甚者の䜓隓をあたり損わない ずいったメリットがあり、芋通しのよい構成ずなりたした。 IAM Identity Center / Cognito l掻甚の䞀助ずしお本皿が参考になれば幞いです。 MNTSQ 株匏䌚瀟 SRE 秋本 参考 IAM Identity Center と Cognito を統合してみた 先行事䟋です *1 : https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html *2 : https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools.html *3 : https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html *4 : https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-federation.html?icmpid=docs_cognito_console_help_panel *5 : https://docs.aws.amazon.com/singlesignon/latest/userguide/manage-your-applications.html *6 : https://docs.aws.amazon.com/singlesignon/latest/userguide/customermanagedapps-saml2-oauth2.html *7 : 特に遞定に理由はなく、ずりあえず動くものずサクッず䜜りたかった皋床です
アバタヌ
はじめに 匊瀟では BI ツヌルずしお Redash を EC2 䞊でセルフホストするかたちで利甚しおいたす。CS や Sales 等の郚門で日々の指暙を远うのに䜿われるだけでなく、開発や運甚のためのアラヌティングにも掻甚されおいたす。 今回この Redash ぞ SSO ログむンの仕組みを導入し、Redash 利甚者の操䜜履歎を 郚分的に 監査できるようにしたした。SSO の IdP ずしおは掲題通り IAM Identity Center を遞定しおいたす。これら察応に぀いお蚘録したす。 なぜ IAM Identity Center を IdP に? 去る2025幎5月䞋旬に以䞋蚘事を公開したした。 この䞭で ナヌザから芋た堎合に各 AWS アカりントぞのアクセスが透過的になるのも IAM Identity Center 導入での嬉しさのひず぀ですが、IAM Identity Center 自䜓がアむデンティプロバむダ (IdP) ずしおの利甚も可胜であるずいう点から、SRE 䞻䜓での他のサヌビスぞの SSO 化の詊みに぀いおも着手し易くなっおきたした。今埌は瀟内で独自のナヌザ管理䜓系が存圚する諞々のサヌビスを着実に SSO 化し、利甚者の利䟿性向䞊やセキュアな䜓制構築に繋げおゆこうず蚈画しおいたす。 ずいう䞀節がありたす。匊瀟では Redash を SRE ず CRE ずが連携しお運甚しおおり、SSO 化の取り組みは SRE の責任範囲にお実斜するこずずしたした。匊瀟 CRE に぀いおは以䞋もぜひご参照ください。 note.mntsq.co.jp さお前掲の IAM Identity Center に関する蚘事は Entra ID を IdP ずしお IAM Identity Center から AWS ぞの SSO ログむンを可胜にするずいう趣旚になりたす。぀たり IAM Identity Center が抱え蟌んでいるナヌザ / グルヌプの元ネタは Entra ID ずなりたす。 ここで IAM Identity Center を IdP ずしお Redash ぞ SSO するよう構成するこずで、 Entra ID 偎での察応をするこずなく Entra ID ナヌザ / グルヌプを掻甚しお Redash ぞのログむンが可胜になりたす。 これは Redash を利甚する立堎にしおみるず Entra ID ナヌザを䜿っお Redash が䜿えるずいう䜓隓に぀ながり、匊瀟が普段の業務で䜿う他の SaaS ず同様の利甚䜓隓が埗られる栌奜になりたす。サヌビス利甚の煩雑さが䜎枛できるずいう嬉しさがあるでしょう。 構成 SSO ログむン化前埌での構成倉遷を瀺したす。なお Redash のためのむンフラは EC2 + ALB で構成され、Redash そのものは EC2 むンスタンス 内で docker-compose によっお動かしおいたす。公匏ドキュメントに沿った方法 *1 ずいえるでしょう。 SSO ログむン前 至極シンプルです。パスワヌドマネヌゞャ匊瀟では党瀟的に Keeper *2 を䜿甚しおいたす䞊の Redash ナヌザ名およびパスワヌドを必芁に応じお利甚しログむンする方匏です。 SSO ログむン埌 盞応に耇雑になりたした。Redash 以倖にも登堎人物が増えおいたす。 IAM Identity Center に Entra ID から連携されたナヌザを䜿い、Redash ぞログむンしたす Redash が出力するログを EC2 から CloudWatch Logs に出力し、そこから Data Firehose を経由し、最終的には S3 に配眮したす IAM Identity Center 経由での Redash ぞのログむン履歎を CloudTrail ログこれも最終的には S3 に配眮されたすから远跡したす S3 䞊に配眮された各皮ログは必芁に応じお Athena から怜玢が可胜です SSO 蚭定 おおむね以䞋のような順番での䜜業が必芁になりたす IAM Identity Center アプリケヌションの甚意 IAM Identity Center アプリケヌションからの必芁な情報の取埗ず蚭定 Redash 䞊での SAML 蚭定 IAM Identity Center アプリケヌションの甚意 前述のずおり、IAM Identity Center は倖郚 IdP を匕き受けお AWS アカりントぞのアクセスを可胜ずする利甚圢態以倖に、自身が IdP ずなり AWS サヌビスや倖郚サヌビスぞのアクセスを担うこずもできたす。 これには IAM Identity Center 内でアプリケヌションずいうものを甚意し、アクセスさせたいナヌザ / グルヌプをアプリケヌションに玐付けるこずで達成可胜です *3 。 AWS サヌビスず連携させる堎合ずそれ以倖ずでそれぞれ察応が異なり、今回のように Redash が察象の堎合は利甚者自身が管理する (customer managed) アプリケヌションで構成するずよいでしょう *4 。 Redash 公匏ドキュメントのうち SSO に関するもの *5 を読むず、今回䜿うべきは SAML であるこずがわかりたす。したがっお IAM Identity Center も SAML を前提ずしたアプリケヌションを甚意したす。 サクッず Terraform で䜜っおしたいたしょう。コヌド䟋は以䞋の通りです。 こちらを参照 locals { entra_id_groups = { /* Redash ぞのアクセスを蚱可したいナヌザが属する Entra ID グルヌプ名を指定 Entra ID グルヌプ / ナヌザが IAM Identity Center ぞ SCIM で連携などされおいるこずを前提にする */ redash = [ "Engineer" , "CS" , "Sales" , ... ] } } /* IAM Identity Center むンスタンスに察し必芁な蚭定が行われる これは Terraform リ゜ヌスでの管理が難しいので、蚭定は AWS マネゞメントコン゜ヌル䞊から行い、Terraform からは参照に留める */ data "aws_ssoadmin_instances" "main" {} # IAM Identity Center アプリケヌション定矩。SAML を前提ずするもの resource "aws_ssoadmin_application" "redash" { name = "Redash" description = "IAM Identity Center appliction to access Redash" application_provider_arn = "arn:aws:sso::aws:applicationProvider/custom-saml" instance_arn = tolist (data.aws_ssoadmin_instances.main.arns) [ 0 ] portal_options { visibility = "ENABLED" sign_in_options { origin = "IDENTITY_CENTER" } } } # IAM Identity Center アプリケヌションの利甚を蚱可する Entra ID グルヌプが IAM Identity Center に同期されたものを参照できるようにする data "aws_identitystore_group" "redash" { for_each = toset (local.entra_id_groups.redash) identity_store_id = tolist (data.aws_ssoadmin_instances.main.identity_store_ids) [ 0 ] alternate_identifier { unique_attribute { attribute_path = "DisplayName" attribute_value = each.key } } } # アプリケヌション / Entra ID グルヌプ間の玐付けをする resource "aws_ssoadmin_application_assignment" "redash" { for_each = toset (local.entra_id_groups.redash) application_arn = aws_ssoadmin_application.redash.arn principal_id = data.aws_identitystore_group.redash [ each.key ] .group_id principal_type = "GROUP" } IAM Identity Center アプリケヌションからの必芁な情報の取埗ず蚭定 ここたでで IAM Identity Center アプリケヌションの甚意はできたした。しかし残念ながら SAML 蚭定そのものは Terraform では蚭定できたせん 。公匏ドキュメントを参照し぀぀、適宜蚭定する必芁がありたす。以䞋を芋るずよいでしょう。 docs.aws.amazon.com 明瀺的な蚭定が必芁になるのは Application metadata における以䞋内容です。必芁な倀は Redash 公匏ドキュメント *6 をもずに蚭定したす。 なお Application metadata ず同じ画面にある IAM Identity Center metadata はその内容を控えおおきたす。Redash 偎の蚭定で䜿いたす。IAM Identity Center ずしおはこのあたりの蚭定をたずめた XML ファむルの取埗が可胜なのですが、生憎 Redash にはこの XML ファむルを読み取れる仕組みがありたせん。よっお逐次蚭定を行う必芁がありたす。 重芁attribute mapping の蚭定 ここたで枈めば䞀段萜  ず思いきや、前述の AWS 公匏ドキュメントには以䞋の䞀節がありたす。 However, you must also provide additional SAML attribute mappings for your own SAML 2.0 applications. These mappings enable IAM Identity Center to populate the SAML 2.0 assertion correctly for your application. You can provide this additional SAML attribute mapping when you set up the application for the first time. You can also provide SAML 2.0 attribute mappings on the application details page in the IAM Identity Center console. このあたりも Terraform では盎接の蚭定が䞍可胜です。 AWS マネゞメントコン゜ヌルから蚭定しおしたうのが手っ取り早いでしょう。 蚭定した IAM Identity Center アプリケヌションの詳现画面から Actions -> Edit attribute mappings ず蟿り、以䞋の倀を蚭定したす。 User attribute in the application Maps to this string value or user attribute in IAM Identity Center Format Subject倉曎䞍可 ${user:email} emailAddress FirstName ${user:givenName} basic LastName ${user:familyName} basic Redash 䞊での SAML 蚭定 基本的には Redash の公匏ドキュメントに埓えば蚭定が枈みたす *7 。Admin グルヌプに属する Redash ナヌザでないず蚭定画面が出ないので泚意が必芁です。実際に蚭定が必芁な内容は以䞋の通りです。 SAML Enabled チェックをいれる SAML Metadata URL IAM Identity Center 偎 IAM Identity Center metadata 内のうち “IAM Identity Center SAML metadata file” で瀺される URL を蚘入 SAML Entity ID こだわりがなければ Redash IAM Identity Center 偎 Application metadata 内 “Application SAML audience” ず䞀臎しおいればよい SAML NameID Format  urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress 固定倀 ログ監査 ここたでで Redash ぞ IAM Identity Center を IdP ずしお SSO アクセスする手筈が敎いたした。折角 IAM Identity Center を経由しおの操䜜ずなるので、CloudTrail あたりから操䜜ログを棚卞したくなるのが人情ずいうものでしょう。このあたりの敎備をしたす。 ただし本皿冒頭で Redash 利甚者の操䜜履歎を 郚分的に 監査できるようにしたした ず蚘茉した通り、Redash ぞのログむンからログアりトたでの党操䜜をナヌザにひもづくかたちで远跡できるような状況には出来おいたせん。ナヌザを絞れる状態で远えるログは以䞋たでずなりたす。 IAM Identity Center (CloudTrail) ログで远跡可 Redash にログむンした Redash ログを駆䜿しお远跡可 ク゚リを実行した実行したク゚リたで远跡化 以䞋は䞊蚘ログでは远跡䞍可な様子でした。無論ログ自䜓はあるのですが、ナヌザにひもづく圢でのログは存圚せず、他の情報ず突き合わせお远跡する必芁がありたす。 ク゚リ実行結果を芋た ダッシュ ボヌドを芋た 远跡可胜な範囲でログをどのように远えるかを瀺せればず思いたす。 IAM Identity Center (CloudTrail) ログから Redash ログむン圢跡を远う いきなり結論です。以䞋のような Athena ク゚リが䜿えたす。盎近7日間のログを远跡する䟋です select userIdentity.onBehalfOf.userId as userId, json_extract_scalar(serviceEventDetails, ' $["app-id"] ' ) as appId, eventtime from <CloudTrail ログ甚 Athena DB> where timestamp between date_format( current_date - interval ' 7 ' day, ' %Y/%m/%d ' ) and date_format( current_date , ' %Y/%m/%d ' ) and eventSource = ' sso.amazonaws.com ' and eventName = ' Federate ' and json_extract_scalar(serviceEventDetails, ' $["app-id"] ' ) = ' <Redash ぞの SAML ログむンで䜿う IAM Identity Center アプリケヌション ID> ' 日本語で蚘茉しおある箇所に぀いお解説したす。 CloudTrail ログ甚 Athena DB S3 䞊にある CloudTrail ログを取り扱う為の Athena DB。 パヌティション 射圱を䜿う AWS 公匏ドキュメントに沿った内容を前提ずする *8 Redash ぞの SAML ログむンで䜿う IAM Identity Center アプリケヌション ID IAM Identity Center アプリケヌション に察応する むンスタンス の ID 2. に぀いおは解説が必芁でしょう。ここで指定すべき ID は ARN に含たれる ID (ex. api-xxx ) ではなく、 URL 䞭に瀺される ID (ex. ins-xxx ) のほうです。䌏字が倚い䟋で恐瞮ですが、以䞋 スクリヌンショット 䞭の赀枠の箇所を䜿っお始めお䞊蚘ク゚リで所望のログが埗られたす。 このあたりに぀いおは AWS 公匏のドキュメントを含めおも公開されおいる情報がなく、匊瀟を担圓頂いおおりたす AWS 瀟の SA の方々に倚倧なるご協力を頂き実珟したものずなりたす。この堎を借りお感謝申し䞊げたす。なおこの内容は今埌倉化する可胜性が倚分にある暡様で、その際にログの取り方を倉えられるようにしおおく必芁がありたす。 匊瀟では䞊蚘 Athena ク゚リを名前付きク゚リずしお敎備したうえで専甚の workgroup を甚意し、適圓な期間でのログ怜玢ずその結果のレポヌティングの仕組みを運甚するようにしおいたす。これは今回の Redash ログ以倖でも同様の仕組みでレポヌティングを行っおいるものがあるので、それらをたずめお別の機䌚にブログ化できればず思いたす。少しだけネタばらしをするず以䞋のような仕組みです。 Lambda で Athena ク゚リを実行 Athena がク゚リの実行を終える Athena workgroup にひもづくかたちでク゚リ実行完了むベントが EventBridge から発火する 3. のむベントを拟う Lambda 関数が Athena ク゚リ実行結果を取埗し、必芁な凊理を実行 最終的な結果を Lambda -> SNS -> AWS Chatbot -> Slack ずいう流れで通知 Redash ログからク゚リ実行圢跡を远う 匊瀟のように EC2 むンスタンス 䞊で Redash をセルフホストする堎合、圓然ながらそのログは EC2 むンスタンス 䞊に存圚するこずになりたす。 これではログを継続的に远う堎合に郜合が悪いので、EC2 䞊からいったん CloudWatch Logs ロググルヌプにログを出し、そのうえで Data Firehose を経由し S3 にログを出力し、Athena で远跡が可胜なようにしおいたす。 EC2 から CloudWatch Logs ぞログを出すにはいく぀か方法がありたすが、幞いなこずに匊瀟での Redash の動かし方は docker-compose によるのものなため、以䞋を利甚できたす。 docs.docker.com これを䜿い logging : driver : awslogs options : awslogs-region : ap-northeast-1 awslogs-group : <CloudWatch ロググルヌプ名> awslogs-create-group : "false" ずいった蚘述を Redash コンテナ管理甚の docker-compose.yml ぞ远蚘し、EC2 むンスタンス プロファむルぞ CloudWatch Logs 関係の必芁なアクションを蚱可しおやれば、EC2 から CloudWatch Logs ぞのログ出力に぀いおは解決したす。具䜓的には以䞋アクションが認可できおいれば OK です。 logs:CreateLogStream logs:PutLogEvents Redash の兞型的なク゚リ実行ログは Redash サヌバにおける [2025-08-08 02:11:13,060][PID:31][INFO][root] Inserting job for 915b2acd6d435dfd3ea3578a457f4ded with metadata={'Username': u'foo.bar@example.com', 'Query ID': u'1234'} なので、これだけが Athena で取り扱えればひずたずは充分ずいう指針のもず、以䞋のような仕組みを入れおいたす。 Redash サヌバが CloudWatch Logs ぞログを党お出力する CloudWatch Logs は log subscription filter を介しお Data Firehose ぞログを送出する この際に必芁なログだけを Data Firehose ぞ送出するようフィルタ蚭定を行う Data Firehose は最終目的地の S3 バケット ぞログを送出する Terraform コヌドずしおは以䞋のようになりたす。EC2 から送られおくるログを䞀旊集める CloudWatch Logs ロググルヌプの甚意から S3 バケット たでを担圓しおいたす。 こちらを参照 data "aws_caller_identity" "current" {} /* Redash ログ保管甚 EC2 内で docker-compose にお動かしおいるコンテナ矀からログを飛ばす想定 */ resource "aws_cloudwatch_log_group" "redash_log" { name = "/$ { var.env } -redash" retention_in_days = 7 # ほずんど実甚に堪えない内容なので長く保持しおおく必芁なし } resource "aws_cloudwatch_log_subscription_filter" "redash_log" { name = "$ { var.env } -redash-log-to-data-firehose" log_group_name = aws_cloudwatch_log_group.redash_log.name /* ク゚リ実行ログでしか Redash 操䜜者を絞れない このログは "Inserting job" を含むものになるので、こい぀だけを S3 ぞ送出するログずする */ filter_pattern = "Inserting job" destination_arn = aws_kinesis_firehose_delivery_stream.redash_log.arn role_arn = aws_iam_role.redash_log_to_data_firehose.arn } # Redash ログを Athena で取り扱えるように S3 ぞ保存したい。そのためのバケット resource "aws_s3_bucket" "redash_log" { bucket = "$ { var.env } -redash-log" } # 半幎皋床はログを確保できおおくようにする。監査目的であればもっず長く取っおおくべきかも resource "aws_s3_bucket_lifecycle_configuration" "redash_log" { bucket = aws_s3_bucket.redash_log.id rule { status = "Enabled" id = "delete after 180 days" expiration { days = 180 } filter { prefix = "" } } } resource "aws_s3_bucket_versioning" "redash_log" { bucket = aws_s3_bucket.redash_log.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_public_access_block" "redash_log" { bucket = aws_s3_bucket.redash_log.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_server_side_encryption_configuration" "redash_log" { bucket = aws_s3_bucket.redash_log.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } data "aws_iam_policy_document" "redash_log_bucket_policy" { statement { effect = "Allow" actions = [ "s3:PutObject" ] resources = [ "$ { aws_s3_bucket.redash_log.arn } /*" ] principals { type = "Service" identifiers = [ "logging.s3.amazonaws.com" ] } condition { test = "StringEquals" variable = "aws:SourceAccount" values = [ data.aws_caller_identity.current.account_id ] } } } resource "aws_s3_bucket_policy" "redash_log" { bucket = aws_s3_bucket.redash_log.bucket policy = data.aws_iam_policy_document.redash_log_bucket_policy.json } /* CloudWatch Logs から S3 ぞログを保存するための Data Firehose リ゜ヌス CW Logs 偎 subscription filter を経由しお流れおくるデヌタを扱う */ resource "aws_kinesis_firehose_delivery_stream" "redash_log" { name = "$ { var.env } -redash-log" destination = "extended_s3" extended_s3_configuration { role_arn = aws_iam_role.redash_log_to_s3.arn bucket_arn = aws_s3_bucket.redash_log.arn buffering_size = 64 # MB 単䜍。dynamic partitioning が有効の堎合必須 dynamic_partitioning_configuration { enabled = "true" } processing_configuration { enabled = "true" processors { type = "MetadataExtraction" parameters { parameter_name = "JsonParsingEngine" parameter_value = "JQ-1.6" } parameters { parameter_name = "MetadataExtractionQuery" parameter_value = "{prefix: {dummy: (\"logs\")} | .dummy}" # 実質的に "logs" ずいう固定文字列を返すだけ } } processors { type = "Decompression" parameters { parameter_name = "CompressionFormat" parameter_value = "GZIP" } } processors { type = "AppendDelimiterToRecord" } } prefix = "!{partitionKeyFromQuery:prefix}/$ { data.aws_caller_identity.current.account_id } /!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/" error_output_prefix = "error/!{firehose:error-output-type}/$ { data.aws_caller_identity.current.account_id } /!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/" cloudwatch_logging_options { enabled = true log_group_name = aws_cloudwatch_log_group.redash_log.name log_stream_name = "S3Delivery" } } } Athena テヌブル / ビュヌの䟋は以䞋のようになりたす。Redash ログは少々やんちゃな内容ずなる堎合があり、冗長な衚蚘になっおいたす。 CREATE TABLE 文 CREATE EXTERNAL TABLE " redash_server_log_source " ( messageType string, owner string, logGroup string, logStream string, subscriptionFilters array<string>, logEvents array<struct<id:string, timestamp :bigint, message:string>> ) PARTITIONED BY ( ` timestamp ` string ) ROW FORMAT SERDE ' org.openx.data.jsonserde.JsonSerDe ' LOCATION ' s3://mntsq-ops-redash-log/logs/465191204183/ ' TBLPROPERTIES ( ' projection.enabled ' = ' true ' , ' projection.timestamp.type ' = ' date ' , ' projection.timestamp.format ' = ' yyyy/MM/dd ' , ' projection.timestamp.range ' = ' 2025/01/01,NOW ' , ' projection.timestamp.interval ' = ' 1 ' , ' projection.timestamp.interval.unit ' = ' DAYS ' , ' storage.location.template ' = ' s3://mntsq-ops-redash-log/logs/465191204183/${timestamp}/ ' ); CREATE VIEW 文 CREATE OR REPLACE VIEW redash_server_log AS WITH unnested_logs AS ( SELECT " day " , l.id AS event_id, from_unixtime(l. " timestamp " / 1000 ) AS timestamp , l.message FROM " redash_server_log_source " CROSS JOIN UNNEST(logEvents) AS t (l) ), parsed_logs AS ( SELECT *, -- TRY関数を远加 TRY(regexp_extract(message, ' ([ -:,0-9]+) ' , 1 )) AS log_timestamp, TRY(regexp_extract(message, ' \[PID:([0-9]+)\] ' , 1 )) AS pid, TRY(regexp_extract(message, ' \[([A-Z]+)\] ' , 1 )) AS log_level, TRY(regexp_extract(message, ' Inserting job for ([a-f0-9]+) ' , 1 )) AS job_id, TRY(regexp_extract(message, ''' Username '' : u '' ([^ '' ]+) ''' , 1 )) AS username, TRY(regexp_extract(message, ''' Query ID '' : u '' ([^ '' ]+) ''' , 1 )) AS query_id FROM unnested_logs ) SELECT " day " , event_id, timestamp , log_timestamp, pid, log_level, job_id, username, query_id, message AS original_message FROM parsed_logs; なおテヌブル以倖にビュヌを䜜成しおいるのは Data Firehose 内でログの敎圢などをせず S3 に送出しおいるために本来欲しいログが message ゚ントリ内に抌し蟌められおいる点の察策によりたす。これは拙皿の以䞋ず同様です。 tech.mntsq.co.jp おわりに IAM Identity Center を IdP ずしお SSO アクセスするケヌスに぀いお、Redash を題材に解説したした。Redash ぞの SSO アクセスに必芁な情報よりもログ関連のオペレヌションに関する内容のほうが倚いように思い、本皿題名をミスったのではないかず今にしお思っおしたいたした。 SSO アクセスするこずでの ID 管理手法の敎頓やサヌビス利䟿性向䞊もさるこずながら、利甚察象ずは別の箇所でログが埗られるこずでの状況把握が出来るようになるずいうメリットも、倖郚 IdP を䜿うこずでのメリットずいえるでしょう。本皿で述べたように CloudTrail ログが䜿える堎合であっおもその探玢には難儀するケヌスがありたすが、手を尜くせば远跡は可胜ずいう事実に勇気付けられる面もあり、たた実務䞊も有益な点はあるず感じたした。 Redash をお䜿いの方で IdP の遞定に悩たれおいる方の䞀助になれば幞いです。 MNTSQ 株匏䌚瀟 SRE 秋本 *1 : https://redash.io/help/open-source/setup/#-Docker *2 : https://www.keepersecurity.com/ja_JP/solutions/enterprise-password-management.html *3 : https://docs.aws.amazon.com/singlesignon/latest/userguide/manage-your-applications.html *4 : https://docs.aws.amazon.com/singlesignon/latest/userguide/customermanagedapps.html *5 : https://redash.io/help/user-guide/users/authentication-options/ *6 : https://redash.io/help/user-guide/users/authentication-options/#Self-Hosted-SAML *7 : https://redash.io/help/user-guide/users/authentication-options/#SAML-2-0 *8 : https://docs.aws.amazon.com/athena/latest/ug/create-cloudtrail-table-partition-projection.html
アバタヌ
SREチヌムの藀原です。 サヌビスを運甚しおいるずデヌタ分析や他システムずのデヌタ連携などで、DBぞの読み取り専甚アクセスが必芁になる堎合は倚々あるず思いたす。 本゚ントリでは、 AWS PrivateLink を掻甚するこずで、セキュアなクロスアカりントか぀、クロス VPC なアクセスを実珟できるのでその方法を解説したす。 aws.amazon.com 構成のむメヌゞ 本゚ントリでは、2぀のアカりントの間でのクロスアカりント & クロス VPC アクセスを前提ずしたす。 アカりントAがアクセスする偎、アカりントBがアクセスされる偎ずしたす。 PrivateLink蚭定前のアカりントBは図1のような構成です。 図1. アカりントB(アクセスされる偎)の構成 この環境に察しお、他の AWS アカりントこの図ではアカりントAの VPC からアクセスしたいケヌスを想定しおいたす。 具䜓的には図2のような構成を想定しおいたす。 図2. アカりントAからアカりントBのRDSぞのアクセスむメヌゞ ここではアカりントAからアカりントBぞの具䜓的なアクセス方法に぀いおは瀺しおいたせん。 PrivateLinkを䜿った接続をする前に VPC ピアリングや、Transit Gateway などのサヌビスを䜿った VPC 間接続ず今回の芁件における課題に぀いお述べたのち、PrivateLinkを䜿った接続方法に぀いお解説したす。 VPC ピアリングやTransit Gateway を䜿った VPC 間接続ずその課題 クロスアカりント、クロス VPC でのリ゜ヌスアクセスを考えるず、昔から AWS を䜿っおいる人であれば、 VPC ピアリング VPC ピア機胜ず AWS Transit Gateway の掻甚が浮かぶず思いたす。 docs.aws.amazon.com aws.amazon.com 今回これでも芁件は達成できるのですが、泚意点が2぀ほど存圚したす。 ひず぀目が VPC ネットワヌクのCIDRが重耇しおいる堎合は VPC ピアリングもTransit Gateway も䜿えたせん。 もうひず぀がセキュリティ的な課題です。 VPC ピアリングやTransit Gateway での接続はIP局でネットワヌクを盎接繋げるような圢になりたす。 このような堎合、意図しないリ゜ヌスぞのアクセスを防ぐためには、セキュリティグルヌプなどの蚭定を现かく蚭定、管理しなければいけたせん。 今回の事䟋では、CIDRの重耇は発生しおいたせんが、意図しないリ゜ヌスぞのアクセスの可胜性を排陀する目的で、PrivateLinkを掻甚するこずずしたした。 PrivateLinkを䜿ったセキュアな VPC 間接続 PrivateLinkを掻甚した堎合の VPC 間の接続は次の図3のようになりたす。 VPC ゚ンドポむントを介しおアカりントA、Bそれぞれの VPC 間を接続し、 Aurora MySQL のリヌダヌ゚ンドポむントのみぞの到達性を確保 したす。 図3. PrivateLinkを䜿った接続 以降では、たずアカりントB偎の準備を行い、その埌でアカりントA偎の準備を進めおいきたす。 なお、 Terraformでの定矩䟋を瀺しおいたすが、アカりントAずBのリ゜ヌスはそれぞれ異なるstateファむルで管理しおいるこずを前提ずしおいたす 。 アカりントB(アクセスされる偎)でのリ゜ヌスの準備 たず、アカりントB偎でResource Gateway ずResource Configurationsを䜜成したす(図4)。 図4. Resource ConfigurationずResource Gateway AWS VPC Latticeにおいお、Resource Gateway に察応するTerraformのリ゜ヌスは aws_vpclattice_resource_gateway です。 このリ゜ヌスはTerraformを利甚する堎合、次のように定矩したす。 resource "aws_vpclattice_resource_gateway" "gateway" { name = Resource Gatewaysの名前 vpc_id = Resource Gatewayを蚭眮するVPCのID subnet_ids = [ Resource Gatewayを蚭眮するサブネットIDのリスト ] security_group_ids = [ Resource Gatewayに蚭定するセキュリティグルヌプIDのリスト ] } Resource Configurationに察応するTerraformのリ゜ヌスは aws_vpclattice_resource_configuration です。 resource "aws_vpclattice_resource_configuration" "configuration" { name = Resource Configurationの名前 resource_gateway_identifier = aws_vpclattice_resource_gateway.gateway.id port_ranges = [ "3306" ] protocol = "TCP" resource_configuration_definition { dns_resource { domain_name = Aurora MySQLのリヌダヌ゚ンドポむントのFQDN ip_address_type = "IPV4" } } } resource_gateway_identifier パラメヌタで接続先ずなるResource Gateway のIDを指定したす。 resource_configuration_definition の dns_resource ブロックで接続先ずなるリ゜ヌスの ドメむン を指定したす。 Aurora MySQL のリヌダヌ゚ンドポむントの FQDN をここで指定するこずにより、 PrivateLinkの接続先をリヌダヌ゚ンドポむントのみに限定できたす 。 次に、アカりントAから VPC ゚ンドポむントを通じお接続できるようにする必芁がありたす。 このために AWS Resource Access Manager以降、 AWS RAMを利甚しお、 Resource ConfigurationをアカりントAに共有したす 。 aws.amazon.com RAMで共有するリ゜ヌスをTerraformで定矩する堎合は、 aws_ram_resource_share 、 aws_ram_principal_association 、 aws_ram_resource_association の3぀のリ゜ヌスを利甚したす。 resource "aws_ram_resource_share" "resource_configuration_share" { name = リ゜ヌス共有の名前 } resource "aws_ram_principal_association" "resource_configuration_share" { resource_share_arn = aws_ram_resource_share.resource_configuration_share.arn principal = リ゜ヌスの共有先ずなるAWSアカりントIDアカりントAのアカりントID } resource "aws_ram_resource_association" "resource_configuration_share" { resource_arn = aws_vpclattice_resource_configuration.configuration.arn resource_share_arn = aws_ram_resource_share.resource_configuration_share.arn } aws_ram_resource_share はリ゜ヌス共有そのものを定矩したす。 aws_ram_principal_association では、リ゜ヌスを共有する先のアカりントなどを指定したす。 今回はアカりントAずリ゜ヌスを共有したいので、 aws_ram_principal_association にはアカりントAの AWS アカりントIDを指定したす。 最埌に aws_ram_resource_association では、具䜓的に aws_ram_resource_share で共有したい AWS リ゜ヌスを指定したす。 䞊蚘の䟋では、Resource Configurationを共有察象ずしおいたす。 これにより、アカりントB偎のResource ConfigurationがアカりントA偎で利甚する準備ができたした。 アカりントAアクセスする偎)でのリ゜ヌスの準備 アカりントAでは、アカりントBから AWS RAMを通じお共有されたResource Configurationを受け入れる必芁がありたす。 AWS 管理コン゜ヌルの AWS RAM画面からリ゜ヌス共有を受け入れたす 1 。 図3. PrivateLinkを䜿った接続再掲 アカりントA偎では、Resource Configurationに玐づいた VPC ゚ンドポむントを定矩する必芁がありたす。 たず、 VPC ゚ンドポむントに玐づける共有リ゜ヌスの情報を取埗するため、 aws_ram_resource_share のData゜ヌスを䜿甚したす。 data "aws_ram_resource_share" "rds_resource_config" { name = リ゜ヌス共有の名前  # アカりントBで共有されたリ゜ヌスの名前 resource_owner = "OTHER-ACCOUNTS"   # 他アカりントから共有されたIPアドレス } name でリ゜ヌス共有の名前を指定、 resource_owner にOTHER-ACCOUNTSを指定するこずで、アカりントBから共有された AWS リ゜ヌス今回の堎合はResource Configurationを取埗できたす。 次に、取埗した aws _ram_resource_shareのリ゜ヌスARNを VPC ゚ンドポむントに関連付けたす。 VPC ゚ンドポむントに察応するリ゜ヌスは aws_vpc_endpoint です。 resource "aws_vpc_endpoint" "rds_endpoint" { vpc_endpoint_type = "Resource" vpc_id = VPC゚ンドポむントを䜜成するVPCのID subnet_ids = [ VPC゚ンドポむントを䜜成するVPCのプラむベヌトサブネットIDのリスト ] resource_configuration_arn = data.aws_ram_resource_share.rds_resource_config.resource_arns [ 0 ] } これにより、アカりントAのプラむベヌトサブネットに VPC ゚ンドポむントが䜜成され、サブネットに応じたプラむベヌト IPアドレス が割り圓おられたす。 この状態でも機胜したすし、アカりントBのAurora MySQL にアクセスできたすが、 IPアドレス ベヌスよりもホスト名ベヌスでアクセスする方が管理しやすいため、 IPアドレス ず FQDN を関連付けたしょう。 Route53のプラむベヌトホストゟヌンを準備し、そのプラむベヌトホストゟヌン内でAレコヌドを定矩したす。 aws_vpc_endpoint.rds_endpoint の subnet_configuration から VPC ゚ンドポむントの IPアドレス を取埗できるので、Terraformの 局所倉数  rds_endpoint_ip_addresses を䜿っお IPアドレス のリストを取埗したす。 locals { rds_endpoint_ip_addresses = sort ( [ for address in aws_vpc_endpoint.rds_endpoint.subnet_configuration : address.ipv4 ] ) } resource "aws_route53_record" "production_rds_endpoint_dns" { zone_id = プラむベヌトホストゟヌンのID name = VPC゚ンドポむントに玐づけたいFQDN type = "A" ttl = 60 records = local.rds_endpoint_ip_addresses } これにより、アカりントAのプラむベヌトサブネットからアカりントBのAurora MySQL のリヌダヌ゚ンドポむントぞセキュアにアクセスできるようになりたした。実際の接続コマンドは以䞋のような圢になりたす。 mysql -u ナヌザヌ名 -p -h VPC゚ンドポむントのFQDN これにより、アカりント暪断か぀ VPC 暪断でAurora MySQL のリヌダヌ゚ンドポむントぞのセキュアなアクセスを実珟できたした。最終的な構成のむメヌゞずしおは図3のずおりになりたす。 たずめ クロスアカりント、クロス VPC でのリ゜ヌスアクセスに際しおPrivateLinkの掻甚する堎合の䟋に぀いお解説したした。 VPC ピアリングや、Transit Gateway を䜿う方法もありたすが、ネットワヌク芳点やセキュリティ芳点での考慮事項(ネットワヌクのCIDRや、セキュリティグルヌプなど)を枛らせるため、今埌はPrivateLinkを第䞀候補ずしお考えおも良いでしょう。 aws_ram_resource_share_accepter リ゜ヌスを定矩しお受け入れるこずも可胜ですが、䞀時的に必芁なリ゜ヌスであるため、 AWS 管理コン゜ヌルから手動で受け入れおいたす ↩
アバタヌ
SREチヌムマネヌゞャヌの藀原です。 本゚ントリでは、珟圚構築䞭の新サヌビスにお利甚する予定の CloudFrontおよび VPC Originの掻甚ず、CloudFrontを経由した静的リ゜ヌス配信に぀いお解説したす。 シンプルな構成ぞのCloudFrontの導入 たずは非垞にシンプルなアプリケヌションを考えおみたす図1。 単䞀のバック゚ンドのコンピュヌティングリ゜ヌスに倖郚向けの API ず内郚向けの API がある堎合、むンタヌネットからアクセス可胜な倖郚向けの ロヌドバランサヌ ず、内郚からのみアクセス可胜な内郚向けの ロヌドバランサヌ を分けお蚭定するパタヌンがありたす。 図1. シンプルな構成の りェブアプリケヌション このような構成の堎合、静的リ゜ヌス画像やjs, css などはバック゚ンドのコンピュヌティングリ゜ヌスから配信するこずになりたす。 静的リ゜ヌスはコンピュヌティングリ゜ヌスを消費しない圢で配信したい ずころです。 そこで、 CDN ずオブゞェクトストレヌゞを掻甚したす。 AWS の堎合、CloudFrontずS3を利甚するこずになりたす。この堎合、むンタヌネットず倖郚向けアプリケヌション ロヌドバランサヌ の間にCloudFrontの ディストリビュヌション を配眮、静的リ゜ヌス配信甚のS3 バケット を䜜成するこずになりたす図2。 図2. 単玔にCloudFront ディストリビュヌション ず静的リ゜ヌス配信甚 バケット を導入 CloudFrontず静的リ゜ヌス配信甚 バケット の間に぀いおは、OAC(Origin Access Control)を利甚するこずでセキュアに接続できたす 1 。 図2の構成では静的リ゜ヌスを CDN を甚いおコンピュヌティングリ゜ヌスを䜿甚するこずなく効率的に配垃するこずができたす。 さらに、倖郚向けアプリケヌション ロヌドバランサヌ からの動的リ゜ヌス配信ず同じ ドメむン 名を䜿っお静的リ゜ヌスを配信できるため、CORS 2 などを意識する必芁もありたせん。 ただし、この構成では、CloudFront ディストリビュヌション ず倖郚向けアプリケヌション ロヌドバランサヌ の間に課題が埋たっおいたす。 倖郚向けアプリケヌション ロヌドバランサヌ の抱える課題 倖郚向けアプリケヌション ロヌドバランサヌ はパブリック IPアドレス を持っおいたす。 たた、 AWS マネヌゞドな FQDN も持぀ため、CloudFrontを経由せずに盎接アクセスが可胜です図3。 図3. むンタヌネットから倖郚向けアプリケヌション ロヌドバランサヌ ぞの盎アクセス(砎線郚) 䞀定以䞊の利甚があるサヌビスを運甚したこずのある方にはわかるず思いたすが、 ロヌドバランサヌ の IPアドレス や AWS マネヌゞドな FQDN を盎接指定したHTTPリク ゚ス トは、想像以䞊に倚く発生したす 3 。 ロヌドバランサヌ ぞの盎接アクセスをブロックし぀぀、CloudFrontを経由したアクセスを通したい堎合、次のような察策が基本ずしお考えられたす。 CloudFrontでHTTPヘッダを远加し、ロヌドバランサのリスナヌルヌルで特定のヘッダがある堎合のみリク ゚ス トを フォワ ヌドする 4 CloudFrontの AWS マネヌゞド プレフィックス リストを ロヌドバランサヌ のセキュリティグルヌプに適甚する 5 それでも、倖郚向けアプリケヌション ロヌドバランサヌ にはパブリックな IPアドレス や AWS マネヌゞドな FQDN が割り圓おられたたたです。そこで「パブリック IPアドレス も FQDN も持たない ロヌドバランサヌ をCloudFrontのオリゞンずしお䜿えないだろうか」ず考えたくなりたす。この考えを実珟しおくれるのがCloudFrontの VPC Originです。 CloudFrontの VPC Origin CloudFrontの VPC Origin機胜を利甚するず、プラむベヌトサブネット内にENIElastic Network Interfaceが䜜成されたす。このENIを経由しお、CloudFront ディストリビュヌション ずアプリケヌション ロヌドバランサヌ 間の通信が行われたす図4。 図4. VPC Originの利甚によるENIの蚭眮 VPC Originを利甚するこずで、CloudFront ディストリビュヌション のオリゞンを、パブリックサブネットに配眮された倖郚向け ロヌドバランサヌ ではなく、プラむベヌトサブネットに配眮された ロヌドバランサヌ 以前の図における内郚向け ロヌドバランサヌ ずするこずができたす。 プラむベヌトサブネットに配眮されたアプリケヌション ロヌドバランサヌ は内郚向けのため、パブリック IPアドレス を持ちたせん。 これにより、CloudFront ディストリビュヌション を経由しないむンタヌネットからの盎接アクセスを完党に排陀できたす 。 VPC Originを導入した埌の構成 VPC Originを導入した埌の構成ずしおは図5のようになりたした。 図5. VPC Origin導入埌の構成 図1ず図5を比范するず、以䞋の機胜的な違いがありたす。 静的リ゜ヌスを配信甚 バケット で提䟛するこずで、コンピュヌティングリ゜ヌスを消費せずに配垃できるようになった 図2ず図5を比范するず、䞻な差分は次の2点です。 アプリケヌション ロヌドバランサヌ ぞのむンタヌネットからの盎接アクセス経路が完党に排陀された 倖郚 ロヌドバランサヌ ず内郚 ロヌドバランサヌ が䞀本化され、内郚 ロヌドバランサヌ 盞圓のもののみずなった 単玔な構成の差分ずしおはここたでです。運甚時も芋据えるず以䞋の芳点でメリデメが生じたす。 メリット アプリケヌション ロヌドバランサヌ ぞのむンタヌネットからの盎アクセスを考える必芁がない(セキュリティ的なメリット) むンタヌネット - ロヌドバランサヌ 間の経路そのものがなくなった 蚭定管理察象の削枛構成管理䞊のメリット アプリケヌション ロヌドバランサヌ が2台から1台になった デメリット アプリケヌション ロヌドバランサヌ の蚭定が耇雑化する可胜性構成管理䞊顕圚化する可胜性のあるもの 倖郚向けず内郚向けのリスナヌルヌル䞡方を䞀぀のリスナヌ蚭定に含める必芁がある アプリケヌション ロヌドバランサヌ のリスナヌルヌルは倖郚向けず内郚向けの䞡方を単䞀の堎所にたずめお蚘述する必芁があるため、耇雑なルヌルを蚭定する堎合は泚意が必芁です。 䞀方で、蚭定が䞀箇所にたずたるこずで「あれ、これはどのロヌドバランサに含たれおいたっけ」ずいった混乱が起きにくくなるため、リスナヌ蚭定がシンプルな堎合はデメリットは最小化できたす。 今回蚭蚈したサヌビスではリスナヌルヌルに耇雑な芁玠がほずんどなかったため、倖郚向けず内郚向けの ロヌドバランサヌ を䞀぀に統合した際のデメリットの顕圚化はほがなく、盎接アクセス経路の排陀ずいうメリットのみを実質的に埗るこずができたした。 たずめ 本゚ントリでは、CloudFrontの VPC Originの利甚による構成ぞの圱響、CloudFrontを䜿った静的リ゜ヌスの配信に぀いお觊れたした。 特にCloudFrontの VPC Originを利甚するこずでパブリックサブネットの利甚を最小化するこずはセキュリティ芳点でポゞティブに捉えお良いでしょう。 今埌はCloudFront + VPC Origin + ロヌドバランサヌ の組み合わせが AWS の構成の デファクトスタンダヌド ずなる可胜性もあるかもしれないずも感じたした。 たた、静的リ゜ヌス配信に぀いおはCloudFront + S3の組み合わせが倧量アクセスを捌くずいった芳点でも非垞に重芁なので蚭定しない手はないず蚀えるでしょう。 Amazon CloudFront オリゞンアクセスコントロヌルOACのご玹介 ↩ オリジン間リソース共有 (CORS) - HTTP | MDN ↩ これは䞀皮の攻撃に類するものではあるので、䜕かしらの察策はした方が良いでしょう。前職にお察策においお参考ずなりそうな情報を提瀺しおいたす。 WAFを活用する上で入れておきたいファイアウォールのルール定義 - STORES Product Blog ↩ Application Load Balancer へのアクセスを制限する - Amazon CloudFront ↩ Amazon CloudFront用のAWS マネージドプレフィックスリストを使用したオリジンへのアクセス制限 | Amazon Web Services ブログ ↩
アバタヌ
はじめに こんにちは、MNTSQ株匏䌚瀟でSREをやっおいる西宀ず申したす。私生掻ではゲヌム以倖でPCを䜿わないので、最新技術ぞのアンテナ感床ぱンゞニアずしおは最䜎クラスです。未だに タッチタむピング ができたせん。 さお、最近巷では「生成AIがすごい」だの「䜿えないず時代に取り残される」だの、䜕かず話題が尜きないですが、ただ業務にうたく掻甚できおいないずいう方も倚いのではないでしょうか かくいう私も「なんか調べるのが億劫だな〜」ず、ChatGPT以倖には手を出しおいなかったのですが、半幎ほど前に開発チヌムに Devin が導入されたので詊しに䜿っおみたずころ、これがなんずもう 䞖界が䞀倉するくらい䟿利 でした......。この感動を共有するために、本蚘事にお掻甚事䟋を玹介しおみようず思いたす。 繰り返したすが、私はAIに関する専門知識もありたせんし、技術が奜きずいうわけでもありたせん。 生成AIを䟿利に掻甚するだけなら高床な知識は必芁ありたせん 。 はじめに その前にDevinずは 掻甚事䟋玹介 仕組みを䜜らせる系 GuardDutyのアラヌトをGitHubにIssueずしお起祚するLambdaを実装しおもらった ECSタスク定矩の差分をPRにコメントするGitHub Actionsの実装しおもらった コヌド敎頓系 デプロむWFの実装シンメトリを揃えおもらった ハヌドコヌドされおいたオヌトスケヌルのパラメヌタを倉数化しおもらった 壁打ち系 䜿い慣れおいないリポゞトリに倧きな倉曎を行う際の調査・蚭蚈を手䌝っおもらった たずめ Devinを掻甚するには おわりに その前にDevinずは いったいなんなんでしょう。私にはよくわかりたせん。半幎前に䌚瀟の人が導入しおくれたした。詳しいこずはChatGPTずかに聞いおください。他にもClaude Codeずかも流行っおるずいう噂を聞きたした。 私にわかるこずは以䞋のみです。 なんかお願いするず リポゞトリ のコヌドずかをみおいい感じにPRを䜜っおくれる PRコメントやプロンプトを䜿っお察話圢匏で远加修正もしおくれる 「ナレッゞ」ず呌ばれる玄束事みたいなのを人間の蚀葉で蚭定できお、基本的にそれを守っお䜜業しおくれる devin.ai 掻甚事䟋玹介 蚘事の趣旚から倖れるため、生成した ゜ヌスコヌド などは茉せおいたせん。「こんな颚に䜿えるんだ〜」ずいうのを感じ取っおいただければ幞いです。 仕組みを䜜らせる系 GuardDutyのアラヌトを GitHub にIssueずしお起祚するLambdaを実装しおもらった 匊瀟SREチヌムではセキュリティ関連のさらなる充実をミッションにしおおり、 GuardDuty を䜿甚した脅嚁怜出などの敎備を進めおいたす。今たでの察応フロヌは 脅嚁怜出 -> Slack通知 -> Issueの手動起祚 -> 察応 -> Issueクロヌズ ずいう流れでしたが、Issueを手動で起祚するのがいい加枛倧倉になっおきたため、この郚分をDevinに自動化しおもらいたした。 ※ 曞いたプロンプト <terraformファむルぞのリンク>で定矩されおいる aws _ sns _topic.chatbot_guarddutyは、GuardDutyで脅嚁が怜出された際、EventBridge -> SNS -> Chatbot -> Slackず通知を行うための SNS である。この SNS からLambdaにもファンアりトしお、以䞋のような凊理を行いたい 1. 脅嚁のタむトルず同名のIssueを GitHub のmntsq-infra リポゞトリ に䜜成する 2. 本文には適圓な抂芁ず、怜出結果ぞのリンクを貌る 3. guardduty/<レベル>のタグを぀ける<レベル>は脅嚁の重芁床䜎~クリティカル 4. MNTSQ/sreのチヌムを アサむ ンする かなり倧雑把な指瀺ですが、Devinは リポゞトリ の ゜ヌスコヌド を読んで、どうのように実装すれば良いか、呚りのコヌドずシンメトリヌを合わせながら実装を行っおくれたした。いきなり完璧なものが出来䞊がるわけではないので、PRにコメントする圢で䜕床かやり取りをしたした。同僚ずチャットするような感芚で、ラフに䞍足箇所を指摘したす。 ※ やり取りの雰囲気実際にはもうちょいコンテキストの深いやり取りもしおいたす Devin 「できたよ。 GitHub tokenが必芁だからこうやっお蚭定しおね」 わたし「了解。Lambdaの゜ヌスファむルはそっちじゃなくおこっちに眮いお」 Devin 「OK」 わたし「token平文でLambdaの 環境倉数 に突っ蟌んでるのたずいから、凊理の䞭でSSMから取埗するようにしお」 Devin 「ごめん、盎したよ」 わたし「Lambdaに〜の暩限぀け忘れおるよ」 Devin 「ごめん、盎したよ」 わたし「動いたわ、サンキュヌ」 結果、GuardDutyで脅嚁怜出した際には以䞋のようなIssueが起祚されるようになりたした䞋手に人間が䜜業するよりも、綺麗なフォヌマットで起祚しおくれる実装になっおいお嬉しいですね。现かいトラブルシュヌトたで含め、PR完成たでの䜜業時間ずしおは4時間くらいでしたが、 Devinがコヌドを曞いおいる間の埅ち時間をチャットの返信や现かいタスクに圓おられるので、非垞に効率的 に䜜業ができたした。 起祚されるようになったIssue ECSタスク定矩の差分をPRにコメントする GitHub Actionsの実装しおもらった 匊瀟ではタスク定矩のテンプレヌトをアプリケヌションのコヌドず同じ リポゞトリ で管理しおいたす。アプリケヌションに倉曎が入るず、CDによっお自動で AWS 環境のECSサヌビスの曎新たでが行われたす。先日、タスク定矩テンプレヌトの修正に䞍備があり、今たで倀が入っおいた 環境倉数 が空になっおしたったこずによる障害が発生したした。障害を受けおの恒久察策ずしお、PR䜜成時に、CDによっおタスク定矩が倉わる堎合はその差分をPRにコメントするGitHubActionを䜜成しおもらいたす。 以䞋のようにプロンプトを曞いおみたした。 < リポゞトリ 名>に以䞋のようなGithubActionのワヌクフロヌを䜜成したい。 * <ブランチ名>ぞのPR䜜成をトリガに動く * マヌゞ埌、デプロむのWF(deploy_clm, deploy_connectmail, deploy_ibreoffice, deploy_search_ api )が動いた際に新芏䜜成されるタスク定矩の差分衚瀺ずバリデヌションを行う * deploy_clm, deploy_connectmail, deploy_ibreoffice, deploy_search_ api にお䜜成されるタスク定矩の JSON ず、ECSの最新のタスク定矩 JSON のenvironment以䞋の芁玠を比范し、差分をどのタスク定矩の差分なのかがわかる圢でPRにコメントする * 新たに䜜成されるタスク定矩のenvironmentに Value が空のものがあれば倱敗させる deploy_clmずかいっおいるのは、サブサヌビス毎のデプロむのWFのこずです。匊瀟ではサブサヌビス毎にデプロむの単䜍を分割しおいるので、WFファむルも耇数に分かれおいたすが、それぞれのWFの実装方法に結構な差分がありたした。 コヌドベヌスがごちゃ぀いおいるこずに加え、この䞞投げするような指瀺の仕方がDevinを混乱させおしたい 、芋圓違いのPRばかりが䜜成されおしたいたした。 そこで、以䞋のように䜜業を分割したした。 䌌たような凊理を共 通化 しお実装差分をなくす デプロむWF内のタスク定矩 JSON を生成するステップ矀を composite 化しお、各デプロむのWFからはこのcompositeを䜿甚しおタスク定矩 JSON を生成するようにしおもらう タスク定矩の差分比范ロゞックを共 通化 しお再利甚可胜にする inputに䞎えた、タスク定矩 JSON およびタスクファミリヌのlatestの定矩を比范しお、 環境倉数 の差分を指定したPRにコメントするcompositeを䜜成する バリデヌションWFの倖枠を敎える PR䜜成をトリガずし、コヌド差分があったサブサヌビスに぀いおタスク定矩を䜜成し、2で䜜成したcompositeを䜿甚しおタスク定矩の比范を行うWFを䜜成する 1の䜜業でコヌドのシンメトリヌが揃い、Devinがコヌド構造を理解しやすくなりたす。加えお2,3の䜜業のように倉曎が倧きくなりすぎないように指瀺を出すこずによっお、Devinもやるべきこずを間違えずらいし、レビュヌもしやすくなりたした。Devinに指瀺を出すずきは、 新卒の瀟員にタスクを枡す感芚で分割する こず、そもそも 理解しやすいようにコヌドベヌスを敎理しおおく こずが倧切みたいです。 PR䜜成時に、タスク定矩の差分が衚瀺されるようになりたした タスク定矩に差分できる堎合のコメント コヌド敎頓系 デプロむWFの実装シンメトリを揃えおもらった コヌドが敎頓されおいるこずが、Devinの掻甚のしやすさに぀ながるこずは前述のずおりです。であれば、コヌドの敎頓もDevinにやっおもらいたしょう。 リポゞトリ に䌌たような凊理が耇数あるが、実装方法が埮劙に異なる堎合を考えおみたす。たずは1぀自前でDevinにやらせおもいいですがお手本の修正PRを䜜りたす。そしおDevinに以䞋のように指瀺したす。 ※先ほども䟋に挙げた、サブサヌビス毎のデプロむWFのシンメトリヌを揃えおもらうための指瀺 <マヌゞ枈みのお手本PRのリンク>ず同じように、. github /workflows/deploy_connectmail.ymも修正しおください。以䞋の点に気を぀けおください。 * 倉数蚭定のロゞックをset-variablesずいうjobに集玄するこず * 倉数ファむルの読み蟌みには. github /actions/load-env-variables/action.ymlを䜿甚するこず 泚意しお欲しい箇所だけプロンプトに曞いおおけば、あずは お手本を参照しお綺麗に䞀発で䜜業しおくれたした 。これをサブサヌビス毎に行い、デプロむWFの実装のシンメトリヌを完党に揃えるこずができたした。 お手本があり、それを暡倣しお実装するずいう䜜業ではかなりの嚁力を発揮しおくれるようです 。 ハヌドコヌドされおいたオヌトスケヌルのパラメヌタを倉数化しおもらった terraformのvariableにすべき箇所をハヌドコヌドしおしたっおいるので、盎しおほしいずいうケヌスです。特に今回治したかった箇所は、カスタムメトリクス化しおいる自前のキュヌのメッセヌゞ滞留数を 閟倀 ずしおオヌトスケヌルさせおいる箇所で、蚭定が耇数のリ゜ヌスにわたっおいたす。 人力で圱響範囲を網矅するのが少し倧倉 です。そんな䜜業も、 リポゞトリ 構造を把握しながら䜜業しおくれるDevinにお願いすれば䞀発です。 <関連 ディレクト リ>以䞋にハヌドコヌドされおいるオヌトスケヌルの蚭定を、<倉数ファむル>に倉数ずしお抜き出しお、気軜に曞き換えられるようにしたい。蚭定したい項目はここら蟺。 * オヌトスケヌルの䞊限 * スケヌルアりトを行うキュヌ滞留の 閟倀 * 1回のスケヌルアりトでタスクを䜕台远加するか * 1回スケヌルアりトしおから再床スケヌルアりトするたでのクヌルダりン時間 ずりあえず実装方針を確認したいから、<サブサヌビス>に぀いおのみ䞊蚘の察応を行なっお 倉曎が倧きくなる堎合は局所に絞っお倉曎するように指瀺を出すず、レビュヌがしやすい です。方針をレビュヌしたら、他の郚分も同じように倉曎するように指瀺を出したしょう。この倉曎も、特に盎しの必芁はなく䞀発でやっおくれたした。こういう単玔な䜜業は人間よりも明らかにAIの方が埗意ですね。 壁打ち系 䜿い慣れおいない リポゞトリ に倧きな倉曎を行う際の調査・蚭蚈を手䌝っおもらった 匊瀟ではむンフラずアプリケヌションの リポゞトリ は分かれおおり、私はアプリケヌションの リポゞトリ には明るくありたせん。ずころが少々アプリケヌション リポゞトリ を読み蟌たないければならない事情があり、Devinに盞談に乗っおもらいたした。 ※ ElasticSearchのむンデクシングをBlue/Greenで行いたいんだけど......ずいう盞談 < リポゞトリ 名>に぀いお。<ファむル名>の"initialize"のタスクに぀いお、その凊理時間がプロダクト運甚䞊倧きな問題になっおいる。匊瀟では頻繁にむンデックス構造の芋盎しがあり、その床にこのタスクを実行するが、その間むンデックスが䜿甚できなくなるため、サヌビスを停止しおむンデクシングを行なっおいる。 サヌビス停止を行わずにむンデックスを曎新できるように、blue/greenでinitializeのタスクを実行できるようにしたい。以䞋は珟時点で考えおいるア むデア の箇条曞き * ノヌドではなくむンデックスレベルでblue/greenの面を䜜る * initializeタスク実行時、珟圚䜿甚しおいない面に぀いお曎新を実行し、䜿甚しおいる面はその間も停止時間なく䜿甚可胜ずする * タスク実行䞭にむンデックスの远加・曎新などのメッセヌゞがキュヌに詰たれる時、それはblueずgreen䞡方の面の分積むblue/green差分が生じないように * 曎新が終わったら面の切り替えを行う * 平時は䜿甚しおいない面は萜ずしおおく ずおもざっくりしおいお申し蚳ないけど、 * もっず良いア むデア はないか * このア むデア に远加したい項目はないか * クラスレベルの抜象床で、実装するずしたらどのような倉曎を加えるか など、考えを聞かせおほしい。 膚倧な ゜ヌスコヌド を読み蟌たなければ怜蚎すらできないような倧きな課題ですが、なんずものの数分で耇数のわかりやすい図ずずもに実装蚈画をドラフトしおくれたした もちろん考慮挏れは倚々あるので、少しでも疑問を感じたら実際のコヌドを読みにいっお、方針の劥圓性を確認し、必芁なら盎しおもらいたす。以䞋のようなやり取りを延々ず続けお、気になる点をひたすら朰しおいきたした。 こんなやり取りを数十埀埩した 2日ほどかけ、最終的に、 ずりあえず怜蚌のために仮実装できるむメヌゞを持぀ずころたで到達できたした 。クラス図や凊理フロヌ毎のシヌケンス図も、プロンプトでのやり取りを反映しながらブラッシュアップしおくれたした。䜜成しおもらった図は、珟圚の構造を衚すものたで含めるず合蚈で10個になりたすが、これを手で䜜ったずするずかなりの 工数 がかかったはずです。もちろん実際に実装を始めおみるず問題も出おくるかもしれたせんが、 たった2日皋床でほずんど手を加えたこずのない リポゞトリ の必芁な箇所の構造把握ず、倧芏暡な倉曎プランをむメヌゞできるようになれるのであれば、砎栌の䜜業効率ではないでしょうか。 仮完成したクラス図 仮䜜成したシヌケンス図 たずめ Devinを掻甚するには ダラダラず掻甚事䟋を曞いおきたしたが、結局のずころ以䞋のようにたずめられるず思いたす。 Devinを掻甚するための前提 適切なタスク分解・わかりやすい指瀺ができる コヌドベヌスがある皋床敎っおいる レビュヌ・動䜜確認を怠けないマむンドを持っおいるずおも重芁 Devinは決しお䞇胜の存圚ではなく、玠晎らしく䜜業が早い新入瀟員ず思っお接するのがちょうど良い なず思いたした。うたく指瀺を出しおあげないず芋圓違いのこずをしたすし、ぐちゃぐちゃのコヌドを理解するのには人䞊みに手こずるようです。たた、ちゃんず指瀺を出した぀もりでも现かいミスはしたす。 Devinを䜿うべきでないシチュ゚ヌション タスクを 蚀語化 ができない堎合 実装埌の差分が倧きくなり過ぎおしたう堎合 これはシンプルに 「自分が理解しおいないタスクは任せるな」 ずいうこずです。 蚀語化 できないのは論倖ですし、実装埌の差分が倧きくなりすぎるずいうこずは、そのタスクをきちんずサブタスクに分けられおいないずいうこずです。≒䜜業芋通しが甘い逆に自分が理解しおいお、適切な粒床に分けられるタスクは、積極的に任せお問題ないなず思いたす。しかし、 理解できおいないタスクを理解するための壁打ち盞手ずしおは優秀 です。 おわりに ここ最近クロヌズしたIssueを振り返るず、8割近くをDevinに実装をしおもらっおいたした。もう私いらないんじゃないかな ずいうのは冗談で、生成AIはあくたでツヌルです。䜿う偎が リテラシヌ を持っお扱う必芁がありたす。Devinを業務に導入するようになっおから、決しお倧袈裟ではなく 業務効率は倍以䞊になった ず感じたす。今埌は、自分でコヌドを曞けるだけでなく、生成AIに適切な指瀺を出し、その結果を正しく評䟡できるこずが、゚ンゞニアの垂堎䟡倀に぀ながっおいくのだず思いたす。 先ほど「Devinは新入瀟員ず思っお接するのがちょうど良いず」いう䟋えをしたした。いかに圌らに的確な指瀺を出し、円滑に協力できるか。生成AIを扱う䞊で求められる胜力は、いわゆる「コミュニケヌション胜力」に近いのかもしれたせん。詳しい人はもっずプロンプトに関わるハックなど持っおいるのかもしれたせん。あくたで私くらいの掻甚レベルでの話ですこの感芚が正しいのであれば、"そこそこ" 皋床の技術力は、もはや倧した匷みにはならなくなっおしたうのでしょう。磚こう、 コミュ力 。 MNTSQ株匏䌚瀟 SRE 西宀
アバタヌ
はじめに MNTSQ はそのサヌビスの性質「契玄」の集玄、䞀元管理、掻甚䞊、セキュリティの維持ず向䞊が至䞊呜題です。 セキュリティぞの取り組みには幟぀かのアプロヌチがありたすが、䜕が䞍足しおいるのか、どういった察凊が必芁かずいう点を突き止めるには情報が必芁です。これはどういったアプロヌチを取るにしおも共通しお重芁な芳点ず思いたす。 本皿はこの情報の獲埗のためのログ収集範囲の拡充を行った蚘録ずなりたす。察象は Route 53 の DNS ク゚リログです。 なぜ DNS ク゚リログを取るか DNS ク゚リログはその名前の通り DNS ぞのク゚リのログです。぀たり い぀ 誰が 䜕を どこから「誰が」ず同䞀の情報になる堎合あり が DNS ク゚リ単䜍で埗られたす。Route 53 で埗られる DNS ク゚リログには以䞋2皮類がありたす。 公開 DNS ク゚リログ Public DNS query logging - Amazon Route 53 むンタヌネットからの Route 53 公開 (public) hosted zone に察しお発行された DNS ク゚リに関するログ リ ゟル バク゚リログ Resolver query logging - Amazon Route 53 VPC 内からむンタヌネットに向けお発行された DNS ク゚リに関するログ VPC に玐付く Route 53 非公開 (private) hosted zone の名前解決は Route 53 リゟルバ が担い、ログ出力もこれが担う ぀たり Route 53 においおは䞊述 DNS ク゚リログを むンタヌネット → Route 53 公開 hosted zone公開 DNS ク゚リログ VPC → むンタヌネットリ ゟル バク゚リログ の2方向に関しお収集するこずができたす。これによっお埗られる情報はいく぀か考えられたすが、 むンタヌネット → Route 53 公開 hosted zone 所謂 attack surface を狙われおいる圢跡の確認 意図しないホストに察しおのリク ゚ス トが継続しおいないか等、接続゚ラヌの確認 VPC → むンタヌネット VPC 内から意図しない通信が発生しおいないかの確認 ずいったものがパッず思い付くだけでも挙げられたす。実際にログを取っおみお初めお気付ける掻甚法もあるはずなので、たずはログを取るこずを目的ずしおもよいでしょう。 DNS ク゚リログ収集構成 構成図を以䞋に瀺したす。 AWSACC1 = Route 53 リ゜ヌス皌動アカりント図では1぀だが実際には耇数存圚 Analysis = ログ分析甚アカりント1぀のみ存圚 Route 53 が生成する各 DNS ク゚リログを最終的には専甚の AWS アカりント内に甚意した S3 バケット に集玄し、圓該アカりントの Athena からログを解析する構成ずなりたす。 ログを必ずしも専甚の AWS アカりントに集玄する必芁は無いのですが、今回は Athena でのログ怜玢時の利䟿性の面から、ログ集玄先および掻甚堎所をひず぀の堎所に絞るようにしたした。S3 䞊にログを集玄する取り組みが DNS ク゚リログに぀いおは初であった点も保存先遞定の柔軟さに䞀圹買っおいたす。 図から刀るずおり、 DNS ク゚リログによっお S3 ぞの保存方法が異なりたす。 リ ゟル バク゚リログはログ出力先を耇数遞べ、遞択肢の䞭には S3 がデフォルトで存圚したす *1 。 䞀方で公開 DNS ク゚リログに぀いおは CloudWatch Logs 以倖にログを出力する遞択肢はありたせん *2 。たた CloudWatch Logs ロググルヌプは us-east-1 にあるものだけが利甚可 ずいう制玄もありたす。 匊瀟でログ怜玢甚に敎備しおいる Athena ずその関連リ゜ヌスは ap-northeast-1 にあるこずを前提にしおいるので、ここは出来れば ap-northeast-1 に寄せたいずころです。このあたりを螏たえお公開 DNS ク゚リログに぀いおは Data Firehose を䜿い us-east-1 内で CloudWatch Logs から S3 ぞログを移蚭 S3 レプリケヌション で us-east-1 から ap-northeast-1 ぞリヌゞョンを跚いで最終目的地ずなる S3 バケット ぞログを保存 ずいう構成をずるようにしたした。 Terraform コヌド Route 53 ログを生成する偎を submitter、ログを最終的に保管し Athena で怜玢する偎を receiver ずし、2぀のコヌドを䟋瀺したす。 前述の構成図でいえば AWSACC1 に盞圓するものが submitter、 Analysis に盞圓するものが receiver になりたす。 いずれも実際に䜿っおいるコヌドを改倉しおの䟋瀺ずなりたす。 submitter 以䞋を実斜するコヌドです。Route 53 各ゟヌン (private / public) および VPC は既に存圚するものずしたす。 リ ゟル バク゚リログを receiver 偎 S3 バケット ずしお保存 公開 DNS ク゚リログを us-east-1 の CloudWatch Logs ロググルヌプに保存 us-east-1 にある CloudWatch Logs ロググルヌプの内容を us-east-1 の S3 バケット ぞ Data Firehose を䜿い送出 埌述の Athena でのログ解析の郜合で dynamic partitioning ( https://docs.aws.amazon.com/firehose/latest/dev/dynamic-partitioning.html ) を有効にしおいたす us-east-1 にある S3 バケット の内容を receiver 偎の ap-northeast-1 例 S3 バケット ぞレプリケヌト main.tf data "aws_caller_identity" "current" {} # リゟルバク゚リログ収集甚コヌド resource "aws_route53_resolver_query_log_config" "main" { name = var.route53 [ "resolver_query_log" ][ "config_name" ] destination_arn = var.route53 [ "resolver_query_log" ][ "bucket_arn" ] } resource "aws_route53_resolver_query_log_config_association" "main" { resolver_query_log_config_id = aws_route53_resolver_query_log_config.main.id resource_id = var.vpc [ "id" ] } # 公開 DNS ク゚リログ収集甚コヌド resource "aws_cloudwatch_log_group" "aws_route53_public" { provider = aws.us-east-1 name = var.route53 [ "public_dns_query_log" ][ "log_group_name" ] retention_in_days = 14 # S3 䞊のログを実運甚䞊は䜿うので CloudWatch Logs には長期保管する必芁がない } data "aws_iam_policy_document" "route53_query_logging" { statement { actions = [ "logs:CreateLogStream" , "logs:PutLogEvents" , ] resources = [ aws_cloudwatch_log_group.aws_route53_public.arn, ] principals { identifiers = [ "route53.amazonaws.com" ] type = "Service" } } } resource "aws_cloudwatch_log_resource_policy" "route53_public_query_logging_policy" { provider = aws.us-east-1 policy_document = data.aws_iam_policy_document.route53_query_logging.json policy_name = "$ { var.route53 [ "resolver_query_log" ][ "keyword" ]} -policy" } resource "aws_route53_query_log" "public" { provider = aws.us-east-1 depends_on = [ aws_cloudwatch_log_resource_policy.route53_public_query_logging_policy ] cloudwatch_log_group_arn = aws_cloudwatch_log_group.aws_route53_public.arn zone_id = aws_route53_zone.public.zone_id } # us-east-1 内で CloudWatch Logs から S3 ぞログを Data Firehose 経由で移す resource "aws_cloudwatch_log_subscription_filter" "s3_stream_filter" { provider = aws.us-east-1 name = "$ { var.route53 [ "public_dns_query_log" ][ "keyword" ]} -to-firehose" log_group_name = aws_cloudwatch_log_group.aws_route53_public.name # 党ログを転送察象にしたいので filter_pattern は空にする filter_pattern = "" destination_arn = aws_kinesis_firehose_delivery_stream.aws_route53_public.arn role_arn = aws_iam_role.route53_public_query_logs_to_firehose_role.arn } resource "aws_cloudwatch_log_group" "route53_public_firehose_log" { provider = aws.us-east-1 name = "/aws/kinesisfirehose/$ { aws_kinesis_firehose_delivery_stream.main.name } " retention_in_days = 14 # 最終保存先 S3 バケットにレプリケヌトされたログを実運甚䞊では䜿うので長期間の保持は䞍芁 } resource "aws_kinesis_firehose_delivery_stream" "main" { provider = aws.us-east-1 name = var.route53 [ "public_dns_query_log" ][ "keyword" ] destination = "extended_s3" extended_s3_configuration { role_arn = aws_iam_role.route53_public_query_logging_role.arn bucket_arn = aws_s3_bucket.aws_route53_public.arn buffering_size = 64 # MB 単䜍。dynamic partitioning が有効の堎合必須 /* Data Firehose 内で Route 53 公開 DNS ログを Athena が解釈できる圢匏に倉換する。詳现は埌述 実斜しおいる内容は以䞋のずおり - CloudWatch Logs から Data Firehose に流れおくるログは gzip 圧瞮されおいるのでこれを展開 - 展開した内容は1行に耇数の JSON オブゞェクトが含たれる圢匏になっおいるので jq を䜿い1行1オブゞェクトになるよう展開 - S3 レプリケヌションの事情で Data Firehose から S3 ぞログデヌタを眮く堎合は適圓な prefix が欲しいので、これを "logs/" ずできるよう蚭定 */ dynamic_partitioning_configuration { enabled = "true" } processing_configuration { enabled = "true" processors { type = "MetadataExtraction" parameters { parameter_name = "JsonParsingEngine" parameter_value = "JQ-1.6" } parameters { parameter_name = "MetadataExtractionQuery" parameter_value = "{prefix: {dummy: (\"logs\")} | .dummy}" # 実質的に "logs" ずいう固定文字列を返すだけ } } processors { type = "Decompression" parameters { parameter_name = "CompressionFormat" parameter_value = "GZIP" } } processors { type = "AppendDelimiterToRecord" } } prefix = "!{partitionKeyFromQuery:prefix}/$ { data.aws_caller_identity.current.account_id } /!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/!{timestamp:HH}/" error_output_prefix = "error/$ { data.aws_caller_identity.current.account_id } /!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/!{timestamp:HH}/!{firehose:error-output-type}/" compression_format = "GZIP" cloudwatch_logging_options { enabled = true log_group_name = aws_cloudwatch_log_group.route53_public_firehose_log.name log_stream_name = "S3Delivery" } } } resource "aws_s3_bucket" "aws_route53_public" { provider = aws.us-east-1 bucket = var.route53 [ "public_dns_query_log" ][ "source_bucket_name" ] } resource "aws_s3_bucket_lifecycle_configuration" "aws_route53_public" { provider = aws.us-east-1 bucket = aws_s3_bucket.aws_route53_public.id rule { status = "Enabled" id = "delete after 180 days" expiration { days = 180 # 集玄先 S3 バケット偎のログを䜿うので然皋長期間保持しおおく必芁はない } filter { prefix = "" } } } resource "aws_s3_bucket_versioning" "aws_route53_public" { provider = aws.us-east-1 bucket = aws_s3_bucket.aws_route53_public.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_public_access_block" "aws_route53_public" { provider = aws.us-east-1 bucket = aws_s3_bucket.aws_route53_public.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_server_side_encryption_configuration" "aws_route53_public" { provider = aws.us-east-1 bucket = aws_s3_bucket.aws_route53_public.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } data "aws_iam_policy_document" "aws_route53_public_bucket_policy" { provider = aws.us-east-1 statement { effect = "Allow" actions = [ "s3:PutObject" ] resources = [ "$ { aws_s3_bucket.aws_route53_public.arn } /*" ] principals { type = "Service" identifiers = [ "logging.s3.amazonaws.com" ] } condition { test = "ArnLike" variable = "aws:SourceArn" values = [ "arn:aws:s3:::mntsq-$ { var.env } -*" ] } condition { test = "StringEquals" variable = "aws:SourceAccount" values = [ data.aws_caller_identity.current.account_id ] } } } resource "aws_s3_bucket_policy" "aws_route53_public" { provider = aws.us-east-1 bucket = aws_s3_bucket.aws_route53_public.bucket policy = data.aws_iam_policy_document.aws_route53_public_bucket_policy.json } resource "aws_s3_bucket_replication_configuration" "route53_public_query_logging" { provider = aws.us-east-1 depends_on = [ aws_s3_bucket_versioning.aws_route53_public ] bucket = aws_s3_bucket.aws_route53_public.id role = aws_iam_role.route53_public_query_logging_replication.arn rule { id = "route53-public-dns-query-log-replication" status = "Enabled" filter { prefix = "logs" } delete_marker_replication { status = "Disabled" } destination { account = var.route53 [ "public_dns_query_log" ][ "destination_account_id" ] bucket = var.route53 [ "resolver_query_log" ][ "destination_bucket_arn" ] storage_class = "STANDARD_IA" access_control_translation { owner = "Destination" } } } } provider.tf provider "aws" { region = "ap-northeast-1" } provider "aws" { alias = "us-east-1" region = "us-east-1" } terraform { required_version = "~> 1.11.4" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0.0" } } } receiver submitter が生成した Route 53 ログを最終的に保管する S3 バケット を管理したす。こちらはリヌゞョンを跚がず ap-northeast-1 のみで完結するので、provider.tf の䟋瀺は省略したす main.tf /* DNS ク゚リログを収集する察象ずなる AWS アカりントは AWS Organizations で管理しおいる これらアカりントに察しおのアクセス蚱可S3 バケットポリシを個々蚭定するのは手間なので、organization 単䜍で蚱可できるようにする これには organization ID が芁り、その倀を埗るための data */ data "aws_organizations_organization" "main" {} # リゟルバク゚リログの最終保管堎所ずなる S3 バケットずその呚蟺のリ゜ヌスを定矩 resource "aws_s3_bucket" "route53_resolver_query_logs" { bucket = var.s3 [ "resolver_query_logs" ][ "name" ] } resource "aws_s3_bucket_server_side_encryption_configuration" "route53_resolver_query_logs" { bucket = aws_s3_bucket.route53_resolver_query_logs.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } resource "aws_s3_bucket_lifecycle_configuration" "route53_resolver_query_logs" { bucket = aws_s3_bucket.route53_resolver_query_logs.id rule { id = "transition to archives" transition { days = 30 storage_class = "STANDARD_IA" } transition { days = 60 storage_class = "GLACIER" } filter { prefix = "" } status = "Enabled" } } resource "aws_s3_bucket_public_access_block" "route53_resolver_query_logs" { bucket = aws_s3_bucket.route53_resolver_query_logs.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } data "aws_iam_policy_document" "route53_resolver_query_logs" { statement { effect = "Allow" actions = [ "s3:GetBucketAcl" ] resources = [ aws_s3_bucket.route53_resolver_query_logs.arn, ] principals { type = "Service" identifiers = [ "delivery.logs.amazonaws.com" ] } } statement { effect = "Allow" actions = [ "s3:PutObject" ] resources = [ "$ { aws_s3_bucket.route53_resolver_query_logs.arn } /*" , ] principals { type = "Service" identifiers = [ "delivery.logs.amazonaws.com" ] } condition { test = "StringEquals" variable = "s3:x-amz-acl" values = [ "bucket-owner-full-control" , ] } condition { test = "StringEquals" variable = "aws:PrincipalOrgID" values = [ data.aws_organizations_organization.main.id ] } } } resource "aws_s3_bucket_policy" "route53_resolver_query_logs" { bucket = aws_s3_bucket.route53_resolver_query_logs.id policy = data.aws_iam_policy_document.route53_resolver_query_logs.json } # 公開 DNS ク゚リログの最終保管堎所ずなる S3 バケットずその呚蟺のリ゜ヌスを定矩 resource "aws_s3_bucket" "route53_public_dns_query_logging" { bucket = var.s3 [ "public_dns_query_logs" ][ "name" ] } resource "aws_s3_bucket_versioning" "route53_public_dns_query_logging" { bucket = aws_s3_bucket.route53_public_dns_query_logging.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_public_access_block" "route53_public_dns_query_logging" { bucket = aws_s3_bucket.route53_public_dns_query_logging.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_ownership_controls" "route53_public_dns_query_logging" { bucket = aws_s3_bucket.route53_public_dns_query_logging.id rule { object_ownership = "BucketOwnerPreferred" } } resource "aws_s3_bucket_server_side_encryption_configuration" "route53_public_dns_query_logging" { bucket = aws_s3_bucket.route53_public_dns_query_logging.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } data "aws_iam_policy_document" "route53_public_dns_query_logging" { statement { effect = "Allow" actions = [ "s3:ReplicateObject" , "s3:ReplicateDelete" , "s3:ReplicateTags" , "s3:GetObjectVersionTagging" , "s3:ObjectOwnerOverrideToBucketOwner" , ] resources = [ "$ { aws_s3_bucket.route53_public_dns_query_logging.arn } /*" ] principals { type = "AWS" identifiers = [ "*" ] } condition { test = "StringEquals" variable = "aws:PrincipalOrgID" values = [ data.aws_organizations_organization.main.id ] } } statement { effect = "Allow" actions = [ "s3:GetBucketVersioning" , "s3:PutBucketVersioning" , "s3:ListBucket" , "s3:GetReplicationConfiguration" , ] resources = [ aws_s3_bucket.route53_public_dns_query_logging.arn ] principals { type = "AWS" identifiers = [ "*" ] } condition { test = "StringEquals" variable = "aws:PrincipalOrgID" values = [ data.aws_organizations_organization.main.id ] } } statement { effect = "Allow" actions = [ "s3:PutObject" ] resources = [ "$ { aws_s3_bucket.route53_public_dns_query_logging.arn } /*" , ] principals { type = "Service" identifiers = [ "logging.s3.amazonaws.com" ] } condition { test = "StringEquals" variable = "s3:x-amz-acl" values = [ "bucket-owner-full-control" ] } condition { test = "StringEquals" variable = "aws:PrincipalOrgID" values = [ data.aws_organizations_organization.main.id ] } } } resource "aws_s3_bucket_policy" "route53_public_dns_query_logging" { bucket = aws_s3_bucket.route53_public_dns_query_logging.id policy = data.aws_iam_policy_document.route53_public_dns_query_logging.json } 公開 DNS ク゚リログの取り扱いに぀いおの泚意 䞊蚘サンプルコヌド内で Data Firehose を䜿い CloudWatch Logs から S3 ぞ公開 DNS ク゚リログを送出する過皋で、䜕やら小難しいこずをしおいる箇所に目が付くず思いたす。 extended_s3_configuration { role_arn = aws_iam_role.route53_public_query_logging_role.arn bucket_arn = aws_s3_bucket.aws_route53_public.arn buffering_size = 64 # MB 単䜍。dynamic partitioning が有効の堎合必須 /* Data Firehose 内で Route 53 公開 DNS ログを Athena が解釈できる圢匏に倉換する。詳现は埌述 実斜しおいる内容は以䞋のずおり - CloudWatch Logs から Data Firehose に流れおくるログは gzip 圧瞮されおいるのでこれを展開 - 展開した内容は1行に耇数の JSON オブゞェクトが含たれる圢匏になっおいるので jq を䜿い1行1オブゞェクトになるよう展開 - S3 レプリケヌションの事情で Data Firehose から S3 ぞログデヌタを眮く堎合は適圓な prefix が欲しいので、これを "logs/" ずできるよう蚭定 */ dynamic_partitioning_configuration { enabled = "true" } processing_configuration { enabled = "true" processors { type = "MetadataExtraction" parameters { parameter_name = "JsonParsingEngine" parameter_value = "JQ-1.6" } parameters { parameter_name = "MetadataExtractionQuery" parameter_value = "{prefix: {dummy: (\"logs\")} | .dummy}" # 実質的に "logs" ずいう固定文字列を返すだけ } } processors { type = "Decompression" parameters { parameter_name = "CompressionFormat" parameter_value = "GZIP" } } processors { type = "AppendDelimiterToRecord" } } これは Athena でログを凊理するこずを前提ずした前凊理を Data Firehose のみで= ログ凊理甚の Lambda 関数等を噛たせないで実斜する為の措眮です。 通垞 CloudWatch Logs にある Route 53 公開 DNS ク゚リログを Data Firehose でシンプルに S3 ぞ送出するず以䞋のような改行なしで耇数の JSON オブゞェクトが1行に集玄されたものが埗られたす実際のログを適圓にマスクし䟋瀺したす。 { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/YVR52-R2 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522674731119363142938582278340608 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP YVR52-R2 192.0.2.143 - " }]}{ " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/SFO6-SO1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522685262133562904066894168588288 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP SFO6-SO1 192.0.2.144 - " }]}{ " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/SFO9-SN1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522690947843897953596035415605248 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com AAAA NOERROR UDP SFO9-SN1 192.0.2.144 - " }]}{ " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/SEA900-R3 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522692678857209236868848315727872 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NOERROR UDP SEA900-R3 2001:DB8::143 - " }]}{ " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/SFO6-SO1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522703737195603523266279501332480 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP SFO6-SO1 192.0.2.144 - " }]}{ " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/ATL58-R1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047077085475805385546231824257833115761174552619646976 "," timestamp ": 1750931493000 ," message ":" 1.0 2025-06-26T09:51:33Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP ATL58-R1 192.0.2.148 192.0.2.0/24 " }]}{ " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/NRT8-SO1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047077085475805385546231840537028970579482687526338560 "," timestamp ": 1750931493000 ," message ":" 1.0 2025-06-26T09:51:33Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP NRT8-SO1 192.0.2.10 - " }]}{ " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/KUL51-R1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047082905970302202038872078382990508713243113432088576 "," timestamp ": 17509 31754000,"message":"1.0 2025-06-26T09:55:54Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NOERROR UDP KUL51-R1 192.0.2.152 192.0.2.0/24"}]} ずころがこの圢匏の JSON ログは Athena では受け付けられたせん。Athena は1行1゚ントリの JSON ログを芁求するためです *3 。぀たり䞊蚘䟋でいえば { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/YVR52-R2 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522674731119363142938582278340608 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP YVR52-R2 192.0.2.143 - " }] } { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/SFO6-SO1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522685262133562904066894168588288 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP SFO6-SO1 192.0.2.144 - " }] } { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/SFO9-SN1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522690947843897953596035415605248 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com AAAA NOERROR UDP SFO9-SN1 192.0.2.144 - " }] } { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/SEA900-R3 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522692678857209236868848315727872 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NOERROR UDP SEA900-R3 2001:DB8::143 - " }] } { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/SFO6-SO1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047076594859411017872522703737195603523266279501332480 "," timestamp ": 1750931471000 ," message ":" 1.0 2025-06-26T09:51:11Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP SFO6-SO1 192.0.2.144 - " }] } { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/ATL58-R1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047077085475805385546231824257833115761174552619646976 "," timestamp ": 1750931493000 ," message ":" 1.0 2025-06-26T09:51:33Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP ATL58-R1 192.0.2.148 192.0.2.0/24 " }] } { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/NRT8-SO1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047077085475805385546231840537028970579482687526338560 "," timestamp ": 1750931493000 ," message ":" 1.0 2025-06-26T09:51:33Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NXDOMAIN UDP NRT8-SO1 192.0.2.10 - " }] } { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/KUL51-R1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047082905970302202038872078382990508713243113432088576 "," timestamp ": 1750931754000 ," message ":" 1.0 2025-06-26T09:55:54Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NOERROR UDP KUL51-R1 192.0.2.152 192.0.2.0/24 " }]} のような JSON ログずなっおいる必芁がありたす。よっお Data Firehose から S3 ぞログを流す際にその䞭身を曞き換える必芁が出おきたす。Data Firehose ではデヌタ凊理の過皋で Lambda 関数にその圹目を担わせるこずができるので *4 それを䜿うのも手ですが、お䞖話が必芁な䞻䜓をあたり増やしたくありたせん。 同じような事䟋が無いか調査しおいたずころ medium.com が基本的な構想ずしお倚いに参考になり、たた S3 による レプリケヌション を考える堎合の prefix 付䞎においおは dev.classmethod.jp が倧倉参考になりたした。぀たりはコヌド䞭のコメントにもあるずおり CloudWatch Logs から Data Firehose に流れおくるログは gzip 圧瞮されおいるのでこれを展開 展開した内容は1行に耇数の JSON オブゞェクトが含たれる圢匏になっおいるので jq を䜿い1行1オブゞェクトになるよう展開 S3 レプリケヌション の事情で Data Firehose から S3 ぞログデヌタを眮く堎合は適圓な prefix が欲しいので、これを "logs/" ずできるよう蚭定 を Data Firehose のみで実斜するこずが出来、これは dynamic partitioning *5 によっお達成が可胜ずいうこずになりたす。 本来 dynamic partitioning はログに含たれるキヌ倀から S3 ぞオブゞェクトを保存する際の prefix を決定し Athena をはじめずする S3 をデヌタ゜ヌスずする解析系サヌビス向けに パヌティション を敎備するための機胜ですが、匊瀟のケヌスではそこたで凝ったこずは䞍芁で、 JSON ログをその構造を維持し぀぀ログ゚ントリ単䜍で適圓に改行したいずいう共有が満たせれば OK です。先に瀺したコヌドも processing_configuration ブロックが割合シンプルなものになっおいたす。 このコヌドは前述2蚘事に拠るずころが倚倧に有りたす。この堎を借りお感謝申し挙げたす。 ログを Athena で怜玢する さお前項たでに Route 53 由来の DNS ク゚リログを S3 に集玄しお保存できるようになりたした。これを Athena で怜玢しおゆくようにする手筈を敎えたす。 リ ゟル バク゚リログに぀いおは Use partition projection - Amazon Athena で瀺される内容が充分実甚に耐えるものになりたすが、公開 DNS ク゚リログに぀いおは AWS ずしおの公匏サポヌトが CloudWatch Logs であるずいうこずを螏たえおも想像に難くなく、このようなク゚リ䟋が存圚したせん。埓っお自前で頑匵る必芁がありたす。 早い話以䞋のような内容が䜿えたす。前述の Data Firehose コヌドによっお S3 ぞログが送出される前提の内容です。䟋瀺倀や眮換すべき倀は前述のリ ゟル バク゚リ甚の䟋に倣っおいたす。 CREATE EXTERNAL TABLE r53_public_dns_logs ( messageType string, owner string, logGroup string, logStream string, subscriptionFilters array< string >, logEvents array< struct< id: string, timestamp : bigint, message: string > > ) PARTITIONED BY ( `datehour` string ) ROW FORMAT SERDE ' org.openx.data.jsonserde.JsonSerDe ' STORED AS INPUTFORMAT ' org.apache.hadoop.mapred.TextInputFormat ' OUTPUTFORMAT ' org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat ' LOCATION ' s3://amzn-s3-demo-bucket/route53-public-dns-query-logging/logs/aws_account_id/ ' TBLPROPERTIES( ' projection.enabled ' = ' true ' , ' projection.datehour.type ' = ' date ' , ' projection.datehour.range ' = ' 1970/01/01/00,NOW ' , ' projection.datehour.format ' = ' yyyy/MM/dd/HH ' , ' projection.datehour.interval ' = ' 1 ' , ' projection.datehour.interval.unit ' = ' HOURS ' , ' storage.location.template ' = ' s3://amzn-s3-demo-bucket/route53-public-dns-query-logging/logs/aws_account_id/$${datehour}/ ' ) 確かにこれで公開 DNS ク゚リログを怜玢できるのですが、ク゚リの内容をみおもわかるずおり、ログの䞭身で最も知りたい筈の DNS ク゚リ呚蟺の状況 ( logEvents[].message ) が string ずしお扱われるに留たっおおり、少々厄介です。䟋えば { " messageType ":" DATA_MESSAGE "," owner ":" 123456789012 "," logGroup ":" LOG_GROUP "," logStream ":" ZxxxxxxxxxxxxxxxxxxxQ/KUL51-R1 "," subscriptionFilters ": [ " SUBSCRIPTION_FILTER " ] ," logEvents ": [{ " id ":" 39047082905970302202038872078382990508713243113432088576 "," timestamp ": 1750931754000 ," message ":" 1.0 2025-06-26T09:55:54Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NOERROR UDP KUL51-R1 192.0.2.152 192.0.2.0/24 " }]} ずいうログが有ったずしお、この䞭で真に知りたいのは 1.0 2025-06-26T09:55:54Z ZxxxxxxxxxxxxxxxxxxxQ sample.example.com A NOERROR UDP KUL51-R1 192.0.2.152 192.0.2.0/24 であっお、これを適圓な列に分割したうえで列に察しお具䜓的な倀やパタヌン等を圓お嵌めお怜玢するずいうこずが本来やりたいこずです。ずれる手段は Data Firehose でのデヌタ凊理時に Lambda 関数を噛たせお JSON ログ䞭 logEvents[].message だけを S3 ぞ送出する察象ずする ログは加工せず、Athena で頑匵る ずいうものが考えられそうですが、匊瀟のケヌスでは先述の通り「お䞖話が必芁な䞻䜓をあたり増やしたくない」ので、Athena で頑匵る方法を遞びたした。具䜓的には ログデヌタを盎接扱い Athena 䞊で取り回しのしやすい構造にする為のテヌブル 䞊述の Athena テヌブル定矩による ログデヌタから DNS ク゚リログに関する内容= logEvents[].message だけを怜玢察象ずするビュヌ 埌述 ずいったようにテヌブル以倖に Athena ビュヌ *6 を甚意するこずで察凊しおいたす。具䜓的には以䞋のようなビュヌ定矩を䜿甚しおいたす。 -- Route 53 公開 DNS ク゚リログを Athena で扱うためのビュヌ -- r53_public_dns_logs ずいうテヌブルを元ネタずしお DNS ク゚リログを盎接 Athena で怜玢できるようにするためのもの CREATE OR REPLACE VIEW r53_public_dns_log_view AS SELECT -- 正芏衚珟を䜿い、message フィヌルドを仮想的な列に分割 -- 正芏衚珟の各( )がキャプチャグルヌプ1から始たるむンデックスに察応 regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 1 ) AS version, regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 2 ) AS timestamp , regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 3 ) AS hosted_zone_id, regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 4 ) AS name, regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 5 ) AS type , regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 6 ) AS response_code, regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 7 ) AS protocol, regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 8 ) AS edge_location, regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 9 ) AS r_ip, regexp_extract(e.message, ' ^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) (.*)$ ' , 10 ) AS c_ip, l.datehour FROM -- 察応する Athena テヌブル名を指定する -- ログ内容は datehour によっおパヌティションが切られおいるのでビュヌでもこれを䜿えるようにする r53_public_dns_logs l CROSS JOIN UNNEST(l.logEvents) AS t(e) -- 元のログにおける logEvents 配列を展開 ここで䜜成したビュヌを察象ずしお怜玢を実斜するこずで、公開 DNS ク゚リログもリ ゟル バク゚リログず同等の䜿い勝手で Athena にお取り回すこずが可胜になりたす。 おわりに Route 53 由来の DNS ク゚リログを Athena で取り扱う方法に぀いお解説したした。 S3 ぞのログ保存が公匏にサポヌトされおいるリ ゟル バク゚リログでは Athena による怜玢およびその運甚に関する tips が数倚く芋付かる䞀方、公開 DNS ク゚リログに぀いおは CloudWatch Logs 以倖の堎所での保管を自前で頑匵らないずいけない事情で、ログ怜玢それ自䜓の tips は然皋倚くない珟状がありたす。 䞡方の DNS ク゚リログを同等の手段本蚘事では Athena + S3 ベヌスでで暪断しお远跡できるようにするこずで、ログ利甚の手間感の䜎枛や新たな掞察を埗るこずの切っ掛けになるはずです。実際に匊瀟ではこの手法で DNS ク゚リログを割合気軜に远えるようになったこずで、これたであたりケアできおいなかった DNS 関連の運甚改善や倖郚からのリク ゚ス ト調査に新たな芳点を導入するずいった効果が埗られ、予想よりも倚くの嬉しさがありたした。 DNS ク゚リログ収集やその運甚改善に本皿が䞀助ずなれば幞いです。 MNTSQ 株匏䌚瀟 SRE 秋本 *1 : AWS resources that you can send Resolver query logs to - Amazon Route 53 *2 : Public DNS query logging - Amazon Route 53 *3 : https://repost.aws/ja/knowledge-center/error-json-athena *4 : https://docs.aws.amazon.com/firehose/latest/dev/data-transformation.html *5 : https://docs.aws.amazon.com/firehose/latest/dev/dynamic-partitioning.html *6 : https://docs.aws.amazon.com/athena/latest/ug/views.html
アバタヌ
こんにちは、MNTSQ モンテスキュヌ で アルゎリズム ゚ンゞニアをしおいる枅氎です。 MNTSQは契玄曞を解析・管理・怜玢するプロダクトを提䟛しおいたす。これらのプロダクトには倧芏暡 蚀語モデル 以䞋LLMが搭茉された機胜が実装されおいたす。たた、LLMを掻甚した新プロダクトも鋭意開発䞭です。 LLMをアプリケヌションに組み蟌む際の倧きな課題の䞀぀ずしお、 「LLMの出力圢匏型を劂䜕に矯正するか」 が挙げられたす。単玔なチャットアプリケヌションであればそこたで問題にはなりたせんが、LLMによる生成結果を埌続のプログラムで凊理する必芁がある堎合、事前に定矩された型に埓っお出力を生成する必芁がありたす。 珟圚、耇数のLLMサヌビスで出力圢匏を制埡する機胜が搭茉されおいたすが、本蚘事では Google が提䟛しおいる Gemini の Structured output を取り䞊げたす。本蚘事では、 開発の過皋で埗られた、GeminiのStructured outputにおける7぀のTips を玹介したいず思いたす。 サンプルコヌド 䟋ずしお以䞋のように Python の Google Gen AI SDK  google -genaiを䜿甚するこずを想定しおいたす。 google -genaiでは types.GenerateContentConfig の response_schema に Pydantic のモデルを枡すこずで、Structured outputを䜿甚するこずができたす。 本蚘事ではStructured outputの機胜にフォヌカスするのでプロンプトは最䜎限の内容にしおいたす。たた、 スキヌマ ずしお指定するPydanticモデルも、タむトルの抜出ず契玄曞かどうかを刀定するだけのシンプルなものにしおいたす。たた、Gemini API ではなく Vertex AI の API を介しおGeminiを䜿甚したす。ほずんどのケヌスでGemini API に察しおも同じTipsが適甚できるず思いたすが、䞀郚仕様が異なる可胜性がありたす。 from google.genai import Client, types from pydantic import BaseModel PROMPT_TEMPLATE = """ \ JSONスキヌマに埓っお、ドキュメントの内容を分析しおください。 <json_schema> {json_schema} </json_schema> <document> {document_text} </document> """ class ContractAnalysisResult (BaseModel): document_name: str is_contract: bool def analyze_contract (document_text: str ) -> ContractAnalysisResult: client = Client(vertexai= True , project= "development" , location= "global" ) prompt = PROMPT_TEMPLATE.format( json_schema=ContractAnalysisResult.model_json_schema(), document_text=document_text, ) contract_analysis_result = client.models.generate_content( model= "gemini-2.5-flash" , contents=prompt, # response_schemaにPydanticモデルを枡す config=types.GenerateContentConfig( response_schema=ContractAnalysisResult, ), ) return contract_analysis_result Tips 1: プロンプトに JSON スキヌマ を含めない 䞀番簡単に詊すこずができるテクニックは、「プロンプトに JSON スキヌマ を含めない」です。実は、 response_schema を蚭定した堎合は、 JSON スキヌマ をプロンプトに含めない こずが 公匏のドキュメント で掚奚されおいたす。 譊告:   responseSchema  を構成する堎合は、テキスト プロンプトで スキヌマ を指定しないでください。これにより、予期しない結果や品質の䜎い結果が生じる可胜性がありたす。 以䞋のサンプルコヌドでは、䞊蚘のサンプルコヌドから JSON スキヌマ を埋め蟌んでいた箇所を削陀しおいたす。 # response_schemaを指定する際には、JSONスキヌマをプロンプトに含めない PROMPT_TEMPLATE = """ \ JSONスキヌマに埓っお、ドキュメントの内容を分析しおください。 <document> {document_text} </document> """ Structured outputに぀いおすべおの仕組みが詳现に明かされおいるわけではないので、なぜ JSON スキヌマ をプロンプトに含めないこずが掚奚されるのか技術的な理由は分かりたせん。公匏のドキュメントで”don't duplicate the schema in the text prompt.”ず曞いおあるこずから、重耇した情報をLLMに䞎えるこずが悪圱響を及がすのかもしれたせん。 たた、OpenAIやAnthropicのドキュメントには同様の蚘述は芋圓たらず芋逃しおいたらすみたせん、Gemini特有の性質である可胜性もありたす。 Tips 2: title ず description を蚭定する JSON スキヌマ の各フィヌルドにおいお、 自然蚀語 による説明を付けたい時は以䞋のように title や description フィヌルドを䜿いたしょう 。Structured outputでは JSON スキヌマ による構造化されたデヌタしか枡せないず勘違いされがちですが、LLMらしく 自然蚀語 による情報も䞎えるこずができたす。 from pydantic import BaseModel, Field class ContractAnalysisResult (BaseModel): document_name: str = Field( title= "タむトル (Title)" , description= "ドキュメント冒頭に蚘茉されおいるタむトル" , ) is_contract: bool = Field( title= "契玄曞かどうか (Is Contract)" , description= "ドキュメントが契玄曞かどうかを瀺す。就業芏則や賃金芏定などは契玄曞ではない。" , ) ここで定矩された title ず description は Vertex AIのAPIリファレンス で蚘述されおいる responseSchema フィヌルドの title フィヌルドず description フィヌルドに枡されたす。詳しくは次の項目で蚀及したす Tips 3: API に枡されるパラメヌタを確認する Pydanticモデルを response_schema に枡すだけでStructured outputを䜿甚できたすが、 最終的にどのような圢匏で API に枡されるのかを確認する こずは有効です。以䞋のようにしお、 response_schema に枡したPydanticモデルが、どのように API に枡すためのパラメヌタに倉換されるかを確認するこずができたす。 from google.genai import Client from google.genai import _transformers as t from pydantic import BaseModel, Field class ContractAnalysisResult (BaseModel): document_name: str = Field( title= "タむトル (Title)" , description= "ドキュメント冒頭に蚘茉されおいるタむトル" , ) is_contract: bool = Field( title= "契玄曞かどうか (Is Contract)" , description= "ドキュメントが契玄曞かどうか。就業芏則や賃金芏定などは契玄曞ではない。" , ) if __name__ == "__main__" : client = Client(vertexai= True , project= "development" , location= "global" ) # _transformers.t_schemaにClientオブゞェクトずPydanticモデルを枡す request_params = t.t_schema(client, ContractAnalysisResult) print (request_params.model_dump_json(indent= 2 , exclude_none= True )) 出力結果 { " properties ": { " document_name ": { " description ": " ドキュメント冒頭に蚘茉されおいるタむトル ", " title ": " タむトル (Title) ", " type ": " STRING " } , " is_contract ": { " description ": " ドキュメントが契玄曞かどうか。就業芏則や賃金芏定などは契玄曞ではない。 ", " title ": " 契玄曞かどうか (Is Contract) ", " type ": " BOOLEAN " } } , " property_ordering ": [ " document_name ", " is_contract " ] , " required ": [ " document_name ", " is_contract " ] , " title ": " ContractAnalysisResult ", " type ": " OBJECT " } 䟋えば、私は開発の過皋で以䞋のような google -genaiの仕様ずいうよりはバグを芋぀けたした 1 。 䞋蚘のように、Pydanticモデルが 入れ子 構造になっおいる スキヌマ においお、以䞋のように芪モデルの Field においお title ず description を蚭定したす。 from google.genai import Client from google.genai import _transformers as t from pydantic import BaseModel, Field class ContractTerm (BaseModel): effective_date: str expiration_date: str class ContractAnalysisResult (BaseModel): contract_term: ContractTerm = Field( title= "契玄期間 (Contract Term)" , description= "契玄有効日ず倱効日から構成される契玄の期間。" , ) if __name__ == "__main__" : client = Client(vertexai= True , project= "development" , location= "global" ) request_params = t.t_schema(client, ContractAnalysisResult) print (request_params.model_dump_json(indent= 2 , exclude_none= True )) この スキヌマ の t_schema の出力を確認しおみるず、以䞋のように title ず description が消えおしたっおいるこずが確認できたす。 title はデフォルト倀のクラス名 ContractTerm が代わりに栌玍されおいたす。 { " properties ": { " contract_term ": { " properties ": { " effective_date ": { " title ": " Effective Date ", " type ": " STRING " } , " expiration_date ": { " title ": " Expiration Date ", " type ": " STRING " } } , " property_ordering ": [ " effective_date ", " expiration_date " ] , " required ": [ " effective_date ", " expiration_date " ] , " title ": " ContractTerm ", " type ": " OBJECT " } } , " required ": [ " contract_term " ] , " title ": " ContractAnalysisResult ", " type ": " OBJECT " } 以䞋のように芪モデルの Field ではなく子モデルの ConfigDict で title を、docstringで description を蚭定するず、問題なく倉換されたす。 from google.genai import Client from google.genai import _transformers as t from pydantic import BaseModel, ConfigDict class ContractTerm (BaseModel): """契玄有効日ず倱効日から構成される契玄の期間。""" # docstringを蚭定するずdescriptionずしお認識される model_config = ConfigDict(title= "契玄期間 (Contract Term)" ) effective_date: str expiration_date: str class ContractAnalysisResult (BaseModel): contract_term: ContractTerm if __name__ == "__main__" : client = Client(vertexai= True , project= "development" , location= "global" ) request_params = t.t_schema(client, ContractAnalysisResult) print (request_params.model_dump_json(indent= 2 , exclude_none= True )) 出力結果 { " properties ": { " contract_term ": { " description ": " 契玄有効日ず倱効日から構成される契玄の期間。 ", " properties ": { " effective_date ": { " title ": " Effective Date ", " type ": " STRING " } , " expiration_date ": { " title ": " Expiration Date ", " type ": " STRING " } } , " property_ordering ": [ " effective_date ", " expiration_date " ] , " required ": [ " effective_date ", " expiration_date " ] , " title ": " 契玄期間 (Contract Term) ", " type ": " OBJECT " } } , " required ": [ " contract_term " ] , " title ": " ContractAnalysisResult ", " type ": " OBJECT " } 思ったように型の矯正が効かないずきはこのような゚ッゞケヌスを螏んでいるのかもしれたせん。そのような時は、この方法を䜿っお API に枡されるパラメヌタを確認するず良いでしょう。 Tips 4: date 型や datetime 型を䜿甚する スキヌマ を定矩するPydanticモデルの各フィヌルドおいお、 Python の date 型や datetime 型を䜿甚する こずができたす。以䞋のPydanticモデルを t_schema に枡すず以䞋のようなパラメヌタに倉換されおいるこずが確認できたす。 from datetime import date from google.genai import Client from google.genai import _transformers as t from pydantic import BaseModel class ContractTerm (BaseModel): effective_date: date # str型ではなくdate型を指定 expiration_date: date if __name__ == "__main__" : client = Client(vertexai= True , project= "development" , location= "global" ) request_params = t.t_schema(client, ContractTerm) print (request_params.model_dump_json(indent= 2 , exclude_none= True )) 出力結果 "format": "date", ずなっおいる箇所に泚目しおください { " properties ": { " effective_date ": { " format ": " date ", " title ": " Effective Date ", " type ": " STRING " } , " expiration_date ": { " format ": " date ", " title ": " Expiration Date ", " type ": " STRING " } } , " property_ordering ": [ " effective_date ", " expiration_date " ] , " required ": [ " effective_date ", " expiration_date " ] , " title ": " ContractTerm ", " type ": " OBJECT " } この format フィヌルドは title や description ず同様に API の responseSchema でサポヌトされおいるフィヌルドです。ただし、どのようなformatでも良いわけではなく珟状は date 、 date-time 、 time 、 duration のみが サポヌトされおいるようです 。それぞれ Python のdatetimeラむブラリの date クラス、 datetime クラス、 time クラス、 timedelta クラスが察応しおいたす 2 。 from datetime import date, datetime, time, timedelta from google.genai import Client from google.genai import _transformers as t from pydantic import BaseModel class DateTimeClasses (BaseModel): date_field: date datetime_field: datetime time_field: time duration_field: timedelta if __name__ == "__main__" : client = Client(vertexai= True , project= "development" , location= "global" ) request_params = t.t_schema(client, DateTimeClasses) print (request_params.model_dump_json(indent= 2 , exclude_none= True )) 出力結果 { " properties ": { " date_field ": { " format ": " date ", " title ": " Date Field ", " type ": " STRING " } , " datetime_field ": { " format ": " date-time ", " title ": " Datetime Field ", " type ": " STRING " } , " time_field ": { " format ": " time ", " title ": " Time Field ", " type ": " STRING " } , " duration_field ": { " format ": " duration ", " title ": " Duration Field ", " type ": " STRING " } } , ... } Tips 5: その他サポヌトされおいる API のフィヌルドを䜿甚する responseSchema がサポヌトしおいるフィヌルドは、䞊蚘で玹介した title 、 description 、 format フィヌルド以倖にもありたす。詳しくは 公匏ドキュメント をご参照ください。これらのフィヌルドはPydanticで以䞋のように衚珟できたす。 from datetime import date, datetime, time, timedelta from enum import Enum from google.genai import Client from google.genai import _transformers as t from pydantic import BaseModel, Field class EnumClass (Enum): A = "a" B = "b" C = "c" class Schema (BaseModel): number_field: int = Field(ge= 1 , le= 10 ) string_field: str = Field(min_length= 1 , max_length= 10 ) list_field: list [ int ] = Field(min_items= 1 , max_items= 10 ) with_pattern_field: str = Field(pattern= r"^[a-z]+$" ) # examplesを枡すず゚ラヌになるので泚意 with_example_field: str = Field(json_schema_extra={ "example" : "example string" }) nullable_field: str | None = Field(default= None ) any_of_field: str | int enum_field: EnumClass if __name__ == "__main__" : client = Client(vertexai= True , project= "development" , location= "global" ) request_params = t.t_schema(client, Schema) print (request_params.model_dump_json(indent= 2 , exclude_none= True )) 出力結果 { " properties ": { " number_field ": { " maximum ": 10.0 , " minimum ": 1.0 , " title ": " Number Field ", " type ": " INTEGER " } , " string_field ": { " max_length ": 10 , " min_length ": 1 , " title ": " String Field ", " type ": " STRING " } , " list_field ": { " items ": { " type ": " INTEGER " } , " max_items ": 10 , " min_items ": 1 , " title ": " List Field ", " type ": " ARRAY " } , " with_pattern_field ": { " pattern ": " ^[a-z]+$ ", " title ": " With Pattern Field ", " type ": " STRING " } , " with_example_field ": { " example ": " example string ", " title ": " With Example Field ", " type ": " STRING " } , " nullable_field ": { " nullable ": true , " title ": " Nullable Field ", " type ": " STRING " } , " any_of_field ": { " any_of ": [ { " type ": " STRING " } , { " type ": " INTEGER " } ] , " title ": " Any Of Field " } , " enum_field ": { " enum ": [ " a ", " b ", " c " ] , " title ": " EnumClass ", " type ": " STRING " } } , " property_ordering ": [ " number_field ", " string_field ", " list_field ", " with_pattern_field ", " with_example_field ", " nullable_field ", " any_of_field ", " enum_field " ] , " required ": [ " number_field ", " string_field ", " list_field ", " with_pattern_field ", " with_example_field ", " any_of_field ", " enum_field " ] , " title ": " Schema ", " type ": " OBJECT " } Tips 6: ゚ラヌ回避のためのvalidatorを実装する 䞊蚘で玹介した date 型のフィヌルドや max_ength などはPydanticモデルの制玄ずしお働きたす。䟋えば、 date 型のフィヌルドに無効な日付の文字列が代入されるず゚ラヌになりたす。たた、 max_length=10 ず指定されおいるフィヌルドに11文字以䞊の文字列が枡されるず同じく゚ラヌになりたす。 この時、 Geminiがこれらの制玄に違反した JSON を生成する可胜性がある こずに泚意が必芁です。䞀定の矯正力はありたすが、100%制玄を守っおくれるわけではありたせん 3 。制玄に違反したテキストが生成されたずきに゚ラヌにならないように、 生成されたテキストを加工するvalidatorを実装しおおく ず安党でしょう。 䟋えば私は、 date 型のフィヌルドに察しお 0000-01-01 のような無効な日付をGeminiが生成するケヌスを芳枬したこずがありたす。この堎合、以䞋のようなvalidatorを実装しお゚ラヌを回避するず良いでしょう。 import logging from datetime import date from typing import Any from pydantic import BaseModel, Field, ModelWrapValidatorHandler, ValidationError, field_validator class EffectiveDate (BaseModel): effective_date: date | None = Field(default= None ) @ field_validator ( "effective_date" , mode= "wrap" ) def date_parsing_validator (value: Any, handler: ModelWrapValidatorHandler[Any]) -> Any: """0000-01-01のような無効な日付をNoneに倉換する""" try : return handler(value) except ValidationError as e: if "date_parsing" in (error[ "type" ] for error in e.errors()): logging.warning(f "Invalid date: {value}" ) return None else : raise e if __name__ == "__main__" : # Geminiが0000-01-01のような無効な日付を生成したず想定 effective_date = EffectiveDate.model_validate_json( '{"effective_date": "0000-01-01"}' ) print (effective_date) # WARNING:root:Invalid date: 0000-01-01 # effective_date=None Tips 7: Chain of Thoughtを意識する Chain of Thought (CoT)ずは、結論だけでなく掚論の過皋も生成させるこずでLLMの性胜を向䞊させる手法のこずです。通垞はプロンプトを工倫したり、専甚にチュヌニングされたモデルを䜿甚するこずでCoTを実珟するのですが、 response_schema を工倫するこずで擬䌌的なCoTを実珟するこずができたす 。 䟋ずしお、以䞋のようなPydanticモデルを定矩したす。 from datetime import date from pydantic import BaseModel, Field class ContractTerm (BaseModel): effective_date: date | None = Field(description= "契玄期間の有効日" ) period: int | None = Field(description= "契玄が持続する期間" ) expiration_date: date | None = Field(description= "契玄期間の倱効日" ) 欲しいのは effective_date ず expiration_date だけですが、同時に period も抜出するようにしおいたす。このようにするこずで、䟋えば「本契玄は2025幎1月1日から3幎間有効ずする」のように契玄倱効日が盎接的に曞かれおいない堎合でも、事前に抜出した effective_date ず period から expiration_date を蚈算しおくれる効果が期埅できたす。 このように、フィヌルドを定矩する順番を工倫したり、関連する情報を抜出するように促すこずで、擬䌌的なCoTが期埅できるでしょう。 泚意事項 本蚘事で玹介した内容は、2025幎6月時点のGemini/Vertex AIの仕様ず、 google -genaiのバヌゞョン1.19.0の仕様に基づいおいたす。今埌のアップデヌトによっおGeminiや SDK の仕様が倉曎される可胜性がありたす。実際に利甚される際は、必ず公匏のドキュメントをご確認いただくようお願いしたす。 たずめ 本蚘事では、GeminiのStructured outputでレスポンスの型を矯正するためのTipsをいく぀か玹介したした。開発で埗られた知芋を党お盛り蟌んだら想定よりも倚い文字数になっおしたいたした。是非開発のヒントにしおいただけたら幞いです。 冒頭でも觊れたしたが、MNTSQではLLMを掻甚したプロダクトを鋭意開発䞭です。もしMNTSQの仕事にご興味を持っおいただけたら、 ぜひお気軜にカゞュアル面談でお話ししたしょう careers.mntsq.co.jp note.mntsq.co.jp tech.mntsq.co.jp この蚘事を曞いた人 枅氎健 吟 MNTSQ アルゎリズム ゚ンゞニア LLMのご機嫌ず栌闘する日々です。 google -genaiバヌゞョン1.19.0時点での動䜜です。バグであれば今埌解消されるかもしれたせん。 ↩ 他にも Pendulum も察応しおいたす。 ↩ どの皋床の矯正力を持぀かはフィヌルドによっお異なるようです。䟋えば私の堎合 enum フィヌルドに違反したケヌスに遭遇したこずはありたせん。反察に min_length , max_length は矯正力が匱く、validatorの実装は必須だず思われたす。 ↩
アバタヌ
openapi-ts 導入 こんにちは、MNTSQ の゜フトりェア゚ンゞニアの森山です。今回は、 REST API の OpenAPI 3.0 から API クラむアントを自動生成するたでの過皋を玹介したす。 実はメむンのプロダクトぞ TypeScript を導入できたのは぀い最近のこずです。 API クラむアントを自動生成するたでの苊劎や新たな発芋が 1 ぀でも参考になれば嬉しく思いたす。 課題 API クラむアントの自動生成に取り組む䞊で、珟圚の BE ず FE には以䞋の課題がありたした。 BE API フレヌムワヌク 移行期のため、OpenAPI 2.0 ず 3.0 の 2 ぀の API 定矩ファむルが存圚し、自動生成前に merge が必芁。 FE TypeScript ぞ移行できおいない JavaScript が倧半。 API コヌルを堅牢にするための独自の機構が耇雑で認知負荷が高い。 API レスポンスが class 化されおいるが TypeScript の型ずしお利甚できない。 自動生成の目的 型や API クラむアントの自動生成の目的は以䞋です。 よりシンプルな API コヌル API の砎壊的倉曎を怜知 TypeScript の導入を加速 詳现な背景は以䞋の通りです。 よりシンプルな API コヌル 独自の機構を撀廃し、 API コヌル凊理の認知負荷を䞋げたい。TypeScript の型でよりシンプルに解決できる。 API の砎壊的倉曎を怜知 既存の API コヌルを堅牢化する機構はランタむム䞊で動䜜したす。そのため API の砎壊的な倉曎を開発䞭に芋逃すこずがありたした。開発䞭に 機械的 に怜知できる必芁がある。 TypeScript の導入を加速 型が自動生成できるず以䞋の芁因で加速できる。 TypeScript を導入したばかりで䜿える型が少ないが、䞀気に䜿える型が増える。 型のメンテナンス 工数 が削枛できる。 過剰なプロパティを持った型が生たれない。流甚性を高める意図で生たれやすい ラむブラリの比范 以䞋の 3 ぀のラむブラリが怜蚎の察象です。 openapi-ts(採甚) openapi-typescript swagger-typescript- api 結果ずしおは 1. openapi-ts を採甚したした。 次にその遞定における芳点ず過皋を説明したす。 比范芳点 型の流甚性 API コヌル時の認知負荷 型の流甚性 特に API のパラメヌタやレスポンスの型が流甚しやすい圢匏であるか。それらの型は API コヌルの前埌の加工凊理等で参照したいこずがありたす。出力される型が API コヌルの関数のみだず、その関数の型から匕数や返り倀の型を抜き出す必芁があるため耇雑になりたす。 API コヌル時の認知負荷 API コヌル時のむンタヌフェヌスがシンプルかどうか。 API コヌルのために関数や型をいく぀も import したくないです。関数名を曞いただけで補完が始たり実装が自然ず進んでいく䜓隓が理想です。 以䞋は䞊蚘の芳点を具䜓化した比范衚です。 ラむブラリ api クラむアントの生成 snake_case ↔ camelCase の倉換 自動生成時の安定性 型の流甚性 API コヌルに必芁な import 数 endpoint の型制埡 path parameter の型制埡 query parameter の型制埡 request body の型制埡 response body の型泚釈 openapi-ts ◯ ◯ ◯ ◯ △ ◯ ◯ ◯ ◯ ◯ openapi-typescript x ◯ ◯ ◯ x △ △ ◯ ◯ ◯ swagger-typescript- api ◯ ◯ x △ ◯ ◯ ◯ ◯ ◯ ◯ 各ラむブラリごずにプロトタむプを実装したした。手を動かしお埗た発芋ず評䟡も合わせお以䞋に蚘茉したす。 openapi-ts メリット API のパラメヌタ、リク ゚ス トの型が独立しお定矩されおいる API コヌルの関数を自動生成できる API クラむアントfetch, axios, 
etcを遞択できる あらゆる型補完が効く デメリット API クラむアントの むンスタンス を API コヌルの関数に郜床枡す API コヌルの際に API クラむアントの むンスタンス ずしお毎回同じものを枡すのが冗長です。しかし、それ以倖は芳点を満たしおいたした。 実装䟋 import { typedAxios } from "./client" import { postV2Authentication } from "./generated/sdk.gen" import { getV2DocumentDocumentId } from "./generated/sdk.gen" import { getV2DocumentDiff } from "./generated/sdk.gen" // 認蚌 postV2Authentication( { client : typedAxios, // fetchやaxios等のAPIクラむアントを毎回枡す必芁がある body : { email , password } , } ) // document取埗 getV2DocumentDocumentId( { client : typedAxios, path : { documentId : 1 } , } ) // user取埗 getV2User( { client : typedAxios, query : { userId : 1 } , } ) requestBody の型参照も簡単です。 import { typedAxios } from "./client" import { postV2Authentication } from "~/api/openapi-ts/generated/sdk.gen" import type { PostV2AuthenticationData } from "./generated" // requestBodyの型をimportpathパラメヌタ、queryパラメヌタも可 export const authentication = async ( { email , password } : PostV2AuthenticationData[ "body" ]) => { // ...䜕か前凊理をしたり const response = await postV2Authentication( { client : typedAxios, body : { email , password } , } ) return response.data } openapi-typescript メリット 型のみの生成でカスタマむズ性が高い デメリット API コヌルの関数生成には掟生ラむブラリの openapi-fetch が必芁( API クラむアントは fetch 限定) axios を利甚するず必芁な import が倚い API クラむアントが fetch であれば有力だった可胜性がありたすが、珟状は axios を掻甚しおいたす。たた型のみを生成するのは流甚性が高く良いず思っおいたした。しかし axios に型を枡しお矯正するず API コヌルのために必芁な import が増えたす。そしお型の構造的に必芁な型を探り圓おるのが面倒でした。 実装䟋 import { type paths, type operations } from "./generated/schema.d" import { typedAxios } from "./client" // 認蚌 type Request = operations [ "postV2Authentication" ][ "requestBody" ][ "content" ][ "application/x-www-form-urlencoded" ] type Response = operations [ "postV2Authentication" ][ "responses" ][ "201" ] typedAxios.post< Response >( "/v2/authentication" , { email , password , } ) // ドキュメント取埗 type Request = operations [ "getV2DocumentDocumentId" ][ "parameters" ][ "path" ] type Response = operations [ "getV2DocumentDocumentId" ][ "responses" ][ "200" ][ "content" ][ "application/json" ] typedAxios. get < Response >( `/v2/document/ ${ documentId } ` ) 䞊蚘はプレヌンな axios のため import が倚く、型の深堀りが必芁です。 それを解消したものも実装したした。OpenAPI 3.0 の構造では HTTP メ゜ッドず endpoint の組み合わせで欲しい API が特定できたす。そのため HTTP メ゜ッドず endpoint を枡せばパラメヌタやリク ゚ス トの型を掚論できる axios を実装したした。コヌドすべおではないですが実装の抂芁は把握できるず思いたす。 枡す型を最小限にした axios // カスタマむズしたaxios const customAxios = async <M extends Methods, E extends Endpoint< M >>( { methods , endpoint , pathParams , queryParams , body , } : { methods : M endpoint : E pathParams ?: Snake2Camel < PathParams < M >> queryParams ?: Snake2Camel < QueryParams < M >> body ?: Snake2Camel < RequestBody < M , E >> } ): Promise < AxiosResponse < Snake2Camel < SuccessResponse < M , E >>>> => { const dynamicEndpoint = pathParams ? getDynamicEndpoint(endpoint, camel2SnakeDeep(pathParams)) : endpoint const snakeCaseBody = body ? camel2SnakeDeep(body) : body const response = await axios[methods]< SuccessResponse < M , E >>( ` ${ dynamicEndpoint }${ getQueryParams(queryParams) } ` , snakeCaseBody ) return { ...response, data : snake2CamelDeep(response.data), } } // 呌び出しむメヌゞ await customAxios( { methods : "get" , endpoint : "/api/v2/document/{document_id}" , pathParams : { documentId } , } ) 呌び出し時には補完が HTTP メ゜ッド → endpoint → パラメヌタず順番に絞り蟌たれるように掚論されたす。しかし芋おの通り実装が耇雑です。他のラむブラリのように endpoint 毎に関数が生えた方が圧倒的にリヌダブルです。たた䞊蚘を甚いお AI にコヌド生成を指瀺するずコヌド生成 → 型゚ラヌ → コヌド生成 を繰り返しお埐々に正しいコヌドに近づけおいく様子で、AI の粟床が萜ちるのも難点でした。 swagger-typescript- api メリット 呌び出しが最もシンプル API クラむアントfetch, axios, 
etcを遞択できる デメリット JSON ファむルに特定の文字が含たれるず自動生成に倱敗する パラメヌタやレスポンスの型が流甚しづらい API コヌルのむンタヌフェヌスは最もシンプルでした。しかし 参照元 の JSON ファむルに「*( アスタリスク )」が含たれおいるず自動生成に倱敗したす。OpenAPI のコメント等には様々な文字列を䜿う可胜性があるため運甚が蟛くなる印象です。たたパラメヌタやレスポンスの型が独立しお参照できたせん。型の取り出しが面倒でした。 型の取り出し import { typedAxios } from "./typedAxios" export const getDocument = async ( { documentId , } : // 特定のqueryパラメヌタが欲しい時にapiの関数から匕数の型を抜き出す必芁がある。 Parameters< typeof typedAxios.v2.getV2Document >[ "0" ]) => { return await typedAxios.v2.getV2Document( { documentId , } ) } 実装䟋 import { Api } from "./api" const typedAxios = new Api() // 認蚌 typedAxios.v2.postV2Authentication( { email , password , } ) // ドキュメント取埗 typedAxios.v2.getV2DocumentDocumentId(documentId) ラむブラリ比范たずめ 消去法的に openapi-ts を採甚したした。 以䞋の懞念が他ラむブラリのノックアりトファクタヌでした。 openapi-typescript 呌び出し時の認知負荷の高さ swagger-typescript- api JSON ファむルに䜿われおいる文字を気にする必芁がある 型の流甚性が䜎い 導入の前凊理 冒頭にあった課題を払拭するために以䞋の前凊理が必芁です。 OpenAPI 2.0(Swagger) → OpenAPI 3.0 の倉換 openapi. json の merge snake_case ↔ camelCase の倉換 OpenAPI 2.0(Swagger) → OpenAPI 3.0 の倉換 API クラむアントの自動生成ラむブラリは OpenAPI 3.0 圢匏であるこずを想定しおいるため、 前凊理ずしお swagger2openapi ずいうラむブラリで OpenAPI 2.0(Swagger) → OpenAPI 3.0 ぞ倉換したした。 npx swagger2openapi swagger.json -o openapi.json 1 コマンドでキレむに 2 ç³» →3 系になっおくれお嬉しかったです。 openapi. json の merge openapi-ts が読み蟌める JSON ファむルは 1 ぀なので、2 ぀の API 定矩 JSON を merge したす。openapi. json の䞭には様々なプロパティがありたすが、merge したいのは以䞋の 2 ぀です。 path: 各 endpoint ず HTTP method 等の情報が定矩 components: 具䜓的な スキヌマ を内包 䞊蚘を単玔に merge するこずで欲しい json が手に入りたした。 import * as openapiJson1 from "openapi-1.json" import * as openapiJson2 from "openapi-2.json" import fs from "fs" /** * openapi.jsonをマヌゞしお新芏ファむルずしお出力 */ const mergedJson = { ...openapiJson1, paths : { ...openapiJson1.paths, ...openapiJson2.paths, } , components : { ...openapiJson1.components, ...openapiJson2.components, } , } fs.writeFileSync( "merged.json" , JSON . stringify (mergedJson, null , 2 )) snake_case ↔ camelCase の倉換 FE のコヌディングスタむルが camelCase なのに察しお BE は snake_case です。この乖離に぀いおは API コヌルのパラメヌタ䜜成時やレスポンス受け取り時に倉換をする必芁がありたす。 API コヌル時に郜床倉換するのは認知負荷が高いため共通凊理に含めるこずにしたした。 共通凊理に含めるデメリットずしお以䞋がありたす。 倉換ナヌティリティの開発・メンテナンスの手間 ランタむム䞊の倉換凊理によるオヌバヌヘッド しかし䞊蚘よりも開発者䜓隓の方が䟡倀があるず刀断したした。 たた重芁なポむントずしお API クラむアントの自動生成前の API 定矩 JSON にもケヌス倉換を斜したした。぀たり API 定矩 JSON の時点でパラメヌタやレスポンスを camelCase にしおおきたした。これをしないず生成埌の API クラむアントが型補完で snake_case を芁求しおしたうので type error になりたす。openapi. json の時点でケヌス倉換ができるず関数の匕数ず返り倀の型ずしおは camelCase で出力しおくれたす。あずは axios の interceptors に倉換凊理を入れるだけです。 axios の interceptors import { client } from "client.gen" // 自動生成されたaxiosのクラむアント client.setConfig( { baseURL : "/" } ) /** リク゚ストパラメヌタをsnake_caseに倉換 */ client.instance.interceptors.request.use(( request : InternalAxiosRequestConfig < any >) => { // snake_caseぞの倉換凊理 return request } ) /** レスポンスデヌタをcamelCaseに倉換 */ client.instance.interceptors. response .use(( response : AxiosResponse < any , any >) => { // camelCaseぞの倉換凊理 return response } ) export { client } 本筋から脱線: 型の䞊曞きでケヌス倉換 BE が生成した 参照元 の JSON を FE の郜合に合わせお倉曎しおしたうず思わぬ䞍郜合が起きるのではず懞念がありたした。そのため型の䞊曞き等も詊しおみたした。 いざ詊すず生成埌の関数や型に察しおの TypeScript 䞊でのケヌス倉換はしんどいです。䟋えばパスパラメヌタの型を snake_case から camelCase に倉換するだけでも埌述の耇雑な型が必芁になりたす。たたランタむムで実行されるコヌドず比范しお型に察しおの怜蚌は難しいです。そのためこの耇雑な実装よりかは JSON を曞き換える方が劥圓ず考えたした。 型倉換の䞀郚 type Snake2CamelString < T extends string > = T extends ` ${ infer R } _ ${ infer U } ` ? ` ${ R }${ Capitalize< Snake2CamelString < U >> } ` : T // keyをsnake_case → camelCase type Snake2Camel < T > = T extends any [] ? Snake2Camel < T [number]>[] : T extends object ? { [ K in keyof T as Snake2CamelString < string & K >]: Snake2Camel < T [K]> } : T // httpメ゜ッド type Methods = "get" | "post" | "put" | "patch" | "delete" // endpointのURL type Endpoint < M extends Methods > = { [ Key in keyof paths ]: M extends keyof paths [Key] ? Key : never } [keyof paths] // path parameter type PathParams < M extends Methods > = { [ Key in Endpoint < M >]: M extends keyof paths [Key] ? paths [Key][M] extends { parameters : { path : infer T } } ? T extends { [ key : string ]: string | number } ? T : never : never : never } [Endpoint<M>] // 最終的に欲しいpathParamsの型。 // これ以倖にもqueryParamsやrequestBody,responseBodyにも䌌たようでちょっず違う倉換をするgenericが必芁 Snake2Camel< PathParams < M >> 脱線終わり。 before / after 今たでは API コヌルの前埌にクラスを通しおいたした。 API クラむアント自動生成埌は関数を呌ぶだけでシンプルです。BE で砎壊的倉曎も type error ずしお怜知できたす。 今たでの呌び出しむメヌゞ import { repositoryFactory } from "./repositoryFactory" // parameterのバリデヌションやケヌス倉換 import { DocumentEntityClass } from "./documentEntityClass" // responseのケヌス倉換やオブゞェクト化 const documentGetRequest = repositoryFactory . documentDiff . getParam () documentGetRequest . documentId = documentId const documentEntity = new DocumentEntityClass () const response = await repositoryFactory . documentDiff . get ({ documentGetRequest }) documentEntity = response . data 新しい API コヌル import { typedAxios } from "./client" import { getV2DocumentDocumentId } from "./generated/sdk.gen" getV2DocumentDocumentId( { client : typedAxios, path : { documentId } , } ) たずめ API 定矩 JSON から型だけを出力しおも、認知負荷の䜎い API コヌルの実珟は難しいこずが分かりたした。型のみでは結局、認知負荷を䞋げるために共通凊理に耇雑さが必芁になっおしたいたす。 共 通化 するのではなく、シンプルな成果物に倉換できる機構が必芁でした。頑匵っお共 通化 し、むンタヌフェヌスがシンプルになれば実装が捗るず思っおいたしたが、耇雑さのシワ寄せずしお AI のコヌド生成粟床に圱響するずいう気づきも埗るこずができたした。 たた今回の遞定においおは早めにプロトタむプを実装したこずが良かった点だず振り返っお思いたす。やりたいこずや実珟したいこずの䞭栞はがんやりありたしたが、実際に手を動かしおみるこずで比范するべき芳点や実装むメヌゞが具䜓化されたした。ドキュメントに蚘茉のない思わぬ欠点を早めに怜知したこずも収穫でした。 ご粟読ありがずうございたした。こうした技術的な意思決定のプロセスや、MNTSQ の日々の開発の進め方にご興味を持っおいただけた方は、ぜひお気軜にカゞュアル面談でお話ししたしょう。 careers.mntsq.co.jp
アバタヌ