RedisCacheConfigの設定

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

SQL Serverからデータを取得し、redisにデータをキャッシュさせようとしたら以下のエラーが出ました。

Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\" to enable handling

どうやらdatatype-jsr310が効いていないようです。

https://github.com/FasterXML/jackson-databind/issues/2983

issuesでもあったようですね。

objectMapperをカスタマイズする

redisとのやり取りの部分をobjectMapperの設定を追加してやります。

ポイントは、redisにキャッシュする際のデータの書き読み込みする際のobjectMapperの設定をGenericJackson2JsonRedisSerializerに設定しているところです。 SerializationFeature.WRITE_DATES_AS_TIMESTAMPSをすることでISO8601 形式にシリアライズします。

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * ElastiCacheではconfigコマンドの実行が禁止されているため、「ConfigureRedisAction」を「NO_OP」に設定しておく
     *
     * @return ConfigureRedisAction
     */
    @Bean
    public ConfigureRedisAction configureRedisAction() {
        return ConfigureRedisAction.NO_OP;
    }

    /**
     * Redisの設定
     *
     * @param factory
     * @return ReactiveRedisTemplate
     */
    @Bean
    public ReactiveRedisTemplate reactiveRedisTemplate(LettuceConnectionFactory factory) {
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(this.objectMapper());
        RedisSerializationContext.RedisSerializationContextBuilder<String, Object> builder =
                RedisSerializationContext.newSerializationContext(keySerializer);
        RedisSerializationContext<String, Object> context = builder.value(valueSerializer).build();
        return new ReactiveRedisTemplate<>(factory, context);
    }

    /**
     * Redisの中に平文でデータを保存するための設定
     *
     * @return RedisCacheManagerBuilder
     */
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return builder -> {
            var map = Arrays.stream(CacheKeyType.values())
                    .collect(
                            Collectors.toMap(
                                    e -> e.getKey()
                                    , e -> RedisCacheConfiguration
                                            .defaultCacheConfig()
                                            .entryTtl(Duration.ofSeconds(e.getTtl()))
                                            .disableCachingNullValues()
                                            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                                            .serializeValuesWith(RedisSerializationContext.SerializationPair
                                                    .fromSerializer(new GenericJackson2JsonRedisSerializer(this.objectMapper())))
                            ));
            builder.withInitialCacheConfigurations(map);
        };
    }

    /**
     * redisにキャッシュする際のデータの書き読み込みする際のobjectMapperの設定
     *
     * LocalDateTime などの Date and Time API 関連のフィールドを扱う
     * 不明なプロパティがあっても無視
     * オブジェクト情報を追加する
     *
     * @return
     */
    private ObjectMapper objectMapper() {
        return new ObjectMapper()
                .registerModule(new JavaTimeModule())
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
                                .allowIfBaseType(Object.class)
                                .build(),
                        ObjectMapper.DefaultTyping.EVERYTHING)
                ;
    }

    /**
     * キャッシュからのデシリアライズに失敗した場合は、
     * エラーにはせずに対象のメソッドをそのまま呼ぶ
     * その結果はキャッシュされる
     *
     * @return
     */
    @Override
    @Bean
    public CacheErrorHandler errorHandler() {

        return new SimpleCacheErrorHandler() {

            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                if (isSerializationError(exception)) {
                    return;
                }

                throw exception;
            }

            private boolean isSerializationError(RuntimeException exception) {
                if (exception instanceof SerializationException) {
                    return true;
                }

                Throwable cause = exception.getCause();
                return (cause != null && cause instanceof SerializationException);
            }
        };
    }
}

設定を追加したら以下のようにキャッシュされるようになりました。

[
    "クラス名",
    {
        "id": "0000000",
        "longValue": [
            "java.lang.Long",
            7261626
        ],
        "date": [
            "java.time.LocalDateTime",
            "2021-05-25T06:17:30.743"
        ],

Android 12におけるスプラッシュスクリーン

こんにちは。エキサイト株式会社 Androidエンジニアの克です。

Androidアプリでは、起動時に最初の画面が表示されるまで多少の時間がかかり、真っ白の画面がしばらく続いてしまうということがよくあります。

そのため、最初の画面が表示されるまでアプリのロゴを表示しておくというのはよく使われる手法です。(以下スプラッシュスクリーンと呼称)

Android 12ではこのスプラッシュスクリーンにより拡張性が加わりましたが、注意しなければならない点もあったので紹介させていただこうと思います。

※ 今回紹介する内容は Android 12 Beta におけるものです。正式版では仕様が変わっている可能性もあるため詳細については公式のドキュメントを参照してください。

Android 7までの実装

公式で推奨しているようなやり方は特にはありませんでしたが、概ね下記のようにテーマの差し替えという形で実装されてきました。

<!-- theme.xml -->
<resources>

    <!-- 通常のテーマ -->
    <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar" />

    <!-- スプラッシュスクリーンのテーマ -->
    <style name="AppTheme.Splash">
        <item name="android:windowBackground">@drawable/splash_screen</item>
    </style>

</resources>
<!-- splash_screen.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">

    <!-- 画像をそのまま使うと全体に引き伸ばされてしまうので、layer-listを使う -->

    <item>
        <color android:color="#FFF" />
    </item>

    <item>
        <bitmap
            android:gravity="center"
            android:src="@drawable/splash_image" />
    </item>

</layer-list>
<application
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme.Splash"> <!-- スプラッシュスクリーンのテーマを指定  -->
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>
// MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // スプラッシュスクリーンのテーマから通常のテーマに戻す
        setTheme(R.style.AppTheme)

        setContentView(R.layout.activity_main)
    }
}

こうすることで最初の画面が立ち上がるまではスプラッシュスクリーンが表示され、立ち上がった後は通常の表示に切り替わります。

Android 8からの実装

Android 8からは、スプラッシュスクリーン用のAPIとして windowSplashscreenContentが追加されました。

こちらをテーマに設定することで、テーマの切り替えをしなくても自動でスプラッシュスクリーンの表示と非表示をやってくれます。

<resources>

    <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="android:windowSplashscreenContent">@drawable/splash_screen</item>
    </style>

</resources>

Android 12からの実装

Android 12の端末で起動されたアプリは、なんとデフォルトでアプリアイコンがスプラッシュスクリーンとして表示されます。

そのためアプリアイコンをそのままスプラッシュスクリーンに表示するだけの場合は実装は不要です。

別の画像を表示したい場合はAndroid 8からの場合と同様に windowSplashscreenContentを使用します。

Android 12では、他にもSplash screensという形で専用のAPI群が追加され、スプラッシュスクリーンの細かい表示内容や表示時間などのカスタマイズが可能になりました。 developer.android.com

Android 12での注意点

デフォルトでスプラッシュスクリーンが表示されることは喜ばしいことでもありますが、注意点として windowBackgroundで画像を表示していた場合については無視されます。

また、テーマの設定ではなくスプラッシュスクリーン用の画面を別途用意していた場合、スプラッシュスクリーンが2回表示されてしまうことにも繋がります。

さらに、これはtargetSdkVersionAndroid 12未満に設定されている場合でも適用されます。

よってスプラッシュスクリーンを自前で実装しているアプリについては、Android 12の正式版がリリースされる前に動作を確認しておいたほうがよいでしょう。

まとめ

昔はスプラッシュスクリーンの実装方法も統一されておらず、無駄に長い時間ユーザーを拘束してしまうような実装になっている場合もありました。

OSが新しくなるにつれて画一された実装方式になり、よりシンプルで拡張性のある表現をすることができるようになってきています。

スプラッシュスクリーンはブランドイメージを強調するという側面もあるかもしれませんが、ユーザーはロゴを見るためにアプリを起動するのではないということを念頭に置き、使いやすいアプリにしていきたいですね。

🎉Twitterアカウントを開設しました!🎉

f:id:excitech:20210526151104p:plain

お知らせ

こんにちは。excite新卒デザイナーの山﨑です。

この度excitechという技術発信のTwitterアカウントを開設しました!

twitter.com

じゃ〜ん!

f:id:excitech:20210526143256p:plain

アイコンとヘッダーは内波さんという直属のメンターさんが製作してくれました🙏めちゃくちゃかっこいいです!

個人的にロゴの元になったAvenirという書体が大好きなので、すごく嬉しいです〜!Avenirは最強💪

こちらのテックブログの更新情報や、イベントについても随時発信していく予定なので、是非フォローしてみてください!

それでは!

Javaのメソッドの返り値に、オブジェクトを使うべきかインターフェースを使うべきか

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

Javaで配列を扱う時、すべての配列のインターフェースである List と、具体的に実装したオブジェクトである ArrayListLinkedList 等があります。 メソッドの返り値で配列を返したいときはこれらのどれでも使うことが出来ますが、実際にどれを使うべきなのでしょうか?

今回は、メソッドの返り値にインターフェースを使うべきか、それともオブジェクトを使うべきかについて説明していきます。

Javaの配列のインターフェースとオブジェクト

例えばPHPを使っている人にとっては、配列はそれ以上でもそれ以下でもありません。 ですが実はJavaには、「配列」と一口に言っても内部の保存方法によって種類が存在します。

例えば ArrayListLinkedList という形式がありますが、保存方法の違いにより、 ArrayList はランダムアクセスに強く、 LinkedList は要素の追加・削除に強いという特長があります。 そのため、その配列に対してどんな操作をしたいかによって、使う配列の形式を使い分けるのが良いでしょう。

そして、それらはすべて List というインターフェースを実装したものです。

さて、以上のことを踏まえると、配列を返すメソッドを考える時、ざっくり以下の2通りがあることがわかります。

// 返り値が、インターフェースである List
public List<String> getList() {
    return new ArrayList<String>();
}
// 返り値が、オブジェクトである ArrayList
public ArrayList<String> getList() {
    return new ArrayList<String>();
}

どちらでもコードとしては正しく動きますが、どちらを使うべきなのでしょうか? 実はこれは常に正しい方法が決まっているわけではなく、状況次第で変わってきます。

返り値の決定方法

まずは、それぞれのメリットを見ていきます。

インターフェースを使うメリット

  • 各オブジェクトそれぞれに固有のメソッドを使えなくなるので、配列オブジェクトを別の種類に変更しやすい
  • 空配列を使う場合のみに使用する emptyList 等が併用できる

オブジェクトを使うメリット

  • 各オブジェクトに固有のメソッドが使える
  • 使用するオブジェクトの種類を固定できる

以上のことから、まず各オブジェクトに固有のメソッド(例えば、 ArrayList にしか存在しないメソッド)を使いたい場合は、必ず返り値はオブジェクトにする必要があります。 また、パフォーマンス上どうしても使用するオブジェクトを固定する必要がある場合は、返り値をオブジェクトにしてもいいかもしれません。

それ以外の場合は、コードの柔軟性を考えると基本的にはインターフェースを使うのが良いかと思います。 また、仮にパフォーマンス上オブジェクトを固定する場合でも、メソッドの内部で負荷の高い処理が完結する場合や、よほどコードを固くする(ルールをがっちり決める)必要がない限りは返り値はインターフェースで十分なのではないでしょうか。 多くの場合は、コードの柔軟性確保のほうが全体としてメリットが上がる場合が多いです。

まとめ

どうしてもオブジェクトを使わなければならない状況以外では、コード柔軟性のためにインターフェースを使うほうが良いでしょう。

こうしたインターフェースとオブジェクトの仕組みは、配列だけでなくマップ(インターフェースの Map とオブジェクトの HashMap )等にもありますので、そういった状況でも同じ考え方が通用するはずです。

restTemplateでElasticsearchに問い合わせる

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

rest-high-level-clientのようなライブラリではなく、restTmeplateでElasticsearchに問い合わせる方法を説明します。

https://mvnrepository.com/artifact/org.elasticsearch.client/elasticsearch-rest-high-level-client

使う場面として、Elasticsearch5系のバージョン以下の場合、Elasticsearchで使っているオプションがライブラリで扱われていない(auto_generate_synonyms_phrase_query)などあるので、

DAOとして、以下のようにJsonのNodeを渡します。

@Component
@RequiredArgsConstructor
public class ElasticSearchDaoImpl implements ElasticsearchDao {

    @Value("${spring.elasticsearch.url}")
    public String url;

    private final RestTemplate restTemplate;

    private final ObjectMapper objectMapper;

    @Override
    public JsonNode searchRequest(JsonNode jsonNode) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity = new HttpEntity<String>(jsonNode.toString(), headers);
        ResponseEntity<String> exchange =
                restTemplate.exchange(this.url, HttpMethod.POST, entity, String.class);

        if (exchange.getStatusCode() != HttpStatus.OK) {
            throw new ElasticsearchException(exchange.getBody());
        }
        try {
            return objectMapper.readTree(exchange.getBody());
        } catch (JsonProcessingException e) {
            throw new ElasticsearchException(e.getMessage());
        }
    }
}

どんなJsonNodeを渡すかというと、以下のようにElasticsearchに問い合わせるのに必要そうな値をセットするデータクラスを用意し、SearchSourceBuilder.toStringでstringにした後、objectMapperでJsonNode化します。

JsonNode jsonNode = objectMapper.readTree(elasticSearchForm.toString());
@Data
@Accessors(chain = true)
public class ElasticSearchForm {

    /**
     * Elasticsearchから取得するインデックス
     */
    private String index;

    /**
     * Elasticsearchから取得するカラム
     */
    private String[] includes;

    /**
     * Elasticsearchから取得しないカラム
     */
    private String[] excludes;

    /**
     * 検索条件
     */
    private BoolQueryBuilder boolQueryBuilder;

    /**
     * offset
     */
    private int from = 0;

    /**
     * limit
     */
    private int size = 10;

    /**
     * order
     */
    private FieldSortBuilder fieldSortBuilder;

    public String toString(){
        return new SearchSourceBuilder()
                .fetchSource(
                        includes,
                        excludes)
                .query(boolQueryBuilder)
                .from(from)
                .size(size)
                .sort(fieldSortBuilder)
                .toString();
    }
}

JsonNode化すれば、不要なところは以下のように消すことができます。

※本当はもっとネストしていると思いますが、簡略しています

((ObjectNode) jsonNode.get("query").........remove("auto_generate_synonyms_phrase_query");

あとは、このJsonNodeを先程のDAOの引数に入れるだけです。

レスポンスについて、以下のようにstreamを使えば、必要なデータのListが作れると思います。 他に必要なデータは適宜JsonNodeから取得してください。

                    StreamSupport.stream(
                            response.get("hits").get("hits").spliterator(), false)
                            ).map(e -> {
                                 // 処理
                                 }
                            )
                            .collect(Collectors.toList());

Elasticsearchに問い合わせるJsonはネストが深く、文字列結合やMapだとどうしても見通しが悪くなる場合があります。

Builderを使えるところは使って、それをjsonに変換した方が見通しがよくなると思うので、よかったら使ってください。

Javaのカスタムバリデーションで2つ以上のプロパティをチェックするアノテーション

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

現在、SpringBootで2つ以上のプロパティをチェックするカスタムバリデーションを共有します。

アノテーションの定義

下記のようにカスタムアノテーションを定義します。 (@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class) ここの部分はエラーになります)

@Documented
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class)
public @interface NotBlankAny {
    String message() default "please {fields} not empty.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] fields();
}

バリデーション定義

次にバリデーション部分を定義します。

    class NotBlankAnyValidator implements ConstraintValidator<NotBlankAny,Object> {

        private String[] fields;

        @Override
        public void initialize(NotBlankAny constraintAnnotation) {
            this.fields = constraintAnnotation.fields();  // ※1
        }

        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (Objects.isNull(value)) {
                return false;
            }
            BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value);
            return Stream.of(fields).allMatch(e -> StringUtils.isNotBlank((String)beanWrapper.getPropertyValue(e))); //  ※2
        }
    }

※1アノテーション内で指定されたフィールドの文字列が配列で入ってきます。※2で一つずつ取り出してチェックします。

使い方

対象のデータクラスに、 @NotBlankAny(fields = {"フィールド名1","フィールド名2"}) を定義すると、そのデータを使ってバリデーションがかかります。

@RestController
@RequestMapping
@RequiredArgsConstructor
@Slf4j
public class DemoController {

    @GetMapping
    public Mono index(@Valid Form form) {
        return Mono.defer(() -> Mono.just(form));
    }

    @Data
    @NotBlankAny(fields = {"firstName","lastName"})  // こんな感じで設定します
    static class Form {
        private String firstName;
        private String lastName;
    }
}

これでチェックができるので、再利用性はかなり高いバリデーションができると思います。

最後に

フィールド複数のバリデーションを使いたいときって結構あると思います。メールアドレスの再入力とか、パスワードを同じもの2つ入れるとか。実際にチェックのところは、一致や異なるものやどれかひとつみたいなものは、バリデーション側での実装でバリエーションは増やせると思います。

宣伝

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

www.wantedly.com

おまけ

全体のコード

@Documented
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class)
public @interface NotBlankAny {
    String message() default "One of {fields} must not be empty";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] fields();

    class NotBlankAnyValidator implements ConstraintValidator<NotBlankAny,Object> {

        private String[] fields;

        @Override
        public void initialize(NotBlankAny constraintAnnotation) {
            this.fields = constraintAnnotation.fields();
        }

        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (Objects.isNull(value)) {
                return false;
            }
            BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value);
            return Stream.of(fields).allMatch(e -> StringUtils.isNotBlank((String) beanWrapper.getPropertyValue(e)));
        }
    }
}

SQL Serverのwith(NOLOCK)の挙動について

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

最近はSQL Serverを使うことが多いので、SQL Serverでよく使われているwith(NOLOCK)の挙動について説明できたらと思います。

with(NOLOCK)とは

簡単にいうと、トランザクションレベルを強制的にREAD UNCOMMITTEDな状態でselect文が使えます。

。。

。。。

難しいですね。

図で説明しましょう。

デフォルトのトランザクションレベル

SQL Server以外のMySQL,PostgresもデフォルトはREAD COMMITTEDと呼ばれる、トランザクションAの中でCUDを発行しても、別のトランザクションからはトランザクションAの変更内容が見えない状態です。

以下の図のように、ID=3のレコードの内容を取得しようとすると、トランザクション開始前の松本一郎が取得できます。

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

しかし、SQL ServerのREAD COMMITTEDはちょっと違います。

READ COMMITTEDにスナップショットという概念が

SQL ServerのREAD COMMITTEDは行のバージョン管理がされていません。 つまり、トランザクションA内でCUDを発行すると、ほかのトランザクションから変更内容は見えないのですが、変更内容をコミット(or ロールバック)するまで待機してしまいます(行ロック)

以下の図のように、ID=3のレコードの内容を取得しようとすると、トランザクション開始しているレコードを取得する場合、コミット(or ロールバック)するまで、名前が取得できません。

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

これを解消するためにはスナップショットを有効にしないといけません。

スナップショットとは、行ごとのバージョン管理をするという意味です。

以下の図のように、ID=3のレコードの更新前のデータをテンポラリに退避しており、ほかのトランザクションから問い合わせをされても、その退避内容を返します。

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

行ごとのバージョン管理をするということは、もちろんtempdbのサイズが大きくなるので、

これからスナップショットを有効にする場合は、パフォーマンスに気をつけてください。

以下のalter文のどちらかを発行することで有効になります。

ALTER DATABASE MyDatabase  
SET ALLOW_SNAPSHOT_ISOLATION ON  

→オンライン中に実行可能で、select文を使う前に、SET TRANSACTION ISOLATION LEVEL SNAPSHOT を実行することで、スナップショットが使えるようになります。

ALTER DATABASE MyDatabase  
SET READ_COMMITTED_SNAPSHOT ON  

→オンライン中に実行不可能ですが、一度実行すると、全てのクエリでスナップショットが有効になります。

もしかしたら、SQL Server以外のRDBはデフォルトでスナップショットという概念で行ごとのバージョン管理をしているかもしれません。

詳細は以下のURLを参考にしてください。

SQL Server でのスナップショット分離 - SQL Server | Microsoft Docs

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