エンジニアのTakuです。
カミナシではデータ保全 のため、RDSのスナップショットを1時間おきに取得し、万が一のデータ破損に備えております。
今回はそのECSタスクで定期的にバッチ処理 を実行する方法 について共有させていただきたいと思います。
取得理由
カミナシでは150社以上の企業様にご利用いただいており、毎日膨大なレポートが作成されております。(2022年4月現在)
ご参考)↓の14ページ
speakerdeck.com
日々膨大な量のデータが増えており、中には監査などの保管が必要なデータもあり、データやDBの破損が発生するとお客様への影響が大きいです。
そのため、万が一の際の障害発生時の影響を少しでも抑えるため、1時間に1回フルバック アップを取得しそれらを最大24時間保持することとしました。
※ポイントタイムリ カバリを利用した特定の時点への復元も可能ですが、そちらが破損する事態にも備え、1時間おきに取得することとしております。
取得方式の検討
結論から申し上げますと、カミナシではECSタスクで1時間に1回取得するようにしました。
取得段階では以下3案ありそれぞれ比較・検討しましたので、採用経緯踏まえお話ししたいと思います。
案1) AWS Backup
案2) AWS Lambda
案3) AWS ECSタスク
途中の経過は不要でECSタスクの設定が見たい!という方は 実装 の項目までとばしてください
案1) AWS Backup
比較するにあたり、
車輪の再開発 はしたくない
メンテナンスコストをなるべく抑えたい
ということから、まずはフルマネージドなサービスを利用できないかと考え、まずはAWS Backupの調査を行いました。
docs.aws.amazon.com
調査の結果、
AWS BackupはRDS Auroraに対応
削除は最小で1日毎
取得間隔はcronで自由に設定できる
ことから1時間毎にスナップショットを取得するという要件を満たしておりました。
docs.aws.amazon.com
気になるコストに関しても、
データ量に応じたスナップショットの料金はRDSのコンソール or CLI 等から取得した料金と同じ
スナップショットの実行回数で追加料金
のみのようでした。
aws.amazon.com
スナップショットを取得する度に必要となる Audit Manager
のコストは 1バックアップ評価あたり 0.00125 USD (※2022/4/18現在) で、
1日1時間ごとに実行した場合(※)でも Lamdba or ECSタスクの実行コストよりは間違いなく低いだろうということで、こちらの実装を進めることにしました。
※1時間ごとに毎日実行した場合、月単位で 24*31=744 と1 USD 以下となる
設定はコンソールから以下のようなバックアッププランを作成することで簡単に設定ができました。
しかし、ここで開発環境で繰り回し検証をしている際に以下のエラーが出て問題が発覚しました。
Backup job XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX could not start because it is either inside or too close to the automated backup window configured in RDS cluster.
AWS Backupの仕様で、データ不整合を防ぐためにRDSのシステムバックアップの前後4時間はバックアップ取得ができないようです。
これは、RDS メンテナンス期間または RDS 自動バックアップ期間が近づいているときに発生する可能性があります。AWS Backup では、RDS メンテナンス期間または RDS 自動バックアップ期間の数時間前に行う RDS バックアップは許可されません。RDS データベースのバックアッププランは、RDS メンテナンス期間と RDS 自動バックアップ期間から 4 時間以上空けてスケジュールを組んでいることを確認してください。
aws.amazon.com
Auroraの場合こちらのバックアップを停止することも不可(ポイントタイムリ カバリで使うバックアップでもあり不可)であることから、1時間おきの取得と言う要件を満たさなかったため泣く泣く利用を断念しました。。
案2) AWS Lambda
次に考えたのがAWS Lambdaです。
検索するとAWS Lambdaを利用してRDSのスナップショットを1時間おき取得している例は多々あったのですが、バッチ実行にはコンソールから編集が可能なPython or Node.jsを使っているものが主でした。
しかし、弊社ではバックエンド開発にGolang を利用しており、私を含めPython or Node.jsに長けたエンジニアがあまりおりません。
過去にAWS SAMを利用した記事を書かせていただいたとおり、それらを利用してGoでも書けなくはないのですがやはり手間がかかります。
現時点でAWS SAMを本番運用している実績もないことからこの案も見送りとしました。
案3) AWS ECSタスク
最終的に検討した案がECSタスクの利用です。
ECSタスク(Fargate)はプロダクトのAPI サーバー等でも利用しており利用実績がある
DockerFileを用意することで利用が可能
Golang でスクリプト を作成できる
ことから、今後他のエンジニアがメンテナンス容易だろうということで導入を決定しました。
実装
RDSスナップショット取得
前述のとおり、RDSスナップショットを取得するスクリプト はGolang を利用しました。
CLI はAWS からGolang 用のものも提供されているため以下を利用
aws.amazon.com
実装する際には pkg.go.dev
を参照しました。
pkg.go.dev
Golang はこういった公式ドキュメントがしっかりしているのも良いですね。
スナップショットを取得〜削除するものですので、簡易なものとし、以下のようなスクリプト を作成しました。
main.go
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/rds"
"github.com/joho/godotenv"
)
type rdsClient struct {
client *rds.Client
clusterIdentifier string
instanceIdentifier string
}
func newRDSClient() *rdsClient {
clusterIdentifier := os.Getenv("DB_CLUSTER_IDENTIFIER")
instanceIdentifier := os.Getenv("DB_INSTANCE_IDENTIFIER")
reagion := os.Getenv("REAGION")
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
log.Printf("faild load config, %v", err)
}
cfg.Region = reagion
client := rds.NewFromConfig(cfg)
return &rdsClient{
client,
clusterIdentifier,
instanceIdentifier,
}
}
func (rc *rdsClient) createRDSSnapShot(startUpTime time.Time) {
currentDateHour := fmt.Sprint(startUpTime.Year(), "-", int(startUpTime.Month()), "-",startUpTime.Day(), "-", startUpTime.Hour())
p := rds.CreateDBClusterSnapshotInput{
DBClusterIdentifier: aws.String(rc.clusterIdentifier),
DBClusterSnapshotIdentifier: aws.String(rc.clusterIdentifier + "-" + currentDateHour),
}
result, err := rc.client.CreateDBClusterSnapshot(context.TODO(), &p)
if err != nil && strings.Contains(err.Error(), "DBClusterSnapshotAlreadyExistsFault: Cannot create the cluster snapshot because one with the identifier") {
log.Printf("snapshot already exist, %v", err)
} else if err != nil {
log.Fatalf("faild describe snapshot, %v", err)
}
log.Printf("createRdsSnapShot result, %v", result)
}
func (rc *rdsClient) describeRDSSnapShot() {
p := rds.DescribeDBClusterSnapshotsInput{
DBClusterIdentifier: aws.String(rc.clusterIdentifier),
SnapshotType: aws.String("manual"),
}
result, err := rc.client.DescribeDBClusterSnapshots(context.TODO(), &p)
if err != nil {
log.Fatalf("faild describe snapshot, %v", err)
}
for _, snapshot := range result.DBClusterSnapshots{
log.Printf("describeRDSSnapShot DBClusterSnapshotIdentifier, %v", *snapshot.DBClusterSnapshotIdentifier)
log.Printf("describeRDSSnapShot SnapshotCreateTime, %v", *snapshot.SnapshotCreateTime)
}
}
func (rc *rdsClient) deleteRDSSnapShot(startUpTime time.Time) {
yestadayCurrentDate := startUpTime.AddDate(0, 0, -1)
yestadayCurrentDateHour := fmt.Sprint(yestadayCurrentDate.Year(), "-", int(yestadayCurrentDate.Month()), "-",yestadayCurrentDate.Day(), "-", yestadayCurrentDate.Hour())
p := rds.DeleteDBClusterSnapshotInput{
DBClusterSnapshotIdentifier: aws.String(rc.clusterIdentifier + "-" + yestadayCurrentDateHour),
}
result, err := rc.client.DeleteDBClusterSnapshot(context.TODO(), &p)
if err != nil && strings.Contains(err.Error(), "DBClusterSnapshotNotFoundFault: DBClusterSnapshot not found") {
log.Printf("snapshot not found, %v", err)
} else if err != nil {
log.Fatalf("faild delete snapshot, %v", err)
} else {
log.Printf("deleteRDSSnapShot result, %v", result)
}
}
func main() {
if os.Getenv("APP_ENV") == "local" {
err := godotenv.Load(fmt.Sprintf("./%s.env", os.Getenv("GO_ENV")))
if err != nil {
log.Printf("faild read env file, %v", err)
}
}
client := newRDSClient()
locale, _ := time.LoadLocation("Asia/Tokyo")
startUpTime := time.Now().In(locale)
client.describeRDSSnapShot()
client.createRDSSnapShot(startUpTime)
client.deleteRDSSnapShot(startUpTime)
}
利用している関数は以下のとおりです。
他にもCreateDBSnapshot
やDeleteDBSnapshot
という関数もあったのですが、クラスタ ー化してあるDBに対しては使えなかったのがちょっとしたハマりポイントでした。
api error InvalidParameterValue: The specified instance is a member of a cluster and a snapshot cannot be created directly. Please use the CreateDBClusterSnapshot API instead.
ECSタスクの設定
スクリプト が作成できましたら、こちらをAWS コンソールからECSへ設定していきます。
ECSにはサービスとタスクという概念があるのですが、スケジュールを作成し、バッチ的に実行する場合はサービスの作成は不要 です。
クラスタ ーとタスク定義の設定でECSタスクの実行ができます。
サービスとタスクという概念はこちらの記事が参考になりました。
qiita.com
手順は以下のとおりです。
ECRへDockerイメージのアップロード
クラスタ ーの作成
タスク定義の作成
タスクのスケジューリング
1. ECRへDocker Imageのアップロード
まずは作成したDockerのImageをECRへアップロードします
可能であればGIthub 等と連携したCI/CDを利用したいですが、もしなくてもECRのコンソールに記載のコマンドを実行することでBuild&Uploadすることができます。
ECRへのプッシュコマンドの場所
コンソールからクラスタ ーの作成を行います。
こちらはECSタスク(Fargate)を利用するのであれば「ネットワーキングのみ」でよく、名前を作成したいものに即したものを設定すれば問題ございません。
※CloudWatch Container Insightsについては必要に応じて有効にしていただければ良いですが、ここでは割愛します。
3. タスク定義の作成
続いて、1でアップしたImageを利用して、タスクの定義を作成します。
設定は以下のとおりです。
ポイントになってくるのは「タスクロール」と「コンテナの定義」です。
ECSタスクからRDSのスナップショットを取得するには、必要な権限を持つIAMロールを付与してあげる必要があります。
AWSServiceRoleForRDS
のようなフルでDBのアクセス権限を持つIAMを付与することで実行はできると思いますが、今回はSnapShotへの取得・作成・削除権限があれば良いため、それらの権限だけを持つポリシー→ロールを作成して「タスクロール」として付与すると良いかと思います。
今回は以下のような権限を持つポリシーを作成しました。
DescribeDBClusterSnapshots
CreateDBClusterSnapshot
DeleteDBClusterSnapshot
あとは「コンテナの定義」ですが、1でアップしたImageを選択し、環境変数 をコンテナに渡してあげることができるため、取得対象のクラスタ ー名などはこちらで定義します。
4. タスクのスケジューリング
ここまで来るとあとはスケジューリングのみです。
スケジューリングタブは Amazon EventBridge
から設定を行います。
docs.aws.amazon.com
※Amazon EventBridge
昨年末にECSのコンソールが新しくなりましたので、旧コンソールの場合は「タスクのスケジューリング」タブから作成することも可能です。
必要になってくるのは、3で作成したタスク定義 と、スケジュールルール(スケジュール式) です。
※キャプチャを取り直したため「ルールを編集」となっておりますが、記載項目は新規作成と同じです
スケジュールルールは1時間おきなど定型的なものであれば「固定された間隔で実行」で設定しても良いですし、Cron式の利用も可能です。
検証中、DBの利用状況にもよるのか、以下のエラーが発生してSnapShotの取得バッチが失敗することがありました。
20XX/MM/DD XX:XX:XX faild describe snapshot, operation error RDS: DescribeDBClusterSnapshots, exceeded maximum number of attempts, 3, https response error StatusCode: 0, RequestID: , request send failed, Post "https://rds.ap-northeast-1.amazonaws.com/ ": dial tcp 54.239.96.231:443: i/o timeout
そのため、カミナシでは10分おき(開発は15分間隔)実行しており、Cron式で以下のように書いております。
cron(0/10 * * * ? *)
タイミングによっては均等な1時間ごとにならない場合があるのですが、RDSのコンソールを見てスナップショットが定期的に取得できていることが確認できればOKです。
終わりに
前置きが少し長くなってしまいましたが、ECSにて定期的にRDSのスナップショットを取得する例でした。
今回はRDSのスナップショットでしたが、ECSタスクを利用することで定期的なバッチ処置を行うことができます。
カミナシでも他の様々なバッチ処理 に利用しているため、今回の木々が定期的なバッチ処理 を行う際の参考になれば幸いです。
また、カミナシでは新たなサービスの追加に加え、増え続けるユーザー様へ安定した価値を提供するために今回のようなサービス安定化からユーザー体験の向上に取り組んでおります。
「ノンデスクワーカー向けのシステム開発 ってどんな感じ?」
「成長を続けるサービスに早いフェーズから携わりたい」
など、カミナシの業務に少しでも興味ある方がいらっしゃいましたら、以下採用ページからお気軽にご応募いただけると幸いです。
careers.kaminashi.jp
最後までご覧いただきありがとうございました。