谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

Dapr Advent Calendar 5日目 - Daprでメッセージング

こんにちは、Dapr Advent Calendar 5日目です。日曜日なのでゆっくりエントリーを書きました。

Pub/Subメッセージングをしてみよう

今回はDaprを使ってキューを使ったメッセージングをします。このメッセージング機能(Pub/sub API)とサービス呼び出し(Invoke API)の2つが、僕がDaprを使おうという決め手になったくらい重要機能だと思っています。

f:id:cero-t:20211205083946p:plain
今回作るアプリケーション

Pub/sub機能についてのドキュメントはこちらにあります。

docs.dapr.io

今回作成するアプリケーションのソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021/

今回は、このリポジトリにある「publish」と「subscribe」モジュールを作ります。

メッセージを送る側のアプリケーションの作成 (publish)

publish側のコントローラークラスの作成

まずはメッセージを送る側、つまりキューに対してエンキューする側のWebアプリケーション(publishモジュール)を作成します。

メッセージを送るために、DaprのPub/sub APIを使います。

(publish) PublishController.java

@RestController
public class PublishController {
    private RestTemplate restTemplate;

    @Value("${pubsubUrl}")
    private String pubsubUrl;

    public PublishController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @PostMapping("/publish")
    public void publish(@RequestBody Object message) {
        restTemplate.postForObject(pubsubUrl, message, Void.class);
    }
}

publish メソッドにて、リクエストで受け取ったオブジェクトをそのまま pubsuburl(DaprのPub/sub APIのURL)に投げているだけです。

publish側のアプリケーション設定ファイルの作成

前回のエントリーではURLをソースコードに直書きしましたが、今回は環境への依存性を下げるためURLを設定ファイルに書くことにしました。

(publish) application.properties

server.port=8083
pubsubUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/publish/pubsub/my-message

このアプリケーションは8083番ポートを利用します。

Pub/sub APIのURLは /v1.0/publish/(pubsub名)/(メッセージトピック) となり、ここでは pubsub という名前のpubsubを使っています。なんかちょっと分かりづらいですね。

なぜこの名前にしたかというと、ローカル環境のDaprのデフォルトpubsub名がそうなっているためです。前回のエントリーでデータストアの設定について説明した際、設定ファイルがは~/.dapr/components/statestore.yaml にあると説明しました。

pubsubについても同様で、~/.dapr/components/pubsub.yaml に初期設定が記述されています。

~/.dapr/components/pubsub.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""

metadata.name がpubsubの名称です。要するにRedisをキューとして使う、という設定に pubsub という名称がついているのです。ここではひとまずこのデフォルト設定のものを使い、Redisを使うことにします。なお、このRedisは dapr init コマンドを実行した際に作成されたDockerコンテナに繋がるようになっています。

少しpubsub名の説明が長くなりましたが、元の話に戻りましょう。

Pub/sub APIのURLは /v1.0/publish/(pubsub名)/(メッセージトピック) でした。最後のメッセージトピックは、メッセージをルーティングするためのパスのようなものです。RabbitMQを利用したことがある人ならexchangeの名称として使われる、と言えば分かりやすいかも知れません。

今回は my-message という名前のメッセージトピックにしました。これが実際にどう使われるかは、後ほど説明します。

メッセージを受け取る側のアプリケーションの作成 (subscribe)

subscribe側のコントローラークラスの作成

続いてメッセージを受け取る側、つまりキューに対してデキューする側のWebアプリケーション(subscribeモジュール)を作成します。

メッセージを受け取る側は、通常のWeb APIとして作成します。

(subscribe) SubscribeController

@RestController
public class SubscribeController {
    @PostMapping("/subscribe")
    public void subscribe(@RequestBody String message) {
        System.out.println(message);
    }
}

メッセージを受け取って標準出力に出力するだけです。特にデキューする処理を書いたりせず、ただのWeb APIとして実装します。

subscribe側のアプリケーション設定ファイルの作成

このアプリケーションは8084番ポートを利用するため、設定ファイルにポート番号を書いておきます。

(subscribe) application.properties

server.port=8084

subscribe側のDapr設定ファイルの作成

さらに、上で作成したWeb APIでメッセージを受け取るように設定します。そのためにはDaprの Subscription 設定ファイルを作成する必要があります。このファイルは、少し抵抗があるかも知れませんが ~/.dapr/components/ ディレクトリに作成します。

~./dapr/components/subscription.yaml

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: subscription
spec:
  pubsubname: pubsub
  topic: my-message
  route: /subscribe
scopes:
  - subscribe-app

metadata.name の値は特に利用しないため、任意の名前をつけて構いません。

pubsubname はpubsubの名称です。デフォルトで設定されていた pubsub を利用します。

topic にはメッセージトピックを指定します。ここではpublish側のアプリケーションで指定した my-message を指定しています。

route は、呼び出すWebアプリケーションのパスです。SubscribeController/subscribe というパスで待ち受けているため、それを指定します。

scopes には、待ち受けているアプリケーションのapp-idを指定します。今回はsubscribe側のアプリケーションを subscribe-app というapp-idで起動する予定ですので、それを指定しています。

ここに複数のアプリケーションのapp-idを列挙しておけば、同じメッセージを複数のアプリケーションで受け取れるようになるわけです。

GitHubのサンプルコードでは、このファイルを subscribe/.dapr/components/subscription.yaml に置いています。そちらを利用する場合は、ユーザーディレクトリにコピーしてください。

Daprを使ったアプリケーションの起動

それではDaprを使ってアプリケーションを起動します。まずは送る側のアプリケーション (publishモジュール) から起動します。

cd publish
dapr run --app-id publish-app --app-port 8083 ../mvnw spring-boot:run

引数などは特に説明する必要はないですね。

続いて受け取る側のアプリケーション (subscribeモジュール) を起動します。

cd subscribe
dapr run --app-id subscribe-app --app-port 8084 ../mvnw spring-boot:run

起動したら、publish側のAPIJSONでメッセージを渡します。

curl -XPOST "localhost:8083/publish" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

コマンドを実行したら、subscribe側のコンソールを見てください。次のようなメッセージが表示されているはずです。

== APP == {"id":"661aace2-dcbe-4d6e-be26-e4d4313d5f9c","source":"publish-app","type":"com.dapr.event.sent","pubsubname":"pubsub","specversion":"1.0","datacontenttype":"application/json","topic":"my-message","traceid":"00-016cf5eccd3ce47cc9bb85ba90742553-244bd4331034542d-01","data":{"name":"Shin Tanimoto","twitter":"@cero_t"}}

何やらたくさんのパラメータが表示されましたが、メッセージ本体が data に入っていることが確認できます。

CloudEvents 1.0について

ここで受け取ったメッセージには、メッセージ本体以外にもいくつかパラメータが追加されていますね。これらのパラメータはCloudEvents 1.0という仕様に準拠したものです。

github.com

CloudEvents 1.0ではいくつかパラメータが指定されており、この仕様に従えば他のミドルウェアとの相互運用性が上がって、Dapr以外のアプリケーションでも受信できるようになるとのこと。ただ正直、僕自身の経験としてはまだそのような使い方をしたことはありません。いつも取得したメッセージから data の部分のみを取り出しています。

CloudEventsなど不要という声が多いのでしょうか、Daprのサイトにも、CloudEventsを使わないためのドキュメントがあります。

docs.dapr.io

publishする際のURLに ?metadata.rawPayload=true というクエリパラメータを追加すればメッセージのみが送られるとのことです。

(publish) application.properties

server.port=8083
pubsubUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/publish/pubsub/my-message?metadata.rawPayload=true

このように設定ファイルを書き換えてアプリケーションを再起動し、再度メッセージを送信すれば、subscribe側のコンソールに次のようなメッセージが表示されるはずです。

== APP == {"name":"Shin Tanimoto","twitter":"@cero_t"}

特にCloudEventsの仕様に準拠させる必要がない場合には、この方法を使っても良いでしょう。

ここまででRedisを介したメッセージングを行うことができました。キュー処理もデキュー処理も、RedisのAPISDKを利用するのではなく、HTTP処理だけで書けることが分かりました。

f:id:cero-t:20211205084027p:plain
Daprを経由したメッセージング

異なるメッセージブローカーを使う

ここまではRedisを用いたメッセージングを行いましたが、他のメッセージブローカーを使うこともできます。

サポートされているメッセージブローカーの一覧はこちらにあります。

docs.dapr.io

ここではRabbitMQを使ってみることにします。まだRabbitMQ対応のコンポーネントはAlpha版のようですが、Dapr v1.5でデッドレターキュー(DLX)にも対応され、実際に運用しても特に問題は起きていません。

RabbitMQの起動

まずはRabbitMQを起動しましょう。Dockerを使って起動します。

docker run -d --name dapr_rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management

キューは5672番ポート、管理コンソールは15672番ポートで待ち受けるようにしました。

アプリケーションの修正

Dapr設定ファイルの作成

続いて、このRabbitMQに接続するための設定ファイルをpublishモジュール、subscribeモジュールのcomponentsディレクトリにそれぞれ作成します。

components/rabbitmq-pubsub.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: rabbitmq-pubsub
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: amqp://localhost:5672

pubsubname を分かりやすいよう rabbitmq-pubsub にしています。

host にはRabbitMQへの接続アドレスを指定します。

publish側のアプリケーション設定ファイルを追加

pusbsub名を変更したためPub/sub APIのパスも修正が必要です。URLの設定ファイルを追加で作成します。

(publish) application-rabbitmq.properties

pubsubUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/publish/rabbitmq-pubsub/my-message

メッセージトピックに my-message を指定しました。

subscribe側にDar設定ファイルの追加

さらに、rabbitmq-pubsub からデキューするようsubscribe側にSubscriptionの設定ファイルを作成します。

subscribe/components/subscription.yaml

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: subscription
spec:
  pubsubname: rabbitmq-pubsub
  topic: my-message
  route: /subscribe
scopes:
  - subscribe-app

先に作成した設定ファイルから pubsubname の部分を rabbitmq-pubsub に変えただけです。

設定を有効にしてアプリケーションを起動

publish側の起動

それでは作成した設定ファイルが有効になるようDaprを使ってアプリケーションを起動します。まずはpublish側から起動します。

いったん停止させた後、次のコマンドで起動します。

dapr run --app-id publish-app --app-port 8083 --components-path ./components -- ../mvnw spring-boot:run -Dspring-boot.run.profiles=rabbitmq

components/rabbitmq-pubsub.yaml を有効にするために --components-path ./components を追加したのと、application-rabbitmq.properties が有効になるよう -Dspring-boot.run.profiles=rabbitmq を追加しました。

この時点でブラウザからRabbitMQの管理コンソールにアクセスしてみましょう。

http://localhost:15672/

初期アカウントは Username Password ともに guest です。

「Exchanges」タブでRabbitMQが管理しているExchangeの一覧を参照することができます。

f:id:cero-t:20211205082123j:plain
publish起動後のExchange一覧

この時点では、まだDaprに関するExchangeなどは特に作成されていませんね。

subscribe側の起動

続いて、subscribe側のアプリケーションを設定ファイルを有効にした状態で起動します。

dapr run --app-id subscribe-app --app-port 8084 --components-path ./components ../mvnw spring-boot:run

上と同じように --components-path ./components を追加しました。

もう一度、RabbitMQの管理コンソールにアクセスして、「Exchanges」タブを開いてみます。

f:id:cero-t:20211205082355j:plain
subscribe起動後のExchange一覧

Exchangeに my-message が作成されています。このExchangeをクリックして詳細を開いてみます。

f:id:cero-t:20211205082712j:plain
my-messageの詳細

このExchangeから subscribe-app-my-message というQueueにバインドされた状態になっています。RabbitMQに不慣れな方に説明すると、Exchangeはキューのルーティングを行うようなもので、送り先のキューを選択したり、複数のキューに同じメッセージを送ったりすることができます。Queueは文字通り単一のキューです。 AWSに詳しければ、ExchangeはSNS、QueueがSQSのような役割を果たすものだと考えてください。

メッセージを送る

起動を確認できたら、次のコマンドでメッセージを送ってみます。

curl -XPOST "localhost:8083/publish" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

subscribe側のコンソールに次のようなメッセージが表示されたはずです。

== APP == {"specversion":"1.0","source":"publish-app","type":"com.dapr.event.sent","topic":"my-message","id":"e9f066d7-d129-430e-b069-4bf01a50fb2d","datacontenttype":"application/json","pubsubname":"rabbitmq-pubsub","traceid":"00-ee177cd7c4aaaf83c591c04e34e2edb2-49e157665d77f0e3-01","data":{"name":"Shin Tanimoto","twitter":"@cero_t"}}

pubsubnamerabbitmq-pubsub に変わっていることが分かります。CloudEventsを無効にしているとこれらのパラメータが表示されないので、確認のため有効にしておくと良いでしょう。

ソースコードを変えることなく、設定を変えるだけで接続先をRedisからRabbitMQに変えることができました。

まとめ

  • Pub/sub APIを用いてメッセージブローカーに対してHTTPでメッセージを送ることができます
  • メッセージブローカーからメッセージを受け取る側は、Web APIと設定ファイルだけで作成ができます
  • つまりHTTPの送受信だけでメッセージングができます
  • 設定を変えるだけで別のメッセージブローカーに切り替えることができます

なお、長くなるので本文中では触れませんでしたが、publish側のURLを http://localhost:8084/subscribe に変更すれば、直接subscribe側のAPIを呼び出すこともできます。開発時にはDaprやメッセージブローカーを利用せずに同期処理としてビジネスロジックが正しく動くことを検証し、テスト時にはDaprやメッセージブローカーを持ちいて非同期通信で検証する、ということも可能になります。

特にキューを用いたアプリケーションをJUnitなどでテストする際には、非同期処理側が終わることを少し待ってからassertしなければなりませんが、テスト時には同期処理にしてしまえば、すぐに検証ができて楽ですよね。

今回はメッセージブローカーの独自APISDKを使わず、Daprを用いて通信をHTTP(gRPC)に寄せることで、設定だけで柔軟に環境を変えられるということが分かりました。僕がDaprを好きな理由の一つです。

それでは、また来週(明日)!

Dapr Advent Calendar 4日目 - Daprでデータストアにアクセス

こんにちは、Dapr Advent Calendar 4日目です。ここは "3日坊主の向こう側" です!

データストアの読み書きをしてみよう

今回はDaprを使ってデータストアへの読み書きを行います。

f:id:cero-t:20211203231211p:plain
今回作るアプリケーション

ソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021/tree/main/state

データを読み書きするアプリケーションの作成

まずはWebアプリケーションの作成です。DaprのState management APIを使い、データの書き込み処理と、読み込み処理をそれぞれ作成します。

StateController.java

@RestController
public class StateController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/state/statestore")
    private String stateUrl;

    public StateController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @PostMapping("/write")
    public void write(@RequestBody Object message) {
        restTemplate.postForObject(stateUrl, message, Void.class);
    }

    @GetMapping("/read/{key}")
    public Object read(@PathVariable String key) {
        return restTemplate.getForObject(stateUrl + "/" + key, Object.class);
    }
}

writeメソッドは書き込み処理、readメソッドは読み込み処理ですが、DaprのState management APIをそのまま呼び出しているだけで、他に何もしてません。APIの詳細は後で説明します。

このアプリケーションをポート番号8082で起動するよう、設定ファイルでポート番号を指定します。

application.properties

server.port=8082

これでアプリケーションの作成は完了です。

Daprを使ったアプリケーションの起動

このアプリケーションはDaprのState management APIを活用する前提で実装しているため、Daprを使って起動します。

dapr run --app-id state-app --app-port 8082 ../mvnw spring-boot:run

起動したら、まずは書き込み用のAPIJSONでメッセージを渡してデータの登録を行います。

curl -XPOST "localhost:8082/write" -H "Content-type:application/json" -d '[
  {
    "key": "cero_t",
    "value": {
      "name": "Shin Tanimoto",
      "twitter": "@cero_t"
    }
  },
  {
    "key": "BABYMETAL",
    "value": [
      {
        "name": "SU-METAL",
        "alias": "Suzuka Nakamoto"
      },
      {
        "name": "MOAMETAL",
        "alias": "Moa Kikuchi"
      },
      {
        "name": "YUIMETAL",
        "alias": "Yui Mizuno"
      }
    ]
  }
]'

key に登録するデータのキー、value に登録する内容を指定しています。これがState management APIの仕様で、後ほど詳しく説明します。

続いて、読み込み用のAPIを使ってデータの取得を行います。

curl localhost:8082/read/cero_t

次のようなデータが取得できるはずです。

{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}

もう一つのデータも取得してみましょう。

curl localhost:8082/read/BABYMETAL

次のようなデータが取得できます。

[
  {
    "alias": "Suzuka Nakamoto",
    "name": "SU-METAL"
  },
  {
    "name": "MOAMETAL",
    "alias": "Moa Kikuchi"
  },
  {
    "name": "YUIMETAL",
    "alias": "Yui Mizuno"
  }
]

問題なく取得できましたね。

この状態でアプリケーションを終了し、もう一度起動しても、問題なくデータは取得できます。データがきちんと保存されていることが分かりました。

f:id:cero-t:20211203231226p:plain
Daprを経由したアクセス

このデータはどこに保存されて、どこから取ってきたのでしょうか。その辺りを次の項で説明します。

やったことの解説

それではコードや打ったコマンドなどについて説明します。

State management APIと設定ファイル

今回はDaprのState management APIにそのままリクエストを流しただけですから、このAPIについて説明します。

State management APIのドキュメントはここにあります。

docs.dapr.io

DaprのState management APIのURLは http://localhost:${DAPR_HTTP_PORT}/v1.0/state/(データストア名) となります。

上で作ったソースコードで、このように記述していましたが

@Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/state/statestore")

データストア名を statestore としています。もしこのストア名に異なる名前、たとえば my-store などを指定すると、次のようなエラーとなります。

state store my-store is not found

なぜ statestore と名前が決まっているのでしょうか。そして、これはどこのデータストアを指しているのでしょうか。

実はデータストアの設定は ~/.dapr/components/statestore.yaml に記載されています。

~/.dapr/components/statestore.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"

dapr run コマンドを実行してDaprを起動すると ~/.dapr/components にある設定ファイルが読まれます。今回はそこにある statestore.yaml が読み込まれたのです。

設定ファイルの仕様をよく知らなくても、なんとなくRedisを用いることと、localhost:6379を参照していることは分かると思います。また metadata.name に記載された statestore がデータストア名となります。

ここで指定されているRedisは、Dapr CLIdapr init した際に作成されたものです。docker ps コマンドでRedisが起動していることを確認してみましょう。

docker ps --filter name=redis

Redisのコンテナが見つかるはずです。

CONTAINER ID   IMAGE     COMMAND                  CREATED        STATUS        PORTS                    NAMES
de8b1f054509   redis     "docker-entrypoint.s…"   48 hours ago   Up 48 hours   0.0.0.0:6379->6379/tcp   dapr_redis

つまり、このRedisが6379番ポートで待ち受けており、それを使うよう ~/.dapr/components/statestore.yaml に記載されており、dapr run コマンド実行時にこのファイルが読み込まれて利用できるようになった、ということです。

ちなみに設定ファイルのパスは dapr run コマンド実行時の引数に --components-path を指定することで、任意のパスを指定することができます。それは後ほど試してみましょう。

データの登録と取得

データの登録と取得はState management APIを使いました。

登録のJSONは次のような形でした。

[
  {
    "key": "cero_t",
    "value": {
      "name": "Shin Tanimoto",
      "twitter": "@cero_t"
    }
  },
  {
    "key": "BABYMETAL",
    "value": [
      {
        "name": "SU-METAL",
        "alias": "Suzuka Nakamoto"
      },
      {
        "name": "MOAMETAL",
        "alias": "Moa Kikuchi"
      },
      {
        "name": "YUIMETAL",
        "alias": "Yui Mizuno"
      }
    ]
  }
]'

配列を使って複数のデータを登録できます。

それぞれのデータは keyvalue という項目が必須で、key が検索時にも使われるキーで、value がメッセージ本体となります。メッセージ本体は任意の形式のJSONを指定することができます。一般的なスキーマレスのKVSと同じですね。

取得する際にはState management APIのパスに key の値を渡せば1件ずつ取得できます。

{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}

他にも、複数のkeyを指定して取得するBulk APIや、削除するためのDelete API、またDapr v1.5ではフィルタリングをするためのQuery APIも追加されました(まだアルファ版です)

それぞれのAPIについてはAPIリファレンスを参考にしてください。

https://docs.dapr.io/reference/api/state_api/

異なるデータストアを使う

ここまでは、Daprが用意した設定ファイルとRedisを使ってデータの登録を行いましたが、他のデータストアを使うこともできます。

サポートされているデータストアの一覧はこちらに記載されています。

docs.dapr.io

ここではPostgreSQLを使ってみることにします。

PostgreSQLの起動

まずはPosgreSQLを起動しましょう。手っ取り早くDockerを使って起動します。

docker run -d --name dapr_postgres -p 5432:5432 -e POSTGRES_PASSWORD=secretpassword postgres

ローカルホストの5432番ポートで待ち受けるようにしました。

設定ファイルの作成

続いて、このPostgreSQLに接続するための設定ファイルをcomponentsディレクトリに作成します。

state/components/posgresql-store.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.postgresql
  version: v1
  metadata:
  - name: connectionString
    value: host=localhost user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres

上の方で作ったソースコード内で指定した statestore の名前を変えなくて良いよう、ここでも metadata.name には statestore を使用しました。

そしてこの設定ファイルを使って起動するようDaprのコマンド引数に --components-path ./components を追加します。

dapr run --app-id state-app --app-port 8082 --components-path ./components ../mvnw spring-boot:run

この時点で一度、PostgreSQLのデータがどうなっているか見に行ってみましょう。

起動中のdockerコンテナに入って

docker exec -it $(docker ps -q --filter name=dapr_postgres) /bin/bash

psqlコマンドでアクセスします。

psql -U postgres

テーブル一覧を参照します。

\d

結果がこうなるはずです。

         List of relations
 Schema | Name  | Type  |  Owner   
--------+-------+-------+----------
 public | state | table | postgres

Daprが起動した時点で state という名前のテーブルが作成されていました。

いちおうデータを参照してみると

select * from state;

もちろん空っぽです。

 key | value | isbinary | insertdate | updatedate 
-----+-------+----------+------------+------------
(0 rows)

それでは別のコンソールでcurlコマンドを打って、データを登録してみましょう。

curl -XPOST "localhost:8082/write" -H "Content-type:application/json" -d '[
  {
    "key": "cero_t",
    "value": {
      "name": "Shin Tanimoto",
      "twitter": "@cero_t"
    }
  },
  {
    "key": "BABYMETAL",
    "value": [
      {
        "name": "SU-METAL",
        "alias": "Suzuka Nakamoto"
      },
      {
        "name": "MOAMETAL",
        "alias": "Moa Kikuchi"
      },
      {
        "name": "YUIMETAL",
        "alias": "Yui Mizuno"
      }
    ]
  }
]'

その後、もう一度PostgreSQLのコンテナ内でデータを参照してみます。

select * from state;

今度はきちんとデータが登録されていました。

         key          |                                                                     value                                                                     | isbinary |          
insertdate           | updatedate 
----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------+----------+----------
---------------------+------------
 state-app||cero_t    | {"name": "Shin Tanimoto", "twitter": "@cero_t"}                                                                                               | f        | 2021-11-3
0 22:05:34.759534+00 | 
 state-app||BABYMETAL | [{"name": "SU-METAL", "alias": "Suzuka Nakamoto"}, {"name": "MOAMETAL", "alias": "Moa Kikuchi"}, {"name": "YUIMETAL", "alias": "Yui Mizuno"}] | f        | 2021-11-3
0 22:05:34.760984+00 | 
(2 rows)

keyの部分だけ見てみると

         key          
----------------------
 state-app||cero_t
 state-app||BABYMETAL

(app-id)||key となっていることが分かり、app-idごとにデータが永続化されているということが分かりましたね。

え、なぜRedisではきちんと調べなかったのに、PostgreSQLではこんなにきちんと調べるか、ですって?

そりゃ僕がRedisを使ったことがなくて、コマンドを全然知らないからですよ。ガハハハハ。

まとめ

  • State management APIを用いてデータの登録や取得ができます
  • ローカル環境では ~/.dapr/components にある設定ファイルが読み込まれます
  • Daprの初期化時に作成したRedisが利用されるように設定されています
  • 設定ファイルの場所は dapr run コマンドの --components-path で指定できます
  • PostgreSQLをはじめ、いくつかのRDBMSやNoSQLなどを同じAPIで利用できます

最後の最後でこういうことを言うのもアレなのですが、僕はこのState management APIを仕事で使ったこともなければ、使おうと思ったことすらありません。正直なところ、シンプルなCRUDで済むアプリケーションならまだしも、少しでも複雑な業務アプリケーションではこのAPIでは機能が全く足りないため、あまり使うメリットがないと判断しています。

ちょうどよいユースケースがあれば良いのですけどね。

それでは、また明日!

Dapr Advent Calendar 3日目 - Daprでサービスを呼ぶ

こんにちは、Dapr Advent Calendar 3日目です。ついに3日坊主達成です!

別のサービスを呼んでみよう

前回のエントリー ではDaprを使ってHello Worldアプリケーションを呼び出しましたが、今回はひとつのWebアプリケーションから、Dapr経由で別のWebアプリケーションを呼び出してみます。

f:id:cero-t:20211203033040p:plain
今回作るアプリケーション

ソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021

このソースコードの例ではMavenのマルチモジュール構成で、次のように作ってあります。

  • dapr-advent-2021
    • mvnw
    • pom.xml
    • hello (前回説明したもの)
    • invoke (今回新たに作るもの)

Daprを使わないWebアプリケーションの作成

まずはDaprを使わずに、呼び出される側と呼び出す側のWebアプリケーションをそれぞれ作成します。

呼び出される側のWebアプリケーションは、前回のHello World(helloモジュール)をそのまま使います。

(hello) HelloController.java

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello, world!");
    }
}

そして新たに別のWebアプリケーション(invokeモジュール)を作成します。上で作ったHello Worldのアプリケーションを呼び出すためのアプリケーションです。

(invoke) InvokeController.java

@RestController
public class InvokeController {
    private RestTemplate restTemplate;

    public InvokeController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = restTemplate.getForObject("http://localhost:8080/hello", Map.class);
        return Map.of("remoteMessage", result);
    }
}

helloアプリケーションの /hello を呼び出すだけの簡単な処理です。

RestTemplateを使うため、ApplicationクラスでBean定義をしておきます。

(invoke) InvokeApplication.java

@SpringBootApplication
public class InvokeApplication {
    public static void main(String[] args) {
        SpringApplication.run(InvokeApplication.class, args);
    }

    @Bean
    RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}

Spring Bootの使い方みたいなところの説明は割愛しますね。

また、このアプリケーションをポート番号8081で起動するよう、設定ファイルでポート番号を指定します。

(invoke) application.properties

server.port=8081

これでアプリケーションは完成です。

まずはそれぞれのWebアプリケーションを別々のコンソールで起動します。

cd hello
../mvnw spring-boot:run
cd invoke
../mvnw spring-boot:run

アプリケーションが起動したらcurlコマンドでアクセスします。

curl localhost:8081/invokeHello

無事に実行結果が表示されました。

{"remoteMessage":{"message":"Hello, world!"}}

結果を見やすいようjqコマンドなどで整形するとこんな感じです(以降も基本的にjqで整形した結果を表示します)

{
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

これで別サービスの呼び出しができました。

f:id:cero-t:20211203034815p:plain
ここまでで作ったアプリケーション

ただ、ソースコード内に相手側のアドレスやポート番号を書かなくてはならないという問題がありますね。これをDaprで解決しましょう。

URLを設定ファイルに移動させる

Daprを使ってアプリケーション起動する前に、ソースコードのURLの一部を設定ファイルに移動させます。

(invoke) InvokeController.java(抜粋)

@Value("${baseUrl}")
private String baseUrl;

@GetMapping("/invokeHello")
public Map<String, ?> invokeHello() {
    Map<?, ?> result = restTemplate.getForObject(baseUrl + "/hello", Map.class);
    return Map.of("baseUrl", baseUrl, "remoteMessage", result);
}

URL部分をbaseUrlの値で動的に変えられるようにしました。また、利用したbaseUrlの値を確認するために、レスポンスにbaseUrlを入れるようにしました。

baseUrlは application.properties で定義します。

(invoke) application.properties

server.port=8081
baseUrl=http://localhost:8080

これでURLの一部を設定ファイルに移動させることができました。このURLをDapr向けに書き換えた設定ファイルも別に作っておきます。

(invoke) application-dapr.properties

baseUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method

DAPR_HTTP_PORT は、Daprのポート番号を示しています。Dapr経由でアプリケーションを起動すると、この環境変数にDapr自身のポート番号が入ります。アプリケーションはこのポートを経由して、自身のサイドカーであるDaprプロセスのAPIを利用できるようになるのです。このポート番号を使い、前回説明したInvoke APIを使ってアプリケーションにアクセスするように記述しました。

念のため、この時点でアプリケーションの動作確認をしておきましょう。invokeアプリケーションをいったん停止してから再起動します。

../mvnw spring-boot:run

curlコマンドでアクセスします。

curl localhost:8081/invokeHello 

実行結果にbaseUrlが入るようになりました。

{
  "baseUrl": "http://localhost:8080",
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

これで準備が整いました。次はDaprを使ってアプリケーションを起動してみましょう。

Daprとともにアプリケーションを起動

まずはDaprを使ってhelloアプリケーションを起動します。

helloアプリケーションを停止させた後、次のコマンドで起動します。

dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080 ../mvnw spring-boot:run

続いて、invokeアプリケーションも同じように停止させたあと、次のコマンドで起動します。

dapr run --app-id invoke-app --app-port 8081 --dapr-http-port 18081 -- ../mvnw spring-boot:run -Dspring-boot.run.profiles=dapr

mvnwコマンドに -Dspring-boot.run.profiles=dapr という引数を追加して、application-dapr.properties の設定が有効になるようにしました。また引数を追加するためmvnwコマンドの前に -- を追加しました。

それではDapr経由でWebアプリケーションにアクセスしてみましょう。curlコマンドでアクセスします。

curl localhost:18081/v1.0/invoke/invoke-app/method/invokeHello

次のような結果が表示されるはずです。

{
  "remoteMessage": {
    "message": "Hello, world!"
  },
  "baseUrl": "http://localhost:18081/v1.0/invoke/hello-app/method"
}

ふつうにアクセスした時と同じメッセージが取得でき、baseUrlはDaprのAPI形式になりました。

なおinvokeアプリケーションは、別にDaprを経由せずに呼び出しても構いません。

curl localhost:8081/invokeHello

上と同じ実行結果が得られます。

{
  "remoteMessage": {
    "message": "Hello, world!"
  },
  "baseUrl": "http://localhost:18081/v1.0/invoke/hello-app/method"
}

これで、Dapr経由で2つのWebアプリケーションが連携できました。

あくまでも自身のサイドカーであるDaprと通信するだけで、別のDapr経由で起動しているアプリケーションに対してアクセスができた、ということです。

f:id:cero-t:20211203035529p:plain
Daprを経由したアクセス

やったことの解説

今回は2つのアプリケーションを起動して、Dapr経由でアクセスすることができました。Daprがちょうどサービスディスカバリーやルーティングのような機能を有していると言えます。Spring CloudでいうEurekaやDiscovery Clientに相当する機能です。

この機能に関するドキュメントはこちらになります。

docs.dapr.io

f:id:cero-t:20211203034219p:plain

Dapr同士が協調して、Invoke APIに含まれるapp-id(今回は hello-app)を名前解決して相手のアプリケーションを発見し、その相手のアプリケーションのサイドカーを経由してアプリケーションにアクセスする形となっています。

この名前解決には、ローカル環境ではマルチキャストDNSが利用され、k8s上ではk8sの名前解決が利用されるほか、Consulを利用することもできます。

この名前解決の機能を利用すれば、たとえばこのようなアクセスも可能です。

curl http://localhost:18080/v1.0/invoke/hello-app/method/hello
curl http://localhost:18081/v1.0/invoke/hello-app/method/hello

Daprのポート番号 1808018081 で異なっていますが、どちらのURLでアクセスしても、同じ結果が返ってきます。

{
  "message": "Hello, world!"
}

どのDaprにアクセスしても hello-app という名前からアクセスすべきアプリケーションを探してアクセスを行うのです。Daprを使う時には「どのDaprにアクセスするか」はあまり関係なく、Daprネットワークにアクセスしている、という感覚が近いと言えるでしょう。

環境への依存を下げる

今回説明したサービス呼び出しに関連して、もう少し環境周りのパラメータを減らす方法を説明しておきます。

Daprのポート番号を指定せずに起動する

Daprの理解を深めるために、Daprのポート番号を指定せずにアプリケーションを起動するようにします。

helloアプリケーションを一度停止し、次のコマンドで起動します。--dapr-http-port を指定しない形です。

dapr run --app-id hello-app --app-port 8080 ../mvnw spring-boot:run

invokeアプリケーションも一度停止して、同じように --dapr-http-port を指定せずに起動します。

dapr run --app-id remote-call-app --app-port 8081 -- ../mvnw spring-boot:run -Dspring-boot.run.profiles=dapr

そしてcurlコマンドで直接invokeアプリケーションにアクセスします。

curl localhost:8081/invoke

これでも正常に実行結果が得られました。

{
  "remoteMessage": {
    "message": "Hello, world!"
  },
  "baseUrl": "http://localhost:65275/v1.0/invoke/hello-app/method"
}

DaprのHTTPポート番号がランダムに決まったたとしても、環境変数 DAPR_HTTP_PORT を使っているため自分のサイドカーとして起動しているDaprにアクセスできます。空いているポート番号を適当に使っても構わなくなるのは楽で良いですよね。

Daprの設定ファイルを使わずに起動する

ここまでの例では application-dapr.properties を使っていましたが、これはアプリケーション側がDaprに依存している形にも見えます。このファイルを使わずに、コマンド引数で接続先を指定する形も試してみましょう。

次のコマンドで起動します。

dapr run --app-id remote-call-app --app-port 8081 -- ../mvnw spring-boot:run -Dspring-boot.run.arguments='--baseUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method'

baseUrl をコマンド引数として渡すようにしました。このコマンド実行時点で ${DAPR_HTTP_PORT} が解釈されてしまわないよう、引数はシングルクォートで囲っています。

これで起動したアプリケーションにcurlコマンドでアクセスすると

curl localhost:8081/invoke

正常に実行結果が得られました。

{
  "remoteMessage": {
    "message": "Hello, world!"
  },
  "baseUrl": "http://localhost:51939/v1.0/invoke/hello-app/method"
}

このようにすればアプリケーション側のソースコードや設定ファイルからDaprに対する依存を避けることができます。Spring Bootが環境変数に応じて処理を切り替えやすい構造になっているおかげとも言えます。

もちろんコマンドライン引数でわざわざこんな長いURLを指定するのは面倒ですが、たとえばk8sのマニュフェストファイルで環境変数に指定する、というような状況であればこの形で指定することも妥当なやり方の一つになると思います。

まとめ

それでは今回の内容を簡単に振り返りましょう。

  • Dapr経由でアプリケーションを複数起動すると、お互いに相手の名前を指定して通信することができます
  • ローカル環境ではマルチキャストを使ってDapr同士が連携し、他のDaprを探しに行きます
  • アプリケーションは DAPR_HTTP_PORT という環境変数で自分のサイドカーであるDaprのポート番号を認識します
  • 環境変数で処理を切り替えやすい構造にしていれば、より環境への依存を下げることができます

正直、僕はこの機能を見ただけでもDaprを使ってみようという気持ちになりました。他のサービスディスカバリーのサーバなしで名前解決できるのって、何気に嬉しいですよね。

それでは!

Dapr Advent Calendar 2日目 - DaprでHello World

こんにちは、Dapr Advent Calendar 2日目です。3日坊主まであと1日です。

Hello Worldを動かしてみよう

前回はDapr CLIをインストールして動かしてみただけですが、今回はDaprとともにWebアプリケーションを起動して呼び出すところまで説明します。

動かすアプリケーションは、もちろん、プログラミング界で最も著名なアプリケーション「Hello World」です。

f:id:cero-t:20211202051328p:plain
今回作るアプリケーション

Webアプリケーションの作成

まずはWebアプリケーションの作成です。Hello Worldをサクッと作りましょう。

Spring BootでこのようなControllerクラスを作りました。

HelloController.java(抜粋)

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello, world!");
    }
}

ソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021

このソースコードの例ではMavenのマルチモジュール構成で、次のように作ってあります。

  • dapr-advent-2021
    • mvnw
    • pom.xml (親モジュール)
    • hello
      • pom.xml (子モジュール)
      • src

まずはWebアプリケーション「hello」を単体で起動します。

../mvnw spring-boot:run

アプリケーションが起動したらcurlコマンドでアクセスします。

curl localhost:8080/hello

無事に実行結果が表示されました。

{"message":"Hello, world!"}

f:id:cero-t:20211202051454p:plain
ここまでで作ったアプリケーション

Daprとともにアプリケーションを起動

続いて、Daprを使ってアプリケーションを起動します。

上で起動したアプリケーションを一旦停止させた後、次のコマンドで起動します。

dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080 ../mvnw spring-boot:run

コマンドの引数についての詳しい説明はあとで行います。

起動に成功すると、WebアプリケーションのログとDaprのログが両方ともコンソールに表示されるはずです。

(前半部分は割愛)
== APP == 2021-12-02 00:00:00.000  INFO 58739 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
== APP == 2021-12-02 00:00:00.000  INFO 58739 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
== APP == 2021-12-02 00:00:00.000  INFO 58739 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
INFO[0003] application configuration loaded              app_id=hello-app instance=macmini.local scope=dapr.runtime type=log ver=1.5.0
INFO[0003] actor runtime started. actor idle timeout: 1h0m0s. actor scan interval: 30s  app_id=hello-app instance=macmini.local scope=dapr.runtime.actor type=log ver=1.5.0
INFO[0003] dapr initialized. Status: Running. Init Elapsed 3540.333ms  app_id=hello-app instance=macmini.local scope=dapr.runtime type=log ver=1.5.0
INFO[0005] placement tables updated, version: 0          app_id=hello-app instance=macmini.local scope=dapr.runtime.actor.internal.placement type=log ver=1.5.0

ここまで表示されれば起動は完了です。

もしここで次のエラーが出た場合はDaprの初期化が済んでいない可能性が高いので dapr init コマンドを実行してください。

ℹ️  Starting Dapr with id hello-app. HTTP Port: 18080. gRPC Port: 60117
❌  fork/exec /Users/(ユーザ名)/.dapr/bin/daprd: no such file or directory

それでは起動したDapr経由で、Webアプリケーションにアクセスしてみましょう。curlコマンドでアクセスします。

curl localhost:18080/v1.0/invoke/hello-app/method/hello

URLが少しややこしいのですが、このURLの詳細についてもあとで説明します。

ふつうにアクセスした時と同じ実行結果が表示されました。

{"message":"Hello, world!"}

dapr list コマンドで、起動中のDaprアプリケーションを確認することができます。

dapr list

先ほど起動した hello-app が表示されます。

  APP ID     HTTP PORT  GRPC PORT  APP PORT  COMMAND               AGE  CREATED              PID    
  hello-app  18080      61164      8080      ../mvnw spring-bo...  1m   2021-12-02 00:00.00  59584  

なお dapr run コマンドで起動したアプリケーションは、特にコンテナの中で動いているなどではなく、普通のプロセスとして起動しているだけなので、Daprを経由せずにアクセスすることもできます。

curl localhost:8080/hello
{"message":"Hello, world!"}

Dapr経由でアクセスしようとしたけどなぜかアクセスできない、という時にはアプリケーションに直接アクセスして正常に動作しているかを確認してみてください。

f:id:cero-t:20211202051829p:plain
Daprを経由したアクセス

やったことの解説

それでは打ったコマンドなどについて説明します。

起動オプション

今回はDaprを次のコマンドで起動しました。

dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080 ../mvnw spring-boot:run

dapr run コマンドが、Daprの起動コマンドです。このコマンドでアプリケーションを起動します。ちなみに何の引数も指定せずに dapr run だけ実行してもDaprのプロセスを立ち上げることができます。

--app-id がアプリケーションのIDです。任意の名前を指定することができます。Daprはアプリケーションをこのapp-idで識別していて、アプリケーションをinvokeする時などに利用します。省略すると自動的にIDが発行されます。

--app-port がWebアプリケーションのポート番号です。Hello Worldアプリケーションは8080番ポートで起動しているため、そのポート番号を指定します。省略すると0番ポートが指定されてしまってアプリケーションにアクセスできなくなるため、必ず指定してください。

--dapr-http-port がDapr側のポート番号です。任意のポート番号を指定できます。Daprにアクセスする際にはこのポートを利用します。省略するとランダムなポート番号が指定されます。

../mvnw spring-boot:run の部分がアプリケーションの起動コマンドです。

アプリケーションの起動コマンドに引数を渡す場合は、起動コマンドの前に -- を追加する必要があります。たとえばアプリケーションの起動を java -jar コマンドで行う場合は次のようなコマンドになります。

../mvnw clean package
dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080 -- java -jar target/hello-1.0.0.jar

javaコマンドの前に -- をつけているところがポイントです。

その他のコマンド一覧

もちろんコマンド引数は他にもあり、このページに一覧が掲載されています。

https://docs.dapr.io/reference/arguments-annotations-overview/

また dapr run --help コマンドを実行すると、dapr run コマンドに対する引数の一覧を見ることができます。

Daprからアプリケーションへのアクセス

Dapr経由でWebアプリケーションにアクセスする際に、次のコマンドを用いました。

curl localhost:18080/v1.0/invoke/hello-app/method/hello

18080 がDaprのポート番号です。

/v1.0 がDaprのAPIバージョンで、今のところ v1.0 しかありません。

/invokeInvoke APIで、起動したWebアプリケーションにアクセスするAPIになります。他にもデータを保存する /state や、エンキューを行う /pubsub などのAPIがあります。

Invoke API/invoke/(app-id)/method/(アプリケーションのパス) という形式になります。今回は app-idhello-app を指定し、アプリケーションのパスは /hello を指定しました。これで localhost:8080/hello にアクセスするのと同じになります。

その他のアクセス方法

DaprのInvoke APIは、Dapr CLIを用いて実行することもできます。

dapr invoke --app-id hello-app --method hello --verb GET

このCLIはverbを指定しないと POST となるため、GETリクエストしたい場合は --verb GET を指定します。

また、Daprのv1.4からInvoke APIを別のURLでも起動できるようになりました。

curl -H 'dapr-app-id:hello-app' localhost:18080/hello
curl dapr-app-id:hello-app@localhost:18080/hello

Daprを運用する際に「パスの書き換えはできないけど任意のヘッダを付けることができる」ようなミドルウェアを利用している場合には、この形式のURLを用いてアクセスすることができるようになりそうですが、実際に使ったことがないのでどれくらい便利になるかはちょっと説明できません。てへぺろ

まとめ

それでは今回の内容を簡単に振り返りましょう。

  • dapr run コマンドでDaprとともにWebアプリケーションを起動することができます
  • Invoke APIを用いてDapr経由でWebアプリケーションにアクセスができます
  • WebアプリケーションそのものもローカルPC上で起動しているため、直接アクセスすることができます

今のところDaprの良さは何も引き出していませんが、次回からはDaprの便利な機能を利用していきます!

Dapr Advent Calendar 1日目 - Dapr CLIのインストール

こんにちは、Dapr Advent Calendar を作りました @cero_t です。このエントリーは Dapr Advent Calendar の1日目です。

Dapr Advent Calendarはじめました

Dapr (https://dapr.io/) とは、Microsoftが中心になって開発しているOSSの分散アプリケーションランタイム、Distributed Application Runtimeの略でDaprです。

様々なクラウドサービスやミドルウェアを良い感じに透過的に扱うことのできるプロダクトで、なかなか筋が良いのですが、何に使えるかよく分からないというか、そもそもどういうものか分かりづらい、発音の仕方すら分からないということもあって注目度はあまり高くなく、日本語の情報も多くありません。

ただ、このDaprはちょうど僕の感じていた課題を解決してくれそうなプロダクトだったので、わりと調べたり触ったりしていて、最近は仕事でも使い始めたところです。そんな時にちょうどAdvent Calendarの季節になりましたので、一人Advent Calendarになることを覚悟の上でカレンダーを立ててみました。

それでは12月25日まで、お付き合いいただければ、いや、きちんと25日まで継続できることを祈ってくだされば、ありがたいです!

あ、ちなみにDaprの発音ですが、英語読みすれば「だぱー」が近いと思いますが、このスペルだと日本語では「だっぱー」になるのが自然です。topを「とぷ」ではなく「とっぷ」と発音するのと同じです。とは言え英語の日本語読みなんてどうでも良いことですので、好きなように発音してもらえば良いと思います。

はじめに

このAdvent Calendarで僕が書くエントリーは、大きく分けて次の三部で構成する予定です。

  1. 基本編 - 各機能を使ってみる
  2. 分散環境編 - k8s上で各機能を使ってみる
  3. 実践編 - 実際に仕事で使ってみた感想や知見をまとめる

基本的な使い方くらい知ってらぁ! という方は12月の半ばあたりから読み始めてもらえれば良いかなと思います。

また、僕が書く記事は「macOS + Java + Spring Boot + Dapr」という構成を前提とします。DaprはOSや言語を問わず使えるサービスですので、他のOSや言語、フレームワークで読み替えてもらえれば良いと思います。

Daprの特徴

「それではDaprの使い方を見ていきましょう」

、、、と書き始めても誰もこの先を読み進めないと思うので、Daprの特徴とメリットを先に簡単に書いておきますね。長々と書いても仕方ないので、ここでは簡単に済ませます。

  1. サイドカーサービスとして動作する
    • Daprは、アプリケーションのプロセスとは別のプロセスとして起動するもので、アプリケーションとHTTP/gRPCプロトコルで通信することで機能を提供する
    • つまり、HTTPかgRPC通信さえできれば、どのような言語やフレームワークでも利用できる
  2. 様々なビルディングブロックを提供する
    • データストアや、pubsub(エンキュー、デキュー)、分散トレーシングなどの機能を提供する
    • サービスディスカバリーやルーティングに相当する機能も提供する
  3. 様々なクラウドサービスやミドルウェアを抽象化したAPIを提供する
    • たとえばメッセージングミドルウェアであれば、RabbitMQやKafka、Amazon SNS/SQSなどを独自APISDKに依存することなく同じAPIで利用できる
    • つまり、アプリケーション開発者はどのようなインフラの上で動作させるかを強く意識する必要がない
  4. ローカル環境でもk8s上でも運用できる
    • ローカル環境ではk8sやDockerイメージなどを作成することなく、分散アプリケーション開発ができる
    • 検証環境や本番環境などでk8s上で動作させる際には、ソースコードを変えることなく設定ファイルを書くだけで対応できる
    • さらにいえば、ローカル環境で開発する際にはDaprそのものを使わずに開発することもできる
  5. キューからデキューして行う非同期メッセージング処理を、通常のWeb APIとして実装できる
    • アプリケーション開発者はあくまでも「Web API」の開発にのみ注力できる
    • 開発時にキューを立てなくても良い

私見なので少し偏りがある気はしますが、総じて、開発と運用の境界にあるような部分を切り離すことで開発をシンプルにできるようになる、というところにメリットを感じています。

Dapr CLIのインストール

それではDaprの使い方を見ていきましょう。

Daprの利用にはDapr CLIが不可欠です。アプリケーションを起動する際にDaprをサイドカーとして起動したり、Dapr経由でアプリケーションを呼び出したり、起動中のアプリケーションの一覧を見たりするために利用します。

Daprを利用するためにはDockerが必要なため、まずはDockerをインストールしておきましょう

https://docs.docker.com/get-docker/

Dockerをインストールしたら、次はDapr CLIのインストールです。インストール方法はOSごとに異なるため、詳しくはドキュメントを参照してください。

https://docs.dapr.io/getting-started/install-dapr-cli/

たとえば、macOSでは次のコマンドでインストールできます。

curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash

Dapr CLIを触ってみる

Dapr CLIのコマンドを簡単に触ってみましょう。まずはバージョンの確認です。

dapr --version

出力結果はこうなります。

CLI version: 1.4.0 
Runtime version: n/a

まだ環境の初期化が済んでいないため、Runtime versionはn/aとなっています。

Daprを動作させるため、初期化を行いましょう。

dapr init

このコマンドで、ローカル環境に設定ファイルなどが作成され、またDaprの動作に必要なDockerコンテナが作成されます。

初期化が完了したら、もう一度バージョンを確認しましょう。

dapr --version

先ほどと出力結果が変わっているはずです。

CLI version: 1.4.0 
Runtime version: 1.5.0

また、Dockerのコンテナ一覧も確認してみましょう。

docker ps

dapr_placement、dapr_zipkin、dapr_redisの3つのコンテナが起動しているはずです。

CONTAINER ID   IMAGE               COMMAND                  CREATED              STATUS                        PORTS                              NAMES
f7dc118a4bbc   daprio/dapr:1.5.0   "./placement"            About a minute ago   Up About a minute             0.0.0.0:50005->50005/tcp           dapr_placement
46555ffc1d22   openzipkin/zipkin   "start-zipkin"           About a minute ago   Up About a minute (healthy)   9410/tcp, 0.0.0.0:9411->9411/tcp   dapr_zipkin
c55c6700a22c   redis               "docker-entrypoint.s…"   About a minute ago   Up About a minute             0.0.0.0:6379->6379/tcp             dapr_redis

この辺りの使い方は、後のエントリーでおいおい説明していきます。

最後にdapr関連のファイルができていることも確認しましょう。

ls ~/.dapr   

設定ファイルや実行ファイルがいくつか追加されています。

bin      components  config.yaml

ちなみにアンインストールしたい場合には、dapr unistall コマンドを利用します。

dapr uninstall --all

--all オプションを指定しなければ、~/.dapr フォルダや、zipkinとredisのDockerイメージも残ってしまいますが、--all をつけることですべて削除されます。

まとめ

今回はここまでです。今回の内容を簡単に振り返りましょう。

  • Daprはサイドカーサービスとして動作し、様々なクラウドサービスやミドルウェアを共通のAPIで利用できるようになります
  • DaprとアプリケーションはHTTP/gRPCで通信するため、開発者は好きな言語やフレームワークを利用してアプリケーション開発ができます
  • まずはDockerとDapr CLIをインストールしましょう

まだCLIをインストールしただけの小さな一歩ですが、ここから続くAdvent Calendarを読むために欠かせない作業です。

それでは、これからも引き続きDapr Advent Calendarをよろしくお願いします。継続することを祈っていてください!!(!?)

Bootiful SQL Template 2.1.0でRecord対応をしました。

年に一回くらい、突然Javaの話をブログに書き始めるJava Championの @cero_t です。

数年前、「Spring BootでもSQLファイルを使いたいな」と思って勢いで作った Bootiful SQL Template ですが、何気に仕事で使うことがたまにあるので細々とメンテナンスを続けています。

最近、APIを全面的に見直してFluent API化したバージョン2.0.0をリリースし、日本語のドキュメントも書き始めました。

sqltemplate/README_ja.md at main · cero-t/sqltemplate · GitHub

今回は、これをRecordに対応させたというお話です。Record対応されたバージョンは2.1.0になります。

TL;DR

  • JdbcTemplateがRecordに対応してないから自分で対応したよ
  • Java16から追加された Class#getRecordComponents とリフレクションを使えば良いよ
  • ライブラリをJava8でも使えるようにするために、Record関連の処理をリフレクションにしたよ

はじめに

Recordとは

Recordは、Java 16から使えるようになったデータクラスというか値オブジェクトというかエンティティクラスというかDTOというか、まぁなんかそういうヤツを良い感じに作れるようにする機能です。説明が雑。

Recordの定義はこうなります。

public record EmpRecord(Integer empno,
                        String ename,
                        String job,
                        Integer mgr,
                        LocalDate hiredate,
                        BigDecimal sal,
                        BigDecimal comm,
                        Integer deptno) {
}

このようにフィールドを列挙するだけで、そのフィールドすべてを使ったコンストラクタが自動的に生成されるため、これを使ってインスタンスを作成します。

EmpRecord emp = new EmpRecord(1000, "cero-t", "MANAGER", 7839,
        LocalDate.of(2004, 4, 1), new BigDecimal(4000), new BigDecimal(400),
        10);

Recordの値はコンストラクタでしか設定できません。そのためRecordのインスタンスはイミュータブルということになります。

値を取り出す時は、フィールド名と同じ名前のメソッドを使います。

System.out.println(emp.ename()); // cero-tが出力される

Recordを使えば、getterやsetterをたくさん書かなくて良くなりますし、toStringやhashCodeメソッドも動的に生成されるようになって生産性アゲアゲですね! まぁもともと自動生成してましたけど。

先月LTS版のJava 17がリリースされたので、このRecordを使う機会も増えることになりそうですよね。

Spring BootのRecord対応

そんなRecordですが、じゃぁSpring Bootで使えるのかって話ですよね。

まずspring-webでは、既にRecordが使えます。JSONJavaオブジェクトを相互変換するJacksonがRecordの対応をしていて、spring-webではそれを使っているためです。ちょっとどのバージョンから使えるようになったかは確認してないですけど。

ただ、DBアクセスをするライブラリであるJdbcTemplate(spring-jdbc)はRecordには対応していません。既に試された人がいらっしゃいました。

ashishtechmill.com

JdbcTemplateは新しい機能が追加されることがほとんどなく、Spring Framework 5.3でStreamを返すAPIが追加されただけでも、「クララが立った!」に匹敵するレベルで興奮しましたね。たとえ話が昭和ですね。

そんなわけで、たとえJdbcTemplateがRecordに対応しなくても、それを対応するのがBootiful SQL Templateの役割なので、今回実装してみようと思い立ちました。

Recordクラス対応の流れ

Bootiful SQL TemplateをRecordに対応させる流れは、次のようになりました。

  1. 与えられたクラスがRecordか否かを判定する
  2. Recordのフィールドの一覧を取り出す
  3. Recordのコンストラクタを取り出す
  4. Recordのコンストラクタを使ってインスタンスを作成する
  5. Recordから値を取り出す側は?
  6. Java 16以降に依存しないコードに書き換える

順を追って見ていきます。

1. 与えられたクラスがRecordか否かを判定する

まずは与えられたクラスがRecordなのか、ただのValue Objectなのかを判定する必要があります。RecordであればRecord対応の処理を行い、Recordでなければ既存の処理を行う、というような分岐をしたいためです。

Recordクラスは java.lang.Record を継承したクラスとなるため、次のようなコードで判定できます。

if (Record.class.isAssignableFrom(targetClass)) {
    return new RecordMapper<>(targetClass); // Recordを扱う処理
}

return new BeanMapper<>(targetClass); // 既存処理

ここで instanceof も使えるのですが、なんやかんや色々あって僕はいつも isAssignableFrom を使うようにしています。

2. Recordのフィールドの一覧を取り出す

次にRecordのフィールドの一覧を取得する方法を確認します。Java 16でClassクラスに新たに追加された getRecordComponents メソッドが使えるようです。

RecordComponent[] components = targetClass.getRecordComponents();

// フィールドの数を取る。最初の例に書いたEmpRecordなら 8
System.out.println(components.length); 

// 最初のフィールドの名前を取る。EmpRecordなら "empno"
System.out.println(components[0].getName());

// 最初のフィールドの型を取る。EmpRecordのempnoなら java.lang.Integer
System.out.println(components[0].getType());

説明はコメントで書いたので、割愛しますね。
どうあれRecordの情報は簡単に取れるようです。複雑な継承関係をあまり考えなくて済むので、通常のクラスよりも扱いが楽ですね。

3. Recordのコンストラクタを取り出す

続いて、コンストラクタの取得です。
今回やりたいことを簡単に言うと、データベースの検索結果である ResultSet から値を取り出して、Recordのコンストラクタにそれを渡してオブジェクトを作成する、ということになります。そのためにコンストラクタが必要なのです。

先にも書いたとおり、Recordはフィールドを列挙するだけでそのフィールドすべてを使ったコンストラクタを自動的に生成します。これはcanonical constructorと呼ばれています。コンストラクタがcanonical constructor 1つのみであれば、次のようなコードでコンストラクタを取得できます。

targetClass.getDeclaredConstructors()[0]

ただ、Recordでは自分でコンストラクタを追加することもできるのです。

public record EmpRecord(Integer empno,
                        String ename,
                        String job,
                        Integer mgr,
                        LocalDate hiredate,
                        BigDecimal sal,
                        BigDecimal comm,
                        Integer deptno) {
    public EmpRecord(String ename) {
        this(null, ename, null, null, null, null, null, null);
    }
}

こうなると、上に書いた方法でコンストラクタを取得しても、canonical constructorが取得できる保証はないですよね。そのため、フィールドの型を列挙してコンストラクタを取得することにしました。こんなコードになります。

Class<?>[] parameterTypes = Arrays .stream(EmpRecord.class.getRecordComponents())
        .map(RecordComponent::getType)
        .toArray(Class<?>[]::new);
Constructor<?> constructor = targetClass.getDeclaredConstructor(parameterTypes);

ちなみに後から知ったのですが、Class#getRecordComponentsJavadocにもこれと全く同じ実装が掲載されています。みんなやることは同じだ。

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#getRecordComponents%28%29

Java標準のコードと一致してたら、著作権違反だって訴えられちゃうかな? #やめなさい

4. Recordのコンストラクタを使ってインスタンスを作成する

そうやって取得したコンストラクタに、データベースの検索結果である ResultSet から取り出した値を渡せばインスタンスを生成できます。

おおむねこういうコードになります。

Object[] params = new Object[constructor.getParameterCount()];

// 何やかんやResultSetから値をきちんと取り出してparamsに代入する

return constructor.newInstance(params);

実際には「何やかんや」の部分で、大文字小文字を無視したり、スネークケースとキャメルケースを同一視するみたいなコードを入れてるのですが、あくまでもRecordに関係する処理に限ると、この部分だけになります。

これで、データベースから検索してその結果からRecordのインスタンスを作成する部分ができました。
もう少し詳しく処理を見たい人は、このコミットログを見てください。

https://github.com/cero-t/sqltemplate/commit/2910ae82fc8b3e46149635e5c237d74081cf5d98

5. Recordから値を取り出す側は?

ここまでの処理で、SELECT文に相当する処理は書けたのですが、INSERT文についても考えなくてはなりません。INSERTの処理はここまでのSELECTの処理とは逆で、Recordのインスタンスから値を取り出す必要があります。

・・・なのですが、これは既存のBootiful SQL Templateのコードでも問題なく動作しました。
というのも、JdbcTemplateでは元々データクラスのオブジェクトに対してprivateフィールドからアクセサメソッドを経由して値を取り出す仕組みがあり、その仕組みがRecordにも機能しているためです。

なのでINSERT側は実装なしで対応済みということになりました。

6. Java 16以降に依存しないコードに書き換える

今回このようにしてRecord対応を行いましたが、Java 16以降で追加されたAPIやクラスを使っているため、これをJava 15以前のバージョンで動作させようとすると、たとえば java.lang.Record#isAssignableFrom を呼んだ際などに ClassNotFoundException が発生してしまいます。

Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Record
    at ninja.cero.sqltemplate.core.mapper.MapperBuilder.mapper(MapperBuilder.java:20)
    at ninja.cero.sqltemplate.core.executor.ArrayExecutor.forList(ArrayExecutor.java:37)
    at ninja.cero.sqltemplate.example.SampleProcess.gettingStarted(SampleProcess.java:25)
    at ninja.cero.sqltemplate.example.SampleProcess.process(SampleProcess.java:123)
    at ninja.cero.sqltemplate.example.SampleApplication.main(SampleApplication.java:22)
Caused by: java.lang.ClassNotFoundException: java.lang.Record
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    ... 5 more

Bootiful SQL Template自体はJava 8でも動くようにしたいため、ここに対応する必要があります。

①局所的にリフレクションにする(ビルドはJava 17、sourceとtargetは1.8)

手っ取り早く対応するのであれば、isAssignableFrom の判定を行うところを、次のように修正すれば動きます。

try {
    if (Record.class.isAssignableFrom(targetClass)) {
        return new RecordMapper<>(targetClass); // Recordを扱う処理
    }
} catch (ClassNotFoundException e) {
    // ignore
}

return new BeanMapper<>(targetClass); // 既存処理

このようにすればJava 15以前ではClassNotFoundExceptionが発生して既存処理を実行することになるため、Record対応の処理は実行されません。そのため、これ以降でClassNotFoundExceptionが発生することもありません。

この修正を行う場合、Record対応の処理(RecordMapperの実装)のコンパイルにはJava 17が必要となるため、Bootiful SQL TemplateのビルドはJava 17で行い、sourceとtargetを1.8にすれば、「Java 8でも動く、Java 17でビルドされたライブラリ」ができます。

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

これは一つの方法としてアリですが、なんかちょっとしたはずみで動かなくなりそうですし、今回はもう少し慎重な方法を選びました。

Java 16以降で追加されたAPIをすべてリフレクションにする(Java 8でビルド)

より慎重を期すために、Bootiful SQL Template自体をJava 8でビルドするようにしました。そうすれば、うっかりJava 16に以降で追加されたAPIを使ったコードを書いてしまっても、コンパイルエラーが起きて気づけるためです。

そのために、Java 16以降で追加されたAPIはリフレクションで扱うことになります。まず上に書いたisAssignableFromのコードは次のようになります。

try {
    Class<?> recordClass = Class.forName("java.lang.Record");
    if (recordClass.isAssignableFrom(targetClass)) {
        return new RecordMapper<>(targetClass); // Recordを扱う処理
    }
} catch (ClassNotFoundException e) {
    // ignore
}

return new BeanMapper<>(targetClass); // 既存処理

このコードであればJava 8でもコンパイルも実行もでき、Java 16以降で使えばRecord対応されるようになります。

また、この先の処理も同じように文字列を使ってリフレクションで操作することになります。この辺りはJacksonの実装を参考にして、次のようなコードにしました。

Method classGetRecordComponents = Class.class.getMethod("getRecordComponents");
Class<?> c = Class.forName("java.lang.reflect.RecordComponent");
Method recordComponentGetName = c.getMethod("getName");
Method recordComponentGetType = c.getMethod("getType");

Object[] components = (Object[]) classGetRecordComponents.invoke(targetClass);
paramTypes = new Class<?>[components.length];
for (int i = 0; i < components.length; i++) {
    paramTypes[i] = (Class<?>) recordComponentGetType.invoke(components[i]);
}

// この後もう少しコードが続く

リフレクションのための処理をリフレクションで行うという、なかなか妙な感じになっていますが、こういう事をやろうとする時のあるあるな気がしますね。

どうあれこれで、Bootiful SQL Template本体はJava 8でビルド、テストができるようになり、Java 16以降で利用した際にはRecordが使えるようになりました。
もう少し詳しく処理を見たい人は、このコミットログを見てください。

https://github.com/cero-t/sqltemplate/commit/2d5a7e07693a0faf5ad47841767552da78268bea

無事に完成しました

このような対応をして、Bootiful SQL TemplateのRecord対応は無事に完成しました。

ちなみに、今回は作業を始めてからMaven Centralリポジトリにアップするところまで、3時間弱と意外と短時間で済みました。リフレクションの知識さえあれば、Record関連の処理はわりと素直に書けるように思います。

さらにちなむと、このブログを書き始めてからここまで4時間くらい掛かりました。実装よりも説明のほうが大変ですよね・・・。

そんなわけで、このRecord対応が少しでも皆さんのお役に立てば、ブログを書いたかいがあるってものです。

それではね、See You!

三井住友のナンバーレスカードを作りました。

前から思ってるんですけど、コンビニってなんかワクワクするというか、購買欲をそそるような陳列をしてないですか?
あまり具体的にどうとは言えないんですけど、スーパーに比べてコンビニのほうが派手で目を引くパッケージの商品を、煽るように陳列してあるなと思うのですよね。

そういうコンビニの戦略にまんまと引っかかり、用もないのにぶらりと立ち寄ってはお菓子をたくさん買ってしまう @cero_t です。皆さんいかがお過ごしでしょうか。

今回は、コンビニとSBI証券のために三井住友のナンバーレスカードを作ったよというお話です。

陸マイラーがコンビニに行くと

僕とかって陸マイラーなので、地上で行うすべての購買行動で航空会社のマイルが効率よく貯まるようにしようとしています。

たとえばコンビニだと、

という使い分けになります。
いやこれホントにこういう風に使い分けてるんですよ、実際。

そんなにマイル(ポイント)をセコセコ貯めるならそもそもコンビニを使わずスーパー使えやって言われそうですし、まぁ実際言われてるんですけど、夜型というか深夜型の生活をしているとコンビニの利用頻度って高くなるし、あとほら、コンビニって冒頭に書いてる通りワクワクするじゃないですか。ワクワク感。

まぁでも正直、支払い方法を使い分けるのは面倒くさいなぁと思っていたところ、三井住友のナンバーレスカードだったら上のどの方法よりもマイルが貯まることに気づいたのです。

いや、なんか何をどう書いてもステマっぽくなってしまうんですけど、特にステマでもダイマでもないお得情報のつもりなのです。

コンビニでの5%還元がやばい

三井住友ナンバーレスカードは、クレジットカードに番号が書いておらず、アプリで番号を確認するタイプのカードです。なんか最近の流行っぽいですね。
別にそんなの大したメリットだと思ってなかったのであまり注目してなかったんですが、たまたま見たサイトで「コンビニ利用で5%還元」って書いてあったのを見つけてオッとなって、よく調べてみることにしました。

www.smbc-card.com

ちなみに広告に出ている中条あやみは出身中学が同じなので、それだけで好感が持てます。どうでも良い話ですね。

セブン、ファミマ、ローソン、マクドで使えばポイント2.5%還元、タッチ決済だとさらに+2.5%されて、合計5%還元。
タッチ決済はスマホでも大丈夫なので、物理カードを持ち歩く必要はありません。

還元ポイントは「Vポイント」で貯まって、ANAマイルに交換するなら2ポイント = 1マイルになるため、2.5%分のマイルが還元されるという計算になります。

これまで僕が使っていた支払い方法だと、一番マイルが貯まるファミマでも2%のマイル還元(Tポイントカード併用でも2.25%)だったのでそれを超えていますし、何よりコンビニごとに決済方法を変えなくて済むのでシンプルです。
さらにファミマのTポイントカードとか、ローソンのPontaカードも併用したら、2.75%分のマイル還元になりますね。

コンビニでの支払い方法が統一できて、マイルも効率よく貯まるので、こりゃ作るしかないなとなったわけです。
もちろんマイルに限らず、還元されたポイントをそのまま支払いに使ってもOKです。

SBI証券のためにゴールドにした

ナンバーレスカードは、一般カード(年会費無料)とゴールドカード(年会費5500円)があります。

ゴールドカードの年会費は初年度無料で、さらに1年で100万円使えば以降の年会費が永年無料になるので、ゴールドカードにすることもやぶさかではない気持ちになったのですが、とはいえ別にコンビニでの還元率は変わらないし、年会費無料の一般カードで決まりでしょ、って考えていました。

ただもう一つ、ゴールドカードのほうはSBI証券の積立投信に使うと2%のポイント還元(来年からは1%還元)になるというキャンペーンがありました。

go.sbisec.co.jp

いま僕はSBI証券ANA VISAカードを使って積立投信をしていて、月5万円の積み立てで毎月450マイル(来年からは毎月150マイル)貯まっているのですが、これをナンバーレスカードのゴールドに切り替えれば毎月500マイル(来年からは毎月250マイル)貯まることになります。微妙な差だとはいえ、来年からは年間1200マイルの差になるので、だったらゴールドカードにしようかなとなりました。

そんなわけでゴールドカードを作って、最初の1年だけ100万円使って永年無料にして、以降はコンビニとSBI証券専用にしよう、という結論になりました。

au PAYのためにマスターカードにした

三井住友カードはVISAとマスターカードの2つから選べます。この2択だと、だいたいの人は名前の通ってるVISAを選ぶと思うのですが、今回はマスターカードを選びました。*1
理由は「au PAYにチャージできるから」という一点だけです。au PAYは陸マイラー的には少し効率のよいPayなので、いまはメインで使っています。東京や神奈川では納税にも使えるようになっていて、納税額の1.25%分のマイルが貯まるとか胸熱ですよね。

そんなau PAYですが、チャージに使えるクレジットカードには制限があり、VISAやJCBでは一部の発行元のカードしか使えません。ただマスターカードならどこが発行したカードでもチャージに使えるので、今回はマスターカードを選んだのです。実際に試してみたところ問題なくチャージできました*2

ちなみにナンバーレスカードについて調べると「マスターカードでないとiPhoneでタッチ決済できない」という情報がいくつか見つかりますが、今年の5月頃にVISAのタッチ決済もiPhoneに対応したので、この件についてはVISAでもマスターカードでも構わないと思います。

まとめ

  • コンビニのヘビーユーザーは、ナンバーレスカードを作ろう
  • SBI証券でクレカ積立をやってるなら、ゴールドカードを選ぼう。そして頑張って100万円使って年会費無料にしよう
  • au PAYを使うならマスターカードを選ぼう

ちなみにいま紹介キャンペーンをやっていて、紹介した人にも紹介された人にもポイントが貯まるようになっています。
この記事を読んでナンバーレスカードを作りたくなったなという人は、このURLから発行してもらえると嬉しいです。

https://www.smbc-card.com/olentry/affiliate/online_entry.do?bno=03400708611

あ、いえ、この紹介URLのためにこのエントリーを書いたわけじゃないですからね!

よろしくね!!

*1:申請時に間違ってVISAを選んでしまったので、申し込み後に電話してマスターカードに変えてもらいました

*2:初回チャージ時に不正利用防止機能が働いてカードを止められちゃいましたけど