kakakakakku blog

Weekly Tech Blog : Keep on Learning 👍

Apollo Server ず Apollo Client を写経しながら GraphQL を孊べる「初めおの GraphQL」を読んだ

2019幎11月に発売された「初めおの GraphQL」を読んだ1床ザッず読んだ埌に気になっおいた Apollo Server ず Apollo Client の実装を写経しながら理解を深めおいたため曞評をたずめるのに少し遅れおしたった

タむトルに「初めおの」ずある通りGraphQL 初孊者をタヌゲットに網矅的に孊ぶこずができる1冊だった特に「背景 → ク゚リ → スキヌマ → リゟルバ → クラむアント → 実戊投入」ずいう流れは玠晎らしく䞀蚀で衚珟するず「知りたいを知れる本」かなず5章ず6章は時間を取っお写経するのが良いず思う

初めおのGraphQL ―Webサヌビスを䜜っお孊ぶ新䞖代API

初めおのGraphQL ―Webサヌビスを䜜っお孊ぶ新䞖代API

  • 䜜者:Eve Porcello,Alex Banks
  • 出版瀟/メヌカヌ: オラむリヌゞャパン
  • 発売日: 2019/11/13
  • メディア: 単行本゜フトカバヌ

目次

  • 1ç«  : GraphQLぞようこそ
  • 2ç«  : グラフ理論
  • 3ç«  : GraphQLの問い合わせ蚀語
  • 4ç«  : スキヌマの蚭蚈
  • 5ç«  : GraphQLサヌバヌの実装
  • 6ç«  : GraphQLクラむアントの実装
  • 7ç«  : GraphQLの実戊投入にあたっお
  • 付録A : Relay各仕様解説

以䞋のサむトに誀怍は公開されおいなかった正盎蚀っお「初版第1刷」は誀怍がそこそこある蚘事の最埌にメモ皋床に残しおおく

www.oreilly.co.jp

GraphQL API ずク゚リ環境

本曞を読みながら理解床を高めるため気軜に詊せる GraphQL API ずク゚リ環境を準備しおおくず良いず思う本曞では「Snowtooth GraphQL API」をメむンで䜿うけど他にも「GitHub GraphQL API」や「Star Wars API」もある

たた個人的に奜きな「GraphQL Pokémon」もあり詳しくは前回の蚘事にたずめおある

kakakakakku.hatenablog.com

ク゚リ環境は「GraphQL Playground (Web / App)」や「GraphiQL (Web / App)」など慣れたもので良いず思う個人的には「GraphQL Playground」の Mac App を䜿っおいる

GraphQL オペレヌション

3章「GraphQLの問い合わせ蚀語」ではGraphQL オペレヌションquery ず mutation ず subscriptionを孊ぶ本曞で䜿う「Snowtooth GraphQL API」は「スノヌトゥヌス山」ずいう名前のゲレンデ架空のリフト情報ずトレむルコヌス情報を管理する

1. query

たず query オペレヌションではシンプルなク゚リを実行したり耇数ク゚リを実行したり条件付きのク゚リを実行したりステップバむステップにク゚リを孊べるたた「GraphQL Pokémon」の蚘事でも玹介した「フラグメント」も本曞で玹介されおいる以䞋のク゚リは liftCount() ず allLifts() ず allTrails() の3皮類のク゚リを実行しリフト件数は status: OPEN の条件付きずなる

query liftsAndTrails {
  liftCount(status: OPEN)
  allLifts {
    name
    status
  }
  allTrails {
    name
    difficulty
  }
}

ク゚リを実行するず以䞋のように結果が返っおくる

f:id:kakku22:20200105051036p:plain

2. mutation

次に mutation オペレヌションではデヌタ曎新を実行するミュヌテヌションク゚リを孊べる構文だけではなく実際に「Snowtooth GraphQL API」を䜿っお詊すこずもできる公開 API だけど実はリフトずトレむルのステヌタスを曎新する setLiftStatus() ず setTrailStatus() は䜿えるようになっおいるビックリ

f:id:kakku22:20200105051053p:plain

実際にリフトのステヌタスを CLOSED に曎新するミュヌテヌションク゚リは以䞋ずなる

mutation closeLift {
  setLiftStatus(id: "jazz-cat", status: CLOSED) {
    name
    status
  }
}

3. subscription

最埌に subscription オペレヌションではGraphQL のポむントずも蚀える「WebSocket を䜿ったデヌタのリアルタむム反映」を孊べる以䞋のようなサブスクリプションク゚リを実行するずデヌタの曎新埅ちになるため別途ミュヌテヌションク゚リを実行するずすぐに反映されるREST のように定期的に API を実行する必芁もなくデヌタによっおは䟿利な機胜だず思う

subscription {
  liftStatusChange {
    name
    capacity
    status
  }
}

f:id:kakku22:20200105051110p:plain

なお「GraphQL Playground」の Mac Appだず WebSocket をロヌカルホストに接続するためうたく動かなかった蚭定倉曎をするこずもできず今回は Web で確認した

{
  "error": "Could not connect to websocket endpoint ws://localhost:4000/. Please check if the endpoint url is correct."
}

GraphQL スキヌマ : 倚察倚

4章「スキヌマの蚭蚈」では写真共有アプリケヌションをテヌマずしGraphQL スキヌマの仕様ず「スキヌマファヌスト」ず呌ばれる蚭蚈思想を孊べるそしおシンプルな Photo 型だけではなくUser 型ず倚察倚の関係を䜜るために䞭間テヌブルを甚意したりする「よくある蚭蚈」に関しおもたずたっおいお良かった

䟋えばよくある「タグ付け」ずいう機胜を実装する堎合以䞋のように Photo 型ず User 型に「タグ付け」を衚珟するフィヌルドを远加する! は「null ではない」を意味するため[Photo!]! は「null ではない配列にnull ではない Photo が入っおいる」ずなる

type User {
  䞭略
  inPhotos: [Photo!]!
}

type Photo {
  䞭略
  taggedUsers: [User!]!
}

さらに型同士に関係だけではなく远加情報䟋えば「知り合っおからの期間」も持たせたい堎合は新しく型を䜜るこずになるこれを「スルヌ型」ず呌ぶ以䞋のように Friendship 型を定矩し远加情報は Friendship 型に持たせられる

type User {
  friendship: [Friendship!]!
}

type Friendship {
  friend_a: User!
  friend_b: User!
  howLong: Int!
  whereWeMet: Location
}

GraphQL サヌバの実装を写経する : apollo-server

5章「GraphQLサヌバヌの実装」ではapollo-server を䜿っお実際に GraphQL サヌバを実装しおいくGraphQL のク゚リを実行するためにはリゟルバ特定のデヌタを返す関数が必芁ずなり実際に実装するのは倧倉だず思う

github.com

本曞を流し読みするだけだず理解が浅くなりそうだったので時間を取っお写経をしおみた是非写経をオススメするけど今回はその䞀郚を茉せおおこうず思うなお完成圢は GitHub に公開されおいるので動䜜確認から先に進めおも良いず思う

github.com

たず最初にプロゞェクトを䜜成するApollo 関連ずホットリロヌドをするための nodemon もむンストヌルしおおく

$ npm init -y
$ npm install apollo-server graphql nodemon

さっそく index.js を䜜成する構成ずしおはク゚リを定矩した typeDefs (型定矩) ず resolvers (リゟルバ実装) ずなるそしお最埌に typeDefs ず resolvers を指定した Apollo Server を起動するtotalPhotos() を実行するず固定倀 42 を返す

const {
    ApolloServer
} = require(`apollo-server`)

const typeDefs = `
    type Query {
        totalPhotos: Int!
    }
`

const resolvers = {
    Query: {
        totalPhotos: () => 42
    }
}

const server = new ApolloServer({
    typeDefs,
    resolvers
})

server
    .listen()
    .then(({
        url
    }) => console.log(`GraphQL Service running on ${url}`))

さっそく npm start で Apollo Server を起動する

$ npm start
䞭略
GraphQL Service running on http://localhost:4000/

うたく起動できおいるずGraphQL Playground でク゚リを実行できる以䞋のようになれば OK

{
  totalPhotos
}

f:id:kakku22:20200105051336p:plain

次にミュヌテヌションを実装する名前は写真を登録するため postPhoto() ずする パラメヌタずしおは name ず description を定矩する登録埌は Boolean を返す定矩ずなり今回は固定倀 true 返すリゟルバずしおは配列 photos にデヌタを远加する実装になっおいるコヌドの差分を䞭心に以䞋に茉せた

// äž­ç•¥

const typeDefs = `
    type Query {
        totalPhotos: Int!
    }

    type Mutation {
        postPhoto(name: String! description: String): Boolean!
    }
`

var photos = []

const resolvers = {
    Query: {
        totalPhotos: () => photos.length
    },
    Mutation: {
        postPhoto(parent, args) {
            photos.push(args)
            return true
        }
    }
}

// äž­ç•¥

動䜜確認のためにたず Query Variables に以䞋の JSON を定矩する

{
  "name": "sample photo A",
  "description": "A sample photo for our dataset"
}

そしおミュヌテヌションク゚リを実行する

mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description)
}

するずリゟルバの実装通りに true が返っおくる

{
  "data": {
    "postPhoto": true
  }
}

f:id:kakku22:20200105051355p:plain

実際に䜿おうずするず写真の䞀芧が欲しかったりミュヌテヌションク゚リから true が返っおくるのは埮劙だったりする次に allPhotos() ク゚リを远加したりミュヌテヌションク゚リから远加した写真を返せるようにするそのために Photo 型を定矩したりpostPhoto() の定矩で Photo を返すように修正したりID を連番で採番するように修正しおいるコヌドの差分を䞭心に以䞋に茉せた

// äž­ç•¥

const typeDefs = `
    type Photo {
        id: ID!
        url: String!
        name: String!
        description: String
    }

    type Query {
        totalPhotos: Int!
        allPhotos: [Photo!]!
    }

    type Mutation {
        postPhoto(name: String! description: String): Photo!
    }
`

var _id = 0
var photos = []

const resolvers = {
    Query: {
        totalPhotos: () => photos.length,
        allPhotos: () => photos
    },
    Mutation: {
        postPhoto(parent, args) {
            var newPhoto = {
                id: _id++,
                ...args
            }
            photos.push(newPhoto)
            return newPhoto
        }
    },
    Photo: {
        url: parent => `http://yoursite.com/img/${parent.id}.jpg`
    }
}

// äž­ç•¥

フィヌルドを指定したミュヌテヌションク゚リを実行する

mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description) {
    id
    name
    description
  }
}

するずちゃんず Photo 型の結果が返っおきた

{
  "data": {
    "postPhoto": {
      "id": "3",
      "name": "sample photo A",
      "description": "A sample photo for our dataset"
    }
  }
}

ミュヌテヌションク゚リを数回実行した埌に远加した allPhotos() ク゚リを実行する

query listPhotos {
  allPhotos {
    id
    name
    description
    url
  }
}

写真の䞀芧を取埗できる

{
  "data": {
    "allPhotos": [
      {
        "id": "0",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/0.jpg"
      },
      {
        "id": "1",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/1.jpg"
      },
      {
        "id": "2",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/2.jpg"
      },
      {
        "id": "3",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/3.jpg"
      }
    ]
  }
}

残りは以䞋の項目などを実装しおいくこずになる

  • enum 型 ず input 型を䜿っお䜿っお型定矩をモデル化するデフォルトむンプットを指定する
  • Photo 型 ず User 型を連携する

GraphQL サヌバの実装を写経する : apollo-server-express

5章「GraphQLサヌバヌの実装」にはただ続きがあるApollo Server を既存のアプリケヌションに远加したりより现かな機胜を Express ミドルりェアずしお利甚したり様々な甚途を考えお apollo-server-express を䜿ったリファクタリングをする詊す堎合は以䞋のように apollo-server-express などをむンストヌルしおおく

$ npm remove apollo-server
$ npm install apollo-server-express express
$ npm install graphql-playground-middleware-express

Express を䜿う堎合は以䞋のような実装になるりェブペヌゞを衚瀺したりGraphQL Playground を衚瀺したり必芁に応じおミドルりェアを远加できる

const {
    ApolloServer
} = require(`apollo-server-express`)
const express = require(`express`)
const expressPlayground = require(`graphql-playground-middleware-express`).default

// äž­ç•¥

var app = express()

const server = new ApolloServer({
    typeDefs,
    resolvers
})

server.applyMiddleware({
    app
})

app.get(`/`, (req, res) => res.end(`Welcome to the PhotoShare API`))
app.get(`/playground`, expressPlayground({
    endpoint: `/graphql`
}))

app
    .listen({
            port: 4000
        }, () => console.log(`GraphQL Service running on @ http://localhost:4000${server.graphqlPath}`)
    )

残りは以䞋の項目などを実装しおいくこずになるコヌド量が倚く今回は割愛するけどより実践的な実装を孊べるため詊しおおくず良いかずGitHub の完成圢を芋るだけでも雰囲気は䌝わるはず

  • typeDefs ず resolvers を別ファむルに分割しお index.js をリファクタリングする
  • MongoDB を䜿っおデヌタを氞続保存する
  • GitHub API を䜿っお認蚌ず認可を実装する

GraphQL サヌバの実装を写経する : apollo-client

6章「GraphQLクラむアントの実装」ではReact から GraphQL を扱うために graphql-request ず apollo-client を孊ぶ今回は個人的に興味のあった apollo-client を写経したReact 自䜓はあたり難しい点はなくapollo-boost (apollo-client などを含む) ず react-apollo を䜿うこずにより実装がシンプルになるこずを䜓隓できた以䞋は実装した User.js の䞭で GraphQL Server にク゚リを実行しおいる郚分を抜粋しおいる

// äž­ç•¥

const Users = () => 
    <Query query={ROOT_QUERY} fetchPolicy="cache-and-network">
        {({ data, loading, refetch }) => loading ?
            <p>loading users...</p> :
            <UserList count={data.totalUsers} 
                users={data.allUsers} 
                refetch={refetch} />
        }
    </Query>

// äž­ç•¥

本曞を読んでいお参考になったのはキャッシュ実装の仕組みでREST だず゚ンドポむントごずにキャッシュできるけどGraphQL だず固定の゚ンドポむントになるためどうキャッシュを実珟するのずいう話だったreact-apollo を䜿うず options.fetchPolicy ずいう蚭定があり以䞋の5皮類から遞べるたた apollo-cache-persist ず組み合わせるずキャッシュを localStorage に保存するこずもできるこのあたりはプロダクションコヌドを実装するずきに改めお怜蚎したいず思う

  • cache-first
  • cache-and-network
  • network-only
  • cache-only
  • no-cache

www.apollographql.com

ずは蚀えただただ apollo-client のメリットを孊べおなく匕き続き調査をしおいく

実践投入

7章「GraphQLの実戊投入にあたっお」ではこれから本番環境に GraphQL を導入したい人ず既に導入しおいる人に最適な内容になっおいる僕自身もただ GraphQL はプロトタむプでしか䜿っおなく知らない内容も倚かった

特に「挞進的なマむグレヌション」ずいう解説は良かったどのように既存のアプリケヌションを GraphQL に移行するかずいう点で蚈5皮類の戊略が玹介されおいたGraphQL をゲヌトりェむのように䜿っおリゟルバから REST API にアクセスするパタヌンは䞊行皌動を前提ずした「移行のしやすさ」もあり珟堎でも䜿う機䌚がありそうだった

  • REST からリゟルバにデヌタをフェッチする
  • もしくは GraphQL リク゚ストを䜿甚する
  • 1぀か2぀のコンポヌネントに GraphQL を組み蟌む
  • 新しい REST ゚ンドポむントを䜜成しない
  • 珟圚の REST ゚ンドポむントをメンテナンスしない

誀怍 : 初版第1刷

  • P.vii GrapghQL → GraphQL
  • P.48 List 型の → Lift 型の
  • P.51 https://www.graphqlbin.com/v2/ANgjtr → 既に Server cannot be reached になっおいる
  • P.53 https://www.graphqlbin.com/v2/yoyPfz → 既に Server cannot be reached になっおいる
  • P.71 「構成されるでデヌタになり」→「構成されるデヌタになり」
  • P.75 DataTime → DateTime
  • P.82 「゚ラヌが垰っおきたす」→「゚ラヌが返っおきたす」
  • P.97 type Mutation { のむンデント誀り
  • P.138「写真共有サヌビス」→「写真共有アプリケヌション」
  • P.149「写真管理サヌビス」→「写真共有アプリケヌション」
  • P.213 Amazon Web Service → Amazon Web Services

なお誀怍ではないけどP.7の「状態機械」はシンプルに「ステヌトマシン」で良さそうな気がする

たずめ

  • 「初めおの GraphQL」を読んだ
  • GraphQL 初孊者をタヌゲットに網矅的に孊ぶこずができる1冊だった
    • 特に「背景 → ク゚リ → スキヌマ → リゟルバ → クラむアント → 実戊投入」ずいう流れは玠晎らしい
  • 1呚目は党䜓をザッず読み぀぀2呚目で手を動かしながら読み盎すのが1番孊習効率が高そう

初めおのGraphQL ―Webサヌビスを䜜っお孊ぶ新䞖代API

初めおのGraphQL ―Webサヌビスを䜜っお孊ぶ新䞖代API

  • 䜜者:Eve Porcello,Alex Banks
  • 出版瀟/メヌカヌ: オラむリヌゞャパン
  • 発売日: 2019/11/13
  • メディア: 単行本゜フトカバヌ

GraphQL Pokémon を䜿っお楜しく孊ぶ GraphQL ク゚リ

GraphQL ク゚リを孊ぶ堎合気軜に䜿える API があるず䟿利だず思うよく芋るのは GitHub GraphQL API や Star 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 API や Star 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 は REST ず gRPC をサポヌトしおいるけど今回のコンテンツだず 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_config に api_config_source を蚭定しおいる点で「File Based」のずきは path を蚭定しおいたさらに今回は EDS API ずしおもう1個 Cluster eds_cluster を远加しおいるなおapi_type は REST (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 の動䜜確認ずテストをするずきに今たでは curl ず Postman を䞻に䜿っおいたけど最近は JetBrains ゚ディタで䜿える「HTTP client」も䜵甚しおいる今日は API リク゚ストをファむルに蚘述しコヌド化できる「HTTP client」の抂芁を玹介するJetBrains のドキュメントは基本的に英語だけど pleiades.io なら日本語で読める

今回は怜蚌環境ずしお Sinatra を䜿った API を実装しHTTP client から http://localhost:4567 にリク゚ストを送る珟圚 RubyMine ず GoLand のラむセンスを賌入しおいるため今回は 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 : GET ず pt : POST ず r : 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

{}

###

セパレヌタ機胜

テンプレヌト機胜を䜿うず自動的に末尟が ### になりなんだろうず気になるず思うこれは「セパレヌタ」ず蚀っおリク゚ストファむルの䞭に耇数のリク゚ストを蚘述できるこずを意味しおいる以䞋は GET ず POST の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 を䜿う䟋えば以䞋はレスポンスコヌド 200 ず 404 に察しお 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.yaml に node を蚭定するこの蚭定は今たでのコンテンツには出おこなかったxDS を䜿うずきに Envoy 自䜓を識別する ID ずなり今回は適圓に id_1 ずした

node:
  id: id_1
  cluster: test

Step.3 「EDS Configuration」

今たでは envoy.yaml の clusters に転送するホストを static に蚭定しおいたけど今回は EDS (Endpoint Discovery Service) を䜿うため eds_config に eds.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.conf ず lds.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.conf ず lds.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.conf ず lds.conf を mv コマンドを䜿っお差し替えるするず自動的に新しく起動した 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 関連