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を書いていたのは内緒です。
クラウドサービスは常に進化し続けているのだ、ということを忘れず、日々の情報収集を欠かさないようにしていきたいですね。

新卒デザイナーがエキサイトテックブログをリニューアルした話

f:id:excite_ny:20210510180431p:plain

はじめに

初めまして!4月にエキサイト株式会社に入社した21卒デザイナーの山﨑と申します。

この度、エキサイトテックブログのリニューアルを担当させていただきました。

f:id:excitech:20210510123301p:plain

新卒入社してから初めての仕事で、このリニューアルを通して学びになった事を記していこうと思います。

なぜリニューアルしたのか

今回エキサイトテックブログがリニューアルに至ったのは、以下の2つが理由でした。

  1. テックブログなのにコードが見辛い。
  2. デザインがデフォルトのままで素っ気ない。

これらの問題を解決するために、同じ21卒のエンジニア達に「現状のテックブログのコードの、どの辺りが見辛いのか」をヒアリングを行いました。

f:id:excitech:20210510125650p:plain

f:id:excitech:20210510125656p:plain:w300

「Zenn」や「Qiita」等のエンジニア情報共有サイトのコードが見やすいという意見があったので、この2つのサイトを参考にしながらリデザインしてみました。

f:id:excitech:20210510130837p:plain

従来のテックブログは背景黒+文字白でバチバチした印象だったのですが、背景紺色+文字パステルカラーにカラーチェンジを行い視認性を向上しました。

デザインのパターン出し

リニューアルを行う上で、ヘッダーとサイドバーにあるバナーを追加する事に決定し、4パターンほどラフデザインを製作しました。

f:id:excite_ny:20210510161039p:plain

デザイナーの先輩達にレビューを頂き、右下のアイソメトリックイラストを使ったバナーに決定しました!

学生時代はよく教授に作品をボコボコに言われていたので、初めてのデザインレビューは「どんな風にボコボコにされるんだろう…」と緊張していたのですが、すごく優しく指導してくださって良かったです😭

f:id:excite_ny:20210510161712p:plain:w300

決定したヘッダーに合わせてバナーも製作しました。

CSSにつまづいたり上手く実装できなかったりなど色々ありましたが、無事にリニューアル出来て良かったです!

最後に

デザイナーとしての最初の仕事はこんな感じになります!

学生時代にデザイン先行で色々作ってきた自分としては、デザインに移る前に仕様書を書いたりなど新しい事ばかりで、すごく勉強になることが多かったです!

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

それでは!

www.wantedly.com

nodejsがDockerでいきなりインストールできなくなった話

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

ある日、久々にDockerのイメージを消してビルドした時、以前は普通にビルド出来ていたはずなのにできなくなっていた…。そんな経験はありませんか? 今回は、nodejsに関するそんなお話です。

Jenkinsの掃除と問題の発生

現在弊社の一部のプロジェクトでは、Jenkinsを使ってデプロイ等を行っています。 ただ、長期に渡ってJenkinsを使い続けていると無駄データが少しずつ溜まっていき、ストレージを圧迫して不具合が起きてしまうので、アラートが起きるとJenkins内の不要なDockerイメージ・コンテナの削除等を行うようにしています。

その日もJenkinsの一時停止・掃除を行い、無事再起動が終了してホッとしていたのですが、その後あるプロジェクトのデプロイができなくなる問題が発生しました。 原因究明をしていく中で、Jenkinsの再起動自体に問題があったわけではなく、Jenkins内のあるDockerイメージを削除したことで再ビルドが必要になり、その再ビルドがnodejsのインストール部分で詰まっていることが判明しました。

WARNING: The following packages cannot be authenticated!
  nodejs

当然、以前は普通に動いていたビルドです。 いわゆる、「何もしていないのに動かなくなった」が発生したのでした。

なにが問題だったのか

結論から言うと、これは GPG error と呼ばれる問題でした。 これは、パブリックキーの認証に失敗しているために起きるエラーです。

おそらく以前は自動的にやってくれていたものが、何かしらの環境や仕様の変更等によって自動では認証できなくなったものと考えられます。

解決方法は簡単で、自動で認証してくれないのであれば手動で認証するようにすれば問題ありません。

例えばnodejsのver.13をインストールしたい場合、

こちらを

ENV NODEJS_VERSION 13
RUN curl -sL https://deb.nodesource.com/setup_${NODEJS_VERSION}.x | bash - \
    && apt-get install -y nodejs

このようにすれば

ENV NODEJS_VERSION 13
RUN curl -sL https://deb.nodesource.com/setup_${NODEJS_VERSION}.x | bash - \
    && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 1655A0AB68576280 \
    && apt-get update \
    && apt-get install -y nodejs

動くはずです。

まとめ

久々に実行してみると動かなかった…というのはたまに起きます。 焦らず、冷静に対応していきたいものです。

参考文献

UbuntuでGPG errorが出た時の対処法の紹介です。

Javaのenumの基本的な使い方

エキサイト株式会社メディア開発佐々木です。Javaの列挙型であるenumの基本的な使い方を共有します。

enumとは

列挙型と言われます。Javaの列挙型はJava1.5の頃から使え、当時の他の言語の列挙型より強力だったという話があります。そして、Javaの列挙型は単なる定数ではなく、オブジェクトとなり、メソッド等を実装することが可能となっています。これにより使い勝手が格段に向上します。

enumの基本

Javaの列挙型は、このような感じで宣言します。

public enum FruitType {
    ORANGE("オレンジ")
    ,APPLE("りんご")
    ,MELON("メロン")
    ,OTHER("その他")
    ;

    private String fruit;

    FruitType(String fruit) {
        this.fruit = fruit;
    }
}

これですぐに使えます。 しかし、これだけだと、使うときに外のクラスで分岐処理してまうことがほとんどです。

FruitType getType(String type){
     if (FruitType.ORANGE.name().equals(type)) {
         return FruitType.ORANGE;
     }
     if (FruitType.APPLE.name().equals(type)) {
         return FruitType.APPLE;
     }
     if (FruitType.MELON.name().equals(type)) {
         return FruitType.MELON;
     }
     return FruitType.OTHER;
}

こういう分岐処理を外部クラスがやると、内部情報が漏れてしまっていて、弱い設計になってしまいます。

enumはこう使うと便利

enumはこのままだと、ただの定数扱いですが、Javaenumは、Enumクラスというものを継承しており、さらにメソッド等を実装することで処理を内部に閉じ込めて保守性も高く使いやすいものになります。

public enum FruitType {
    ORANGE("オレンジ")
    , APPLE("りんご")
    , Melon("メロン")
    , OTHER("その他");

    private String fruit;

    FruitType(String fruit) {
        this.fruit = fruit;
    }

    private static final Map<String, FruitType> map;
    static {
        Map<String, FruitType> typeMap = Arrays.stream(FruitType.values()).collect(Collectors.toMap(e -> e.name(), e -> e));
        map = Collections.unmodifiableMap(typeMap);
    }


    public static FruitType get(String fruit) {
        return map.getOrDefault(fruit, FruitType.OTHER);
    }
}

static変数やstaticメソッドを使用して

FruitType getType(String type){
    return FruitType.get(type);
}

呼び出しはこんな感じになるので、内部情報も隠蔽できていてenumとしての要件も満たせています。

さいごに

今回はenumの初歩の初歩ですが、他の言語にはenumが無いこともあるので、紹介させていただきました。しっかり使いこなせればとても有用なものになると思います。

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

www.wantedly.com

SpringBootでRestTemplateを使った外部APIを実行している実装をテストする。

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

SpringBootでRestTemplateを使った外部APIを実行している実装をテストする方法を記載します。

やり方は単純で、mockitoを使います。

ユースケース

  • RestTemplateを使った外部APIを実行している実装をテストする

前提条件

amazonにペットショップの一覧と金額を返すAPIがあると、仮に設定します。

  • APIのレスポンス
[
  {
    "id": 1,
    "type": "dog",
    "price": 249.99
  },
  {
    "id": 2,
    "type": "cat",
    "price": 124.99
  },
  {
    "id": 3,
    "type": "fish",
    "price": 0.99
  }
]

実装

  • interface
public interface AmazonStore {
    List<PetGoods> getPetGoods();
}
  • Impl
@Component
@RequiredArgsConstructor
public class AmazonStoreImpl implements AmazonStore {

    private final RestTemplate restTemplate;

    @Value("${spring.amazon.store.pet.url}")
    private String url;

    @Override
    public List<PetGoods> getPetGoods() {
        final UriComponents uriComponents = UriComponentsBuilder
                .fromUriString(url)
                .build();

        HttpHeaders headers = new HttpHeaders();
        final HttpEntity<String> entity = new HttpEntity<>(headers);

        final ResponseEntity<PetGoods[]> exchange = restTemplate
                .exchange(uriComponents.toUri(), HttpMethod.GET, entity, PetGoods[].class);

        if (exchange.getStatusCode().isError()) {
            throw new RuntimeException();
        }

        return Arrays.asList(exchange.getBody());
    }
}
@Data
@Accessors(chain = true)
public class PetGoods {
    private long id;
    private String type;
    private float price;
}

入力例

テストコードです

@ExtendWith(MockitoExtension.class)
class AmazonStoreImplTest {

    @Mock
    private RestTemplate restTemplate;

    @InjectMocks
    private AmazonStoreImpl amazonStore;

    @Test
    @Description("amazon ペットリストの一覧取得")
    void getPetGoods() {

        final PetGoodsDto dog = new PetGoodsDto()
                .setId(1)
                .setType("dog")
                .setPrice((float) 249.99);

        final PetGoodsDto cat = new PetGoodsDto()
                .setId(2)
                .setType("cat")
                .setPrice((float) 124.99);

        final PetGoodsDto fish = new PetGoodsDto()
                .setId(3)
                .setType("fish")
                .setPrice((float) 0.99);

        final List<PetGoodsDto> result = List.of(dog, cat, fish);

        final UriComponents build = UriComponentsBuilder
                .fromUriString("http://localhost/petstore/pets")
                .build();

        HttpHeaders headers = new HttpHeaders();
        final HttpEntity<String> entity = new HttpEntity<>(headers);

        PetGoodsDto[] returnMock = {
                dog, cat, fish
        };

        final ResponseEntity<PetGoodsDto[]> responseEntity = new ResponseEntity<>(returnMock, HttpStatus.OK);

        Mockito.when(restTemplate.exchange(
                build.toUri(),
                HttpMethod.GET, entity, PetGoodsDto[].class))
                .thenReturn(responseEntity);

        ReflectionTestUtils.setField(amazonStore, "url", "http://localhost/petstore/pets", String.class);

        final List<PetGoodsDto> petGoods = amazonStore.getPetGoods();

        Assertions.assertEquals(
                result,
                petGoods
        );
    }
}

出力例(テストが成功しているコンソールログです)

BUILD SUCCESSFUL in 2s

ポイント解説

用意するmockを設定します。

    @Mock
    private RestTemplate restTemplate;

テスト対象をInjectMocksで設定します

    @InjectMocks
    private AmazonStoreImpl amazonStore;

返却されるResponseEntityを設定します。

        final ResponseEntity<PetGoodsDto[]> responseEntity = new ResponseEntity<>(returnMock, HttpStatus.OK);

urlは@ValueでSpringBootのプロパティファイルから設定しているので、以前の記事で設定したReflectionTestUtilsを使って、urlを設定します。

ReflectionTestUtilsで変数に値をセットする - Excite Tech Blog

        ReflectionTestUtils.setField(amazonStore, "url", "http://localhost/petstore/pets", String.class);

あとはいつも通りのmockitoを使ってテストをします。

HttpStatus.OK以外を設定すれば、エラーハンドリングのテストもできます。

MyBatisのDynamicSQLにFreeMarkerを採用する

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

SpringBoot/Javaで既存システムのリビルド開発を行っていますが、ORマッピー(正確にはORマッパーではない)にはMyBatisを採用しています。テーブル構成はなかなか変えられない為、良くも悪くも自由度の高いMyBatisを採用しています。

MyBatisとは

JavaのORマッパーになります。

MyBatisのクエリ生成(デフォルト)

単純な動的クエリはメソッドが用意されているのですが、joinやら分岐やら複雑なSQLになるとそうはいきません。デフォルトはXMLの設定になります。メリットとしてはこういったものになるかと思います。

  • デフォルトの設定
  • 複雑なクエリで複雑なマッピングXML上で完結できる
  • Javaの型とDBの型とのマッピングが柔軟
  • 1ファイルに複数のクエリを書ける

しかし、XMLを採用しているがゆえのデメリットもあります。

  • XML形式だと、比較演算子で面倒(CDATA[]の中で書かないとエラーになる)
  • XMLの記述が冗長で覚えるのが面倒
  • 複雑なクエリで複雑なマッピングを行えてしまい、XML上でしかテストができないので使わないようにしたい

比較演算子でCDATAを使わないといけないなど、割と面倒です。代替手段がないかの検討を行いました。

FreeMarkerの採用

MyBatisではFreeMarkerSQLを書くファイルとしてサポートしていたので、採用しています。早速ファイルの中身を比較してみましょう。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.UserDataMapper">
    <select id="findById" resultType="com.example.demo.UserData">
        SELECT 
                 id
               , name
               , birth_year
               , birth_month
               ,birth_day 
        FROM 
               USER_DATA 
        WHERE 
               id = #{id} 
               and now() <![CDATA[ < ]]> birth_month
    </select>
</mapper>

FreeMarkerではこうなります。

findById.ftl

SELECT 
          id
          , name
          , birth_year
          , birth_month
          , birth_day 
FROM 
          USER_DATA 
WHERE 
           id = <@p name="id" />
          and now() < birth_month

記述量が結構違いますよね。XMLの方が複雑な制御ができたりするのですが、テストしづらいですし、コードを見たときに理解しづらいコードになったりします。メディア開発ではアジリティを大事にしていますので、同じ効果であれば効率のイイものを選択し、リターンがない複雑なものは選択しないように意識しています。

最後に

MyBatisの標準はXMLですが、エキサイトのメディア開発では、使い勝手や開発のアジリティを考えて最適なものを選ぶようにした結果、FreeMarkerを選択しています。小さなことですが、技術や自分たちが抱えてる問題をしっかり把握し、少しずつでも現状にフィットした適切な選択を積み重ねられる組織運営ができればと思います。

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

www.wantedly.com

JavaのSpring Bootにおいて、クエリパラメータのキー名と引数名が異なる場合のフォームクラスの書き方

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

今回は、Java / Spring Bootを使っているとき、GETリクエスト受付時のクエリパラメータのキーと受け取り変数名が異なる場合の、受け取り用クラス(フォームクラス)の書き方について説明します。

課題

突然ですが、あなたは今Java / Spring Bootを使ってAPIを開発しているとします。 RESTのGETリクエストにおいてクエリパラメータを受け付ける処理を書く時、クエリパラメータのキー名がそのまま受け取る変数名として使えるのであればいいのですが、どうしてもキー名と変数名が異なることがあります。 たとえば、クエリパラメータのキーはスネークケースだが変数名はキャメルケースにしたい、という経験がある方は多いのではないでしょうか。

@RequestParam を使って

@GetMapping()
public String getSampleData(
    @RequestParam(value = "data_a") String dataA,
    @RequestParam(value = "data_b") String dataB,
    @RequestParam(value = "data_c") String dataC,
    @RequestParam(value = "data_d") String dataD
) {
    return "Hello world!";
}

と書けるのであればいいですが、キーが大量にあって1つ1つ引数を書いていると可読性に大きな問題がある場合や、受取時に2つ以上の引数を同時に対象とするバリデーションを行いたい場合(たとえば、 data_adata_b のどちらかは必ず値があることをバリデーションしたい場合)は、引数にクラスを指定することになります。

@GetMapping()
public String getSampleData(
    @ModelAttribute SampleModel sampleModel
) {
    return "Hello world!";
}

@Data
public class SampleModel {
    private String data_a;
    private String data_b;
    private String data_c;
    private String data_d;

}

ただし、このままだと変数名がキー名と同じになってしまいます。

解決策1(コンストラクタの引数名をキー名にし、プロパティ名を使用したい変数名にする)

そこで、以下のようにすることでキー名を想定通りにすることが出来ます。

@Getter
public class SampleModel {
    private String dataA;
    private String dataB;
    private String dataC;
    private String dataD;

    public SampleModel(
        String data_a,
        String data_b,
        String data_c,
        String data_d
    ) {
        this.dataA = data_a;
        this.dataB = data_b;
        this.dataC = data_c;
        this.dataD = data_d;
    }
}

クエリパラメータからデータを受け取るのはフォームクラスのコンストラクタであるため、コンストラクタの引数名をキー名と同じにした上で、フォームクラスのプロパティ名を使用したい変数名とします。

ただしこの場合、コンストラクタの引数名はキー名と同じになってしまうことになります。

解決策2(ConstructorPropertiesを使用する)

コンストラクタの引数名の時点で使用したい変数名にしたい場合は、 ConstructorProperties を使います。

@Getter
public class SampleModel {
    private String dataA;
    private String dataB;
    private String dataC;
    private String dataD;

    @ConstructorProperties({"data_a", "data_b", "data_c", "data_d"})
    public SampleModel(
        String dataA,
        String dataB,
        String dataC,
        String dataD
    ) {
        this.dataA = dataA;
        this.dataB = dataB;
        this.dataC = dataC;
        this.dataD = dataD;
    }
}

ConstructorProperties がクエリパラメータのキーを紐付けてくれます。

まとめ

フォームクラスを使ってクエリパラメータのキーを異なる名前の変数に紐付けたい場合は、コンストラクタや ConstructorProperties を使うと良いでしょう。 プロジェクトのルールにもよりますが、引数名から使用する変数名と同じにしたいというケースが多いと思いますので、 ConstructorProperties を使用する場面が多いのではないでしょうか。

Java15のTextBlockとMyBatisのカスタムクエリについて

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

Java15で入ったTextBlockでMyBatisのカスタムクエリがアノテーション内でできないかなと思い試してみました。

結果

できた。これは嬉しい。Javaのことだからアノテーションだけ使えないとかはさすがにないと思いましたが、試してみてできたので、やっぱりさすがだなと思いました。

内容

Javaでは、TextBlockがなかったので、カスタムクエリをXMLやFreeMarkerを用いて別ファイルにしていましたが、TextBlockが入ってくれたおかげで、アノテーション内にも、クエリがかけるようになりました。

@Mapper
public interface BookCustomMapper {

    // いままで
    @Lang(FreeMarkerLanguageDriver.class)
    @Select("findByBookId.ftl")
    List<Books> findByBookIdFtl(@Param("id") Long id);


    // TextBlockの恩恵
    @Lang(FreeMarkerLanguageDriver.class)
    @Select("""
            select
             *
            from
              book
            where
              1 = 1
              <#if id?has_content>
              AND book_id = <@p name="id"/>
              </#if>
            """)
    List<Books> findByBookId(@Param("id") Long id);

FreeMarker記法もちゃんと認識してくれているんで便利ですねー。ちょっとしたカスタムクエリには使ってもいいかなと思います。

最後に

現状は、Java11(LTS)を使用しているんで、Java17になったら思う存分使おうと思います。

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

json diffを効率よく、ワンライナーで

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

今回はjson diffを簡単にできる方法を記載しようと思います。

例えば、以下のようなjsonが2種類あったとします。

{
  "userId": 1,
  "id": 1,
  "title": "naka",
  "body": "sho"
}
{
  "body": "sho"
  "title": "naka",
  "id": 1,
  "userId": 1,
}

ただのdiff コマンドだと、以下のように、順番違うので差分が出てきてしまいます。

diff a.json b.json
2,3c2
< "userId": 1,
< "id": 1,
---
> "body": "sho",
5c4,5
< "body": "sho"
---
> "id": 1,
> "userId": 1

そこで、jq sort-keysでkeyで並び替えて比較すると綺麗に差分を比較することができます。

jqについては以下の公式を参考にしてください。

jq

diff <(jq --sort-keys . a.json) <(jq --sort-keys . b.json)

API通しの比較であれば、curlコマンドを中に入れ込めば、いちいちjsonファイルを作らなくても比較ができます。 新旧APIの比較などに使えそうですね。

差分がない場合

diff <(curl -s 'https://jsonplaceholder.typicode.com/posts/1' | jq --sort-keys) <(curl -s 'https://jsonplaceholder.typicode.com/posts/1' | jq --sort-keys)

差分がある場合

$ diff <(curl -s 'https://jsonplaceholder.typicode.com/posts/1' | jq --sort-keys) <(curl -s 'https://jsonplaceholder.typicode.com/posts/2' | jq --sort-keys)
2,4c2,4
<   "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
<   "id": 1,
<   "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
---
>   "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
>   "id": 2,
>   "title": "qui est esse",

差分がない場合は何も表示されないので簡単ですね。 差分がある場合は、どのキーなのかがわかるためすぐに調査できると思います。 vimdiffを使えば、差分のある文字列がどこなのかを特定しやすいので参考に使ってください。

$ vimdiff <(curl -s 'https://jsonplaceholder.typicode.com/posts/1' | jq --sort-keys) <(curl -s 'https://jsonplaceholder.typicode.com/posts/2' | jq --sort-keys)

f:id:excite-naka-sho:20210430184816p:plain