虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

シェルからのSQL実行をAmazon ECSを使ってサーバーレスに実現してみる

こんにちは。虎の穴ラボの鷺山です。

この記事は「虎の穴ラボ 夏のアドベントカレンダー」の3日目の記事です。
2日目は植竹さんによる「GCPの監視機能 Monitoring の推しポイント紹介」が投稿されました。
4日目はH.Y.さんによる「Amazon WorkSpacesで色々試してみる。」が投稿されます。こちらもぜひご覧ください。

はじめに

データベースを運用していると、「挿入・変更・削除などのちょっとしたデータ操作を、シェルスクリプトの中にSQLを書いて実行」したりすることはあると思います。

今回はそのような処理をAmazon ECSのタスク実行を使ってサーバーレスに実現する方法をご紹介したいと思います。
コンテナの起動にAWS Lambdaも使用します。

環境

  • データベース: MySQL 5.7 (Aurora 2.10.2)
    • この記事の付録にPostgreSQLでの方法もご紹介しています。
  • シェルスクリプト: Bash 5.0.17
  • AWS CLI (2.7.9)
  • AWSのアカウントID: 111111111111
  • AWSリージョン: ap-northeast-1

ECRリポジトリの作成

まず、ECSのタスク実行に使用するDockerイメージを登録するためのECRリポジトリを作成します。
ここではリポジトリ名をdatabase-executorとしています。

Dockerイメージの準備・登録

次にSQL実行用のシェルスクリプトentrypoint.shと、それを内包するイメージを作るためのDockerfileを作成します。

▼ entrypoint.sh

#!/bin/bash
set -Ceuo pipefail

# MySQLのパスワードを環境変数に設定
export MYSQL_PWD=$DB_PASSWORD

mysql \
  --host="$DB_HOSTNAME" \
  --port="$DB_PORT" \
  --database="$DB_DATABASE" \
  --user="$DB_USERNAME" \
  --execute="$@"

このスクリプトでは、データベースの接続情報を環境変数で指定できるようにしています。

DB_HOSTNAMEデータベースのホスト名
DB_PORTデータベースのポート番号
DB_DATABASEデータベース名
DB_USERNAMEユーザー名
DB_PASSWORDパスワード

また、実行するSQLをシェルのコマンドの引数 $@ で指定できるようにしています。

▼ Dockerfile

FROM ubuntu:20.04

RUN apt-get update && \
    apt-get install -y \
    default-mysql-client

WORKDIR /app
COPY ./entrypoint.sh /app
RUN chmod +x /app/entrypoint.sh

ENTRYPOINT ["/app/entrypoint.sh"]

このDockerfileでは、MySQLのクライアントdefault-mysql-clientをインストールしています。
また、ENTRYPOINTに上記のentrypoint.shを指定しています。

上記2つのファイルを作成したら、Dockerイメージを以下のコマンドでビルドし、ECRへプッシュします。
イメージ名はdatabase-executor、タグ名はtag01としています。

# 1. イメージのビルド
$ docker build --no-cache -t 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/database-executor:tag01 .

# 2. ECRにログイン (要AWS CLI)
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com

# 3. ECRにイメージをプッシュ
$ docker push 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/database-executor:tag01

ECSの準備

ECRへのイメージのプッシュが完了したら、それを実行するECSクラスターECSタスク定義を作成します。

ECSクラスターの作成

database-executor-clusterという名前のクラスターを作成します。

ECSタスク定義の作成

以下の設定でECSタスク定義を作成します。

  • タスク定義名: database-executor-task
  • 起動タイプ: FARGATE
  • オペレーティングシステムファミリー: Linux
  • コンテナの定義
    • コンテナ名: database-executor
    • イメージ: 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/database-executor:tag01

タスク定義の作成が完了すると、以下のようなARNが (「JSON」タブのtaskDefinitionArn属性から) 確認できます。次のステップで使用するので控えておいて下さい。

arn:aws:ecs:ap-northeast-1:111111111111:task-definition/database-executor-task:1

Lambda関数の作成

ECSのクラスターやタスク定義の作成が完了したら、それを起動するためのAWS Lambda関数を作成します。

▼ lambda_function.py

import json
import boto3

ecs = boto3.client("ecs")


def lambda_handler(event, context):
    # ECSタスクの起動
    response = ecs.run_task(
        cluster="database-executor-cluster",
        taskDefinition="arn:aws:ecs:ap-northeast-1:111111111111:task-definition/database-executor-task:1",
        launchType="FARGATE",
        networkConfiguration={
            "awsvpcConfiguration": {
                "subnets": ["subnet-11111111"],
                "assignPublicIp": "ENABLED",
            },
        },
        overrides={
            "containerOverrides": [
                {
                    "name": "database-executor",
                    "command": ["select now();"],
                    "environment": [
                        {"name": "DB_HOSTNAME", "value": "xxxxxx.cluster-xxxxxxx.ap-northeast-1.rds.amazonaws.com"},
                        {"name": "DB_PORT", "value": "3306"},
                        {"name": "DB_DATABASE", "value": "xxxxxx"},
                        {"name": "DB_USERNAME", "value": "admin"},
                        {"name": "DB_PASSWORD", "value": "xxxxxx"},
                    ],
                },
            ],
        },
    )

    if len(response["failures"]) == 0:
        # 正常終了
        return {"statusCode": 200, "body": json.dumps("OK")}

    # エラー終了
    print(response["failures"])
    return {"statusCode": 500, "body": json.dumps("Error")}
  • ecs.run_task()でECSタスクを起動します。
  • clusterにECSクラスター名を、taskDefinitionにECSタスク定義のARNを指定します。
  • subnetsに対象のデータベースに接続可能なサブネットのIDを指定します。
  • containerOverridesに実行するコンテナのパラメータを指定します。
    • nameにコンテナ名を指定します。
    • commandに実行するSQLを指定します。
      • 上記の例では説明を簡単にするために、データ操作の代わりに現在の時刻を取得するSQL (select now();) を指定しています。
    • environmentにデータベースの接続情報を環境変数で指定します。
      • 上記の例では説明を簡単にするために、データベースの接続情報をコードの中に直接記述していますが、実際の環境では接続情報はLambdaの環境変数に設定して読み出すほうが望ましいと思われます。

また、このLambda関数にはiam:PassRoleecs:RunTaskの権限を実行ロールに付与する必要があります。

▼ 実行ロールに付与するポリシーの例 (JSON)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [ "iam:PassRole", "ecs:RunTask" ],
            "Resource": [
                "arn:aws:iam::111111111111:role/*",
                "arn:aws:ecs:*:111111111111:task-definition/*:*"
            ]
        }
    ]
}

実行

Lambdaの「テスト」から関数を実行します。
この関数の実行にはパラメータは不要なので、テストイベントJSONには {} を指定すればOKです。

関数が実行されて正常終了し、ECS側のログにSQL (select now();) の結果が出力されていることが確認できればOKです。

定期実行するには

上記の関数を定期的に実行するには、Amazon EventBridgeにて上記の関数を実行するCron式のルールを作成することで実現することができます。

まとめ

Amazon ECSやAWS Lambdaを使ってシェルスクリプトからSQLをサーバーレスに実行する方法をご紹介しました。

この方法を応用すれば、これまでサーバーを立てて実施していた夜間の定期バッチなどの処理もサーバーレスに切り替えることができるかもしれません。運用のサーバーレス化をご検討中の方の一助になれば幸いです。

付録: PostgreSQLの場合

PostgreSQLの場合はentrypoint.shは以下のようになります。

▼ entrypoint.sh

#!/bin/bash
set -Ceuo pipefail

# PosgtreSQLのパスワードを環境変数に設定
export PGPASSWORD=$DB_PASSWORD

psql \
  -h "$DB_HOSTNAME" \
  -p "$DB_PORT" \
  "$DB_DATABASE" \
  -U "$DB_USERNAME" \
  --command "$@"

また、DockerfileではPostgreSQL用のクライアントpostgresql-clientをインストールする必要があります。

▼ Dockerfile (抜粋)

RUN apt-get update && \
    apt-get install -y \
    postgresql-client

P.S.

採用情報