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,实践:
准备工作
确保 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 用户,输入名称,其它保持默认,然后点击“确定”:
创建完成后,点击用户名进入用户详情,依次点击权限操作
- 新增权限
,在弹出的侧栏中,搜索 AliyunDNSFullAccess
并分配给用户:
最后,回到 认证管理
tab,在下方找到 创建 AccessKey
,在弹出的“确认当前 AccessKey 用于轮转”中,选哪个都行(比如“其他”),然后勾选“我确认必须创建 AccessKey”并点击“继续创建”。
下载或复制生成的 AccessKey 并妥善保存,AccessKey Secret
只会显示一次,如果弄丢了再创建一个新的就行:
最后回到 K3s 中,创建一个 secret 保存刚刚获取到的 AccessKey ID
和 AccessKey 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_ADDRESS
和 groupName
),并执行 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.yaml
并 kubectl 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
中指定的 secretName
;match
中配置的域名,要先做好域名解析,添加好 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
,则说明配置无误:
常见问题
如果浏览器提示“此网站没有证书”,可能是证书还没签发好,或者出现了问题。
可以检查 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.yaml
和 tls-staging.yaml
,复制这两个文件并命名为 prod.yaml
和 tls-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.name
、spec.secretName
和 issuerRef.name
的 staging
字样改为 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
的部分,把 secretName
的 staging
字样改为 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)”也没有了。