helm相关的一些实践操作,db migrate、私有helm repo。持续更新…

主要内容

如何使用helm进行数据库迁移

在我们将新版本的服务部署到k8s上面时,有的服务会有对数据库进行migrate的操作。这时我们就需要在 部署服务之前,提前运行数据库的修改指令。

方法1:使用kubectl

主要思路是我们可以在CICD工具上,使用kubectl在集群创建一个一次性的pod。运行它时引用该服务在集群里 创建好的secret资源文件,运行的command是我们要执行的db migrate命令。

下面是我们之前在使用Jenkins时的用法,部分相关片断。

kubeDBMigrate定义的片断:

#!groovy

/* Usage example:
runMigrate(imageFullName: "registry.xxx.com/hi42/pipeline-test:staging-344ea60", namespace: "devops", command: '"sleep","30"', configMap: "fluentd-test-config")
*/


def call(Map migrateConfig = [:]) {
    if (migrateConfig.context && migrateConfig.config) {
        defaultContext = migrateConfig.context
        defaultConfig = migrateConfig.config
    } else {
        defaultContext = "kubernetes-admin@kubernetes"
        defaultConfig = "/home/user_name/.kube/jenkins-k8s-config"
    }

    // pod name(Optional,default value 'db-migrate')
    if (!migrateConfig.taskName) {
        podName = "db-migrate"
    } else {
        podName = migrateConfig.taskName
    }

    // Default's envfrom is Secret
    if (!migrateConfig.envFrom) {
      jsonContent = """{"spec": {"containers": [{"image": "$migrateConfig.imageFullName", "command": [$migrateConfig.command], "name": "$podName", "envFrom": [{"secretRef": {"name": "$migrateConfig.configMap"}}]}]}}"""
    } else if (migrateConfig.envFrom == 'configmap') {
      jsonContent = """{"spec": {"containers": [{"image": "$migrateConfig.imageFullName", "command": [$migrateConfig.command], "name": "$podName", "envFrom": [{"configMapRef": {"name": "$migrateConfig.configMap"}}]}]}}"""
    }

    // start a pod exec db migrate command
    def runMigrateCmd = [
        "kubectl run",
        "$podName",
        "--image=$migrateConfig.imageFullName",
        "--attach=true --rm=true --restart=Never",
        "--namespace $migrateConfig.namespace",
        "--context=$defaultContext",
        "--kubeconfig=$defaultConfig",
        "--overrides='$jsonContent'"
    ]
    sh(runMigrateCmd.join(" "))

Jenkinsfile文件中调用kubeDBMigrate的片断:

stage("Deploy to Production") {
  waitForApprove("Deploy to Production?", 3)
  node("${buildAgent}") {
    kubeDBMigrate(imageFullName: "${imageName}:${imageTag}", namespace: "${namespace}", command: '"bundle", "exec", "rails", "db:migrate"', configMap: "${prodConfigMap}", envFrom: "configmap")

    PRODUCT_DEPLOY_CONTAINER.each { key, value ->
      kubeDeployToServer(namespace: "${namespace}", deploymentName: key, imageFullName: "${imageName}:${imageTag}", containerName: value)
    }
  }
}

在Jenkinsfile中,一旦某个命令执行失败,那么整个流水线将会失败。通过上面jenkinsfile中的片断, 可以看出我们是在deploy production服务之前运行了 db migrate 指令。

TIPS: –restart 可选 [Always, OnFailure, Never],根据值的不同,创建相对应的资源类型,[Deployment, Job, Pod]

方法2:使用Helm Hook

根据helm官方文档介绍,使用helm hook的作用可以在helm install 集群之前或之后创建加载的资源(这里的资源是指helm所支持的所有类型)。

我们这里使用了带hook的job资源,去处理最开始的问题. 即在使用helm部署新版本的服务之前,先执行数据库migrate 操作。

如果要想把一个资源变成hook类型的,只要在annotations添加helm.sh/hook 就行了,如下:

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate-{{ .Chart.Name }}-{{ .Values.migrateJobNameSuffix }}
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  activeDeadlineSeconds: 3600
  template:
    spec:
      restartPolicy: Never
      {{- if .Values.imagePullSecrets }}
      imagePullSecrets:
{{ toYaml .Values.imagePullSecrets | indent 8 }}
      {{- end }}
      containers:
      - name: db-migrate
        image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
        envFrom:
        - secretRef:
            name: {{ .Chart.Name }}-migrate-job-{{ .Values.migrateJobNameSuffix }}
        command:
        - rake
        - db:migrate

"helm.sh/hook": pre-install,pre-upgrade: 表示在更新或安装chart之前执行的操作

"helm.sh/hook-delete-policy": hook-succeeded:删除策略是在 资源执行成功之后

调整hook的优先级

接着上一节,在第一次使用hook执行db migrate之后,发现正常Chart里的 secret资源无法被hook里的资源引用到, 这样的话,就得在首次安装这个Chart之前,手动创建一个secret供job使用。后来想了想,可以根据hook-weight 在定义一个高优先级的hook资源,这样后面的带hook 的job就可以引用到了。如下:

apiVersion: v1
kind: Secret
metadata:
  name: {{ .Chart.Name }}-migrate-job-{{ .Values.migrateJobNameSuffix }}
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-delete-policy": hook-succeeded,hook-failed
    "helm.sh/hook-weight": "-88"
type: Opaque
data:
  DB_PORT: {{ .Values.pgPort | toString | b64enc }}

helm.sh/hook-weight: 只要该值小于job的权重(默认值0)即可

TIPS:这里提一下secret里的一个小知识,就是templates 的secret资源里面的字段值要是string类型的, 像port 数字会自动判断为int,所以需要使用toString方法给转换成string。

搭建一个自己的helm仓库

项目多了之后,需要一个集中存储Chart的地方。Helm repository的支持方式很简单,有个index.html, 可提供web下载的就行了。

主要思路是:

helm dependency -> helm package -> helm repo index -> Web Server(nginx)

用python根据上面的思路自己写了个简单粗糙的项目: helm-private-repository

另外还有一个比较好的helm 仓库项目推荐给大家:chartmuseum

Helm template中资源引用value 值会强制转换为int

最近在项目中遇到一个问题:helm chart中有些资源文件里,定义了一些变量,变量值是从–set时获得的, 但是到了实际helm解析的时候,将该值转换了int,k8s会报错,说资源名字不符合规范。即使在资源的定义变量后面 加上toString 也不好使。

具体案例:gitlab的 commit-short-hash。当遇到8位是纯数字(49113771)的时候,helm就转换成 这种格式4.9113771e+07. 导致无法创建资源,流水线失败。错误如下:

Error: Job.batch "db-migrate-xxxx-4.9113771e+07" is invalid: [metadata.name: Invalid value: "db-migrate-xxxx-4.9113771e+07": a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*'),

解决的方法:在使用helm命令设置该变量值时,将--set 替换成 --set-string.

参考

database-migrations

Where to run database migrations