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

SpringBoot でSQLをテストする(Mybatis)

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

SpringBootでmybatisで発行したSQLのテストのやり方を記載します。

H2というin-memoryデータベースを用意し、schema.sql、data.sqlの初期データを挿入してから、SQLを発行し、正しくデータが取得できているかテストをします。

ユースケース

  • SQLのテストする

前提条件

  • MybatisTestを使う
  • H2を使う

gradleに以下を追加

        testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.1.3'
        testImplementation 'com.h2database:h2:1.4.200'

入力

application.yml

  • SQL server を使用するため、ModeをMSSQLServerに設定します。
  • 細かい設定は公式サイトから参照してください。

www.h2database.com

spring:
  datasource:
    initialization-mode: always
    driver-class-name: org.h2.Driver
    url: jdbc:h2:~/test;MODE=MSSQLServer
    username: sa
    password: sa
  h2:
    console:
      enabled: true
mybatis:
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    org.springframework.jdbc: debug
    com.exblog: debug

schema.sql

CREATE TABLE user
(
    user_id  bigint IDENTITY(1,1) PRIMARY KEY,
    name NVARCHAR(20) NOT NULL
);

data.sql

INSERT INTO user VALUES (1, 'nakao');

MapperTestApplication.java

  • h2を起動させるため、テスト用にSpring Applicationの設定を追加します
  • testファイルが置いてあるパッケージに配置してください。
package com.sqlserver.mapper;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MapperTestApplication {

}

UserCustomMapper

  • SQLを実行するinterfaceとftlです。
@Mapper
public interface UserCustomMapper {
    @Lang(FreeMarkerLanguageDriver.class)
    @Select("find_user.ftl")
    UserModel getUser(
            @Param("user_id") Long userId
    );
}

find_user.ftl

select
  *
from
  user
where
  user_id = <@p name="user_id"/>

入力例

テストコードです

@ExtendWith(SpringExtension.class)
@MybatisTest
class UserCustomMapperTest {
    @Autowired
    private UserCustomMapper userCustomMapper;

    @Test
    public void findByStateTest() {
        final UserModel user = userCustomMapper.getUser(1L);
        assertTrue(user.getUserId().equals(1L));
        assertTrue(user.getName().equals("nakao"));
    }
}

出力例 デバッグモードを使うと発行したSQLの詳細が出力されます。

2021-04-29 23:39:04.683 DEBUG 4206 --- [    Test worker] c.e.mapper.UserCustomMapper.getUser  : ==>  Preparing: select * from user where user_id = ?
2021-04-29 23:39:04.705 DEBUG 4206 --- [    Test worker] c.e.mapper.UserCustomMapper.getUser  : ==> Parameters: 1(Long)
2021-04-29 23:39:04.725 DEBUG 4206 --- [    Test worker] c.e.mapper.UserCustomMapper.getUser  : <==      Total: 1
BUILD SUCCESSFUL in 3s

SQLのテストは大変

in-memoryデータベースを用意しましたが、事前準備からテスト実施まで大変ですよね。。。 Readのテストなら事前データの投入ですみますが、CUDだったらもっと大変になります。 それはまた別の機会に記載させていただけたらと思います。

MybatisTestについて、もっと知りたい方は以下を参考にしてください。

mybatis.org

JSR303の@ValidとSpringBootの@Validatedの違い

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

Javaには、JSR303 Bean Validationという私の好きなValidation仕様があります。@Validをつければ、クラスのフィールドに@Empty@Min(1)のようなアノテーションをつけるだけで、値のバリデーションが可能です。Springにはこれを拡張した@Validatedがあります。この使い方について軽く触れます。

@Validはなに?

これはJavaの標準仕様で、クラスのフィールドにアノテーションをつけて、所定メソッドを実行するとバリデーションを行ってくれます。

@Data
class Form {
   @NotEmpty
   private String name;
 }


Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); // validatorのインスタンス生成
Form form = new Form(); // データクラスのインスタンス
Set<ConstraintViolation<Form>> validate = validator.validate(form); // バリデーションの実行

フィールドに付けられたアノテーションの情報を元にバリデーションをしてくれます。フィールドの仕様とバリデーションがセットになっているので視認性もよく使い勝手はいいです。ただ、使い勝手が良すぎるせいで、ある程度約束を守ってもらわないと設定がバラバラになってしまうという問題もあります。

@Validatedはなに?

カンタンにいうと、@Validを拡張したものが、SpringFramework内にあります。拡張の主なポイントはグループの指定です。

グループの指定とは?

とあるユースケースで、バリデーションを指定したいフィールドを絞りたいことがあるかと思います。1つのデータクラスにリクエストパラメータを入れるのですが、入力フォームが多段になっていたり、簡易フォームと詳細フォームでわかれていたりとあると思います。そういうユースケースに効果を発揮します。

実装

下記のような実装があります。simpleのエンドポイントは、 idnameは必須、detailの方のエンドポイントは、simpleのエンドポイントに加えて、agenickNameが必須になります。groupsという属性を定義することによって、バリデーションをどこまで行うかの識別を行っています。

@RestController
@RequestMapping("valid")
public class ValidController {

    @GetMapping("simple")
    public Form simple(@Validated(value = {SimpleForm.class}) Form form) {
        return form;
    }

    @GetMapping("detail")
    public Form detail(@Validated(value = {SimpleForm.class, DetailForm.class}) Form form) {
        return form;
    }

    @Data
    static class Form {
        @NotNull(groups = {SimpleForm.class})
        private Integer id;
        @NotBlank(groups = {SimpleForm.class})
        private String name;

        @NotNull(groups = {DetailForm.class})
        private Integer age;
        @NotBlank(groups = {DetailForm.class})
        private String nickName;

    }
}

simpleのエンドポイント

@Validated(value = {SimpleForm.class}) Form form が引数に定義されることによって、Formクラスのフィールドにgroups = {SimpleForm.class}アノテーションが付与されているところだけバリデーションが実行されます。

$ curl "http://localhost:8080/valid/simple?id=1&name=sample"

{"id":1,"name":"sample","age":null,"nickName":null}

detailのエンドポイント

simpleのエンドポイントをdetailに変えて、クエリパラメータはそのままに実行してみます。

$ curl "http://localhost:8080/valid/detail?id=1&name=sample"

{"timestamp":"2021-04-29T15:17:56.903+00:00","status":400,"error":"Bad Request","trace":"org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'form' on field 'age': rejected value [null]; codes [NotNull.form.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [form.age,age]; arguments []; default message [age]]; default message [null は許可されていません] ....

上記のようにエラーがバリデーションエラーが発生しています。これは先程のsimpleのエンドポイントとは異なるバリデーション設定がされています。@Validated(value = {SimpleForm.class, DetailForm.class}) Form formのバリデーション設定で、DetailForm.classが追加されています。こちらが追加されている為、 Formクラスに定義してあるとおり、agenickName の定義が必須になっています。

さいごに

@Validatedについてカンタンに説明させていただきました。気をつけねばならないのが、 groupsをつけると@Validは動作しなくなるというのがあります。ただ、@ValidatedはSpring依存になってしまうので、早いところ共通仕様をだしてほしいところであります

AWS RDSのダウンサイジングとオートスケーリング

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

今回はAWS上のRDSのダウンサイジングに加えAurora Auto Scalingを設定しコストダウンを測った話と、その際の注意点や挙動についてご紹介させていただきます。

また、今回はGUI上での設定についてお話させていただきます。

DBインスタンスの追加・削除

現行で既に動いてるRDSを触る場合、インスタンスサイズを変更すると変更中はそのインスタンスでリクエストを捌くことはできません。

そのため、一時的に大きめのインスタンスを先に作って作業すると良いです。

f:id:excite-at-ma:20210428152508p:plain 1段階〜2段階上のサイズにしておくと安心です。

クラスターの設定に合わせてリーダーを作ってくれるため、一時的なリーダーの名前を決めて設定します。

f:id:excite-at-ma:20210428153802p:plain 便利なことに、自動で異なるAZ配置に振り分けてくれます。

書き込みインスタンスを変える時は、事前にフェイルオーバーで読み取りインスタンスに降格させてインスタンスの変更をすると管理しやすいです。

f:id:excite-at-ma:20210428152957p:plain フェイルオーバーの優先順位は各インスタンスの追加設定から変更できます。

オートスケールの設定

オートスケールは、オートスケールを設定したいクラスターを選択し、[アクション]=>[レプリカのAuto Scallingの追加]で設定できます。

f:id:excite-at-ma:20210428154135p:plain

指定のターゲット値を基にインスタンスの台数が増減していきます。 CPU以外にも、DBの接続数を基にスケールイン・アウトを行うことができます。

今回のプロジェクトの場合、比較的早くアクセスが増えてしばらく高負荷の状態が続くケースが多かったです。

そのため、時間はデフォルトの300秒ではなくスケールイン60秒、スケールダウンは少し長めに300秒に設定しています

f:id:excite-at-ma:20210427164118p:plain 実際にオートスケールを設定すると上記のようにapplication-autoscalling-** といったインスタンスが増えます。

アクセスが増えたことにより、2台のインスタンスが立ち上がり大量の負荷で落ちないようになっています。

また、オートスケールの設定自体はクラスタの[ログとイベント]内の[AutoScalling ポリシー]からいつでも変更可能です。

f:id:excite-at-ma:20210428154716p:plain

障害時のフェイルオーバーに対応する

障害などでフェイルオーバーが起きた際、書き込みのインスタンスと読み込みのインスタンスが入れ替わってしまうことがあります。

f:id:excite-at-ma:20210427165908p:plain
入れ替わりの例
上記のように青が読み込み、赤が書き込みだったものが、ある時を境にフェイルオーバーが起きて入れ替わっています。

こうなると書き込みと読み込みのサイズに差があるとリクエストが捌き切れずにパンクしてしまう恐れがあります。

そのため、基本的にはmasterとreadのインスタンスサイズは同じにしておくのが望ましいです。

終わりに

今回のスケールダウンでRDSコストを当初の半分以下にまで抑えることができました。

ただしあまりにもギリギリに調整しすぎると、万が一の障害が起こった際や想定外のアクセスがきた場合に対処が難しくなるため、サイズにはある程度余裕は持たせましょう。

また、サービスにもよりますがオートスケールのターゲット値も50~60%程度が一番汎用的に使えそうです。

可用性を保ったまま低コストの運用を実現するため、サービスに合わせてうまく負荷分散できるようにチューニングしていくことが重要です。

AWSとGitHub Actionsでデプロイをカイゼン

エキサイトのみーです。

アプリケーションのリリースサイクルを速めるためにも、デプロイの改善は重要な要素の1つだと考えています。
今回は、アプリケーションのコンテナ化に際してデプロイをどのように改善させたのか、について紹介します。

これまで

オンプレにおける従来のデプロイは非常に面倒で、ミスしてくれと言わんばかりのものでした。箇条書きにすると、

  • GitHubでPull Requestして、
  • レビューして、
  • mainブランチにマージして、
  • 踏み台サーバにログインして、
  • デプロイサーバにログインして、
  • シェルスクリプト(rsync)を叩く

というような手順。酷いときは、mainブランチにマージしたけどデプロイを忘れる、なんてことも。
mainブランチと本番環境にズレが生じる、みたいなあり得ない状況も度々発生。
そもそもの作業が億劫なのでデプロイ頻度が下がっていく、となるのも当然の結果でした。

アプリケーションをコンテナ化してそれをデプロイするとなると、同じようなやり方では破綻してしまいます。
ということで、より簡単で、より安全なデプロイ方法を検討することになったわけです。

コンテナ化後

試行錯誤の結果、今では以下のような手順に落ち着いています。

  • GitHubでPull Requestして、
  • レビューして、
  • mainブランチにマージされたら、GitHub Actionsで自動デプロイ

これにより、デプロイ作業がGitHubだけでほぼ完結するようになりました。手数も減って、デプロイサーバの管理も不要になって、まさに一石二鳥。
さらにBlue/Greenデプロイも取り入れたことで、より安全にアプリケーションをデプロイできるようになりました。
ここまで来てしまうと、もう昔には戻れません。

構成

大まかな全体構成は以下のような図になっています。今回は左半分の話になりますね。
特別なことは何もしていません。コントロールプレーンはECS、データプレーンはFargate、イメージはECRで管理しています。

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

GitHub Actions

アクションの作成は難しくはないので、全て自作しても良いとは思います。
が、今回はAWSのサービスをフル活用しているので、AWSが公開しているアクションを有効活用することができました。

# GitHub Secretsに登録したアクセスキーなどをセット
- name: Configure AWS credentials
    id: configure-credentials
    uses: aws-actions/configure-aws-credentials@v1
    with:
        aws-access-key-id: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

# ECRにログイン
- name: Login to Amazon ECR
    id: login-ecr
    uses: aws-actions/amazon-ecr-login@v1

# イメージをビルドして、ECRへPush
- name: Build, tag, and push image to Amazon ECR
    id: build-image
    env:
        DOCKER_BUILDKIT: 1
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        IMAGE_TAG: ${{ github.sha }}
    run: |
        docker build -t $ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG .
        docker push $ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG
        echo "::set-output name=image::$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG"

# タスク定義に、先ほどPushしたイメージを反映
- name: Render Amazon ECS task definition
    id: render-web-container
    uses: aws-actions/amazon-ecs-render-task-definition@v1
    with:
        task-definition: task-definition.json
        container-name: app
        image: ${{ steps.build-image.outputs.image }}

# CodeDeployを使用してECSにデプロイ
- name: Deploy to Amazon ECS
    id: deploy
    uses: aws-actions/amazon-ecs-deploy-task-definition@v1
    with:
        task-definition: ${{ steps.render-web-container.outputs.task-definition }}
        service: ${{ env.ECS_SERVICE_NAME }}
        cluster: ${{ env.ECS_CLUSTER_NAME }} 
        wait-for-service-stability: false
        codedeploy-appspec: appspec.yaml
        codedeploy-application: ${{ env.CODEDEPLOY_APPLICATION }}
        codedeploy-deployment-group: ${{ env.CODEDEPLOY_DEPLOYMENT_GROUP }}

aws-actions/amazon-ecs-deploy-task-definitionは、Blue/Greenデプロイにも対応しています。事前にCodeDeployアプリケーションやデプロイメントグループを作成しておくだけで、簡単にBlue/Greenデプロイができるようになります。

このCodeDeployを使用したBlue/Greenデプロイが、私たちに最高にクールな体験を与えてくれているのですが、それはまた別の機会に。

残課題

いろいろと改善できたものの、まだまだ課題は山積みです。

GitHub Actionsで使用するIAMユーザ

GitHub ActionsのワークフローからAWSリソースへアクセスするため、事前にIAMユーザを作成してアクセスキーをGitHubのSecretsに登録しておく必要があります。

IAMユーザには必要な権限のみを付与しているものの、現在はアクセスキーのローテーションをしていないのでセキュリティ的には微妙です。
万が一に備えて、ローテーションの自動化等を検討しておくべきかと思われます。

Pull Requestへのコメントでデプロイ

開発中はもっと簡単にデプロイしたいものです。
そこで、テスト環境などはPull Requestにコメントすることでデプロイされるよう、GitHub Actionsのワークフローを設定しています。

on:
    # コメントが作成・編集されたときに発火
    issue_comment:
        types: [created, edited]

jobs:
    deploy:
        # Pull Request内のコメントが対象、且つ、コメントの先頭に「/test」と入力された場合のみ
        if: contains(github.event.comment.html_url, '/pull/') && startsWith(github.event.comment.body, '/test')

/testとコメントすればテスト環境へデプロイされちゃいます。すごくお手軽。
ただし、どのPull Requestからもデプロイできてしまうという問題も。チーム開発時は事前にルールを決めておくのが良さそうです。

CodePipelineは使わないの?

GitHubだけで完結したかった、ので使っていません。
また、テスト環境へのデプロイを手軽にやりたかったということもあり、GitHub Actionsのほうが適していると判断しました。

おわりに

特に目新しいことはしておらず、公式ドキュメントなどに記載されていることを愚直に実践しただけに過ぎません。
ですが、普通のことを普通に実行するだけで多くの恩恵を得られたのも事実です。カイゼンのポイントはそこかしこに眠っているはず。

クラウドへの移行は大変な作業ですが、クラウドのメリットを活かした構築をすることで、生産性の向上に大きく寄与できると思っています。
この記事が、クラウド移行を検討されている方にとって少しでも参考になれば幸いです。