kakakakakku blog

Weekly Tech Blog: Keep on Learning!

GraphQL Pokémon を使って楽しく学ぶ GraphQL クエリ

GraphQL クエリを学ぶ場合,気軽に使える API があると便利だと思う.よく見るのは GitHub GraphQL APIStar Wars API だけど,個人的に Star Wars の映画を観たことがなく,データの理解が難しいため,良さそうな API を探していた.すると,GitHub で公開されている「GraphQL Pokémon」を発見した.GraphQL で「ポケモン(第一世代)」のポケモンを検索することができる.これは楽しそう!さっそく試してみた.

github.com

クエリ環境

GraphQL Pokémon には GraphiQL コンソールがあり,ブラウザから簡単にクエリを実行できる.

個人的には GraphQL Playground の Mac アプリをよく使っている.インストールしておくと良いと思う.

electronjs.org

f:id:kakku22:20191230121746p:plain

GraphQL スキーマ

GraphQL Pokémon の GraphQL スキーマを以下に載せた.要点をまとめると,ポケモンを表現する Pokemon 型を軸とし,ワザを表現する PokemonAttack 型 と Attack 型 / サイズを表現する PokemonDimension 型 / 進化条件を表現する PokemonEvolutionRequirement 型となる.マスタデータとしてはそこそこ揃っている気がする.そして「ポケモン(第一世代)」を対象とするため,ミュウ (No.151) までデータセットに含まれている.

  • Pokemon
    • PokemonAttack
      • Attack
    • PokemonDimension
    • PokemonEvolutionRequirement
type Attack {
  name: String
  type: String
  damage: Int
}

type Pokemon {
  id: ID!
  number: String
  name: String
  weight: PokemonDimension
  height: PokemonDimension
  classification: String
  types: [String]
  resistant: [String]
  attacks: PokemonAttack
  weaknesses: [String]
  fleeRate: Float
  maxCP: Int
  evolutions: [Pokemon]
  evolutionRequirements: PokemonEvolutionRequirement
  maxHP: Int
  image: String
}

type PokemonAttack {
  fast: [Attack]
  special: [Attack]
}

type PokemonDimension {
  minimum: String
  maximum: String
}

type PokemonEvolutionRequirement {
  amount: Int
  name: String
}

type Query {
  query: Query
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
}

クエリ :「ピカチュウ」を検索する

まず,1番簡単なクエリを実行する.pokemon クエリに name: "Pikachu" を指定する.取得するフィールドは number (番号)name (英語名)types (タイプ) とした.

query {
  pokemon(name: "Pikachu") {
    number
    name
    types
  }
}

すると「ピカチュウ」を検索できる.ピカチュウは Electric (でんき) タイプであると確認できる.

{
  "data": {
    "pokemon": {
      "number": "025",
      "name": "Pikachu",
      "types": [
        "Electric"
      ]
    }
  }
}

クエリ :「ピカチュウ」の進化を検索する

evolutions (進化) フィールドを指定すると進化するポケモンを Pokemon 型で取得できる.

query {
  pokemon(name: "Pikachu") {
    number
    name
    types
    evolutions {
      number
      name
      types
    }
  }
}

すると「ピカチュウ」の進化は「ライチュウ」であると確認できる.REST API と異なり,1回のクエリで階層データまで取得できるのは GraphQL のメリットと言える.

{
  "data": {
    "pokemon": {
      "number": "025",
      "name": "Pikachu",
      "types": [
        "Electric"
      ],
      "evolutions": [
        {
          "number": "026",
          "name": "Raichu",
          "types": [
            "Electric"
          ]
        }
      ]
    }
  }
}

クエリ :「ヒトカゲ」の進化を検索する

3段階進化をする「ヒトカゲ」を検索する.英語版だと名前が異なるため,今回は name: "Charmander" を指定する.

query {
  pokemon(name: "Charmander") {
    number
    name
    types
    evolutions {
      number
      name
      types
    }
  }
}

すると「ヒトカゲ (Charmander)」の進化は「リザード (Charmeleon)」「リザードン (Charizard)」であると確認できる.

{
  "data": {
    "pokemon": {
      "number": "004",
      "name": "Charmander",
      "types": [
        "Fire"
      ],
      "evolutions": [
        {
          "number": "005",
          "name": "Charmeleon",
          "types": [
            "Fire"
          ]
        },
        {
          "number": "006",
          "name": "Charizard",
          "types": [
            "Fire",
            "Flying"
          ]
        }
      ]
    }
  }
}

クエリ : フラグメントを使う

進化を検索したときに Pokemon 型のフィールドを2箇所に定義していたため,GraphQL クエリの「フラグメント」を使って共通化する.以下のように Pokemon 型のフラグメント pokemonInfo を定義し,クエリに含めた.さらに今回は取得するフィールドに weaknesses (弱点) を追加した.

fragment pokemonInfo on Pokemon {
  number
  name
  types
  weaknesses
}

query {
  pokemon(name: "Pikachu") {
    ...pokemonInfo
    evolutions {
      ...pokemonInfo
    }
  }
}

「ピカチュウ」「ライチュウ」Ground (じめん) タイプが弱点であると確認できる.フラグメントを使うと GraphQL クエリをシンプルに書けるし,可読性も高くなるため,タイミングを見極めて積極的に使っていく.

{
  "data": {
    "pokemon": {
      "number": "025",
      "name": "Pikachu",
      "types": [
        "Electric"
      ],
      "weaknesses": [
        "Ground"
      ],
      "evolutions": [
        {
          "number": "026",
          "name": "Raichu",
          "types": [
            "Electric"
          ],
          "weaknesses": [
            "Ground"
          ]
        }
      ]
    }
  }
}

クエリ :「ピカチュウ」と「ライチュウ」のワザを検索する

attacks (ワザ) フィールドを指定すると,ポケモンのワザを PokemonAttack 型と Attack 型で取得できる.PokemonAttack 型 では fast (ワザ)special (わざマシン) の2種類のフィールドが定義されている.今回は Pokemon 型に加えて Attack 型も「フラグメント」を使って共通化する.

fragment attackInfo on Attack {
  name
  type
  damage
}

fragment pokemonInfo on Pokemon {
  number
  name
  types
  weaknesses
  attacks {
    fast {
      ...attackInfo
    }
    special {
      ...attackInfo
    }
  }
}

query {
  pokemon(name: "Pikachu") {
    ...pokemonInfo
    evolutions {
      ...pokemonInfo
    }
  }
}

すると「ピカチュウ」「ライチュウ」のワザを取得できる.ワザの英語名を整理すると以下になると思う(間違っている可能性もある).

  • ピカチュウ
    • fast
      • Quick Attack (でんこうせっか)
      • Thunder Shock (でんきショック)
    • special
      • Discharge (ほうでん)
      • Thunder (かみなり)
      • Thunderbolt (10まんボルト)
  • ライチュウ
    • fast
      • Spark (スパーク)
      • Thunder Shock (でんきショック)
    • special
      • Brick Break (かわらわり)
      • Thunder (かみなり)
      • Thunder Punch (かみなりパンチ)
{
  "data": {
    "pokemon": {
      "number": "025",
      "name": "Pikachu",
      "types": [
        "Electric"
      ],
      "weaknesses": [
        "Ground"
      ],
      "attacks": {
        "fast": [
          {
            "name": "Quick Attack",
            "type": "Normal",
            "damage": 10
          },
          {
            "name": "Thunder Shock",
            "type": "Electric",
            "damage": 5
          }
        ],
        "special": [
          {
            "name": "Discharge",
            "type": "Electric",
            "damage": 35
          },
          {
            "name": "Thunder",
            "type": "Electric",
            "damage": 100
          },
          {
            "name": "Thunderbolt",
            "type": "Electric",
            "damage": 55
          }
        ]
      },
      "evolutions": [
        {
          "number": "026",
          "name": "Raichu",
          "types": [
            "Electric"
          ],
          "weaknesses": [
            "Ground"
          ],
          "attacks": {
            "fast": [
              {
                "name": "Spark",
                "type": "Electric",
                "damage": 7
              },
              {
                "name": "Thunder Shock",
                "type": "Electric",
                "damage": 5
              }
            ],
            "special": [
              {
                "name": "Brick Break",
                "type": "Fighting",
                "damage": 30
              },
              {
                "name": "Thunder",
                "type": "Electric",
                "damage": 100
              },
              {
                "name": "Thunder Punch",
                "type": "Electric",
                "damage": 40
              }
            ]
          }
        }
      ]
    }
  }
}

クエリ :「イーブイ」と進化ポケモンの画像を検索する

GraphQL スキーマを見ると Pokemon 型に image フィールドがある.3種類に進化する可能性のある「イーブイ (Eevee)」を検索して,画像を取得する.

fragment pokemonInfo on Pokemon {
  number
  name
  image
}

query {
  pokemon(name: "Eevee") {
    ...pokemonInfo
    evolutions {
      ...pokemonInfo
    }
  }
}

すると image フィールドに画像 URL が含まれていた.

{
  "data": {
    "pokemon": {
      "number": "133",
      "name": "Eevee",
      "image": "https://img.pokemondb.net/artwork/eevee.jpg",
      "evolutions": [
        {
          "number": "134",
          "name": "Vaporeon",
          "image": "https://img.pokemondb.net/artwork/vaporeon.jpg"
        },
        {
          "number": "135",
          "name": "Jolteon",
          "image": "https://img.pokemondb.net/artwork/jolteon.jpg"
        },
        {
          "number": "136",
          "name": "Flareon",
          "image": "https://img.pokemondb.net/artwork/flareon.jpg"
        }
      ]
    }
  }
}

「GraphQL Pokémon」の GitHub を見ると,ポケモン画像を使った React サンプルアプリも公開されていた.GraphQL / React / Relay / Material-UI を組み合わせた実装例としても参考になる.以下にサンプルアプリのキャプチャを載せておく.

f:id:kakku22:20191230130835p:plain

クエリ : ポケモン一覧を検索する

GraphQL スキーマを見ると,もう1種類 pokemons というクエリも用意されている.柔軟に検索できるのかと予想したけど,実際には first パラメータしかなく,最初から何種類のポケモンを取得する?という用途だった.一覧を作る以外は用途はなさそう.

query {
  pokemons(first: 3) {
    name
  }
}

今回は first: 3 を指定したため,「フシギダネ(Bulbasaur)」「フシギソウ (Ivysaur)」「フシギバナ (Venusaur)」を取得できる.ポケモンの英語名を覚える練習に使えそう.英語名だと本当にわからなかった.

{
  "data": {
    "pokemons": [
      {
        "name": "Bulbasaur"
      },
      {
        "name": "Ivysaur"
      },
      {
        "name": "Venusaur"
      }
    ]
  }
}

まとめ

  • GraphQL クエリを学ぶ場合,気軽に使える API があると便利
  • GitHub GraphQL APIStar Wars API 以外にもたくさんある
  • 「GraphQL Pokémon」を使うと GraphQL で「ポケモン(第一世代)」のポケモンを検索することができて楽しく学べる!

Envoy の EDS を REST API で体験する「API Based Dynamic Routing Configuration」を試した

今回は「Try Envoy」「API Based Dynamic Routing Configuration」を紹介する.前回紹介した「File Based Dynamic Routing Configuration」と関連した内容だけど,今回は API を使った「ディスカバリサービス (xDS)」を学べる.特に Envoy は RESTgRPC をサポートしているけど,今回のコンテンツだと REST を学べる.なお,前回の記事も載せておく.

kakakakakku.hatenablog.com

API Based Dynamic Routing Configuration

手順は以下の「計7種類」ある.今までの Katacoda と同じく文章だと理解しにくい点も多く,自分なりに理解を整理しながら進めた.

  • Step.1 「Introduction」
  • Step.2 「EDS Configuration」
  • Step.3 「Start upstream services」
  • Step.4 「Start EDS」
  • Step.5 「Add endpoint to EDS」
  • Step.6 「Delete Endpoint」
  • Step.7 「Disconnect EDS server」

www.envoyproxy.io

www.katacoda.com

Step.1 「Introduction」

今回は EDS (Endpoint Discovery Service) を REST API を使って実現することにより,エンドポイント情報を取得する.過去記事の内容を復習すると,エンドポイントはクラスタメンバーと表現することもできて,転送先のことを意味する.xDS API を実装する場合,現在だと Java と Go でコントロールプレーンの実装 (gRPC + Protocol Buffers) が Envoy から公開されていて参考になる.ただし,今回は独自実装の API を試す.

Step.2 「EDS Configuration」

envoy.yaml の設定は前回と似ている.今回異なるのは eds_configapi_config_source を設定している点で「File Based」のときは path を設定していた.さらに今回は EDS API として,もう1個 Cluster eds_cluster を追加している.なお,api_typeREST (v2) と REST_LEGACY (v1) があり,既に API v1 は deprecated になっている.API v2 の詳細は以下のドキュメントに載っている.

www.envoyproxy.io

clusters:
- name: targetCluster
  type: EDS
  connect_timeout: 0.25s
  eds_cluster_config:
    service_name: myservice
    eds_config:
      api_config_source:
        api_type: REST
        cluster_names: [eds_cluster]
        refresh_delay: 5s

- name: eds_cluster
  type: STATIC
  connect_timeout: 0.25s
  hosts: [{ socket_address: { address: 172.18.0.4, port_value: 8080 }}]

さっそく Envoy を起動する.ただし,EDS API はまだ存在してなく設定のみとなる.構成図は以下のようになる.

$ docker run --name=api-eds -d \
    -p 9901:9901 \
    -p 80:10000 \
    -v /root/:/etc/envoy \
    envoyproxy/envoy:latest

f:id:kakku22:20191226121518p:plain

Step.3 「Start upstream services」

今回も katacoda/docker-http-server を起動し,個別に接続確認をしておく.

$ docker run -p 8081:8081 -d -e EDS_SERVER_PORT='8081' katacoda/docker-http-server:v4

$ curl http://localhost:8081 -i
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 36
Server: Werkzeug/0.15.4 Python/2.7.16
Date: Thu, 26 Dec 2019 00:00:00 GMT

35c4aef0-e0df-46ac-bd3b-8b12cb3741c9

ただし,コンテナイメージのタグが v4 になっていて,サイズも大きく,何やら Python 環境が入っているけど,詳細は書かれていなかった.あと,環境変数 EDS_SERVER_PORT が追加されている点も異なる.

$ docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" katacoda/docker-http-server
REPOSITORY                    TAG                 SIZE
katacoda/docker-http-server   v4                  932MB
katacoda/docker-http-server   latest              7.59MB

Envoy のログを確認すると,まだ EDS API に接続できず,エラーになっている.

$ docker logs xxx
[2019-12-26 00:00:00.000][000008][warning][config] [bazel-out/k8-opt/bin/source/common/config/_virtual_includes/http_subscription_lib/common/config/http_subscription_impl.h:101] REST config update failed: fetch failure

構成図は以下のようになる.

f:id:kakku22:20191226182058p:plain

Step.4 「Start EDS」

次にやっと EDS API を起動する.今回は Python で実装されたプロトタイプを使うことになり,コードは GitHub に公開されている.ここで Envoy と EDS API の接続はできたけど,まだエンドポイントが設定されてなく,実際に Envoy にリクエストを送ると no healthy upstream と返ってくる.挙動としては期待値と言える.

$ docker run -p 8080:8080 -d katacoda/eds_server;

$ curl http://localhost
no healthy upstream

構成図は以下のようになる.

f:id:kakku22:20191226182113p:plain

Step.5 「Add endpoint to EDS」

次に EDS API に curl でリクエストを送り,エンドポイントを追加する.今回は既に起動済の katacoda/docker-http-server にルーティングする.

curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
  "hosts": [
    {
      "ip_address": "172.18.0.3",
      "port": 8081,
      "tags": {
        "az": "us-central1-a",
        "canary": false,
        "load_balancing_weight": 50
      }
    }
  ]
}' http://localhost:8080/edsservice/myservice

Envoy にリクエストを送ると,同じ結果が返ってくる.

$ curl http://localhost
35c4aef0-e0df-46ac-bd3b-8b12cb3741c9

$ curl http://localhost
35c4aef0-e0df-46ac-bd3b-8b12cb3741c9

次に katacoda/docker-http-server を4コンテナ追加し,計5コンテナになる.

$ for i in 8082 8083 8084 8085
  do
      docker run -d -e EDS_SERVER_PORT=$i katacoda/docker-http-server:v4;
      sleep .5
done

追加した4コンテナをエンドポイントとして EDS API に追加する.なお,設定している load_balancing_weight はドキュメントを読むと「合計値をエンドポイント数で割る」と書いてあるため,5コンテナに均等分散するという意味になる.

www.envoyproxy.io

$ curl -X PUT --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
    "hosts": [
        {
        "ip_address": "172.18.0.3",
        "port": 8081,
        "tags": {
            "az": "us-central1-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.18.0.5",
        "port": 8082,
        "tags": {
            "az": "us-central1-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.18.0.6",
        "port": 8083,
        "tags": {
            "az": "us-central1-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.18.0.7",
        "port": 8084,
        "tags": {
            "az": "us-central1-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        },
        {
        "ip_address": "172.18.0.8",
        "port": 8085,
        "tags": {
            "az": "us-central1-a",
            "canary": false,
            "load_balancing_weight": 50
        }
        }
    ]
    }' http://localhost:8080/edsservice/myservice

実際に Envoy にリクエストを送ると,うまく分散されている.エンドポイント数が少ないからかもしれないけど,たまに2回連続で同じコンテナからレスポンスが返ることもあった.

$ while true; do curl http://localhost; sleep .5; printf '\n'; done
dbc1fe2f-7bfa-44e6-8fff-52a57190de95
28959add-c2df-47cf-8045-c09911a9e913
8d366f05-18d8-4f72-936f-ab282615f6ca
f7f7599d-1be9-480c-96e9-6619b2843499
35c4aef0-e0df-46ac-bd3b-8b12cb3741c9

構成図は以下のようになる.

f:id:kakku22:20191226182130p:plain

Step.6 「Delete Endpoint」

今度は EDS API から5コンテナの情報を消す.すると,Step.4 と同じ no healthy upstream となった.

$ curl -X PUT --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
  "hosts": [  ]
}' http://localhost:8080/edsservice/myservice

$ curl -v http://localhost
no healthy upstream

Step.7 「Disconnect EDS server」

最後は Step.5 と同じくもう1度 EDS API にエンドポイントを追加してから,EDS API を停止する.

$ docker ps -a | awk '{ print $1,$2 }' | grep katacoda/eds_server  | awk '{print $1 }' | xargs -I {} docker stop {};
$ docker ps -a | awk '{ print $1,$2 }' | grep katacoda/eds_server  | awk '{print $1 }' | xargs -I {} docker rm {}

一度 Envoy 側で EDS API からエンドポイント情報を取得した場合,Envoy 側には影響なしで使えることがわかる.

$ while true; do curl http://localhost; sleep .5; printf '\n'; done
35c4aef0-e0df-46ac-bd3b-8b12cb3741c9
dbc1fe2f-7bfa-44e6-8fff-52a57190de95
8d366f05-18d8-4f72-936f-ab282615f6ca
f7f7599d-1be9-480c-96e9-6619b2843499
28959add-c2df-47cf-8045-c09911a9e913

構成図は以下のようになる.

f:id:kakku22:20191226182144p:plain

まとめ

  • 「Try Envoy」のコンテンツ「API Based Dynamic Routing Configuration」を試した
  • EDS (Endpoint Discovery Service) API を REST で起動し,エンドポイントの dynamic な反映を体験できた
  • katacoda/docker-http-server:v4 の解説がなかったり,gRPC の xDS API の体験がなかったり,もう少し学びたかった点もある

引き続き,進めていくぞ!

Try Envoy 関連

JetBrains エディタで API リクエストをコード化できる「HTTP client」

実装した API の動作確認とテストをするときに,今までは curlPostman を主に使っていたけど,最近は JetBrains エディタで使える「HTTP client」も併用している.今日は API リクエストをファイルに記述しコード化できる「HTTP client」の概要を紹介する.JetBrains のドキュメントは基本的に英語だけど pleiades.io なら日本語で読める.

今回は検証環境として Sinatra を使った API を実装し,HTTP client から http://localhost:4567 にリクエストを送る.現在 RubyMineGoLand のライセンスを購入しているため,今回は RubyMine を使って記事をまとめたけど,他の JetBrains エディタでも基本的に使える(IntelliJ IDEA だと Ultimate 限定).

リクエストファイル

まず「リクエストファイル」を作る.RunyMine で「New → HTTP Request」と選択することもできるし,新規ファイルの拡張子を .http もしくは .rest にして作ることもできる.最もシンプルなリクエストファイルは以下となる.

GET http://localhost:4567/ping

今回はファイル名を api.http とした.ファイルを作成するとエディタは以下のような UI になる.

f:id:kakku22:20191224233117p:plain

「Run All Requests in Files」ボタンもしくは GET の左にある「Run ▶」ボタンを押すとリクエストファイルを実行できる.実行すると「ヘッダー」「レスポンス」を確認できる.

GET http://localhost:4567/ping

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 4
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Server: WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29)
Date: Sat, 24 Dec 2019 14:00:00 GMT
Connection: Keep-Alive

pong

Response code: 200 (OK); Time: 52ms; Content length: 4 bytes

テンプレート機能

リクエストファイルで ⌘ + J と入力すると,以下の「計6種類」あるテンプレートから書き出せる.名前の略称は謎だけど,gt : GETpt : POSTr : Request など,傾向はありそう.

  • fptr : POST Request with file
  • gtr : GET Request
  • gtrp : GET Request with parameters
  • mptr : POST Request with multipart
  • ptr : POST Request
  • ptrp : POST Request with parameters

f:id:kakku22:20191224233705p:plain

例えば gtr を選択すると,以下のようにシンプルな GET リクエストを記述できる.

GET http://localhost:80/api/item
Accept: application/json

###

例えば ptr を選択すると,以下のようにシンプルな POST リクエストを記述できる.

POST http://localhost:80/api/item
Content-Type: application/json

{}

###

セパレータ機能

テンプレート機能を使うと自動的に末尾が ### になり,なんだろう?と気になると思う.これは「セパレータ」と言って,リクエストファイルの中に複数のリクエストを記述できることを意味している.以下は GETPOST の2種類のリクエストを記述している.

GET http://localhost:4567/ping

###

POST http://localhost:4567/users
Content-Type: application/json

{
  "name": "kakakakakku"
}

###

「Run All Requests in Files」ボタンを押すとまとめて実行できるし,「Run ▶」ボタンを押すと個別に実行できる.以下のように結果もまとめて確認できる.

f:id:kakku22:20191225001900p:plain

Examples 機能

リクエストファイルのメニューバーにある「Examples」ボタンを押すと HTTP client に同梱されたリクエストファイルの具体的な記述例を確認できる.現在は「計4種類」あり,実行することもできる.

  • Get Requests
  • Post Requests
  • Requests with Authorization
  • Requests with Tests

個人的には「テンプレート機能」よりも使う場面が多いと思う.変数を使ったり,認証をしたり,応答ハンドラスクリプトを書いたり(後述),すぐに使える記述例をコピーできる.

f:id:kakku22:20191225002111p:plain

Convert from cURL 機能

リクエストファイルのメニューバーにある「Convert from cURL」ボタンを押すと,curl コマンドをリクエストファイルの記述に変換できる.既存スクリプトを HTTP client に移行しやすくなる便利な機能だと思う.

f:id:kakku22:20191225003550p:plain

変数機能

HTTP client には「動的変数」「環境変数」がある.「動的変数」は計3種類あり,自動的に値を設定してくれる.要件に合う場合に使える.

  • $uuid : UUID を返す
  • $timestamp : UNIX Timestamp を返す
  • $randomInt : 0 - 1000 の範囲から乱数を返す

実際にリクエストファイルを記述すると以下のようになる.

POST http://localhost:4567/logging
Content-Type: application/json

{
  "uuid": "{{$uuid}}",
  "randomInt": "{{$randomInt}}",
  "timestamp": "{{$timestamp}}"
}

###

「環境変数」は環境ごとに任意の値を設定できる.まず,環境設定を記述する http-client.env.json もしくは rest-client.env.json を作成する.今回は dev 環境と prd 環境に name 変数を定義するファイルを作成する.

{
  "dev": {
    "name": "kakakakakku-dev"
  },

  "prd": {
    "name": "kakakakakku-prd"
  }
}

リクエストファイルには {{name}} と記述する.

POST http://localhost:4567/users
Content-Type: application/json

{
  "name": "{{name}}"
}

###

すると「Run All Requests in Files」ボタンを押して実行するときに環境を選択できるようになり,該当する環境変数が設定される.

f:id:kakku22:20191225010103p:plain

応答ハンドラスクリプト機能

「応答ハンドラスクリプト機能」を使うと,リクエストファイルを実行した後にレスポンスを検証し,テストコードを記述できるようになる.ハンドラスクリプト自体は JavaScript を使う.例えば,以下はレスポンスコード 200404 に対して response.status === 200 という条件で検証している.

GET https://httpbin.org/status/200

> {%
    client.test("Request executed successfully", function() {
        client.assert(response.status === 200, "Response status is not 200");
    });
%}

###

GET https://httpbin.org/status/404

> {%
    client.test("Request executed successfully", function() {
        client.assert(response.status === 200, "Response status is not 200");
    });
%}

実行すると,期待した通りに2個目のテストは落ちる.もしかしたら簡単な TDD もできそう!

f:id:kakku22:20191225010956p:plain

ログ機能

作業ディレクトリの .idea/httpRequests/ 直下に HTTP client のログが「最大50件」保存されている.さらにリクエストファイルのメニューバーにある「Open Log」ボタンを押すと,ログを確認しながら「Run ▶」ボタンで個別に再実行もできる.

機能は他にもある

今回紹介しなかった機能もある.例えば以下など.

  • リクエストファイルにコメントを書く
  • 認証のために Authorization ヘッダーを送信する(Basic 認証 / Digest認証)
  • リダイレクトに対応する

リクエストファイルの具体的な解説は以下のドキュメントにある.

リクエストファイルの構文仕様は GitHub に載っている.

github.com

まとめ

JetBrains エディタで使える「HTTP client」は API リクエストをファイルに記述できる.今回紹介した多くの機能を使ってリッチな API の動作確認とテストを実現できるし,何よりもチーム開発においては「リクエストファイルをコード化してリポジトリで管理できる」という点に価値がある.最近は個人 GitHub リポジトリに api.http を置くようにしている.

とは言え,メンバー全員を JetBrains エディタに統一するのは本質的ではなく,例えば VS Code など他のエディタでもリクエストファイルを認識できると良さそう.調べてみると,リクエストファイルをサポートする拡張機能「REST Client」はあるけど,構文が微妙に違う気がする.

「HTTP client」 を使って API リクエストをコード化しよう!

Envoy のディスカバリサービス (xDS) を学べる「File Based Dynamic Routing Configuration」を試した

今回は「Try Envoy」「File Based Dynamic Routing Configuration」を紹介する.今までの内容は envoy.yaml に static な設定をしていたけど,設定を dynamic に反映できる Envoy の「ディスカバリサービス (xDS)」を学べる.また Envoy は xDS として「File Based(ファイル)」「API Based(REST / gRPC)」をサポートしている.今回は「File Based」を試す.

File Based Dynamic Routing Configuration

手順は以下の「計8種類」ある.それでもなお「Estimated Time: 10 minutes」と書いてあって,雑すぎるでしょ!

  • Step.1 「Envoy Dynamic Configuration」
  • Step.2 「Cluster ID」
  • Step.3 「EDS Configuration」
  • Step.4 「EDS Configuration」
  • Step.5 「Start Envoy」
  • Step.6 「Apply Changes」
  • Step.7 「CDS Configuration」
  • Step.8 「CDS Apply Changes」

www.envoyproxy.io

www.katacoda.com

Step.1 「Envoy Dynamic Configuration」

Envoy には,今まで使ってきた static な設定以外に dynamic な設定があり,「ディスカバリサービス (xDS)」と言う.xDS と言われている通り,様々な種類がある.今回のコンテンツでは「EDS / CDS / LDS」を試す.

  • EDS (Endpoint Discovery Service)
  • CDS (Cluster Discovery Service)
  • RDS (Route Discovery Service)
  • LDS (Listener Discovery Service)
  • SDS (Secret Discovery Service)

ドキュメントを読むと,コンテンツに載っている xDS 以外にも種類があった.例えば,以下など.

  • VHDS (Virtual Host Discovery Service)
  • SRDS (Scoped Route Discovery Service)
  • RTDS (RunTime Discovery Service)

www.envoyproxy.io

Step.2 「Cluster ID」

最初に envoy.yamlnode を設定する.この設定は今までのコンテンツには出てこなかった.xDS を使うときに Envoy 自体を識別する ID となり,今回は適当に id_1 とした.

node:
  id: id_1
  cluster: test

Step.3 「EDS Configuration」

今までは envoy.yamlclusters に転送するホストを static に設定していたけど,今回は EDS (Endpoint Discovery Service) を使うため eds_configeds.conf を指定する.ファイル自体は Step.4 で作成する.

clusters:
- name: targetCluster
  connect_timeout: 0.25s
  lb_policy: ROUND_ROBIN
  type: EDS
  eds_cluster_config:
    service_name: localservices
    eds_config:
      path: '/etc/envoy/eds.conf'

今回は「File Based」なので path を指定した.もし api_config_source を指定すると「API Based」になる.

www.envoyproxy.io

Step.4 「EDS Configuration」

次に eds.conf を作成する.まず,エンドポイントを1個にして,IP アドレスを 172.18.0.3 にする.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
    "cluster_name": "localservices",
    "endpoints": [{
      "lb_endpoints": [{
        "endpoint": {
          "address": {
            "socket_address": {
              "address": "172.18.0.3",
              "port_value": 80
            }
          }
        }
      }]
    }]
  }]
}

Step.5 「Start Envoy」

実際に Envoy と katacoda/docker-http-server を起動し,以下の構成図のようになる.

$ docker run --name=proxy-eds-filebased -d \
    -p 9901:9901 \
    -p 80:10000 \
    -v /root/:/etc/envoy \
    envoyproxy/envoy:latest

$ docker run -d katacoda/docker-http-server

$ docker run -d katacoda/docker-http-server

f:id:kakku22:20191217065539p:plain

eds.conf にエンドポイントを1個しか設定していないため,何度リクエストを送っても,特定のコンテナに転送される.

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

Step.6 「Apply Changes」

eds.conf を修正し,エンドポイントを2個にする.以下の構成図のようになる.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
    "cluster_name": "localservices",
    "endpoints": [{
      "lb_endpoints": [{
        "endpoint": {
          "address": {
            "socket_address": {
              "address": "172.18.0.3",
              "port_value": 80
            }
          }
        }
      },
        {
        "endpoint": {
          "address": {
            "socket_address": {
              "address": "172.18.0.4",
              "port_value": 80
            }
          }
        }
      }]
    }]
  }]
}

f:id:kakku22:20191217065554p:plain

修正した eds.conf を Envoy に自動的に反映するために mv コマンドを使ってファイルを差し替える.ドキュメントにも記載がある通り,Envoy はファイルの mv を監視している.

# Envoy will only watch the file path for moves.
$ mv eds.conf tmp; mv tmp eds.conf

直後に Envoy にリクエストを送ると,Envoy コンテナを再起動せずに eds.conf を反映できた.

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost
<h1>This request was processed by host: edd664e9d604</h1>

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost
<h1>This request was processed by host: edd664e9d604</h1>

Step.7 「CDS Configuration」

最後 2 Steps は CDS (Cluster Discovery Service)LDS (Listener Discovery Service) を試す.まず,CDS の設定となる cds.conf を作成する.既に作成した EDS と連携している.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.Cluster",
    "name": "targetCluster",
    "connect_timeout": "0.25s",
    "lb_policy": "ROUND_ROBIN",
    "type": "EDS",
    "eds_cluster_config": {
      "service_name": "localservices",
      "eds_config": {
        "path": "/etc/envoy/eds.conf"
      }
    }
  }]
}

次に LDS の設定をする lds.conf を作成する.今まで envoy.yaml に設定していた filters を LDS に移したイメージとなる.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.Listener",
    "name": "listener_0",
    "address": {
      "socket_address": {
        "address": "0.0.0.0",
        "port_value": 10000
      }
    },
    "filter_chains": [{
      "filters": [{
        "name": "envoy.http_connection_manager",
        "config": {
          "stat_prefix": "ingress_http",
          "codec_type": "AUTO",
          "route_config": {
            "name": "local_route",
            "virtual_hosts": [{
              "name": "local_service",
              "domains": [
                "*"
              ],
              "routes": [{
                "match": {
                  "prefix": "/"
                },
                "route": {
                  "cluster": "targetCluster"
                }
              }]
            }]
          },
          "http_filters": [{
            "name": "envoy.router"
          }]
        }
      }]
    }]
  }]
}

最後に新しく envoy1.yaml を作成し,cds.conflds.conf を設定している.全てを dynamic_resources の中に設定したため,とてもシンプルになった.おおおー!

node:
  id: id_1
  cluster: test

dynamic_resources:
  cds_config:
    path: "/etc/envoy/cds.conf"
  lds_config:
    path: "/etc/envoy/lds.conf"

既に Envoy を起動しているため,今回は 81 Port で新しい Envoy を起動する.特に挙動は変わらないけど,以下の構成図のようになる.

docker run --name=proxy-eds-cds-lds-filebased -d \
    -p 9902:9901 \
    -p 81:10000 \
    -v /root/:/etc/envoy \
    -v /root/envoy1.yaml:/etc/envoy/envoy.yaml \
    envoyproxy/envoy:latest

$ curl localhost:81
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost:81
<h1>This request was processed by host: edd664e9d604</h1>

$ curl localhost:81
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost:81
<h1>This request was processed by host: edd664e9d604</h1>

f:id:kakku22:20191217070613p:plain

Step.8 「CDS Apply Changes」

最後は cds.conflds.conf を修正して Envoy に反映する.まず,新しく cds.conf に Cluster newTargetCluster を追加する.

{
  "version_info": "0",
  "resources": [{
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "targetCluster",
      "connect_timeout": "0.25s",
      "lb_policy": "ROUND_ROBIN",
      "type": "EDS",
      "eds_cluster_config": {
        "service_name": "localservices",
        "eds_config": {
          "path": "/etc/envoy/eds.conf"
        }
      }
    },
    {
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "newTargetCluster",
      "connect_timeout": "0.25s",
      "lb_policy": "ROUND_ROBIN",
      "type": "EDS",
      "eds_cluster_config": {
        "service_name": "localservices",
        "eds_config": {
          "path": "/etc/envoy/eds1.conf"
        }
      }
    }
  ]
}

Cluster newTargetCluster から転送されるエンドポイントも新しく eds1.conf として設定する.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
    "cluster_name": "localservices",
    "endpoints": [{
      "lb_endpoints": [{
          "endpoint": {
            "address": {
              "socket_address": {
                "address": "172.18.0.6",
                "port_value": 80
              }
            }
          }
        },
        {
          "endpoint": {
            "address": {
              "socket_address": {
                "address": "172.18.0.7",
                "port_value": 80
              }
            }
          }
        }
      ]
    }]
  }]
}

lds.conf は Cluster を newTargetCluster に変更しておく.

"route": {
  "cluster": "newTargetCluster"
}

修正した cds.conflds.confmv コマンドを使って差し替える.すると,自動的に新しく起動した katacoda/docker-http-server に接続できるようになった.

$ docker run -d katacoda/docker-http-server

$ docker run -d katacoda/docker-http-server

$ mv cds.conf tmp; mv tmp cds.conf; mv lds.conf tmp; mv tmp lds.conf

$ curl localhost:81
<h1>This request was processed by host: beac7d8bc5d3</h1>

$ curl localhost:81
<h1>This request was processed by host: 8d90f60e8a19</h1>

$ curl localhost:81
<h1>This request was processed by host: beac7d8bc5d3</h1>

$ curl localhost:81
<h1>This request was processed by host: 8d90f60e8a19</h1>

最終的に構成図は以下のようになる.

f:id:kakku22:20191217071552p:plain

まとめ

  • 「Try Envoy」のコンテンツ「File Based Dynamic Routing Configuration」を試した
  • Envoy でサポートされている「ディスカバリサービス (xDS)」の一部を学べた
    • EDS (Endpoint Discovery Service)
    • CDS (Cluster Discovery Service)
    • LDS (Listener Discovery Service)
  • 次は 「API Based」な xDS を試すために「API Based Dynamic Routing Configuration」をまとめる!

プルリクエスト

試しながら気付いた誤りを修正してプルリクエストを送っておいた!

github.com

Try Envoy 関連

今から Ansible に入門する初学者は読むべし /「Ansible 実践ガイド 第3版」を読んだ

2019年10月に発売された「Ansible 実践ガイド 第3版」を読んだ.実は今年の頭に「第2版」を購入していて,読もう読もうと積読をしていたら「第3版」が発売されたため,すぐに買い直して積読の優先順位を入れ替えた.個人的にプロダクション環境だと Chef の経験が長く,Ansible の経験が少ないこともあり,体系的に知識を整理しておこうという目的で読んだ.

読んだ感想としては,Ansible 初学者を中心に「Ansible で実現できることを知る」ことに適した良い本だと思う.Ansible の基礎から応用(徹底活用)まで幅広く学べる.逆に言うと,仕様を網羅したリファレンス本ではないし,ステップバイステップに写経をしながら試す本でもなく「どう読むと効果的なのか?」は気になるところ.今回は検証環境を Vagrant と Amazon EC2 に構築し,本書をリファレンスのように読みながら,知らなかった機能が出てきたら,実際にプレイブックを実装して試した.試しながら読み進めたことにより,学習効率は高かったと思う.

Ansible実践ガイド 第3版 (impress top gear)

Ansible実践ガイド 第3版 (impress top gear)

目次

  • 第1章 : Ansible の概要
  • 第2章 : Ansible の基礎
  • 第3章 : プレイブックとインベントリ
  • 第4章 : アプリケーションデプロイメント - Orchestration
  • 第5章 : システムの構成管理 - Configuration Management
  • 第6章 : ブートストラッピング - Bootstrapping
  • 第7章 : Ansible の徹底活用
  • 第8章 : 組織で実践する自動化

以下のサイトに正誤表は公開されているけど,特に報告はされていなさそうだった.実際に読むと数点誤植があるため,記事の最後にメモ程度に残しておく.

book.impress.co.jp

with_itemsloop

第3章「プレイブックとインベントリ」では,基本的なプレイブックの構文を学べる.その中に loop の解説があり,前から使える with_items との差を理解できてなく,実際に試しながら理解を整理した.loop は Ansible 2.5 で追加された構文で,ドキュメントを読むと「推奨 (recommend)」と書いてある.ただし,まだ完全に with_ の置き換えになるわけではなく,用途次第であるとも書かれている.

docs.ansible.com

まず,本書を参考に以下のプレイブックを実装した.loop の中にリスト(シーケンス)を実装し,{{ item }} で参照すると,順番に展開されて実行できる.今回は user モジュールを使って Linux にユーザーを3個追加する.

- hosts: all
  tasks:
    - name: Add users
      user:
        name: "{{ item }}"
        state: present
        groups: wheel
      loop:
        - kakakakakku1
        - kakakakakku2
        - kakakakakku3

実際に実行すると,正常に3個追加できた.なお,シーケンスに { name: 'kakakakakku1', groups: 'wheel' } のようなマッピングを設定し,{{ item.name }} とドット区切りにすると,複数の変数の値を展開することもできる.

$ getent passwd | grep kakakakakku | cut -d: -f1
kakakakakku1
kakakakakku2
kakakakakku3

なお,本書には with_itemsloop の差も解説されていた.簡単に言うと,with_items はシーケンスを flatten に展開し,loop は記載通りに展開する.変数の値を debug モジュールで標準出力する以下のプレイブックを実装した.シーケンスを変数にし,さらにリスト形式と文字列形式を混在させている.

- hosts: all
  vars:
    loop_test:
      - [kakakakakku1, kakakakakku2]
      - kakakakakku3
  tasks:
    - name: with_items
      debug:
        msg: "{{ item }}"
      with_items: "{{ loop_test }}"
    - name: loop
      debug:
        msg: "{{ item }}"
      loop: "{{ loop_test }}"

実際に実行すると,以下のようになる.with_items だと,リスト形式は無視されて,3回独立に実行されている.loop だと,2回実行されている.覚えておこう.

TASK [with_items] **************************************************************

ok: [localhost] => (item=kakakakakku1) => {
"msg": "kakakakakku1"
}

ok: [localhost] => (item=kakakakakku2) => {
"msg": "kakakakakku2"
}

ok: [localhost] => (item=kakakakakku3) => {
"msg": "kakakakakku3"
}

TASK [loop] ********************************************************************

ok: [localhost] => (item=[u'kakakakakku1', u'kakakakakku2']) => {
"msg": [
"kakakakakku1",
"kakakakakku2"
]
}

ok: [localhost] => (item=kakakakakku3) => {
"msg": "kakakakakku3"
}

serialmax_fail_percentage

第4章「アプリケーションデプロイメント」では,HAProxy / PHP / MariaDB / Keepalived などを組み合わせたフルスタックな WordPress 環境を Ansible で構築しながら,オーケストレーションに該当する Ansible の機能を学べる.管理するノード数が増えるため,Vagrant などを使って検証環境を作らないと,流し読みをして終わりになってしまう懸念もある.

その中に「nginx をローリングアップデートする」という内容がある.ノードにプレイブックを実行するときに,ミドルウェアの再起動を伴う場合などもあり,ロードバランサからノードを順番に切り離していく場面は多いと思う.ただし,デフォルトだと並列に実行されてしまうため,Ansible では serialmax_fail_percentage を使って,うまくローリングアップデートを実現できる.

docs.ansible.com

まず,プレイブックに serial を設定すると,任意の並行数を設定できる.例えば,以下のようにプレイブックを実装すると,1ノードごとにローリングアップデートとなる.

- name: Rolling Update
  hosts: apps
  serial: 1
  (中略)

ノード数が増えると,1ノードごとだとデプロイ時間が長時間化してしまう可能性もある.serial「割合 (%)」を設定することで,デプロイ時間を短縮しつつ,サービス影響のない範囲でローリングアップデートができる.

- name: Rolling Update
  hosts: apps
  serial: "30%"
  (中略)

さらに serial「並行数と割合 (%) を組み合わせたシーケンス」を設定することで,最初は1ノード,次に5ノード,残りを 20% ずつといったカナリア的なアップデートも実現できる.これは便利!

- name: Rolling Update
  hosts: apps
  serial:
  - 1
  - 5
  - "20%"
  (中略)

最後は max_fail_percentage で,プレイブックの実行を止める失敗率を定義することができる.ローリングアップデートによる全面障害を避けるためにも max_fail_percentage は設定しておくのが良さそう.

- name: Rolling Update
  hosts: apps
  serial: 1
  max_fail_percentage: 30
  (中略)

reboot モジュール

第5章「システムの構成管理」では,Linux と Windows の構成管理を学べる.その中に reboot モジュールの紹介があり,Ansible で再起動が必要なときに,SSH の接続を維持したまま再起動ができるようになる.Ansible 2.7 から使える機能となる.

- hosts: all
  tasks:
    - name: Reboot
      reboot:

docs.ansible.com

徹底活用

第7章「Ansible の徹底活用」は今後の参考になる実践的な内容だった.例えば,インベントリとプレイブックを管理するディレクトリ構成の紹介があったり,Ansible Galaxy の紹介もあった.

galaxy.ansible.com

また,パフォーマンス改善として「ファクト収集を無効化」したり,「ファクトキャッシュを有効化」したり,ansible.cfgforks パラメータを設定して並行数を上げたり,今まで知らなかったアプローチを知ることができた.実際に現場で使うときにもう1度読み直す.

誤植

  • 第1章 P.22 : Amazon Web ServiceAmazon Web Services
  • 第5章 P.202 : 疎結合しておくと疎結合にしておくと
  • 第7章 P.352 : ファクト収集ファクト取得 が表記揺れになっている

なお,誤植ではないけど「第4章」の基本構成で,PHP 実行環境として PHP 5.6 が前提になっている.PHP 5.6 は既に EOL になって1年となり,「第3版」として書き直すなら変えても良さそうだった(執筆の開始時期にもよるから判断は難しいけど).

まとめ

  • 「Ansible 実践ガイド 第3版」を読んだ
  • Ansible 初学者を中心に「Ansible で実現できることを知る」ことに適した1冊だった
  • ブログに載せた機能以外にも Ansible の機能で知らなかった部分を整理することができた

Ansible実践ガイド 第3版 (impress top gear)

Ansible実践ガイド 第3版 (impress top gear)