點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Docker 建立 GraphQL API

Sam Xiao's Avatar 2021-10-31

GraphQL 本質只能算是 Middleware,因此也能如 Node + Express 般包進 Docker,本文將介紹使用 Babel 後 Apollo GraphQL 如何打包成 Docker Image。

Version

Node 10.16.3
Apollo GraphQL 2.9.6
Babel 7.6.4

GraphQL Project

gql000

在根目錄下新增以下檔案:

  • dockerfile
  • src/index.js
  • .babelrc
  • nodemon.json
  • package.json
  • .env
  • docker-compose.yml

Node

src/index.js

import { ApolloServer, gql } from 'apollo-server'

let data = [
  { title: 'FP in JavaScript', category: 'FP'},
  { title: 'RxJS in Action', category: 'FRP'},
  { title: 'Speaking JavaScript', category: 'JS'}
]

let typeDefs = gql`
  type Query {
    books(category: BookCategory!): [Book]
  }

  type Book {
    title: String
    category: BookCategory
  }

  enum BookCategory {
    FP
    FRP
    JS
  }
`

let books = (_, { category }) => data.filter(x => x.category === category)

let resolvers = {
  Query: {
    books
  }
}

let apolloServer = new ApolloServer({ typeDefs, resolvers });

apolloServer
  .listen()
  .then(({ url }) => `GraphQL Server ready at ${ url }`)
  .then(console.log)

在 project 根目錄下新增 src 目錄放 GraphQL,有別於 Babel 編譯後在 dist 目錄所產生的 js

index.js 為 GraphQL 主程式。

第 1 行

import { ApolloServer, gql } from 'apollo-server'

apollo-server import 進 ApolloServergql

第 3 行

let data = [
  { title: 'FP in JavaScript', category: 'FP'},
  { title: 'RxJS in Action', category: 'FRP'},
  { title: 'Speaking JavaScript', category: 'JS'}
]

原始資料為 array of object,包含 titlecategory 兩個 property。

第 9 行

let typeDefs = gql`
  type Query {
    books(category: BookCategory!): [Book]
  }

  type Book {
    title: String
    category: BookCategory
  }

  enum BookCategory {
    FP
    FRP
    JS
  }
`;

typeDefs 內宣告 books query、Book type 與 BookCategory enum。

28 行

let resolvers = {
  Query: {
    books
  }
}

resolvers 內宣告 books query。

26 行

let books = (_, { category }) => data.filter(x => x.category === category)

定義 books query 實現方式。

34 行

let apolloServer = new ApolloServer({ typeDefs, resolvers })

建立 ApolloServer,將 typeDefsresolvers 組合成 Object 傳進其 constructor。

36 行

apolloServer
  .listen()
  .then(({ url }) => `GraphQL Server ready at ${ url }`)
  .then(console.log)

apolloServer.listen() 啟動 Apollo GraphQL,其回傳為 Promise,將其 url destructure 之後以 console.log 顯示。

Apollo Server 預設啟動在 4000 port

Babel Configuration

.babelrc

{
  "presets": [
    "@babel/preset-env"
  ]
}

在 project 根目錄下建立 .babelrc 設定 Babel,@babel/preset-env 為預設 Babel 組態。

Nodemon Configuration

nodemon.json

{
  "exec": "cross-env NODE_ENV=development yarn dev",
  "watch": ["src/*"],
  "ext": "js, json"
}
  • exec:當有變動時,將執行 yarn dev
  • watch:Nodemon 將持續觀察的目錄
  • ext:Nodemon 將持續觀察的 extension

watchext 可視實際需求加以修改

NPM Configuration

package.json

{
  "name": "apollo-gql",
  "version": "0.1.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "serve": "nodemon",
    "start": "node ./dist/index.js",
    "dev": "babel-node ./src/index.js",
    "prod": "yarn clean && cross-env NODE_ENV=production yarn build && yarn start",
    "clean": "rimraf dist",
    "build": "babel ./src --out-dir dist",
    "docker:build": "yarn clean && cross-env NODE_ENV=production yarn build && docker build -t $npm_package_name:$npm_package_version .",
    "docker:up": "docker-compose up -d",
    "docker:down": "docker-compose down"
  },
  "dependencies": {
    "apollo-server": "^2.9.7",
    "graphql": "^14.5.8"
  },
  "devDependencies": {
    "@babel/cli": "^7.6.4",
    "@babel/core": "^7.6.4",
    "@babel/node": "^7.6.3",
    "@babel/preset-env": "^7.6.3",
    "nodemon": "^1.19.4",
    "rimraf": "^3.0.0"
  }
}

17 行

"dependencies": {
  "apollo-server": "^2.9.7",
  "graphql": "^14.5.8"
},
"devDependencies": {
  "@babel/cli": "^7.6.4",
  "@babel/core": "^7.6.4",
  "@babel/node": "^7.6.3",
  "@babel/preset-env": "^7.6.3",
  "nodemon": "^1.19.4",
  "rimraf": "^3.0.0"
}

GraphQL 專案會用到的 package,包含 Apollo GraphQL、與 Babel。

第 7 行

"serve": "nodemon",

yarn serve 啟動 Nodemon,由 Nodemon 執行 yarn dev

yarn devyarn serve 差異在 js 變動後是否重啟 Apollo GraphQL,實務上開發時建議使用 yarn serveyarn dev 留給 Nodemon 使用

第 8 行

"start": "node ./dist/index.js",

yarn start 使用 node 執行 Babel 轉譯後的 js 並啟動 Apollo Server。

第 9 行

"dev": "babel-node ./src/index.js",

yarn dev 使用 babel-node 將 ES6+ 轉譯成 Node 能執行的 js 在記憶體內,並啟動 Apollo Server。

10 行

"prod": "yarn clean && cross-env NODE_ENV=production yarn build && yarn start",

yarn prod 設定 NODE_ENVproduction,且依序先執行:

  • yarn clean 刪除 dist 目錄
  • yarn build 使用 Babel 編譯成 Node 能執行的 js 在 dist 目錄下
  • yarn start 使用 Node 執行 Babel 編譯後的 js 並啟動 Apollo Server

若想在本機測試 Babel 編譯後結果是否正常,則要執行 yarn prod 而非 yarn serve

11 行

"clean": "rimraf dist",

yarn clean 刪除 dist 目錄。

12 行

"build": "babel ./src --out-dir dist",

yarn build 使用 Babel 將 src 目錄下的 ES6+ 轉譯成 Node 能執行的 js 到 dist 目錄下。

將來要包進 Docker 的 js 也是 dist 目錄,而非 src 目錄

13 行

"docker:build": "yarn clean && cross-env NODE_ENV=production yarn build && docker build -t $npm_package_name:$npm_package_version .",

yarn docker:buildyarn prod 類似,差異只在最後 docker:build 包成 image 而非啟動 Apollo GraphQL。

$npm_package_version 回隨著 package.jsonversion 而變,因此改版只要改 version 即可

14 行

"docker:up": "docker-compose up -d",

使用 docker-compose 啟動 Apollo GraphQL。

稍後即將建立 docker-compose.yml

15 行

"docker:down": "docker-compose down"

使用 docker-compose 結束 Apollo GraphQL。

Dockerfile

FROM node:lts-alpine
WORKDIR /usr/app
COPY package.json .
RUN yarn install --production
COPY dist/* ./
CMD [ "node", "index.js" ]

在 project 目錄下建立 dockerfile,由於我們要另外安裝 Apollo GraphQL,且將自己寫的 js 包進 Docker image,因此不可直接使用 docker-compose.yml

第 1 行

FROM node:lts-alpine

使用最新 LTS 版的 node:lts-alpine 為基底建立 image。

建議使用 alpine 為 production image,size 會小很多

第 2 行

WORKDIR /usr/app

將 working directory 切換到 /usr/app,相當於:

mkdir /usr/app
cd /usr/app

稍後 COPYRUNCMD 都會在此目錄下。

第 3 行

COPY package.json .

將根目錄的 package.json 複製到 Docker 內的 /usr/app,因為要使用 yarn install 安裝 Apollo GraphQL。

第 4 行

RUN yarn install --production

執行 yarn install 安裝 dependencies 下的 package。

第 5 行

COPY dist/* ./

將 Babel 編譯過的 js 複製進 Docker 內的 /usr/app

第 6 行

CMD [ "node", "index.js" ]

執行 Docker 內的 /usr/app/index.js 啟動 Apollo GraphQL。

Docker Compose

docker-compose.yml

version: "3"
services:
  graphql:
    image: apollo-docker:${GQL_TAG}
    container_name: MyGraphQL
    restart: always
    ports:
      - ${GQL_PORT}:4000

dockerfile 只能建立 Docker image,要啟動 Apollo GraphQL 則要靠 docker-compose.yml

第 4 行

image: apollo-docker:${GQL_TAG}

image 定義 graphql service 要使用的 Docker image 與 tag 版本,其中 GQL_TAG 稍後可由 .env 設定。

第 5 行

container_name: MyGraphQL

container_name 定義 container 名稱。

第 6 行

restart: always

當 container crash 時,會自動重啟。

第 7 行

ports:
  - ${GQL_PORT}:4000

MyGraphQL container 內部使用 4000 port,可透過 GQL_PORT 定義在 host os 所使用的 port 避免與其他 service 衝突。

.env

GQL_TAG=0.1.0
GQL_PORT=4000

定義 GQL_TAGGQL_PORT

對於經常變動部分,應該放在 .env,避免 user 直接修改 docker-compose.yml

Build Image

$ yarn docker:build

使用 yarn docker:build 產生 Docker image。

gql001

Start Container

$ yarn docker:up

使用 yarn docker:up 啟動 Apollo GraphQL。

gql002

GraphQL Playground

query {
  books(category: FP) {
    title
    category
  }
}

使用 books query 查詢資料,category argument 傳入 FP,並回傳 titlecategory 兩個 property。

gql003

Stop Container

$ yarn docker:down

使用 yarn docker:down 結束 Apollo GraphQL。

gql004

Conclusion

  • 僅管使用 Babel 寫 GraphQL,只要將 Babel 編譯過的 js 包進 Docker 即可

Reference

Kian Wallace, Building Microservices and a GraphQL API with Docker