nozaki takahiroのテックブログ

AWS Pricing Calculatorの一括インポートテンプレートファイルの改訂版作ってみた

サービス概要

AWS Pricing Calculatorの一括インポートとは、AWSが用意したExcelのテンプレートに従って記入するだけでCalculatorを自動生成するツールです。
ただし、現時点で適応範囲はEC2とEBSに限定されています。

aws.amazon.com

 

まずは手順に従って用意されたテンプレートをダウンロードしてみます。

 

ダウンロードしたファイルがこちら

目的・やりたいこと

で、上記のファイルを開いてみて、

  • 全部英語
  • フォントがやや小さくて見にくい

など、若干の使いにくさを感じた人も少なくないはず

そこで、このテンプレートファイルを日本人でも使いやすくカスタマイズして見ました!

列をいじると動作しなくなるので、残念ながら列名は英語のままです。

そのファイルがこちら!

https://docs.google.com/spreadsheets/d/1BHQF50m0Mb7TZebaXTPfSW1sMZEe7qtD/edit?usp=sharing&ouid=110681056841624021809&rtpof=true&sd=true

・改良その1

まずInputsシートについて、フォントを日本人馴染みのメイリオにし、サイズも12と若干大きくしてみました。

あといくつか入力例のサンプルも記入してあります。

・改良その2

次に左側の「説明 日本語」シート。こちらは記入方法を日本語でかなりわかりやすく記載してあります!

改良時の注意点も記載しています。

 

どうでしょうか?オリジナルよりだいぶ使いやすくなったはずです。

あとは開発が止まっているのか?現在まだEC2とEBSにしか対応していませんが、S3とかそろそろ他の周辺サービスのCaliculatorにも対応して欲しいところですね!

BedrockにPowerPointファイルの中身の日本語部分を校正してもらう

背景

出来上がったお客様向けの提案書(.pptx形式のPowerPointファイル)を、仕上げとして体裁レビューを依頼するとします。これはあくまで最後に「体裁」を整えるという意味で、見栄えや統一感、日本語文章の校正、表のズレなど非常に多義に渡ります。
例えば以下は体裁レビューの一例です。

P2:目次の4.プロジェクト管理のページがズレてます
P15:6行目「(Linux only) 」→()全角にと、Linuxonlyでは無いでしょうか?
P17:初期費用の合計だけ太文字になっています
P17:月額費用表の合計が無いのですがあった方がいいかと思います
P20:9行目「(含みます)」→()全角に
P23:見出しの「2−2」→2−3

問題は人が目視でやっているため、レビュー者の主観やレビュー者によって指摘点がまちまちだということ、もちろん人的コストも掛かっています。

目的・やりたいこと

そこで、この体裁レビューをBedrockにやらせるAIレビューを挟むのはどうかと考えました。もちろん最初のうちはLLMやプロンプトの未熟さで人による体裁レビューほどの正確・柔軟な指摘は難しいかもしれませんが、最初は併用⇨徐々に改善を重ねて人に置き換わることができればと思っています。

生成AIレビューの問題点

ファイルを噛ませるくらいであれば、巷の無料生成AIサイトに投げてレビューして貰えばいいのでは?ということで幾つか試しましたが、次の問題点が浮かび上がりました。

  • ファイル投稿に対応していないものが多い(Perplexity、Geminiなど)
  • Bedrockはファイルに対応しているが、pptx拡張子には対応していない
    チャットのプレイグラウンド

    Chat with your document

  • ChatGPTなどはpptxファイルに対応しているが、プロンプトが悪いのか、以下のような差し障りのない指摘しか返ってこないことが多い

全体的に文の流れやフォーマルなトーンは適切です。しかし、一部の表現がやや長く、文のリズムが崩れがちなので、もう少し簡潔にまとめると、さらに読みやすくなります。
提案書として、技術的な内容の正確性が重要ですが、ビジネスライクな敬語や表現を使用することで、より丁寧で洗練された印象を与えることができます。

  • 短文の文章であれば、文章部分を直接貼り付けて生成AIに回答させることもできるが、提案書は50ページ(2MB)くらいあるのがザラ
  • ファイルサイズに制限がある
  • 無料版だと1日の試行回数に制限がある
  • 何より無料でWebで公開されている生成AIサイトに機密ファイルを投げることがなんとなくセキュリティ的に不安
  • Slack内で気軽にファイルを貼り付けたい

そこで、SlackからAWS(Lambda⇨Bedrock)と連携して、レビュー結果をSlackに表示させるというアプリを作ることにしました!

対象となる技術

条件(導入にあたっての前提事項)

  • LLMモデルには当初現時点で文系思考日本語最強LLMと言われる「Claude 3 Opus」を使おうと思いましたが、比較してみた結果、最新の高性能「Claude 3.5 Sonnet」を採用することにしました。この辺りはもっといいモデルが出たら随時置き換えていこうと思います。
  • Lambdaに付与したBedrock_S3.roleには、次のロールをアタッチ
    • AmazonBedrockFullAccess(AWS 管理)
    • AWSLambdaBasicExecutionRole(AWS 管理)

参考URL

注意事項

  • コードに関しては絶対にこれが正しいという保証はありません。余分な処理やモジュールも含まれてしまっていると思います。あくまで参考程度に留め、自分でカスタマイズして使ってください。
  • 今回はSlackアプリ部分にBoltやLazyリスナーという仕組みを使用しています。Slackアプリに特化した部分となるため、本格的に説明すると本質からズレるため、詳細は上記リンク先を参考にしてください。

概要図

作業の流れ

事前作業

1.機密情報を含まない架空の提案書を用意

2.以下の内容の3つのファイルを作成

requirements.txt
boto3==1.35.5
botocore==1.35.5
jmespath==1.0.1
pillow==10.2.0
python-dateutil==2.9.0.post0
s3transfer==0.10.0
six==1.16.0
requests
python-pptx
python-lambda
python-dotenv

slack_sdk
slack_bolt
Dockerfile
FROM public.ecr.aws/lambda/python:3.12

# コンテナの作業ディレクトリを設定
WORKDIR /var/task

RUN echo sslverify=false >> /etc/yum.conf

# libreoffice に必要なパッケージをインストール
RUN dnf -y install tar gzip zlib freetype-devel make bison libxslt wget\
  gcc ghostscript lcms2-devel libffi-devel libjpeg-devel libtiff-devel \
  libwebp-devel openjpeg2-devel tcl-devel tk-devel xorg-x11-server-Xvfb \
  zlib-devel java ipa-gothic-fonts ipa-mincho-fonts ipa-pgothic-fonts ipa-pmincho-fonts \
  && dnf clean all

# libreoffice をインストール
RUN wget https://download.documentfoundation.org/libreoffice/stable/24.2.5/rpm/x86_64/LibreOffice_24.2.5_Linux_x8
6-64_rpm.tar.gz --no-check-certificate && \
    tar -xvzf LibreOffice_24.2.5_Linux_x86-64_rpm.tar.gz && \
    cd LibreOffice_24.2.5.2_Linux_x86-64_rpm/RPMS && rpm -iUvh *.rpm && \
    rm *.rpm && cd ../
    
RUN dnf -y install cairo

# pdf2img に必要なパッケージをインストール
RUN dnf -y install poppler-utils python3 python-pip

# 必要なPythonパッケージをインストール
COPY ./requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org

# 必要なファイルをコンテナにコピー
COPY ./app.py ${LAMBDA_TASK_ROOT}

ENV HOME=/tmp
ENV SLACK_BOT_TOKEN="xoxb-****"
ENV SLACK_SIGNING_SECRET="bc****"

# ハンドラー情報
CMD ["app.handler"]

Dockerfileのポイント

  • libreofficeをパッケージインストール
    libreofficeyumなどで入れると古いバージョンで入ってしまい、最新のpython-pptx 1.0.2に対応できないため、LibreOffice 24.2.5のRPMパッケージをダウンロードしてきて入れる必要がありました。
  • FROM public.ecr.aws/lambda/python:3.12
    LibreOffice 24.2.5を入れるためにDockerのOSをAmazon Linux 2023にする必要があるため、それに対応したpython 3.12をここで指定しています。

▼app.py

import json
import boto3
import re
import os
import requests
from pptx import Presentation
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from io import BytesIO
from slack_bolt import App,Ack
from slack_bolt.adapter.aws_lambda import SlackRequestHandler

# 環境変数推奨(トークン直書きは非推奨)
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET")

# Slack APIクライアントを初期化
client = WebClient(token=SLACK_BOT_TOKEN)

# Boltアプリケーションを初期化
app = App(
    token=SLACK_BOT_TOKEN,
    signing_secret=SLACK_SIGNING_SECRET,
    process_before_response=True
)

# Bedrock呼び出し
bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-west-2")

def just_ack(ack):
    ack()

def handle_mention(event, context):
    # JSON文字列をPythonの辞書に変換する
    channel = event.get("channel")
    type = event.get("type")
    
    # スレッドのタイムスタンプを取得
    thread_ts = event.get("thread_ts")
    # thread_ts が None の場合、元のメッセージのタイムスタンプを使用
    if thread_ts is None:
        thread_ts = event.get("ts")

    # スレッドメッセージを取得
    response = client.conversations_replies(channel=channel, ts=thread_ts)
    message = response['messages'][0]

    if "files" in message:
        for file in message["files"]:
            # ファイルのタイプを確認# 添付ファイルがpptxファイルの場合
            if file["filetype"] == "pptx":
                file_url = file["url_private_download"]
                # ファイルをダウンロード
                response2 = requests.get(file_url, headers={"Authorization": f"Bearer {client.token}"})
    
                # レスポンスのステータスコードをチェック
                if response2.status_code == 200:
                    # ファイルコンテンツを取得
                    file_content = response2.content

    pptx_file = file_content
    slides_text = extract_text_from_pptx(pptx_file)
    corrected_text = process_slides(slides_text)

    # Slackのチャンネルに投稿
    client.chat_postMessage(
        channel=channel,
        thread_ts=thread_ts,
        text=f"校正結果:\n{corrected_text}"
    )

    return {"statusCode": 200}               

# pptxファイルからテキストを抽出する関数
def extract_text_from_pptx(pptx_content):
    prs = Presentation(BytesIO(pptx_content))
    slides_text = []
    
    for i, slide in enumerate(prs.slides, 0):
        slide_text = f"スライド{i}:\n"
        for shape in slide.shapes:
            if hasattr(shape, 'text'):
                slide_text += shape.text + '\n'
            if shape.has_table:
                for cell in shape.table.iter_cells():
                    for text in cell.text.splitlines():
                        slide_text += text + '\n'
        slides_text.append(slide_text)
    
    return slides_text

# スライドを処理する関数
def process_slides(slides_text, batch_size=5):
    results = []
    for i in range(0, len(slides_text), batch_size):
        batch = slides_text[i:i+batch_size]
        batch_text = "\n\n".join(batch)
        result = bedrock_check(batch_text)
        
        # 不要な回答を除去
        filtered_result = filter_unnecessary_responses(result)
        
        if filtered_result.strip():  # 空の結果を除外
            results.append(filtered_result)
        time.sleep(1)  # APIリクエストの間隔を空ける
    
    return "\n\n".join(results)

# Bedrockで校正させる関数
def bedrock_check(text):
    prompt = f"""あなたは日本語の校正と文書フォーマットの専門家です。
以下の指示に従って、パワーポイントファイル内のテキスト部分を抽出したテキストファイルの内容を徹底的にチェックしてください:

1. 内容チェック:
   - 公開すべきでない情報や、社内限りの情報が含まれていないか
   - お客様に対して失礼または不適切な表現
   - 業界用語や専門用語の適切な使用

2. 文法チェック:
   - 敬語(尊敬語、謙譲語、丁寧語)の適切な使用
   - 誤字脱字(スペルミスや漢字の間違いがないか、句読点の使用が適切か)
   - 文法的な誤り

3. スタイルチェック:
   - 一貫性のある文体
   - 簡潔で明瞭な表現
   - 自然な日本語表現になっているか
   - 用語の使用が文書全体で統一されているか

4. フォーマットチェック:
   - フォントの一貫性(種類、サイズ)
   - 太字、斜体、下線の適切な使用
   - 文字間隔、行間の統一性
   - 箇条書きや番号付きリストの一貫性

5. お客様に対する配慮:
   - 失礼または不適切な表現がないか
   - ビジネス文書として適切な丁寧さが保たれているか

6. テーブルチェック
   - 表内に数値があった場合、その数値の合計などが合っているか
   - 誤字脱字がないか

各スライドを順番にチェックし、指摘事項がある場合のみ以下の形式で報告してください。

スライド番号:
- [カテゴリ] 具体的な問題点と修正案

チェック対象のテキスト:

{text}
"""

    content_prompt = {
        "type": "text",
        "text": prompt,
    }
    content = [content_prompt]
    messages = [
        {"role": "user", "content": content},
    ]

    body = json.dumps(
        {
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 8192,
            "temperature": 0,
            "messages": messages,
        }
    )

    response = bedrock_runtime.invoke_model(
        body=body,
        modelId="anthropic.claude-3-opus-20240229-v1:0",
 #        modelId="anthropic.claude-3-5-sonnet-20240620-v1:0",
        accept="application/json",
        contentType="application/json",
    )
    response_body = json.loads(response.get("body").read())
    corrected_text = response_body['content'][0]['text']

    return corrected_text

# 不要な指摘を排除する関数
def filter_unnecessary_responses(text):
    # スライド番号ごとに分割
    slides = re.split(r'(スライド\d+:)', text)
    
    filtered_slides = []
    for i in range(1, len(slides), 2):
        slide_header = slides[i]
        slide_content = slides[i+1] if i+1 < len(slides) else ""
        
        # "指摘事項はありません" や "特に問題ない" などの不要な回答を含まないスライドのみを保持
        if not re.search(r'指摘事項(は|が)ありません|特に問題(は|が)ない|該当なし|指摘事項なし', slide_content, re.IGNORECASE):
            filtered_slides.append(slide_header + slide_content)
    
    return "\n".join(filtered_slides)

# Lazy listeners
app.event("app_mention")(ack=just_ack, lazy=[handle_mention])

# Lambda用ハンドラー
def handler(event, context):
    header = event.get('headers', {})
    # ヘッダーにx-slack-retry-numが入っていたらリトライなので終了にする
    if "x-slack-retry-num" in header:
        return 200
    else:
        slack_handler = SlackRequestHandler(app=app)
        return slack_handler.handle(event, context)

app.pyのポイント

pip install slack_bolt

import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

app = App(token=os.environ.get("SLACK_BOT_TOKEN"))
  • Slackからファイルをダウンロード
file_url = file["url_private_download"]
# ファイルをダウンロード
response2 = requests.get(file_url, headers={"Authorization": f"Bearer {client.token}"})

【Python】SlackbotでSlackからファイルをダウンロードする - みやびのどっとぴーわい を参考に、まずfile["url_private_download"]でファイルのURLを取ってきます。次に、そのURLを指定して実際のファイルをダウンロードします。その際にAPIトークンの指定が必要になります。headers={"Authorization": f"Bearer {client.token}"}

pip install python-pptx

from pptx import Presentation

prs = Presentation() # Presentationクラスをインスタンス化
  • Bedrockのプロンプト
prompt = f"""あなたは日本語の校正と文書フォーマットの専門家です。
以下の指示に従って、パワーポイントファイル内のテキスト部分を抽出したテキストファイルの内容を徹底的にチェックしてください:
1. 内容チェック:
〜〜
2. 文法チェック:
〜〜

ここはかなり苦労しましたし、今回の「日本語校正」というテーマでは最も肝になる部分です。箇条書きが長すぎると、

  1. 内容チェック:問題ありません
  2. 文法チェック:問題ありません
    というように全部「問題ありません」で終わってしまいます。ですのでチェックポイントの羅列は長すぎず短すぎずの程よいバランスを見つけることが大事です。
  • 回答のランダム性を極力減らすため、Bedrockのtemperature(温度)は0に設定
    温度:ランダム性の度合いを調整するために使用される数値(デフォルト0.9、0〜5)
    生成モデルからのサンプリングにはランダム性が組み込まれているため、同じプロンプトでも世代ごとに異なる出力が生成される場合があります。応答のランダム性を減らすには、より低い値を使用します。

  • Lazy listeners
    Lazy(怠慢な)リスナーとは、非同期処理を別のLambda関数へ割り当てる機能です。
    Lazy リスナー(FaaS) | Bolt for Pythonに記載されているように、「lazy=」で非同期処理関数を呼び出しています。

app.event("app_mention")(ack=just_ack, lazy=[handle_mention])
  • X-Slack-Retry-Num
    X-Slack-Retry-NumというHTTPヘッダーに「X-Slack-Retry-Num:1」のようにリトライの回数の情報が含まれているので、このヘッダーが含まれているときは200を返して終わらせています。
header = event.get('headers', {})
if "x-slack-retry-num" in header:
     return 200

Slackアプリの登録

slack apiにてアプリを登録します。

1.Create an appで「From scratch」を選びます。

2.アプリ名と導入するワークスペースを選んで[Create App]

3.Slackから呼び出すURLを決める
アプリは、選択したURLでSlackのイベント(ユーザーがリアクションを追加したり、ファイルを作成したときなど)の通知を受け取るようにサブスクライブできます。この際に用いるリクエストURLを決めるため、ここは一旦保留して、後のLambdaが準備できた後にまた戻ってくることにします。

手順

コードをECRリポジトリにPUSH

手元のMacかCloud 9どちらでもいいので、用意した3つのファイルを次のように配置し、ビルドからPUSHまでdockerコマンドが使えるdocker環境で下記作業を行います。
pptxディレクト
├Dockerfileファイル
├requirements.txtファイル
└app.pyファイル

1.ビルド

% docker build -t nozaki-rep .
[+] Building 26.6s (16/16) FINISHED                                            docker:desktop-linux
 => [internal] load build definition from Dockerfile                                           0.0s
〜〜〜
 => => naming to docker.io/library/nozaki-rep                                                  0.0s 
                                                                                                    
What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview 

2.タグ付け

% docker tag nozaki-rep:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/nozaki-rep

3.ECRログイン

% aws ecr get-login-password --region ap-northeast-1 --no-verify | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com

urllib3/connectionpool.py:1063: InsecureRequestWarning: Unverified HTTPS request is being made to host 'api.ecr.ap-northeast-1.amazonaws.com'. 
Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings
Login Succeeded

4.PUSH

% docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/nozaki-rep

Using default tag: latest
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/nozaki-rep]
cf7c7eefe9c2: Pushed
456541b5bfba: Pushed 
c8d68278ba1b: Pushed 
5ac14a4ce153: Pushed 
efc06e5766e7: Pushed 
93d6f734e916: Pushed 
58c3eb3010cf: Pushed 
5f142f1e76fb: Pushed 
32d6a65b3647: Pushed 
5f70bf18a086: Pushed 
0274404cdadc: Layer already exists 
3afe23385c51: Layer already exists 
c88f3983c839: Layer already exists 
f5e83de1dfd4: Layer already exists 
0d39f7236e4f: Pushed 
0044b36762be: Layer already exists 
latest: digest: sha256:d*******

 

Lambda環境の作成

1.Lambda > 関数 > [関数の作成]

2.関数の作成
コンテナイメージからデプロイするため、[コンテナイメージ]を選択し、以下のように入力

3.実行ロール
条件で用意していたBedrock_S3.roleを選択

最後に[関数の作成]

4.[イメージ]タブ > [新しいイメージをデプロイ]

5.イメージを参照
ECRイメージリポジトリで先ほど作成した「nozaki-rep」を選択
イメージタグが「latest」になっている[イメージを選択]して[保存]

「更新中です」と表示され、1分くらいかかるので待つ

6.Lambda関数名など側を作成したら、設定⇨関数URLで「関数URLを作成」してLambda関数のURLを生成します。

7.認証はNone(なし)で大丈夫です。

8.[保存]すると、以下のような関数URLが生成

その他Lambdaの設定値調整
タイムアウトと再試行数を設定して、Lambda関数が複数回実行される不具合を修正してみた | DevelopersIO を読むと、タイムアウトが短く再試行回数があるとLambdaが何度も実行されてしまうようなので、パラメータを調整します。

  • タイムアウト
    デフォルトの3秒では短すぎるので、10分と十分長くしておきました。実際6分くらいかかるので

  • 非同期呼び出し再試行数
    2回になっていたので0に。また最大有効期間も60時間じゃ長すぎるので10分にしました。

     

再びslack api

1.アプリのFeatures⇨Event Subscriptions⇨Enable EventsをOnにします。

2.Request URLに前手順3.で生成した関数URLを入れるのですが、その前に少しコードをいじります。
イベントが発生すると、HTTP POSTリクエストがこのURLに送信されます。URLを入力するとすぐに、challengeパラメータを含むリクエストが送信されます。エンドポイントはこのchallenge値で応答する必要があるため、次のコードを追加します。

def lambda_handler(event, context):
    data = json.loads(event["body"])
    if "challenge" in data:
        return {"statusCode": 200, "body": data["challenge"]}

これでVerifiedになりました。

一旦これが通りさえすれば、後から上記のコードを外しても構いません。

3.Subscribe to bot events
アプリは、ボット ユーザーがアクセスできるイベント(チャネル内の新しいメッセージなど)を受信するためにサブスクライブできます。つまり、どんな時にslack botを発動させるかをここで定義します。アプリをメンションした時だけ発動する「app_mention」を選択し、[Save Changes]

4.OAuth & Permissions
Lambdaのコード内で使うことになるBot User OAuth Token(xoxb〜で始まる)をメモしておきます。

次に、slack botに何をできるようにさせるかの権限を付与します。

ここでは以下を付与しました。

  • app_mentions:read
    アプリが参加している会話で、アプリについて直接言及しているメッセージを表示する
  • calls:read
    進行中および過去の通話に関する情報を表示
  • calls:write
    ワークスペースで通話を開始および管理
  • channels:history
    アプリが追加されたパブリックチャネルのメッセージやその他のコンテンツを表示
  • channels:join
    ワークスペースのパブリックチャンネルに参加
  • channels:read
    ワークスペース内のパブリック・チャンネルの基本情報を表示
  • chat:write
    メッセージをアプリとして送信
  • groups:history
    アプリが追加されたプライベートチャネルのメッセージやその他のコンテンツを表示
  • groups:read
    アプリが追加されたプライベートチャンネルの基本情報を見る
  • im:history
    アプリが追加されたダイレクト メッセージ内のメッセージやその他のコンテンツを表示
  • im:read
    アプリに追加されたダイレクトメッセージの基本情報を見る
  • incoming-webhook
    Slackの特定のチャンネルにメッセージを投稿
  • mpim:history
    アプリが追加されたグループダイレクトメッセージのメッセージやその他のコンテンツを表示
  • mpim:read
    アプリが追加されたグループダイレクトメッセージの基本情報を見る
  • users:read
    ワークスペース内のユーザーを表示
  • users:write
    アプリのプレゼンスを設定
  • files:read
    アプリが追加されているチャンネルや会話で共有されているファイルを表示

5.ワークスペースへのアプリのインストールが完了すると、OAuthトークンが自動的に生成されます。これらのトークンを使用してアプリを認証します。

 

これでようやくBot User OAuth Tokenが生成されるため、メモしておきます。

6.アプリをチャンネルに参加

 

苦労ポイント:Slack特有の3秒ルール

ファイルが2MBあってPostされるまで時間かかるせいか、最初のSlack⇨Lambdaで何度もLambdaをInvocationしてるようで、同じレスが何度も入ってしまっていました。

この辺りの問題については、Slack Events APIの再送仕様と回避方法まとめ(Serverless on AWS) | DevelopersIO がよくまとまっています。
システム的にSQSを挟んでキューイングしたり、LambdaからLambdaをキックする方法もあるようですね。
他に対処的だけど、リトライ無視するようにしてます。

def ignore_retry_request(request, ack, next):
    """リトライを無視する関数"""
    if "x-slack-retry-num" in request.headers:
        return ack()
    next()

app.use(ignore_retry_request)

こちらはif文を使ってヘッダーを見て回避しました。

モジュール側で実装されているLazy Listeners機能を使った小技は、Bolt for Python が FaaS での実行のために解決した課題 #AWS - Qiita が詳しかったです。

以上により、CloudWatchでもこのように2回の発動で済んでいます。

 

 

目的達成!

@PowerPoint Proofreadingをメンションしてpptxファイルを投稿します。

すると、5分経過後、そのスレッド内にリプライ(2回に分けてるので2件)して、日本語文章の校正をして指摘してくれるようになりました!

モデルは結局どれがいいの?

ここでClaude 3 opusとClaude 3.5 sonnetどちらのAnthropicモデルを採用すべきか、その指摘結果を比較して判断することにしました。50スライド全部比較すると大変なので、最初の10スライド分だけ見てみます。

LLM Claude 3 opus Claude 3.5 sonnet
スライド1 [文法チェック] 「弊社は、2003年に創業してから常に、一歩進んだソリューションを提供し続けるパートナーとして、大企業に引けを取らない高品質な開発を実現してまいりました。」の文が長く、読みにくいです。文を分割することをおすすめします。修正案:「弊社は、2003年の創業以来、常に一歩進んだソリューションを提供し続けるパートナーとして歩んでまいりました。大企業に引けを取らない高品質な開発を実現してきたことが弊社の強みです。」 [文法チェック] "引けを取らない"は"引けを取らない"が正しい表記です。
[スタイルチェック] "パブリッククラウドAWS, Azure, GCP)"の括弧内はカンマの後にスペースを入れるのが一般的です。修正案: "パブリッククラウドAWS, Azure, GCP)"
スライド2 [フォーマットチェック] 目次の各項目の字下げにばらつきがあります。インデントを統一することをおすすめします。
[フォーマットチェック] ページ番号の位置が統一されていません。ページ番号は右下に配置するのが一般的です。
[フォーマットチェック] 目次の番号付けが一貫していません。例えば、"4. プロジェクト管理"の後に"5. 監視・運用保守"がありますが、その間に番号のない項目があります。
[スタイルチェック] "Appendix"は日本語文書では"付録"とするのが一般的です。
スライド4 - [文法チェック・スタイルチェック] 「弊社対応方針」の箇条書きの文末が統一されていません。「〜致します」「〜しております」「〜致します」となっています。「〜いたします」で統一することをおすすめします。
[誤字脱字] 「下記2通り提示させていただきす」の「きす」は誤字です。「きます」が正しいです。
[文法チェック] "CLOUDからAWSへの移行"の"CLOUD"は小文字が適切です。
[スタイルチェック] "F社"という表記は、実際の社名を伏せているものと思われますが、文書全体で一貫して使用されているか確認が必要です。
[文法チェック] "提示させていただきす"は"提示させていただきます"の誤りです。
[フォーマットチェック] 箇条書きの形式が一貫していません。一部の項目にのみ丸点(・)が使用されています。
スライド5 [文法チェック] 「移行検討中の対象サーバは下記100台を想定しております。」の文末表現が不適切です。「想定しています。」や「想定しております。」に修正することをおすすめします。
[文法チェック] 表内の「Zabix監視サーバ」のスペルミスがあります。「Zabbix監視サーバ」が正しいスペルです。
[スタイルチェック] 表内の文章の末尾が「。」で統一されていません。文章の終わりには「。」をつけるように統一することをおすすめします。
[内容チェック] "Zabix監視サーバ"は"Zabbix監視サーバ"の誤りだと思われます。修正をお勧めします。
[フォーマットチェック] 表の列幅が不均一です。特に"備考"列が狭く、内容が読みにくくなっています。列幅を調整することをお勧めします。
スライド6 [文法チェック] 「本プロジェクトにおけるスケジュールは下記の通りを想定しておりす。」のスペルミスがあります。「想定しております。」が正しい表現です。
[フォーマットチェック] 表内のフォントサイズがばらばらで統一されていません。フォントサイズを統一することをおすすめします。
[文法チェック] "想定しておりす。"は"想定しております。"の誤りです。修正が必要です。
[フォーマットチェック] 表の列幅が不均一で、特に月の列が狭くなっています。調整をお勧めします。
[スタイルチェック] "9月"のみ全角数字になっています。他の月と統一して"9月"とすることをお勧めします。
スライド8 [文法チェック] 「移行進捗に必要なミーティングへの同席、情報整理・提供」が箇条書きになっていません。他の項目と同様に箇条書きにすることをおすすめします。
[フォーマットチェック] 想定参画メンバーの人数表記が数字のみになっています。「名」を追加して「100名」のように統一することをおすすめします。
[フォーマットチェック] "想定参画メンバー"の表で、人数が100名、101名となっていますが、これは誤りの可能性が高いです。実際の想定人数に修正することをお勧めします。
スライド9 [文法チェック] 「移行進捗に必要なミーティングへの同席、情報整理・提供」が箇条書きになっていません。他の項目と同様に箇条書きにすることをおすすめします。 [フォーマットチェック] "想定参画メンバー"の表で、人数が100名、200名となっていますが、これも誤りの可能性が高いです。実際の想定人数に修正することをお勧めします。
表のフォーマットが一貫していない箇所があります。特に列幅や行高さの調整が必要な箇所があります。
数値の表記(特に人数)に不自然な点があります。実際の数値を確認し、修正することをお勧めします。
スライド10 [内容] "Zbix監視サーバ"は一般的な用語ではないため、"Zabbix監視サーバ"に修正することを提案します。
[内容] "New Relic"は製品名のため、"New Relic"とすべきです。
[スタイル] "致します"は少し硬い表現なので、"いたします"に変更することをおすすめします。
[フォーマット] 箇条書きの記号が"※"となっていますが、他のスライドでは"・"が使用されているため、統一することを推奨します。
[誤字] "Zbix" → "Zabbix"
[文法] "検討致します。" → "検討いたします。"(謙譲語の使用)
[一貫性] "構成案①" と "構成案②" の後に句点がないため、追加するか両方とも省略するか統一すべき

どうでしょうか?お?と思う際立った指摘だけ太字にしてみたのですが、Claude 3.5 sonnetの方がその傾向が顕著だった気がします。

でもきっとお高いんでしょう?

試算してお見積もり出してみました。実質的な料金が発生しそうなのはLambdaとBedrockです。

  • Lambda
    体裁レビューが大体1日に1回、1ヶ月で20回=リクエスト数=20回/月
    1リクエストにつき5分かかるため、5分=5分/秒×60秒/分×1,000ミリ秒/秒 =300,000ミリ秒
    20リクエスト/月×300,000ms/リクエスト×0.001s/ms=6,000(秒/月)
    割り当てたメモリ: 256MB×1/1024 (GB/MB)=0.25GB
    0.25GB×6,000秒=1,500コンピューティング(GBs/月)
    Lambdaの無料利用枠には、毎月1,000,000件の無料リクエスト、毎月400,000GB秒のコンピューティング時間が含まれているため、無料利用枠に軽々と収まってしまう。
  • Bedrock
    On-Demandのテキスト⽣成モデルでは、処理された⼊⼒トークンと⽣成された出⼒トークンごとに課⾦されます。
    基盤モデルを使用した生成 AI アプリケーションの構築 – Amazon Bedrock の料金表 – AWS のAnthropicのオンデマンド料金のClaude 3.5 Sonnetの行を見ると、
    入力トークン1,000個あたりの価格:0.003 USD
    1,000出力トークンあたりの料金:0.015 USD
    生成AIに聞いたところ、架空の提案書は約10,000トークン、Slackへのリプライ文章は約5,000トークンとなったことから、
    入力トークン料金=0.003(USD/1000トークン)×10,000トークン/1,000トークン×140(円/USD)=4.2円/リクエス
    出力トークン料金=0.015(USD/1000トークン)×5,000トークン/1,000トークン×140(円/USD)=10.5円/リクエス
    つまり、1リクエストにつき合計14.7円かかっている。これに20リクエスト/月を乗じると、294円/月

ということで、合計月額 300円 も行ってない計算になりました。

安全性(セキュリティ)

このアプリで唯一懸念することがあるとすれば、提案書の中身の漏洩です。実際にお客様に送信する予定の社外秘情報の提案書を生成AIに掛けるわけですから、情報漏洩などがあったら大変です。

  • Slack
    何か脆弱性があったりしない限りはSlackアプリに問題が生じることはないと思っています。
  • AWS
    Amazon Inspectorを有効にし、実行中のLambda関数とECRリポジトリにスキャンを行い、ソフトウェアの脆弱性と意図しないネットワークのエクスポージャーを検出するようにします。
  • Bedrock
    AWSはBedrockがインプットした情報を漏洩させることがないことを前から謳っているので、セキュリティ攻撃に備えておけばいいかと思います。この辺りは別の機会で別途Bedrock活用安全セキュリティガイドラインみたいなものを作成し、上司を安心させたいです。
    あとは個人情報を一切出さないようガードレールを設定し、提案書に野崎の個人情報をあえて含ませてみたところ、以下のレスポンスが返ってきたので、誤って個人情報を回答してしまわないことも確認できました。
  • [内容チェック] 個人情報(メールアドレス、電話番号、住所)が含まれています。公開すべきでない情報のため、削除または匿名化が必要です。

改善点

  • 全てを鵜呑みにしてはいけない
    例えば「「致します」は「いたします」と表記するのが一般的です。」などは特に従わなくてもよいどっちでもいい指摘だったりします。この辺りはプロンプトの工夫次第で改善の余地がまだまだありそうです。
  • ファインチューニング
    日本語の校正をするだけなのでこの辺りのトレーニングは特に不要そうな気はしますが、活用していくうちにこういった指摘をするようにしてほしいとか、この程度の知識は持っていてほしいなどAIに対する要望が出てくるかもしれません。

ユースケース・活用例

  • 提案書のレベルアップ

 今回はあくまでお客様へ提出する上で恥ずかしくない日本語を整えるという観点で体裁レビューだけを行いましたが、提案書の内容をもっとレベルアップさせることにも活用できると思います。例えば、本提案書でもっとアピールした方がいい部分、追加した方がいい部分、逆に削った方がいい不要な部分を指摘してもらうというような使い方です。

  • 他のフォーマット形式のファイルへ応用

 今回はpptxファイルに特化してその変換関数まで用意しましたが、本アプリを基にして活用すれば、色々なフォーマット形式のファイルをBedrockに食わせてその中の日本語文章部分の校正などを行わせることができるようになると思います。

 

まとめ

  • PythonPowerPointを操作するには、python-pptxという外部ライブラリが必要
  • プロンプトで文章校正を促すには、チェックポイントを箇条書きで羅列すると良い
  • Slack3秒ルールに注意。LazyリスナーとX-Slack-Retry-Numヘッダーで対策
  • 現時点で日本語文章に強そうなLLMClaude 3.5 sonnetと推測
  • Bedrockへの入力・Bedrockからの出力は、どのモデルプロバイダーとも共有されず、Titanまたはサードパーティのモデルをトレーニングすることはない
  • ガードレールを使って機密コンテンツの出力を防ぐ
  • 生成AIの指摘は参考程度にし、微調整で精度を高めていくのが良い

AWS Client VPNのOSごと接続設定まとめ

サービス概要

AWS Client VPNは、オンプレミスネットワークからAWSリソースに安全にアクセスできるマネージドクライアントベースのVPNサービスです。

  • OpenVPNベースの技術を使用し、エンドツーエンドの暗号化を提供
  • ACMとの統合により、証明書ベースの認証
  • AWS Directory Serviceとの統合により、既存のADを使用した認証
  • SSO(SAMLベースのフェデレーション認証)
  • Windows, macOS, Linux, iOS, Androidなど多くのプラットフォームをサポート

目的・やりたいこと

AWS Client VPNの接続手順をまとめるため、キャプチャを取る。
以下のクライアントOSからの接続を想定

対象者・ ユースケース

リモートアクセス

対象となる技術

条件(導入にあたっての前提事項)

  • 認証方式は今回は相互認証+AD認証

参考URL

注意事項

  • クライアントCIDR範囲は、関連付けられたサブネットVPCが配置されているCIDRのローカル、またはクライアントVPNエンドポイントのルートテーブルに手動で追加されたルートと重複できない
  • クライアントCIDR範囲のブロックサイズは、/22以上、/12以下
  • VPNエンドポイントに関連付けられたサブネットは、同じVPCに存在する必要がある
  • 同じAZの複数のサブネットをVPNエンドポイントに関連付けることはできない
  • セルフサービスポータルは、相互認証を使用して認証するクライアントでは利用できない
  • IPアドレスを使用してVPNエンドポイントに接続することは非推奨
    マネージドサービスであるため、DNS名前が解決されるIPアドレスに変更が表示されることがある。指定されたDNS名前を使用してVPNエンドポイントに接続することが推奨
  • VPNエンドポイントに登録したクライアント証明書を入れ替えることはできない
  • クライアントデバイスのLAN IPアドレス範囲が、以下の標準プライベートIPアドレス範囲内にある必要がある
    10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、169.254.0.0/16

(参考)AWS Client VPNを使用するためのルールとベストプラクティス - AWS Client VPN

概要図

作業の流れ

事前作業

AWS側手順

1.クライアントVPNエンドポイントを作成
VPCダッシュボード > VPN > クライアントVPNエンドポイント > [クライアントVPNエンドポイントを作成]

2.詳細
以下のように設定

3.認証情報
「相互認証」、「AD認証」を選択
サーバー証明書、クライアント証明書は同じものを適当に選択

4.接続ログ記録
念のため接続ログ記録を有効にしておきます。[クライアント接続のログの詳細を有効化]し、CloudWatch Logs のロググループ名を選択

5.その他のパラメータ(オプション)
VPCとSGはいつもの「nozaki-」を選択

残りはデフォルトとし、これで[クライアント VPN エンドポイントを作成]
しばらくPending-associateになるので待ち

6.ターゲットネットワークの関連付け
ターゲットネットワークとは、クライアントVPNエンドポイントに関連付けるVPCサブネットです。
作成後、「ターゲットネットワークの関連付け」タブから[ターゲットネットワークを関連付ける]

nozaki-privateサブネットを選択し、[ターゲットネットワークを関連付ける]

7.承認ルール
承認ルールは、ネットワークにアクセスできるユーザーを制限します。アクセスを許可するADまたはIdPグループを構成します。
次は「承認ルール」タブから[認証ルールを追加]

Domain Usersのみにアクセスを付与

SIDは以下コマンドで確認

PS C:\Windows\system32> (Get-ADGroup -Identity "Domain Users").SID

BinaryLength AccountDomainSid Value
------------ ---------------- -----
28           S-1-5-21-****17  S-1-5-21-****17-513

最後に[認証ルールを追加]

8.ルートテーブル
外部にも出れるようにするため、「ルートテーブル」タブにデフォルトルートを追加

9.クライアント設定をダウンロード
[クライアント設定をダウンロード]し、クライアントに設定する用のVPNクライアント設定ファイル「downloaded-client-config.ovpn」をダウンロード

ちなみに中身はこのようになっています。

client
dev tun
proto udp
remote cvpn-endpoint-****.prod.clientvpn.ap-northeast-1.amazonaws.com 443
remote-random-hostname
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
<ca>
-----BEGIN CERTIFICATE-----
MII*********
-----END CERTIFICATE-----
</ca>

auth-user-pass
reneg-sec 0
verify-x509-name server.com name

手順

クライアント側手順

ダウンロードしたクライアント設定ファイル(downloaded-client-config.ovpn)を開き、<ca>CA証明書</ca>の後にクライアント証明書certと秘密鍵keyを追記
自分は以下のようにベタ書きしましたが、

〜〜〜
</ca>

<cert>
-----BEGIN CERTIFICATE-----
〜
-----END CERTIFICATE-----
</cert>

<key>
-----BEGIN PRIVATE KEY-----
〜
-----END PRIVATE KEY-----
</key>

以下のようにパス指定でもいいようです。

cert *クライアント証明書crtファイルのフルパス*.crt
key *証明書の秘密鍵keyファイルのフルパス*.key

以下では、各OSごとに実際に接続してみます。

macOSの場合

(参考)macOSでAWS Client VPN接続を確立する - AWS クライアント VPN
1.https://tunnelblick.net/downloads.html より、好きなバージョンのTunnelblickをダウンロードしてインストール

2.Tunnelblickクライアントアプリケーションを起動し、[設定ファイルがある]を選択

3.[接続先]パネルに設定ファイルをドラッグ&ドロップ

4.[接続先]パネルで3.で入れた設定ファイルを選択し、[接続]

5.ADアカウントでログイン

6.途中、警告が何度か出ますが、全て[了解]でOK

7.接続確認

8.VPNに接続しながらインターネットに出れることも確認

Windowsの場合

(参考)証明書を使用して を確立する AWS Windows でのクライアントVPN接続 - AWS クライアント VPN

1.クライアント証明書と秘密キーを含む .pfx ファイルを作成
まず、pfxファイルを生成するために、クライアント証明書client_certificate.crt、秘密キーclient_private.keyを単独で作成するため、downloaded-client-config.ovpnからそれぞれの該当部分をコピーし、viで貼り付けて作成
作成用のフォルダを作ってその中で作りました。

$ pwd
/Users/nozaki/pki/server.com
$ ls
client_certificate.crt	client_private.key

次にこのフォルダでpfx生成コマンドを実行

$ openssl pkcs12 -export -out client_cert.pfx -inkey client_private.key -in client_certificate.crt
Enter Export Password:
Verifying - Enter Export Password:
$ ls
client_cert.pfx		client_certificate.crt	client_private.key

2..pfxファイルをWindowsの個人証明書ストアにインポート
ファイル名を指定して実行で「certlm.msc」を入れれば一発で証明書MMC スナップインが開きます。client_cert.pfxをダブルクリックするとインポートウィザードが開くので、インポートして完了

3.証明書を開き、サブジェクトを確認

4.OpenVPN設定ファイルを更新し、3.の証明書のサブジェクト「vpn-test.grasys.domain」を使用して証明書を指定

cryptoapicert “SUBJ:vpn-test.grasys.domain”

この時、やの項目は不要なので消しておきましょう。

5.OpenVPNソフトをダウンロード
https://openvpn.net/community-downloads/
今回自分は「OpenVPN-2.6.10-I003-amd64.msi」を入れました。

6.設定ファイルのインポート

7.アイコン右クリックで接続。AD情報を入力

8.このように表示されれば接続成功

ちなみにその時のログはこんな感じでした。

2024-06-19 14:59:08 OpenVPN 2.6.10 [git:v2.6.10/ba0f62fb950c56a0] Windows [SSL (OpenSSL)] [LZO] [LZ4] [PKCS11] [AEAD] [DCO] built on May 23 2024
2024-06-19 14:59:08 Windows version 10.0 (Windows 10 or greater), amd64 executable
2024-06-19 14:59:08 library versions: OpenSSL 3.2.1 30 Jan 2024, LZO 2.10
2024-06-19 14:59:08 DCO version: 1.2.1
2024-06-19 14:59:08 MANAGEMENT: TCP Socket listening on [AF_INET]127.0.0.1:25340
2024-06-19 14:59:08 Need hold release from management interface, waiting...
2024-06-19 14:59:08 MANAGEMENT: Client connected from [AF_INET]127.0.0.1:50339
2024-06-19 14:59:08 MANAGEMENT: CMD 'state on'
2024-06-19 14:59:08 MANAGEMENT: CMD 'log on all'
2024-06-19 14:59:08 MANAGEMENT: CMD 'echo on all'
2024-06-19 14:59:08 MANAGEMENT: CMD 'bytecount 5'
2024-06-19 14:59:08 MANAGEMENT: CMD 'state'
2024-06-19 14:59:08 MANAGEMENT: CMD 'hold off'
2024-06-19 14:59:08 MANAGEMENT: CMD 'hold release'
2024-06-19 14:59:11 MANAGEMENT: CMD 'username "Auth" "Admin@nozaki.com"'
2024-06-19 14:59:11 MANAGEMENT: CMD 'password [...]'
2024-06-19 14:59:11 MANAGEMENT: >STATE:1718776751,RESOLVE,,,,,,
2024-06-19 14:59:11 TCP/UDP: Preserving recently used remote address: [AF_INET]18.179.225.999:443
2024-06-19 14:59:11 ovpn-dco device [OpenVPN Data Channel Offload] opened
2024-06-19 14:59:11 UDP link local: (not bound)
2024-06-19 14:59:11 UDP link remote: [AF_INET]18.179.225.999:443
2024-06-19 14:59:11 MANAGEMENT: >STATE:1718776751,WAIT,,,,,,
2024-06-19 14:59:11 MANAGEMENT: >STATE:1718776751,AUTH,,,,,,
2024-06-19 14:59:11 TLS: Initial packet from [AF_INET]18.179.225.999:443, sid=14477cb6 183ca10b
2024-06-19 14:59:11 WARNING: this configuration may cache passwords in memory -- use the auth-nocache option to prevent this
2024-06-19 14:59:11 VERIFY OK: depth=1, CN=Easy-RSA CA
2024-06-19 14:59:11 VERIFY KU OK
2024-06-19 14:59:11 Validating certificate extended key usage
2024-06-19 14:59:11 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
2024-06-19 14:59:11 VERIFY EKU OK
2024-06-19 14:59:11 VERIFY X509NAME OK: CN=server.com
2024-06-19 14:59:11 VERIFY OK: depth=0, CN=server.com
2024-06-19 14:59:11 Control Channel: TLSv1.2, cipher TLSv1.2 ECDHE-RSA-AES256-GCM-SHA384, peer certificate: 2048 bits RSA, signature: RSA-SHA256, peer temporary key: 256 bits ECprime256v1
2024-06-19 14:59:11 [server.com] Peer Connection Initiated with [AF_INET]18.179.225.999:443
2024-06-19 14:59:11 TLS: move_session: dest=TM_ACTIVE src=TM_INITIAL reinit_src=1
2024-06-19 14:59:11 TLS: tls_multi_process: initial untrusted session promoted to trusted
2024-06-19 14:59:12 MANAGEMENT: >STATE:1718776752,GET_CONFIG,,,,,,
2024-06-19 14:59:12 SENT CONTROL [server.com]: 'PUSH_REQUEST' (status=1)
2024-06-19 14:59:17 SENT CONTROL [server.com]: 'PUSH_REQUEST' (status=1)
2024-06-19 14:59:17 PUSH: Received control message: 'PUSH_REPLY,redirect-gateway def1 bypass-dhcp,block-outside-dns,dhcp-option DOMAIN-ROUTE .,route-gateway 192.168.0.1,topology subnet,ping 1,ping-restart 20,echo,echo,ifconfig 192.168.0.2 255.255.255.224,peer-id 0,cipher AES-256-GCM'
2024-06-19 14:59:17 Options error: --dhcp-option: unknown option type 'DOMAIN-ROUTE' or missing or unknown parameter
2024-06-19 14:59:17 OPTIONS IMPORT: --ifconfig/up options modified
2024-06-19 14:59:17 OPTIONS IMPORT: route options modified
2024-06-19 14:59:17 OPTIONS IMPORT: route-related options modified
2024-06-19 14:59:17 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified
2024-06-19 14:59:17 interactive service msg_channel=680
2024-06-19 14:59:17 ROUTE_GATEWAY 10.0.0.1/255.255.240.0 I=12 HWADDR=06:fa:f5:f9:c9:c7
2024-06-19 14:59:18 MANAGEMENT: >STATE:1718776758,ASSIGN_IP,,192.168.0.2,,,,
2024-06-19 14:59:18 INET address service: add 192.168.0.2/27
2024-06-19 14:59:18 IPv4 MTU set to 1500 on interface 37 using service
2024-06-19 14:59:18 Blocking outside dns using service succeeded.
2024-06-19 14:59:18 C:\Windows\system32\route.exe ADD 18.179.225.999 MASK 255.255.255.255 10.0.0.1
2024-06-19 14:59:18 Route addition via service succeeded
2024-06-19 14:59:18 C:\Windows\system32\route.exe ADD 0.0.0.0 MASK 128.0.0.0 192.168.0.1
2024-06-19 14:59:18 Route addition via service succeeded
2024-06-19 14:59:18 C:\Windows\system32\route.exe ADD 128.0.0.0 MASK 128.0.0.0 192.168.0.1
2024-06-19 14:59:18 Route addition via service succeeded
2024-06-19 14:59:18 Initialization Sequence Completed
2024-06-19 14:59:18 MANAGEMENT: >STATE:1718776758,CONNECTED,SUCCESS,192.168.0.2,18.179.225.999,443,,
2024-06-19 14:59:18 Data Channel: cipher 'AES-256-GCM', peer-id: 0
2024-06-19 14:59:18 Timers: ping 1, ping-restart 20

iOSの場合

(参考)OpenVPN Connect for iOS

1.プロファイルのインポート
https://openvpn.net/connect-docs/import-profile.html#import-a-profile-57126



 

料金

AWS Client VPNは、以下の単位で課金されます。

  • アクティブなクライアント接続数
  • Client VPNエンドポイントに関連付けられているサブネット数
  • VPN接続時間
  • 1時間ごと

所要時間

2時間

S3にドロップされたCURデータをGlue経由でAthenaで読む

サービス概要

コストと使用状況ダッシュボード:AWS Cost and Usage Report (CUR) の概要ビューを基にしたもの
QuickSightを利用したコストと使用状況ダッシュボード(Cost & Usage Dashboard:CUD)を、Billing and Cost Managementコンソールから直接デプロイできるようになりました。

対象となる技術

  • AWS Organizations
  • QuickSight

参考URL

https://qiita.com/siwa/items/86a5323094795eb55d2a

注意事項

  • コスト使用状況ダッシュボードででカバーされない情報含め可視化したい場合には、個別にダッシュボード開発を行うことで対応可能

作業の流れ

事前作業

親アカウントでデータエクスポートを保存する用のS3バケットを作成しておく(ここでは「quicksight-dataexport」としました)

手順

1.データがあることを確認

2.クエリエディタ > 設定 > [管理]

3.デフォルトのまま[保存]

[Glue]
4.[Data Catalog] > [Crawlers] > [Create crawler]

5.Nameは適当に入力し、[Next]

6.「Data source configuration」で「Not yet」を選択し、[Add a data source]

7.「S3 path」にクロールするファイルがあるS3フォルダを指定し、残りはデフォルトのまま[Add an S3 data source]で、[Next]

8.IAM roleの設定では[Create new IAM role]で「AWSGlueServiceRole-nozaki」を作成してみました。
ここでロールを見に行くと、実際に「AWSGlueServiceRole-nozaki」が作成されています。


中身を見ると、「AWSGlueServiceRole」ポリシーが付与されており、その中を見ると、このような許可が付与されていました。


S3では"Resource": に "arn:aws:s3:::aws-glue-/" が指定されており、「aws-glue-」で始まるS3バケットに出力できるようになっています。

9.Target database では、まだデータベースが作成されていないので[Add database]

10.Nameを適当に入力し、[Create database]

11.Add crawlerの入力画面に戻り、10.で作成したデータベースを選択し、それ以外はデフォルトのまま[Next]

12.Review and create
設定を最終確認し、[Create crawler]

13.Run crawler

 

問題:なぜかタイプエラー発生

hive_partition_schema_mismatch: there is a mismatch between the table and partition schemas. the types are incompatible and cannot be coerced. the column 'pricing/publicondemandcost' in table 'nozaki-database.hourly' is declared as type 'bigint', but partition 'partition_0=20190201-20190301' declared column 'pricing/publicondemandcost' as type 'double'.

エラー「HIVE_PARTITION_SCHEMA_MISMATCH」で失敗する Athena クエリのトラブルシューティング | AWS re:Post

解決方法
パーティションスキーマを更新するように AWS Glue クローラーを設定する
AWS Glue コンソールを開きます。
ナビゲーションペインで、[クローラ] を選択します。
設定するクローラを選択します。
[アクション] を選択し、[クローラの編集] をクリックします。
[クローラの出力を選択] ページに移動するまで [次へ] を選択します。
[設定オプション] を展開します。
[すべての新規および既存のパーティションをテーブルからのメタデータで更新します] を選択します。

所要時間

2時間

CloudFormation StackSets(AWS Configログ集約)の実行エラー調査

サービス概要

スタックセットは、1つのCloudFormationテンプレートを使用して、複数アカウントや複数リージョンにまたがってスタックを作成できるのが、通常のスタックとの大きな違いです。
AWS Configは、AWSリソースの設定や変更を追跡し、それらが適切な設定であるかどうかを評価するためのサービスです。リソースの設定変更を記録し、コンプライアンスを監視することができます。
今回はこのCloudFormation StackSetsのサンプルテンプレートとして提供されている、S3やSNSなどを使って 中央ログで AWS Config を有効にする テンプレートのデプロイを扱います。
AWS Organizationsなどのマルチアカウント環境化での検証となります。

問題(お客様からの問い合わせ)

組織内の全アカウント、全リージョン(デフォルト有効化分のみ)に対してconfigを有効化、ログをログアカウントのS3に集約させるstacksetsを実行しているのですが、エラーが解消できない。
S3バケットポリシーでcondition句にSourceOrgIDを利用した場合はどのような権限設定が必要でしょうか。公式サイト上で、KMS上の利用では予期しない挙動が発生する可能性があると記載があったため、アカウントIDを列挙する方式でも試したのですが、同様のエラーになってしまうことを確認しております。

【環境】

  • stacksets実行アカウント(organizationsで委任設定済) 
  • ログ集約アカウント

AWS提供のサンプルテンプレート「集中ロギングでAWS Configを有効にする」をベースに、activate-config.ymlというstackテンプレートを作成

  • エラーメッセージ:
    「S3バケットへの配信ポリシーが不十分、S3キープレフィックスがNULL」
    「ResourceStatusReason:Insufficient delivery policy to s3 bucket: log-123456789012-qcloud-config, unable to assume」
    「ResourceLogicalId:ConfigDeliveryChannel, ResourceType:AWS::Config::DeliveryChannel, ResourceStatusReason:Insufficient delivery policy to s3 bucket: log-123456789012-qcloud-config, unable to write to bucket, provided s3 key prefix is 'null', provided kms key is 'null'. (Service: AmazonConfig; Status Code: 400; Error Code: InsufficientDeliveryPolicyException; Request ID: ****; Proxy: null).」

  • S3バケットポリシー:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSConfigBucketPermissionsCheck",
            "Effect": "Allow",
            "Principal": {
                "Service": "config.amazonaws.com"
            },
            "Action": [
                "s3:GetBucketAcl",
                "s3:ListBucket"
            ],
            "Resource": "arn:aws:s3:::log-123456789012-cloud-config",
            "Condition": {
                "StringEquals": {
                    "aws:SourceOrgID": "o-*****"
                }
            }
        },
        {
            "Sid": "AWSConfigBucketDelivery",
            "Effect": "Allow",
            "Principal": {
                "Service": "config.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::log-123456789012-cloud-config/AWSLogs/*/Config/*",
            "Condition": {
                "StringEquals": {
                    "aws:SourceOrgID": "o-*****",
                    "s3:x-amz-acl": "bucket-owner-full-control"
                }
            }
        }
    ]
}

対象者・ ユースケース

AWS Organization内の全AWSアカウントにてAWS Configを集中ロギングで有効化したいケース

対象となる技術

  • AWS CloudFormation - StackSets
  • AWS Config

条件(導入にあたっての前提事項)

  • AWS Organizationを使える環境であること

参考URL

注意事項

概要図

  • Organizations管理アカウント
  • Organizations検証用-01アカウント(管理アカウントからstacksetsの実行を委任)
  • Organizations検証用-02アカウント(AWS Configのログ集約S3バケット

作業の流れ

事前作業

管理アカウント

1.委任された管理者を登録
CloudFormation > StackSets > [委任された管理者を登録]

 

2.アカウントIDを入れ、[委任された管理者を登録]

このように01アカウントの情報が登録されます。

02アカウント

マルチアカウントでAWS Configのログを集約するためのS3バケットを用意します。

1.暗号化
設定はほぼデフォルトでOKですが、暗号化だけお客様環境に合わせてSSE-KMSを使用

2.[KMSキーを作成する]
デフォルトのまま[次へ]

エイリアスも適当に設定し、[次へ]

キーの管理アクセス許可、キーの使用法アクセス許可は何もせずそのまま[次へ]
最後に確認して[完了]
(キーポリシーは後述)

3.S3のデフォルトの暗号化に戻り、[変更を保存]
バケットポリシーに関しては後述)

手順

01アカウント

管理アカウントからstacksetsを委任された01アカウントで、stacksetsを実行します。

1.サービスマネージド StackSets
AWS Organizations が管理するターゲットアカウントにデプロイするため、[サービスマネージド]タブを選択し、[StackSetsを作成]

2.以下のように設定

https://cloudformation-stackset-sample-templates-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/EnableAWSConfigForOrganizations.yml にアクセスすれば、[集中ロギングでAWS Configを有効にする]テンプレート EnableAWSConfigForOrganizations.yml をダウンロードできます。次回からはここにデフォルト値など書き込んで読み込むようにすると楽です。

3.StackSet 名は適当に入力し、パラメータは以下のように設定

4.StackSet オプションの設定
デフォルトのまま[次へ]

5.東京リージョンだけを選択しておき、[次へ]

6.レビュー
問題なければそのまま[送信]

7.オペレーション
ここから待たされます。順調だと6分、失敗してると20分近く待たされる印象です。

[スタックインスタンス:]タブに行くとインスタンスごとの状態が見れます。

さて、ここからが大変でした。。

エラーその1

ResourceLogicalId:ConfigRecorder, 
ResourceType:AWS::Config::ConfigurationRecorder, 
ResourceStatusReason:Failed to put configuration recorder 'StackSet-CentralLogging-AWSConfig-StackSet-620f4576-31d3-44ac-b722-71649402e3b2-ConfigRecorder-GC5RZIAA81LN'
because the maximum number of configuration recorders: 1 is reached. 
(Service: AmazonConfig; Status Code: 400; 
Error Code: MaxNumberOfConfigurationRecordersExceededException; 
Request ID: 616dfd37-1d14-42ff-b20c-186ae7cde89d; Proxy: null).

MaxNumberOfConfigurationRecordersExceededException エラーは、リージョンのアカウントに設定レコーダーが既に存在しているため、新しく作成できないことを示します。
既にAWS Configが有効になっていると起きるエラーです。有効になっている場合は再度有効化しないなど場合分け処理を入れればいいのですが、ここでは一気に全リージョン無効化してくれる素晴らしい以下のワンライナーをCloudShellで投入して解決です!

# for a in `aws configservice describe-delivery-channels | jq -r '.DeliveryChannels[].name'`; do aws configservice delete-configuration-recorder --configuration-recorder-name ${a}; aws configservice delete-delivery-channel --delivery-channel-name ${a}; done

エラーその2

ResourceLogicalId:ConfigDeliveryChannel, 
ResourceType:AWS::Config::DeliveryChannel, 
ResourceStatusReason:Insufficient delivery policy to s3 bucket: abe-test-config-log, 
unable to assume role: arn:aws:iam::156083320778:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig-ap-northeast-1. 
(Service: AmazonConfig; Status Code: 400; Error Code: 
InsufficientDeliveryPolicyException; 
Request ID: 0aa57a08-1d87-4ffa-8b65-c2ed631896a1; Proxy: null).

この不十分な配信ポリシーが一番多く出たエラーでした。その原因はバケットポリシーです。お客さんのバケットポリシーには "StringEquals": {"aws:SourceOrgID": "o-*****"} という条件が記載されていました。ところが、

AWS Config 配信チャネルの Amazon S3 バケットのアクセス許可 - AWS Config

Amazon S3 バケット AWS Config へのアクセスを許可する際のセキュリティのベストプラクティスとして、 AWS:SourceAccount条件を使用してバケットポリシー内のアクセスを制限することを強くお勧めします。既存のバケットポリシーがこのセキュリティのベストプラクティスに従わない場合は、この保護を含めるようにバケットポリシーを編集することを強くお勧めします。これにより、 AWS Config が想定ユーザーに代わってのみアクセス許可を付与されます。

AWS的にはSourceAccount条件を使用することがベストプラクティスなのです。
なのでバケットポリシーのCondition句を以下のように変更しました。

"Condition": {
    "StringEquals": {
        "AWS:SourceAccount": "836550492281"
    }
}

エラーその3

ResourceLogicalId:ConfigDeliveryChannel, 
ResourceType:AWS::Config::DeliveryChannel, 
ResourceStatusReason:Insufficient delivery policy to s3 bucket:
abe-test-config-log, unable to write to bucket, provided s3 key prefix is 'null'.

S3のキープレフィックスが無いってやつです。これは AWS Organizations の CloudFormation StackSets で AWS Config を有効化 | 空想ブログ によると、「S3KeyPrefix: !Ref OrganizationId」と指定したらうまくいったらしいので、適当にバケット名の変数を利用して、次のようにしてみました(ヌルにならなければ何でもいいと思います)

S3KeyPrefix: !If
  - UsePrefix
  - !Ref S3KeyPrefix
  - !Ref S3BucketName

エラーその4

ResourceLogicalId:ConfigDeliveryChannel, 
ResourceType:AWS::Config::DeliveryChannel, 
ResourceStatusReason:Insufficient delivery policy to s3 bucket:
abe-test-config-log, unable to write to bucket, provided s3 key prefix is 'abe-test-config-log', provided kms key is 'null'. 
(Service: AmazonConfig; Status Code: 400; 
Error Code: InsufficientDeliveryPolicyException; 
Request ID: 66ce4e69-3294-4977-ac5c-0c925e87e21f; 
Proxy: null).

今度はKMSキーがヌル!これが結構苦労しました。

サービスにリンクされたロールを使用する場合の AWS KMS キーに必要な権限 (S3 バケット配信) - AWS Config

上記の AWS KMS キーポリシーの AWS:SourceAccount 条件を使用すると、特定のアカウントに代わって操作を実行するときにのみ Config サービスプリンシパルAWS KMS キーと対話するように制限できます。

そこで、KMSのキーポリシーにも"AWS:SourceAccount"条件を入れてみました。
また、Actionにkms:Encryptも追加してみました。

{
    "Sid": "AWSConfigKMSPolicy",
    "Effect": "Allow",
    "Principal": {
        "Service": "config.amazonaws.com"
    },
    "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey"
    ],
    "Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/****-****-****",
    "Condition": { 
        "StringEquals": {
            "AWS:SourceAccount": "836550492281"
        }
    }
}

AWS Configのログ配信先S3バケットをKMS暗号化する #kms - Qiita によると、put-delivery-channelコマンドで簡単にテストできるようです。これが通れば問題なし。

$ aws configservice describe-delivery-channels
{
    "DeliveryChannels": [
        {
            "name": "configlog",
            "s3BucketName": "abe-test-config-log",
            "s3KeyPrefix": "abe-test-config-log",
            "configSnapshotDeliveryProperties": {
                "deliveryFrequency": "One_Hour"
            }
        }
     ]
}

$ aws configservice put-delivery-channel --delivery-channel name=default,s3BucketName=abe-test-config-log,s3KmsKeyArn=arn:aws:kms:ap-northeast-1:156083320778:key/54ae0f87-fe85-4048-888e-8a47a0ab5ea2,s3KeyPrefix=abe-test-config-log

$ aws configservice describe-delivery-channels
{
    "DeliveryChannels": [
        {
            "name": "configlog",
            "s3BucketName": "abe-test-config-log",
            "s3KeyPrefix": "abe-test-config-log",
            "s3KmsKeyArn": "arn:aws:kms:ap-northeast-1:156083320778:key/54ae0f87-fe85-4048-888e-8a47a0ab5ea2",
            "configSnapshotDeliveryProperties": {
                "deliveryFrequency": "One_Hour"
            }
        }
     ]
}

このように「s3KmsKeyArn」がエラーなく入ればOKです!

結果

以上のトラシューの数々を潜り抜けてきた結果、なんとか成功!

おまけ(代替策)

サービスにリンクされたロールを使わなくていい場合は普通にIAMロールで代替できるみたいです。。

所要時間

6時間!
(1回のスタックセットのオペレーションに6〜20分かかります。。)

ALBでターゲットをAPI Gatewayにして相互TLSトラストストア検証できるか

サービス概要

昨年のre:Invent 2023で発表されたアップデートにより、ALBで相互TLS(mutual TLS)認証(クライアント認証)ができるようになりました。相互TLSには、X.509v3クライアント証明書を検証するための2つのオプションがあり、そのうちの一つがトラストストア検証です。
トラストストア検証とは、ALBがTLS接続をネゴシエートするときに、クライアントに対してX.509クライアント証明書認証を実行して互いのIDを検証し、TLS接続を確立して互いの間の通信を暗号化します。通常、相互TLS検証モードといった場合はこちらのトラストストア検証を指します。

目的・やりたいこと

マイクロサービスを統合し、セキュアで柔軟なAPIマネジメントを実現する

ユースケース

  • 既存のオンプレミスシステムと新規クラウドサービスを統合し、一貫したAPI管理を行う
  • 複数の部門や事業部が独自に開発したサービスを、統一されたインターフェースで外部に公開したい

上記の目的を達成するため、トラストストア検証で、ALBの後段にAPI Gatewayする構成が有効かどうかを検証する。

以下要件:

  • ALBでmTLSトラストストア検証を有効、S3に設置したCA証明書、失効リストの設定を行いALBでクライアント認証行う
  • ALBでセキュリティポリシーをTLS1.3のみ有効なポリシーを当てる

API Gateway への到達性をカスタムドメイン(例:https://api.example.com)を介して提供します。カスタムドメイン名のDNSレコードは、Route 53サービスによってホストされます。デプロイに必要な公開SSL証明書を作成するために、AWS Certificate Managerを使用します。

対象となる技術

  • ALB(Application Load Balancer)
  • 相互TLS(トラストストア)検証
  • Amazon API Gateway

条件(証明書の要件)

相互TLS認証で使用される証明書について、以下をサポートしています。

  • サポートされている証明書:X.509v3
  • サポートされているパブリックキー:RSA 2K–8K または ECDSA secp256r1、secp384r1、secp521r1
  • サポートされている署名アルゴリズム:SHA256、384、512 と RSA/SHA256、384、512 と EC/SHA256、384、512ハッシュとMGF1のRSASSA-PSS

(参考)Application Load Balancer で相互 TLS の設定を開始する前に

参考URL

注意事項

概要図

 

作業の流れ

事前作業

1.APIエンドポイントの作成
VPC > エンドポイント > [エンドポイントを作成]

AWSのサービス」で「com.amazonaws.ap-northeast-1.execute-api」サービスを選択

AZは今回検証のため1つだけ選択

残りはデフォルトのまま[エンドポイントの作成]
エンドポイントが作成されたら、こちらの2つのDNS名を確認

nslookupでIPを見ると2つとも同じIPが返ってきたので、これをメモ

# nslookup vpce-****.execute-api.ap-northeast-1.vpce.amazonaws.com
Non-authoritative answer:
Name:	vpce-****.execute-api.ap-northeast-1.vpce.amazonaws.com
Address: 10.0.22.**

# nslookup vpce-****-ap-northeast-1a.execute-api.ap-northeast-1.vpce.amazonaws.com
Non-authoritative answer:
Name:	vpce-****-ap-northeast-1a.execute-api.ap-northeast-1.vpce.amazonaws.com
Address: 10.0.22.**

2.APIゲートウェイの作成
API Gateway > API > [APIを作成]

REST APIを[構築]
「サンプル API」を選択

エンドポイントタイプは「プライベート」にし、エンドポイントIDに1.で作成したAPIエンドポイントIDを選択し、[APIを作成]

3.リソースポリシーの作成
API > リソースポリシー > [ポリシーを作成]

テンプレートを選択 から[ソースVPC許可リスト]を選び、検証なのでこのように比較的緩めのポリシーを作成

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "execute-api:/*/*/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:sourceVpc": "vpc-****"
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "execute-api:/*/*/*"
        }
    ]
}

4.接続テスト
ポリシーを作成するとデプロイができます。

インターネット経由だとWebアクセスできないので、パブリックサブネットにある踏み台WindowsからWebアクセスして接続確認

見れない方はエンドポイントのセキュリティグループを確認してみてください。自分は最初それがデフォルトのままでhttpsアクセスが許可されておらず、若干ハマりました。

5.ACM証明書の取得
自分は「nozaki2.com」ドメインをお名前.comで取得していたので、「*.nozaki2.com」でACM証明書を取得しました。

手順

API Gatewayの事前準備が終わったので、ALBを作成していきます。

1.ターゲットグループの作成
ターゲットタイプは「IPアドレス」を選択
ヘルスチェックには「HTTPS」を使用し、プロトコルバージョンとして「HTTP1」を指定

[ヘルスチェックの詳細設定]を開き、「成功コード」の値を「403」に変更

注: ALB は、VPC エンドポイントの IP アドレスに対して HTTPS リクエストを送信して API Gateway のヘルスを検証します。API Gateway は、ヘルスチェックのプローブ中に正しいドメイン名とステージの URL を提供しないため、403コード(アクセス禁止)で応答します。

事前準備1.でメモしたIPをターゲットとして追加

2.ALBの作成
通常通りALBを作成します。ターゲットグループには1.で作成した「nozaki-privateapi-TG」を選択

ここでは一旦まずmTLSなしで作成して、接続確認を優先します。

また、念のためポリシーでTLS 1.3になっていることを確認しておきます。
(ユーザーからのエンドポイントであるALB側でTLS1.3のみで受け付けられれば、ALB→プライベートAPIはTLS1.2でよいと考えています)

 

Web接続テスト

ところがこの状態でWeb接続確認してみたところ、{"message":"Forbidden"}となりました。エンドポイントのDNSやIPに直接アクセスしても同様に{"message":"Forbidden"}となります。

手順追加

何か設定が足りていないのだろうと、以下を追加することにしました。

3.カスタムドメインの設定
APIマッピングを使用して、APIステージをカスタムドメイン名に関連づける必要があることがわかりました。デプロイメント用にカスタムドメインを作成します。ドメイン名は、ACM証明書に含まれている必要があります。

3.1. API Gatewayコンソールで、カスタムドメイン名 > [作成]

3.2. ドメイン名の詳細

  • ドメイン名:適当に入力
  • TLSの最小バージョン:デフォルトのまま
    ただし、相互TLS認証はあくまでALBにやらせるので、ここでは有効にしません。

3.3. エンドポイント設定
ここで困りました。選択できるエンドポイントタイプがリージョン or エッジ最適化しかありません。あれ?プライベート選べないの?と思いましたが、どっちを選んでもいいらしいです。
ACM証明書は事前作業 5.で作成したACMを選びます。

以上で[ドメイン名を作成]

3.4. APIマッピング
作成したドメイン名を選択し、[APIマッピング]タブ > [APIマッピングを設定]

「dev」ステージを選択して[保存]

4.エイリアスAレコードの設定
上記でカスタムドメイン名とAPIステージの関連付けが無事完了したので、今度はそのカスタムドメインでWebアクセスした時にALBに向けられるよう、DNSレコードを設定する必要があります。
外部からのアクセスを想定しているため、Route 53で「nozaki2.com」のパブリックホストゾーンを作成しておきます。
[レコードを作成]でこのようなエイリアスAレコードを作成します。

今度こそ気を取り直して再度Web接続テスト

「aaa.nozaki2.com」にWebアクセスし、無事API Gatewayに設定したPet Storeページが表示されました!

次はいよいよ相互トラストストアを設定していきます。

手順追加(相互トラストストア設定)

相互トラストストアは、トラストストアの作成が結構面倒なので、先にトラストストアを単独で作成しておきます。そのためにはクライアント証明書を作成する必要があります。

5.クライアント証明書の作成
opensslコマンドで秘密鍵(root_key.pem)、ルートCA証明書(root_cert.pem)、クライアント証明書(client_cert.pem)の順に作成していきます。生成されたルートCA証明書はトラストストアに、クライアント証明書はクライアント認証で使います。

6.トラストストアの作成
ロードバランシング > トラストストア > [トラストストアを作成]

5.で作成したルートCA証明書(root_cert.pem)をS3のどこかに上げておき、そのS3 URIパスを認証局バンドルとして指定します。

そのほかはデフォルトのまま、[トラストストアを作成]

※注意
ここでルートCA証明書が正しい形式で作成されていないと、[トラストストアを作成]しても、下記のような「The basic constraints extension must specify that the certificate is for a CA(基本制約では、証明書がCA用であることを指定しなければならない)」という謎のエラーが出てしまいます(ググっても出てきません。。)

ルートCA証明書の中身が以下のようなフォーマットになっていることを確認してください。自分はここでハマりました。

-----BEGIN CERTIFICATE-----
MIIDdTCCAl2gAwIBAgIUQREbhxa83orBI9vac76/sr5JCvIwDQYJKoZIhvcNAQEL
〜〜〜〜〜
W71mqNugfiXQC0GuPZdCs/WillAsHhlp+A==
-----END CERTIFICATE-----

7.ALBの設定変更
再度ALBの設定に戻り、[リスナーとルール]タブ > [ルールを管理] > [ルールの編集]

「クライアント証明書の処理」で今度は相互認証を有効にし、「トラストストアで検証」を選択、トラストストアには6.で作成したトラストストア(nozaki-truststore)を選択して、[変更内容の保存]

 

再度Web接続テストで最終確認

ここで接続テストはWindows上で行いました。このため、クライアント証明書を.pfx形式に変換してブラウザに入れておきます。
「aaa.nozaki2.com」に再度Webアクセスすると、今度はクライアント証明書を求められるダイアログが表示

正しい証明書を選択すると、無事Pet Storeのページが表示されます。

ちなみに間違った証明書を選択したり、キャンセルしたりすると、「このサイトにアクセスできません」として拒否できていることが確認できます。

以上により、ALB(トラストストア) → API Gatewayエンドポイント → API Gateway(プライベートAPI) でクライアント認証経由で通ることを確認できました。

まとめ

  • ALBでAPI Gatewayをターゲットにしたい場合は、API GatewayエンドポイントのIPアドレスを指定する
  • プライベートAPIはTLSv1.2のみサポート
  • ALB経由でAPI GatewayにWebアクセスするには、「カスタムドメインの設定」「エイリアスAレコードの設定」を行う
  • 相互認証(mTLS)を有効にするとクライアント証明書認証ができる
  • トラストストア検証では、ALB側でクライアント認証を行う
  • トラストストアを作成する場合は、ルートCA証明書の形式に注意

所要時間

2時間

ユースケース

ALBでAPI Gatewayエンドポイントをターゲットにしたい場合に、TLS 1.3+クライアント認証も行ってセキュリティを強化したいケース

AWS OrganizationsにおけるQuickSightを利用したコストと使用状況ダッシュボードの利用手順

サービス概要

コストと使用状況ダッシュボード:AWS Cost and Usage Report (CUR) の概要ビューを基にしたもの
QuickSightを利用したコストと使用状況ダッシュボード(Cost & Usage Dashboard:CUD)を、Billing and Cost Managementコンソールから直接デプロイできるようになりました。

目的・やりたいこと

  • AWSを触ったことがあるお客様で設定できるレベルのキャプチャ付き手順書
  • 今回のお客様だけではなく、他のお客様にも展開できる命名での検証(キャプチャ取得)
  • ざっくりでいいので構築にかかった時間の計測
  • デフォルトでQuickSightに表示される項目、内容(キャプチャ付き説明で設定手順とは別であるほうがよい)
  • QuickSight触ってみてコスト見るのに注意点とかあればそれを箇条書きで
  • (オプション)Organizationsで設定したときにアカウント別で表示できるかを確認とその表示のキャプチャ

対象となる技術

  • AWS Organizations
  • QuickSight

参考URL

注意事項

  • コスト使用状況ダッシュボードででカバーされない情報含め可視化したい場合には、個別にダッシュボード開発を行うことで対応可能

概要図

https://youtu.be/hDObb-wiYk8?t=498 より)

作業の流れ

事前作業

親アカウントでデータエクスポートを保存する用のS3バケットを作成しておく(ここでは「quicksight-dataexport」としました)

手順

1.請求とコスト管理 > データエクスポート から、その他の機能より[コストと使用状況ダッシュボード]をクリック

2.エクスポートタイプは[QuickSight を利用するコストと使用状況のダッシュボード]、エクスポート名はわかりやすい名前に、QuickSight アカウントは既にない場合は[作成]を選択します。

3.(QuickSight アカウントを作成する場合)
[QUICKSIGHT にサインアップ]を選択

4.以下のように設定
メールアドレス:通知したいメールアドレス
認証方法:IAM フェデレーティッド ID と QuickSight で管理されたユーザーを使用する
リージョン:Asia Pacific(Tokyo)
QuickSight アカウント名:わかりやすいアカウント名

IAM ロール:QuickSight で管理されるロールを使用する (デフォルト)
これらのリソースへのアクセスと自動検出を許可する:
[S3バケットを選択する]で、事前に作成しておいた「quicksight-dataexport」を選択

あとはデフォルトのまま[完了]

5.再び[エクスポートを作成]画面に戻り、QuickSight アカウントで[リフレッシュ]を選択
すると設定したQuickSight アカウント名が表示されます。

6.データエクスポートストレージ設定
S3バケットの設定で[既存のバケットを選択]を選び、事前に作成した「quicksight-dataexport」バケットを選択します。

Billing And Cost Management データエクスポートに必要なバケットポリシーで既存のバケットポリシーを上書きしますので、「S3 バケットポリシーを上書きすることに同意します」にチェックを入れ、[バケットを選択]します。

S3 パスプレフィックスには適当にわかりやすい名前を付けます。

7.サービスアクセス
[新しいサービスロールを作成]を選択します。

そして最後にようやく[作成]をクリック

8.数分後、無事稼働していることを確認します。

9.[コストと使用状況ダッシュボード]にアクセスして見ると、数値部分が「データなし」になっています。

10.QuickSightにアクセスし、データセットから「costanalysis-export」をクリック

11.更新タブに移動すると、開始時刻が「02:38」、タイムゾーンが「America/New_York」になっており、下の履歴を見ても取り込まれたデータ量が0になっています。

アクションで「編集」を選び、

タイムゾーンをJTCに、開始時刻を少し先の未来の時刻にして[保存]

右上の[今すぐ更新]をクリック

[更新]

この際にデータが取り込まれなくても、しばらくしてからもう一度更新すると、このようにデータの取り込みが行われました。

12.これで元のダッシュボードに戻ると、データもちゃんと表示され、メンバーアカウントのコストも表示されていることがわかります。

考察

S3バケットを見に行くと、quicksight-dataexport/cur-export/costanalysis-export/data/BILLING_PERIOD=2024-01/ に costanalysis-export-00001.csv.gz という圧縮ログファイルがあり、解凍して中を見ると、以下のようなデータが入っていた。

billing_period usage_date payer_account_id payer_account_name linked_account_id linked_account_name invoice_id charge_type charge_category purchase_option ri_sp_arn product_code product_name service product_family usage_type operation item_description availability_zone region instance_type_family instance_type platform tenancy processor processor_features database_engine product_group product_from_location product_to_location current_generation legal_entity billing_entity pricing_unit usage_quantity unblended_cost amortized_cost ri_sp_trueup ri_sp_upfront_fees public_cost
2024-01-01T00:00:00.000Z 2024-01-10T00:00:00.000Z 9.78993E+11 ****-payment - aws.cloudpack 1.56083E+11 ****02 - aws.cloudpack JPIN24-123456 Usage Running_Usage OnDemand   AmazonS3 Amazon Simple Storage Service AWSDataTransfer Data Transfer USE1-EUC1-AWS-Out-Bytes ListAllMyBuckets $0.02 per GB - US East (Northern Virginia) data transfer to EU (Germany)   us-east-1                 US East (N. Virginia) EU (Frankfurt)   Amazon Web Services Japan G.K. AWS GB 8.55E-07 1.71E-08 1.71E-08 0 0 1.71E-08

2024年1月のS3データ転送の料金が記載されている。こういう一つ一つの料金ログの集合体でコストデータが形成される。このS3データをQuickSightが見に行ってダッシュボードに表示している感じ

デフォルトでQuickSightに表示される項目・内容

Cost & Usage Dashboard (v1.0.1)の表示項目は、Cloud Intelligence Dashboardsのダッシュボードの1つであるCUDOSの主要なシートやビジュアル (グラフやチャート) を抽出したもののようです。
簡単に構築できる AWS コスト可視化ダッシュボードのユースケース – Cost and Usage Dashboard (CUD) と CUDOS | Amazon Web Services ブログ
以下、CUDに表示される項目をタブごとにキャプチャ付きで表示します。
(「データなし」となっていてグラフがない箇所はキャプチャ省略します)

Executive: Billing Summary

保持するアカウント全体の支払い状況と変動の傾向、請求書の支出

Invoice Spend(請求書経費)
  • Invoice spend by account(アカウント別の請求書経費)

  • Invoice spend by product(製品別の請求書経費)

Amortized spend(償却経費)

予約や Savings Plans などのコミットメントベースのコストを契約期間全体に按分して表⽰できます。毎⽉の傾向を把握可能です。

  • Amortized spend by account(アカウント別の償却経費)

  • Amortized spend by product(製品別の償却経費)

  • Top 10 spending accounts(支出額上位10アカウント)

Savings and discounts(節約と割引)
  • Discount details: Ctredits, refunds, and other discounts(割引の詳細: クレジット、払い戻し、その他の割引)

  • Total savings and discounts details(合計節約額と割引の詳細)

Executive: MoM Trends(前月比トレンド)

アカウントやサービス単位、その他のディメンションの月次の変動傾向

  • Top movers by Product (Amortized without Refunds and Credits)(製品別トップ変動(返金およびクレジットなしで償却))
    製品ごとにコストが最も大きく変動した上位の製品を示すものです。ディスカウントなどの影響を除外した、償却処理後の正味のコストの推移を分析することで、製品の動向を把握できます。
  • Top movers by Account (Amortized)(アカウント別トップ変動(償却済み))
    各アカウント別にコストの変動が最も大きかった上位のアカウントを示すものです。ディスカウントなどの影響を除外した、償却処理後の正味の変動を基準にしており、特に大きく増減した主要なアカウントを把握できます。
Month over month cost trend details(月ごとのコスト傾向の詳細)
  • MoM trends by product code (AWS Marketplace items use product name)(製品コード別の前月比トレンド(AWSマーケットプレイスの項目では製品名を使用))

  • MoM trends by account(アカウント別前月比トレンド)

Month over month amortized cost trend details(月ごとの償却コスト傾向の詳細)
  • Amortized cost by account(アカウント別の償却コスト)

  • Amortized cost by product family (top 5)(製品ファミリー別の償却コスト(上位5))

  • Amortized cost by product (top 10)(製品別償却コスト(上位10))
  • Amortized cost by region(リージョン別償却コスト)

  • Amortized cost by operations (top 20)(償却コストのオペレーション別内訳(上位20))

AWS Marketplace

AWS 特定製品の月次傾向とコスト

Amazon Elastic Compute

EC2、Fargate、Lambdaの使用状況や変動の傾向

  • EC2 Compute unit cost and Normalized Hours by purchase option(EC2コンピューティング単位コストと購入オプションによる標準時間)

  • EC2 Coverage by purchase option in Normalized Hours(正規化時間での購入オプションによるEC2カバレッジ
    リザーブインスタンス、オンデマンドインスタンス、スポットインスタンスの利用状況を、共通の尺度(Normalized Hours)で比較するためのものです。

  • EC2 Normalized Hours by platform(プラットフォーム別のEC2正規化時間)

  • Top 10 EC2 running hours spending accounts in the last 3 months(過去3か月間のEC2実行時間の消費上位10アカウント)
  • Top 10 EC2 running hours spending accounts in the last 2 months(過去2か月間のEC2実行時間の消費上位10アカウント)

  • Top 10 EC2 running hours spending accounts in the last month(先月のEC2実行時間の消費上位10アカウント)
Amazon EC2 Spot instances savings
  • EC2 Spot savings
  • EC2 Spot savings detailed view
Amazon EC2 Spot instances savings group option controls(EC2スポットインスタンス節約グループオプションコントロール
  • EC2 daily compute unit cost and Normalized Hours by purchase option(EC2の1日あたりのコンピューティング単位コストと購入オプションによる標準時間)

  • EC2 daily cost by Instance Family(インスタンスファミリー別のEC2 1日あたりのコスト)

  • Top 10 accounts by EC2 CPU Credits usage cost(EC2 CPUクレジット使用コスト上位10アカウント)
  • Unused On-Demand capacity reservations cost per account(未使用のオンデマンド容量予約のアカウントあたりのコスト)
Amazon Fargate Summary
  • Fargate accounts by cost(コスト別のFargateアカウント)
  • Fargate cost by purchase option(Fargate購入オプションによるコスト)
AWS Lambda Summary
  • Top 10 Lambda accounts by cost(コスト別Lambdaアカウント上位10件)

  • Lambda purchase option(Lambda購入オプション)

Storage

Amazon Elastic Block Storage (EBS) Summary
EBSやEFS、FSxなどのストレージファイルシステム

  • Top EBS spend accounts in the last 3 months(過去3か月間のEBS支出上位アカウント)

  • EBS volume coverage (GB-Mo) last month(先月のEBSボリュームカバレッジ(GB-月))

  • EBS Storage Unit Cost in the last 5 months(過去5か月間のEBSストレージ単位コスト)

  • EBS storage spend in the last 5 months(過去5か月間のEBSストレージ支出)

  • EBS operations usage cost in the last 90 days(過去90日間のEBS操作使用コスト)

  • EBS snapshot spend in the last 7 months(過去7か月間のEBSスナップショットの支出)

Storage FS: EFS / FSx
  • Storage FS usage cost by top 10 accounts in last 4 months(過去4か月間の上位10アカウントによるストレージFS使用コスト)
  • Storage FS usage cost by region in last 4 months(過去4か月間のリージョン別ストレージFS使用コスト)
  • Storage FS usage cost by product code in last 4 months(過去4か月間の製品コード別のストレージFS使用コスト)
  • Storage FS usage cost by usage type in last 4 months(過去4か月間の使用タイプ別のストレージFS使用コスト)
  • Storage FS usage cost by operation in last 90 days(過去90日間の操作によるストレージFS使用コスト)

Amazon S3

S3の使用状況と変動の傾向

  • Top 10 S3 accounts

  • Top 5 accounts migration savings opportunity(アカウント移行による節約機会トップ5)

Databases

RDSやDocumentDB (MongoDB互換)、Redshift、ElastiCache、OpenSearch Serviceなどデータベースに関する使用状況と変動の傾向

  • Amortised cost by accounts(アカウント別の償却コスト)

  • Amortised cost by regions(リージョン別の償却コスト)

  • Cost by Service Product Family(サービス製品別コスト)

  • Cost by Database Engines(データベースエンジン別コスト)
RI coverage

稼働インスタンスにおけるRIのカバー率状況

  • RI coverage per region | engine | instance type or family (for size exible) in Normalized Hours(リージョン別RIカバレッジ | エンジン | インスタンスタイプまたはファミリー (サイズが柔軟な場合) の正規化時間

  • Daily RI coverage in Normalized Hours(標準化時間での毎日のRIカバレッジ

  • Database Daily Elasticity in Normalized Hours by Purchase Option(購入オプション別の正規化時間でのデータベースの毎日の弾力性)
    データベースの利用オプション別に、1日の需要変動の傾向を時間単位で示したもの

  • Daily cost by Instance Family(インスタンスファミリー別の1日あたりのコスト)

  • Daily storage cost for Amazon Relational Database Service(RDSの1日あたりのストレージコス

Amazon DynamoDB

DynamoDB の使用状況と変動の傾向

  • DynamoDB accounts by category(カテゴリ別DynamoDBアカウント)
    • 3ヶ月
    • 2ヶ月
    • 先月
  • DynamoDB cost per category (カテゴリ別DynamoDBコスト)
  • DynamoDB on-demand usage and cost(DynamoDBオンデマンドの使用状況とコスト)

Messaging & Streaming

Kinesis と Managed Streaming for Apache Kafka (Amazon MSK) の使用状況と変動の傾向

Amazon Kinesis
Amazon Managed Streaming for Apache Kafka (MSK)
  • Amazon MSK cost by accounts(アカウント別Amazon MSKコスト)
  • Amazon MSK cost by regions(リージョン別Amazon MSKコスト)
  • Amazon MSK cost by usage type(使用タイプ別Amazon MSKコスト)
  • Amazon MSK daily cost by usage type(使用タイプ別Amazon MSK 1日あたりのコスト)

Data Transfer and Networking

データ転送とネットワークの使用状況と変動の傾向

  • Data transfer costs by accounts in last 3 months(過去3か月間のアカウント別のデータ転送コスト)
  • Data Transfer Costs per Type(タイプ別のデータ転送コスト)
  • Data transfer gigabytes per service(サービスあたりのデータ転送ギガバイト
  • Data transfer details usage and cost(データ転送の詳細、使用量、コスト)
  • Data transfer operation per account last month(先月アカウントあたりのデータ転送操作)
  • Data Transfer Daily GB per Operations(操作ごとのデータ転送量(1日あたりGB)):特定の操作やアクションに関連するデータ転送量を、日ごとにGB単位で表した指標
  • Data transfer usage in GB last month(先月のデータ転送使用量(GB))
Network Resource Utilisation Summary(ネットワークリソース使用率の概要)
  • Network product group usage cost per top 10 accounts(上位10アカウントあたりのネットワーク製品グループ使用コスト)

Public IPv4 addresses
  • Public IPv4 cost and projection in last 30 days(過去30日間のパブリックIPv4コストと予測)

  • Public IPv4 cost and projection per account in last 30 days(過去30日間のアカウントあたりのパブリックIPv4コストと予測)

Amazon CloudFront Summary
  • CloudFront top 10 accounts last 3 months(過去3か月間のCloudFrontトップ10アカウント)
  • CloudFront regions in last 3 months(過去3か月間のCloudFrontリージョン)
  • CloudFront operations in last 3 months(過去3か月間のCloudFrontオペレーション)

AI/ML

SageMaker や Comprehend、Textract、Rekognition、Bedrockの使用状況と変動の傾向

Amazon SageMaker Summary
  • SageMaker spend per account(アカウントあたりのSageMaker支出)
  • SageMaker spend per region(リージョンごとの SageMaker支出)
  • SageMaker training jobs per compute type(コンピューティングタイプごとのSageMakerトレーニングジョブ)
  • SageMaker spend per build environment: Studio vs Notebook instance(SageMakerのビルド環境あたりの費用: StudioとNotebookインスタンス
  • SageMaker spend and unit cost by instance type(SageMakerのインスタンスタイプ別の支出と単価)
  • SageMaker spend per usage type group(使用タイプグループごとのSageMaker支出)
Amazon Comprehend Summary
  • Comprehend spend per account(アカウントごとのComprehend支出)
  • Comprehend spend per region(リージョンごとのComprehend支出)
  • Comprehend topic modeling cost and usage (MB)(Comprehendトピックモデリングのコストと使用状況 (MB))
  • Comprehend natural language processing cost and usage (Units)(Comprehend自然言語処理のコストと使用状況(単位))
Amazon Textract and Rekognition Summary
  • Amazon Textract and Rekognition spend per account(アカウントごとのAmazon TextractおよびRekognitionの支出)
  •  Amazon Textract and Rekognition spend per region(リージョンごとのAmazon TextractおよびRekognitionの支出)
  •  Amazon Textract and Rekognition spend per service(サービスごとのAmazon TextractおよびRekognitionの支出)
  • Amazon Textract and Rekognition units processed(Amazon TextractおよびRekognitionの処理された単位)
  •  Amazon Textract and Rekognition daily cost and units(Amazon TextractおよびRekognitionの毎日のコストと単位)

Monitoring & Observability

CloudWatchとCloudTrailの使用状況と変動の傾向

Amazon CloudWatch
  • CloudWatch usage cost by accounts(アカウント別のCloudWatch使用コスト)

  • CloudWatch usage cost per usage type group(使用タイプグループごとのCloudWatch使用コスト)

  • CloudWatch usage cost per operation (CloudWatch操作ごとの使用コスト)

  • CloudWatch usage cost per usage type(使用タイプごとのCloudWatch使用コスト)

AWS CloudTrial
  • Top 10 CloudTrail usage cost by accounts(アカウント別のCloudTrail使用コスト上位10件)

  • CloudTrail Usage Cost by Usage Type(使用タイプ別のCloudTrail使用コスト)

AWS Config
  • AWS Config usage cost by accounts(アカウント別のAWS Config使用コスト)
  • AWS Config Usage Cost by Region(リージョン別のAWS Config使用コスト)
  • AWS Config usage by usage type group(使用タイプグループ別のAWS Config使用状況)
  • AWS Config usage cost by usage type group(使用タイプグループ別のAWS Config使用コスト)

Amazon WorkSpaces

Amazon Workspacesの使用状況と変動の傾向

GameTech & Media

Amazon GameLift と AWS Elemental MediaConnect などの使用状況と変動の傾向

  • GameLift cost by accounts(アカウントごとのGameLiftコスト)
  • GameLift cost by region(リージョンごとのGameLiftコスト)
  • GameLift costs by instance types(インスタンスタイプごとのGameLiftコスト)
  • GameLift operation(GameLiftの操作)
  • GameLift instance types in last 90 days(過去90日間のGameLiftインスタンスタイプ)
AWS Elemental Summary
  • Elemental cost by accounts(アカウントごとのElementalコスト)
  • Elemental cost by product family(プロダクトファミリーごとのElementalコスト)
  • Elemental MediaConvert elasticity by purchase option(購入オプションによるElemental MediaConvert弾力性)
  • Elemental Elasticity by purchase option(購入オプションによるElemental弾力性)

OPTICS Explorer

その他すべての使用状況と変動の傾向

  • Spend chart monthly(月間支出チャート)

  • Forecast spend in next 6 months(今後6か月間の支出予測)

  • Spend chart daily(支出チャート(日別))

  • Spend table(支出表)

  • Top 10 accounts to Top 10 resources relation(上位10アカウントと上位10リソースの関係)

  • Amortized usage cost per operation(操作ごとの償却使用コスト)

後始末

本検証で作成したQuickSightすべてのリソースを削除します。
データエクスポートの削除自体は[請求とコスト管理]からでもできるのですが、QuickSightからの方がすべて削除できるので、QuickSightからやります。
1.[請求とコスト管理] > [データエクスポート]より、「costanalysis-export」を削除

このようにS3とQuickSightの方は残るため、これらも後で削除します。

2.当該アカウントのQuickSight UIにログイン
3.[分析]より、生成された分析「costanalysis-export analysis」をすべて削除

4.[ダッシュボード]より、「costanalysis-export」を削除

5.[データセット]より、「costanalysis-export」を削除

6.[QuickSightを管理]に移動
[セキュリティとアクセス許可] > [管理]より、S3バケット[quicksight-dataexport]のチェックを外して[完了] > [保存]

7.QuickSightアカウントの削除
作成したQuickSightアカウントを削除するには、Amazon QuickSight サブスクリプションの削除とアカウントの閉鎖 - Amazon QuickSightの「QuickSight UI を使用してアカウントを終了するには」に従います。
[アカウント設定] > [管理]

「アカウントの終了保護は on です。」をoffにし、「確認」を入力して[アカウントを削除]

8.S3削除
バケット「quicksight-dataexport」をまず空にします。

次に削除

番外編(要注意!)

実はCUDとは直接関係ないんですが、この検証を始めてからある日QuickSightでとんでもない料金が課金されていることに気づきました。

内訳を見ると、「QS Paginated Reports Monthly Subscription 225 Report」となっています。ページ分割されたレポートの月間サブスクリプションが225レポートという意味です。ページ分割レポートとは

Amazon でのページ分割レポートの使用 QuickSight - Amazon QuickSight

Amazon QuickSight ページ分割レポートを使用すると、高度にフォーマットされた複数ページの PDF レポートを作成、スケジュール、共有できます。これにより、これまで別々だったダッシュボードとレポートのシステムが統合されます。

こんな機能使った覚えがないので、QuickSightの[価格を管理]を調べてみると、確かにしっかり有効になってます。。


(プランをすぐに解約したので、画像では残り日数が表示されてます)

誰が有効にしたのかなと、CloudTrailでSubscriptionなどのイベント名で検索しても全くヒットせず。
どうやらQuickSightに新規にアサインするとき、東京リージョンにするとページ分割レポートアドオンがデフォルトONになるようです。確かに下の方までよく見ずデフォルトのまま進めた記憶があります。

225ドル日割りで課金されていたということは、225ドル÷31日=14日 つまり31−14=17日から課金が発生したことになる。自分が使い始めたのはその17日の辺りなので符号してます。
どうやら今年の4月のQuickSightのアップデート辺りから、新しくQuickSightにサインアップするときはページ分割レポートアドオンはデフォルトオンになるようなので要注意です!ここで注意喚起しておきます。

所要時間

2時間