Uzabase Tech Blog

SPEEDA, NewsPicks, FORCASなどを開発するユーザベースの技術チームブログです。

EnvoyをFront Proxyとして利用する

こんにちは、ユーザベースのProductチームでSREをやっています阿南です。弊社ではKubebrnetes + Istioを利用してサービスメッシュの構築、マイクロサービスの運用を行っています。Istioでは sidecar proxyとしてEnvoyが利用されていますが、このEnvoyをFront Proxyとしても利用できないかと思い、よく使われる設定について調べてみました。下記目次です。

少し長いので、気になる部分だけでも読んで頂ければ幸いです。 今回作成した全体の設定はこちらにありますので、適宜参照してください。

envoy proxy for frontend · GitHub

また、実際にEnvoyを起動して確認してみたい方は、下記に環境構築について記載しています。

GitHub - tanan/migrate-from-apache-to-envoy

EnvoyをFront Proxyとして利用するメリット

(Front Proxyに限らずですが...) Envoyを利用することで下記のようなメリットがあります。

  • 柔軟なLoadBalancing機能
    • Blue/GreenやWeightを利用して簡単にBalancingの設定を変更することができます。また、rate limitやcircuit breakerの機能も有しているため、適切に設定を行うことでエラーが全体に波及するのを防ぐことができます。
  • control-planeやファイルベースのdynamic configurationが利用できる
    • Apache等では明示的に各プロセスに対してreload/restart等の動作を実行する必要がありますが、Envoyではcontrol-planeを利用して設定をDynamicに反映することができます。Apacheでもupstream先を有効/無効にする等は動的に設定できますが、プロセスがrestartすると状態が消えてしまいます。
  • Observabilityを高めるための他ツールとの連携のしやすさ
    • prometheus / grafana / jaegerとの連携が簡単に実現できるエコシステムが整っています。

Envoyのバージョン

envoyバージョンはv1.16.0-devで、v3 APIを利用しました。

Front ProxyのためのEnvoy Configuration

現在の弊社環境はざっくり下記のような構成になっています。WebサーバとしてApacheを利用し、元々開発されてきたtomcatのモノリスアプリケーションと新しく開発しているマイクロサービス群にPathベースでルーティングをしています。(Apacheとtomcatはon-premiseに構築されています。)

f:id:tanan55:20200927132457p:plain
architecture

今回は、このApacheをEnvoy Proxyに代替するために、基本的な設定を行いましたので、以降でそれぞれの設定を見ていきたいと思います。

80(HTTP),443(HTTPS)でアクセスを受け付ける

listenerを定義することで特定のポートでアクセスを受け付ける事ができます。

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 80 }

TLSの設定をする場合は、filter_chainsにtls_contextを設定します。

tls_context:
  common_tls_context:
    tls_certificates:
      - certificate_chain:
          filename: "/etc/envoy/certs/example-com.crt"
        private_key:
          filename: "/etc/envoy/certs/example-com.key"

ここではfilenameを指定していますが、証明書の内容を直接記載することも可能です。

Pathベースのルーティングを設定する

Routingの設定はlistenerのfilter部分に記載します。

route_config:
  name: backend_route
  virtual_hosts:
  - name: backend
    domains:
    - "www.example.com"
    - "example.com"
    routes:
    - match:
        prefix: "/service/2"
      route:
        prefix_rewrite: "/"
        cluster: service2
        hash_policy:
          - cookie:
              name: balanceid
              ttl: 0s
    - match:
        prefix: "/"
      route:
        cluster: service1

上記の設定は、

  • ドメインはwww.example.com,example.comのみ許可
  • pathが/service/2にマッチした場合、service2のclusterを参照する
  • pathが/にマッチした場合、service1のclusterを参照する

という設定になります。ちなみにpathのルールは上から順に判定されるようで、matchの順番を逆にするとすべてservice1にリクエストが送られるので注意が必要です。

clusterではupstreamのendpointやbalancing ruleを定義します。 lb_policyには、ROUND_ROBIN の他に、 LEAST_REQUEST や後述の RING_HASH などが指定できます。

clusters:
  - name: service1
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: service1
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: service1-1
                port_value: 8080
        - endpoint:
            address:
              socket_address:
                address: service1-2
                port_value: 8080

また、 https_redirect をtrueにすることで、HTTPSへのリダイレクトが可能です

routes:
- match:
    prefix: "/"
  redirect:
    https_redirect: true

レスポンスに特定のヘッダーを付与する

response_headers_to_add に追加したいヘッダーを記載することができます。 注意点としては、v1.8でRouteActionレベルでは本パラメータがdeprecatedになりましたので、Routeレベルで設定する必要があります。 その他、リクエストヘッダーに特定の値を付与できる request_headers_to_addrequest_headers_to_remove もここで指定できます。

response_headers_to_add:
  - header:
      key: "x-frame-options"
      value: "sameorigin"
  - header:
      key: "x-xss-protection"
      value: "1; mode=block"

Healthcheckに失敗した場合、upstreamの対象から除外する

普段の運用ではupstreamに複数のエンドポイントを設定して、正しく応答が返せない場合はリクエストが割り振られないようにしたいケースがよくあると思います。Envoyではhealth_checks をclusterの設定に追加することでヘルスチェックを実施することができます。

health_checks:
  - timeout: 1s
    interval: 10s
    interval_jitter: 1s
    unhealthy_threshold: 6
    healthy_threshold: 2
    http_health_check:
      path: "/healthz"

jitterはinitial_jitterinterval_jitterが指定でき、ヘルスチェックがすべて同時にリクエストされないように、ばらつきを与えたい場合に設定します。 ヘルスチェックに失敗したupstreamはendpointのリストから除外され、healthy_thresholdの数だけ成功するとhealthyとみなされ再度リストに追加されます。 ちなみに初回起動時はhealthy_thresholdに関係なく1回の成功でhealthyとみなされます。

Cookieをもとに、upstreamを固定(sticky session)する

アプリケーション内にセッション情報を持っているようなシステムでは、純粋にROUND ROBINでリクエストが振られるとアプリが正常に動作しなくなってしまいます。そのため、Session Cookie(sessionが続く間だけ有効なcookie)を使って、ユーザーのリクエストを同じupstreamにルーティングするようにするsticky sessionが利用されます。 Envoyではclusterでlb_policy: RING_HASHを指定した上で、下記を設定することにより、sticky sessionが実現できます。

hash_policy:
  - cookie:
      name: balanceid   ## nameは何でもよい
      ttl: 0s

この設定でのポイントはttlです。ttlの有無により下記のように動作が異なります。

  • ttlが指定されない場合、cookieはつくられません。既にあればその値が利用されます。
  • ttlが指定されている場合、かつ、cookieがない場合はcookieを生成します。また、ttlが0sのときはsession cookieになります。

詳細はhashpolicy-cookieのDocumentを参照して下さい。

まとめ

EnvoyをFront Proxyとして利用するための設定を見ていきました。EnvoyのDocumentの読み方を覚えるまでに少し時間がかかりましたが、無事意図通り動作するよう設定できました。 実際に利用する場合は、control-planeを導入してdynamic configurationで設定反映をすることが多いかと思いますが、staticな設定を一度書いてみると理解が早まると思いますので、興味のある方はぜひやってみて下さい。

仲間募集!!

ユーザベースでは、「経済情報で、世界をかえる」 というミッションの実現に向けて、日々邁進しており、SREのメンバーを募集しています。 技術的にも様々なことにチャレンジしてますので少しでも興味を持ってくださった方はこちらまで!

Kubernetes で運用する JVM アプリケーションの OutOfMemoryError に備える

こんにちは。SPEEDA 開発チームの old_horizon です。

JVM アプリケーションの運用について回るのが、OutOfMemoryError (以下 OOM) への対処です。
しかし実際に発生した際に、適切なオペレーションを行うのは意外と難しいのではないでしょうか。

特に本番環境では、まず再起動して復旧を急ぐことも多いかと思います。しかし、ただそれを繰り返すばかりでは原因がいつまでも特定できません。

今回は Kubernetes で運用する JVM アプリケーションに対して、ダウンタイムを抑えつつ調査に役立つ情報を自動的に収集する仕組みを構築してみたいと思います。

環境構築

実際に構築してみたサンプルを、こちらのリポジトリに用意しました。

https://github.com/old-horizon/k8s-oom-sample

動作確認は以下の環境で行っています。

  • Ubuntu 18.04
  • MicroK8s 1.18.6

clone して kubectl apply -f kubernetes/ を実行すると oom-sample ネームスペースに各種リソースが作られます。
(おそらく初回は途中で失敗するため、二度実行してみてください)

次章より、このサンプルを使って解説を進めていきます。

OOM 発生時に、自動的にヒープダンプを取得しコンテナを再起動する

ヒープダンプがあれば、Eclipse Memory Analyzer 等で解析してリークしていたオブジェクトを特定できます。

しかし OOM で処理が長時間滞留した場面では、さらに復旧を遅らせてまでダンプを取得するのに抵抗を感じ、再起動を優先させることもあるかと思います。

とはいえ通知を受けて手動で対応している限り、多少なりとも遅延は発生しているわけです。この際すべて自動化して、焦らなくてもユーザーへの影響が最小限に収まるようにしましょう。

java コマンドのオプション指定

Dockerfile (ktor-oom/Dockerfile) で、java コマンドに次のオプションを指定しました。

  • -XX:+ExitOnOutOfMemoryError
    OOM 発生時に JVM が終了する

  • -XX:+HeapDumpOnOutOfMemoryError
    OOM 発生時にヒープダンプを出力する

  • -XX:HeapDumpPath
    -XX:+HeapDumpOnOutOfMemoryError によるヒープダンプの出力先を指定する

JVM が終了すると、Kubernetes の自己修復機能によりコンテナが再起動されます。

補足

  • ヒープダンプは java_pid(プロセス ID).hprof のファイル名で出力されます。
    なお同名のファイルがすでに存在する場合は上書きされません。
    コンテナ環境の場合、プロセス ID が常に一定ゆえファイル重複が起きやすいためご注意ください。

  • -XX:OnOutOfMemoryError で OOM 発生時に任意のコマンドを実行できます。
    上記のオプションと併用可能です。

ヒープダンプ出力先のボリュームをマウント

ヒープダンプは Pod が存在する間のみ存続する emptyDir ボリュームに出力します。

Dockerfile (ktor-oom/Dockerfile) で、ヒープダンプ出力先に /dump を指定します。

-XX:HeapDumpPath=/dump

Deployment (kubernetes/ktor-oom.yml) で /dump にボリュームをマウントします。

        volumeMounts:
        - mountPath: /dump
          name: dump-volume
      volumes:
      - name: dump-volume
        emptyDir: {}

これにより、コンテナ再起動前に出力されたダンプが取得できるようになります。

readinessProbe によるヘルスチェック

コンテナの再起動直後は、リクエストを処理できない場合があります。
readinessProbe で正しく処理できる状態になったことを確認してから、Service のルーティング対象に復帰するようにします。

Deployment (kubernetes/ktor-oom.yml) より抜粋

        readinessProbe:
          httpGet:
            port: 8080
            path: /v1/ping

レプリカ数の確認

基本的なことですが、常に使用可能な Pod が複数存在するようにしましょう。
今回はサンプルなのでレプリカ数は 2 にしています。

Deployment (kubernetes/ktor-oom.yml) より抜粋

spec:
  replicas: 2

Prometheus + Grafana で JVM のメトリクスを可視化する

ライブラリ・フレームワークの設定ミスによるリークは、ネット上にも情報が多いためヒープダンプの解析結果から比較的特定しやすい印象があります。

一方で社内で実装したプロダクションコードでリークが起きている場合、先人の知恵に頼ることはできません。
そのため地道にコードを読む必要はありますが、ヒープ使用量の推移からリークが疑われる事象が発生するタイミングを特定できれば、調査範囲を絞ることができます。

そこで Prometheus + Grafana により JVM のメトリクスを可視化して、その記録をもとに疑うべきポイントに目星がつけられる状況を目指します。

JVM アプリケーションの Java agent に JMX Exporter を指定する

JMX とは

JVM アプリケーションでは、JMX (Java Management Extensions) という監視・管理フレームワークが利用できます。
以下に例を挙げますが、標準でかなり多くの指標を取得することができます。

  • 領域ごとのメモリー使用量
  • ロード済みクラス数
  • スレッド数

JMX から取得できた情報を Prometheus で収集するには、所定のテキストフォーマットに変換して HTTP で配信する必要があります。
今回アプリケーション本体には手を加えず、実行時にこの機能を追加するために Java agent を使います。

Java agent とは

Java agent は J2SE 5.0 で java.lang.instrument パッケージと同時に追加された仕組みです。
Javadoc によると、正式名称は「Javaプログラミング言語エージェント」といったところでしょうか。

-javaagent:(エージェント JAR ファイルへのパス) を指定して java コマンドを実行すると、main に先んじてエージェント JAR ファイル内の premain が呼び出されます。
このタイミングでバイトコード操作が可能になるため、AspectJ の動的ウィービングなどで利用されています。

いわゆる黒魔術を実現するための仕組みですが、応用することでアプリケーションの起動前に任意の処理を差し込むことができます。

Dockerfile の編集

Prometheus が提供する JMX Exporter を Java agent に指定してアプリケーションを起動します。
するとアプリケーションの main 実行前に、指定されたポートで JMX メトリクスを公開する HTTP サーバーが立ち上がります。

まずは JAR ファイルをダウンロードし、設定ファイルである config.yaml を作成します。
今回の用途ではデフォルト設定で問題ないため、中身は空のままです。

ADD https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.13.0/jmx_prometheus_javaagent-0.13.0.jar .
RUN touch config.yaml

最後に java コマンドで Java agent を指定してアプリケーションを起動します。

-javaagent:./jmx_prometheus_javaagent-0.13.0.jar=9100:config.yaml

これで 9100 ポートで JMX メトリクスが公開されました。

詳細は Dockerfile (ktor-oom/Dockerfile) をご覧ください。

Prometheus の Service Discovery で動的に監視対象を指定する

Service Discovery について

Prometheus は、自身が監視対象からデータを収集する Pull 型アーキテクチャを採用しています。
したがって、基本的には監視対象への接続情報をあらかじめ知っておく必要があります。
しかし Kubernetes で運用するアプリケーションの場合、自動的に Pod のスケールや再配置が行われるため接続先を静的に定義しておくことはできません。

このように接続先が動的に変わる場合には、Service Discovery が便利です。
その名の通り、Prometheus 自身が監視対象を検出する機能です。Kubernetes の他にも Azure VM, AWS EC2, GCP GCE といった主要なクラウド仮想マシン等に標準で対応しています。

Kubernetes の設定

まずは、アプリケーションの Service (kubernetes/ktor-oom-svc.yml) に任意のアノテーションを付与します。
サンプルで使用したアノテーションの用途は次のとおりです。

  • prometheus.io/java_scrape
    メトリクスを取得する対象の場合は true
  • prometheus.io/java_port
    メトリクスを公開しているポート番号を指定
  annotations:
    prometheus.io/java_scrape: 'true'
    prometheus.io/java_port: '9100'

アプリケーション本体 の 8080 に加え、メトリクスの 9100 ポートも忘れずに公開します。

  ports:
  - name: app
    protocol: TCP
    port: 8080
  - name: metrics
    protocol: TCP
    port: 9100

同様に、Deployment (kubernetes/ktor-oom.yml) でも両方のポートを公開します。

        ports:
        - containerPort: 8080
        - containerPort: 9100

Prometheus の設定

先ほど付与したアノテーションを持つ Service 配下の Pod を監視対象として設定します。
今回は Kubernetes クラスタ内に Prometheus を立てましたが、認証情報を設定すればクラスタ外からも監視できます。

デフォルト設定の場合、Prometheus は /etc/prometheus/prometheus.yml を設定ファイルとして参照します。
サンプルでは ConfigMap の形式で Pod にマウントしているため、内容は (kubernetes/prometheus-config.yml) に記載されています。
以下、その一部を抜粋しながら解説していきます。

role が監視対象を検出する際のルールです。
endpoints を設定したことで Service のルーティング先であるバックエンドが対象になりました。

    kubernetes_sd_configs:
    - role: endpoints

続いて relabel_configs のブロックに入ります。

role: endpoints により、Prometheus は Kubernetes クラスタ上のすべての Service に紐づくバックエンドを検出します
しかし実際に監視したい対象は一部のため、条件を指定して絞り込んでいく必要があります。

Prometheus は検出した候補から取得できるメタデータを、ラベルとして扱うことができます。
このラベルを元にしてフィルタしたり、値を設定したりすることで必要なものだけが抽出されるようにします。

    - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_java_scrape]
      action: keep
      regex: true

アノテーション prometheus.io/java_scrape の値が、正規表現 true に一致した場合は候補に残ります。
一致しなければ、この段階で除外されます。

    - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_java_port]
      action: replace
      target_label: __address__
      regex: ([^:]+)(?::\d+)?;(\d+)
      replacement: $1:$2

まずメトリクス取得先ホストが入った __address__ と、アノテーション prometheus.io/java_port の値を区切り文字 ; で連結します。
対象の Service は 2 つのポートを公開しているため、監視対象は Pod ごとに 8080 と 9100 の 2 つがヒットします。
したがって、ここまでの出力は (Pod の IP):(8080 または 9100);9100 になります。

次に正規表現 ([^:]+)(?::\d+)?;(\d+) でマッチさせ、$1$2 で置換した値を __address__ に設定します。
よって __address__(Pod の IP):9100 に上書きされ、正しく監視先 URL が組み立てられるようになりました。
このルールは __address__ に含まれるポートが 8080 でも 9100 でも同じ値を返すため、その結果 9100 だけが監視対象に残ります。

Grafana にダッシュボードを作成する

JMX Exporter の出力に対応する JVM dashboard を使用しました。

サンプルでは grafana-init ジョブにより、自動的にダッシュボードが追加されるようにしています。
簡単なシェルスクリプトで実現していますので、もし興味があれば kubernetes/grafana-init-config.yml をご覧ください。

動作確認

それでは、サンプルの動作確認を進めていきましょう。
なお MicroK8s 前提での手順になりますので、異なる環境をお使いの方は適宜読み替えてください。

まずは Kubernetes クラスタ内のアプリケーションにアクセスするための InternalIP を取得します。

$ kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'
192.168.0.11

各アプリケーションの Service は NodePort であり、ランダムに割り当てられたポートでクラスタ外に公開しています。
以下は prometheus-svc のポートを取得した例です。

$ kubectl -n oom-sample get svc prometheus-svc -o jsonpath='{.spec.ports}'
[map[nodePort:31122 port:9090 protocol:TCP targetPort:9090]]

上記より、Prometheus にアクセスするための URL は http://192.168.0.11:31122 であることが確認できました。
他のアプリケーションに接続する場合も、この要領で URL を取得してください。

JMX メトリクスの取得

Prometheus にアクセスして、画面上部メニューの Status > Targets を選択します。
以下のように、2 台の Pod それぞれが監視対象として認識されています。

f:id:old_horizon:20200816170455p:plain

次に Grafana を開きます。ユーザー名およびパスワードは admin です。
画面左メニューから Dashboards > Manage を選択すると、表示された一覧に JVM dashboard が存在しています。

f:id:old_horizon:20200816170522p:plain

実際に表示してみると、グラフ形式で値の推移が確認できることがわかります。

f:id:old_horizon:20200816170534p:plain

OOM を発生させてヒープダンプを取得してみる

サンプルアプリケーション (ktor-oom) は、/v1/oom にアクセスすると OOM が発生する実装になっています。
ここにリクエストして、構築した仕組みが動作することを確かめます。

最初に現在の Pod の状態を見てみます。
まだコンテナは一度も再起動していないため RESTARTS は 0 のはずです。

$ kubectl -n oom-sample get po -l app=ktor-oom -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
ktor-oom-646544cc67-4x9bd   1/1     Running   0          42m   10.1.52.222   t470s   <none>           <none>
ktor-oom-646544cc67-ljgpt   1/1     Running   0          42m   10.1.52.223   t470s   <none>           <none>

どちらの Pod も /dump にファイルは存在しません。

$ kubectl -n oom-sample exec ktor-oom-646544cc67-4x9bd -- ls /dump
$ kubectl -n oom-sample exec ktor-oom-646544cc67-ljgpt -- ls /dump

それでは OOM を発生させてみましょう。

以下のコマンドで /v1/oom/v1/ping に連続してアクセスします。
/v1/ping のリクエストは正しく処理されたため、OOM が発生した Pod はルーティング対象から外されたことがわかります。

$ curl -v http://192.168.0.11:31072/v1/{oom,ping}
*   Trying 192.168.0.11...
* TCP_NODELAY set
* Connected to 192.168.0.11 (192.168.0.11) port 31072 (#0)
> GET /v1/oom HTTP/1.1
> Host: 192.168.0.11:31072
> User-Agent: curl/7.58.0
> Accept: */*
> 
* Empty reply from server
* Connection #0 to host 192.168.0.11 left intact
curl: (52) Empty reply from server
* Connection 0 seems to be dead!
* Closing connection 0
* Hostname 192.168.0.11 was found in DNS cache
*   Trying 192.168.0.11...
* TCP_NODELAY set
* Connected to 192.168.0.11 (192.168.0.11) port 31072 (#1)
> GET /v1/ping HTTP/1.1
> Host: 192.168.0.11:31072
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Length: 4
< Content-Type: text/plain; charset=UTF-8
< 
* Connection #1 to host 192.168.0.11 left intact
pong

再度 Pod の状態を確認すると、片方の Pod の RESTARTS が 1 になっていました。
したがって、この Pod で OOM が発生したものと考えられます。

$ kubectl -n oom-sample get po -l app=ktor-oom -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
ktor-oom-646544cc67-4x9bd   1/1     Running   0          73m   10.1.52.222   t470s   <none>           <none>
ktor-oom-646544cc67-ljgpt   1/1     Running   1          73m   10.1.52.223   t470s   <none>           <none>

実際にヒープダンプが出力されていることを確認できました。

$ kubectl -n oom-sample exec ktor-oom-646544cc67-ljgpt -- ls /dump
java_pid1.hprof

出力されたヒープダンプの回収には kubectl cp が利用できます。

$ kubectl -n oom-sample cp ktor-oom-646544cc67-ljgpt:/dump/java_pid1.hprof ./java_pid1.hprof

おわりに

OOM 発生時に十分な情報が取得できず、再現待ちとなってしまうケースを過去に見てきました。
しかし各種ダンプ・メトリクスがあるだけで調査は大いに捗るため、その取得の自動化は費用対効果が高いと思います。
まさに備えあれば憂いなしです。機会があればぜひお試しください。

Android Studioに入門しよう ~UI・レイアウト編~

こんにちは。SPEEDA開発チームの佐藤です。
今回はAndroid Studioに入門しようということで、
初心者向けに簡単な使い方から、今回は主にUI部品・レイアウトについて紹介したいと思います!

Android Studioとは?


JetBrains社のIntellJ IDEAをベースとしたAndroidアプリ開発のための統合開発環境で、
Windows、Mac、Linuxなど複数の環境向けに用意されています。
まずはAndroid Studioをインストールして環境を設定しておきましょう。

developer.android.com

初期プロジェクト作成


インストールしたAndroid Studioを起動してください。このような画面がでますので、
Start a new Android Studio Projectを押してください。

f:id:teddy0x0:20200319125229p:plain


そのあとテンプレートを選ぶ画面が出てきますので、 Empty Activityを選択してください。
(※テンプレートにはナビゲーションが下部についたものや、 フローティングアクションボタンが右下に表示されたものなど、様々なパターンが用意されています! 説明は省きますが、自分が作りたいアプリに近いテンプレートを選ぶことができるようになっています)

f:id:teddy0x0:20200319135348p:plain

あとは写真のようにプロジェクトに名前を付けて、
適当なフォルダを指定すれば初期プロジェクトは完成です!
ちなみに開発言語を選ぶこともできて、ひとまずJavaになっていますがKotlinも選べますので、
好きな方で開発できます!

f:id:teddy0x0:20200319135520p:plain

動作確認の方法


次にレイアウトとUI部品について紹介したいと思います。
プロジェクトを作成するとこのような画面に変わって、activity_main.xmlMainActivity.javaの二つが既にOpenされています。

f:id:teddy0x0:20200319141701p:plain

画面のレイアウトの記述はこのactivity_main.xmlに記載します。
それでは試しにボタンを表示させてみましょう。
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BUTTON"
        android:id="@+id/id1"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BUTTON"
        android:id="@+id/id2"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BUTTON"
        android:id="@+id/id3"/>

</LinearLayout>

記載できたら実行してみたいと思います。
Android Studioにはエミュレータと呼ばれる実行環境が用意されていて、
実機を使用しなくてもレイアウトや挙動の確認をすることができます。
エミュレータを使用するためには、実行するためのVirtual Device環境を設定する必要がありますので、
そちらを設定してからエミュレータを起動させましょう。
画像のとおりAVD Managerをクリックし
Create Virtual DeviceSelect Hardware 画面に進み、デバイスの設定をしてください。

AVD Manager
f:id:teddy0x0:20200319145605p:plain

Create Virtual Device
f:id:teddy0x0:20200319150551p:plain

Select Hardware
f:id:teddy0x0:20200319151021p:plain

System Image
f:id:teddy0x0:20200319151241p:plain

Verify Configuration
f:id:teddy0x0:20200319151431p:plain

ちなみにメモリ設定の部分はわたしの環境だとこのようになっております

f:id:teddy0x0:20200319152021p:plain

設定が終わるとこのようにエミュレータにデバイスが追加されますので
これで実行環境の設定はおしまいです。

f:id:teddy0x0:20200319152446p:plain

あとは今設定したデバイスを選んで実行ボタンを押せば、
さきほど記載したレイアウトの確認ができます。

f:id:teddy0x0:20200319152845p:plain

f:id:teddy0x0:20200319155939p:plain

ボタンが3つ表示されましたね!
エミュレータでの動作確認ができたのでレイアウトの説明に入りたいと思います。

レイアウト


UI部品をどのように配置するかはレイアウトによって決まります。
レイアウトには下記の通りいくつか種類があります。

1. LinearLayout
2. RelativeLayout
3. FrameLayout
4. CoordinatorLayout
5. ConstraintLayout

今回は基本となる1と2のレイアウトについて紹介しようと思います。
(3、4、5は動的なレイアウトを作成したいときに使います!気になる人は調べてみてください)

LinearLayout

LinearLayoutとは縦もしくは横一列に要素を並べて表示させる一番シンプルなレイアウトです。
先ほど記述したactivity_main.xmlでは、このLinerLayoutを使用しました。
それではLinerLayoutの属性について、もう少し詳しく見ていきましょう。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
</LinearLayout>
  • xmlns:android=
    XML 名前空間を "http://schemas.android.com/apk/res/android" として定義している必須の記述

  • android:layout_width=
    縦の大きさを指定する属性。設定値はmatch_parentもしくはwrap_contentの二つ。
    match_parentは親の要素と同じ大きさにしたいときに、
    wrap_contentは自身のサイズと同じ大きさにしたいときに指定します。
    一番外側のレイアウトでは親の要素=画面サイズになります。

  • android:orientation=
    並べ方を縦横どちらにするかを指定する属性。設定値はhorizontalverticalの二つ。

RelativeLayout

RelativeLayoutとは要素の位置関係を相対的に決めるレイアウトのことです。
別のUI部品に対して相対的に位置を決めるか、もしくは親に対してどのように配置するかを指定できます。
では先ほどのactivity_main.xmlを次のように書き換えてみてください。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="5dp"
    android:paddingLeft="5dp"
    android:paddingRight="5dp"
    android:paddingTop="5dp"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="中央寄せ"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:text="中央上"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:text="中央下"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="中央左"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:text="中央右"/>

</RelativeLayout>

書き換えてエミュレータを実行すると写真のようになると思います。

f:id:teddy0x0:20200319165700p:plain

RelativeLayoutの属性についてはpaddingが指定されているだけなので割愛します。
Buttonの中で指定されている属性値については下記のとおりです。

属性名配置方法
layout_centerVerticaltrueにすると親に対して垂直方向中央寄せ
layout_centerHorizontaltrueにすると親に対して水平方向中央寄せ
layout_alignParentToptrueにすると親に対して上寄せ
layout_alignParentBottomtrueにすると親に対して下寄せ
layout_alignParentLefttrueにすると親に対して左寄せ
layout_alignParentRighttrueにすると親に対して右寄せ
layout_centerInParenttrueにすると親に対して水平垂直方向共に中央寄せ


ここまでが親に対して配置する場合の属性値です。
それでは他の部品に対する配置を行うパターンも見てみましょう。
activity_main.xmlを次のように書き換えてから、エミュレータを起動しなおしてみてください。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="5dp"
    android:paddingLeft="5dp"
    android:paddingRight="5dp"
    android:paddingTop="5dp"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン1"
        android:id="@+id/id1"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン2"
        android:layout_toLeftOf="@+id/id1"
        android:id="@+id/id2"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン3"
        android:layout_toRightOf="@+id/id1"
        android:id="@+id/id3"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン4"
        android:layout_below="@+id/id1"
        android:id="@+id/id4"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="ボタン5"
        android:layout_below="@id/id4"
        android:id="@+id/id5"/>

</RelativeLayout>



f:id:teddy0x0:20200319174013p:plain

他の部品に対する配置方法と属性の種類は次のようなものがあります

属性名配置方法
layout_below指定したUI部品に対し下側に配置
layout_above指定したUI部品に対し上側に配置
layout_toEndOf指定したUI部品に対し右側に配置
layout_toStartOf指定したUI部品に対し左側に配置
layout_toRightOf指定したUI部品に対し右側に配置
layout_toLeftOf指定したUI部品に対し左側に配置
layout_alignRight指定したUI部品の右端の位置とViewの右端を揃えて配置
layout_alignLeft指定したUI部品の左端の位置とViewの左端を揃えて配置
layout_alignBottom指定したUI部品の下端の位置とViewの下端を揃えて配置
layout_alignTop指定したUI部品の上端の位置とViewの上端を揃えて配置


使用するときは、android:layout_below="@id/基準にしたい部品のid"という指定の仕方で配置位置を決めます。
部品のID名はandroid:id="@+id/部品のid名"の記述で決定します。

UI部品


続いてAndroidの代表的なUI部品をいくつか説明したいと思います。
各UI部品にはイベントがあった際に特定のメソッドが呼び出されるイベントリスナーという仕組みがありますが、 本記事ではイベントリスナーの説明は省きます。
UIに対するイベント(メソッド)の設定については別の機会で詳しく説明したいと思います。

1. Button
f:id:teddy0x0:20200330172032p:plain
ボタンです。
クリックされるとonClickメソッドが呼ばれます。

@Override
public void onClick(View v) {
        println("クリックされました");
    }

2. TextView
f:id:teddy0x0:20200330174018p:plain
文字を表示するために使用するUI。

3. EditText
f:id:teddy0x0:20200330175946p:plain
ユーザーからの入力を受け付けるUI。
ボタンとセットで使用し、ボタンのonClickメソッドの中で入力値を受け取る、
というような使い方をします。

@Override
 public void onClick(View v) {
        textView.setText(editText.getText().toString());
    }


4. TimePickerDialog
f:id:teddy0x0:20200330181038p:plain
ユーザーに時刻の入力をさせたいときに使用するUI。
以下のように初期値を設定してダイアログを表示させる。

private void createTimePickerDialog() {
        TimePickerDialog timePickerDialog = new TimePickerDialog(this,
                new TimePickerDialog.OnTimeSetListener() {
                    @Override
                    public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
                        println(String.valueOf(hourOfDay) + ":" + String.valueOf(minute));
                    }
                },13, 0, true);
        timePickerDialog.show();
    }


おわりに

今回はAndroid Studioの代表的なレイアウト、UI部品について紹介してみました。
UI部品の詳細な使い方や簡単なアプリ作成までは本記事で解説できなかったので、 また別の機会で改めてこれらについての説明とアプリ作成のハンズオン記事を書ければと思います。
Android Studioについて少しでも興味が湧いた方は、ぜひ実際に触ってみてください!

Smalltalkで『オブジェクト指向設計実践ガイド』の「第3章 依存関係を管理する」をハンズオンしたら快適で楽しかった

今日は。 SPEEDA を開発している濱口です。

前回の続きです。趣旨も同じ。

『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』のサンプルコードを
Ruby から Smalltalk に翻訳しながら読み進めることで、ただの写経をアクティブな学びにし、 いろいろな道草、発見をしながら楽しもう、というものです。

前回も触れましたが、やはり自分のコードとクラスライブラリの境界が無く、よいお手本がすぐに手に入るのがよいです。
わざわざドキュメントを紐解いたり、ググる必要がほぼないのですね。
Smalltalk 環境で完結します。
シンプルです。

f:id:yhamaro:20191209054645p:plain
Let's 写経!

今回も、わりと忠実な写経が可能でした

わりと忠実な写経は以下です。(プログラムの進化の過程が見えるようにコミットを分けました)

Ruby と Smalltalk がすごく似ていることは重々わかりました。
しかし、そこはアクティブ写経なので、気付きや考えたことがありました。
それを書いていきます。

ダック・タイピング良好

「 3.2 疎結合なコードを書く」では、本来的に避けられないオブジェクト間の共同作業の中で、依存関係をいかに管理するか、蜜結合なオブジェクトの巨大団子を作らないための設計テクニックがいくつか紹介されています。

まずは、「依存オブジェクトの注入」です。
コンストラクタで、 Wheel オブジェクトを注入しています。

Class {
    #name : #Gear,
    #superclass : #Object,
    #instVars : [
        'chainring',
        'cog',
        'wheel'
    ],
    #category : #'Example-OOD-gear'
}

{ #category : #private }
Gear >> setChainring: chainringInteger cog: cogInteger rim: rimInteger tire: tireFloat [
    chainring := chainringInteger.
    cog := cogInteger.
    rim := rimInteger.
    tire := tireFloat.
    ^ self
]

{ #category : #calculating }
Gear >> gearInches [
    ^ self ratio * wheel diameter
]
Class {
    #name : #Wheel,
    #superclass : #Object,
    #instVars : [
        'rim',
        'tire'
    ],
    #category : #'Example-OOD-gear'
}

{ #category : #calculating }
Wheel >> diameter [
    ^ rim + (tire * 2)
]

Ruby によるサンプルコードと同様、ダック・タイピングが使えるのでとても簡単でした。
「 diameter というメッセージに答えることが出来る、ある抽象的なもの」への依存を注入しています。

静的型付け言語の場合だと、「抽象的なもの」はインタフェースで表現されると思います。
陽に、インタフェースを定義するには名付けが必要で、そこに難しさがあると思います。
○○Able ? IWheel ? なかなかよい名前が浮かびません。
「 diameter というメッセージに答えうるもの」という概念が抽象的すぎるのでしょうか。
陰に、インタフェースがかたちづくられるダック・タイピングが、名付け問題を先送りに出来ているという意味で、ここでは有効だと思いました。
(インスタンス変数を wheel と名付けしていますが、クラス名、インタフェース名ほど目立つものではありません。)

その代わり、コンパイラが事前に実行時エラー検出してくれるとうメリットを捨てているわけですが、
以下のような心意気でテストを書いていればよいのでは、と考えています。

むしろソフトウェア開発は科学のようなものである。どれだけ最善を尽くしても正しくないことを証明できないことによって、その正しさを明らかにしているのである。
Clean Architecture 達人に学ぶソフトウェアの構造と設計 > 第4章 構造化プログラミング > テスト

上記、ダック・タイピングのWikiページの記述にもあるように、 OCaml などの言語は型推論により上記のいいとこ取りが出来るようなので、近いうちに使ってみたいなと思いました。

その他、依存を管理するテクニック

依存を隔離する」では、Wheel の初期化のためのメソッドを用意して依存をむしろ明示して局所化しています。
インスタンス変数の遅延初期化の書き方が以下のように異なりました。

# Ruby
def gear_inches
  ratio * wheel.diameter
end

def wheel
  @wheel ||= Wheel.new(rim, tire)
end
"Smalltalk"
gearInches [
    ^ self ratio * wheel diameter
]

wheel [
    wheel ifNil: [ wheel := Wheel rim: rim tire: tire ].
    ^ wheel
]

Ruby のほうがシンタックス・シュガー(||=)がある分、コード量は少ないですね。
Smalltalk にはシンタックス・シュガーが無いですが、この側面から見ると Ruby は Easy 、Smalltalk は Simple と言えると考えています。

次に、「引数の順番への依存を取り除く」です。
ここでは、その目的のためにハッシュを引数に渡すこと(デフォルト値にも対応)をしています。
同じことを Smalltalk で書くとこうなります。

setArgs: args [
    chainring := args at: #chainring ifAbsent: 40.
    cog := args at: #cog ifAbsent: 18.
    wheel := args at: #wheel.
    ^ self
]

または、デフォルト値を持つオブジェクトとのマージを使って書くと以下のようになります。

setArgs: args [
    defaults
        ifNil: [ defaults := Dictionary new.
            defaults at: #chainring put: 40.
            defaults at: #cog put: 18 ].
    defaults addAll: args.
    chainring := args at: #chainring.
    cog := args at: #cog.
    wheel := args at: #wheel.
    ^ self
]

Smalltalk ではハッシュ( Dictionary )同士のマージを addAll というメッセージで行います。
Ruby のサンプルコードでは merge というメソッド名になっていますが、
個人的には addAll という名前のほうが、キーが一致した場合に値が後勝ちすることがわかりやすい気がしていて好みです。

「自身より変更されないものに依存しなさい」というマントラ

「依存方向の管理」では、まず Gear と Wheel の依存関係を敢えて逆転させて、依存関係の方向は恣意的に選ぶものだと示します。

OOとは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。
Clean Architecture 達人に学ぶソフトウェアの構造と設計 > 第5章 オブジェクト指向プログラミング > まとめ

Ruby も Smalltalk も OO なので、依存関係を自由に制御できるわけですが、
選択基準として、「自身より変更されないものに依存しなさい」が示されます。
つまり、具象より抽象に依存すべきと言い換えられると思いますが、 さきほど行った Gear が Wheel を注入するようにしたのは、 依存方向は変わっていませんが、「 Wheel 」という具象より、「 diameter というメッセージに答えるもの」という抽象に依存するようにしており、結果的に依存の方向を強調したことになったと思っています。

まとめ

前回より、Ruby と Smalltalk の違いが際立たなかったので、すなおにハンズオンを進めていけました。(TDD遵守)
さらに加速して、第9章までやりきりたいと思います。

フロントエンドのコンポーネント設計で気をつけているn個のこと

はじめまして、昨年の12月に入社しました根岸です。

UZABASEに入社する前はフロントエンドエンジニアとして働いており、ここ1年間くらいはReactとTypeScriptの開発ばかりやっていました。 今回はフロントエンドのコンポーネントを設計するときに気をつけていることについてまとめます。

対象読者

  • ReactやVueなどフロントエンドフレームワークで開発している人
  • コンポーネントの設計に自信がない人
  • 拡張性の高いコンポーネントを作りたいと思っている人

Propsの名前に一貫性をもたせる

例えばページ内でスタイルをあわせるためにButtonコンポーネントを作ったとしましょう。

Buttonコンポーネントがクリックされたときに関数を呼び出したいとき、どうやって関数をpropsに渡しますか? 多くのひとがonClick propsに渡そうするでしょう。

もしclick propsに渡す必要があったら驚くでしょう。 なぜならば標準DOM要素<button>の場合はonClickに渡せばよいから。

標準DOM要素と同じようなことをしたい場合ときは、標準DOM要素のprops名に合わせましょう。

  • クリックしたときに関数を実行 -> onClick
  • フォームの送信時に関数を実行 -> onSubmit
  • link先のurl -> href (toが使われる場合も多い)
// 良い例
const Button = ({onClick, children}) => (
  <button onClick={onClick}>{children}</button>
)
// アンチパターン
const Button = ({click, children}) => (
  <button onClick={click}>{children}</button>
)

標準DOM要素と同様の役割を持つコンポーネントのpropsは標準DOM要素に合わせる

atom層のコンポーネントは標準DOM要素と同じ役割をもつケースが多く、標準DOM要素をラップするだけのことがよくあります。 その場合は標準DOM要素のpropsをすべて使えるようにしましょう。

どうやって実装するかというとすべてのpropsをラップした標準DOM要素に渡せばよいです。 スプレッド演算子を使うことでまとめてpropsを渡すことができます。

// 良い例
const Button = ({color, ...props}) => {
  // propsで受け取ったものをすべて<button>に渡す
    return <button style={{background: color}} {...props} />
}

const Example = () => {
  const handleClick = () => alert('click')
  return <Button type="button" disabled={false} name="example">ボタン</Button>
}
// アンチパターン
const Button = ({color, children}) => {
    return <button style={{background: color}}>{children}</button>
}

const Example = () => {
  const handleClick = () => alert('click')
  // typeやdisabledをButtonは<button>に渡していない
  // Buttonにtypeやdisabledを渡しても使えない
  return <Button type="button" disabled={false} name="example">ボタン</Button>
}

TypeScriptの場合はJSX.IntrinsicElementsから標準DOM要素のpropsの型を取得してReact.FCに渡せば良いです。

// 良い例
const Button: React.FC<JSX.IntrinsicElements['button']> = props => {
    return <button style={{background: 'red'}} {...props} />
}

styled-componentやemotionなどで使えるstyledを使うとより簡単です。

// 良い例

// Buttonはbuttonのpropsを継承する
const Button = styled.button`
  background: red;
`

レイアウトをコンポーネントに切り出す

f:id:t-NeGI:20200329164536p:plain
2カラムページ

上の図のようなページのコーディングを考えます。

図は左側にメニュー、右側にメインのコンテンツを表示する2カラムのレイアウトを持っています。 このようなレイアウトは他のページでも再利用されることが考えられます。 再利用される場合はレイアウトに関してもコンポーネントに切り出してコードの共通化をしましょう。

下の例ではレイアウトに関することはTwoColumnLayoutに切り出しています。 レイアウトをコンポーネント化する場合はpropsにReactElementを受け取るときれいに切り出すことができます。 レイアウトをTwoColumnLayoutに切り出したことによって、Pageコンポーネントは左側と右側のカラムにMenuとMainを表示することだけに関心を持てばよいコードになっています。

// 良い例
const Page = () => (
  <>
    <Header />
    <TwoColumnLayout>
      <Menu />
      <Main />
    </TwoColumnLayout>
  </>
)

const TwoColumnLayout = ({ children }) => (
  <div style={{display: 'flex'}}>
    <div style={{flex: '1 1 30%'}}>{children[0]}</div>
    <div style={{flex: '1 1 70%'}}>{children[1]}</div>
  </div>
)
// 良い例 (TypeScript版)
const TwoColumnLayout:React.FC<Props> = ({ children }) => (
  <div style={{display: 'flex'}}>
    <div style={{flex: '1 1 30%'}}>{children[0]}</div>
    <div style={{flex: '1 1 70%'}}>{children[1]}</div>
  </div>
)

interface Props {
  children: [React.ReactElement, React.ReactElement];
}

下のコードのPageコンポーネントはレイアウトの情報を持っています。 このように特定のコンポーネントがレイアウトの情報を持つと、共通のレイアウトを1つのコードで管理できなくなってしまいます。 また、コードの見通しも悪くなります。

// アンチパターン
const Page = () => (
  <>
    <Header />
    <div style={{display: 'flex'}}>
      <div style={{flex: '1 1 30%'}}>
        <Menu />
      </div>
      <div style={{flex: '1 1 70%'}}>
        <Main />
      </div>
    </div>
  </>
)

インタラクティブな部分もコンポーネント切り出す

f:id:t-NeGI:20200329164443g:plain
クリックすると開閉するメニュー

上の図のようなアイコンをクリックすると開くメニューのコーディングを考えます。

このメニューはアイコンをクリックすると開いたり閉じたりできます。 またメニューにはフェードイン・フェードアウトのアニメーションがついています。

「アイコンをクリックしたら要素がフェードイン・フェードアウトする」という動作は、メニュー以外でも使うことがあるでしょう。 その場合はレイアウトと同様にコンポーネントに切り出して再利用できるようにしましょう。

クリック時の動作をコンポーネントへ切り出さずにコーディングすると下のようになります。 ToggleMenuは表示する要素に加えて、開閉の状態やフェードイン・フェードアウトに関するスタイルなどクリック時の動作に関する役割も持っています。

// クリック時の動作をコンポーネントに切り出す前のコード
const ToggleMenu = ({ menuItems }) => {
  const [isOpen, setIsOpen] = useState(false);
  const handleClickButton = useCallback(() => setIsOpen(isOpen => !isOpen), [
    setIsOpen
  ]);

  const opacityStyle = isOpen ? { opacity: "1" } : { opacity: "0" };
  return (
    <>
      <FiMenu size="30" onClick={handleClickButton}/>
      <ul
        style={{
          ...opacityStyle,
          transition: "opacity 0.2s",
          border: "1px solid",
          width: "170px",
        }}
      >
        {menuItems.map(item => (
          <li>{item}</li>
        ))}
      </ul>
    </>
  );
};

下の例ではクリックしたときの動作に関することはToggleElementWrapperに切り出しています。 ToggleElementWrapperはクリックされる要素(clickableElement)と表示・非表示される要素(toggledElement)をpropsで受け取っています。 prosで受け取ったclickableElementtoggledElementにonClickとstyleをそれぞれのpropsに渡すことでクリック時に要素を表示・非表示する動作を実現しています。

また、ToggleElementWrapperを利用する側のToggleMenuは表示する要素だけに関心を持てば良いコードになっています。 開閉の状態やアニメーションに関してToggleMenuは一切考える必要がありません。

// 良い例
const ToggleMenu = ({ menuItems }) => {
  const clickableElement = <FiMenu size="30" />;
  const toggledElement = (
    <ul
      style={{
        border: "1px solid",
        width: "170px"
      }}
    >
      {menuItems.map(item => (
        <li>{item}</li>
      ))}
    </ul>
  );

  return (
    <ToggleElementWrapper
      clickableElement={clickableElement}
      toggledElement={toggledElement}
    />
  );
};

const ToggleElementWrapper = ({ clickableElement, toggledElement }) => {
  const [isOpen, setIsOpen] = useState(false);
  const handleClickButton = useCallback(() => setIsOpen(isOpen => !isOpen), [
    setIsOpen
  ]);

  const opacityStyle = isOpen ? { opacity: "1" } : { opacity: "0" };

  // clickableElementにonClick propsを追加している
  const clickableElementAddedOnClickProps = React.cloneElement(
    clickableElement,
    { onClick: handleClickButton }
  );

  // toggledElementにスタイルに関するpropsを追加している
  const toggledElementAddedOpacityStyle = React.cloneElement(toggledElement, {
    style: {
      ...toggledElement.props.style,
      ...opacityStyle,
      transition: "opacity 0.2s"
    }
  });
  return (
    <>
      {clickableElementAddedOnClickProps}
      {toggledElementAddedOpacityStyle}
    </>
  );
};

単一責任を意識してコンポーネントを作る

オブジェクト指向プログラミングでは単一責任の原則という設計の考え方があります。 Reactのコンポーネントを作るときも単一責任の原則を意識しましょう。

例としてレイアウトのコードを再掲してそれぞれのコンポーネントの役割について考えてみます。

const Page1 = () => (
  <>
    <Header />
    <TwoColumnLayout>
      <Menu />
      <Main />
    </TwoColumnLayout>
  </>
)

const TwoColumnLayout = ({ children }) => (
  <div style={{display: 'flex'}}>
    <div style={{flex: '1 1 30%'}}>{children[0]}</div>
    <div style={{flex: '1 1 70%'}}>{children[1]}</div>
  </div>
)

const Page2 = () => (
  <>
    <Header />
    <div style={{display: 'flex'}}>
      <div style={{flex: '1 1 30%'}}>
        <Menu />
      </div>
      <div style={{flex: '1 1 70%'}}>
        <Main />
      </div>
    </div>
  </>
)

Page1Header,Menu,MainTwoColumnlayoutというコンポーネントで表示するという役割だけを持っていて具体的なスタイルについて何も知りません。 またTwoColumnLayoutは具体的なスタイルの情報だけを持っています。 Page1TwoColumnLayoutはそれぞれ表示する要素をまとめるという役割と具体的なスタイルを定義する役割だけを持っており、単一責任の原則が守られています。

一方、Page2は表示するコンポーネントの情報とスタイルの情報の2つを持っています。 これは単一責任の原則違反であり、Page1TwoColumnLayoutのようにコンポーネントを分割すべきです。

propsを増やすことでコンポーネントのバリエーションを増やさない

f:id:t-NeGI:20200329164557p:plain

上の図のようなECサイトの商品紹介用のボックスについて考えます。 このボックスのコンポーネントを下のコードのように作ったとします。 Productは画像のsrc,商品名,価格をpropsから受け取って表示します。

const Product= ({image, name, price}) => {
  return (
    <Box>
      <Image src={image} />
      <Name>{name}</Name>
      <Price>価格:{price}円</Price>
    </Box>
  )
}

後日、セール時の価格を下の図のように表示したくなったとしましょう。

f:id:t-NeGI:20200329164608p:plain

このときどうやってコーディングすべきでしょうか。 安直に考えるとProductを拡張して、セールのときはセール価格をpropsで受け取り表記を変えれば実装できそうです。

const Product= ({image, name, price, salePrice}) => {
  return (
    <Box>
      <Image src={image} />
      <Name>{name}</Name>
      {/* salePriceがあれば打ち消し線を引く */}
      <Price style={!!salePrice ? {textDecoration: "line-through" } : {}}>
        価格:{price}円
      </Price>
      {/* salePriceがあるときだけ表示する*/}
      {!!salePrice && (
        <Price>セール価格:
          <Text color={"red"}>{salePrice}円</Text>
        </Price>
      )}
    </Box>
  )
}

しかし、salePriceをpropsに追加したおかげでProductのなかでセールのときとそうではないときの制御をしないといけなくなってしまいました。 また、さらに電子版や中古の商品があるときの価格を追加したい要件があったらどうなるでしょう。 Productにpropsを追加していくとどんどんカオスになっていってしまいます。

このようなコンポーネントのバリエーションを増やしたいときは、propsを増やすことで対応するのではなくて別のコンポーネントに分けることで対応しましょう。

const Product= ({image, name, price}) => {
  return (
    <Box>
      <Image src={image} />
      <Name>{name}</Name>
      <Price>価格:{price}円</Price>
    </Box>
  )
}

const SaleProduct= ({image, name, price, salePrice}) => {
  return (
    <Box>
      <Image src={image} />
      <Name>{name}</Name>
      <Price style={{textDecoration: "line-through" }}>価格:{price}円</Price>
      <Price>セール価格:
        <Text color={"red"}>{salePrice}円</Text>
      </Price>
    </Box>
  )
}

ProductSaleProductに分けることで、セールとそうでない場合の制御がなくなりシンプルなコードになりました。

ProductSaleProductを見るとコードの重複が気になる人もいるかも知れません。 しかし無理に1つのコンポーネントにまとめてコードを複雑にすることよりも、別のコンポーネントにわけてコードの重複を許容しつつシンプルなコードにしたほうが長期的に見て得だと私なら考えます。

まとめ

コンポーネントの設計に関しては資料もすくないため、初めはどうするのがよいか悩む人も多いと思います。 この記事を読んで、コンポーネント作るときに少しでも参考にしていただけると嬉しいです。

Mockitoを使ってDartでのTDDを加速させよう

初めて会社のブログに書きます。SPEEDA事業でCTOをしている林です。 TDDをこよなく愛する身として今日はDartでTDD、そしてテストの独立性を担保していく上で欠かせないMockライブラリーのMockitoについて書こうと思います。

Mockitoとは

Dart開発チームが作成している公式Mockライブラリーです。

名前の通りJavaにおいてメジャーなMockライブラリーの1つであるMockitoにインスパイアされたもので、DartでMockオブジェクトを使う場合においてもっともメジャーな選択肢となっています。

では使い方を見ていきましょう。Mockライブラリーを使ってテストを書いたことがある人であれば特に違和感なく使えると思います。

※以下単にMockitoと記述する場合はDartのMockitoを指します。

今回Mock化するクラス

以下のクラスに対してMockの振る舞いを定義していきたいと思います。

class Count {
  final int countValue;
  final int unit;
  Count(this.countValue, this.unit);

  int get nativeValue => countValue;

  Count increment() => Count(countValue + unit, unit);

  Count changeUnit(int newUnit) => Count(countValue, newUnit);

  Future<Count> sampling() => Future.value(Count(0, unit));

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Count &&
          runtimeType == other.runtimeType &&
          countValue == other.countValue &&
          unit == other.unit;

  @override
  int get hashCode => countValue.hashCode ^ unit.hashCode;
}

事前準備

Mock用クラスを定義する

違和感無く使えると言ってすぐで申し訳ないのですが、この事前準備は他のMockライブラリーではあまり見ない形です。下記のようにモック対象のクラスをimplementsしてMockitoのMockクラスをextendsします。

import 'package:mockito/mockito.dart';

class CountMock extends Mock implements Count{}

正直な所、この一手間が毎回面倒だなと感じてしまう部分です。そんな面倒な事がなぜ必要かと言うと、Dartはリフレクションが非推奨になっているため裏でいろいろゴニョゴニョ黒魔術を使うのが難しいというのが大きな理由になります。*1 じゃあどうやってるのかというとextendsするMockクラスの方でNoSuchMethodをオーバーライドしていてその中でいろいろハンドリングをしています。通常、暗黙的インターフェースを実装したのみであればCountMockはコンパイルエラーになりますが、MockクラスでNoSuchMethodをオーバーライドしているためコンパイルエラーにはなりません。これを利用してMockitoはMockライブラリーとして必要な機能を提供しています。*2

ちょっと前置きが長くなってしまいましたが実際の振る舞い定義等を見ていきましょう。

振る舞いを定義する

下記のようにwhenでmock化したいメソッドを指定し、thenReturnで戻り値を定義します。

class CountMock extends Mock implements Count{}

void main() {
  test('incrementの振る舞いに関するサンプル', (){
    var mock = CountMock();
    when(mock.increment()).thenReturn(Count(0,1));
    expect(mock.increment(), Count(0, 1));
  });
}

振る舞いを定義する(Future、Streamの場合)

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {
  test('戻り値がFutureの場合に関するサンプル', () async {
    var mock = CountMock();
    when(mock.sampling()).thenAnswer((_) => Future.value(Count(1,1)));
    expect(await mock.sampling(), Count(1, 1));
  });
}

非同期のメソッドに対して振る舞いと戻り値を設定する場合は thenReturn ではなく thenAnswer を使用する必要があります。戻り値もFutureやStreamにしないといけない点などが面倒ですが thenReturn にしている場合はエラーになるので注意が必要です。

検証する

一般的なMockライブラリーと同様にMockitoでもメソッド呼び出しの検証が可能です。この検証を記述する事で「実は重要なメソッドを呼び出してなかった」というのを防げますし、TDDをしていく上でより良い設計の指針にもなります。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {
  test('検証に関するサンプル', (){
    var mock = CountMock();
    when(mock.increment()).thenReturn(Count(0,1));

    expect(mock.increment(), Count(0, 1));

    verify(mock.increment());
  });
}

検証する(回数チェック)

対象のメソッドが何回呼び出されたかというチェックをしたい場合に使います。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {
  test('検証に関するサンプル(回数)', (){
    var mock = CountMock();
    when(mock.increment()).thenReturn(Count(0,1));

    expect(mock.increment(), Count(0, 1));

    verify(mock.increment()).called(1);
    verify(mock.increment()).called(greaterThan(0));
    verifyNever(mock.nativeValue);
  });
}
  • calledにて指定した回数の検証
  • calledにはMatcherを指定可能
  • verifyNeverにて一度も呼び出されない事を検証

検証する(引数チェック)

検証時に引数を柔軟にチェックする事が出来ます。引数のチェックには柔軟性を持たせたい場合などに使います。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {
  test('引数の検証に関するサンプル', (){
    var mock = CountMock();
    
    when(mock.format('回目です')).thenReturn('2回目です');

    expect(mock.format('回目です'), '2回目です');

    verify(mock.format(argThat(startsWith('回'))));
  });
}

argThat を使用します。引数にMatcherを受け取るので独自にMatcherを作成して検証する事も可能です。

検証する(呼び出し順序)

Mock化したオブジェクトの各メソッドがどういう順番で呼び出されたかというのを検証する事が出来ます。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

void main() {

  test('呼び出し順序検証に関するサンプル',(){
    var mock = CountMock();
    when(mock.increment()).thenReturn(Count(0,1));
    when(mock.nativeValue).thenReturn(0);

    expect(mock.nativeValue, 0);
    expect(mock.increment(), Count(0, 1));

    verifyInOrder([
      mock.nativeValue,
      mock.increment()
    ]);
  });
}

verifyInOrder を使って呼び出されるべき順序通りに記述する事で呼び出される順番の検証が出来ます。

Fakeクラス

自分は使ったことが無いのですがFakeクラスというのが用意されています。これを使うとオーバーライドしてテスト用に独自に振る舞いを定義したメソッド以外を呼び出すとエラー(テストが失敗)になります。

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

class CountFake extends Fake implements Count{
  @override
  int get nativeValue => 10;
}

void main() {
  test('Fakeのサンプル', (){
    var mock = CountFake();
    mock.increment(); // OK
    mock.increment(); // UnimplementedErrorがThrowされる
  });
}

出来ない事

Mockitoでは出来ない事が結構あります。

  1. インスタンス化(new)のMock
  2. staticメソッドのMock
  3. 拡張関数のMock

リフレクションを使用していないというのが理由なのですが、JMockit等の強力なMockライブラリーに慣れている人等は不都合に感じるかと思います(実際自分はそうでした)

まとめ

Mockitoは上記のように出来ない事も結構あります。ただし逆に上記のような制限がある事でいろいろな依存関係を整理するきっかけになるとも思っています。インスタンスはRepositoryやPort等から必ず取得するように意識したり、staticなメソッド等は小さくラップするクラスを作って結合度を緩めたり。昔JMock*3を使い始めた時はそういうのを意識しないと綺麗なテストが書けなかったので、結果的に自分の設計力が上がったと思っています。なんでもやってくれるMockライブラリーは逆に設計に対する示唆に欠ける可能性があるとポジティブに考えてMockitを使うのが精神衛生上良いかなと思います!

以上になりますがいかがでしたでしょうか。それでは皆さんDartでも良いTDDライフをお送りください!

*1:dart:mirrorsがあるのですが、JSへのトランスパイルやFlutterでのビルド時のサイズが肥大化し、パフォーマンスも悪くなるためです

*2:Rubyのmethod_missing的なもの

*3:名前が似てますが上記のJMockitとは別。やれる事はJMockitほど多くなかった

方法より原理 〜正規化ルールとリレーショナルモデルについて〜 【実践編】

今日は。
SPEEDA を開発している濱口です。

理屈編では、まずリレーショナルデータベース(以下、RDB)の論理設計やその後において、
正規化ルールを運用する難しさについて述べました。

主な要因として、
例えば正規化を一度完了したテーブルに対し SELECT した結果もまたテーブル*1
つまりは正規形であることが求められるため、 SELECT するたびにいちいちその結果について、
「第 n 正規化についてクリアだな…次に第 n + 1 正規化についてはどうかな…」などと、
ルールに当てはめてチェックしなければならないことを挙げました。

リレーショナルモデルの世界で、常に正しい処理結果を得るため、
リレーショナル演算の閉包性を維持しなければならないためです。

そこで、ルールに頼らなくてもそれが依って立つリレーショナルモデルさえ正しく理解していれば常に、自然に正規形がつくれるのではないか、という仮説のもとモデルの説明をしました。

今回は、実際に設計を行いながら、それを確かめてみることにします。
正規化ルールを一旦忘れて取り組んでみます。

こんなデータをもらったら…

よくある飲食店のレシートです。

f:id:yhamaro:20200314083602p:plain
とある日

店主から依頼され、この取引を記録したいとしましょう。
この取引があったという事実を私が責任を持って永続化するのです。

とりあえず

レシートにある情報をもらさず、ひとつの取引事実としてRDBに格納しました。
(「合計」のような導出データは入れません。)

番号 日付 品目 種別 単価(円) 数量 店舗
1 2020/02/29 ラーメン, 餃子, ビール 麺飯類, 冷菜・点心, ドリンク 650, 450, 630 1, 2, 3 西東京_久我山店

ところで、店主が「月末の集計が楽しみだな。なにせウチは西東京でトップの売上らしいから…。」と言っているのを耳にしました。
ここ久我山店は「西東京」というエリアに属するらしいので、店舗名にその情報も加えておきました。

また、このチェーン店らしきお店で使っているレジは全店舗に設置されており、
レシートの番号は日毎に全店共通の連番として払い出されていることもわかりましたので、
日付と番号で一意キーを設定しておきます。(属性の下に二重線で表しています)

これでいいのだろうか…

今時点で、このレシート情報を記録、蓄積したものをどのように使うのかはよくわかっていません
誰かが(私かもしれない)がクエリでアドホックに分析を行うのか、これをデータストアとしたWebアプリケーションが後々できるのか…。

でも、そんなことを気にする必要はありません
リレーションに登録するのは事実であり、今時点で捉えたい事実を正しく登録できるよう設計すればよいのです。
分析要件やアプリケーションの仕様によって、リレーションに登録された事実が変わることは無いからです。
そういう意味では、RDBの論理設計はアプリケーション設計から独立していると言えるでしょう。

なので、今は自信と確信を持って事実をより正しくリレーションにマッピングすることに集中しましょう!

f:id:yhamaro:20200315081429p:plain
コックタイのほうを優先しました

最初に気になること

リレーションに登録するデータは命題の集合であることを前回述べました。
ここで、一部の属性を取り出して以下の述語を考えてみます。

(種別) として、 (品目) を注文しました。

これに登録したデータを当てはめて命題にしてみます。

麺飯類, 冷菜・点心, ドリンク として、 ラーメン, 餃子, ビール を注文しました。

この命題は事実を正しく表現できているでしょうか。
なにを言っているのか、わかりませんね。
いやわかるよ、という人も頭の中で以下の命題に分解しているはずです。

麺飯類 として、 ラーメン を注文しました。
冷菜・点心 として、 餃子 を注文しました。
ドリンク として として、 ビール を注文しました。

使用には、用法を守って正しく

前回、リレーションに格納された命題に順序が無いことを述べましたが、 リレーショナルモデルでは順序の概念を意図的に排除しています。
上記の設計のように属性の値に順序の概念を持ち込むとしたら、それを扱える演算を一緒に用意しなければなりません。

例えば、品目「ラーメン」の種別が知りたい場合、該当のレコードを取り出し、品目と種別の値をカンマで分割し、順序をまもってマッピングした後、「麺飯類」にたどり着けます。
そんな演算を実装できたとしても、少なくともリレーショナルモデルには持ち込みたくないですね。

なので、同じことをリレーショナル演算の一部である射影( SQL の SELECT ) と制限( SQL の WHERE ) で出来る設計に変えます。

番号 日付 品目 種別 単価(円) 数量 地域名 店舗名
1 2020/02/29 ラーメン 麺飯類 650 1 西東京 久我山店
1 2020/02/29 餃子 冷菜・点心 450 2 西東京 久我山店
1 2020/02/29 ビール ドリンク 630 3 西東京 久我山店
SELECT 種別 FROM 取引 WHERE 品目 = 'ラーメン';
-> '麺飯類'

できました。

わかったこと

上記の例は順序という概念で分割できるものでしたが、意味的に単一で無いものを属性の値として格納するのは(それを扱える演算をリレーショナルモデルに追加できないかぎり)避けるべきことがわかりました。
秒で反省して、「店舗」属性も、「地域名」と「店舗名」に分割しています。

ところで、ここで行った作業によって設計がリレーショナルモデルにとってふつうのカタチになったので、この作業を指して Normalization と呼び習わすことにします。

次に気になること

次の客が来て、ラーメンだけ食べて帰ったのでそれを記録します。

番号 日付 品目 種別 単価(円) 数量 地域名 店舗名
1 2020/02/29 ラーメン 麺飯類 650 1 西東京 久我山店
1 2020/02/29 餃子 冷菜・点心 450 2 西東京 久我山店
1 2020/02/29 ビール ドリンク 630 3 西東京 久我山店
2 2020/02/29 ラーメン 麺飯類 650 1 西東京 久我山店

ここでまた、一部の属性を取り出して以下の述語を考えてみます。

(品目) は、 (種別) である。

これに登録済みのデータを適用し、命題にしてみます。

ラーメン は 麺飯類 である。
餃子 は 冷菜・点心 である。
ビール は ドリンク である。
ラーメン は 麺飯類 である。

同じことを2度言っていますね
前回、事実は一度言えば十分であると述べました。
そうでなければならない理由は、この三千世界に遍在する多重管理による問題と同じです。
例えば、以下のような間違った命題が混入するのを避けるためです。

ラーメン は ドリンク である。

「種別」属性と同じく品目に従属する、「単価」属性においてはもっとシビアな問題が発生するかもしれません。
そこで、設計を見直して以下のようにしました。

番号 日付 品目 数量 地域名 店舗名
1 2020/02/29 ラーメン 1 西東京 久我山店
1 2020/02/29 餃子 2 西東京 久我山店
1 2020/02/29 ビール 3 西東京 久我山店
2 2020/02/29 ラーメン 1 西東京 久我山店


品目 種別 価格(円)
ラーメン 麺飯類 650
餃子 冷菜・点心 450
ビール ドリンク 630

同じことを一度しか言わなくなりました
同時に閉世界仮説により、『ラーメンはドリンク説』も完全に否定されました。

また、よく見ると以下も潜んでいるのでそれも一度しか言わなくなるようにします。

この日のレシート番号 1 番は 久我山店 に発行しました。
この日のレシート番号 1 番は 久我山店 に発行しました。
この日のレシート番号 1 番は 久我山店 に発行しました。
番号 日付 品目 数量
1 2020/02/29 ラーメン 1
1 2020/02/29 餃子 2
1 2020/02/29 ビール 3
2 2020/02/29 ラーメン 1


品目 種別 価格(円)
ラーメン 麺飯類 650
餃子 冷菜・点心 450
ビール ドリンク 630


番号 日付 地域名 店舗名
1 2020/02/29 西東京 久我山店
2 2020/02/29 西東京 久我山店

ところで、ここで行った作業によって設計がリレーショナルモデルにとってよりふつうのカタチになったので、この作業も Normalization と呼べるでしょう。
先程の作業結果を前提にしており、段階的なカタチを区別するために
最初のものを 1st Normal Form 、今出来たものを 2nd Normal Form と呼び習わすことにします。

もう気になっていたこと

もう気づいていましたが、まだ2回同じことを言っているところがあります。

久我山店 は 西東京 エリアの店舗です!
久我山店 は 西東京 エリアの店舗です!

以下のとおり設計を見直します。

番号 日付 品目 数量
1 2020/02/29 ラーメン 1
1 2020/02/29 餃子 2
1 2020/02/29 ビール 3
2 2020/02/29 ラーメン 1


品目 種別 価格(円)
ラーメン 麺飯類 650
餃子 冷菜・点心 450
ビール ドリンク 630


番号 日付 店舗名
1 2020/02/29 久我山店
2 2020/02/29 久我山店


店舗名 地域名
久我山店 西東京

ところで、ここで行った作業も Normalization と呼べるでしょう。
先程の作業と同じような基準で、同じようなことをした気がしますが、
先程の作業で一意キーにばかり気を取られていた自分を戒めるためにも、
今出来たカタチをきちんと区別して 3rd Normal Form と呼び習わすことにします。

おわりに

リレーショナルモデルのあるべきカタチに導かれて、無事に第3正規形( 3rd Normal Form )までたどり着けました。
論理設計は、だいたいここまでやれば大丈夫です。
設計はもちろん、リレーションを操作する場合も考えれば、
「正規化ルール」というカードをあえて持たないことが、最高の手なのかもしれません。

*1:SELECT が UNION のように一意性を保つ結果を返してくれると随分良くなると思います。 今は、 SELECT DISTINCT と意識的に書かなくてはいけません。 SELECT ⇔ UNION ALL、 SELECT DISTINCT ⇔ UNION というように表記の対象性を損なっているし、一意性は常に保つべきなので、 SELECT ALL ⇔ UNION ALL(一意性を保たない)、 SELECT ⇔ UNION(一意性を保つ) というようにすべきかと思います。

Ktor で小さな API を作る

こんにちは。SPEEDA 開発チームの緒方です。

システムをマイクロサービスで構成するメリットのひとつに、採用する技術にバリエーションを持たせることができるという点が挙げられると思います。

実際、SPEEDA でも様々な言語・フレームワークを利用してマイクロサービスを開発しています。 その中でも Kotlin はかなり多くのプロジェクトで採用されている言語です。

Kotlin で利用できるフレームワークと言えば Spring Boot など Spring 系のものが真っ先に思い付くと思うのですが、本当に小さな API を作りたい場合には少し大袈裟すぎる気もします。

今回はそういう場合に手軽に使える Ktor という小さなフレームワークを使った API について、簡単に紹介していきたいと思います。

ベースとなるプロジェクトの作成

まずはベースとなる Ktor プロジェクトを作成します。(Maven を使ったバージョン。)

公式の Maven - Quick Start - Ktor あたりが参考になると思います。

pom.xml の dependency に Ktor の依存関係を追加します。 サーバエンジンには Netty を利用します。

<dependency>
    <groupId>io.ktor</groupId>
    <artifactId>ktor-server-netty</artifactId>
    <version>${ktor.version}</version>
</dependency>

これで準備は完了です。最初のアプリケーションを作ります。

適当なパッケージに main 関数を作成します。

package sample

import io.ktor.application.call
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main() {
    embeddedServer(Netty, 8080) {
        routing {
            get("/") {
                call.respondText("Hello, world!")
            }
        }
    }.start(true)
}

実行してみます。

$ mvn exec:java -Dexec.mainClass=sample.MainKt

エラーなく実行できたら、curl コマンドを使って動作を確認します。

$ curl -v localhost:8080
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 13
< Content-Type: text/plain; charset=UTF-8
<
* Connection #0 to host localhost left intact
Hello, world!

無事に実行できました!これでベースとなるプロジェクトは完成です。

簡単な説明

まず embeddedServer 関数を使って、このプロセスで起動するサーバを生成しています。第一引数の Netty はサーバエンジンとして Netty を利用することを明示しています。

ポイントはこの関数に渡しているブロックです。このブロック内でサーバの各種設定を行っています。 このブロックのレシーバは io.ktor.application.Application のインスタンスで、このインスタンスを操作することでサーバの設定をすることができます。

例えば、routing 関数は名前の通り API のルーティングを表す io.ktor.routing.Routing を登録するものです。 io.ktor.application.Application の拡張関数として定義されているので設定変更を行う関数としてこのブロック内で呼び出すことができます。

get は Routing を構成する Route を登録する関数で、パス "/" の HTTP GET メソッドに対応する Route を登録しています。

Route は Routing に対して複数設定することができます。試しに Route を増やしてみます。

package sample

import io.ktor.application.call
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main() {
    embeddedServer(Netty, 8080) {
        routing {
            get("/") {
                call.respondText("Hello, world!")
            }
            get("/ping") {
                call.respondText("Pong")
            }
        }
    }.start(true)
}
$ curl -v localhost:8080/ping
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /ping HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 5
< Content-Type: text/plain; charset=UTF-8
<
* Connection #0 to host localhost left intact
Pong

Route を増やすことができました。

このように、Ktor では拡張関数とブロックを使った DSL (的な記法) で様々な設定を行っていきます。 慣れるまでは理解しにくいかもしれませんが、Kotlin の言語仕様をうまく使って直感的に定義できるようにした上手い記法だと思います。

レスポンスを JSON にする

さて、ここまではテキストでレスポンスを返していましたが、もちろん JSON 形式でレスポンスを返すこともできます。

Ktor には Feature と呼ばれる様々な機能が予め用意されています。

例えば認証を行ったりレスポンスヘッダを付与したりなど、一般的な用途であれば大抵のものに対しては自前で実装することなく利用が可能です。

レスポンスを JSON に変換する Content Negotiation と呼ばれる Feature もそのひとつです。

まず、pom.xml に依存を追加します。(Jackson を使ったバージョンを使うことにします。)

<dependency>
    <groupId>io.ktor</groupId>
    <artifactId>ktor-jackson</artifactId>
    <version>${ktor.version}</version>
</dependency>

次に、embeddedServer に対して Feature を install し、テスト用のルーティングを追加します。

package sample

import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main() {
    embeddedServer(Netty, 8080) {
        install(ContentNegotiation) {
            jackson {
                enable(SerializationFeature.INDENT_OUTPUT)
            }
        }
        routing {
            get("/") {
                call.respondText("Hello, world!")
            }
            get("/ping") {
                call.respondText("Pong")
            }
            get("/json") {
                call.respond(ResponseJson("JSON response"))
            }
        }
    }.start(true)
}

data class ResponseJson(val message: String)

リクエストしてみます。

$ curl -v localhost:8080/json
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 27
< Content-Type: application/json; charset=UTF-8
<
* Connection #0 to host localhost left intact
{
  "message":"JSON response"
}

これだけです。めっちゃ簡単ですね。

jackson 関数に渡されるブロックのレシーバは Jackson の ObjectMapper となりますので、JSON 変換のオプションを指定したい場合はそのブロック内で指定します。

まとめと問題点

Ktor について簡単にまとめてみます。

  • サーバの設定は embeddedServer 生成時のブロック内で行うことがきる。(ルーティングなど。)
  • Feature と呼ばれる予め用意された機能を利用できる。使う場合は install 関数を呼び出して登録する。

ここまでのコード例で (本当にミニマムな) API サーバであれば作成することができると思います。 ですが同時に、改善の余地はかなり残っています。

例えば、

  • ルーティングが増えたら embeddedServer の初期化ブロックが肥大化するのでは?
  • リクエストハンドラは routing 内に記述する?
  • 設定ファイルの読み込みは?
  • DI コンテナは併用できない?

などです。

このあたりについては、改善案を紹介していければと思います。

Sealed Secretsを利用したKubernetes Secretリソースのセキュアな管理

はじめに

はじめまして、UZABASE SPEEDA SREの鈴木(@sshota0809)です。

今回は、Sealed Secretsを利用したKubernetesのSecretリソースをセキュアに管理する方法を紹介します。

目次

TL;DR

KubernetesのSecretリソースはBase64の形式にエンコードされるため、エンコードされた内容が確認できれば簡単にデコードできてしまいます。 そのため、SecretリソースをGitHubなどで公開してしまうと機密情報を盗まれてしまうリスクがあります。

一方で、昨今はGitOpsが流行っていたりとKubernetesのリソース(マニュフェストファイル)をGitHub等など管理する需要は高く、当然Secretリソースもその例外ではありません。

  • Bitnamiが開発するOSSであるSealed Secretsを利用することで、Secretリソースをセキュアに管理することが可能となる

Sealed Secretsとは

概要

Bitnamiが開発している、KubernetesのSecretリソースをセキュアに管理することを目的としたOSSです。

github.com

Sealed Secrets以外にもSecretリソースのセキュアな管理を実現するツールは存在しますが、Sealed Secretはアーキテクチャやできる事がシンプルで学習コストが極めて低いという部分が魅力だと思います。

アーキテクチャ

f:id:sshota0809:20200310103551p:plain
Sealed Secretsアーキテクチャ

まず、Sealed Secretsのアーキテクチャを構成しているメインとなるリソースは下記となります。

  • sealed-secretsコントローラー(を管理するDeployment)
    • Sealed Secretsのコアとなるコントローラー。kind: SealedSecretのCRDをCreate/Update/Deleteを監視する
  • 公開鍵/秘密鍵のペアを格納したSecretリソース
    • Secretリソースを暗号化/復号化するために利用するキー。

上記のリソースを利用し、下記のような流れでSecretリソースを管理します。

  1. Sealed Secretsによって生成された公開鍵を利用しkubesealコマンドによってSecretリソースをパース
    • kubesealコマンドによってパースすることでkind: SealedSecretリソースという機密情報が公開鍵で暗号化されたマニュフェストファイルが生成される
  2. kubectlコマンドでSealedSecretリソースをKubernetesクラスタにデプロイ
  3. sealed-secretsコントローラーがSealedSecretリソースのデプロイを検知
  4. sealed-secretsコントローラーがSealed Secretsによって生成された秘密鍵を利用しSealed SecretsリソースをSecretリソースにパースしデプロイ
    • 1の手順で利用した公開鍵のペアとなっている秘密鍵を利用しパースすることで暗号化された内容が復号化されたマニュフェストファイルがデプロイされる

このように、暗号化されたSealedSecretリソースをKubernetesクラスタにデプロイすることで、sealed-secretsコントローラがそれを検知し、自動的に暗号化された内容が復号化されたSecretリソースをクラスタ内部にデプロイしてくれます。

また、SealedSecretリソースは暗号化されているため、GitHub等に公開したとしても対となる秘密鍵を知っている人にしか復号化することができません。 これによりセキュアにSecretリソースをGitHub等で管理することが可能になります。

インストール〜リソースデプロイ

アーキテクチャを説明したところで、実際にインストールからSecretリソースのデプロイまでを行いたいと思います。

インストール

今回はhelmを利用してインストールを行います。 helmのチャートは下記リポジトリにstableなものが公開されています。

github.com

$ git clone https://github.com/helm/charts.git
$ helm template charts/stable/sealed-secrets --name sealed-secrets --namespace sealed-secrets > sealed-secrets.yaml
$ kubectl apply -f sealed-secrets.yaml

helmによって必要なリソースがすべてデプロイされます。 それではデプロイされたリソースの一覧を見てみます。

$ kubectl get all -n sealed-secrets
NAME                                 READY   STATUS    RESTARTS   AGE
pod/sealed-secrets-fff45fbcf-29mrg   1/1     Running   0          4d10h

NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/sealed-secrets   ClusterIP   10.60.174.242   <none>        8080/TCP   77d

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sealed-secrets   1/1     1            1           77d

NAME                                        DESIRED   CURRENT   READY   AGE
replicaset.apps/sealed-secrets-fff45fbcf    1         1         1       33d

$ kubectl get secret -n sealed-secrets
NAME                         TYPE                                  DATA   AGE
default-token-94w7c          kubernetes.io/service-account-token   3      77d
sealed-secrets-keysmg5q      kubernetes.io/tls                     2      77d
sealed-secrets-token-59zdg   kubernetes.io/service-account-token   3      77d

重要なのはSecretリソースの一覧に表示されているsealed-secrets-keysmg5qというリソースです。 こちらが公開鍵と秘密鍵のペアを定義しているSecretリソースとなります。

リソースの中身は、確かに下記のようにtls.crttls.keyが定義されているのが確認できます。

apiVersion: v1
data:
  tls.crt: .........
  tls.key: .........
kind: Secret
...

インストール手順の一貫として、SecretリソースをSealedSecretリソースにパースする際に利用するkubesealコマンドをインストールします。 筆者の環境はmacOSのため、それじ準じた手順としております。

$ brew install kubeseal

Secretリソースのパース

それでは、インストールが完了し準備が整ったのでSecretリソースをパースしてSealedSecretリソースを生成します。

繰り返しにはなりますが、kubesealコマンドによってSecretリソースをパースする際、Sealed Secretsによって生成された公開鍵が必要となります。 そのため、何らかの方法でそれを抜き出す必要があります。

やり方は自由なのですが、kubesealコマンドでは公開鍵のexport機能があるため、それを利用します。

$ kubeseal --fetch-cert \
  --controller-namespace=sealed-secrets \
  --controller-name=sealed-secrets \
  > pub-cert.pem

こうすることで、kubesealコマンドを実行しているローカルの環境にpub-cert.pemという形で公開鍵をexportすることができます。 それでは、この公開鍵を使ってSecretリソースをパースしたいと思います。

今回利用するテスト用のSecretリソースは下記となります。

test-secret.yaml

apiVersion: v1
data:
  test.txt: cGFzc3dvcmQK
kind: Secret
metadata:
  name: test-secret
  namespace: sealed-secrets
type: Opaque

下記コマンドでパースを行います。

$ kubeseal --format=yaml --cert=pub-cert.pem < test-secret.yaml > test-sealedsecret.yaml

すると下記のようなtest-sealedsecret.yamlが生成されました。

test-sealedsecret.yaml

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: test-secret
  namespace: sealed-secrets
spec:
  encryptedData:
    test.txt: AgCKSFmH7nARRrUmQSMsJs5YuRiaFe43eG7ynScc3yKAsJaOUSWyCAyGHauyU0jGlySqhXV2LNnhEtzR1gVIsi2ScWb1+fLOqsw7ronOkmVO8XwddII4ytE9Cb8UA5plZgy6ujRaea++Zr85UuXwLavsQRSMp/rne4bJ7bvng1a4znjSRN5JXPoAIpf4zUWKoLUrhOt6HHhmS8VZ8tM7yf+uS3t3dqDLYfuGStH6ECKszQaCFlNUdokTBv0th1V2rzbzgeRTT0eA3inThsFCa+cnVGqUeM3EdvTLxIltRCQsLBQJmkmGug8xATkPFDWAvn9SLvow4jHcQolzVrRSXZCVs3oyceriK6f2k/Umohs2g78u9lCGRWzrBp+kWsdfQh+cv7+mZvr46evYbAQUJ7pbRk6axXwPJmD3GCg9oIk6RtB/ug8AIUjdkI81RELfMcliQYuySSb2GF8+hkCAgMXLBRZkS1oxCHinbn0AOeRQbm87QwcXFpICBWlyliLtjZ7oa9QB53zRf3pbxsNcxYSTzA6fZEH8GFopADjuZ8OvS2qT83I9ULPhONb2Y7wJkDrpuKQIBqzRsMgv7zMQD16TilzFcXtEVWkDz+Zjwsoio/+lJ83QLJLkGZw5Y8p4rW9D0lfqOo2W0l1q6BpiH4TmZawLmP5TOxQ+xwcWfHtDf6nzJLwvfzsZRhqrmjXD3j+pexf3QeqI3aA=
  template:
    metadata:
      creationTimestamp: null
      name: test-secret
      namespace: sealed-secrets
    type: Opaque
status: {}

Kubernetesクラスタへのデプロイ

SealedSecretリソースが生成できたら、最後にこれをKubernetesクラスタにデプロイします。

$ kubectl apply -f test-sealedsecret.yaml

すると下記のようにSealedSecretリソースとコントローラーがパースしたSecretリソースそれぞれがデプロイされたことが確認できます。

$ kubectl get sealedsecret -n sealed-secrets
NAME          AGE
test-secret   49s

$ kubectl get secret -n sealed-secrets
NAME                  TYPE                                  DATA   AGE
test-secret      Opaque                                      1      55d

後はこのSecretリソースを通常通り各種リソースで利用するだけです。

おわりに

Sealed Secretsは学習コストも低く、できる事もSecretリソースを暗号化/復号化という部分にフォーカスされているのでとてもシンプルです。 GitOpsを実践しようと思っているけどSecretリソースの管理をどうしよう、と悩んでる方ぜひ利用してみてください。

弊社SPEEDAチームではGitOpsはまだ実践できていませんが、アプリケーションに利用する機密情報を一部Sealed Secretsを利用し管理し始めたりしています。

方法より原理 〜正規化ルールとリレーショナルモデルについて〜 【理屈編】

今日は。 SPEEDA を開発している濱口です。

アプリケーションデータの永続化を担うデータストアには様々な選択肢があります。
その1つとして、リレーショナルデータベース(以下、RDB)がありますが、
RDBを選択した場合、データの容れものとしてリレーショナルモデルを選択した、という表明になります。
ひいては、このモデルを正しく使用することが生産性の観点から必要となります。
(明白な設計によるコミュニケーションや制約によるデータ不整合の回避など)
その方法の1つとして正規化ルールがあります。

正規化ルール遵守は有効か

あの星野源さんも知っているはずという、正規化ルールですが、基本情報技術者の試験範囲でもあり、エンジニアであれば少なくとも聞いたことはあり、多かれ少なかれ意識しているものだと思います。
このルールをみんなで正しく運用できればよいのですが、それにはいくつかの阻害要因があると考えています。

「あたりまえ」と軽視されがち

正規化のWikiページの解説は、厳密な定義として書かれているため読みやすくはないと思いますが、
一度理解に達すると「あたりまえのことしか言っていないな」と思うでしょう。
そう考えて安心してしまうと、このルールを積極的に遵守しようという姿勢は失われます。
また、「常識」でしかないのにルールとして厳然と存在しているため疎ましく感じることすらあるかもしれません。

「設計時のみに発生するタスク」という誤解

(ルールを軽視せず)RDBのテーブル設計を行う際に、設計の妥当性を確認するためのチェックリストとして正規化ルールを適用するのはよいのですが(付番されているためチェックリストになりやすいですね)、それで終わりではありません
設計し終わったテーブルに操作を加えるとまた新しいテーブルが生まれます。
ここで言う操作とはリレーショナル演算のことで(SQLでは例えば単体のSELECT文もそのひとつで「射影」という操作です)、
すべてのリレーショナル演算は入力と出力に同じモノを想定します(閉包性*1を持つ、と言います)。
つまり、テーブルをSELECTした結果もまたテーブルだということです。
その出力結果であるテーブルを新たな操作対象(入力)として見た際には当然、正規化ルールが適用済みであることが期待されます。
急いでさっきのチェックリストのレ点を全部消しましょう!
だいぶ疎ましく感じてきました…。

以上のことから、RDBを正しく扱うために、正規化ルールの適用はもちろん必要ですが、ただ「正規化ルールを守りましょう!」というスローガンを掲げるだけでは、効果的とは言えないと考えました。
まさに、言うは易し…ですね。

f:id:yhamaro:20200302123615p:plain
すてちまおう

正規化ルールを忘れ、モデルに導かれて、正規化ルールに至る

正規化をルールとして運用することは難しいことがわかりました。
それならばまず、原点に立ち返る意味で、一度正規化ルールを忘れ、正規化ルールが依って立つところの背景、原理を理解しましょう。
そして、その原理のみによって自然に導かれて設計を行った結果と、正規化ルールを適用したそれとが少なくとも同等であるなら、「ルール」を「運用」する必要が無くなると考えました。
あるべき姿さえ正しく捉えていれば、ルールに縛られずとも大筋で間違うことはなく、且ついつでもどこでも(基底テーブルでも、派生テーブルでも)正しい設計ができるのではないでしょうか。
その原理とはリレーショナルモデルそのものです。

f:id:yhamaro:20200302123639p:plain
こころのめでみるのだ

リレーショナルモデルとは

リレーショナルモデルのリレーション(SQLのテーブルに相当)のデータ構造はひとことで、
「属性のドメイン値の、取りうる組み合わせのすべてを規定しているもの*2」です。
なので、リレーションを設計する行為は上記に当てはめると、「…のすべてを規定すること」と言えます。

ひとことで言ってもわかりにくいと思うので順を追って説明していきます。

まず、「属性のドメイン値」について。
「属性」(SQLの列に相当)には名前と型があります。
その名前と型に規定された、取りうる値の集合がドメインです。
例えば、よく交差点で見かける車両用の信号機の色は赤、青、黄ですが、これをひとつの属性としてみてみると、
「交通信号」という属性名に、「色」という型付けがなされて { '赤', '青', '黃' } というドメインを形成します。

次に、「取りうる組み合わせのすべて」について。
仮に「ハリウッド俳優たちの代表作」というリレーションがあり、
その属性が {'俳優名', '映画名' } の2つ、
ドメイン値がそれぞれ {'ポール・ニューマン', 'デニス・ホッパー' } の2つ、
および、 {'暴力脱獄', 'タワーリング・インフェルノ', 'ブルーベルベット' } 3つだとしたときに、
以下の通り、組み合わせで6通りのタプルが設計上想定されるものとして決定されます。

俳優名映画名
ポール・ニューマン暴力脱獄
ポール・ニューマンタワーリング・インフェルノ
ポール・ニューマンブルーベルベット
デニス・ホッパー暴力脱獄
デニス・ホッパータワーリング・インフェルノ
デニス・ホッパーブルーベルベット

これで設計は終わりです(ちなみに属性名の下線は一意キーを表しています。上記だと { '俳優名', '映画名' } の複合キーです)。
あとはこの属性の設計が表している、下記の述語に対して、真の命題(SQLの行に相当)を入れていくことになります。

ハリウッド俳優、 (俳優名) の最高傑作は『 (映画名) 』である

デニス・ホッパーについては『イージーライダー』をまだ観ていないので、この中から最高傑作を選ぶことは出来ません。
また、『ブルーベルベット』にポール・ニューマンは出演していないのでこれも真の命題になりません。
『タワーリングインフェルノ』と『暴力脱獄』ですが、私は圧倒的に後者の方が好きなので、以下のタプルのみ真の命題としてこのリレーションに入れることにします。

( 'ポール・ニューマン', '暴力脱獄' )

これを入れると、述語に当てはめて、以下の命題が真であることを世界に表明したことになります。

ハリウッド俳優、 ポール・ニューマン の最高傑作は『 暴力脱獄 』である

他の5つのすべて組み合わせについては、"未表明"などというあいまいな扱いではなく、偽であることを表明したことになります。(閉世界仮設に基づいています)

ハリウッド俳優、 ポール・ニューマン の最高傑作は『 タワーリング・インフェルノ 』ではない
(残り4つも同じ)

また、命題は事実について述べているので重複しません
大事なことは2回言ったほうがいいかもしれませんが、事実自体は変わりません。
あと、複数の命題間で順序はありません
大事なことは先に言ったほうがいいかもしれませんが、どちらでも事実自体は変わりません。

リレーションとその設計、そこに値やタプルをマッピングすることが何を意味しているかを述べました。
また、リレーションは先述したリレーショナル演算子が処理できるものである必要があります。
リレーションとリレーショナル演算子がリレーショナルモデルを構成します。
リレーショナルモデルがどんなものであるか、おおよその雰囲気を掴んでいただけたかと思います。

この続き

実践編では、上記の理解だけに基づき、つまり正規化ルールに依らず設計行為を行ったらどうなるかを実例をもって確かめてみたいと思います。
尚、私が書いていることは『データベース実践講義』に書いてあることの理解に基づいています。正確な定義や語彙についてはこの書籍を当たるとよいと思います。

*1:テキストに対してUnixコマンドが、S式に対してLisp関数が、オブジェクトに対してSmalltalk関数が持っていると考えています。

*2:ひとつの数式で現すと、 L ⊆ X1 × … × Xk となります