DevContainer@VSCode×Go(Gin)×Air×Delveでデバッグ環境構築




コンテナ開発環境(DevContainers@VSCode) × ホットリロード(air) × ステップインデバッグ(delve)の環境構築メモ

DevContainersならコンテナ内のローカルホストのデバッガポートに直接アタッチ出来るのでシンプルに構成可能できる。

アーキ概要



コンテナ

golang:1.22.11-alpine3.21

delve デバッガ

インストール

go install github.com/go-delve/delve/cmd/dlv@latest

コマンドパレットのGo Install/Update ToolsからインストールでもOK ※VSCodeServer@コンテナにGoエクステンションが入っていること



Air ホットリロード

バイナリインストールと環境初期化

go install github.com/air-verse/air@latest \
&& cd path/to/app_root
&& air init #=> ./.air.tomlが生成される

.air.tomlコンフィグをデバッグ向けに編集

[build]
  # ビルド設定
  cmd = "go build -gcflags=\"all=-N -l\" -o ./tmp/main ."
  # 起動設定
  # --logオプションで標準出力に詳細ログを出力
  full_bin = "dlv --headless=true --listen=:2345 --api-version=2 --accept-multiclient exec --continue ./tmp/main"

# その他はデフォルト

ノーマルビルドと比較してデバッグビルドは時間はかかるようになった

-gcflags="all=-N -l"


デバッガ(delve など)を使う場合、Go の最適化が入っていると変数の値が見えなくなったり、 関数がインライン化されてブレークポイントが期待通り動かなくなることがあるので、Go コンパイラの最適化を無効化するためのオプションを指定する

オプションについて

  • -N: 最適化を無効化してデバッグしやすくする
  • -l: インライン展開を無効化して関数のインライン化を防ぐ

full_bin = "dlv --headless=true --listen=:40000 --api-version=2 --accept-multiclient exec --continue ./tmp/main"


dlv execでビルドバイナリ./tmp/mainをデバッグ実行する

オプションについて

  • —headless=true により、CLI(ターミナル)ではなく、エディタや別のデバッガから接続できる
  • --listen=:2345により、ポート2345にデバッガを待機
  • --accept-multiclient により、複数のクライアント(VSCode, GoLand など)から同時に接続できる
  • --continue により、Delve 起動時に自動的にプログラムを実行(手動で continue を打たなくて済む)

VSCode

デバッグ構成はアタッチ式で

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Attach Server",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "port": 2345,
            "host": "localhost",
            "showLog": true
        }
    ]
}

デバッグ

  1. アプリケーションディレクトリでairを実行する
cd path/to/app_root && air
  1. VSCodeの実行とデバッグ > Attach Serverでアタッチ開始
  2. ブレークポイントを設定してhttpリクエストを入れたらブレークする

SQLでカレンダーとかインデックスの付番


一定期間のカレンダーを生成したり、結果に論理番号を付番するメモ

カレンダー

起算日(2025-01-01) + オフセット(0..*)でカレンダーを作成する

オフセットの準備

指定レンジでインクリメンタルなオフセット値を持つテーブルを作成する

  • オフセット値は変数に持ち歩かせる
  • information_schema.COLUMNSのクエリ結果でレンジ行を確保しつつ、オフセット値を格納
  • LIMITでレンジを切る
-- 1行目でレンジ変数初期化
SELECT @_index := 0 AS _index -- 0
-- 2行目以降をUNIONで結合
UNION
-- 行確保のため、information_schema.COLUMNSをクエリして、レンジ変数をインクリメント
SELECT @_index := @_index + 1 AS _index FROM information_schema.COLUMNS -- 1..*
-- レンジの上限=7行
LIMIT 7;
+--------+
| _index |
+--------+
|      0 |
|      1 |
|      2 |
|      3 |
|      4 |
|      5 |
|      6 |
+--------+

カレンダーにする


  • 起算日(2025-01-01)にオフセット値を加算する
SELECT '2025-01-01' + INTERVAL _range._index DAY AS DATE FROM (
  -- 1行目でレンジ変数初期化
  SELECT @_index := 0 AS _index
  -- 2行目以降をUNIONで結合
  UNION
  -- 行確保のため、information_schema.COLUMNSをクエリして、レンジ変数をインクリメント
  SELECT @_index := @_index + 1 AS _index FROM information_schema.COLUMNS
  -- レンジの上限=10個
  LIMIT 7
) _range;
+------------+
| DATE       |
+------------+
| 2025-01-01 |
| 2025-01-02 |
| 2025-01-03 |
| 2025-01-04 |
| 2025-01-05 |
| 2025-01-06 |
| 2025-01-07 |
+------------+

論理番号を付番する

クエリ結果に対して、論理番号を付番して意味付けする

こんな科目-点数テーブルがあったとして

+-----------+-------+
| category  | score |
+-----------+-------+
| 国語       |    84 |
| 国語       |    91 |
| 国語       |    77 |
| 数学       |    88 |
| 数学       |    98 |
| 数学       |    81 |
| 理科       |    50 |
| 理科       |    80 |
| 理科       |    68 |
+-----------+-------+

結果に行番号を付番する

  • 行番号を変数に持ち歩かせる
  • FROM句で変数を初期化する

全教科の点数ランキング

SELECT
  -- 結果順に変数を加算
  @_rank := @_rank + 1 AS rank,
  score.category,
  score.score
FROM
  score,
  -- 変数を初期化。テーブルに用はない
  (SELECT @_rank:= 0) AS row_num
-- score降順
ORDER BY score.score DESC;
+------+----------+-------+
| rank | category | score |
+------+----------+-------+
|    1 | 数学      |    98 |
|    2 | 国語      |    91 |
|    3 | 数学      |    88 |
|    4 | 国語      |    84 |
|    5 | 数学      |    81 |
|    6 | 理科      |    80 |
|    7 | 国語      |    77 |
|    8 | 理科      |    68 |
|    9 | 理科      |    50 |
+------+----------+-------+

教科ごの点数ランキング

  • 付番する軸=教科の変わり目で行番号を初期化する
SELECT
  -- 付番する軸=教科の変わり目で行番号を初期化する
  @_rank := IF(@prev_category = score.category, @_rank + 1, 1) AS rank,
  @prev_category := score.category AS category,
  score.score
FROM score
-- カテゴリ×score降順
ORDER BY score.category ASC, score.score DESC
+------+----------+-------+
| rank | category | score |
+------+----------+-------+
|    1 | 国語      |    91 |
|    2 | 国語      |    84 |
|    3 | 国語      |    77 |
|    1 | 数学      |    98 |
|    2 | 数学      |    88 |
|    3 | 数学      |    81 |
|    1 | 理科      |    80 |
|    2 | 理科      |    68 |
|    3 | 理科      |    50 |
+------+----------+-------+

SESのバウンスメールをウェブフック


SESでバウンスレート由来のレビュー・配信停止を抑止するために、SNSのhttpsサブスクリプションでアプリケーションをフックするアーキ

ハードバウンスはSESがアカウントレベルのサプレッションリストに入れたあと、90日間の再送が抑止されるが、アプリケーション層でもレビュー対象として管理し、送信管理、ユーザへの修正依頼に利用する。 Amazon SESアカウントレベルのサプレッションリストの使用 - Amazon Simple Email Service

SES(ID) → ( バウンス/苦情フィードバック) → SNSトピック → (httpsウェブフック) → アプリケーション でフラグを立てる 


サブスクライブ認可

SNSトピックにhttpsプロトコルでサブスクリプションを作成すると、エンドポイントに次のhttpsリクエストが入るのでSubscribeURLを叩いて、承認を完了する。 エンドポイントで応答する実装を組んでもいいし、コピペしてブラウザUAからリクエストしても良い。

{
  "Type" : "SubscriptionConfirmation",
  "MessageId" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "Token" :"xxx...",
  "TopicArn" : "arn:aws:sns:ap-northeast-1:{AWSアカウントID}:{SNSトピック名}",
  "Message" : "You have chosen to subscribe to the topic arn:aws:sns:ap-northeast-1:{AWSアカウントID}:{SNSトピックID}.¥nTo confirm the subscription, visit the SubscribeURL included in this message.",
  // これ 
  "SubscribeURL" : "https://sns.ap-northeast-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:ap-northeast-1:{AWSアカウントID}:{SNSトピックID}&Token=xxx...",
  "Timestamp" : "2024-10-16T05:07:58.589Z",
  "SignatureVersion" : "1",
  "Signature" : "xxx...==",
  "SigningCertURL" : "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-xxx.pem"
}

認可完了のレスポンス

<ConfirmSubscriptionResponse>
  <ConfirmSubscriptionResult>
    <SubscriptionArn>
      arn:aws:sns:ap-northeast-1:{アカウントID}:{SNSトピック名}:{サブスクリプションID}
    </SubscriptionArn>
  </ConfirmSubscriptionResult>
  <ResponseMetadata>
    <RequestId>{レスポンスID}/RequestId>
  </ResponseMetadata>
</ConfirmSubscriptionResponse>

フィードバック

SESからSNSトピックにフィードバック通知を組む。


SESバウンストピックの受信

バウンスメールが発生したとき、エンドポイントに次のようなPOSTリクエストが入る。 Messageオブジェクトにバウンスまたは苦情メールのプロパティがjson文字列として入っている。

SESでバウンスなテストメールを発信する

{
  "Type" : "Notification",
  "MessageId" : "79a880a8-d9ea-538d-9f74-df218c1aa5f0",
  "TopicArn" : "arn:aws:sns:ap-northeast-1:{AWSアカウントID}:{SNSトピックID}",

  "Message" : "{¥"notificationType¥":¥"Bounce¥",¥"bounce¥":{¥"feedbackId¥":¥"0106019293deea0f-19d2444a-7a3a-4e40-a632-8b776bd2202d-000000¥",¥"bounceType¥":¥"Permanent¥",¥"bounceSubType¥":¥"General¥",¥"bouncedRecipients¥":[{¥"emailAddress¥":¥"bounce@simulator.amazonses.com¥",¥"action¥":¥"failed¥",¥"status¥":¥"5.1.1¥",¥"diagnosticCode¥":¥"smtp; 550 5.1.1 user unknown¥"}],¥"timestamp¥":¥"2024-10-16T05:48:32.000Z¥",¥"remoteMtaIp¥":¥"18.139.69.138¥",¥"reportingMTA¥":¥"dns; e234-11.smtp-out.ap-northeast-1.amazonses.com¥"},¥"mail¥":{¥"timestamp¥":¥"2024-10-16T05:48:32.056Z¥",¥"source¥":¥"ses@hoge.jp¥",¥"sourceArn¥":¥"arn:aws:ses:ap-northeast-1:{AWSアカウントID}:identity/hoge.jp¥",¥"sourceIp¥":¥"121.117.160.85¥",¥"callerIdentity¥":¥"ms-ohashi¥",¥"sendingAccountId¥":¥"{AWSアカウントID}¥",¥"messageId¥":¥"0106019293dee7b8-dc011c08-e485-4f71-bf91-e8b7d0542aa1-000000¥",¥"destination¥":[¥"bounce@simulator.amazonses.com¥"],¥"headersTruncated¥":false,¥"headers¥":[{¥"name¥":¥"From¥",¥"value¥":¥"ses@hoge.jp¥"},{¥"name¥":¥"To¥",¥"value¥":¥"bounce@simulator.amazonses.com¥"},{¥"name¥":¥"Subject¥",¥"value¥":¥"test bounce¥"},{¥"name¥":¥"MIME-Version¥",¥"value¥":¥"1.0¥"},{¥"name¥":¥"Content-Type¥",¥"value¥":¥"multipart/alternative;  boundary=¥¥¥"----=_Part_1489259_127934103.1729057712056¥¥¥"¥"}],¥"commonHeaders¥":{¥"from¥":[¥"ses@hoge.jp¥"],¥"to¥":[¥"bounce@simulator.amazonses.com¥"],¥"subject¥":¥"test bounce¥"}}}",

  "Timestamp" : "2024-10-16T05:48:32.836Z",
  "SignatureVersion" : "1",
  "Signature" : "xxx==",
  "SigningCertURL" : "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-xxx.pem",
  "UnsubscribeURL" : "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:{AWSアカウントID}:{SNSトピックID}:xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

データボディの仕様 Amazon の Amazon SNS通知コンテンツ SES - Amazon Simple Email Service

ウェブサーバーへのIP直アクセスを制限しつつ、ヘルスチェックを受容する



IPアドレス直のサーバーアクセスを403制限しつつ、ロードバランサのヘルスチェックを通すapacheconfの設定


ポイント


  • 一番初めにローディングされるVirtualHostディレクティブがデフォルトサーバーとなるため、Include順を考慮したconfファイルを命名する
  • ファイル命名規則のInclude順でデフォルトサーバーは自明だが、明示的にServerName anyですべてのホスト名を捕まえている
  • デフォルトサーバーは:80:443ポートごとに設定が必要。 <VirtualHost *:*>だと、うまく動かない
  • デフォルトサーバーはhttpd -Sで確認する

httpd -S
VirtualHost configuration:
*:80                   is a NameVirtualHost
         default server any (/usr/local/httpd-2.4/conf/extra/httpd-vhosts.conf:1)
         port 80 namevhost any (/usr/local/httpd-2.4/conf/extra/httpd-vhosts.conf:1)
         port 80 namevhost webapp.com (/usr/local/httpd-2.4/conf/extra/httpd-vhosts.conf:28)
*:443                  is a NameVirtualHost
         default server any (/usr/local/httpd-2.4/conf/extra/httpd-vhosts.conf:15)
         port 443 namevhost any (/usr/local/httpd-2.4/conf/extra/httpd-vhosts.conf:15)
         port 443 namevhost webapp.com (/usr/local/httpd-2.4/conf/extra/httpd-vhosts.conf:45)

設定


  • :80:443それぞれのポートでデフォルトのバーチャルホストを定義する
  • ドキュメントルートを/var/www/_defaultに設定して、ロードバランサのヘルスチェックファイル/hb.html=>/var/www/_default/hb.htmlを作成
  • ヘルスチェック=hb.htmlはELB=サブネットワークにだけ公開したいので、リモートアドレスをRemoteIPTrustedProxyでX-Forwarded-For偽装をはじく。
  • リモートアドレスがtrustでなければ、X-Forwarded-Forを剥がしてクライアントの本来のリモートアドレスを採用する
  • :443のデフォルトサーバーはアクセス不要なので全拒否



<VirtualHost *:80>
    DocumentRoot "/var/www/_default/" 
    ServerName any
    <Directory "/var/www/_default/">
        Require all granted
    </Directory>
    # リモートアドレスがプロキシーのRemoteIPTrustedProxyアドレス帯ならば、X-Forwarded-Forを採用する。X-Forwarded-Forがなければリモートアドレスを採用する=ロードバランサである
    # リモートアドレスがプロキシーのRemoteIPTrustedProxyアドレス帯でなれけば、X-Forwarded-Forを剥がして、リモートアドレスを採用する=プロキシーを経由しないインターネットアクセス
    RemoteIPHeader X-Forwarded-For
    RemoteIPTrustedProxy 172.21.0.0/16

    # リモートアドレスがプロキシーのRemoteIPTrustedProxyアドレス帯ならば、X-Forwarded-Forを採用する。X-Forwarded-Forがなければリモート
アドレスを採用する=ロードバランサである
    # リモートアドレスがプロキシーのRemoteIPTrustedProxyアドレス帯でなれけば、X-Forwarded-Forを剥がして、リモートアドレスを採用する=プ
ロキシーを経由しないインターネットアクセス
    RemoteIPHeader X-Forwarded-For
    RemoteIPTrustedProxy 172.21.0.0/16

    # サブネットワーク直の侵入のみ許可する
    <Location />
        <RequireAll>
            Require ip 172.21.0.0/16
        </RequireAll>
    </Location>

</VirtualHost>

<VirtualHost *:443>
    ServerName any
    <Location />
        Require all denied
    </Location> 
</VirtualHost>

GuardDutyでセキュリティリスクを可視化する




Amazon GuardDutyは、AWSアカウントのイベントやネットワークトラフィックを監視して、不正アクセス、データ漏洩、マルウェアの兆候など不審な動作や脅威を自動的に検出するセキュリティサービス。検出結果とサマリーをGuardDutyコンソールに可視化してくれる。

具体的には次のような不審な動きを検出してくれる(と、AWS営業さんが熱心に語っていた)

  • ブルートフォース攻撃
  • 通常アクセスされない地域やIPからの接続試行
  • S3から外部への大規模データ転送
  • EC2の削除、IAM権限の変更
  • EC2がボットネットのC2サーバーと通信
  • EC2から外部への異常なトラフィック(例: マルウェアのダウンロード)
  • 高頻度でスキャンを行うポートスキャン
  • VPCから外部IPへの大量のデータ送信
  • ブラックリスト登録済みのIPアドレスとの通信
  • 既知のマルウェア感染ホストへの接続

データソースにはAWS CloudTrail、Amazon VPC フローログ、DNS ログをデータソースとして利用する。

Amazon GuardDuty は、AWS CloudTrail、VPC フローログ、AWS DNS ログから独立したデータストリームを直接取得します。Amazon S3 バケットポリシーを管理したり、ログを収集して保存する方法を変更したりする必要はありません。 Amazon GuardDutyを導入する前に知っておきたいこと | DevelopersIO

GuardDutyの有効化

GuardDutyを有効化するだけで、独立したデータストリームからAWS CloudTrail、Amazon VPC フローログ、DNS ログを継続的にモニタリングしてくれる。

検出の通知

【AWS】GuardDutyでイベント検知したらメール通知する仕組みを作る|hiroyu0510 【AWS】GuardDutyの通知メールをカスタマイズしてみる|hiroyu0510 GuardDutyからのイベント通知をちょっと見やすくして通知する | DevelopersIO


脅威を検出すると、==FindingをGuardDutyのコンソールにリストアップしてくれるが、SNS通知はCloudWatch Eventsで自力で構成する必要がある。

GuardDutyでFindingが発生すると、CloudWatch Eventsルールが発火します。 Amazon GuardDutyを導入する前に知っておきたいこと | DevelopersIO

Amazon EventBridge

EventBridgeでGuardDutyの脅威検出のイベントGuardDuty Findingに対して、SNSに連携するためのルールを作成する

イベントパターン

フックするイベントを定義する which

デフォルトのイベントパターン

イベントパターンについては、デフォルトの記載のままだと、検知したすべてを通知することになります。以下はすべての検知を通知する例です。 【AWS】GuardDutyでイベント検知したらメール通知する仕組みを作る|hiroyu0510

{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Finding"]
}

脅威にはかなりのパターンがあるので、SNS通知は緊急度の高い脅威レベル(=severity)でフィルタリングして、定常的にGuardDutyコンソールを保守する運用でいく。

もし、通知をフィルタリングしたい場合、危険度Midium以上とか危険度HIGH以上に限定して通知するというような場合は、以下のようにすることもできます。例ですが、危険度4以上(Midium以上)の場合に通知する設定です。severityでフィルタリングをかけることができます。 【AWS】GuardDutyでイベント検知したらメール通知する仕組みを作る|hiroyu0510

{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Finding"],
  "detail": {
    "severity": [{
      "numeric": [">=", 4]
    }]
  }
}

ターゲット(SNS)への入力

ルール > 編集 > ターゲットを選択 > ターゲットN > 追加設定 > 入力を設定 > ターゲット入力を設定から 入力トランスフォーマーを選択することで、ターゲットへの入力データをカスタマイズできる

入力パス what

使用したいGuardDuty FindingイベントのデータをJSONオブジェクトの任意キーにマッピングする 付録 > GuardDutyのサンプルイベント

{
    "Account_ID": "$.detail.accountId",
    "Finding_ID": "$.detail.id",
    "Finding_Type": "$.detail.type",
    "Finding_description": "$.detail.description",
    "eventFirstSeen": "$.detail.service.eventFirstSeen",
    "region": "$.region",
    "severity": "$.detail.severity"
}

テンプレート how

入力パスに定義したデータキーを使って、ターゲットに渡すテンプレートを作成する テキストでもjsonでも良い

"重要度 <severity> のGuardDutyイベントが発生しました。"
"アカウントID: <Account_ID>"
"発生日時: <eventFirstSeen>" 
"検出タイプ: <Finding_Type>"
"リージョン: <region>"
"タイプの説明: <Finding_description>"
"レポートURL: https://<region>.console.aws.amazon.com/guardduty/home?region=<region>#/findings?macros=current&fId=<Finding_ID>"

出力例 result

重要度 5 のGuardDutyイベントが発生しました。"
"アカウントID: 123456789012"
"発生日時: 2017-10-31T23:16:23Z" 
"検出タイプ: Canary:EC2/Stateless.IntegTest"
"リージョン: us-east-1"
"タイプの説明: Canary:EC2/Stateless.IntegTest"
"詳細はGuardDuty コンソール( https://us-east-1.console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?macros=current&fId=16afba5c5c43e07c9e3e5e2e544e95df )を確認してください。

入力パス、テンプレート、および出力の例


動作確認

GuardDutyに対して、Backdoorテストイベントを発火してSNSトピックのサブスクライバーに届いたら完了

# ディテクタIDはアカウント/リージョンごとに割り当てられたGuardDutyのuuidで、GuardDuty > 設定から確認可能
aws guardduty create-sample-findings \
  --detector-id 96c0a1c32ad10a4bef87a28aad793f46 \
  --finding-types "Backdoor:EC2/DenialOfService.Dns"

脅威に対する公式シューティングガイド

脅威のシューティングはAWS公式ガイドを参考に対策する GuardDuty 検出結果タイプ - Amazon GuardDuty

マルウェアスキャン

[神アップデート]GuardDutyがEC2やECSのマルウェア検知時のスキャンに対応したので実際にスキャンさせてみた #reinforce | DevelopersIO

  • Backdoor:EC2/C&CActivity.BCryptoCurrency:EC2/BitcoinTool.B!DNSなどマルウェアの動作が疑われるFindings検知があると自動でマルウェアスキャンがトリガーされる。
  • オンデマンドのマルウェアスキャンもある

付録

GuardDutyのサンプルイベント

{
  "version": "0",
  "id": "c8c4daa7-a20c-2f03-0070-b7393dd542ad",
  "detail-type": "GuardDuty Finding",
  "source": "aws.guardduty",
  "account": "123456789012",
  "time": "1970-01-01T00:00:00Z",
  "region": "us-east-1",
  "resources": [],
  "detail": {
    "schemaVersion": "2.0",
    "accountId": "123456789012",
    "region": "us-east-1",
    "partition": "aws",
    "id": "16afba5c5c43e07c9e3e5e2e544e95df",
    "arn": "arn:aws:guardduty:us-east-1:123456789012:detector/123456789012/finding/16afba5c5c43e07c9e3e5e2e544e95df",
    "type": "Canary:EC2/Stateless.IntegTest",
    "resource": {
      "resourceType": "Instance",
      "instanceDetails": {
        "instanceId": "i-05746eb48123455e0",
        "instanceType": "t2.micro",
        "launchTime": 1492735675000,
        "productCodes": [],
        "networkInterfaces": [{
          "ipv6Addresses": [],
          "privateDnsName": "ip-0-0-0-0.us-east-1.compute.internal",
          "privateIpAddress": "0.0.0.0",
          "privateIpAddresses": [{
            "privateDnsName": "ip-0-0-0-0.us-east-1.compute.internal",
            "privateIpAddress": "0.0.0.0"
          }],
          "subnetId": "subnet-d58b7123",
          "vpcId": "vpc-34865123",
          "securityGroups": [{
            "groupName": "launch-wizard-1",
            "groupId": "sg-9918a123"
          }],
          "publicDnsName": "ec2-11-111-111-1.us-east-1.compute.amazonaws.com",
          "publicIp": "11.111.111.1"
        }],
        "tags": [{
          "key": "Name",
          "value": "ssh-22-open"
        }],
        "instanceState": "running",
        "availabilityZone": "us-east-1b",
        "imageId": "ami-4836a123",
        "imageDescription": "Amazon Linux AMI 2017.03.0.20170417 x86_64 HVM GP2"
      }
    },
    "service": {
      "serviceName": "guardduty",
      "detectorId": "3caf4e0aaa46ce4ccbcef949a8785353",
      "action": {
        "actionType": "NETWORK_CONNECTION",
        "networkConnectionAction": {
          "connectionDirection": "OUTBOUND",
          "remoteIpDetails": {
            "ipAddressV4": "0.0.0.0",
            "organization": {
              "asn": -1,
              "isp": "GeneratedFindingISP",
              "org": "GeneratedFindingORG"
            },
            "country": {
              "countryName": "United States"
            },
            "city": {
              "cityName": "GeneratedFindingCityName"
            },
            "geoLocation": {
              "lat": 0,
              "lon": 0
            }
          },
          "remotePortDetails": {
            "port": 22,
            "portName": "SSH"
          },
          "localPortDetails": {
            "port": 2000,
            "portName": "Unknown"
          },
          "protocol": "TCP",
          "blocked": false
        }
      },
      "resourceRole": "TARGET",
      "additionalInfo": {
        "unusualProtocol": "UDP",
        "threatListName": "GeneratedFindingCustomerListName",
        "unusual": 22
      },
      "eventFirstSeen": "2017-10-31T23:16:23Z",
      "eventLastSeen": "2017-10-31T23:16:23Z",
      "archived": false,
      "count": 1
    },
    "severity": 5,
    "createdAt": "2017-10-31T23:16:23.824Z",
    "updatedAt": "2017-10-31T23:16:23.824Z",
    "title": "Canary:EC2/Stateless.IntegTest",
    "description": "Canary:EC2/Stateless.IntegTest"
  }
}

MFA認証つきのIAMユーザでaws cliを利用するワンライナーコマンド


IAMユーザのARNとMFAデバイストークンを用意してワンタイムセッションを確立して作業する

eval `aws sts get-session-token --serial-number {IAMユーザのARN} --token-code {MFAトークン} | awk ' $1 == "\"AccessKeyId\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_ACCESS_KEY_ID="$2 } $1 == "\"SecretAccessKey\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SECRET_ACCESS_KEY="$2} $1 == "\"SessionToken\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SESSION_TOKEN="$2 } '`

付録

  • AWS Security Token Service(STS)を使用して、MFAデバイスのトークンコードから一時的な認証情報(Temporary Security Credentials)を取得する
  • 有効期限はデフォルト12時間

aws sts get-session-token --serial-number <IAMユーザのARN> --token-code <MFAトークン>

# 結果
{
    "Credentials": {
        "SecretAccessKey": "secret-access-key",
        "SessionToken": "temporary-session-token",
        "Expiration": "expiration-date-time",
        "AccessKeyId": "access-key-id"
    }
}

次の環境変数にセット

export AWS_ACCESS_KEY_ID={AccessKeyId}
export AWS_SECRET_ACCESS_KEY={SecretAccessKey}
export AWS_SESSION_TOKEN={SessionToken}

ClamAVでアップロードファイルをウィルス対策する

clamavdでサーバーサイドのウィルススキャンするためのアーキ

ClamAV



  • マルチプラットフォームでフリーのアンチウィルススイート
  • ClamAVにはコマンド版とデーモン版がある。デーモン版はTCPでファイル単位のウィルス検閲に対応している
  • clamavのdockerイメージを使ってTCPでウィルススキャンする

clamdホスト構築



clamavの公式イメージによると、


The development of this image will be discontinued. Since 0.104 Cisco provides official docker images for clamav. This image here will be on hold and supported as long as possible.

このイメージの開発は終了します。0.104 以降、Cisco は clamav 用の公式 docker イメージを提供しています。こちらのイメージは保留され、可能な限りサポートされます。

mkodockx/docker-clamav


一応ちゃんと動いてくれるが、現在開発停止していてデータベースの更新中に予期しない切断が発生するため、Ciscoの公式版を使ってくれとのこと。
clamav/clamav | dockerhub


version: '3.2'
services:
  app: ...
    depends_on:
      - clamavd

  clamavd:
    container_name: clamavd
    hostname: clamavd
    image: clamav/clamav
    ports:
      - 3310

ウィルスデータベース



ClamAVのウイルステータベースは、/var/lib/clamavディレクトリにmain.cvdおよびdaily.cvdというファイルで保存されている。 これらのデータベースは、freshclamコマンドで更新する。

ClamAVのウイルスデータベースを自動更新するには - @IT


clamav/clamav | dockerhubイメージではfreshclamデーモンが一日一回(--checks=1)ウィルスデータベースを更新するようにチューニングされている。
freshclamコマンド版を使ってcrontabでスケジュールすることもできるが、まあおまかせのデーモン版で。


ps | grep clamav
PID   USER     TIME  COMMAND
   12 clamav    0:00 freshclam --checks=1 --daemon --foreground --stdout --user=clamav
   13 clamav    0:22 clamd --foreground

なお、かつて高頻度でダウンロードをかけるクライアントに対策するためfreshclamによるダウンロードに制限した経緯があるようだ。


Abuse of the download system has forced us to push people towards FreshClam.
Unfortunately a handful have ruined it for everyone. (Looking at you, handful of IPs that download the daily.cvd 3x a second)

ダウンロードシステムの乱用により、私たちは人々をFreshClamに追い込むことを余儀なくされました。
残念ながら、一握りがみんなのためにそれを台無しにしました。(あなたを見て、daily.cvdを1秒間に3回ダウンロードする一握りのIP)

Official docker images of clamav


freshclamの設定ファイルを見るとデフォルトは12回(2時間に一回)のようだ。


# Number of database checks per day.
# Default: 12 (every two hours)                             
#Checks 24   

ClamAVによるリアルタイムスキャンの設定(ClamAV 1.0版) | 稲葉サーバーデザイン


clamavdのクライアント実装



PINGコマンドで疎通を確認した後に、INSTREAMスキャンコマンドを投げる


<?php

class ClamAVClient
{
    // clamdホストのデフォルト
    const CLAMAVD_HOST = 'clamavd';
    const CLAMAVD_PORT = 3310;

    private $_clamavd_host;
    private $_clamavd_port;
    private $_raw_fact;
    private $_socket;

    public function __construct($host=self::CLAMAVD_HOST, $port=self::CLAMAVD_PORT)
    {
        $this->_clamavd_host = $host;
        $this->_clamavd_port = $port;
    }

    public function __destruct()
    {
        $this->closeSocket();
    }

    /**
     * tcpソケットを閉じます
     */
    public function closeSocket(){
        if( is_resource($this->_socket) ) fclose($this->_socket);
    }

    /**
     * tcpソケットを開きます
     * @return resource TCPセッションハンドル
     */
    private function openSocket(){
        $this->_socket = stream_socket_client("tcp://{$this->_clamavd_host}:{$this->_clamavd_port}", $errno, $errstr, 10);
        if (!$this->_socket) {
            throw new RuntimeException("ホストの接続に失敗しました。({$errno}){$errstr}");
        }
        return $this->_socket;
    }

    /**
     * ソケットを取得します
     * @return resource TCPセッションハンドル
     */
    private function getSocket(){
        return $this->_socket ?: $this->openSocket();
    }

    /**
     * clamavdホストにPINGをリクエストします
     * @note PINGに対するclamavdの正常応答はPONG
     * @return bool true: 正常応答 / false: 異常応答
     */
    public function ping(){

        $socket = $this->getSocket();

        stream_socket_sendto($socket, 'PING');
        $this->_raw_fact = stream_socket_recvfrom($socket, 1024);

        return $this->fact() === 'PONG';
    }

    /**
     * clamavdのレスポンステキストからファクトを抽出します
     * @param string $response_text
     * @return string ファクト
     */
    public function fact(){
        $fact = (strrchr($this->_raw_fact, ":")) ?: ": {$this->_raw_fact}";
        return trim(substr($fact, 1));
    }

    /**
     * clamavdにファイルスキャンをリクエストします
     * 
     * @return bool true: ウィルスなし / false: ウイルスあり
     */
    public function scan($filepath){

        // ファイルが存在するか確認
        if ( file_exists($filepath) === false ) {
            throw new RuntimeException("ファイルが存在しません。file({$filepath})");
        }

        // ファイル内容を読み込み
        $fileContent = file_get_contents($filepath);
        if ($fileContent === false) {
            throw new RuntimeException("ファイルの読み込みに失敗しました。file({$filepath})");
        }

        // PINGセッション
        if( ( new ClamAVClient() )->ping() === false ) {
            throw new RuntimeException("PINGに失敗しました。");
        }

        // スキャンセッション
        $socket = $this->getSocket();

        // ストリームスキャンのハンドシェイク開始 => zINSTREAM\0
        $res = stream_socket_sendto($socket, "zINSTREAM\0");

        // ファイルを8KBでチャンク送信する
        $chunkSize = 8192;
        $bytesSent = 0;
        while ( $bytesSent < strlen($fileContent) ) {
            $chunk = substr($fileContent, $bytesSent, $chunkSize);
            $chunkLength = pack('N', strlen($chunk));
            $res = stream_socket_sendto($socket, $chunkLength . $chunk);
            $bytesSent += $chunkSize;
        }

        // スキャンのハンドシェイク終了 => ゼロバイトを送信
        $res = stream_socket_sendto($socket, pack('N', 0));

        // スキャン結果を読み取り => 問題なければ"OK"文字列が入っている
		// ウィルス検出	=> "stream: Eicar-Signature FOUND"
		// ウィルス未検出	=> "stream: OK"
        $this->_raw_fact = stream_socket_recvfrom($socket, 1024);        
        if( empty( $this->_raw_fact ) ){
            throw new RuntimeException("診断結果が空です。");
		}

        echo "clamscan result ({$filepath}) : {$this->_raw_fact}";
        return $this->fact() === 'OK';
    }
}

検閲



テストにはEICAR テストファイルを使用する。
EICAR テストファイルとは?


echo "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" > /path/to/virus.txt

try{
	$clamav = new ClamAVClient();
	$result = $clamav->scan('/path/to/virus.txt');
}catch(RuntimeException $e){
	echo "clamavスキャンで例外が発生 {$e->getMessage()}";	
}

if( $result === false ){
  	throw new RuntimeException("clamavスキャンで脅威を検出しました。fact({$clamav->fact()})"); //=> Eicar-Signature FOUND
}

AthenaでALBのアクセスログをクエリすると結果が空


直近のAthenaのALBアクセスログ見ようと思ってクエリしたら結果が空っぽになっていた。S3にログは随時上がってきているのに??


問題発生時期を遡っていくと2024/5/20あたりから、と思ったら2024/5/20にALBのバージョンアップでアクセスログにtraceability_idが追加となりカラム数の不一致が原因であった。
Querying Application Load Balancer logs - Amazon Athena


AWS公式のCREATE TABLE スニペットの通りtraceability_idを追加したテーブルを再構成すると無事にクエリは成功した。

新しいテーブル定義


CREATE EXTERNAL TABLE `alb-access-log-rev20240520`(
  `type` string COMMENT '', 
  `time` string COMMENT '', 
  `elb` string COMMENT '', 
  `client_ip` string COMMENT '', 
  `client_port` int COMMENT '', 
  `target_ip` string COMMENT '', 
  `target_port` int COMMENT '', 
  `request_processing_time` double COMMENT '', 
  `target_processing_time` double COMMENT '', 
  `response_processing_time` double COMMENT '', 
  `elb_status_code` string COMMENT '', 
  `target_status_code` string COMMENT '', 
  `received_bytes` bigint COMMENT '', 
  `sent_bytes` bigint COMMENT '', 
  `request_verb` string COMMENT '', 
  `request_url` string COMMENT '', 
  `request_proto` string COMMENT '', 
  `user_agent` string COMMENT '', 
  `ssl_cipher` string COMMENT '', 
  `ssl_protocol` string COMMENT '', 
  `target_group_arn` string COMMENT '', 
  `trace_id` string COMMENT '', 
  `domain_name` string COMMENT '', 
  `chosen_cert_arn` string COMMENT '', 
  `matched_rule_priority` string COMMENT '', 
  `request_creation_time` string COMMENT '', 
  `actions_executed` string COMMENT '', 
  `redirect_url` string COMMENT '', 
  `lambda_error_reason` string COMMENT '', 
  `target_port_list` string COMMENT '', 
  `target_status_code_list` string COMMENT '', 
  `classification` string COMMENT '', 
  `classification_reason` string COMMENT '', 
  -- 追加↓
  `traceability_id` string COMMENT '')
PARTITIONED BY ( 
  `log_date` string)
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.serde2.RegexSerDe' 
WITH SERDEPROPERTIES ( 
  -- 変更↓
  'input.regex'='([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) (.*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-_]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^s]+?)\" \"([^s]+)\" \"([^ ]*)\" \"([^ ]*)\" ?([^ ]*)?( .*)?') 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://alb-access-log/AWSLogs/************/elasticloadbalancing/ap-northeast-1'
TBLPROPERTIES (
  'projection.enabled'='true', 
  'projection.log_date.format'='yyyy/MM/dd', 
  'projection.log_date.interval'='1', 
  'projection.log_date.interval.unit'='DAYS', 
  'projection.log_date.range'='NOW-1YEARS,NOW',
  'projection.log_date.type'='date', 
  'storage.location.template'='s3://alb-access-log/AWSLogs/************/elasticloadbalancing/ap-northeast-1/${log_date}', 
  'transient_lastDdlTime'='1717717328')

以前のテーブル定義


CREATE EXTERNAL TABLE `alb-access-log`(
  `type` string COMMENT '', 
  `time` string COMMENT '', 
  `elb` string COMMENT '', 
  `client_ip` string COMMENT '', 
  `client_port` int COMMENT '', 
  `target_ip` string COMMENT '', 
  `target_port` int COMMENT '', 
  `request_processing_time` double COMMENT '', 
  `target_processing_time` double COMMENT '', 
  `response_processing_time` double COMMENT '', 
  `elb_status_code` string COMMENT '', 
  `target_status_code` string COMMENT '', 
  `received_bytes` bigint COMMENT '', 
  `sent_bytes` bigint COMMENT '', 
  `request_verb` string COMMENT '', 
  `request_url` string COMMENT '', 
  `request_proto` string COMMENT '', 
  `user_agent` string COMMENT '', 
  `ssl_cipher` string COMMENT '', 
  `ssl_protocol` string COMMENT '', 
  `target_group_arn` string COMMENT '', 
  `trace_id` string COMMENT '', 
  `domain_name` string COMMENT '', 
  `chosen_cert_arn` string COMMENT '', 
  `matched_rule_priority` string COMMENT '', 
  `request_creation_time` string COMMENT '', 
  `actions_executed` string COMMENT '', 
  `redirect_url` string COMMENT '', 
  `lambda_error_reason` string COMMENT '', 
  `target_port_list` string COMMENT '', 
  `target_status_code_list` string COMMENT '', 
  `classification` string COMMENT '', 
  `classification_reason` string COMMENT '')
PARTITIONED BY ( 
  `log_date` string)
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.serde2.RegexSerDe' 
WITH SERDEPROPERTIES ( 
  'input.regex'='([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^s]+?)\" \"([^s]+)\" \"([^ ]*)\" \"([^ ]*)\"') 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://alb-access-log/AWSLogs/************/elasticloadbalancing/ap-northeast-1'
TBLPROPERTIES (
  'projection.enabled'='true', 
  'projection.log_date.format'='yyyy/MM/dd', 
  'projection.log_date.interval'='1', 
  'projection.log_date.interval.unit'='DAYS', 
  'projection.log_date.range'='NOW-1YEARS,NOW',
  'projection.log_date.type'='date', 
  'storage.location.template'='s3://alb-access-log/AWSLogs/************/elasticloadbalancing/ap-northeast-1/${log_date}', 
  'transient_lastDdlTime'='1645361779')

GitリモートリポジトリをGitHubからCodeCommitに引っ越しする



Githubリモートリポジトリをブランチやコミット履歴を含めてまるごとCodeCommitにお引っ越しする手順。
引っ越しにはリポジトリのブランチ・タグ、コミット履歴など全てのリファレンスを含んだバックアップであるbareリポジトリを利用する。


bareリポジトリとは




リポジトリの構成管理


  • ブランチ・タグ、コミット履歴など全てのリファレンスを含む、サーバーサイドの構成管理リポジトリであり完全バックアップである。管理対象のファイル実態は持たない。
  • git clone --bareオプションでbareリポジトリをクローンできる。
  • git clone --mirrorオプションでもbareリポジトリをクローンできる。--bareとの違いは文字通りリモートリポジトリと同期するように設計されていて、.git/configはmirror=trueである。よってデフォルトでは全リファレンスをpushするし、git remote updateでは全リファレンスを追跡する

--mirror Set up a mirror of the source repository. This implies --bare. Compared to --bare, --mirror not only maps local branches of the source to local branches of the target, it maps all refs (including remote-tracking branches, notes etc.) and sets up a refspec configuration such that all these refs are overwritten by a git remote update in the target repository.


ソースリポジトリのミラーを設定します。これは --bare を含意します。--bare と比較して、--mirror はソースのローカルブランチをターゲットのローカルブランチにマッピングするだけでなく、すべてのリファレンス(リモート追跡ブランチ、ノートなども含む)をマッピングし、これらのリファレンスがターゲットリポジトリの git remote update によって上書きされるように refspec 設定を行います。

Git - git-clone Documentation


non-bareリポジトリとは




リポジトリのワークスペース


  • チェックアウト済のブランチ・タグのコミット履歴とファイル実態で構成されるクライアントサイドのワークスペースである。
  • いつものgit clonenon-bareリポジトリ。

引っ越し手順




CodeCommitでリポジトリを作成して、IAMユーザにCodeCommitのプッシュできるポリシーとSSH公開鍵を登録しておく


# GitHubからbareリポジトリをクローンする
git clone --mirror git@github.com:path/to/reponame.git

#  push先をCodeCommitのリポジトリに変更
cd ./reponame.git && git remote set-url --push origin ssh://git-codecommit.ap-northeast-1.amazonaws.com/path/to/reponame

# 全てのリファレンスを移行先にプッシュする(--mirror)
# (clone --mirror により、configのmirror=true状態のため、オプションなしでも全リファレンスはプッシュされる)
git push --mirror

S3に格納されたマネージドログをAthenaで解析する



AthenaでS3バケットにストックしているマネージドのログに対してクエリ検索する。
 

データベースを作成する

Athena でのデータベースの作成 - Amazon Athena コンソール上にファンクションはない。クエリエディタで作成する。  

CREATE DATABASE db-name;

 

テーブルを作成する

 

パーティション化

クエリ対象のスキャン量は料金コストに直結するため、パーティションでデータを絞り込んでコスト節約する。 パーティション化するためにS3のログの格納ルールとテーブルDDLに定義が必要。  

データをパーティションすることで、各クエリによってスキャンされるデータの量を制限できるようになるため、パフォーマンスが向上し、コストが削減されます。任意のキーでデータをパーティションに分割することができます。一般的な方法では、時間に基づいてデータをパーティションします。

Athena でのデータのパーティション化 - Amazon Athena

S3格納ルール

  対象ログが日付等の一定ルールに基づいたパーティションに格納されていること。フォーマットには「Hive形式」と「Hiveではない形式」がある。 Hive形式はパーティションが追加される度にテーブルのリペアが必要なので、ログのエクスポート設定はHiveではない方式にする。

Hiveではない形式

s3://path/to/2021/01/26/

Athena では、Hive 以外のスタイルのパーティション化スキームを使用することも可能です。例えば、 CloudTrail ログと Firehose 配信ストリームは、 などの日付部分に対して個別のパスコンポーネントを使用しますdata/2021/01/26/us/6fc7845e.json。これらの Hive スタイルではないパーティションの場合、ALTER TABLE ADD PARTITION を使用して手動でパーティションを追加します。

Athena でのデータのパーティション化 - Amazon Athena

Hive形式

  • ディレクトリが「key=value」形式
  • パーティションが追加される度にテーブルのリペアが必要
s3://path/to/year=2021/month=01/day=26/

Athena では Apache Hive スタイルのパーティションを使用できます。このパーティションのデータパスには、等号で連結されたキーと値のペア (例えば country=us/... または year=2021/month=01/day=26/...) が含まれています。つまり、それぞれのパスにより、パーティションのキーと値、両方の名前が表されます。新しい Hive パーティションをパーティションされたテーブルにロードするには、(Hive スタイルのパーティションのみで機能する) MSCK REPAIR TABLE コマンドを使用します。

Athena でのデータのパーティション化 - Amazon Athena

テーブル

【初心者向け】Amazon Athenaのパーティションとは  Amazon AthenaのPartition Projectionを使ったALBのアクセスログ解析環境をTerraformで構築する – PSYENCE:MEDIA

テーブル作成のDDLにPARTITIONED BY句とTBLPROPERTIES句を追加して、パーティション射影を有効にする

  • LOCATION句にS3のパーティション構造の上位ディレクトリまで指定する
  • PARTITIONED句にパーティション名を定義する
  • TBLPROPERTIES句にパーティションルールを定義する。projection.{パーティション名}.*にパーティションのルールを定義する。
CREATE EXTERNAL TABLE `table-name`( ... )
PARTITIONED BY (`log_date` string)
LOCATION 's3://{バケット名}/AWSLogs/{アカウントID}/elasticloadbalancing/{リージョン}'
TBLPROPERTIES (
  -- パーティション有効
  'projection.enabled'='true', 
  -- パーティションのデータ型 enum,integer,date,injectedをサポートしている
  'projection.log_date.type'='date',
  -- パーティションのフォーマット=S3のパーティション
  'projection.log_date.format'='yyyy/MM/dd',
  -- パーティション間隔 = 1日おき
  'projection.log_date.interval'='1',
  'projection.log_date.interval.unit'='DAYS',
  -- パーティションのレンジ
  'projection.log_date.range'='NOW-1YEARS,NOW',
  -- パーティションを含むS3バケットのロケーション
  'storage.location.template'='s3://{バケット名}/AWSLogs/{アカウントID}/elasticloadbalancing/{リージョン}/${log_date}'
)

Application Load Balancer ログのクエリ - Amazon Athena

テーブル作成のスニペット

 

ALBアクセスログの場合

 Application Load Balancer ログのクエリ - Amazon Athena

CREATE EXTERNAL TABLE `alb-access-log`(
  `type` string COMMENT '',
  `time` string COMMENT '',
  `elb` string COMMENT '',
  `client_ip` string COMMENT '',
  `client_port` int COMMENT '',
  `target_ip` string COMMENT '',
  `target_port` int COMMENT '',
  `request_processing_time` double COMMENT '',
  `target_processing_time` double COMMENT '',
  `response_processing_time` double COMMENT '',
  `elb_status_code` string COMMENT '',
  `target_status_code` string COMMENT '',
  `received_bytes` bigint COMMENT '',
  `sent_bytes` bigint COMMENT '',
  `request_verb` string COMMENT '',
  `request_url` string COMMENT '',
  `request_proto` string COMMENT '',
  `user_agent` string COMMENT '',
  `ssl_cipher` string COMMENT '',
  `ssl_protocol` string COMMENT '',
  `target_group_arn` string COMMENT '',
  `trace_id` string COMMENT '',
  `domain_name` string COMMENT '',
  `chosen_cert_arn` string COMMENT '',
  `matched_rule_priority` string COMMENT '',
  `request_creation_time` string COMMENT '',
  `actions_executed` string COMMENT '',
  `redirect_url` string COMMENT '',
  `lambda_error_reason` string COMMENT '',
  `target_port_list` string COMMENT '',
  `target_status_code_list` string COMMENT '',
  `classification` string COMMENT '',
  `classification_reason` string COMMENT '')
PARTITIONED BY (
  `log_date` string)
ROW FORMAT SERDE
  'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
  'input.regex'='([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^s]+?)\" \"([^s]+)\" \"([^ ]*)\" \"([^ ]*)\"')
STORED AS INPUTFORMAT
  'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://{バケット名}/AWSLogs/{アカウントID}/elasticloadbalancing/{リージョン}'
TBLPROPERTIES (
  'projection.enabled'='true',
  'projection.log_date.format'='yyyy/MM/dd',
  'projection.log_date.interval'='1',
  'projection.log_date.interval.unit'='DAYS',
  'projection.log_date.range'='NOW-1YEARS,NOW',
  'projection.log_date.type'='date',
  'storage.location.template'='s3://{バケット名}/AWSLogs/{アカウントID}/elasticloadbalancing/{リージョン}/${log_date}'
)

 

SELECT
  *
FROM "alb-access-log"
WHERE
  -- パーティションを選択
  log_date BETWEEN '2023/09/01' AND '2023/09/24'
  -- 条件を実装
  AND elb_status_code = '504'

 

VPCフローログの場合

Amazon VPC フローログのクエリ - Amazon Athena  

CREATE EXTERNAL TABLE `vpc-flow-log`(
  `version` int,
  `accountid` string,
  `interfaceid` string,
  `srcaddress` string,
  `dstaddress` string,
  `srcport` int,
  `dstport` int,
  `protocol` int,
  `packets` int,
  `bytes` bigint,
  `timefrom` int,
  `timeto` int,
  `action` string,
  `logstatus` string,
  `instanceid` string,
  `subnetid` string,
  `vpcid` string)
PARTITIONED BY (
  `log_date` string
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ' '
STORED AS INPUTFORMAT
  'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://{バケット名}/AWSLogs/{アカウントID}/vpcflowlogs/{リージョン}'
TBLPROPERTIES (
  'projection.enabled'='true',
  'projection.log_date.format'='yyyy/MM/dd',
  'projection.log_date.interval'='1',
  'projection.log_date.interval.unit'='DAYS',
  'projection.log_date.range'='NOW-1YEARS,NOW',
  'projection.log_date.type'='date',
  'skip.header.line.count'='1',
  'storage.location.template'='s3://{バケット名}/AWSLogs/{アカウントID}/vpcflowlogs/{リージョン}/${log_date}'
)

 

SELECT
  *
FROM vpc-flow-log
WHERE 
  -- パーティションを選択
  log_date >= '2023/05/09'
  -- 条件を実装
  AND dstport = 22

 

WAFログの場合

AWS WAF ログのクエリ - Amazon Athena  

CREATE EXTERNAL TABLE `waf_acl-log`(
  `timestamp` bigint COMMENT 'from deserializer',
  `formatversion` int COMMENT 'from deserializer',
  `webaclid` string COMMENT 'from deserializer',
  `terminatingruleid` string COMMENT 'from deserializer',
  `terminatingruletype` string COMMENT 'from deserializer',
  `action` string COMMENT 'from deserializer',
  `terminatingrulematchdetails` array<struct<conditiontype:string,location:string,matcheddata:array<string>>> COMMENT 'from deserializer',
  `httpsourcename` string COMMENT 'from deserializer',
  `httpsourceid` string COMMENT 'from deserializer',
  `rulegrouplist` array<struct<rulegroupid:string,terminatingrule:struct<ruleid:string,action:string>,nonterminatingmatchingrules:array<struct<action:string,ruleid:string>>,excludedrules:array<struct<exclusiontype:string,ruleid:string>>>> COMMENT 'from deserializer',
  `ratebasedrulelist` array<struct<ratebasedruleid:string,limitkey:string,maxrateallowed:int>> COMMENT 'from deserializer',
  `nonterminatingmatchingrules` array<struct<action:string,ruleid:string>> COMMENT 'from deserializer',
  `httprequest` struct<clientip:string,country:string,headers:array<struct<name:string,value:string>>,uri:string,args:string,httpversion:string,httpmethod:string,requestid:string> COMMENT 'from deserializer')
PARTITIONED BY (
  `date` string
)
ROW FORMAT SERDE
  'org.openx.data.jsonserde.JsonSerDe'
STORED AS INPUTFORMAT
  'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.IgnoreKeyTextOutputFormat'
LOCATION
  's3://{バケット名}/${date}'
TBLPROPERTIES (
  'classification'='csv',
  'compressionType'='gzip',
  'delimiter'='|',
  'projection.date.format'='yyyy/MM/dd',
  'projection.date.interval'='1',
  'projection.date.interval.unit'='DAYS',
  'projection.date.range'='NOW-1YEARS,NOW',
  'projection.date.type'='date',
  'projection.enabled'='true',
  'storage.location.template'='s3://{バケット名}/${date}',
  'typeOfData'='file'
)

 

SELECT from_unixtime(timestamp/1000, 'Asia/Tokyo') AS JST, *
FROM waf_acl-log
WHERE
  -- パーティションを選択
  date BETWEEN '2023/11/01' AND '2023/11/1'
  AND action = 'BLOCK'
--    AND action = 'ALLOW'
--    AND terminatingruleid = 'AWS-AWSManagedRulesSQLiRuleSet'
--    AND terminatingruleid = 'AWS-AWSManagedRulesPHPRuleSet'
ORDER BY timestamp ASC
LIMIT 100;