在 Kubernetes 上运行 CFML 应用:实战操作全流程
在 Kubernetes 上运行 CFML 应用:实战操作全流程
CFML(ColdFusion 标记语言)至今仍支撑着大量企业软件,而其中很多都被困在一台臃肿的单体应用服务器上。这篇文章讲的是我把一个 CFML 应用——基于开源 CFML 引擎 Lucee 构建——容器化并跑在 Kubernetes 上的实际步骤,并把那些真正会坑到你的细节一并点出来。
这不是一篇从零开始的 Kubernetes 教程。它假设你能敲 kubectl get pods,并且手头有个可以连的集群(minikube、kind 或托管集群都行)。
心智模型
CFML 应用跑在 Kubernetes 上,形态和任何其他 Web 应用一样,只有一处特殊:引擎和你的代码是绑在一起、装进同一个镜像里的。
你的 .cfm/.cfc 代码 → Docker 镜像(Lucee + 代码) → Pod → Service → Ingress → 用户
(构建一次) (运行 N 份副本)
凡是不属于代码的东西——数据库密码、数据源配置、会话设置——都在运行时通过 ConfigMap 和 Secret 注入,绝不烤进镜像里。守住这条界线,剩下的就顺理成章了。
第一步:容器化应用
从官方 Lucee 镜像出发,把你的 webroot 拷进去。一个最小化的 Dockerfile:
FROM lucee/lucee:6.0-tomcat
# 应用代码放进 Tomcat 的 webroot
COPY ./webroot /var/www
# Lucee 管理密码与部署期设置来自环境变量,而非镜像
ENV LUCEE_ADMIN_ENABLED="false"
# Lucee 镜像里 Tomcat 监听 8888
EXPOSE 8888在 Kubernetes 还没登场之前,先在本地构建并冒烟测试:
docker build -t myregistry/cfml-app:1.0.0 .
docker run --rm -p 8888:8888 myregistry/cfml-app:1.0.0
# 访问 http://localhost:8888 —— 确认 .cfm 页面能正常渲染
docker push myregistry/cfml-app:1.0.0用真实的版本号打 tag,永远别用 latest。 Kubernetes 会激进地缓存镜像;latest 会让发布变得模糊,回滚则无从谈起。
第二步:Deployment
Deployment 声明要跑几份 Pod 副本、用哪个镜像。
apiVersion: apps/v1
kind: Deployment
metadata:
name: cfml-app
labels:
app: cfml-app
spec:
replicas: 2
selector:
matchLabels:
app: cfml-app
template:
metadata:
labels:
app: cfml-app
spec:
containers:
- name: cfml-app
image: myregistry/cfml-app:1.0.0
ports:
- containerPort: 8888
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
memory: "1Gi"关于内存有一句要说:Lucee 跑在 JVM 上,而除非你明确告诉它,JVM 是看不到容器内存上限的。要把堆大小设在上限之内,否则内核会在请求处理到一半时把 Pod OOM 杀掉。通过 JVM 选项的环境变量传入:
env:
- name: CATALINA_OPTS
value: "-XX:MaxRAMPercentage=75.0"MaxRAMPercentage 让 JVM 按容器上限的比例来设定堆大小——比硬编码一个 -Xmx 安全得多。
第三步:用 Service 暴露出去
Service 给这些 Pod 一个稳定的内部地址。Pod 来来去去,Service 的名字不变。
apiVersion: v1
kind: Service
metadata:
name: cfml-app
spec:
selector:
app: cfml-app
ports:
- port: 80
targetPort: 8888集群内部现在任何东西都能通过 http://cfml-app 访问到这个应用。要让外部世界进来,加一个 Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cfml-app
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cfml-app
port:
number: 80对于要处理文件上传的 CFML 应用来说,那个 proxy-body-size 注解很关键——Nginx 默认上限是 1 MB,超过就会被静默拒绝。
第四步:配置与密钥
千万别把数据库密码烤进镜像。非敏感配置放进 ConfigMap,凭据放进 Secret:
apiVersion: v1
kind: ConfigMap
metadata:
name: cfml-config
data:
DB_HOST: "postgres.internal"
DB_NAME: "appdb"
---
apiVersion: v1
kind: Secret
metadata:
name: cfml-secrets
type: Opaque
stringData:
DB_PASSWORD: "change-me-in-real-life"
LUCEE_ADMIN_PASSWORD: "also-change-me"用 envFrom 接到 Deployment 的容器里:
envFrom:
- configMapRef:
name: cfml-config
- secretRef:
name: cfml-secrets然后在 Application.cfc 里读取它们,让数据源由环境决定,而不是硬编码:
component {
this.name = "myapp";
this.datasources["appdb"] = {
class: "org.postgresql.Driver",
connectionString: "jdbc:postgresql://#server.system.environment.DB_HOST#/#server.system.environment.DB_NAME#",
username: server.system.environment.DB_USER,
password: server.system.environment.DB_PASSWORD
};
}
同一个镜像现在不改一行就能跑在 dev、staging 和 prod——区别只在注入的配置。这正是整套做法的意义所在。
第五步:健康探针
Kubernetes 需要知道一个 Pod 何时存活、何时准备好对外服务。CFML 应用预热引擎可能要花 20–60 秒,所以探针配错是导致部署陷入 crash 循环的头号原因。
往 webroot 里加一个最简单的 health.cfm,只输出 OK,然后:
readinessProbe:
httpGet:
path: /health.cfm
port: 8888
initialDelaySeconds: 20
periodSeconds: 5
livenessProbe:
httpGet:
path: /health.cfm
port: 8888
initialDelaySeconds: 40
periodSeconds: 15- Readiness(就绪) 决定是否把流量发给这个 Pod。在
/health.cfm应答之前,Pod 会被排除在 Service 之外。 - Liveness(存活) 会重启已经卡死的 Pod。
initialDelaySeconds要给得宽裕些——杀得太早,Lucee 永远启动不完。
第六步:应用并验证
kubectl apply -f k8s/
kubectl rollout status deployment/cfml-app
kubectl get pods -l app=cfml-app
kubectl logs -f deploy/cfml-app # 观察 Lucee 启动如果某个 Pod 卡住了,kubectl describe pod <name> 会列出事件——拉镜像失败、OOM 被杀、探针失败都会在这里浮现。
第七步:扩缩容与发布
水平扩容只需改副本数,或者让 Kubernetes 按 CPU 自动扩:
kubectl scale deployment/cfml-app --replicas=4
kubectl autoscale deployment/cfml-app --cpu-percent=70 --min=2 --max=8有一个 CFML 有状态应用独有的坑:会话亲和性(session affinity)。如果你的应用把会话保存在 JVM 内存里,那同一个用户的请求就必须回到同一个 Pod。要么在 Ingress 上开启粘性会话:
nginx.ingress.kubernetes.io/affinity: "cookie"……要么更好的做法是把会话彻底移出 JVM——存到 Redis 或数据库里,这样任何 Pod 都能服务任何请求。无状态的 Pod 才能让扩缩容和滚动更新毫无痛感;粘性会话是权宜之计,不是归宿。
发布新版本就是换一个镜像 tag:
kubectl set image deployment/cfml-app cfml-app=myregistry/cfml-app:1.1.0
kubectl rollout undo deployment/cfml-app # 出问题就秒回滚检查清单
每次把 CFML 应用搬上 Kubernetes,真正决定成败的就是这几件事:
- 固定镜像 tag——永远别用
latest。 - 用
MaxRAMPercentage把 JVM 堆大小框进容器上限。 - 所有配置都注入——通过 ConfigMap 和 Secret,敏感信息一律不进镜像。
- 给探针足够的预热时间——Lucee 启动很慢。
- 让 Pod 无状态——外置会话,或接受粘性会话的局限。
- 设置 resource requests,让调度器能合理安置 Pod。
把这六点做对,CFML 应用就和任何现代工作负载一样——可扩展、能自愈,并且以最好的方式变得"平平无奇"。CFML 那点"遗留技术"的名声,跟语言本身关系不大,更多是它一直以来的部署方式所致。而部署这件事,Kubernetes 给修好了。