年度归档: 2024 年

使用阿里云服务器部署traefik,关联NLB负载均衡器,添加cert-manager免费证书认证

背景需求

我们有一个常规的网站服务需要部署,并且对外提供https访问,从经济的角度考虑,建议购买阿里云的【容器集群ACK【网站域名】【NBL负载均衡】【共享带宽包】【云服务器ECS】,如果有文件需求还可以购买【对象存储】【NAS文件系统】等,有静态文件加速需求还可以购买CDN服务,本篇文章我使用最低需求(钱),仅购买3台服务器,组成k8s集群,部署web网站,自动使用acme.sh申请证书,使用外部负载均衡器来打造一个最低限度的高可用生产环境。

忽略的细节

从阿里云官网购买【容器集群ACK【网站域名】【NBL负载均衡】【共享带宽包】【云服务器ECS】本文忽略,默认读者已购买并添加好,并复制kubeconfig文件到服务器上,kubectl和helm程序已经安装好,接下来直接敲命令

1.安装cert-manager

helm repo add jetstack https://charts.jetstack.io
 
helm repo update
 
helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.15.3 \
  --set installCRDs=true

2.配置cert-manager所需要的阿里云dns挑战

安装阿里云dns挑战所对应的webhook

helm repo add cert-manager-alidns-webhook https://devmachine-fr.github.io/cert-manager-alidns-webhook
helm repo update
helm install alidns-webhook cert-manager-alidns-webhook/alidns-webhook

添加alidns-secret.yaml 文件,注意这里的access-key和secret-key是要通过阿里云的accesskey功能去获取的,获取之后,通过echo命令获取base64加密后的文本,填入yaml文件中

echo -n "原始密钥" | base64 

# alidns-secret.yaml 文件
apiVersion: v1
kind: Secret
metadata:
  name: alidns-secret
  namespace: cert-manager
data:
  access-key: base64加密后的
  secret-key: base64加密后的
kubectl apply -f alidns-secret.yaml

添加letsencrypt-staging.yaml文件,注意不要修改groupName,因为我上面helm install alidns-webhook的时候使用的默认参数里的groupName是example.com,一定要修改groupName的话,需要两边同步修改

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: 357244849@qq.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
    - dns01:
        webhook:
          groupName: example.com
          solverName: alidns-solver
          config:
            region: ""
            accessKeySecretRef:
              name: alidns-secret
              key: access-key
            secretKeySecretRef:
              name: alidns-secret
              key: secret-key
kubectl apply -f letsencrypt-staging.yaml

添加Certificate.yaml文件,这一步完成后,手动去阿里云的域名解析里添加对应的cname解析了,记录值填负载均衡器给到的域名,一般是nlb-xxxx.地域.nlb.aliyuncs.com

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: hello-com
  namespace: traefik
spec:
  # The secretName will store certificate content
  secretName: hello-com-tls
  dnsNames:

  - "*.hello.com"
  - "ops.hello.com"
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer
kubectl apply -f Certificate.yaml

3.安装traefik并关联对应的证书和负载均衡器

1.编辑一个values-traefik.yaml文件,可以参考我下面的配置,

providers:
  kubernetesCRD:
    allowCrossNamespace: true
  kubernetesIngress:
    publishedService:
      enabled: true # 让 Ingress 的外部 IP 地址状态显示为 Traefik 的 LB IP 地址
service:
  enabled: true
  loadBalancerClass: alibabacloud.com/nlb
  annotations:
    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-id: "NLB的ID" # 关联阿里云NLB负载均衡器的ID。
    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-force-override-listeners: "true"
  spec:
    externalTrafficPolicy: Local
# 这里不加的话,80和 443 会报没有权限
securityContext:
  capabilities:
    add:
      - NET_BIND_SERVICE
  runAsNonRoot: false
  runAsUser: 0
updateStrategy:
  # -- Customize updateStrategy: RollingUpdate or OnDelete
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 1
    maxSurge: 0
ports:
  web:
    port: 80
    expose:
      default: true
    exposedPort: 80 # 对外的 HTTP 端口号,使用标准端口号在国内需备案
    redirectTo:
      port: websecure
  websecure:
    port: 443
    expose:
      default: true
    exposedPort: 443 # 对外的 HTTPS 端口号,使用标准端口号在国内需备案
logs:
  access:
     enabled: true
deployment:
  enabled: true
  replicas: 3
ingressRoute:
  dashboard:
    enabled: true
    matchRule: Host(`traefik.hello.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
    entryPoints: ["websecure"]
    middlewares:
      - name: traefik-dashboard-auth
extraObjects:
  - apiVersion: v1
    kind: Secret
    metadata:
      name: traefik-dashboard-auth-secret
    type: kubernetes.io/basic-auth
    stringData:
      username: hello
      password: thankyou

  - apiVersion: traefik.io/v1alpha1
    kind: Middleware
    metadata:
      name: traefik-dashboard-auth
    spec:
      basicAuth:
        secret: traefik-dashboard-auth-secret
# 关联cert-manager设置的秘钥
tlsStore:
  default:
    defaultCertificate:
      secretName: hello-com-tls
helm repo add traefik https://helm.traefik.io/traefik
helm repo update
helm upgrade --install traefik -n traefik -f values-traefik.yaml traefik/traefik

安装完成后,使用kubectl get svc -n traefik就能看到生成的loadbalancer了,通过阿里云控制台也可以看到网络型负载均衡器里面自动创建了对应的监听和服务器组,如需验证部署后的效果,可以用浏览器访问https://traefik.hello.com/dashboard 进行测试

python实现的一个简易注册机,用于离线工控机验证注册码

简易的设计流程

有一批工控机长期是断网离线的,而我们又想检查这批工控机的设备上的注册码是否已经过期,那该怎么办呢?工控机是交付出去了,需要用到非对称加密机制来设计,防止算法被破解之后,别人有能力能破解我们所有的工控机。下面废话不多说,直接上代码

服务端程序

1. 安装 cryptography

pip install cryptography

2. 生成密钥对(一次性操作)

在服务器端生成 RSA 公钥和私钥,这个过程只需要进行一次,并且私钥需要保密。

# 注册机密钥对生成代码
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# 生成 RSA 密钥对
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
)

# 将私钥保存到文件
with open("private_key.pem", "wb") as private_file:
    private_file.write(
        private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption(),
        )
    )

# 将公钥保存到文件
public_key = private_key.public_key()
with open("public_key.pem", "wb") as public_file:
    public_file.write(
        public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
    )

private_key.pem 文件将用于服务器端签名注册码。(千万不要泄漏)

public_key.pem 文件可以安全地分发给客户端,用于验证签名。

3. 服务器端生成注册码并签名

服务器端生成注册码并用私钥签名。这里我们基于 IP、MAC 地址和注册时间生成注册信息。

# 注册码生成程序
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
import hashlib

# 加载私钥
with open("private_key.pem", "rb") as private_file:
    private_key = serialization.load_pem_private_key(
        private_file.read(),
        password=None,
    )


# 生成注册信息
def generate_registration_info(ip, mac, reg_time):
    return f"{ip}#{mac}#{reg_time}"


# 生成签名
def sign_registration_info(private_key, registration_info):
    # 先对注册信息进行哈希处理
    hash_value = hashlib.sha256(registration_info.encode()).digest()
    # 使用私钥对哈希值签名
    signature = private_key.sign(
        hash_value,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH,
        ),
        hashes.SHA256(),
    )
    return signature


# 示例使用
ip = "192.168.1.1"
mac = "00:1A:2B:3C:4D:5E"
reg_time = "2023-09-03T12:00:00"
registration_info = generate_registration_info(ip, mac, reg_time)
signature = sign_registration_info(private_key, registration_info)

print(f"注册信息: {registration_info}")
print(f"签名: {signature.hex()}")

客户端程序(可参考修改)

1.安装在工控机上,用于验证注册码是否正确,是否过期,设置有效期365天

2.客户端使用公钥来验证签名,以确保注册码的有效性。

# 注册机尝试
import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from datetime import datetime

# 加载公钥
with open("public_key.pem", "rb") as public_file:
    public_key = serialization.load_pem_public_key(public_file.read())


# 验证签名
def verify_registration_info(public_key, registration_info, signature):
    # 对注册信息进行哈希处理
    hash_value = hashlib.sha256(registration_info.encode()).digest()
    # 使用公钥验证签名
    try:
        public_key.verify(
            signature,
            hash_value,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH,
            ),
            hashes.SHA256(),
        )
        return True
    except Exception as e:
        print(f"签名验证失败: {e}")
        return False


# 检查注册码是否过期
def is_registration_expired(registration_info):
    # 假设 registration_info 的格式为 "IP#MAC#DATE"
    try:
        _, _, date_str = registration_info.split("#")
        registration_date = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S")
        current_date = datetime.now()
        # 计算日期差
        difference = current_date - registration_date
        if difference.days > 365:
            print("注册码已过期。")
            return True
        else:
            print(f"注册码有效,距离到期还有 {365 - difference.days} 天。")
            return False
    except Exception as e:
        print(f"无法解析注册信息中的日期: {e}")
        return True


# 示例使用
registration_info = "192.168.1.1#00:1A:2B:3C:4D:5E#2023-09-03T12:00:00"
signature = "4747554cb3bc12f1b25f7079f435338f78f8b9a096f7a40fe6f4d22535d5d1038ae0cff10f1f7d648201e585b19b15d45a0b0903886abda9096f7ce3dbba78b3076f7df9cec9f19616512c7dd20a4448ab1eb544be0163e84cb811cf415986551f4be21878c4ca620843e4109d4e625756560f0746dbbccd2f57ee1ba6d8f751bde45839e75229160371c955bc8d19931d647d1281f4ae6baa08dd460dbf3de0d1ac76f2217ed8e81657cce00da6342ef5d453afb0c24da10197896f89347a3dc81a482eab2e41ffe311222e86d0e6a0901ae6cce38e69700d7d4a18c9e902ea802b05292c166d561b4877619283026542e319ca35708fa32e6f86e5615f6fb0"
is_valid = verify_registration_info(
    public_key, registration_info, bytes.fromhex(signature)
)

if is_valid:
    print("注册码验证成功。")
    # 检查注册码是否过期
    is_expired = is_registration_expired(registration_info)
    if not is_expired:
        print("激活成功!")
else:
    print("注册码验证失败。")

UPS软件关闭多台linux服务器的方法

背景

因为我们的施工现场往往是有一到多台服务器的,这些服务器有的是双路电源,有的是只有UPS供电。众所周知,服务器如果遇到突然断电,是有损坏硬盘的风险的,为了避免断电停机的风险,一般会设置UPS来给服务器供电,如果需求支撑长时间供电,甚至会加上储能设备,但是只要不来电,存储的电终究是有用完的时候的,所以我们还额外需要在其中一台服务器上安装一个agent程序。用于和UPS进行通信,实时检查UPS的剩余电量,如果剩余电量不足时,通过ssh关闭机房里的所有服务器

示意图

1.UPS的控制程序winpower下载

接下来的文章,我们将使用山特的linux版本的winpower程序来做配置

下载地址https://www.santak.com.cn/page/santak-downloads.html

我们可以在软件列表找到linux版本的winpower,如果第一页没有就在第二页

2.在linux服务器上安装winpower

在服务器上解压下载下载好的压缩文件Winpower_setup_LinuxAMD64.tar.gz

解压之后进入到该目录下的LinuxAMD64下,里面有一个install.bin文件,这里别急着安装,得先安装几个包,不然会报错

sudo apt install -y libxtst6 libxi6 x11-common libxrender1

安装完后再输入winpower的安装命令,过程中遇到提示一路按回车就可以了

./install.bin -i console

安装完成后,winpower程序会安装在/opt/MonitorSoftware/目录下,建议此时可以重启一次电脑,这样会自动启动agent程序,如果不重启的话,可以手动开启agent,命令如下

sudo systemctl start upsagent.service

3.配置winpower

linux版本的必须进入/opt/MonitorSoftware/目录下然后输入命令./monitor进行启动,注意如果是ssh到服务器上的话,需要用一个支持x11转发的ssh工具,像putty或者xshell,powershell肯定是不行了,可以去下载一个mobaxterm来用。

启动之后,先退出向导界面

然后成为系统管理员,默认密码是空

接下来是打开通讯口设定

我们是需要UPS上接一个RS232转USB的线到服务器上的,要检查linux服务器上的USB的串口信息可以用如下命令,把查出来的USB信息填到winpower的通讯口信息里

ls -l /dev/ttyUSB*

添加完成后,再从菜单栏里,点击自动搜索设备,一般1-2分钟就能搜索到UPS了

接下来是配置关机参数设定

建议配置

1.【允许放电时间】建议勾选。因为实际现场使用来看,这个串口通信并不总是很稳定,如果不勾选,可能agent一直获取不到电池百分比信息,默认是会在市电断开2小时后才关机,而30分钟之后可能UPS都没电了,这不是没用么。所以这个功能和【当电池容量百分比低于80%】可以相辅相成,那个先满足,那个先触发。

2.【低电位立即关机】,【剩余放电时间少于10分】,可选可不选,因为实际现场应该用不到

3.【系统】单选【关闭】。这个必须这样选,因为这个是和下面的【关机前执行档案】是联动的,如果选休眠就没用

4.【关机前执行档案】。我的建议是使用python去写一个程序,然后用pyinstaller打包出来放到这里执行。如果一定想用shell脚本也是可以的,这里如果存在程序调用程序或者配置文件,那需要非常注意路径,因为这个程序是会在/opt/MonitorSoftware/目录下执行。建议如果存在程序调用配置文件,配置文件的路径使用绝对路径。例如我的ups_shutdown程序是使用了paramiko库和os库去查找配置文件、读取需要远程关闭的服务器信息、ssh成功之后按需求去安全关闭服务器上的应用程序,数据库等再执行shutdown的关机命令。

debian12虚拟机导出后再导入无法开机的解决方法

遇到的问题

在迁移debian12虚拟机时,我从esxi导出vmdk和ovf文件,再到另外一台esxi上导入时,出现了找不到硬盘,无法开机的问题,报错:

EFl Virtual disk (0.0)… No compatible bootloader found
EFI VMware Virtual SATA CDROM Drive (0.0)...Mo Media.
EFI Network..

解决方法

1.编辑虚拟机选项,勾选强制执行bios设置

2.重新开机进入bios,选择enter setup

3.选择boot from a file

4.找到硬盘,选择硬盘

5.选择EFI,按回车

6.选择debian

7.选择grubx64.efi,按回车

8.系统跳到grub界面,选择debian进入系统

后续操作,重新设置grub

因为重新导入之后,这个grub信息丢失了,所以我们需要找到efi所在硬盘,并且重新设置grub

# 更新grub设置
update-grub
# 通过df命令找到分区
df -h
# 重新安装grub
grub-install /dev/sda1

docker运行的postgresql优雅关闭

背景

我们的许多项目部署完之后,是会经常关闭或者重启服务器的,比如各种过年过节的日子,停工的日子,这一关机,数据库就容易出问题。我们的postgresql是运行在我自定义的一个debian容器中,因为添加了一些我自定义的定时备份和日志功能,所以运行方式和官方镜像不一致。为了减少数据库因为关机而造成出问题的情况,得想一个优雅关闭的方法(至少不能原地爆炸^_^)

第一个进行不下去的方案(systemctl)

在systemctl里是可以写关机前运行的服务的,但是我遇到了一个问题,我编写的这个服务是在输入关机命令后,先关闭了docker服务才启动这个服务,这会导致数据库关闭命令的时候,因为docker提前关闭而失败,这个方案告终。

命令执行效果是:shutdown–>docker.socket关闭–>pg_shutdown.service关闭

[Unit]
Description=pg_shutdown
DefaultDependencies=no
Before=shutdown.target

[Service]
Type=oneshot

ExecStart=/usr/bin/docker exec -it pg_ctlcluster 14 main stop --m fast

[Install]
WantedBy=shutdown.target

最终采用的方案

既然我们需要在docker关闭时执行命令,那么就得调头去找docker是如何处理接受“终止”信号的方案。根据docker官网的知识,我们可以得知,当触发docker stop或者关机的时候,是会向容器发送一个SIGTERM信号的,我们需要在dockerfile里,修改启动脚本,添加一个“安全关闭”的函数,并且让脚本通过钩子把SIGTERM信号与“安全关闭”函数连在一起。当收到SIGTERM信号时就“安全关闭”

#Dockerfile文件参考


FROM debian:12
# 此处省略安装postgresql过程
....
# 然后放一个docker-entrypoint.sh文件进去
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

#docker-entrypoint.sh文件参考


#!/usr/bin/env bash
file_path="/var/run/postgresql/.s.PGSQL.5432.lock"
if [ -f "$file_path" ]; then
    rm "$file_path"
fi
service cron start
pg_ctlcluster 14 main start

stop_container() {
  mkdir -p /var/lib/postgresql/14/main/log/
  echo "$(date) - Stopping PostgreSQL..." >> /var/lib/postgresql/14/main/log/stop.log
  pg_ctlcluster 14 main stop --m fast
  echo "$(date) - PostgreSQL stopped." >> /var/lib/postgresql/14/main/log/stop.log
}
trap 'stop_container' SIGTERM
tail -f /var/log/postgresql/*.log &
wait

这边有一点要注意的是,结尾需要加& wait 而不能只用tail ,不然stop_container不会触发的

但是到这里还没有完全结束,因为postgresql的关闭有时候不那么快,默认的docker 关闭的超时时间是10秒,为了求稳,我们还需要修改docker运行容器时的超时时间,在docker run的时候增加参数–stop-timeout=60,注意,这个超时时间只能在docker run容器的时候去设置,通过修改/etc/docker/daemon.json里的shutdown-timeout是无效的

最后

经过一番修改与重新编写程序,我又测试了多次poweroff和reboot检查,确定稳如老狗之后,终于将其发布至现场,以后可以安心过节啦。虽然看起来修改的代码不多,但是要知道在那修改,如何修改有效,总共花了我一天的时间($_$)

在Kubernetes里使用Traefik插件实现IP黑名单功能

背景

网上关于traefik的ip黑名单功能的文章几乎没有,ChatGPT讲解的也不太对,于是我根据自己的使用经验记录下在kubernetes里,使用traefik的denyip插件,配置IP黑名单功能,可以对单独的ingress(域名)生效,也可以对整个entrypoint(端口)生效


traefik的denyip安装

1.去traefik插件中间,找到denyip插件的信息

https://plugins.traefik.io/plugins/62947363ffc0cd18356a97d1/deny-ip-plugin

2.参考traefik官方helmchart编写values-traefik.yaml文件(https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml)

# 这里只贴下载插件模块的代码
experimental:
  plugins:
    denyip:
      moduleName: github.com/kevtainer/denyip
      version: v1.0.0

3.安装traefik

helm upgrade --install traefik -n traefik -f values-traefik.yaml traefik/traefik --version 26.0.0
#安装完之后,可以检查pod,会发现在pod配置里添加上了插件信息
kubectl describe pod -n traefik traefik-txxqm

对整个entrypoint限制IP访问(例如限制IP访问80端口下的所有域名)

1.添加middleware.yaml文件,例如禁用192.168.1.1的IP访问

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
    name: denyip
    namespace: traefik
spec:
    plugin:
        denyip:
            ipDenyList:
                - 192.168.1.1

添加完后,使用kubectl apply -f middleware.yaml命令使其生效

2.编辑values-traefik.yaml文件,修改ports部分,找到80端口部分,修改代码如下,只生效于80端口,443端口不生效。

ports:
  web:
    port: 80
    expose: true
    exposedPort: 80 # 对外的 HTTP 端口号,使用标准端口号在国内需备案
    middlewares:
     - traefik-denyip@kubernetescrd
  websecure:
    port: 443
    expose: true
    exposedPort: 443 # 对外的 HTTPS 端口号,使用标准端口号在国内需备案

3.再次安装traeifk

helm upgrade –install traefik -n traefik -f values-traefik.yaml traefik/traefik –version 26.0.0

4,去dashboard页面检查,会发现在这个entrypoint的所有ingress的配置里,都会加上该middleware


对单一IngressRoute生效(例如对指定域名或路径)

1.添加middleware.yaml文件,例如禁用192.168.1.1的IP访问

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
    name: denyip
    namespace: traefik
spec:
    plugin:
        denyip:
            ipDenyList:
                - 192.168.1.1

添加完后,使用kubectl apply -f middleware.yaml命令使其生效

2.这里有一个注意点,往往我们的ingress和middleware不一定在同一个namespace,这个时候需要在安装traefik的时候启用【允许使用跨命名空间】功能,修改values-traefik.yaml,添加如下配置:

providers:
  kubernetesCRD:
    allowCrossNamespace: true

然后再次安装

helm upgrade --install traefik -n traefik -f values-traefik.yaml traefik/traefik --version 26.0.0

3.修改需要使用middleware的ingressroute文件,和service同级增加middlewares信息

4.修改完成后,再去dashboard页面检查,会发现只有该http route有middleware信息,不会影响其他域名的正常访问


苏ICP备18047533号-1