継続は力なり

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

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

DownloadCompleteLogFile を定期実行した時に遭遇した事象についてまとめる

タダです.

前回の記事で RDS ログを S3 にアップロードする処理を ECS で定期実行するように運用し始めました.運用し始めて遭遇した気づきがあり,そのことについて本記事で書いていきます.

sadayoshi-tada.hatenablog.com

運用し始めて発生した事象

定期実行を始めて下記のエラーが頻繁に出るようになりました.DownloadCompleteLogFile のリクエストを発行してそのレスポンスでステータスコードをチェックする機構を設けていたのですが,そのログに記録された 400 のエラーです.また,このエラーは発生するファイルが毎回バラバラだったり,エラーになった後の定期実行ではリクエストが成功していたりしていました.

Error occured: file is error/mysql-error-running.log.2023-xx-xx.xx of hoge-instance and status-error is 400

原因調査

Pythonスクリプトで出力したログではわからなかったため,CloudTrail ログを追ってみました.確認したところ,エラーが発生した時間帯で APIRate exceeded が発生していることがわかりました.API の呼び出しを実行しすぎたために400エラーがでていたのです.

"eventSource": "rds.amazonaws.com",
"eventName": "DownloadCompleteDBLogFile",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "12.34.56.78",
"userAgent": "python-requests/2.28.2",
"errorCode": "ThrottlingException",
"errorMessage": "Rate exceeded",

原因への対策

API を実行しすぎていたので,間隔を空けて再実行するようにリトライを入れることにしました.変更した箇所だけ抜粋していますが,400エラー時に3秒間隔で3回リトライする処理を入れました.このリトライ処理を入れてから400エラーがなくなり,事象が解消しました.

from requests.packages.urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
from requests import Session

retry_config = Retry(total=3, status_forcelist=[400], backoff_factor=3)
http_adapter = HTTPAdapter(max_retries=retry_config)
http_session = Session()
http_session.mount("https://", http_adapter)
awsreq = AWSRequest(method='GET', url=download_complete_logfile_url)
sigv4auth = auth.SigV4Auth(credentials, 'rds', region)
sigv4auth.add_auth(awsreq)
http_session.get(self._download_url(), stream=True, headers={
    'Authorization': awsreq.headers['Authorization'],
    'X-Amz-Date': awsreq.context['timestamp'],
    'X-Amz-Security-Token': credential.token
}

関連情報

urllib3.readthedocs.io

まとめ

DownloadCompleteLogFile の定期実行を運用するようになって遭遇したエラーとその対処についてまとめました.エラーの発生だけじゃわからなかったですが,CloudTrail に原因が記録されていて助かりました.

関連記事

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com

RDS ログを S3 にアップロードする Python スクリプトを ECS で実行する

タダです.

前回記事で RDS ログを S3 にアップロードする Python スクリプトを Docker で動作するようにしました.今回は GitHub Actions でコンテナイメージを ECR に格納し,EventBridge Scheduler で ECS Runtask を実行するようにしてきます.

sadayoshi-tada.hatenablog.com

コンテナイメージを ECR に格納する

コンテナイメージを ECR に格納する処理は GitHub Actions で行うようにしました.

name: ecr push

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  deploy:
    name: ecr push
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    timeout-minutes: 15
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ap-northeast-1
          role-to-assume: "arn:aws:iam::1234567891012:role/ecr-image-push"
          role-duration-seconds: 1800
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - name: Build docker images and push its to ECR
        id: build-image
        env:
          registory: ${{ steps.login-ecr.outputs.registry }}
          repository: hoge-repository
          image_tag: latest
        run: |
          docker build . --tag ${{ env.registory }}/${{ env.repository }}:${{ env.image_tag }}
          docker push ${{ env.registory }}/${{ env.repository }}:${{ env.image_tag }}

ECS クラスターと ECS タスク定義を作成する

次に ECS クラスターと ECS タスク定義を作ります.ECS クラスターはコスト観点で ECS Fargate Spot を実行できるようにし,タスク定義は次のように指定しました.

タスク定義の containerDefinitions 抜粋

"containerDefinitions": [
        {
            "name": "rds-log-to-s3-upload",
            "image": "1234567891012.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-repository:latest",
            "cpu": 1024,
            "memory": 2048,
            "portMappings": [],
            "essential": true,
            "entryPoint": [
                "python3",
                "rds_donwload_to_s3_upload.py"
            ],
            "environment": [],
            "mountPoints": [],
            "volumesFrom": [],
            "readonlyRootFilesystem": true,
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-create-group": "true",
                    "awslogs-group": "/ecs/rds-log-to-s3-upload",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "rds-log-to-s3-upload"
                }
            }
        }
    ],

これで ECS Runtask の実行準備が完了です.

EventBridge Scheduler で ECS Runtask を実行する

そして,EventBridge Scheduler で ECS Runtask を実行するようにしていきます.この記事では Terraform で設定を行いました.EventBridge Scheduler で ECS Runtask を実行する際に気をつけたのは input で渡している containerOverrides 部分です.ここではコマンドの上書きを行っています.自分が作成したスクリプトでは引数で Aurora クラスター名とログを格納する S3 バケットを指定する必要があります.その引数をコマンドの上書きとして containerOverrides で行っております.これで input に指定したパラメーターに応じて Aurora クラスターとバケットを分けてログを出したりが可能です.

resource "aws_iam_role" "evnetbridge_scheduler" {
    name = "evnetbridge-scheduler-role"
    assume_role_policy = data.aws_iam_policy_document.eventbridge_scheduler_assume_policy.json
    inline_policy {
      name = "evnetbridge-scheduler-role-inline-policy"
      policy = jsonencode({
        Version = "2012-10-17"
      Statement = [
        {
          Action = [
            "ecs:StopTask",
            "ecs:RunTask",
            "ecs:DescribeTasks",
          ]
          Effect   = "Allow"
          Resource = "arn:aws:ecs:ap-northeast-1:1234567891012:task-definition/hoge-task-definition:*"
        },
        {
          Action   = "iam:PassRole"
          Effect   = "Allow"
          Resource = ["*"]
          Condition = {
            StringLike = {
              "iam:PassedToService" = "ecs-tasks.amazonaws.com"
            }
          }
        },
      ]
    }
}
data "aws_iam_policy_document" "eventbridge_scheduler_assume_policy" {
  statement {
    effect = "Allow"
    actions = [
      "sts:AssumeRole",
    ]
    principals {
      type = "Service"
      identifiers = [
        "scheduler.amazonaws.com",
      ]
    }
  }
}
resource "aws_scheduler_schedule" "test_scheduler" {
    name = "test-scheduler"
    state = "ENABLED"
    schedule_expression = "cron(0 0 * * ? *)"
    schedule_expression_timezone = "Asia/Tokyo"
    flexible_time_window {
      mode = "OFF"
    }
   target {
    arn      = aws_ecs_cluster.hoge.arn
    role_arn = aws_iam_role.hoge.arn
    ecs_parameters {
      task_count          = 1
      launch_type         = "FARGATE"
      task_definition_arn = aws_ecs_task_definition.hoge_task_definition.arn
      platform_version    = "1.4.0"
      network_configuration {
        assign_public_ip = false
        security_groups  = [aws_security_group.hoge.id]
        subnets          = [var.hoge_subnet1_id,var.hoge_subnet2_id]
      }
    }
    input = jsonencode({
      "containerOverrides" : [
        {
          "name" : "rds-log-to-s3-upload", 
          "command" : ["[Aurora クラスター名]", " [S3 バケット名]"]
        }
      ]
    })
  }
}

以上で ECS 上での RDS ログを S3 にアップロードの定期実行の仕組み化ができました.

関連記事

sadayoshi-tada.hatenablog.com

まとめ

RDS ログを S3 にアップロードする仕組みを EventBridge Scheduler と ECS を使って実現するまでの工程を記載しました.これで定期実行ができるようになったのですが,運用し始めての気づきもあったので次の記事ではその辺の話を書きます.

関連記事

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com

RDS ログを S3 にアップロードする Python スクリプトを Docker 上で実行する

タダです.

前回の記事で RDS ログをダウンロードする Python スクリプトを作った記事を書きました.今回の記事はで Python スクリプトに S3 アップロードする処理を追加したものを ECS 上で動かすためにローカルで Docker イメージにし,ローカル実行をしてみるまでの過程を備忘録にまとめます.

sadayoshi-tada.hatenablog.com

Python スクリプトに S3 アップロードの処理を追加する

前回の記事で書いたコードに S3 アップロード処理を追加してみたコードが次のものになります.

rds_donwload_to_s3_upload.py

import boto3
from botocore.awsrequest import AWSRequest
import botocore.auth as auth
import requests
import tempfile

region = 'ap-northeast-1'
session = boto3.session.Session()
rds_endpoint = 'rds.' + region + '.amazonaws.com'
rds = boto3.client('rds')
parser = argparse.ArgumentParser()
parser.add_argument('aurora_cluster_name', type=str)
parser.add_argument('target_s3_bucket', type=str)
args = parser.parse_args()
aurora_cluster_name = args.aurora_cluster_name
target_s3_bucket = args.target_s3_bucket
s3 = boto3.resource('s3')
bucket = s3.Bucket(target_s3_bucket)

def get_db_instance_identifiers(aurora_cluster_name: str) -> list[str]:
    target_db_instance_identifiers = []
    response = rds.describe_db_clusters(DBClusterIdentifier=aurora_cluster_name)
    members = response['DBClusters'][0]['DBClusterMembers']
    for member in members:
        target_db_instance_identifiers.append(member['DBInstanceIdentifier'])
    return target_db_instance_identifiers

def get_log_file_names_from_aurora(db_instance_identifier: str) -> list[str]:
    download_log_files = []
    describe_db_log_files_pagenator = rds.get_paginator('describe_db_log_files')
    describe_db_log_files_page_iterator = describe_db_log_files_pagenator.paginate(DBInstanceIdentifier = db_instance_identifier)
    for describe_db_log_files_page in describe_db_log_files_page_iterator:
        for describe_db_log_file in describe_db_log_files_page['DescribeDBLogFiles']:
            download_log_files.append(describe_db_log_file['LogFileName'])

def download_rds_logfile(db_instance_identifier:str, log_file_name:str, region:str, rds_endpoint:str) -> str:
    download_complete_logfile_url = 'https://' + rds_endpoint + '/v13/downloadCompleteLogFile/' + db_instance_identifier + '/' + log_file_name
    credential = session.get_credentials()
    awsreq = AWSRequest(method = 'GET', url = download_complete_logfile_url)
    sigv4auth = auth.SigV4Auth(credentials, 'rds', region)
    sigv4auth.add_auth(awsreq)
    res = requests.get(download_complete_logfile_url, stream=True, headers={
            'Authorization': awsreq.headers['Authorization'],
            'X-Amz-Date': awsreq.context['timestamp'],
            'X-Amz-Security-Token': credential.token
    })
    log_file_content = res.text
    res.close
    return log_file_content

def main():
    db_instance_identifiers = get_db_instance_identifiers(aurora_cluster_name)
    for db_instance_identifier in db_instance_identifiers:
        log_file_names = get_log_file_names_from_aurora(db_instance_identifier)
        for log_file_name in log_file_names:
            with download_rds_logfile(db_instance_identifier, log_file_name, region, rds_endpoint) as log_file_content:
                with tempfile.NamedTemporaryFile(dir=".",mode='w') as fp:
                    fp.write(log_file_content)
                    fp.seek(0)
                    bucket.upload_file(tmp.name, f"{db_instance_identifier}/{log_file_name}")
 
if __name__ == '__main__':
    main()

Python スクリプトを動かすための Dockerfile 準備

Python スクリプトができたので,Dockefile をマルチステージビルドで作ります.なお,ライブラリ管理には poetry を使う前提で作っています.

# syntax=docker/dockerfile:1
FROM python:3.11-slim-buster AS builder
ENV POETRY_HOME=/opt/poetry
ENV PATH="$POETRY_HOME/bin:$PATH"
RUN apt-get update && \
    apt-get install --no-install-recommends -y curl && \
    apt-get clean
RUN curl -sSL https://install.python-poetry.org/ | python3 -
WORKDIR /tmp
COPY ./pyproject.toml ./poetry.lock ./
RUN poetry config virtualenvs.create false && \
    poetry install

FROM python:3.11-slim-buster
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY ./rds_donwload_to_s3_upload.py .

ENTRYPOINT [ "python3", "rds_donwload_to_s3_upload.py" ]

上記の Dockerfile をビルドすると自分の手元で 210MB くらいのサイズになりました.

$ docker build -t rds-log-to-s3-upload .
$ docker images
REPOSITORY                TAG               IMAGE ID       CREATED         SIZE
rds-log-to-s3-upload  latest            ced5cc959045   16 hours ago    210MB

Docker を起動してスクリプトを実行してみる

ビルドしたイメージでスクリプトを実行してみたところ S3 バケットにログが入っていることを確認できました.

$ docker run --rm -t -e AWS_DEFAULT_REGION="ap-northeast-1" -e AWS_ACCESS_KEY_ID="hoge" -e AWS_SECRET_ACCESS_KEY="hogehoge" rds-log-to-s3-upload:latest [Aurora クラスター名] [S3 バケット名]

まとめ

前回の記事で Python スクリプトを Docker で実行するためにやったことをまとめました.次回は ECS でタスク実行するために必要なことをまとめていきます.

Python で RDS に格納されている各種ログをダウンロードする

タダです.

AWS 利用料のうち CloudWatch Logs への取り込みが高くかかっていました( DataProcessing-Bytes という項目).そこで,下記のページを参照して調べてみたところ一番容量があったのが Aurora のログでした.そのログを CloudWatch Logs に出力せずダウンロードし,S3 などにアップロードしていけばコスト削減に繋がりそうです.この記事では,ダウンロードを Python でやってみた内容をまとめていきます.

aws.amazon.com

Python で RDS ログダウンロードのための手段

boto3 のドキュメントを見てみたらログダウンロードをするのに, download_db_log_file_portion があります.ただし,説明に記載のようにこの API では 1MB までのダウンロードになると記載があるため,別のアプローチを調べてみました.

Downloads all or a portion of the specified log file, up to 1 MB in size

調べてみたところ, DownloadCompleteLogFile を使って REST API でのダウンロードであれば,容量制限もドキュメントに記載がなくダウンロードできそうなため,この手法を使ってみることにしました.

docs.aws.amazon.com

DownloadCompleteLogFile でのログダウンロードを試してみる

Python でログをダウンロードするコードを書いてみました.SigV4 署名での GET リクエストをダウンロードしたい DB インスタンスのログごとに実行します.そのため,download_rds_logfile 関数を実行する時に対象の DB インスタンスとファイルを get_db_instance_identifiersget_log_file_names_from_aurora で抽出しています.また,デフォルトだとログファイル名を1000件しか取得できないため,Pagenation で全件取得するようにしています.ログダウンロードのリクエストを投げた後に tempfile に書き出しており,このファイルをそのまま S3 にアップロードしたりもできます.

import boto3
from botocore.awsrequest import AWSRequest
import botocore.auth as auth
import requests
import tempfile

region = 'ap-northeast-1'
session = boto3.session.Session()
rds_endpoint = 'rds.' + region + '.amazonaws.com'
rds = boto3.client('rds')
parser = argparse.ArgumentParser()
parser.add_argument('aurora_cluster_name', type=str)
args = parser.parse_args()
aurora_cluster_name = args.aurora_cluster_name

def get_db_instance_identifiers(aurora_cluster_name: str) -> list[str]:
    target_db_instance_identifiers = []
    response = rds.describe_db_clusters(DBClusterIdentifier=aurora_cluster_name)
    members = response['DBClusters'][0]['DBClusterMembers']
    for member in members:
        target_db_instance_identifiers.append(member['DBInstanceIdentifier'])
    return target_db_instance_identifiers

def get_log_file_names_from_aurora(db_instance_identifier: str) -> list[str]:
    download_log_files = []
    describe_db_log_files_pagenator = rds.get_paginator('describe_db_log_files')
    describe_db_log_files_page_iterator = describe_db_log_files_pagenator.paginate(DBInstanceIdentifier = db_instance_identifier)
    for describe_db_log_files_page in describe_db_log_files_page_iterator:
        for describe_db_log_file in describe_db_log_files_page['DescribeDBLogFiles']:
            download_log_files.append(describe_db_log_file['LogFileName'])

def download_rds_logfile(db_instance_identifier:str, log_file_name:str, region:str, rds_endpoint:str) -> str:
    download_complete_logfile_url = 'https://' + rds_endpoint + '/v13/downloadCompleteLogFile/' + db_instance_identifier + '/' + log_file_name
    credential = session.get_credentials()
    awsreq = AWSRequest(method = 'GET', url = download_complete_logfile_url)
    sigv4auth = auth.SigV4Auth(credentials, 'rds', region)
    sigv4auth.add_auth(awsreq)
    res = requests.get(download_complete_logfile_url, stream=True, headers={
            'Authorization': awsreq.headers['Authorization'],
            'X-Amz-Date': awsreq.context['timestamp'],
            'X-Amz-Security-Token': credential.token
    })
    log_file_content = res.text
    res.close
    return log_file_content

def main():
    db_instance_identifiers = get_db_instance_identifiers(aurora_cluster_name)
    for db_instance_identifier in db_instance_identifiers:
        log_file_names = get_log_file_names_from_aurora(db_instance_identifier)
        for log_file_name in log_file_names:
            with download_rds_logfile(db_instance_identifier, log_file_name, region, rds_endpoint) as log_file_content:
                with tempfile.NamedTemporaryFile(dir=".",mode='w') as fp:
                    fp.write(log_file_content)
                    fp.seek(0)
 
if __name__ == '__main__':
    main()

まとめ

Python で Aurora の各種ログをダウンロードする処理を書く経験したのでまとめました.