継続は力なり

タイトル通り定期的な更新を心掛けるブログです。

EventBridge Rules で指定した ECS タスクのイベントをトリガーに Step Functions から Athena にクエリを実行する

タダです.

前回 Athena で Aurora 監査ログをクエリする記事を書きました.,毎回 Athena のクエリをアドホックに実行するのでもいいですが,特定のイベントを契機に定期的に Athena を実行するワークフローを構築して定期チェックできるようにしました.この記事では概観をまとめていきます.

sadayoshi-tada.hatenablog.com

ワークフロー概要

ワークフローとしては,EventBridge Rule で特定のイベントでのみ Step Fucntions をキックするようにします.Step Functions は Athena を実行して結果を得ることをやりました.特定イベントは以前の記事で ECS でログを S3 にアップロードすることをやりましたが,この ECS タスクが停止して command パラメーターを見て制御します.

関連記事

sadayoshi-tada.hatenablog.com

docs.aws.amazon.com

ワークフローの構築

Step Functions のASL と IAM ポリシー

Step Functions から Athena を実行する ASL を定義します.クエリを実行し,そのクエリの結果を取得するワークフローになっています.

{
  "Comment": "A description of my state machine",
  "StartAt": "AthenaQueryExecution",
  "States": {
    "AthenaQueryExecution": {
      "Next": "wait",
      "Type": "Task",
      "ResultPath": "$.AthenaQueryExecution",
      "Resource": "arn:aws:states:::athena:startQueryExecution",
      "Parameters": {
        "QueryString.$": "実行するクエリ",
        "ResultConfiguration": {
          "OutputLocation": "Athena の結果出力場所"
        },
        "WorkGroup": "ワークグループ"
      }
    },
    "Wait": {
      "Type": "Wait",
      "Seconds": 5,
      "Next": "GetAthenaQueryExecution"
    },
    "Choice": {
      "Type": "Choice",
      "Choices": [
        {
          "Or": [
            {
              "Variable": "$.GetAthenaQueryExecutionOutPut.queryState",
              "StringEquals": "QUEUED"
            },
            {
              "Variable": "$.GetAthenaQueryExecutionOutPut.queryState",
              "StringEquals": "RUNNING"
            }
          ],
          "Next": "Wait"
        }
      ],
      "Default": "GetAthenaQueryResults"
    },
    "GetAthenaQueryExecution": {
      "Next": "Choice",
      "Type": "Task",
      "ResultPath": "$.GetAthenaQueryExecutionOutPut",
      "ResultSelector": {
        "queryState.$": "$.QueryExecution.Status.State"
      },
      "Resource": "arn:aws:states:::athena:getQueryExecution",
      "Parameters": {
        "QueryExecutionId.$": "$.AthenaQueryExecutionOutPut.QueryExecutionId"
      }
    },
    "GetAthenaQueryResults": {
      "Type": "Task",
      "Resource": "arn:aws:states:::athena:getQueryResults",
      "Parameters": {
        "QueryExecutionId.$": "$.AthenaQueryExecutionOutPut.QueryExecutionId"
      },
      "End": "True"
    }
  }
}

StepFucntions から Athena を実行するための IAM としては次のポリシーを定義しました.ステートマシンを作ったら自動生成されるものに多少追記しています.

{
  "Version": "2012-10-17",
  "Statement": [
      {
          "Effect": "Allow",
          "Action": [
              "athena:startQueryExecution",
              "athena:getDataCatalog"
          ],
          "Resource": [
              "arn:aws:athena:ap-northeast-1:1234567891911:workgroup/ワークグループ",
              "arn:aws:athena:ap-northeast-1:1234567891911:datacatalog/*"
          ]
      },
      {
          "Effect": "Allow",
          "Action": [
              "s3:GetBucketLocation",
              "s3:GetObject",
              "s3:ListBucket",
              "s3:ListBucketMultipartUploads",
              "s3:ListMultipartUploadParts",
              "s3:AbortMultipartUpload",
              "s3:CreateBucket",
              "s3:PutObject"
          ],
          "Resource": [
              "arn:aws:s3:::*"
          ]
      },
      {
          "Effect": "Allow",
          "Action": [
              "glue:CreateDatabase",
              "glue:GetDatabase",
              "glue:GetDatabases",
              "glue:UpdateDatabase",
              "glue:DeleteDatabase",
              "glue:CreateTable",
              "glue:UpdateTable",
              "glue:GetTable",
              "glue:GetTables",
              "glue:DeleteTable",
              "glue:BatchDeleteTable",
              "glue:BatchCreatePartition",
              "glue:CreatePartition",
              "glue:UpdatePartition",
              "glue:GetPartition",
              "glue:GetPartitions",
              "glue:BatchGetPartition",
              "glue:DeletePartition",
              "glue:BatchDeletePartition"
          ],
          "Resource": [
              "arn:aws:glue:ap-northeast-1:1234567891911:catalog",
              "arn:aws:glue:ap-northeast-1:1234567891911:database/*",
              "arn:aws:glue:ap-northeast-1:1234567891911:table/*",
              "arn:aws:glue:ap-northeast-1:1234567891911:userDefinedFunction/*"
          ]
      },
      {
          "Effect": "Allow",
          "Action": [
              "lakeformation:GetDataAccess",
              "athena:GetQueryExecution",
              "athena:GetQueryResults"
          ],
          "Resource": [
              "*"
          ]
      }
  ]
}

ECS タスクを絞り込む EventBridge Rule

次に,ECS タスクが停止した時に特定の command が指定されていたらイベント発火する EventBridge Rule を設定します.以下のような Rule を定義の上に Step Functions をターゲットに指定します.

{
  "source": ["aws.ecs"],
  "detail-type": ["ECS Task State Change"],
  "detail": {
    "clusterArn": ["ECSクラスター ARN"],
    "containers": {
      "exitCode": [0]
    },
    "lastStatus": ["STOPPED"],
    "overrides": {
      "containerOverrides": {
        "command": ["Aurora クラスター名"]
      }
    }
  }
}

ワークフローの実行結果概観

指定 command が実行された ECS タスクが停止後,EventBridge Rule が発火し Step Functions に以下のようなインプットデータが入ってきます.

{
  "version": "0",
  "id": "xxx",
  "detail-type": "ECS Task State Change",
  "source": "aws.ecs",
  "account": "1234567891911",
  "time": "2023-04-29T06:05:09Z",
  "region": "ap-northeast-1",
  "resources": [
    "arn:aws:ecs:ap-northeast-1:1234567891911:task/クラスター名/タスクID"
  ],
  "detail": {
    "attachments": [
      {
        "id": "xxx",
        "type": "eni",
        "status": "DELETED",
        "details": [
          {
            "name": "subnetId",
            "value": "subnet-xxx"
          },
          {
            "name": "networkInterfaceId",
            "value": "eni-xxx"
          },
          {
            "name": "macAddress",
            "value": "xxx"
          },
          {
            "name": "privateDnsName",
            "value": "ip-12.34.56.789.ap-northeast-1.compute.internal"
          },
          {
            "name": "privateIPv4Address",
            "value": "12.34.56.789"
          }
        ]
      }
    ],
    "attributes": [
      {
        "name": "ecs.cpu-architecture",
        "value": "x86_64"
      }
    ],
    "availabilityZone": "ap-northeast-1a",
    "clusterArn": "arn:aws:ecs:ap-northeast-1:1234567891911:cluster/クラスター名",
    "connectivity": "CONNECTED",
    "connectivityAt": "2023-04-29T06:00:15.714Z",
    "containers": [
      {
        "containerArn": "arn:aws:ecs:ap-northeast-1:1234567891911:container/クラスター名/xxx/xxx",
        "exitCode": 0,
        "lastStatus": "STOPPED",
        "name": "コンテナ名",
        "image": "1234567891911.dkr.ecr.ap-northeast-1.amazonaws.com/コンテナ名:xxx",
        "imageDigest": "sha256:xxx",
        "runtimeId": "xxx-xxx",
        "taskArn": "arn:aws:ecs:ap-northeast-1:1234567891911:task/クラスター名/xxx",
        "networkInterfaces": [
          {
            "attachmentId": "xxx",
            "privateIpv4Address": "12.34.56.789"
          }
        ],
        "cpu": "1024",
        "memory": "2048"
      }
    ],
    "cpu": "1024",
    "createdAt": "2023-04-29T06:00:12.512Z",
    "desiredStatus": "STOPPED",
    "enableExecuteCommand": false,
    "ephemeralStorage": {
      "sizeInGiB": 20
    },
    "executionStoppedAt": "2023-04-29T06:04:46.52Z",
    "group": "family:タスク定義名",
    "launchType": "FARGATE",
    "lastStatus": "STOPPED",
    "memory": "2048",
    "overrides": {
      "containerOverrides": [
        {
          "command": [
           "指定したコマンドデータ"
          ],
          "name": "コンテナ名"
        }
      ]
    },
    "platformVersion": "1.4.0",
    "pullStartedAt": "2023-04-29T06:00:22.932Z",
    "pullStoppedAt": "2023-04-29T06:00:26.962Z",
    "startedAt": "2023-04-29T06:00:27.588Z",
    "startedBy": "chronos-schedule/EventBridge Scheduler 名",
    "stoppingAt": "2023-04-29T06:04:56.548Z",
    "stoppedAt": "2023-04-29T06:05:09.471Z",
    "stoppedReason": "Essential container in task exited",
    "stopCode": "EssentialContainerExited",
    "taskArn": "arn:aws:ecs:ap-northeast-1:1234567891911:task/クラスター名/xxx",
    "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:1234567891911:task-definition/タスク定義名:xx",
    "updatedAt": "2023-04-29T06:05:09.471Z",
    "version": 5
  }
}

前回記事で実行した管理者ユーザーのログインカウントを取った場合ワークフローが正常に動作すると,次の結果が出力されました.この時は管理者ユーザーのログインがなかった時間帯だったので VarCharValue が0となっています.

{
  "ResultSet": {
    "ResultSetMetadata": {
      "ColumnInfo": [
        {
          "CaseSensitive": false,
          "CatalogName": "hive",
          "Label": "_col0",
          "Name": "_col0",
          "Nullable": "UNKNOWN",
          "Precision": 19,
          "Scale": 0,
          "SchemaName": "",
          "TableName": "",
          "Type": "bigint"
        }
      ]
    },
    "Rows": [
      {
        "Data": [
          {
            "VarCharValue": "_col0"
          }
        ]
      },
      {
        "Data": [
          {
            "VarCharValue": "0"
          }
        ]
      }
    ]
  },
  "UpdateCount": 0
}

まとめ

ECS タスクで特定のイベントを絞った形で Step Functions から Athena を実行してみました.これで ECS から Aurora ログを収集したタイミングで都度 Athena からクエリを走行することができました.クエリの結果を Slack に通知して検知の仕組みを次に作っていきます.

S3 に入れた Aurora の監査ログを Athena でクエリを行う

タダです.

過去記事で Aurora のログを S3 に格納することを書きました.S3 にログを格納したら検索して分析したくなりますが,この記事では監査ログの中から管理者ユーザーの利用履歴を検索してみます.

sadayoshi-tada.hatenablog.com

前提

この記事では,Athena で検索する Aurora のログは S3 で Aurora のクラスター名/各インスタンス名/YYYY-MM-DD/audit/ というパスで格納したとします.また,ログは Aurora MySQL 5.7 の環境のものになります.

テーブル作成

今回用のテーブルを以下のように作ります.監査ログはドキュメントにも記載がありますが,UTF-8 形式のカンマ区切り変数 (CSV) ファイルになっており,フィールドもドキュメントに記載のものを使っています.また,S3 のパスで Aurora DB インスタンスと日付を使っているため,それぞれパーティション化しています.

CREATE EXTERNAL TABLE `テーブル名` (
    `timestamp` string,
    `serverhost` string,
    `username` string,
    `host` string,
    `connectionid` string,
    `queryid` string,
    `operation` string,
    `database` string,
    `object` string,
    `retcode` string
)
PARTITIONED BY (`date_day` string, `db_identifier` string)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.OpenCSVSerde'
WITH SERDEPROPERTIES (
    'escapeChar' = '\\',
    'quoteChar' = '`',
    'separatorChar' = ','
)
LOCATION 's3://[Aurora クラスター名]/'
TBLPROPERTIES (
    'projection.enabled' = 'true',
    'projection.db_identifier.values'='[WRITERインスタンス名],[READERインスタンス名]', 
    'projection.db_identifier.type'='enum',
    'projection.date_day.format' = 'yyyy-MM-dd',
    'projection.date_day.range' = '2020-01-01,NOW',
    'projection.date_day.type' = 'date',
    'storage.location.template' = 's3://[Aurora クラスター名]/${db_identifier}/${date_day}/audit/'
);

関連情報

docs.aws.amazon.com

管理者ユーザーの利用を抽出

準備が整ったので,Aurora の管理者ユーザーの利用状況を以下のクエリで検索してみましょう.画像のように抽出することができました.

SELECT *
FROM [テーブル名]
where db_identifier between 'WRITER インスタンス名' and 'READER インスタンス名'
    and username = '管理者ユーザー名'
    and date_day = 'スキャンの日付'

実行結果抜粋

まとめ

Athena を使って S3 に格納した Aurora の監査ログから特定の条件を検索するクエリと抽出結果をまとめてみました.一旦,これでクエリは叩けますがいちいち Athane のクエリを叩くのは煩雑なため,仕組み化していく過程を次の記事で紹介します.

Terraform で Aurora のインスタンスタイプを変更しようとしたら反映されなかった時の対応

タダです.

Terraform で Aurora のインスタンスタイプを変更しようとした時に terraform apply は成功しているが,Aurora のインスタンスタイプが変更されてなかった場面に遭遇したので対処をまとめておきます.

事象概要

Aurora MySQL 5.7 の環境で db.t4g.medium で CPU クレジットを使い切り CPU 負荷が高騰する事象が発生してたため,取り急ぎインスタンスタイプを上げるようにしました.その際に db.r6g.largeterraform apply で変更したところ処理は成功したが,インスタンスタイプが変わらなかったという状態になりました.Aurora の画面で確認したところ保留中の変更で止まっており,今すぐの反映ができていないことがわかりました.

terraform apply 実行結果抜粋

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_rds_cluster_instance.aurora_cluster_instance[0] will be updated in-place
  ~ resource "aws_rds_cluster_instance" "aurora_cluster_instance" {
        id                                    = "hoge"
      ~ instance_class                        = "db.t4g.medium" -> "db.r6g.large"
        tags                                  = {}
        # (26 unchanged attributes hidden)
    }
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_rds_cluster_instance.aurora_cluster_instance[0]: Modifying... [id=hoge
aws_rds_cluster_instance.aurora_cluster_instance[0]: Still modifying... [id=hoge, 10s elapsed]
aws_rds_cluster_instance.aurora_cluster_instance[0]: Still modifying... [id=hoge, 20s elapsed]
aws_rds_cluster_instance.aurora_cluster_instance[0]: Still modifying... [id=hoge, 30s elapsed]
aws_rds_cluster_instance.aurora_cluster_instance[0]: Modifications complete after 31s [id=hoge]
Releasing state lock. This may take a few moments...

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

保留中の変更画面

本事象の対応まとめ

今すぐ反映したかったので,GUI からもできますが,AWS CLIインスタンスタイプの変更を行いました.AWS CLI ですと,下記のコマンドになります.実行後,インスタンスタイプの変更は行われ保留中の変更の表示も消えました.

aws rds modify-db-instance \
    --db-instance-identifier [変更したい Aurora インスタンス名] \
    --db-instance-class db.r6g.large \
    --apply-immediately

関連情報

docs.aws.amazon.com

まとめ

簡単にはなりますが,Aurora のインスタンスタイプを変更しようとした時にインスタンスタイプが変更されなかった事象のまとめと対応をまとめました.あとになって調べたら aws_rds_cluster_instanceapply_immediately オプションが有るのを知りこれを使えば即時反映いけました.すぐに適用したい場合は明示的に指定すると良さそうです.

registry.terraform.io

Terraform を使って Azure のリソースグループを作る

タダです.

Azure を触る機会が増えそうかつ Terraform を使った管理をする必要があるため,備忘録としてこの記事では Azure のリソースグループを Terraform で作ってみた内容をまとめていきます.

事前準備

事前準備として次のものを用意します.2つ目の Azure CLI は Terraform 実行で必要で,今回の作業でインストールせず terraform plan を実行すると怒られます.

  • Azure アカウント(無料試用版を使っています)
  • Azure CLI az
  • Terraform のインストール

怒られたレスポンス

❯❯ terraform plan
╷
│ Error: building AzureRM Client: please ensure you have installed Azure CLI version 2.0.79 or newer. Error parsing json result from the Azure CLI: launching Azure CLI: exec: "az": executable file not found in $PATH.
│ 
│   with provider["registry.terraform.io/hashicorp/azurerm"],
│   on providers.tf line 16, in provider "azurerm":
│   16: provider "azurerm" {
│ 
╵

関連情報

learn.microsoft.com

リソースグループ作成のコード作成と適用

リソースグループ作成の Terraform のコードを書いていきます.ちなみに,リソースグループとは Azure の中で 管理グループ > サブスクリプション の下にある概念で Azure のサービスリソースを管理するために必要です.リソースグループで管理されたサービスリソースは一覧表示化できたり,まとめて削除できたりします.コードは Azure のドキュメントをベースに一部変更しており,東日本リージョンにリソースグループを作るようにしています.

providers.tf

terraform {
  required_version = ">=0.12"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>2.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~>3.0"
    }
  }
}
provider "azurerm" {
  features {}
}

resource_group.tf

resource "random_pet" "rg_name" {
  prefix = var.resource_group_name_prefix
}
resource "azurerm_resource_group" "rg" {
  location = var.resource_group_location
  name     = random_pet.rg_name.id
}

variables.tf

variable "resource_group_location" {
  default     = "japaneast"
  description = "Location of the resource group."
}
variable "resource_group_name_prefix" {
  default     = "rg"
  description = "Prefix of the resource group name that's combined with a random ID so name is unique in your Azure subscription."
}

リソースグループの作成

❯❯❯ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = (known after apply)
    }

  # random_pet.rg_name will be created
  + resource "random_pet" "rg_name" {
      + id        = (known after apply)
      + length    = 2
      + prefix    = "rg"
      + separator = "-"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

random_pet.rg_name: Creating...
random_pet.rg_name: Creation complete after 0s [id=rg-darling-mastodon]
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 1s [id=/subscriptions/7a60c864-0425-42d2-96d1-4b70f1741ecf/resourceGroups/rg-darling-mastodon]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

作成後の確認

関連情報

learn.microsoft.com

まとめ

リソースグループの作成を Terraform でやってみました.次は IAM リソースの管理をやってみたいと思います.

RDS ログを S3 にアップロードの定期実行で運用中に発生した課題への対策をまとめる

タダです.

RDS ログを S3 にアップロードする Python スクリプトを ECS で実行に向けての記事や運用時に遭遇した気づきを書いたのですが,この記事でも運用し続けてみて発生した課題に対する対策をまとめていきます.

運用してみての課題

RDS ログを S3 アップロードを定期的に実行し始めて RDS の CPU 負荷が高騰していることに気付きました.WRITER だと60%ほど,READER だと30%ほどログをダウンロードしてる時間帯で高くなっていたこと,ログダウンロードも全てのファイルを落としており実行時間が長くなっていたため,この課題を解決するように対策を打ちました.

課題に対する対策

対策として,S3 にログがないもしくはログがあってもログファイルのサイズが増えている場合にダウンロードするように変更することにしました.加えて,当初は S3 へログをアップロードする時にダウンロードしたログファイルをそのままアップロードしたのですが,処理スピードを上げるために gzip にしてアップロードするように変更しました

差分の判定

差分のみダウンロード・アップロードするために入れた判定が gzip でアップロードする際にオブジェクトのメタデータとして,gzip 前のログファイルサイズを入れるようにしました. describe_db_log_files API のレスポンスで Size があり,ログファイルサイズを取れるためこの値をメタデータに入れてアップロードしました.

オブジェクトアップロード例

S3.Object.put(
    Body=[ログファイルデータ],
    Metadata={
        'log-raw-size': str(ログファイルサイズの変数),
    }
)

boto3.amazonaws.com

そして,アップロードしたメタデータから log-raw-sizedescribe_db_log_files API からとってきたファイルサイズを比較して同じならダウンロード処理はスキップし,同じじゃなければダウンロードするようにしました.

差分判定部分の抜粋

def compare_size(self, rdslog: 'RDSLog') -> bool:
    return rdslog.size == self.log_raw_size()

差分判定を入れた結果

差分ダウンロード・アップロードを入れた結果,初回実行時はオブジェクトにメタデータがなくフルアップロードになってしまいますが,2回目以降は CPU 負荷は WRITER が最大3%ほど上昇した位で留まり実行時間も元々の3分の1に収まりました.

まとめ

RDS ログを定期的に S3 にアップロードする処理を動かし始めて気づいた運用課題に対する対策をまとめました.

関連記事

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com