AWS Fault Injection Simulatorを使って障害を発生させる

エキサイト株式会社のみーです。

今年の3月より利用可能となったAWS Fault Injection Simulator、FISを使ってフォールトインジェクション実験をしてみました。

aws.amazon.com

いわゆるカオスエンジニアリングです。フォールトインジェクション実験をするツールとしてはNetfilxのChaos Monkeyなどが有名ですが、FISを利用することでマネージドに実施できるようになりました。

さて、CFnオタクなのでテンプレートを書いてみたのですが、書き方に少しハマってしまいました。AWS CLIで実行するときも同様にハマる可能性があるため、備忘録として残しておこうと思います。

最終的なCFnテンプレート

試行錯誤の結果、以下のようになりました。RDSインスタンスを再起動させて、強制的にフェイルオーバーを発生させるという実験内容です。

Resources:
  FISExperimentTemplate:
    Type: AWS::FIS::ExperimentTemplate
    Properties:
      Description: "RDS failover experiment"
      RoleArn: !GetAtt IAMRole.Arn
      Actions:
        RebootInstance:
          ActionId: aws:rds:reboot-db-instances
          Parameters:
            forceFailover: true
          Targets:
            DBInstances: MyInstances
      Targets:
        MyInstances:
          ResourceType: aws:rds:db
          ResourceArns:
            - arn:aws:rds:ap-northeast-1:123456789012:db:hoge-piyo
          SelectionMode: ALL
      StopConditions:
        - Source: none
      Tags:
        Env: !Ref Env

ここに至るまで、1時間くらい悩みました。

書き方がよくわからない

最初はActionsの書き方(特にTargets)がわからず、公式サンプルを参考に以下のように書いていました。

Actions:
  RebootInstance:
    ActionId: aws:rds:reboot-db-instances
    Parameters:
      forceFailover: true
    Targets:
      Instances: MyInstances

すると怒られちゃうわけ。

Resource handler returned message: "Unexpected target "Instances" found in action "RebootInstance".

なるほどわからんInstances というキーがダメなのか、MyInstances というバリューがダメなのか、よくわからん。
っで、一旦諦めてマネコンから作ってみたのですが、そこで原因判明。

f:id:ex-mii:20210520010640p:plain

↑の赤枠内が、先ほどのActionsで指定するTargetsの Key/Value ということ。TargetのResourceTypeによって特定のキーを書く必要があるっぽい。
公式サンプルを見てみると、TargetのResourceTypeがEC2の場合には Instances と書かれていました。なるほど、だからエラーになっていたのか。

docs.aws.amazon.com

他にも少し癖がありますので、公式サンプルにはしっかりと目を通しておきましょう。

おわりに

今回の RDSインスタンスでフェイルオーバーを発生させる という実験はRDSのマネコンからも試すことができます。EC2の停止実験なども同じ、マネコンからポチポチできますね。あれ、FISいらなくね?と思いませんか?

いえいえ。そうではなくて、あくまでもカオスエンジニアリングの一環として、継続的に実験を実行していくことがFISを利用する目的だと考えます。

システムが複雑になり、内部実装がブラックボックスになって・・・そんな中で重大な障害が発生すると現場はてんやわんやです。きっと誰もが1度は経験したことあるはず。
そうならないためにも日々小さな障害を意図的に発生し続けて、「サービスを停止させない」から「障害が発生しても自動で復旧する」へと意識を変えていく必要があるのかなと思いました。

FISはそのための手段の1つ。FISを活用して、より強いシステムやアプリケーションを育てていきたいものですね。

【Flutter】コピペで使える!ダイアログのデザイン集

はじめまして! エキサイト株式会社で長期インターンをしている井関です。  

まだまだ、Flutterを勉強中ですが、アウトプットや繰り返し使えるためにどんどん書いていきます👍  

目次

 

Widgetを作成する

基本的にコピペでできるとは思いますが、違うプロジェクトでペーストしたら、おそらくエラーが発生すると思います。なので、フォーマットを作っておくので、それを参考にしてもらっても構いません。初期サンプルコードを使用しているので、わかりやすいと思います。いくつか削除していますが、少しの修正なので気にせずに。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'ダイアログ'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'ダイアログ',
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          //押したら、ダイアログを出現させる。
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

その中の、OnPressedにメソッドを書いていきます。

   onPressed: () {
    //押したら、ダイアログを出現させる。
    },

ちなみに、画面はこんな感じです。

f:id:Ryutaro_isekiii:20210507184405p:plain
写真1

①まるばつクイズ風(横に均等に並べる)

Widget Dialog() {
    showDialog(
        context: context,
        builder: (_) {
          return AlertDialog(
            title: Text(
                '第一問目',
                textAlign: TextAlign.center),
            content: Text(
                 '南極で暮らす恐竜がいた。○か×か?'
                ,textAlign: TextAlign.center),
            actions: [
              Padding(
                padding: const EdgeInsets.only(
                    left: 70,
                    right: 20,
                    bottom: 20
                ),
                child: FlatButton(
                  onPressed: () {
                    //○だった時の処理を記載
                  },
                  child: Text(
                    '○',
                    style: TextStyle(
                      color: Colors.blue,
                      fontSize: 40,
                    ),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.only(
                  right: 70,
                  left: 20,
                  bottom: 20,
                ),
                child: FlatButton(
                  onPressed: () {
                    //×だった時の処理を記載
                  },
                  child: Text(
                    '×️',
                    style: TextStyle(
                        color: Colors.red,
                        fontSize: 40),
                  ),
                ),
              ),
            ],
          );
        }
    );
  }

f:id:Ryutaro_isekiii:20210517165002p:plain
実際のダイアログ

<解説>

actions: []は、基本的に、右寄せになってしまう。(基本的に右寄せがフォーマットなのかなと思っています。) mainAxisSize: MainAxisSize.centerやCenter() を入れても変わりませんでした。勉強していつか解明できるといいな〜〜 そのため、Paddingを活用することで無理やり入れみてました。

//左側
 Padding(padding: const EdgeInsets.only(left: 70,right: 20,,bottom: 20,),
//右側
 Padding(padding: const EdgeInsets.only(right: 70,left: 20,bottom: 20,),

②3択クイズ風(縦に並べる)


 Widget Dialog() {
    showDialog(
        context: context,
        builder: (_) {
          return AlertDialog(
            title: Text(
                '第一問目',
                textAlign: TextAlign.center),
            content: Text(
                'ティラノサウルスの全身の骨の数は?'
                ,textAlign: TextAlign.center),
            actions: [
              Column(
                children: [
                  FlatButton(
                    padding: const EdgeInsets.symmetric(horizontal: 100),
                    onPressed: () {
                      //①だった時の処理を記載
                    },
                    child: Text(
                      '①約350本',
                      style: TextStyle(
                        fontSize: 20,
                      ),
                    ),
                  ),

                  FlatButton(
                    padding: const EdgeInsets.symmetric(horizontal: 100),
                    onPressed: () {
                      //②だった時の処理を記載
                    },
                    child: Text(
                      '②約500本',
                      style: TextStyle(
                        fontSize: 20,
                      ),
                    ),
                  ),

                  FlatButton(
                    padding: const EdgeInsets.symmetric(horizontal: 100,),
                    onPressed: () {
                      //③だった時の処理を記載
                    },
                    child: Text(
                      '③約820本',
                      style: TextStyle(
                        fontSize: 20,
                      ),
                    ),
                  ),
                ],
              ),
            ],
          );
        }
    );
  }
}

f:id:Ryutaro_isekiii:20210510182353p:plain
実際のダイアログ

<解説>

Column()を入れて、縦に並べました。そして、均等に並べるために、Paddingを使用しました。

padding: const EdgeInsets.symmetric(horizontal: 100),

おまけ

Paddingについて

ここで簡単にPaddingの種類を紹介します。

padding: EdgeInsets.all(30),  //全体に30ピクセル隙間を空ける。
padding: EdgeInsets.only(top: 30)   //上のみに30ピクセル隙間を空ける。

・種類
上 top: 30,
右 right: 30,
下 bottom: 30,
左 left: 30,

と4種類あります。自由に使ってみるといいと思います。

padding: EdgeInsets.symmetry(horizontal: 30);   //水平に30ピクセル隙間を空ける。

horizontal => 水平 = 右と左と考えると分かりやすいと思います。

vertical => 垂直 = 上と下と考えると分かりやすいと思います。

最後に

まだFlutter初心者の私ですが、同じ境遇な方にとっていい手助けになるよう、わかりやすさにこだわってこれからも書いていこうと思います!!!業務中以外にも個人開発でFlutterを勉強している生活を送っています。新しい発見の毎日なので、ワクワクしながらコードを書いています😁

いつか、個人で開発しているアプリが紹介できるくらいいいものになった際に、紹介できたらいいなと思います!!

一通り作成してみて、趣味で作成している個人開発に利用したいと思うようになりました笑

エキサイトでは、一緒に働けるモバイルアプリエンジニアを募集しています! もし興味がありましたら是非こちらのリンクから、お話しましょう! www.wantedly.com

Javaにおける、効率の良い一覧データの回し方

こんにちは。 エキサイト株式会社の三浦です。

コードを書いている時、たまに「一覧データの中から条件に合ったものを持ってくる」ことが必要になる場合があります。 一覧データの件数自体が大したことがなかったり、1~2件程度を持ってくるのであれば filter を使えばいいですが、一覧データの件数や試行回数が多くなってくると、パフォーマンスが気になってきます。

今回は、高パフォーマンスで一覧データから合致データを持ってくる方法を紹介します。

一般的にどうするか

一般的に、一覧データの中から条件にあったものを持ってくる場合は以下のようにすると思います。

@Value
public class SampleModel {
   Integer id;
   String data;
}

public class Sample {
    private SampleModel doSample() {
        List<SampleModel> sampleModelList = List.of(
            new SampleModel(1, "aaa"),
            new SampleModel(2, "bbb"),
            new SampleModel(3, "ccc"),
            new SampleModel(4, "ddd"),
            new SampleModel(5, "eee")
        );
        Integer expectedId = 2;

        return sampleModelList
            .stream()
            .filter(sampleModel -> sampleModel.id == expectedId)
            .collect(Collectors.toList())
            .get(0);
    }
}

5件の SampleModel の中から、 id が2のものを取得してくる処理です。 この条件であれば、これで十分でしょう。

ですが、以下の場合はどうでしょうか?

@Value
public class SampleModel {
   Integer id;
   String data;
}

public class Sample {
    private List<SampleModel> doSample() {
        List<SampleModel> sampleModelList = List.of(
            new SampleModel(1, "aaa"),
            new SampleModel(2, "bbb"),
            new SampleModel(3, "ccc"),
            new SampleModel(4, "ddd"),
            new SampleModel(5, "eee"),
            // 大量にデータが入る
            new SampleModel(9999, "end")
        );
        List<Integer> expectedIdList = List.of(
            2,
            5,
            87,
            // 大量にデータが入る
            8799
        );

        return expectedIdList
            .stream()
            .map(expectedId -> doFilter(expectedId, sampleModelList))
            .collect(Collectors.toList());
    }

    private SampleModel doFilter(Integer expectedId, List<SampleModel> sampleModelList) {
        return sampleModelList
            .stream()
            .filter(sampleModel -> sampleModel.id == expectedId)
            .collect(Collectors.toList())
            .get(0);
    }
}

大量にある SampleModel の中から、指定された大量の id 一覧に合致するものを持ってくる処理です。 動作自体はこれで問題ありませんが、各 List が大量にある上に、 Listmap の中で Listfilter 処理を行っているため、パフォーマンス的に良くないことは容易に想像できます。

では、このパフォーマンスを改善するにはどうすれば良いでしょうか。

HashMapを使って改善する

一覧データを扱う上で、Javaには List 形式の他に Map 形式と呼ばれるものがあります。

List 形式が、

0 -> sampleModelA
1 -> sampleModelB
2 -> sampleModelC
...

のように、自動的に割り振られた数値キーの一覧であるのに対し、 Map 形式は

ID1 -> sampleModelA
ID2 -> sampleModelB
ID3 -> sampleModelC
...

のように、明示的に指定したキーを元にした一覧です。

Map の中でもその Map の保存形式ごとに種類があり、 HashMapハッシュ値を使って保存しているため、ランダムアクセスに強い保存形式の Map になります。

この「ハッシュ値を使っているためにランダムアクセスに強い」という HashMap の特性を使うことで、先程のコードのパフォーマンスを改善できます。

具体的には、以下のような処理を行います。

@Value
public class SampleModel {
   Integer Id;
   String data;
}

public class Sample {
    private List<SampleModel> doSample() {
        Map<Integer, SampleModel> sampleModelMap = new HashMap<Integer, SampleModel>() {
            {
                put(1, new SampleModel(1, "aaa"));
                put(2, new SampleModel(2, "bbb"));
                put(3, new SampleModel(3, "ccc"));
                // 大量にデータが入る
                put(9999, new SampleModel(9999, "end"));
            }
        };
        List<Integer> expectedIdList = List.of(
            2,
            5,
            87,
            // 大量にデータが入る
            8799,
        );

        return expectedIdList
            .stream()
            .map(expectedId -> sampleModelMap.get(expectedId))
            .collect(Collectors.toList());
    }
}

HashMap を使ってフィルタリングを行うようにした結果、ひたすら Listfilter を行っていたときに比べてパフォーマンスが上がったはずです。

最後に

扱うデータ量が少ないのであればパフォーマンスをそこまで考慮する必要はないですが、大量のデータになってくると、段々とパフォーマンスも考慮する必要が出てきます。 Javaには同じ一覧データを扱う上でも様々な選択肢があるので、一度確認してみてもいいかもしれません。

MyBatis + FreeMarkerを使用した環境でレコード追加時にAuto IncrementされたIDを取得する

f:id:excite-kazuki:20210513111813p:plain

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 MyBatisとFreeMarkerを使用した環境において、INSERT文を実行したときにAuto IncrementされたIDが取得できない問題とその解決策についてまとめました。

MyBatisとFreeMarkerを使用した環境

現在のチームでは、簡単なSELECT文やINSERT文であれば、MyBatis Generatorで生成したマッパーを使用してSQLを実行しています。 しかし、結合をする必要があるクエリーやその他複雑なクエリーは、FreeMarkerを使用するようにしています。 MyBatisとFreeMarkerに加えて、下記環境にて開発をしています。

詳細は下記記事にまとめています。 tech.excite.co.jp

問題点:MyBatis Generatorで自動生成生成したマッパーでINSERTを実行するとIDが取得できない

下記の3つをカラムに持つ書籍テーブルがあり、book_idはAuto IncrementされるIDであるとします。

  • 書籍ID(book_id)
  • タイトル(title)
  • 著者(author)

このとき、自動生成したマッパーを使用してレコードを追加するときは下記のように記述します。

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl {
    private final BookMapper mapper;
    
    public Long create(String title, String author) {
        final Book book = new Book();
        book.setTitle(title);
        book.setAuthor(author);
        return mapper.insertSelective(book);
    }
}

自動生成したマッパーのコードを見ても、intが返り値となっているため、なんだかIDが返ってきそうな感じがします。

@Generated(value="org.mybatis.generator.api.MyBatisGenerator")
default int insertSelective(Book record) {...}

しかし、insertSelectiveの返り値はINSERT文で影響を与えた件数が返るため、IDが返ってくるわけではありません。 そのため、なんらかの方法でIDを取得する必要があります。

解決策:OUTPUT句を使用する

SQL Serverでは、OUTPUT句を使用することで、INSERT / DELETE / UPDATE で追加 / 削除 / 更新された行のデータを取得することができます。 ここでは、INSERT文でレコードを追加したときのAuto IncrementされたIDを取得するために使用します。

docs.microsoft.com

まず、下記のようにftlファイルを記述します。

INSERT INTO book (
    title,
    author
)
OUTPUT
    inserted.book_id
VALUES (
    <@p name="title"/>,
    <@p name="author"/>
);

次に、Javaでは下記のようなカスタムマッパーを記述します。 このように記述することで、ftlファイルに記述したSQLを実行することができ、book_idを返り値として受け取ることができます。

@Mapper
public interface BookCustomMapper {
    @Lang(FreeMarkerLanguageDriver.class)
    @Select("insert_book_return_book_id.ftl")
    Long insert(
            @Param("title") String title,
            @Param("url") String author
    );
}

ここで、INSERT文を実行しているのに、@Select("insert_book_return_book_id.ftl")SELECT文のアノテーションを使用していることに注意する必要があります。 @Insertを使用してしまうと、OUTPUT句で指定したbook_idを取得することができません。

補足

現在のプロジェクトでは上記の解決策でAuto IncrementされたIDを取得しています。 上記解決策の他にIDを取得することができる方法についてまとめます

  1. 生成したマッパーに@SelectKey@Option("useGeneratedKeys = true") を付与する
  2. generatorConfig.xml<table> <generatedKey ... /> </table>を記述する
  3. INSERT文を実行した後に、SELECT SCOPE_IDENTITY()を実行する

1については、MyBatis Generatorで生成したマッパーにアノテーションを付与しなくてはならないため、 マッパーを再度自動生成したときにアノテーションが消えてしまうといった問題があります。

2については、INSERT文を実行したときにIDが欲しいと思ったタイミングで、generatorConfig.xmlXMLを記述し、その後自動生成を行うため手間がかかります。

3については、自分の環境で再現できなかったため断念しました。

まず、下記のようなカスタムマッパーを作成しました。 次に、生成したマッパーを使用してINSERT文を実行した後に、get_last_insert_id()を実行したところ、IDを取得することができなかったです。(nullになってしまいました) 本件についてわかる方がいたら教えていただけると嬉しいです!

@Mapper
public interface LastInsertIdCustomMapper {
    @Select("SELECT SCOPE_IDENTITY()")
    Long get_last_insert_id();
}

おわりに

MyBatisとFreeMarkerを使用した環境において、INSERT文を実行したときにAutoIncrementされたIDが取得できない問題とその解決策についてまとめました。 「IDの取得くらい簡単にできるだろう」と思っていましたが、想定していたよりも時間を使ってしまいました。 本記事を見ていただいた方のお役に立てれば幸いです。

UTってどこまでやればいいんだろう?(ポエム)

エキサイト株式会社 メディア事業部エンジニアの中です。

SpringBootでUTをやる方法をいくつか紹介しましたが、、、実際にはどこまでやればいいんだろうと、ちょっとポエムを記載します。

tech.excite.co.jp

tech.excite.co.jp

tech.excite.co.jp

tech.excite.co.jp

上記の記事にコントローラー、DB、外部APIを実行するテストの基本形を記載しています。

が、

が、

が、

果たして基本形だけで本当に良いのでしょうか?

例えば、

  • コントローラー
    • 画像ファイルをアップロードするなど、ファイルの送受信
    • セッション情報の返却
  • DB
    • CRUDのそれぞれのテスト
    • 複数のTBLを結合しているクエリ
  • 外部API
    • 画像ファイルをアップロードするなど、ファイルの送受信
    • 実行するためにheaderやhostなど、設定が複雑

など、基本形のケースではないパターンがあります。

本当に全部やりますか?

大変じゃないですか?

UTにこだわると、コードは数分の修正で終わったのに、テストコードを修正するのに1日かかるとかあります。、、

非効率的じゃないですか?

UT100%は業務において、自己満足だと思います。

※ライブラリの提供など、例外あり

品質と効率のバランスをプロジェクトで認識合わせをした上でUTを進めないと、、、

間違っても

UT100%じゃないとリリースできない

みたいなことはやめましょう

SpringBoot AOP でメソッドの実行時間を計測する

エキサイト株式会社 メディア開発の佐々木です。

SpringBoot/AOPでメソッドの実行時間を計測します。計測は重要です。

依存関係の解消

Gradleで依存関係を解消します。

dependencies {
     ...
     implementation "org.springframework.boot:spring-boot-starter-aop"   // これを追加
     ...
}

マーカーアノテーション

マーカーアノテーションを作成しておきます。計測したいメソッドにマーカーアノテーションをつけるだけで計測対象になるので、とても便利です。

/**
 * メソッドの実行速度を計測する用のマーカアノテーション.
 * 実際の計測は {@link jp.co.excite.web.aop.LatencyMonitorAop} で実行.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface LatencyMonitor {

}

RetensionPolicy が大事で、 RUNTIME をつけないと実行時に動作しないので気をつけてください。

AOPを設定

AOPを下記のように定義します。今回は@Aroundを使います。これはメソッドの開始と終了のときに処理を差し込むことができるAOPになります。

@Aspect // AOPであることを明示
@Component
@Slf4j
public class LatencyMonitorAop {

    @Around("@annotation(jp.co.excite.web.aop.LatencyMonitor)")
    public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();  // メソッド開始前のシステム時刻
        Object proceed = joinPoint.proceed();     // メソッド実行
        long end = System.currentTimeMillis();   // メソッド終了後のシステム時刻
        log.info("{} {}   method latency: {} ms."    // メソッドの実行時間の出力
                , joinPoint.getTarget().getClass().getName()
                , joinPoint.getSignature().getName()
                , end - start);
        return proceed;  // メソッドの内容を返却
    }
}

開始時と終了時にシステム時刻をとっておき差分をとります。

メソッドに付与

計測したいメソッドにはじめに定義したマーカーアノテーションをつけるだけです。

@Controller
@RequestMapping
public class RootController {

    @GetMapping("fast")
    @LatencyMonitor // アノテーションをつけるだけ
    public String fast(){
        return "index";
    }

    @GetMapping("slow")
    @LatencyMonitor  // アノテーションをつけるだけ
    public String slow() throws InterruptedException {

        Thread.sleep(2000);

        return "index";
    }
}

実行ログです。

2021-05-13 00:45:07.245  INFO 17967 --- [nio-8080-exec-4] jp.co.excite.web.aop.LatencyMonitorAop   : jp.co.excite.web.controller.RootController fast   method latency: 0 ms.
2021-05-13 00:45:11.836  INFO 17967 --- [nio-8080-exec-5] jp.co.excite.web.aop.LatencyMonitorAop   : jp.co.excite.web.controller.RootController slow   method latency: 2004 ms.

それぞれ、計測されています。

最後に

SpringAOPを使ってメソッドの中身を変更せずに、実行時間を計測する機能を作ってみました。AOPはとても便利ですが、どこで何が行われてるかわからなくなるので、マーカーアノテーション等をトリガーにした作りにするのがよさそうです。AOPは補助的な処理にとても向いてると思うので、これも拡張していければと思います。

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

www.wantedly.com

RDS for Oracleのバックアップ戦略

エキサイト株式会社のみーです。

RDSインスタンスのバックアップを実施するには、RDSの標準機能を使う方法だけでなく、AWS Backupを使う方法も用意されています。
要件に合わせて適切な選択をしていきましょう。

なお、以下の内容はRDS for Oracleを前提としていますのでご注意ください。

AWS Backupを使う

AWS Backupはバックアップボールトとバックアッププランという2つの概念を押さえておく必要があります。

バックアップボールト

バックアップボールトはバックアップをまとめる論理的なコンテナです。

特徴の1つとして、ボールト毎にアクセスポリシーを設定することができます。
バックアップの削除を禁止したり、特定ユーザしかアクセスできないようにしたり、セキュリティ的に保護する必要性がある場合には非常に有用な機能ですね。

バックアッププラン

RDSの標準機能ではスナップショットの取得は24時間毎になりますが、AWS Backupを使うことでより柔軟な設定が可能になります。
取得間隔を12時間毎にしたり、もちろんcron式で設定することもできます。

先日、RDSインスタンスの継続的バックアップもサポートされたことで、AWS Backupだけでバックアップを集中管理できるようになりました、素敵。

aws.amazon.com

クロスリージョンコピーができない?

RDSインスタンスに個別にオプショングループを設定している方も多いと思いますが、その場合にはAWS Backupを使ったクロスリージョンコピーができないこともあります。

そもそも、RDSのオプショングループはリージョン毎に作成していくものです。
なので、クロスリージョンコピーする場合は、コピー元のオプショングループと同じ設定のオプショングループをコピー先に用意しておく必要があるわけです。

Option groups are specific to the AWS Region that they are created in, and you can't use an option group from one AWS Region in another AWS Region. https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_CopySnapshot.html

そしてコピーするときには、OptionGroupNameに用意したオプショングループを設定することになるのですが、

Specify this option if you are copying a snapshot from one AWS Region to another, and your DB instance uses a nondefault option group. https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CopyDBSnapshot.html

残念ながら、AWS Backupではコピー先のオプショングループを設定する項目がないようです。

You cannot specify RDS options when using AWS Backup to make a backup copy. https://docs.aws.amazon.com/aws-backup/latest/devguide/troubleshooting.html

RDSインスタンスにデフォルトのオプショングループを設定している場合は問題ありません。
そうでない場合は、今のところはAWS Backupでの管理は諦めるしかないのです。いつかサポートされることを願って。

AWS Backupでバックアップを管理したい、ということであれば、スナップショットの取得をAWS Backupで、クロスリージョンコピーはLambdaで、というように分けてしまうのもありだとは思います。

RDSの標準機能を使う

AWS Backupのようにスナップショットの取得間隔を変更したりはできませんが、そういった要件がなければこちらで十分かもしれません。

クロスリージョン自動バックアップ

2020年12月4日、re:invent 2020でクロスリージョン自動バックアップ発表され、本格的なDR構想へ向けて勢いが増し、
2021年3月2日、大阪リージョンがフルリージョンに昇格したことでさらにその勢いが増していき、
2021年5月2日、唯一の課題だった暗号化されたインスタンスのクロスリージョンコピーもサポートされました。

aws.amazon.com

そんなこんなで、大阪リージョンでのDRを手軽に実現できるようになりました。
大阪リージョン、いかがですか?使ってみたくなりましたか?

なお、継続的バックアップを有効にしている場合、東京リージョンと大阪リージョンのLatest restorable timeの差は最大でも15分程度でした。良い感じ。

設定について

マネコンから設定してみます。2021年5月現在、CFnではまだ設定できないようです。

コピー先リージョンでのスナップショット保持期間ですが、こちらは後から変更できないようなので注意が必要です。

また、RDSインスタンスを暗号化している場合、マスターキーを指定する必要があります。
キーのARNを入力するようになっていますが、エイリアスで指定しても問題ないようです。AWSマネージドのデフォルトキーを使っていれば、alias/aws/rdsでOK。

f:id:ex-mii:20210511125133p:plain

オプショングループについて

AWS Backupで問題になっていたオプショングループですが、特に何も設定せずとも、コピー元のオプショングループをベースにRDS側で自動生成してくれます。
これは非常に助かりますね。今まではいちいち用意する必要があったので、その手間を省けるようになったわけです。

画像の一番下の項目が自動生成されたオプショングループです。Option group for automated backup arn:aws:rds:ap-northeast-1...とあります。素晴らしい。

f:id:ex-mii:20210511125155p:plain

まとめ

セキュリティの要件が厳しい場合はAWS BackupとLambdaを組み合わせる、
手軽にDRを実現したい場合はRDSの標準機能を使う、
というような使い分けがベターなのかなと思います。

クロスリージョン自動バックアップの暗号化サポートに気付かず、自前でLambdaを書いていたのは内緒です。
クラウドサービスは常に進化し続けているのだ、ということを忘れず、日々の情報収集を欠かさないようにしていきたいですね。