给 Strapi 应用添加健康检查端点
今天继续上周的主题,给 k8s 集群里的服务添加健康检查探针。上一篇《给 SpringBoot 服务添加健康检查》是针对 Java 服务的。除了 java 服务,公司还有一个 CMS 系统部署在 k8s 集群中,基于 Strapi 开发,是一个 nodejs 项目。粗看了一下 Strapi,感觉它的理念和 Koa 很像,仔细看它果然依赖 koa,并且文档中也提到了 Koa。这让我比较激动,毕竟,我也是 Koa 项目的 Contributor(尽管只有一丢丢贡献):
先追求有,再追求好
如同上一篇给 SpringBoot 服务添加健康检查路由一样,本篇介绍只追求有,即添加最简单的实现。要完美的话,还需要写一些额外的代码,以便在一些外部依赖(比如数据库)发生问题时得到通知,并让健康检查端点返回错误。
测试
如同上一篇一样,仍然是在写实现代码前,先把测试写好,即把最终效果写下来,这是一种以终为始的思维方式,有助于避免掉进软件开发的焦油坑:《我是如何从焦油坑里爬出来的》。
添加测试工具
由于项目里没有引入测试工具,先补上:
yarn add --dev jest supertest sqlite3
jest 是 Facebook(现在叫 Meta?)开发的测试框架。supertest 用来测试 Web 服务,而 sqlite3 可以简化测试时的数据库依赖。
测试配置
增加 config/env/test/database.json 指定测试时使用 sqlite:
{
"defaultConnection": "default",
"connections": {
"default": {
"connector": "bookshelf",
"settings": {
"client": "sqlite",
"filename": ".tmp/test.db"
},
"options": {
"useNullAsDefault": true,
"pool": {
"min": 0,
"max": 1
}
}
}
}
}
测试命令
在 package.json 里的 scripts 字段中加入测试相关命令:
+ "test": "jest --forceExit --detectOpenHandles"
在 package.json 的最后添加:
"jest": {
"testPathIgnorePatterns": [
"/node_modules/",
".tmp",
".cache"
],
"testEnvironment": "node"
}
健康检查测试用例
tests/healthz/index.test.js
const Strapi = require('strapi');
const http = require('http');
const request = require('supertest')
let instance;
async function setupStrapi() {
if (!instance) {
/** the following code in copied from `./node_modules/strapi/lib/Strapi.js` */
await Strapi().load();
instance = strapi; // strapi is global now
await instance.app
.use(instance.router.routes()) // populate KOA routes
.use(instance.router.allowedMethods()); // populate KOA methods
instance.server = http.createServer(instance.app.callback());
}
return instance;
}
jest.setTimeout(20000)
describe('Health Check', () => {
beforeAll(async () => {
await setupStrapi()
})
it('should live', async () => {
await request(
strapi.server)
.get('/healthz/liveness')
.expect(200)
.then(data => {
expect(data.text).toBe('I\'m alive!')
})
})
it('should ready', async()=>{
await request(
strapi.server)
.get('/healthz/readiness')
.expect(200)
.then(data => {
expect(data.text).toBe('I\'m ready!')
})
})
})
实现路由
首先增加 api/healthz 目录
添加路由配置
api/healthz/config/routes.json
{
"routes": [
{
"method": "GET",
"path": "/healthz",
"handler": "Healthz.index"
},
{
"method": "GET",
"path": "/healthz/liveness",
"handler": "Healthz.liveness"
},
{
"method": "GET",
"path": "/healthz/readiness",
"handler": "Healthz.readiness"
}
]
}
注意,一定不要使用官方的默认示例,不能含有 policies 数组:
{
"routes": [
{
"method": "GET",
"path": "/healthz",
"handler": "Healthz.index",
"config": {
"policies": []
}
}
]
}
如果这样,运行测试就会得到 403 的错误,原因是它会触发 user permissions 插件的权限检查。尽管你可以通过管理后台配置其权限公开访问:
但是对于健康检查接口,没有必要专门配置权限,直接绕开权限插件即可:
实现路由逻辑
api/healthz/controllers/Healthz.js
module.exports = {
// GET /healthz
async index(ctx) {
ctx.send('Hello World!')
},
async readiness(ctx) {
ctx.send('I\'m ready!')
},
async liveness(ctx) {
ctx.send('I\'m alive!')
},
}
运行测试,通过。
添加 deployment 配置
readinessProbe:
httpGet:
path: /healthz/readiness
port: 1337
initialDelaySeconds: 30
timeoutSeconds: 10
livenessProbe:
httpGet:
path: /healthz/liveness
port: 1337
initialDelaySeconds: 130
timeoutSeconds: 10
部署后可以验证:
在 k8s 集群里查看是否生效:
kubectl describe pod/your-pod
...
Containers:
cms:
Container ID: docker://7245d2d8644d6bcc7c7ff39fdea5e680457c4edf2ff70610a8607c3cef5d3332
Image: 13659932xxxx.dkr.ecr.cn-northwest-1.amazonaws.com.cn/cms:cccc1aec
Image ID: docker-pullable://13659932xxx.dkr.ecr.cn-northwest-1.amazonaws.com.cn/cms@sha256:bc4317cc2347eb2aed74b8e4e9f39b901b613e4bbc7781e09957e2eb4a0bd0db
Port: 1337/TCP
Host Port: 0/TCP
State: Running
Started: Mon, 15 Nov 2021 10:23:42 +0000
Ready: True
Restart Count: 0
Limits:
cpu: 1
memory: 2000Mi
Requests:
cpu: 500m
memory: 1000Mi
Liveness: http-get http://:1337/healthz/liveness delay=130s timeout=10s period=10s #success=1 #failure=3
Readiness: http-get http://:1337/healthz/readiness delay=30s timeout=10s period=10s #success=1 #failure=3
Environment Variables from:
...
注意以上输出的 Liveness 和 Readiness 部分,小功告成!