Micrometerでメトリクスを収集してAmazon Cloud Watchで可視化する

仕事でMicrometer使う機会が出来たので触ってみました。

Micrometerとは?

Micrometerとは、Pivotal社が作っているJVMベースのアプリケーションのためのメトリクスライブラリです。メトリクスの収集とモニタリングシステムへの通知を行います。 メトリクスの収集(micrometer-core)とモニタリングシステムへの通知(micrometer-registry-xxx)が切り離されているので、モニタリングシステムに依存しません。

Micrometerを使う上で押さえておくべきクラス

  • Meter メトリクスの集合を表すクラス。収集方法ごとにクラスがあります。主に使うCounter、Gauge、Timerについては少し詳しく書きます。(Timer, Counter, Gauge, DistributionSummary, LongTaskTimer, FunctionCounter, FunctionTimer, TimeGauge)
    • Counter 固定値が増えていくような計測に使う。
    • Gauge 現在の値の計測に使う。実行中のスレッド数を計測するときなど。
    • Timer イベントのレイテンシや頻度の計測に使う。指定期間内の合計時間やイベント数を計測するときなど。
  • MeterRegistry Meterを管理するクラス。モニタリングシステムごとに実装があります。
  • MeterBinder メトリクスを収集する方法を登録するクラス。収集対象ごとにクラスがあります。

ネーミングルール

Micrometerのメトリック名は小文字でかつドットつなぎでつけます。(例:jvm.memory.used) モニタリングシステムに送る際にMicrometer側でそれぞれのモニタリングシステムのネーミングルールに変換して送ります。

Prometheus - http_server_requests_duration_seconds
Atlas - httpServerRequests
Graphite - http.server.requests
InfluxDB - http_server_requests

Spring BootでMicrometerを使う

Spring Boot2.0からメトリクスAPIを提供しなくなりました。代わりにMicrometerを使うようになりました。Actuatorを依存関係に追加するだけでMicrometerが使えるようになります。 Spring Boot 2.0 リリースノート Spring Boot 1.5.xではMicrometerを依存関係に追加すれば使えます。

デフォルトで使えるモニタリングシステム

Runtime時にこれらの全モニタリングシステムの設定をAutoConfigurationで設定します。

  • Atlas
  • Datadog
  • Ganglia
  • Graphite
  • Influx
  • JMX
  • New Relic
  • Prometheus
  • SignalFx
  • StatsD
  • Wavefront

その他の対応しているモニタリングシステム

  • AppOptics
  • Azure Monitor
  • Cloud Watch
  • Dynatrace
  • Elastic
  • Humio
  • Kairos
  • Stackdriver

サポートしているメトリクス

デフォルトだと /actuator/metrics を叩くと利用可能なメータを確認することができます。

メトリクスを収集してAmazon Cloud Watchに送る

今回の技術スタックはこんな感じです。

Spring Boot 2.1.3
Amazon Cloud Watch

依存関係を追加

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.cloud:spring-cloud-starter-aws'
    implementation 'io.micrometer:micrometer-registry-cloudwatch'
}

設定を追加

management:
  metrics:
    use-global-registry: false
    export:
      cloudwatch:
        namespace: Micrometer/test
        enable: true
        step: 1s
cloud:
  aws:
    stack:
      auto: false
    region:
      auto: false
      static: ap-northeast-1

management.metrics.use-global-registry=false でデフォルトで使える全モニタリングシステムを無効にします。management.metrics.export.cloudwatch.namespace=Micrometer/test ネームスペースを Micrometer/test にします。management.metrics.export.cloudwatch.enable=true MicrometerでCloud Watchを使うための設定をAutoConfigurationで設定してもらいます。management.metrics.export.cloudwatch.step=1s メトリクスの収集間隔を1秒にします。

cloud.aws.* は、spring-cloud-awsを使っているアプリケーションをローカルで起動するための設定です。spring-cloud-awsは、S3やCloudFormationなど様々なサービスと連携します。AWS環境でアプリケーションが起動していない場合、Exceptionを投げれられてアプリケーションが起動しません。cloud.aws.stack.auto=false CloudFormationとの連携を無効にする。cloud.aws.region.auto=falsecloud.aws.region.static=ap-northeast-1 EC2のメタデータからリージョンを自動で取得せずに ap-northeast-1 をリージョンに指定しています。

Elastic Beanstalkにデプロイ

Coineyでは、Elastic Beanstalkを使っているので同様の環境にデプロイします。デフォルトで55個のメトリクスが収集されます。

独自Metricsクラスを実装

management.metrics.enable.xx=false のように書くことで不要なメトリクスを収集しないようにできます。(xxは、メトリクス名のプレフィックスjvmtomcatなどがあります)

今回は、ヒープの使用量のメトリクスを収集する独自Metricsクラスを実装します。

@ConditionalOnAwsCloudEnvironment
@Component
public class HeapMemoryUsageMetrics {
  public HeapMemoryUsageMetrics(MeterRegistry registry) {
    Gauge.builder("HeapMemoryUsage", this, HeapMemoryUsageMetrics::invoke)
      .tag("InstanceId", EC2MetadataUtils.getInstanceId())
      .baseUnit("bytes")
      .register(registry);
  }
  private Long invoke() {
    MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
      return memoryMXBean.getHeapMemoryUsage().getUsed();
  }
}

特に難しいことはしていないです。 AWS環境以外では、このクラスのインスタンスをBean登録する必要がないので、 @ConditionalOnAwsCloudEnvironment をつけて、AWS環境のときのみBean登録します。ちなみに、 @ConditionalOnAwsCloudEnvironment は、EC2のメタデータからインスタンスIDを取得できたらAWS環境とみなしているようです。

先ほどと同様にElastic Beanstalkにデプロイして、 Cloud Watchで確認すると、メトリクスが送られていることが確認できます。

今回実装したコードは、 GitHubにおいてあります。

参考

AssertJ 3.12.0リリースされたので新機能試してみた

AssertJ 3.12.0が2019/2/14にリリースされました。これに伴ってユーザガイドがリニューアルされました。

新機能いくつか試してみました。

ネストしたオブジェクトの検証

AbstractObjectAssert#usingRecursiveComparison() を呼ぶことでネストしたオブジェクトの検証ができます。

@Test
void nestObjectFieldAssertions() {
    Address address1 = new Address("東京都", "千代田区千代田");
    Person person1 = new Person("内立", 20, address1);

    Address address2 = new Address("東京都", "渋谷区東");
    Person person2 = new Person("良介", 20, address2);

    Address address3 = new Address("東京都", null);
    Person person3 = new Person(null, 20, address3);

    // 指定したフィールドを無視する
    Assertions.assertThat(person1).usingRecursiveComparison()
        .ignoringFields("name", "address.following")
        .isEqualTo(person2);
    Assertions.assertThat(person1).usingRecursiveComparison()
        .ignoringFields("name", "address")
        .isEqualTo(person2);

    // nullのフィールドを無視する
    Assertions.assertThat(person3).usingRecursiveComparison()
        .ignoringActualNullFields()
        .isEqualTo(person1);
}

指定した1要素だけのListであることの検証

AbstractIterableAssert#hasOnlyOneElementSatisfying(Consumer elementAssertions) を使う。

@Test
void hasOnlyOneElementSatisfying() {
    List<Brand> brands = List.of(new Brand("ETHOSENS", "Hashimoto", Gender.MAN));
    assertThat(brands)
        .hasOnlyOneElementSatisfying(brand -> {
            assertThat(brand.getName()).isEqualTo("ETHOSENS");
            assertThat(brand.getDesigner()).isEqualTo("Hashimoto");
            assertThat(brand.getGender()).isEqualTo(Gender.MAN);
        });
}

全フィールドがnullであることの検証

AbstratObjectAssert#hasAllNullFieldsOrProperties() を使います。 AbstratObjectAssert#hasAllNullFieldsOrPropertiesExcept(String... propertiesOrFieldsToIgnore) を使うと、無視するフィールドを指定することができます。

@Test
void hasAllNullFieldsOrPropertiesAssertion() {
    Brand ethosens = new Brand(null, null, null);
    Assertions.assertThat(ethosens)
        .hasAllNullFieldsOrProperties();
    Assertions.assertThat(ethosens)
        .hasAllNullFieldsOrPropertiesExcept();

    Brand bedsidedrama = new Brand("bedsidedrama", "Tanita", null);
    // 指定したフィールドは無視される
    Assertions.assertThat(bedsidedrama)
        .hasAllNullFieldsOrPropertiesExcept("name", "designer");
}

Listサイズの検証

@Test
void hasSizeAssertion() {
    Brand stof = new Brand("stof", "Tanita", Gender.MAN);
    Brand bedsidedrama = new Brand("bedsidedrama", "Tanita", Gender.MAN);
    Brand ethosens = new Brand("ETHOSENS", "Hashimoto", Gender.MAN);
    List<Brand> brands = List.of(stof, bedsidedrama, ethosens);

    Assertions.assertThat(brands)
        .hasSizeGreaterThan(1)
        .hasSizeLessThan(4)
        .hasSizeGreaterThanOrEqualTo(3)
        .hasSizeLessThanOrEqualTo(3)
        .hasSizeBetween(1,4);
}

Mapに含まれる全要素が一致することの検証

AbstractMapAssert#containsAllEntriesOf(Map other) を使う。

@Test
void containsExactlyEntriesOfAssertion() {
    Map<Integer, String> map1 = Map.of(1, "TEST1", 2, "TEST2");
    Map<Integer, String> map2 = Map.of(1, "TEST1", 2, "TEST2");
    Assertions.assertThat(map1).containsAllEntriesOf(map2);
}

指定したkeyのみ含まれていること

AbstractMapAssert#containsOnlyKeys(Iterable keys) を使う。

@Test
void containsOnlyKeysAssertion() {
    Map<Integer, String> map1 = Map.of(1, "TEST1", 2, "TEST2");
    Assertions.assertThat(map1).containsOnlyKeys(List.of(1, 2));
}

一番の目玉はネストしたオブジェクトの検証ができるようになったことです!!

TestContainers使ってみないか?

みなさんは、DBアクセスのテストはどう書いていますか?DBをモックにしていますか?実際にDBを用意してテストしていますか? 私は、だいたいH2を用いてテストを書いています。H2を使っているとDDL問題に直面してしまいます。本番ではMySQLやPostgresを使っていると思うので、テストの為にDDLを用意しないといけません。 わざわざ用意するのめんどくさいとか、これって本当にプロダクトコードのテストになっているのか?とかの疑問を持っていました。

そこで、知り合ったのが今回紹介するTestContainersです。 TestContainersは、JUnitのテストをサポートするJavaのライブラリで、Dockerコンテナ上でDBやSelenium web browserなどを起動することができます。

今回紹介しているサンプルで使用している技術は次の通りです。

Java 11
Maven
Spring Boot 2.1.2.RELEASE
Spring Data JPA
MySQL
JUnit 5
TestContainers 1.10.6

TestContainersをとりあえず動かそう

pom.xmlにまず依存関係を追加します。

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.10.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.10.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.10.6</version>
    <scope>test</scope>
</dependency>

HelloTest.java

@Testcontainers
class HelloTest {

    @Container
    private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer();

    @Test
    void test() {
        assertTrue(MY_SQL_CONTAINER.isRunning());
    }
}

これを実行すると、次のログが出てDockerが起動した後にテストが実行されているのが確認できます。

...
00:43:22.656 [main] INFO org.testcontainers.DockerClientFactory - Connected to docker:
  Server Version: 18.09.1
  API Version: 1.39
  Operating System: Docker for Mac
  Total Memory: 1999 MB
...
00:43:35.506 [main] INFO org.testcontainers.DockerClientFactory - Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
        ℹ︎ Checking the system...
        ✔ Docker version should be at least 1.6.0
00:43:35.513 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 3ff9169b21b876bfc317476f8750851c18881855d46b1ed8a3656354a6f6487a,<null>,true,<null>,<null>,<null>,<null>,{df,-P},com.github.dockerjava.core.exec.ExecCreateCmdExec@a8e6492
00:43:35.610 [tc-okhttp-stream-812143047] DEBUG com.github.dockerjava.core.command.ExecStartResultCallback - STDOUT: Filesystem           1024-blocks    Used Available Capacity Mounted on
overlay               65792556   2810704  59610076   5% /
tmpfs                    65536         0     65536   0% /dev
tmpfs                  1023516         0   1023516   0% /sys/fs/cgroup
/dev/sda1             65792556   2810704  59610076   5% /etc/resolv.conf
/dev/sda1             65792556   2810704  59610076   5% /etc/hostname
/dev/sda1             65792556   2810704  59610076   5% /etc/hosts
shm                      65536         0     65536   0% /dev/shm
tmpfs                   204704       568    204136   0% /run/docker.sock
tmpfs                  1023516         0   1023516   0% /proc/acpi
tmpfs                    65536         0     65536   0% /proc/kcore
tmpfs                    65536         0     65536   0% /proc/keys
tmpfs                    65536         0     65536   0% /proc/timer_list
tmpfs                    65536         0     65536   0% /proc/sched_debug
tmpfs                  1023516         0   1023516   0% /sys/firmware
        ✔ Docker environment should have more than 2GB free disk space
...
00:46:59.144 [main] DEBUG 🐳 [mysql:5.7.22] - Starting container: mysql:5.7.22
00:46:59.144 [main] DEBUG 🐳 [mysql:5.7.22] - Trying to start container: mysql:5.7.22
00:46:59.145 [main] DEBUG 🐳 [mysql:5.7.22] - Trying to start container: mysql:5.7.22 (attempt 1/3)
00:46:59.145 [main] DEBUG 🐳 [mysql:5.7.22] - Starting container: mysql:5.7.22
00:46:59.145 [main] INFO 🐳 [mysql:5.7.22] - Creating container for image: mysql:5.7.22
...
00:47:13.432 [ducttape-1] INFO 🐳 [mysql:5.7.22] - Obtained a connection to container (jdbc:mysql://localhost:32769/test)
00:47:13.434 [main] INFO 🐳 [mysql:5.7.22] - Container mysql:5.7.22 started
00:47:13.467 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31,false,com.github.dockerjava.core.exec.InspectContainerCmdExec@77602954
00:47:13.468 [main] DEBUG com.github.dockerjava.core.exec.InspectContainerCmdExec - GET: OkHttpWebTarget(okHttpClient=org.testcontainers.shaded.okhttp3.OkHttpClient@6941827a, baseUrl=http://docker.socket/, path=[/containers/7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31/json], queryParams={})
00:47:13.479 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31,<null>,com.github.dockerjava.core.exec.KillContainerCmdExec@73c60324
00:47:14.088 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31,false,com.github.dockerjava.core.exec.InspectContainerCmdExec@71ae31b0
00:47:14.088 [main] DEBUG com.github.dockerjava.core.exec.InspectContainerCmdExec - GET: OkHttpWebTarget(okHttpClient=org.testcontainers.shaded.okhttp3.OkHttpClient@6941827a, baseUrl=http://docker.socket/, path=[/containers/7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31/json], queryParams={})
00:47:14.099 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31,true,true,com.github.dockerjava.core.exec.RemoveContainerCmdExec@2c7d121c
00:47:14.151 [main] DEBUG org.testcontainers.utility.ResourceReaper - Removed container and associated volume(s): mysql:5.7.22Class transformation time: 0.043640464s for 3142 classes or 1.3889390197326542E-5s per class

ここを見てもらえばわかるのですが、テスト実行前にコンテナが正しく起動しているかチェックします。しかし、チェックするのでテスト終了に時間がかかってしまいます。 testcontainers.propertiesを置いて、checks.disable=true を設定するとこのテスト実行前のチェックが行われなくなります。

Repositoryのテスト書いてみよう

テスト対象のレポジトリクラス(BrandRepository.java

@Repository
public interface BrandRepository extends JpaRepository<Brand, Integer> {
    List<Brand> findAllByGender(Brand.Gender gender);
}

テスト用の設定ファイル(application.properties)

spring.datasource.url=jdbc:tc:mysql:5.7.22://hostname:port/test?TC_MY_CNF=db/mysql_conf_override
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=test
spring.datasource.password=
spring.datasource.sql-script-encoding=utf-8

spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

MySQL文字コードUTF-8に変更したいので、spring.datasource.urlでTC_MY_CNFパラメータでsomepath/mysql_conf_overrideを設定します。somepath/mysql_conf_overrideの下にある*.cnfファイルを読み込んでくれます。 今回は、db/mysql_conf_override/custom.cnfにcharacter-set-server = utf8だけを書きました。

テストクラス(BrandRepositorTest.java

@Testcontainers
@DataJpaTest(excludeAutoConfiguration = AutoConfigureTestDatabase.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BrandRepositoryTest {

    @Autowired
    private BrandRepository brandRepository;

    @Test
    @Sql(scripts = "classpath:/db/migration/brand/initial_data.sql")
    void findByGender() {
            Assertions.assertThat(brandRepository.findAllByGender(Brand.Gender.MAN))
                .extracting(Brand::getName, Brand::getDesigner, Brand::getGender)
                .containsExactly(Tuple.tuple("ETHOSENS", "橋本 唯", Brand.Gender.MAN));
    }
}

@DataJpaTest は、デフォルトでH2を使うので、その設定がされないようにします。 これで起動すると、application.propertiesの設定が使われて、Docker上に起動したMySQLが使われます。

TestContainersを使うことで本番と同じDBを簡単に用意できます。ドキュメントのボリュームは少ないので1時間半もあれば全部読み切ることができます。 ぜひ試してみてください。Go用JS用のTestContainersもあるようです。(未検証)

今回紹介したソースコードは、GitHubに置いています。

「Web API The Good Part」を読んだ

タイトルのとおり、「Web API The Good Parts」を読んでのまとめです。

第1章 Web APIとは何か

  • この本でいうWeb APIは「HTTPプロトコルを利用してネットワーク越しに呼び出すAPI

Web APIを美しく設計する重要性Web APIを美しく設計した方が良い理由は4つある。

  • 使いやすい
  • 変更しやすい
  • 頑強である
  • 恥ずかしくない

使いやすい

APIを利用するのは自分ではないケースが多い。設計によって使いやすさが大きく異なってきて、開発期間や開発者のストレスに関わってくる。

変更しやすい

Webサービスやシステムはどんどん進化していくため、公開当時と全く同じ状態で2、3年運用が続けられることはない。サービスが変化すれば、そのインターフェースであるAPIも変化を余儀なくされる。自分たちと関係ない第三者が利用している場合、APIの仕様を大きく変えたことで動かなくなってしまうという事態は避けたい。特にモバイルアプリケーションの場合、アップデートのタイミングはユーザ次第なので、APIの仕様変更により古いアプリケーションが動かなくなってしまう。APIを美しく設計することの意味の中には、APIの変化をいかに利用者に影響なく行うか、ということも含まれる。

頑強である

APIもWebサイト同様にセキュリティ問題を考慮する必要がある。これに加えてAPIならでは問題もある。こうした問題をきちんと考慮しているAPIが美しいAPIである。

恥ずかしくない

Web APIは開発者が目にするもの。コードや設計によって技術レベルを判断されてしまうことがある。美しくないAPIを実装してしまうと、採用等にも影響が出てくる可能性がある。

第2章 エンドポイントの設計とリクエストの形式

APIエンドポイントの考え方

APIで提供する機能を決めたら、エンドポイントを考えながらAPIを整理していく。Web APIにおけるエンドポイントとは、APIにアクセスするためのURIのことを指す。APIの利用者はこのエンドポイントにアクセスすることで、APIの機能を利用できる。

エンドポイントの基本的な設計良い設計とは、「覚えやすく、どんな機能を持つURIなのかがひと目でわかる」ものである。美しいURIを設計する方法について、一般的に重要な事柄は次の通りです。

  • 短く入力しやすいURI:理解しやすく、覚えやすく、入力間違いが少ない。
  • 人間が読んで理解できるURIURIを見ればそれ以外の情報がなくてもそれが何の目的としたものかがある程度わかる。極力省略形は使わないようにする。「この単語はこういう機能、情報を表す」という共通認識があることが多いので、APIでよく利用されている英単語を利用する。
  • 大文字小文字が混在していないURI:基本はすべて小文字を使う。大文字小文字の混在は、APIをわかりづらく、間違えやすくする。小文字のエンドポイント対して、大文字でアクセスしてきた場合は、404 NotFoundを返すケースが多くなっている。そもそもHTTPにおいてURIは「スキーマとホスト名を除いては大文字と小文字は区別される」とRFC 7230に書いてある。
  • 改造しやすい(Hackableな)URIURIを修正して別のURIにするのが容易であることを意味する。
  • サーバ側のアーキテクチャが反映されていないURI:Webアプリケーションでは、URIがサーバ側のアーキテクチャディレクトリ構造を反映する必要は全くない。アーキテクチャを特定できるURIにしてしまうと、攻撃されやすくなる。
  • ルールが統一されたURI:ルールが統一化されていないとクライアントを実装する場合に混乱を招きやすく、トラブルの温床になる。

HTTPメソッドとエンドポイント

HTTPメソッドは、HTTPでのアクセス時に指定するもので、GET/POSTなどが有名。URIとメソッドの関係は、操作するものと操作方法の関係である。URIAPIにおいて、"操作する対象=リソース"を表すものだとすれば、HTTPメソッドは"何をするか"を表すもの。1つのURIのエンドポイントに異なるメソッドでアクセスすることで、情報を取得するだけでなく、情報を変更したり、削除したり様々な操作を行うようにすることで、リソースとそれをどう扱うかをきちんと分離して扱うことができる。

GETメソッド

情報の取得を表すメソッド。

POSTメソッド

指定したURIに属する新しいリソースを送信する、つまり新しい情報を登録するために利用するメソッド。

PUTメソッド

更新したいリソースのURIそのものを指定し、その内容を書き換えるメソッド。また、PUTは送信するデータでもともとのリソースを完全に上書きするというもの。

DELETEメソッド

リソースの削除を行うメソッド。

PATCHメソッド

一部を変更することを明示したメソッド。PUTが送信したデータでもともとのリソースを置き換えるものであるのに対し、PATCHではその一部だけを更新したい場合に使う。

APIのエンドポイント設計

ここの例であげられているエンドポイントは、「/users」と「/users/:id」の2つ。これらはそれぞれ「ユーザの集合」と「個々のユーザ」を表すエンドポイントである。この2つの概念は、データベースで例えるならテーブル名とレコードの関係だと言える。そしてそのテーブルやレコードに対してどんな処理を行うかを表しているのがHTTPメソッド。エンドポイントを設計する中で注意すべき点

  • 複数形の名詞を利用する:名詞の複数形を使って「リソースの集合」を表している。HTTPのURIがそもそもリソースを表すものである考え方からきている。
  • 利用する単語を気をつける:利用する単語に困ったら複数のAPIを調べてより多く使われている単語を選択する。
  • スペースやエンコードを必要とする文字を使わない:そのエンドポイントがどのようなものかが一目でわからないため、エンドポイントにはパーセントエンコーディングされた文字が入らないようにすべき。
  • 単語をつなげる必要がある場合はハイフンを利用する:Webページで場ではリンクアドレスに下線が引かれることが多く、アンダースコアだとこれと重なってしまうため見づらい、アンダースコアは歴史的にタイプライターで下線を引くためのものなので目的にそぐわないといった話がある。

単語をつなげる際、スネークケース、キャメルケース、スパイナルケース(またはチェインケースやケバブケース)で迷った際はスパイナルケースにしておくのが無難。理由は、URI中のホストはハイフンを許可されているが、アンダースコアは使えず、大文字小文字の区別がなく、ドットは特別な意味を持つため、ホスト名と同じルールでURI全体を統一しようとするとハイフンでつなぐのが最も適していることになるからである。

検索とクエリパラメータの設計

絞り込みのパラメータを実装すると検索を行うことができる。そのときに利用するのがクエリパラメータ。

クエリパラメータをパスの使い分けクエリパラメータに入れるのかパスに入れるのかはURIを設計する上で必要になるが、その判断基準は以下のようにするとよい。

  • 一意なリソースを表すのに必要な情報かどうか:ユーザIDを指定することで参照したい情報が一意に決まるのでパスに入れた方がよい。リソースとは関係ないアクセストークンのようなものはクエリパラメータの方が適している。
  • 省略可能かどうか:リストや検索の際のoffsetやlimitあるいはpageなどのパラメータは省略すればデフォルトの値が利用されるケースが多くなるため、クエリパラメータの方が適している。

第4章 HTTPの仕様を最大限利用する

HTTPの仕様を利用する意義

インターネット上で利用される仕様の多くはRFC(Request for Comments)と呼ばれる仕様書で定義されている。こういった仕様をよく理解することで不本意に独自仕様を入れてしまう危険性が減る。また、標準の仕様をできる限り利用して作られたAPIは第三者にとっても、少なくとも独自仕様に比べればずっと理解しやすい。

ステータスコードを正しく使う

ステータスコードは先頭の数字1桁でおおよその意味合いを示している。

ステータスコード 意味
100番台 情報
200番台 成功
300番台 リダイレクト
400番台 クライアントサイドに起因するエラー
500番台 サーバサイドに起因するエラー

HTTPステータスコードには意味があるので、適切なコードを返した方が、クライアントがエラーを正しく認識することができる。

200番台:成功

指定したデータの取得が成功した、あるいはリクエスト処理が成功した場合には200番台のステータスコードを返す。 202の"Accepted"は、リクエストした処理が非同期で行われ、処理は受け付けたけど完了していないあ愛に利用される。 204の"No Content"は、レスポンスが空のときに返す。DELETEメソッドなどでデータの削除を行った際に、204を返す。

300番台:追加で処理が必要

300番台のステータスコードのうち、リダイレクトに関するものは301, 302, 303, 307がある。リダイレクトの場合はLocationというレスポンスヘッダにリダイレクト先の新しいURIが含まれる。 300の"Multiple Choices"は、複数の選択肢がある場合に送信されるステータスコード。 304の"Not Modified"は、前回のデータ取得から更新されていないことを表すステータスコードでレスポンスボディは空。

400番台:クライアントのリクエストに問題があった場合

400番台はクライアントのリクエストに起因するエラー、サーバ側に問題がないが、クライアントの送ってきたリクエストが理解できなかったり、理解はできるが実行が許可されていなかったりしてエラーになった場合に利用するステータスコード。 400の"Bad Request"は、その他。他の400番台のエラーで表すことができないエラーに使うためのステータスコード。 401の"Unauthorized"は、認証のエラー(あなたは誰だかわからない)、403の"Forbidden"は、認可のエラー(あなたが誰だかわかったけどこの操作はあなたには許可されていない)をそれぞれ表す。 408の"Request Timeout"は、リクエストをクライアントがサーバに送るのに時間がかかりすぎてサーバ側でタイムアウトを起こした際に発生する。 410の"Gone"は、かつて存在したけれども今はもう存在しないということを表している。例えば、かつて登録されていたメールアドレスが削除されていた際に使うがこれは、セキュリティの観点から指摘される可能性がある。

500番台:サーバに問題があった場合

500番台のエラーは、クライアント側でなくサーバ側に問題があった場合のエラー。 503の"Internal Server Error"は、意図的がどうかは問わず、サーバが一時的に利用できな状態になっていることを示すもの。

キャッシュとHTTPの仕様

ここでいうキャッシュは、サーバへのアクセスの頻度や通信量を減らすためにクライアント側で一度とった情報を保存しておき、再度必要となったときにあらかじめ取得してあった情報を利用することをいう。 キャッシュのメリットは次の通り。

  • サーバへの通信を減らすことができるため、ユーザの体感速度をあげることができる
  • ネットワーク接続が切れた状態でもある程度サービスを継続できる
  • サーバへの通信回数、転送量を減らすことでユーザの通信コストを下げることができる
  • サーバへのアクセス回数が減ることで、サーバの維持費用を抑えることができる

HTTPのキャッシュでは、RFC 7234できちんと定義されている。HTTPのキャッシュにはExpiration Model(期限切れモデル)とValidation Model(検証モデル)という2つのタイプがある。

Expiration Model(期限切れモデル)

期限切れモデルは、あらかじめレスポンスデータに保存期限を決めておき、期限が切れたら再度アクセスして取得を行うというもの。Cache-Controlレスポンスヘッダを使う方法かExpiresレスポンスヘッダを使うを使うことで、いつ期限が切れるかをサーバからのレスポンスをに含めて返すことで実現できる。 ExpiresとCache-Controlを同時に利用した場合には、より新しい仕様であるCache-Controlが優先される。

Expiresヘッダ

ExpiresはHTTP 1.0から存在するヘッダで期限切れを絶対時間でRFC 1123で定義された形式で表す。特定の日時に更新されることがあらかじめわかっているデータは、Expiresで日時を指定することができる。また、今後更新される可能性がないデータや静的データの場合には、遠い未来の日時を指定することで、一度とったキャッシュデータ をずっと保存しておくように指示を出すことができる。

Cache-Controlヘッダ

Cache-ControlはHTTP 1.1から定義されたヘッダで現在時刻からの秒数で表す。「毎日何時」など定期更新でないものの更新頻度がある程度限られているものや、更新頻度は低くないものの、あまりアクセスしてほしくない場合に利用することができる。 max-ageの計算には、Dateヘッダを利用する。これはレスポンスが生成されたサーバ側の日時を示すヘッダで、この日時からの経過時間がmax-ageの値を超えた場合にはそのキャッシュは期限が切れたと考えることができる。

Validation Model(検証モデル)

検証モデルは、今持っているキャッシュが有効かどうかをサーバに問い合わせるというもの。過去に取得したある時点でのデータに関する情報を送り、更新されていたらデータを返し、更新されていなかったら304("Not Modified")というステータスコードを返す。

キャッシュをさせたくない場合

Cache-Control: no-cache

このようにCache-Controlヘッダに指定することで「キャッシュをしてほしくない」と伝えることができる。

第5章 設計変更をしやすいWeb APIを作る。

機能の強化やバグの修正、機能の廃止など状況に応じて、変化していく。

APIをバージョンで管理する

古い形式でアクセスしてきているクライアントに対してはそれまでと変わらないデータを送り、新しい形式でのアクセスには、新しい形式のデータを返す。 バージョニングの方法には、次の3つが主に考えられる。

  • URIにバージョンを埋め込む
  • バージョンをクエリ文字列に入れる
  • メディアタイプでバージョンを指定する

最も利用されているのは、「URIにバージョンを埋め込む」である。URIを見るだけでAPIのバージョンがはっきりわかるため、受け入れられやすいようだ。

バージョン番号をどうつけるか

バージョニングのルールとして広く知られている方法にセマンティックバージョニングがある。"1.2.3"のように3つの数値をドットで繋いだもの。それぞれの数値は、メジャー、マイナー、パッチと呼ばれ、以下のようなルールが適用される。

  • パッチバージョンはソフトウェアのAPIに変更がないバグ修正などを行なったときに増える。 マイナーバージョンは後方互換性のある機能変更、あるいは特定の機能が今後廃止されることが決まった場合に増える。- メジャーバージョンは後方互換性のない変更が行われた際に増える。

APIの提供を終了する

APIが公開終了になったときに、どういったことが起こるかをあらかじめ仕様に盛り込んでおくと良い。一番簡単なのは、APIが公開終了した際にはステータスコード410(Gone)を返すというもの。410を返すだけでは不親切なので、エラーメッセージとして「このAPIは公開が終了しました。より新しいバージョンを使ってください」のようなメッセージを返すとよりよい。

初めてのTech DayでConcourse CIに入門した

コイニーのバックエンドチームでTech Dayを初開催しました! Tech Dayとは、「日々のプロジェクトにリソースを取られて、技術獲得やチャレンジができず知的好奇心を満たせていない」と感じたリーダーが(勝手に)企画してくれたとてもありがたい日です。2週ごとに1日くらいのペースでやっていくつもりです。 題材は、自チームや自身のメリットに繋がることであれば何でもよいです。(他チームの為に便利ツールを作成するとか運用が発生するようなものはやらない)

私は題材に「Concourse CI入門」を選びました。チュートリアルを試したレベルですが、やったことを書きます。 この題材にした理由は2つあります。

  1. 数年前から気になっていて、当時の同僚(とても尊敬している人)がかなり推していたから
  2. 会社でJenkinsを使っているけど、時代的にどうだろうと思ったから。

Concourse CI入門

チュートリアルhttps://concoursetutorial-ja.cfapps.io/ )をやりました。

Concourseをローカルにデプロイ

  1. Docker Composeのインストール
  2. 任意のディレクトリで次のコマンドを実行。Councourseがデプロイされます。
$ wget https://raw.githubusercontent.com/starkandwayne/concourse-tutorial/master/docker-compose.yml
$ docker-compose up -d

fly CLIのセットアップ

flyコマンドについては、こちら concourse-ci.org

fly CLIをダウンロード

http://127.0.0.1:8080/にアクセスして、OSのロゴをクリックするとfly CLIのバイナリが落ちてきます。(今回はMacで話を進めます)

ダウンロードしたら次のコマンドを実行します。

$ sudo mv ~/Downloads/fly /usr/local/bin
$ sudo chmod 0755 /usr/local/bin/fly

ターゲットの指定

fly CLIは、declaring absolutely everything you do to get absolutely the same result every timeの精神に基づいており、flyコマンドを実行する度にターゲットAPIを指定する必要があります。

ターゲットエイリアスを作成

// tutorialという名前でエイリアスを作成
$ fly --target tutorial login --concourse-url http://127.0.0.1:8080
logging in to team 'main'

navigate to the following URL in your browser:

  http://127.0.0.1:8080/sky/login?redirect_uri=http://127.0.0.1:54711/auth/callback

or enter token manually:

メッセージにしたがってhttp://127.0.0.1:8080/sky/login?redirect_uri=http://127.0.0.1:54711/auth/callbackにアクセスすると、ログイン画面が表示されます。 docker-compose.ymlのservices.concourse.environment.CONCOURSE_ADD_LOCAL_USERに設定されている。username:admin、password:adminでログインできます。 コマンドでもログインできます。

$ fly --target tutorial login --concourse-url http://127.0.0.1:8080 -u admin -p admin
logging in to team 'main'

target saved

ターゲットとして指定されているConcourseと同じバージョンのflyコマンドにアップグレード

$ fly --target tutorial sync
version 4.2.1 already matches; skipping

ターゲットの確認

$ cat ~/.flyrc
targets:
  tutorial:
    api: http://127.0.0.1:8080
    team: main
    token:
      type: Bearer
      value: eyJhbGciOiJSUzI1NiIsImtpZCI6IiIsInR5cCI6IkpXVCJ9.eyJjc3JmIjoiNTEzZGIyZTBiMGVkYTEzZGU5MDk5Mzk0YjJkMWNiMTViZDQ2ZTJjZTgzZjRhOTFjYjRmMmQzZjY4ZmFmOWE5YyIsImVtYWlsIjoiYWRtaW4iLCJleHAiOjE1NDcyNzcyMzUsImlzX2FkbWluIjp0cnVlLCJuYW1lIjoiIiwic3ViIjoiQ2dWaFpHMXBiaElGYkc5allXdyIsInRlYW1zIjpbIm1haW4iXSwidXNlcl9pZCI6ImFkbWluIiwidXNlcl9uYW1lIjoiYWRtaW4ifQ.n-3zISYO7poQamnjJ-yYP-ChX3RHROTWkwng3GnHm3d8fefxoO8p-Bxod2we-KQWEfE8w27JuyNejOI53dSK7rORi1bu8xUE7WbLkhZTA1TlvTzPj8sEXT-KJ_PfCBgyhs_vL91LertLkXVA0Orre6aRGb-kHd-tbdqF1OaMFGYKZbJ5oZr0hMtvunQrpm3oIbeHEX0Edvwi2tYDem4ccwqrhoxSEYVouXuBthdq59LeBw5INsM92UIBPQNkZqkSZyQCJqaSzr3xeGDiDxWl5uxIfeAFHfOUSuqefTE1bhWS3dolRDWr5SDWi_oIuW1fCMmsY8bWMrWGnCatI4tXuQ

エイリアスの作成をしたので、flyコマンド実行時に fly --target tutorial とうつことでこのConcourse APIをターゲットにできます。

Taskを実行する

task_hello_world.ymlの実行

任意のディレクトリで次のコマンドを実行し、concourse-tutolialをクローンしてtask_hello_world.ymlを実行します。これは、echo hello worldと出力するだけの簡単タスクです。

$ git clone https://github.com/starkandwayne/concourse-tutorial
$ cd concourse-tutorial/tutorials/basic/task-hello-world
$ fly -t tutorial execute -c task_hello_world.yml

タスクが完了すると、以下のように出力されます。echo hello world が呼ばれていることがわかります。

executing build 1 at http://127.0.0.1:8080/builds/1
initializing
running echo hello world
hello world
succeeded

http://127.0.0.1:8080/builds/1にアクセスするとWebUIで実行結果を確認できます。

task_ubuntu_uname.ymlの実行
$ fly -t tutorial execute -c task_ubuntu_uname.yml
executing build 4 at http://127.0.0.1:8080/builds/4
initializing
running uname -a
Linux 303a03ea-bb52-406c-6da3-93235f258a59 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
succeeded

これも同様に、http://127.0.0.1:8080/builds/4にアクセスするとWebUIで実行結果を確認できます。

今回はとりあえずここまでです。

SpringBoot1系から2系に移行しててハマった点

Spring Boot2系に移行してハマった点について書きます。ほぼ備忘録です。ついでなので2.0.x -> 2.1.xで変更された点もまとめます。
変更点見つけたら随時書いていこうと思います。
移行前のSpring Bootのバージョンは1.5.10です。2系のバージョンは、2.1.1(2018/12/14時点の最新)です。

Gradle

実行可能jarの作り方

1

bootRepackageタスクが呼ばれたあとにjarタスクが呼ばれて実行可能jarが作られるようです。

bootRepackage {
    executable = true
}
2

bootJarタスクで実行可能jarを作ります。jarタスクは無効になっているようです。
https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/gradle-plugin/reference/html/#packaging-executable-and-normal

bootJar {
    launchScript()
}

actuator

エンドポイントプレフィックス

1
/health
2

/actuatorがつくようになりました。
https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/reference/html/production-ready-endpoints.html

/actuator/health

logging

logback-spring.xmlのProfile指定方法

1 ~ 2.0.x

「,」区切りでした。

<springProfile name="dev, staging">
    <!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
</springProfile>
2.1.x

「|」区切りになりました。
https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/reference/html/boot-features-logging.html#_profile_specific_configuration

<springProfile name="dev | staging">
    <!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
</springProfile>

コイニーとFCPと -副業はじめました-

コイニー(本業)で開発をしながら、FASHION CHARITY PROJECT(FCP)というサービスの開発を副業で始めました。

FCPとは

FCPとは、「ファッションアイテムの寄付とお買い物でチャリティ活動できる」サービスです。
コイニーに入る前に勤めていたwajaという会社が運営しているサービスです。
詳しくは、ココをみてください。
バックエンドは1人で1ヶ月半でフルリプレースを行いました。これは、エンジニア生活で一番の大仕事でした。
技術的な変化としては次の通りです。

(旧)

(新)

  • Spring Boot 2.0.x
  • Spring Data JPA
  • Spring Security
  • Thymeleaf

ただリプレースするだけでなくて、リファクタリングをやったり、パフォーマンス改善も行いました。

なぜ副業を始めようと思ったのか

副業を始めた理由として、もちろん「お金が欲しい!」という気持ちはあります。が、1番の理由は、「転職をしないため」です。

自分は、1つの環境だけに長くいられない性格をしているようです。これまで転職を何度かしてきました。自分でいうのもアレなんですけど、その期間でちゃんと貢献はしてきたつもりです。いろんな会社を見てきた経験はできました。しかし、それとは引き換えに長い時間いることでしか味わえない本当に辛い時期や信頼関係というものを経験したことがありません。
これについては、そろそろ経験しておかないとまずいなとは思っています。

コイニーの所属するheyグループは、まだまだ発展途中ですが本当にいい会社だと思います。
けっこうなペースで人が増えていて、これから組織的な強化が行われていくのだと思います。

それに伴って懸念していることがあります。いい環境というのは心地よくて、長くいたい(出たくない)と思うようになり、外を知るということを自然としなくなるのではないだろうかと思います。そうなるのが非常に怖くて、会社とは違うどこかに所属しておきたいと感じていました。そうすることで転職への熱はだいぶ抑えらます。

なぜFCPを選んだのか

3つ理由があります。

スーパーできるディレクターともう1度仕事をしたかったから

この人と入社時から絡むことはあったけど、仕事で絡んだのはやめる3ヶ月前からです。仕事の熱とか視点とか知識はかなり持っていて、純粋に「この人すごいな」ってなりました。仕事だけでなく、その他の引き出しも多いし何者なのだろうかと思ってました。
ある程度コード書けるので、1人で回せなくはないかなと思います。しかし、リプレースしたばっかで数字もよくて、「今がチャンスだからディレクター業に専念したいから可能なら開発はやって欲しい」というのは退職してからも話は持ちかけられてました。
本業の試用期間も終わって少し落ち着いたころに直で、CEOにFCPの開発やらせてよって連絡しました。思ったよりすんなりOKでました。
そんな感じで始まって、また一緒に仕事ができるのでとてもワクワクしています。

自分が作ったサービスは愛着がわいていたから

だいたいの人はそうなんじゃないかな?って思うんですけど、DB設計からリリースまでほぼ1人でやりましたのでとても愛着が湧いてるサービスです。この経験ってなかなか積めないし、社会に貢献できるサービスなので、これからも成長させていきたいと考えてます。

小さいサービスだしいろいろと試せて、会社では学べないことを学べそうだから

会員数はけっこういますけど、サービスの規模は小さめなので技術的にも施策的にもいろいろと試せるのではないかと考えています。
上記のディレクターとほぼ2人で進めて行くので、ビジネスのこともかなり理解しないといけないです。降りてきた仕様をそのまま実装するのはよくないですし。そしてスピード感も必要。ビジネスの話はあまり得意でないので、そこについても学んでいけるのはメリットが大きいです。
ここで学んだことを本業で活かせるタイミングはあるはず。

最後に

開業freeeを使えばすぐに開業届作れます。税務署での手続きも5分で終わります。