
Apollo 配置中心非 Java 客户端实战Python / Go / Node.js 本地服务如何远程读取配置标签Apollo、配置中心、Docker、cpolar、内网穿透很多团队第一次接触 Apollo都会把它和 Java 微服务绑定在一起。原因很直接Apollo 官方最成熟的是 Java 客户端文档和示例也常围绕 Spring Boot 展开。但配置中心本身解决的是“配置集中管理、发布、灰度、回滚、审计”的问题这件事并不只属于 Java。Python 的数据脚本、Go 写的网关服务、Node.js 写的 BFF 或管理后台同样会遇到这些问题测试环境和正式环境配置散落在.env里开关项改一次要重新发版远程同事想验收配置变化时只能截图、发日志、远程桌面来回折腾。本文做一套本地可跑通的路线先用 Apollo Quick Start 在本机启动 Portal、Config Service 和 Admin Service再用 Apollo 官方非 Java 客户端 HTTP 接口让 Python、Go、Node.js 三种本地程序读取同一个SampleApp配置。最后用 cpolar 临时开放示例服务给异地同事做配置变更验收。边界先说在前面本文是本地学习、联调和团队验收链路不是生产部署方案。生产环境要按 Apollo 分布式部署指南配置数据库、服务注册、网络隔离和访问控制Config Service、Admin Service 不要直接长期裸露在公网。1 这套链路里每个端口负责什么先把角色拆清楚后面排错会省很多时间。组件本地端口作用Apollo Portal8070Web 管理页面创建应用、改配置、发布配置Apollo Config Service / Meta Server8080客户端读取配置、发现配置服务Apollo Admin Service8090Portal 后台管理配置发布等动作Python / Go / Node.js 示例程序本地命令行或 5000从 Config Service 读取配置展示给验收方非 Java 客户端最稳的接入点是 Config Service 的 HTTP API。Apollo 官方非 Java 客户端文档里也明确给出了 HTTP 读取方式GET {config_server_url}/configfiles/json/{appId}/{clusterName}/{namespaceName} GET {config_server_url}/configs/{appId}/{clusterName}/{namespaceName} GET {config_server_url}/notifications/v2本文先使用第一个接口http://127.0.0.1:8080/configfiles/json/SampleApp/default/application它会返回applicationnamespace 下已经发布的配置。对本地示例和低频读取来说这条接口足够清晰。需要更精细的刷新控制时再接入/configs和/notifications/v2。2 用 Docker 启动 Apollo Quick StartApollo Quick Start 官方仓库已经准备了 Docker Compose 文件适合本地快速体验。它会启动一个apollo-quick-start容器和一个 MySQL 8 容器并暴露 8070、8080、8090 三个端口。先克隆仓库git clone https://github.com/apolloconfig/apollo-quick-start.git cd apollo-quick-start普通 x86_64 机器执行docker compose up -dApple Silicon 或其他 ARM64 环境使用仓库里的 ARM64 compose 文件docker compose -f docker-compose-arm64.yml up -d查看容器状态docker compose ps看到apollo-quick-start和apollo-db都处于运行状态后再检查 Portal 页面curl -I http://127.0.0.1:8070浏览器打开http://127.0.0.1:8070Quick Start 默认启用了简单登录账号密码是用户名apollo 密码admin登录后可以看到内置的SampleApp。官方样例数据里SampleApp的defaultcluster、applicationnamespace 已经有一个timeout配置项。3 先用 curl 确认 Config Service 能返回配置客户端接入之前先不用写代码直接用 curl 验证 Config Service。执行curl http://127.0.0.1:8080/configfiles/json/SampleApp/default/application正常会看到类似结果{ timeout: 100 }如果你已经在 Portal 里改过timeout并发布返回值会变成你发布后的值。这里有几个关键词要记牢appId本文是SampleAppclusterName本文是defaultnamespaceName本文是applicationconfig_server_url本文是http://127.0.0.1:8080很多接入失败都不是语言 SDK 问题而是这四个值写错了。Portal 是 8070客户端读配置走 8080。把 8070 当成客户端地址会得到页面 HTML 或重定向不会得到配置 JSON。4 在 Portal 修改并发布一条配置为了让三种语言示例读到同一份配置先在 Portal 里做一次配置发布。进入SampleApp后在applicationnamespace 里保留或新建下面几项timeout2000 feature.login.enabledtrue service.nameorder-api保存后一定要点击“发布”。Apollo 的配置修改和配置发布是两个动作。只保存不发布客户端读到的仍然是上一版 release。发布后再用 curl 看一次curl http://127.0.0.1:8080/configfiles/json/SampleApp/default/application返回值里应该能看到刚才发布的键值{ timeout: 2000, feature.login.enabled: true, service.name: order-api }后面 Python、Go、Node.js 都读这同一个地址。这样做的好处是排错路径清楚curl 能通说明 Apollo 端和 URL 参数是对的某一种语言不通再查那种语言的 HTTP 请求和 JSON 解析。5 Python用标准库读取 Apollo 配置Python 示例不依赖第三方包直接用标准库urllib.request。新建目录mkdir -p ~/apollo-non-java-demo/python cd ~/apollo-non-java-demo/python写入apollo_read.pycat apollo_read.py PY import json import os import sys from urllib.error import HTTPError, URLError from urllib.parse import quote from urllib.request import Request, urlopen CONFIG_SERVER os.getenv(APOLLO_CONFIG_SERVER, http://127.0.0.1:8080).rstrip(/) APP_ID os.getenv(APOLLO_APP_ID, SampleApp) CLUSTER os.getenv(APOLLO_CLUSTER, default) NAMESPACE os.getenv(APOLLO_NAMESPACE, application) def apollo_config_url() - str: return ( f{CONFIG_SERVER}/configfiles/json/ f{quote(APP_ID, safe)}/ f{quote(CLUSTER, safe)}/ f{quote(NAMESPACE, safe)} ) def fetch_config() - dict: req Request(apollo_config_url(), headers{Accept: application/json}) with urlopen(req, timeout10) as resp: charset resp.headers.get_content_charset() or utf-8 body resp.read().decode(charset) return json.loads(body) if __name__ __main__: try: config fetch_config() except HTTPError as exc: print(fApollo HTTP error: {exc.code} {exc.reason}, filesys.stderr) sys.exit(1) except URLError as exc: print(fApollo connection error: {exc.reason}, filesys.stderr) sys.exit(1) print(json.dumps(config, ensure_asciiFalse, indent2)) print(timeout , config.get(timeout)) print(feature.login.enabled , config.get(feature.login.enabled)) print(service.name , config.get(service.name)) PY运行python3 apollo_read.py输出示例{ timeout: 2000, feature.login.enabled: true, service.name: order-api }如果要切换应用、集群或 namespace不改代码直接用环境变量APOLLO_APP_IDSampleApp \ APOLLO_CLUSTERdefault \ APOLLO_NAMESPACEapplication \ APOLLO_CONFIG_SERVERhttp://127.0.0.1:8080 \ python3 apollo_read.py这也是更稳妥的接入习惯配置中心地址和应用身份放在启动参数里业务代码只负责读取和使用配置。6 Go用 net/http 读取 Apollo 配置Go 示例也只使用标准库。新建目录mkdir -p ~/apollo-non-java-demo/go cd ~/apollo-non-java-demo/go go mod init apollo-go-demo写入main.gocat main.go GO package main import ( encoding/json fmt io net/http net/url os strings time ) func getenv(key, fallback string) string { if value : os.Getenv(key); value ! { return value } return fallback } func apolloConfigURL() string { base : strings.TrimRight(getenv(APOLLO_CONFIG_SERVER, http://127.0.0.1:8080), /) appID : url.PathEscape(getenv(APOLLO_APP_ID, SampleApp)) cluster : url.PathEscape(getenv(APOLLO_CLUSTER, default)) namespace : url.PathEscape(getenv(APOLLO_NAMESPACE, application)) return fmt.Sprintf(%s/configfiles/json/%s/%s/%s, base, appID, cluster, namespace) } func fetchConfig() (map[string]string, error) { client : http.Client{Timeout: 10 * time.Second} resp, err : client.Get(apolloConfigURL()) if err ! nil { return nil, err } defer resp.Body.Close() body, err : io.ReadAll(resp.Body) if err ! nil { return nil, err } if resp.StatusCode ! http.StatusOK { return nil, fmt.Errorf(apollo status%d body%s, resp.StatusCode, string(body)) } var config map[string]string if err : json.Unmarshal(body, config); err ! nil { return nil, err } return config, nil } func main() { config, err : fetchConfig() if err ! nil { fmt.Fprintln(os.Stderr, read apollo config failed:, err) os.Exit(1) } out, _ : json.MarshalIndent(config, , ) fmt.Println(string(out)) fmt.Println(timeout , config[timeout]) fmt.Println(feature.login.enabled , config[feature.login.enabled]) fmt.Println(service.name , config[service.name]) } GO运行go run .Go 版本的关键点和 Python 一样URL 由APOLLO_CONFIG_SERVER、APOLLO_APP_ID、APOLLO_CLUSTER、APOLLO_NAMESPACE组成。只要这四个值一致三种语言读到的就是同一份 Apollo 发布配置。如果程序返回 404优先回到 Portal 看SampleApp/default/application是否已经发布。Apollo 的“存在配置草稿”和“已有发布版本”不是同一件事。7 Node.js用 fetch 读取 Apollo 配置Node.js 18 及以上版本内置fetch不用额外安装请求库。新建目录mkdir -p ~/apollo-non-java-demo/node cd ~/apollo-non-java-demo/node写入apollo-read.mjscat apollo-read.mjs JS const configServer (process.env.APOLLO_CONFIG_SERVER ?? http://127.0.0.1:8080).replace(/\/$/, ); const appId process.env.APOLLO_APP_ID ?? SampleApp; const cluster process.env.APOLLO_CLUSTER ?? default; const namespaceName process.env.APOLLO_NAMESPACE ?? application; function apolloConfigUrl() { return [ configServer, configfiles, json, encodeURIComponent(appId), encodeURIComponent(cluster), encodeURIComponent(namespaceName), ].join(/); } async function fetchConfig() { const response await fetch(apolloConfigUrl(), { headers: { Accept: application/json }, signal: AbortSignal.timeout(10000), }); if (!response.ok) { const text await response.text(); throw new Error(Apollo status${response.status} body${text}); } return response.json(); } const config await fetchConfig(); console.log(JSON.stringify(config, null, 2)); console.log(timeout , config.timeout); console.log(feature.login.enabled , config[feature.login.enabled]); console.log(service.name , config[service.name]); JS确认 Node 版本node -v执行node apollo-read.mjs输出中应该能看到 Apollo 里发布的配置。Node 服务里常见的做法是启动时读取一次放入内存如果配置更新频率不高可以每隔 30 秒以上刷新一次。刷新频率不要太密配置中心不是用来代替业务数据库的。8 配置变更验证从 100 到 2000再读一次现在做一次完整验收。先读当前值curl http://127.0.0.1:8080/configfiles/json/SampleApp/default/application python3 ~/apollo-non-java-demo/python/apollo_read.py cd ~/apollo-non-java-demo/go go run . node ~/apollo-non-java-demo/node/apollo-read.mjs然后回到 Portal把timeout改成timeout3000保存并发布。发布完成后再执行三种语言程序。正常结果是三边都输出新的timeout3000。这里顺带解释两个 Apollo 接口的差异/configfiles/json/...适合简单读取和轮询服务端有缓存官方文档说明缓存会有很短延迟。/configs/...会返回releaseKey客户端带上上一次的releaseKey后配置未变化时服务端返回 304。/notifications/v2是 long polling 通知接口客户端可以用它感知 namespace 变化再去/configs/...拉取新配置。本篇示例先把“能读到、能切换、能排错”跑通。线上服务如果需要更快感知配置变化再补 long polling 和本地缓存。9 用一个本地 HTTP 页面给同事验收配置只在命令行输出配置同事看起来不直观。下面用 Python 标准库做一个最小验收服务把 Apollo 配置展示成 JSON。回到 Python 目录cd ~/apollo-non-java-demo/python写入config_server_demo.pycat config_server_demo.py PY import json from http.server import BaseHTTPRequestHandler, HTTPServer from apollo_read import fetch_config class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path not in (/, /config, /health): self.send_response(404) self.end_headers() self.wfile.write(bnot found) return if self.path /health: payload {status: ok} else: payload { source: apollo, config: fetch_config(), } body json.dumps(payload, ensure_asciiFalse, indent2).encode(utf-8) self.send_response(200) self.send_header(Content-Type, application/json; charsetutf-8) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) if __name__ __main__: server HTTPServer((127.0.0.1, 5000), Handler) print(demo service: http://127.0.0.1:5000/config) server.serve_forever() PY启动python3 config_server_demo.py本机访问curl http://127.0.0.1:5000/config看到配置 JSON 后就有了一个可给同事看的验收入口。现在可以把这个本地 5000 端口临时映射出去。10 用 cpolar 临时分享验收入口这里优先暴露刚才的示例服务而不是直接暴露 Apollo 管理后台。示例服务只展示当前配置攻击面比 Portal 小得多。保持python3 config_server_demo.py运行再开一个终端cpolar http 5000cpolar 会输出一个公网 HTTPS 地址。把 HTTPS 地址发给同事对方访问https://你的-cpolar-地址/config验收动作可以这样做同事打开/config看到timeout3000。你在 Apollo Portal 把timeout改成5000并发布。同事刷新/config看到新值。验收结束后在运行 cpolar 的终端按CtrlC关闭隧道。如果确实需要让同事看 Portal 操作页面再短时开放 8070cpolar http 8070这时要注意三件事不要把 8090 Admin Service 作为公网入口。Portal 账号密码要改成团队内部临时凭据不要长期使用默认密码。如果 cpolar 套餐支持访问保护给临时地址加上访问保护验收结束立刻关闭隧道。不要长期裸露 Apollo 管理后台。配置中心里常放数据库地址、开关项、第三方服务开关、灰度规则和敏感环境信息临时验收和正式开放是两件完全不同的事。11 常见问题排查11.1 curl 能访问 Portal但客户端读不到配置检查客户端用的是不是 8080。Portal 地址http://127.0.0.1:8070Config Service 地址http://127.0.0.1:8080客户端读取配置要走 8080不是 8070。11.2 返回 404按顺序查这几项appId是否是SampleApp大小写是否一致。clusterName是否是default。namespaceName是否是application。配置是否已经点击发布。自定义 namespace 的类型是否正确。properties 类型使用 namespace 名称例如application非 properties 类型要带后缀例如datasources.json。11.3 Docker 里访问 127.0.0.1 失败如果你的 Python、Go、Node.js 程序也跑在 Docker 容器里容器里的127.0.0.1指的是容器自己不是宿主机。本机 Docker Desktop 环境可以把配置中心地址改成APOLLO_CONFIG_SERVERhttp://host.docker.internal:8080如果客户端容器和 Apollo 容器在同一个 Docker 网络里可以使用服务名或容器名访问。关键是让客户端所在环境能连到 Config Service 的 8080 端口。11.4 改了配置客户端仍然读旧值先确认 Portal 里点了“发布”。Apollo 保存草稿后客户端读不到草稿内容只读已发布 release。再确认客户端是否做了本地缓存。如果你自己在服务里缓存了配置需要重新拉取或等待刷新周期。简单轮询间隔保持在 30 秒以上不要每秒打一次配置中心。11.5 401 UnauthorizedApollo 支持访问密钥。启用访问密钥后客户端请求要带签名 Header。本文 Quick Start 示例没有展开签名流程团队环境如果开启密钥Python、Go、Node.js 都需要按 Apollo 文档生成Authorization和TimestampHeader。11.6 cpolar 地址打不开先按本地顺序查curl http://127.0.0.1:5000/health curl http://127.0.0.1:5000/config本地服务通了再看 cpolar 终端是否仍在运行。映射 Portal 时检查 8070映射示例服务时检查 5000。不要把 8080、8090 一起开放出去。12 小结到这里我们已经跑通了一条非 Java 客户端接入 Apollo 的完整链路本地用 Docker 启动 Apollo Quick Start。在 Portal 里维护SampleApp/default/application配置。Python、Go、Node.js 通过 Config Service HTTP API 读取同一份配置。发布配置后三种语言重新读取都能拿到新值。用 cpolar 临时分享示例服务让远程同事直接验收配置变更结果。这条路线适合本地学习、跨语言服务联调、远程演示和团队验收。等要进团队正式环境时再把重点放到分布式部署、数据库权限、配置访问密钥、服务网络隔离、发布审计和客户端本地缓存上。你现在卡在哪一步是 Apollo 启动、appId/namespace 对不上、客户端读不到配置还是 cpolar 外网验收打不开评论区留一下环境和报错我按排查路径继续拆。参考资料Apollo Quick Starthttps://www.apolloconfig.com/#/zh/deployment/quick-startApollo Quick Start GitHubhttps://github.com/apolloconfig/apollo-quick-startApollo 非 Java 客户端 HTTP 接口https://www.apolloconfig.com/#/zh/client/other-language-client-user-guideApollo 分布式部署指南https://www.apolloconfig.com/#/zh/deployment/distributed-deployment-guide