通过 cert-manager 为 K3s 集群集成阿里云 DNS 并自动签发 Let's Encrypt 证书

Note

这篇文章多年前是与 自己搭建一个k8s环境从部署 httpd 入手,理清 k8s 配置中的 containerPort、port、nodePort、targetPort 一起规划的 #Kubernetes 系列文章之一,但因为太懒没有写完。(当时规划了大长篇,结果只发了两篇 😅)
时隔三年,我已经把博客迁移到 #K3s 后,发现仍然是这套东西,所以还是有记录一下的必要的。

对于现代站点,通常会选择使用 HTTPS 来保护数据的传输,我的博客就使用 Let's Encrypt 颁发的免费证书。在还没有用 Kubernetes/K3s 之前,我使用 acme.sh 来完成 Let's Encrypt 颁发证书所需的一系列验证、获取证书并安装到 Nginx 中,以及证书即将到期时的续订。

而在 Kubernetes 集群中,我们可以通过 cert-manager 来做到同样的事情。

本文结合安装在 #Debian 里的 K3s,实践:

  • 安装 cert-manager
  • cert-manager 使用 #阿里云 域名获取证书
  • 配置域名流量经过 #traefik 并使用证书

准备工作

确保 gpg 已经安装

后面安装 Helm 需要用到 gpg,因此先保证 gpg 已经安装好:

sudo apt update
sudo apt install gpg -y

安装 Helm

Helm 是 Kubernetes 的包管理器,直接按照官方文档安装即可:

curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt-get install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm

请注意,由于 Helm 通过读取环境变量 KUBECONFIG 来获取k3s的信息,如果执行 helm 命令时报错:

Error: INSTALLATION FAILED: Kubernetes cluster unreachable: Get "http://localhost:8080/version": dial tcp 127.0.0.1:8080: connect: connection refused

则需要配置 KUBECONFIG 环境变量:

echo 'export KUBECONFIG=/etc/rancher/k3s/k3s.yaml' >> ~/.bashrc
source ~/.bashrc

安装 cert-manager

cert-manager 安装文档 给出了 通过 kubectl apply 安装通过 Helm 安装等多种安装方式,这里以 kubectl apply 为例。

Note

由于我不在服务器中安装科学上网工具,因此下面的步骤都会使用在本地提前下载好 yaml 或压缩包,然后上传到服务器的方式来安装。
如果你的服务器可以顺利访问 GitHub 等站点,则直接按官方文档安装即可。

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml

通过 kubectl apply 安装中给出的命令里的 yaml 文件 https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml 单独下载好,上传到服务器中,然后直接 kubectl apply 即可:

kubectl apply -f cert-manager.yaml

至此,我们就安装好 cert-manager 了,可以使用 kubectl get pods 来验证一下:

$ kubectl get pods --namespace cert-manager

NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-5c6866597-zw7kh               1/1     Running   0          2m
cert-manager-cainjector-577f6d9fd7-tr77l   1/1     Running   0          2m
cert-manager-webhook-787858fcdb-nlzsq      1/1     Running   0          2m

理解 cert-manager 的机制

首先,cert-manager 需要知道使用哪个证书颁发机构的证书,即 Issuer。对于 Let's Encrypt,将使用 cert-manager 内置的 ACME issuer(Automated Certificate Management Environment 自动证书管理环境)。

然后,通过 Webhook 的方式,让阿里云域名完成 ACME issuer 的 DNS01 验证。简单而言就是我们将阿里云的 AccessKey 配置到 cert-manager 中,它便可以自动完成 Let's Encrypt 验证域名所有权(通常是为要创建证书的域名新增一条 TXT 记录)的步骤。

安装并配置 cert-manager-alidns-webhook

安装 cert-manager-alidns-webhook

阿里云域名的 Webhook 通过 cert-manager-alidns-webhook 实现,在 Release 页面,下载最新版本的安装包(这里使用的是 alidns-webhook-0.8.3.tgz)然后上传到服务器中,然后通过 Helm 安装:

helm install alidns-webhook ./alidns-webhook-0.8.3.tgz \
    --set groupName=alidns \
    --namespace cert-manager

关于 groupName,如果集群里只有一个域名需要获取证书,或者多个域名都是同一个阿里云账号下的,这个 groupName 叫什么都无所谓;如果有多个域名并且属于不同阿里云账号,甚至其它域名供应商的,则需要每个域名供应商的账号都采用唯一的命名,例如 域名供应商名字_账号名alidns_jack

创建阿里云 RAM 用户

接下需要一个有权限操作阿里云域名解析的用户,根据阿里云的最佳实践,我们创建 RAM 用户,输入名称,其它保持默认,然后点击“确定”:

创建RAM

创建完成后,点击用户名进入用户详情,依次点击权限操作 - 新增权限,在弹出的侧栏中,搜索 AliyunDNSFullAccess 并分配给用户:

授权RAM

最后,回到 认证管理 tab,在下方找到 创建 AccessKey,在弹出的“确认当前 AccessKey 用于轮转”中,选哪个都行(比如“其他”),然后勾选“我确认必须创建 AccessKey”并点击“继续创建”。

下载或复制生成的 AccessKey 并妥善保存,AccessKey Secret 只会显示一次,如果弄丢了再创建一个新的就行:

AccessKey

最后回到 K3s 中,创建一个 secret 保存刚刚获取到的 AccessKey IDAccessKey Secret

kubectl create secret generic alidns-secrets \
    --from-literal="access-token=YOUR_ACCESSKEY_ID" \
    --from-literal="secret-key=YOUR_ACCESSKEY_SECRET"

在 Let's Encrypt 的 staging 环境中进行验证

创建 ClusterIssuers

cert-manager 需要 Issuer 或 ClusterIssuers 来处理证书的生成,它们的区别就是 Issuer 可以包含命名空间,而 ClusterIssuers 可以在整个集群中访问。

注意,我们应该先在 Let's Encrypt 的 staging 环境进行验证,如果直接在正式环境验证,容易触发速率限制,导致一段时间内无法签发证书。

ClusterIssuers 为例,将下面的代码保存为 staging.yaml 文件(注意修改 YOUR_EMAIL_ADDRESSgroupName),并执行 kubectl apply -f staging.yaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: alidns-letsencrypt-staging
spec:
  acme:
    email: YOUR_EMAIL_ADDRESS
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt
    solvers:
    - dns01:
        webhook:
            config:
              accessTokenSecretRef:
                key: access-token
                name: alidns-secrets
              regionId: cn-beijing
              secretKeySecretRef:
                key: secret-key
                name: alidns-secrets
            groupName: alidns
            solverName: alidns-solver

创建 Certificate

接下来,创建 Certificate 指定要签发证书的域名,以及用于保存签发好证书的 secret 的名称。

把下面的内容创建为 tls-staging.yamlkubectl apply -f tls-staging.yaml

这里的 dnsNames 是域名列表,可以是多个;commonName 即“公用名(CN)”,从域名列表里挑一个写入即可:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-tls-staging
  namespace: hello-world
spec:
  secretName: example-com-tls-staging
  commonName: example.com
  dnsNames:
  - example.com
  - www.example.com
  issuerRef:
    name: alidns-letsencrypt-staging
    kind: ClusterIssuer

创建 Deployment、Service 和 IngressRoute

最后,我们使用 nginx 的镜像来做一个 hello world 看看效果,将下面的内容保存为 hello-world.yaml 并执行 kubectl apply -f hello-world.yaml

IngressRoute 中的 secretName 即上一步,Certificate 中指定的 secretNamematch 中配置的域名,要先做好域名解析,添加好 A/AAAA 记录到服务器的公网 IP 上。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-world
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
      - name: hello-world
        image: nginx:latest
        ports:
        - containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
  name: hello-world
spec:
  selector:
    app: hello-world
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: NodePort

---

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: hello-world-ingress
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`example.com`)
      kind: Rule
      services:
        - name: hello-world
          port: 80
  tls:
    secretName: example-com-tls-staging

使用浏览器,用 https 访问域名,如果地址栏显示为“不安全”,点击“不安全” - “与此站点的连接不安全” - 点击“×”左侧的证书按钮(这是 Microsoft Edge 浏览器的查看方式,不同浏览器查看方式可能不同),可以查看站点使用的证书,如果证书的“颁发给 - 公用名”与你设置的 commonName 一致,并且“颁发者 - 组织” 为 (STAGING) Let's Encrypt,则说明配置无误:

lets encrypt staging 证书

常见问题

如果浏览器提示“此网站没有证书”,可能是证书还没签发好,或者出现了问题。

可以检查 Certificate 是否有创建好证书,如果创建完成,READY 会显示为 True

$ kubectl get cert -n hello-world
NAME                      READY   SECRET                    AGE
example-com-tls-staging   True    example-com-tls-staging   4m4s

通常几分钟内就能签发完成,如果一段时间后,仍然是 False,检查 alidns-webhook(你需要通过 kubectl get pods -n cert-manager 找到 alidns-webhook 的 pod 名称) 和 cert-manager 的 log:

kubectl logs alidns-webhook-xxxxx -n cert-manager
kubectl logs -n cert-manager -l app=cert-manager

切换到 Let's Encrypt 正式环境

在一切流程都跑通后,让我们切换到 Let's Encrypt 正式环境,签发一个被浏览器信任的证书。

切换 ClusterIssuer 和 Certificate

前面我们已经创建了 staging.yamltls-staging.yaml,复制这两个文件并命名为 prod.yamltls-prod.yaml,再进行简单的修改。

prod.yaml 中,需要:

  • metadata.name 中的 staging 改为 prod
  • spec.server 改为 Let's Encrypt 正式环境的地址 https://acme-v02.api.letsencrypt.org/directory
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
-  name: alidns-letsencrypt-staging
+  name: alidns-letsencrypt-prod
spec:
  acme:
    email: YOUR_EMAIL_ADDRESS
-    server: https://acme-staging-v02.api.letsencrypt.org/directory
+    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt
    solvers:
    - dns01:
        webhook:
            config:
              accessTokenSecretRef:
                key: access-token
                name: alidns-secrets
              regionId: cn-beijing
              secretKeySecretRef:
                key: secret-key
                name: alidns-secrets
            groupName: alidns
            solverName: alidns-solver

tls-prod.yaml 中,将 metadata.namespec.secretNameissuerRef.namestaging 字样改为 prod 即可:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
-  name: example-com-tls-staging
+  name: example-com-tls-prod
  namespace: hello-world
spec:
-  secretName: example-com-tls-staging
+  secretName: example-com-tls-prod
  commonName: example.com
  dnsNames:
  - example.com
  - www.example.com
  issuerRef:
-    name: alidns-letsencrypt-staging
+    name: alidns-letsencrypt-prod
    kind: ClusterIssuer

切换 IngressRoute

修改前面创建的 hello-world.yaml,找到 IngressRoute 的部分,把 secretNamestaging 字样改为 prod 即可:

前文省略

---

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: hello-world-ingress
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`example.com`)
      kind: Rule
      services:
        - name: hello-world
          port: 80
  tls:
-    secretName: example-com-tls-staging
+    secretName: example-com-tls-prod

最后执行和更新这些文件:

kubectl apply -f prod.yaml -f tls-prod.yaml -f hello-world.yaml

同样,稍等片刻,就可以在浏览器访问域名,此时浏览器已经正确识别证书,地址栏上的“不安全”也变为了小锁图标,证书信息里的“(STAGING)”也没有了。

发表于

2025-04-14 08:57

最后修改于

2025-04-14 09:16

Tags
An unhandled error has occurred. Reload 🗙