基于 k3s 和两台轻量服务器搭建自己的集群

如果你看到了这篇文章,那么说明该博客已成功部署到了 k3s 集群上,并成功签发了 SSL/TLS 证书以支持 Https

由于白嫖的腾讯云服务器到期,并且我不再想续费,所以博客已经去集群化 :-P,但下面的步骤仍可参考

概述

两台轻量应用服务器部署 k3s 并通过 GitHub Action 完成 Hugo 博客的更新,同时给集群上 SSL/TLS 证书 支持对静态 WEB 的 Https 访问

所有静态页全部保存在 master 节点上,通过挂载的方式使用 Nginx 部署, 同时将 Nginx 的配置文件,一并挂载,方便后续添加路由反代等

原先使用的是 Caddy,但 Caddy 会出现反复重定向的问题

当前配置

  • 阿里云香港 2vCPU 2G Debain

    Debian GNU/Linux 11 (bullseye) 5.10.0-15-amd64 containerd://1.7.6-k3s1.27

  • 腾讯云上海 2vCPU 2G Debain

    Debian GNU/Linux 12 (bookworm) 6.1.0-9-amd64 containerd://1.7.6-k3s1.27

k3s 的安装与节点添加

在 k3s 中,分为 masteragent 两种类型的 node

Server 节点指的是运行 k3s server 命令的主机,control plane 和数据存储组件由 K3s 管理。

Agent 节点指的是运行 k3s agent 命令的主机,不具有任何数据存储或 control plane 组件。

我将拥有域名解析的香港阿里云服务器当做 master,腾讯云做 agent

k3s 配置要求

https://docs.k3s.io/zh/installation/requirements

安装

开放端口

公网组网需要开放两个端口1

master

aliyun-k3s-open-ports

agent

tencent-k3s-open-ports

51821 适用于 Ipv6

添加节点

k3s 有国内的镜像脚本用来加速安装,但由于我们还没有在两台服务器之间组网,所以需要在安装时加点参数2

感谢某小只叶师傅 x

对于 master 节点

(
  SERVER_EXTERNAL_IP=<Yours>
  curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -s - \
    --node-external-ip=$SERVER_EXTERNAL_IP --flannel-backend=wireguard-native --flannel-external-ip
)

上面的 SERVER_EXTERNAL_IPmaster 的公网地址,<Yours> 需替换成您服务器的公网 IP

在接下来安装 agent 之前,我们需要知道 master 也就是当前节点的 token:

cat /var/lib/rancher/k3s/server/node-token

接下来安装配置 agent 节点

(
  AGENT_EXTERNAL_IP=<Yours>
  curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn K3S_URL=https://<SERVER_EXTERNAL_IP>:6443 K3S_TOKEN=<MasterToken> sh - \
  --node-external-ip=$AGENT_EXTERNAL_IP
)

这里需要三个参数:

  1. 当前 agent 服务器的公网 IP
  2. master 的公网 IP
  3. MasterToken:上一步获取的 master 的 nodetoken

agent 执行完毕后可以在 master 上查看当前集群节点信息:

kubectl get node

k3s-check-nodes

如果类似上面那样就 Ok 了

卸载

因为 k3s 十分的轻量,所以安装卸载的成本很低,如果出现了什么棘手的问题,比如 namespace 卡在 Terminating 无论怎样都删不掉等

可以直接将 k3s 扬掉

下面是卸载脚本,在 k3s 安装的时候已经安装好了

master

/usr/local/bin/k3s-uninstall.sh

agent

/usr/local/bin/k3s-agent-uninstall.sh

部署 Hugo 静态博客

下面使用 Nginx 和 Hugo 来构建

GitHub Action 通过 SFTP 部署到服务器

在 Hugo 项目目录中新建 .github/workflows/deploy.yaml

name: Deploy blog

on:
  push:
    branches:
      - master

permissions:
  contents: write

jobs:
  build-and-deploy:
    name: Build & Deploy
    runs-on: ubuntu-latest
    steps:
      - name: Check out blog code
        uses: actions/checkout@v3
        with:
          ref: master
          submodules: recursive
          fetch-depth: 1

      - name: Add Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: latest

      - name: Build posts
        run: hugo

      - name: Deploy posts to server
        uses: wlixcc/SFTP-Deploy-Action@v1.2.4
        with:
          username: ${{ secrets.USERNAME }}
          server: ${{ secrets.SERVER_IP }}
          ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
          local_path: "./public/*"
          remote_path: ${{ secrets.REMOTE_PATH }}
          sftpArgs: "-o ConnectTimeout=5"

上面的配置基本不需要修改,只需要注意几点:

  1. 当前 Action 是否有相应权限
  2. 正确配置好 secrets

github-action-secrets

  • secrets.USERNAME: 服务器用户名
  • secrets.SERVER_IP: 服务器公网 IP
  • secrets.SSH_PRIVATE_KEY: 私钥
  • secrets.REMOTE_PATH: 博客静态页部署在服务器上的位置,比如 /srv/blog/

关于 SSH 的问题可自行查资料解决,一般以下几个步骤:

  1. 使用 ssh-keygen 生成公私钥
  2. 将公钥 *.pub 上传到服务器,并添加到 authorized_keys

编写 k3s 资源配置文件

# nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
  namespace: zako-api-gateway
  labels:
    app: nginx-deploy
spec:
  selector:
    matchLabels:
      app: nginx-deploy
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx-deploy
    spec:
      nodeName: zako-master # 固定 Pod 在 zako-master 节点,便于访问静态文件
      containers:
        - name: nginx-deploy
          image: nginx
          ports:
            - containerPort: 80
            - containerPort: 443
          volumeMounts:
            - name: nginx-conf
              mountPath: /etc/nginx/conf.d
            - name: nginx-log
              mountPath: /var/log/nginx
            - name: blog
              mountPath: /usr/share/zako.top/blog
      volumes:
        - name: nginx-conf
          hostPath:
            path: /srv/k3s/zako.top/nginx/conf
        - name: nginx-log
          hostPath:
            path: /srv/k3s/zako.top/nginx/log
        - name: blog
          hostPath:
            path: <前面通过 SFTP 上传的文件地址>
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
  namespace: zako-api-gateway
spec:
  selector:
    app: nginx-deploy
  type: ClusterIP
  ports:
    - name: nginx-http
      protocol: TCP
      port: 80
      targetPort: 80
    - name: nginx-https
      protocol: TCP
      port: 443
      targetPort: 443

上面涉及到的 nginx.conf:

server {
    listen 80;
    listen 443;
    server_name zako.top;

    charset utf-8;
    gzip on;

    set $static /usr/share/zako.top;

    location / {
        alias $static/blog/;
        index index.html index.htm;
    }
}

开放博客外部访问

配置 Ingress

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: zako-ingress
spec:
  rules:
    - host: zako.top
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-svc
                port:
                  number: 80

部署后应该能通过域名,我这里是 zako.top 访问到了,但是证书是 k3s 自签的,下面我们换成一个免费的 CA 证书

添加并续签 SSL/TLS 证书3

我选择 Let‘s Encrypt,它提供了免费的 CA 证书: https://letsencrypt.org/

下面的步骤来自:https://juejin.cn/post/7139150146452848648

安装 cert-manager

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

检查状态:

kubectl get pods -n cert-manager

k3s-cert-manager

部署 Issuing Certificates

创建 letsencrypt.yaml 文件:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  namespace: cert-manager
  name: letsencrypt
spec:
  acme:
    email: <替换成您的邮箱>
    privateKeySecretRef:
      name: prod-issuer-account-key
    server: https://acme-v02.api.letsencrypt.org/directory
    solvers:
      - http01:
          ingress:
            class: traefik
        selector: {}

部署(需等待前面 cert-manager 全部进入 Running 状态):

kubectl apply -f letsencrypt.yaml

下面给我们之前部署的 Ingress 添加 TLS:

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: zako-ingress
+ annotations:
+   cert-manager.io/cluster-issuer: letsencrypt
spec:
+ tls:
+   - secretName: zako-ingress
+     hosts:
+       - zako.top
  rules:
    - host: zako.top
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-svc
                port:
                  number: 80

过一小会儿后再次访问,就会发现浏览器没有做出警告了

查看已部署的证书

上面模式是 default namespace,所以没有指定命名空间

kubectl describe certificate

如果您的 Ingress 部署在特定的 namespace 中,需要加上参数:

kubectl describe certificate -n <Namespace>

重定向 http 到 https4

https://forums.rancher.cn/t/k3s-traefik-http-https/450

前面我们支持了 Https,现在我希望所有的 Http 请求全被重定向,使用 Https 来访问

办法有两种:

  1. 修改 k3s 的 HelmChart,全局 Https
  2. 为 ingress 添加 middleware,单独对一个 ingress 作用

这里我选择第二种:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: zako-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
    traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
spec:
  tls:
    - secretName: zako-ingress
      hosts:
        - zako.top
  rules:
    - host: zako.top
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-svc
                port:
                  number: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: redirect-https
spec:
  redirectScheme:
    scheme: https
    permanent: true

前后对比:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: zako-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
+   traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
spec:
  tls:
    - secretName: zako-ingress
      hosts:
        - zako.top
  rules:
    - host: zako.top
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-svc
                port:
                  number: 80
+ ---
+ apiVersion: traefik.containo.us/v1alpha1
+ kind: Middleware
+ metadata:
+   name: redirect-https
+ spec:
+   redirectScheme:
+     scheme: https
+     permanent: true

注意上面的注解(annotations) traefik.ingress.kubernetes.io/router.middlewares 格式需要为:

<namespace>-<middleware-name>@kubernetescrd

缺省的 namespace 在 k3s 中为 default

应用更改:

kubectl apply -f ingress.yaml

现在所有 Http 访问就会重定向到 Https 了

其他问题

Hugo 访问丢失 CSS 样式

如果 Hugo 的 baseURL 直接指明了协议,比如 https,那么在使用 http 访问时博客会丢失 CSS

可行的办法是将协议去掉5,如:https://zako.top/ 换成 //zako.top/

Nginx 日志时间不对

因为 Nginx 默认的是格林尼治时间,所以如果有相应需求,需要在部署时设置好时区:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
  namespace: zako-api-gateway
  labels:
    app: nginx-deploy
spec:
  selector:
    matchLabels:
      app: nginx-deploy
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx-deploy
    spec:
      nodeName: zako-master
      containers:
        - name: nginx-deploy
          image: nginx
+         env:
+           - name: TZ
+             value: Asia/Shanghai
          ports:
            - containerPort: 80
            - containerPort: 443
          volumeMounts:
            - name: nginx-conf
              mountPath: /etc/nginx/conf.d
            - name: nginx-log
              mountPath: /var/log/nginx
            - name: blog
              mountPath: /usr/share/zako.top/blog
      volumes:
        - name: nginx-conf
          hostPath:
            path: /srv/k3s/zako.top/nginx/conf
        - name: nginx-log
          hostPath:
            path: /srv/k3s/zako.top/nginx/log
        - name: blog
          hostPath:
            path: <前面通过 SFTP 上传的文件地址>
      restartPolicy: Always
kubectl apply -f nginx.yaml