部署 Umami 到 TKE

Umami 是一个基于 Node.js 的轻量级开源埋点分析工具。

官方仓库:https://github.com/umami-software/umami
官方网站:https://umami.is/
Demo:https://cloud.umami.is/analytics/us/websites

1. 部署 PostgreSQL

在 TKE (腾讯云容器服务) 上部署 postgres:16-alpine,推荐使用 StatefulSet(有状态副本集)。相比 Deployment,StatefulSet 能确保数据库 Pod 的网络标识和存储挂载是稳定的。

这里是豆包提供的两者的区别:

特性DeploymentStatefulSet
Pod 命名随机后缀(如 nginx-7f987d689d-2x78z)固定命名(如 mysql-0、mysql-1、mysql-2)
Pod 身份无唯一身份,替换后身份丢失固定身份(PVC、主机名、DNS 不变)
启动/销毁顺序无序,可同时创建/删除多个 Pod有序(按 0→1→2 启动,2→1→0 销毁)
网络标识仅通过 Service 随机访问固定 DNS 名称(如 mysql-0.xxx.default.svc.cluster.local)
持久化存储所有 Pod 共享 PVC(或无 PVC)每个 Pod 绑定专属 PVC(自动创建)
更新策略滚动更新/重建更新,无顺序限制有序更新(从最后一个 Pod 开始)

这里由于只有一个 Pod,貌似区别不大。

具体步骤如下:

  1. 购买腾讯云 CBS 云硬盘;

  2. 在 TKE 中配置创建 PV;

  3. 在对应的命名空间下创建 PVC;
    上面几步可以在页面上直接操作。

  4. 在对应的命名空间下创建密钥;

    apiVersion: v1
    kind: Secret
    metadata:
      name: postgres-secret
      namespace: umami-namespace
    type: Opaque
    stringData:
      postgres-password: "YourSecurePassword123" # 替换为您的强密码
      
  5. 在对应的命名空间下创建 StatefulSet;

    这里要注意的是,CBS 云硬盘由于会自动包含一个 lost+found 目录,所以在挂载到容器时,要注意排除这个目录。这里使用 busybox 来执行 initContainer 来移除这个目录,以保证数据库正常启动。

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: postgres-umami
      namespace: umami-namespace
    spec:
      persistentVolumeClaimRetentionPolicy:
        whenDeleted: Retain
        whenScaled: Retain
      podManagementPolicy: OrderedReady
      replicas: 1
      revisionHistoryLimit: 10
      selector:
        matchLabels:
          app: postgres-umami
      serviceName: postgres-umami
      template:
        metadata:
          labels:
            app: postgres-umami
        spec:
          containers:
          - args:
            - -c
            - shared_buffers=4GB
            - -c
            - work_mem=64MB
            - -c
            - max_connections=200
            env:
            - name: POSTGRES_DB
              value: umami
            - name: POSTGRES_USER
              value: admin
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  key: postgres-password
                  name: postgres-secret
                  optional: false
            image: postgres:16-alpine
            imagePullPolicy: IfNotPresent
            name: postgres-umami
            ports:
            - containerPort: 5432
              protocol: TCP
            resources:
              limits:
                cpu: "2"
                memory: 6Gi
              requests:
                cpu: "1"
                memory: 2Gi
            securityContext:
              privileged: false
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
            volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: data
          dnsPolicy: ClusterFirst
          initContainers:
          - command:
            - rm
            - -fr
            - /var/lib/postgresql/data/lost+found
            image: busybox:1.37.0-uclibc
            imagePullPolicy: IfNotPresent
            name: remove-lost-found
            resources:
              requests:
                cpu: 10m
                memory: 10Mi
            securityContext:
              privileged: false
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
            volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: data
          restartPolicy: Always
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
          volumes:
          - name: data
            persistentVolumeClaim:
              claimName: pvc-postgres-data
      updateStrategy:
        rollingUpdate:
          partition: 0
        type: RollingUpdate
      
  6. 在对应的命名空间下创建 Service;

    这里仍然使用 Service 来暴露数据库端口。貌似 StatefulSet 自带一个 DNS 地址,应该也可以直接使用 postgres-umami.umami-namespace.svc.cluster.local 来连接数据库。

    apiVersion: v1
    kind: Service
    metadata:
      name: svc-postgres-umami
      namespace: umami-namespace
    spec:
      internalTrafficPolicy: Cluster
      ipFamilies:
      - IPv4
      ipFamilyPolicy: SingleStack
      ports:
      - name: 5432-5432-tcp-4mdtvqnd87g
        port: 5432
        protocol: TCP
        targetPort: 5432
      selector:
        app: postgres-umami
      sessionAffinity: None
      type: ClusterIP
      

2. 部署 Umami 后端

  1. fork 官方代码到自己的仓库

    因为以后可能还要基于源代码进行定制,所以这里选择将官方代码 fork 到自己的仓库。

  2. 配置 CNB 打包

    打包使用的是 CNB,使用的配置文件如下:

    .jobs: &jobs
      - name: docker build
        script:
          - docker build -t ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}/${CNB_COMMIT} .
      - name: docker push
        script:
          - docker push ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}/${CNB_COMMIT}
      - name: update tke
        imports: https://cnb.cool/my-group/my-secret-repo/-/blob/main/tke.yml
        image: tencentcom/deploy-to-tke
        settings:
          secret_id: ${SECRET_ID}
          secret_key: ${SECRET_KEY}
          region: ${TKE_REGION}
          cluster_id: ${TKE_CLUSTER_ID}
          namespace: umami-namespace
          workload_kind: deployment
          workload_name: umami-deployment
          container_names: umami
          container_images: ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}/${CNB_COMMIT}
    
    .pipeline: &pipeline
      docker:
        image: node:22-alpine
      services:
        - docker
      stages: *jobs
    
    '**':
      web_trigger_release:
        - <<: *pipeline
    
    release:
      push:
        - <<: *pipeline
      
  3. 在 TKE 中创建 Umami Deployment

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: umami-deployment
      namespace: umami-namespace
    spec:
      progressDeadlineSeconds: 600
      replicas: 1
      revisionHistoryLimit: 3
      selector:
        matchLabels:
          app: umami
      strategy:
        rollingUpdate:
          maxSurge: 1
          maxUnavailable: 0
        type: RollingUpdate
      template:
        metadata:
          creationTimestamp: null
          labels:
            app: umami
        spec:
          containers:
          - env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  key: database-url
                  name: umami-secret
                  optional: false
            - name: APP_SECRET
              valueFrom:
                secretKeyRef:
                  key: app-secret
                  name: umami-secret
                  optional: false
            - name: DATABASE_TYPE
              value: postgresql
            - name: DISABLE_BOT_CHECK
              value: "1"
            - name: PRIVATE_MODE
              value: "1"
            - name: DISABLE_TELEMETRY
              value: "1"
            image: docker.cnb.cool/my-group/my-umami-repo/release:latest
            imagePullPolicy: IfNotPresent
            lifecycle:
              preStop:
                exec:
                  command:
                  - sleep 30s
            name: umami
            ports:
            - containerPort: 3000
              protocol: TCP
            readinessProbe:
              failureThreshold: 6
              initialDelaySeconds: 20
              periodSeconds: 10
              successThreshold: 1
              tcpSocket:
                port: 3000
              timeoutSeconds: 5
            resources:
              limits:
                cpu: "1"
                memory: 2Gi
              requests:
                cpu: 500m
                memory: 1Gi
            securityContext:
              privileged: false
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
          dnsPolicy: ClusterFirst
          imagePullSecrets:
          - name: cnb-jiajia-artifacts
          restartPolicy: Always
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
      
  4. 配置 Umami Servcie

    apiVersion: v1
    kind: Service
    metadata:
      labels:
        app: umami
      name: umami-service
      namespace: umami-namespace
    spec:
      internalTrafficPolicy: Cluster
      ipFamilies:
      - IPv4
      ipFamilyPolicy: SingleStack
      ports:
      - name: 80-3000-tcp
        port: 80
        protocol: TCP
        targetPort: 3000
      - name: 3000-3000-tcp
        port: 3000
        protocol: TCP
        targetPort: 3000
      selector:
        app: umami
      sessionAffinity: None
      type: ClusterIP
      
  5. 根据 TKE 安装的网关配置外网访问。

  6. 配置域名解析到 TKE 网关绑定的 CLB 地址。

— END —