エンジニアの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の調査を行いました。
調査の結果、
- AWS BackupはRDS Auroraに対応
- 削除は最小で1日毎
- 取得間隔はcronで自由に設定できる
ことから1時間毎にスナップショットを取得するという要件を満たしておりました。
気になるコストに関しても、
- データ量に応じたスナップショットの料金はRDSのコンソール or CLI等から取得した料金と同じ
- スナップショットの実行回数で追加料金
のみのようでした。
スナップショットを取得する度に必要となる 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 時間以上空けてスケジュールを組んでいることを確認してください。
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タスクの利用です。
ことから、今後他のエンジニアがメンテナンス容易だろうということで導入を決定しました。
実装
RDSスナップショット取得
前述のとおり、RDSスナップショットを取得するスクリプトはGolangを利用しました。
CLIはAWSからGolang用のものも提供されているため以下を利用
実装する際には 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) }
利用している関数は以下のとおりです。
スナップショットの作成 rds package - github.com/aws/aws-sdk-go/service/rds - pkg.go.dev
スナップショットの削除 rds package - github.com/aws/aws-sdk-go/service/rds - pkg.go.dev
他にも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することができます。
2. クラスターの作成
コンソールからクラスターの作成を行います。
こちらは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
最後までご覧いただきありがとうございました。