二進制搭建k8s

實驗環境:

k8s集群master01:192.168.1.11

k8s集群master02:192.168.1.22

master虛擬ip:192.168.1.100

k8s集群node01:192.168.1.33

k8s集群node01:192.168.1.44

nginx+keepalive01(master):192.168.1.55

nginx+keepalive02(backup):192.168.1.66

在所有機器上初始化系統:

systemctl stop firewalld
systemctl disable firewalld
iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X#關閉selinux
setenforce 0
sed -i 's/enforcing/disabled/' /etc/selinux/config#關閉swap
swapoff -a
sed -ri 's/.*swap.*/#&/' /etc/fstab #分別設置主機名
hostnamectl set-hostname master01
hostnamectl set-hostname master02
hostnamectl set-hostname node01
hostnamectl set-hostname node02#添加hosts文件
cat >> /etc/hosts << EOF
192.168.1.11 master01
192.168.1.22 master02
192.168.1.33 node01
192.168.1.44 node02
EOF#調整內核參數
cat > /etc/sysctl.d/k8s.conf << EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv6.conf.all.disable_ipv6=1
net.ipv4.ip_forward=1
EOF
#開啟網橋模式,可將網橋的流量傳遞給iptables鏈
#關閉ipv6協議
sysctl --system#時間同步
yum install ntpdate -y
ntpdate time.windows.com

在所有node節點部署docker:

yum install -y yum-utils device-mapper-persistent-data lvm2 
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 
yum install -y docker-ce docker-ce-cli containerd.io
systemctl start docker.service
systemctl enable docker.service 

部署etcd集群:

準備cfssl證書生成工具cfssl、cfssljson、cfssl-certinfo放在master01上的/usr/local/bin/下并賦予執行權限

wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -O /usr/local/bin/cfssl
wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -O /usr/local/bin/cfssljson
wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -O /usr/local/bin/cfssl-certinfo
chmod +x /usr/local/bin/cfssl*

創建/opt/k8s/目錄并使用腳本生成CA證書、etcd服務器證書以及私鑰

mkdir /opt/k8s
cd /opt/k8s/
chmod +x etcd-cert.sh etcd.sh
mkdir /opt/k8s/etcd-cert
mv etcd-cert.sh etcd-cert/
cd /opt/k8s/etcd-cert/
./etcd-cert.sh

etcd-cert.sh腳本:

#!/bin/bash# 配置證書生成策略
cat > ca-config.json <<EOF
{"signing": {"default": {"expiry": "87600h"},"profiles": {"www": {"expiry": "87600h","usages": ["signing","key encipherment","server auth","client auth"]}}}
}
EOF# 生成根證書的請求文件
cat > ca-csr.json <<EOF
{"CN": "etcd","key": {"algo": "rsa","size": 2048},"names": [{"C": "CN","L": "Beijing","ST": "Beijing"}]
}
EOF# 使用 CSR 文件生成根證書和私鑰
cfssl gencert -initca ca-csr.json | cfssljson -bare ca# 生成服務器證書的請求文件
cat > server-csr.json <<EOF
{"CN": "etcd","hosts": ["192.168.1.11","192.168.1.33","192.168.1.44"],"key": {"algo": "rsa","size": 2048},"names": [{"C": "CN","L": "BeiJing","ST": "BeiJing"}]
}
EOF# 使用根證書簽發服務器證書和私鑰
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=www server-csr.json | cfssljson -bare server

上傳etcd-v3.4.9-linux-amd64.tar.gz 到/opt/k8s/后啟動etcd服務

cd /opt/k8s/
tar zxvf etcd-v3.4.9-linux-amd64.tar.gz
mkdir -p /opt/etcd/{cfg,bin,ssl}
cd /opt/k8s/etcd-v3.4.9-linux-amd64/
mv etcd etcdctl /opt/etcd/bin/
cp /opt/k8s/etcd-cert/*.pem /opt/etcd/ssl/
cd /opt/k8s/
./etcd.sh etcd01 192.168.1.11 etcd02=https://192.168.1.33:2380,etcd03=https://192.168.1.44:2380

把etcd相關證書文件、命令文件和服務管理文件全部拷貝到另外兩個etcd集群節點

scp -r /opt/etcd/ root@192.168.1.33:/opt/
scp -r /opt/etcd/ root@192.168.1.44:/opt/
scp /usr/lib/systemd/system/etcd.service root@192.168.1.33:/usr/lib/systemd/system/
scp /usr/lib/systemd/system/etcd.service root@192.168.1.44:/usr/lib/systemd/system/

在node節點分別修改etcd配置文件并啟動etcd

vim /opt/etcd/cfg/etcd
systemctl start etcd
systemctl enable etcd 
systemctl status etcd

檢查etcd集群狀態

ETCDCTL_API=3   /opt/etcd/bin/etcdctl --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem --endpoints="https://192.168.1.11:2379,https://192.168.1.33:2379,https://192.168.1.44:2379" endpoint health --write-out=table

這個命令用于檢查 etcd 集群中各個節點的健康狀態,并以表格形式輸出。

  • ETCDCTL_API=3:設置 etcdctl 的 API 版本為 3。
  • /opt/etcd/bin/etcdctl:指定 etcdctl 的路徑。
  • --cacert=/opt/etcd/ssl/ca.pem:指定根證書文件的路徑,用于驗證 etcd 服務器證書的有效性。
  • --cert=/opt/etcd/ssl/server.pem:指定客戶端證書的路徑,用于與 etcd 服務器進行雙向身份驗證。
  • --key=/opt/etcd/ssl/server-key.pem:指定客戶端私鑰的路徑,用于與 etcd 服務器進行雙向身份驗證。
  • --endpoints="https://192.168.1.11:2379,https://192.168.1.33:2379,https://192.168.1.44:2379":指定 etcd 集群的各個節點的地址和端口。
  • endpoint health:檢查 etcd 集群中各個節點的健康狀態。
  • --write-out=table:以表格形式輸出結果。
ETCDCTL_API=3 /opt/etcd/bin/etcdctl --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem --endpoints="https://192.168.1.11:2379,https://192.168.1.33:2379,https://192.168.1.44:2379" endpoint status --write-out=table

這個命令用于獲取 etcd 集群中各個節點的狀態,并以表格形式輸出。

  • ETCDCTL_API=3:設置 etcdctl 的 API 版本為 3。
  • /opt/etcd/bin/etcdctl:指定 etcdctl 的路徑。
  • --cacert=/opt/etcd/ssl/ca.pem:指定根證書文件的路徑,用于驗證 etcd 服務器證書的有效性。
  • --cert=/opt/etcd/ssl/server.pem:指定客戶端證書的路徑,用于與 etcd 服務器進行雙向身份驗證。
  • --key=/opt/etcd/ssl/server-key.pem:指定客戶端私鑰的路徑,用于與 etcd 服務器進行雙向身份驗證。
  • --endpoints="https://192.168.1.11:2379,https://192.168.1.33:2379,https://192.168.1.44:2379":指定 etcd 集群的各個節點的地址和端口。
  • endpoint status:獲取 etcd 集群中各個節點的狀態。
  • --write-out=table:以表格形式輸出結果。
ETCDCTL_API=3 /opt/etcd/bin/etcdctl --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem --endpoints="https://192.168.1.11:2379,https://192.168.1.33:2379,https://192.168.1.44:2379" --write-out=table member list

這個命令用于列出 etcd 集群中的成員,并以表格形式輸出。

  • ETCDCTL_API=3:設置 etcdctl 的 API 版本為 3。
  • /opt/etcd/bin/etcdctl:指定 etcdctl 的路徑。
  • --cacert=/opt/etcd/ssl/ca.pem:指定根證書文件的路徑,用于驗證 etcd 服務器證書的有效性。
  • --cert=/opt/etcd/ssl/server.pem:指定客戶端證書的路徑,用于與 etcd 服務器進行雙向身份驗證。
  • --key=/opt/etcd/ssl/server-key.pem:指定客戶端私鑰的路徑,用于與 etcd 服務器進行雙向身份驗證。
  • --endpoints="https://192.168.1.11:2379,https://192.168.1.33:2379,https://192.168.1.44:2379":指定 etcd 集群的各個節點的地址和端口。
  • member list:列出 etcd 集群中的成員。
  • --write-out=table:以表格形式輸出結果。

部署master:

在/opt/k8s/目錄中準備admin.sh、apiserver.sh、controller-manager.sh、scheduler.sh、k8s-sert.sh,kubernetes-server-linux-amd64.tar.gz

admin.sh

#!/bin/bash
mkdir /root/.kube
KUBE_CONFIG="/root/.kube/config"
KUBE_APISERVER="https://192.168.1.11:6443"cd /opt/k8s/k8s-cert/kubectl config set-cluster kubernetes \--certificate-authority=/opt/kubernetes/ssl/ca.pem \--embed-certs=true \--server=${KUBE_APISERVER} \--kubeconfig=${KUBE_CONFIG}
kubectl config set-credentials cluster-admin \--client-certificate=./admin.pem \--client-key=./admin-key.pem \--embed-certs=true \--kubeconfig=${KUBE_CONFIG}
kubectl config set-context default \--cluster=kubernetes \--user=cluster-admin \--kubeconfig=${KUBE_CONFIG}
kubectl config use-context default --kubeconfig=${KUBE_CONFIG}

apiserver.sh

#!/bin/bash
#example: apiserver.sh your_master_ip https://your_etcd01_ip:2379,https://your_etcd02_ip:2379,https://your_etcd03_ip:2379
#創建 kube-apiserver 啟動參數配置文件
MASTER_ADDRESS=$1
ETCD_SERVERS=$2cat >/opt/kubernetes/cfg/kube-apiserver <<EOF
KUBE_APISERVER_OPTS="--logtostderr=false  \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--etcd-servers=${ETCD_SERVERS} \\
--bind-address=${MASTER_ADDRESS} \\
--secure-port=6443 \\
--advertise-address=${MASTER_ADDRESS} \\
--allow-privileged=true \\
--service-cluster-ip-range=10.0.0.0/24 \\
--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota,NodeRestriction \\
--authorization-mode=RBAC,Node \\
--enable-bootstrap-token-auth=true \\
--token-auth-file=/opt/kubernetes/cfg/token.csv \\
--service-node-port-range=30000-50000 \\
--kubelet-client-certificate=/opt/kubernetes/ssl/apiserver.pem \\
--kubelet-client-key=/opt/kubernetes/ssl/apiserver-key.pem \\
--tls-cert-file=/opt/kubernetes/ssl/apiserver.pem  \\
--tls-private-key-file=/opt/kubernetes/ssl/apiserver-key.pem \\
--client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--service-account-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--service-account-issuer=api \\
--service-account-signing-key-file=/opt/kubernetes/ssl/apiserver-key.pem \\
--etcd-cafile=/opt/etcd/ssl/ca.pem \\
--etcd-certfile=/opt/etcd/ssl/server.pem \\
--etcd-keyfile=/opt/etcd/ssl/server-key.pem \\
--requestheader-client-ca-file=/opt/kubernetes/ssl/ca.pem \\
--proxy-client-cert-file=/opt/kubernetes/ssl/apiserver.pem \\
--proxy-client-key-file=/opt/kubernetes/ssl/apiserver-key.pem \\
--requestheader-allowed-names=kubernetes \\
--requestheader-extra-headers-prefix=X-Remote-Extra- \\
--requestheader-group-headers=X-Remote-Group \\
--requestheader-username-headers=X-Remote-User \\
--enable-aggregator-routing=true \\
--audit-log-maxage=30 \\
--audit-log-maxbackup=3 \\
--audit-log-maxsize=100 \\
--audit-log-path=/opt/kubernetes/logs/k8s-audit.log"
EOF#--logtostderr=true:啟用日志。輸出日志到標準錯誤控制臺,不輸出到文件
#--v=4:日志等級。指定輸出日志的級別,v=4為調試級別詳細輸出
#--etcd-servers:etcd集群地址。指定etcd服務器列表(格式://ip:port),逗號分隔
#--bind-address:監聽地址。指定 HTTPS 安全接口的監聽地址,默認值0.0.0.0
#--secure-port:https安全端口。指定 HTTPS 安全接口的監聽端口,默認值6443
#--advertise-address:集群通告地址。通過該 ip 地址向集群其他節點公布 api server 的信息,必須能夠被其他節點訪問
#--allow-privileged=true:啟用授權。允許擁有系統特權的容器運行,默認值false
#--service-cluster-ip-range:Service虛擬IP地址段。指定 Service Cluster IP 地址段
#--enable-admission-plugins:準入控制模塊。kuberneres集群的準入控制機制,各控制模塊以插件的形式依次生效,集群時必須包含ServiceAccount,運行在認證(Authentication)、授權(Authorization)之后,Admission Control是權限認證鏈上的最后一環, 對請求API資源對象進行修改和校驗
#--authorization-mode:認證授權,啟用RBAC授權和節點自管理。在安全端口使用RBAC,Node授權模式,未通過授權的請求拒絕,默認值AlwaysAllow。RBAC是用戶通過角色與權限進行關聯的模式;Node模式(節點授權)是一種特殊用途的授權模式,專門授權由kubelet發出的API請求,在進行認證時,先通過用戶名、用戶分組驗證是否是集群中的Node節點,只有是Node節點的請求才能使用Node模式授權
#--enable-bootstrap-token-auth:啟用TLS bootstrap機制。在apiserver上啟用Bootstrap Token 認證
#--token-auth-file=/opt/kubernetes/cfg/token.csv:指定bootstrap token認證文件路徑
#--service-node-port-range:指定 Service  NodePort 的端口范圍,默認值30000-32767
#–-kubelet-client-xxx:apiserver訪問kubelet客戶端證書
#--tls-xxx-file:apiserver https證書
#1.20版本必須加的參數:–-service-account-issuer,–-service-account-signing-key-file
#--etcd-xxxfile:連接Etcd集群證書
#–-audit-log-xxx:審計日志
#啟動聚合層相關配置:–requestheader-client-ca-file,–proxy-client-cert-file,–proxy-client-key-file,–requestheader-allowed-names,–requestheader-extra-headers-prefix,–requestheader-group-headers,–requestheader-username-headers,–enable-aggregator-routing#創建 kube-apiserver.service 服務管理文件
cat >/usr/lib/systemd/system/kube-apiserver.service <<EOF
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes[Service]
EnvironmentFile=-/opt/kubernetes/cfg/kube-apiserver
ExecStart=/opt/kubernetes/bin/kube-apiserver \$KUBE_APISERVER_OPTS
Restart=on-failure[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable kube-apiserver
systemctl restart kube-apiserver

controller-manager.sh

#!/bin/bash
##創建 kube-controller-manager 啟動參數配置文件
MASTER_ADDRESS=$1cat >/opt/kubernetes/cfg/kube-controller-manager <<EOF
KUBE_CONTROLLER_MANAGER_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--leader-elect=true \\
--kubeconfig=/opt/kubernetes/cfg/kube-controller-manager.kubeconfig \\
--bind-address=127.0.0.1 \\
--allocate-node-cidrs=true \\
--cluster-cidr=10.244.0.0/16 \\
--service-cluster-ip-range=10.0.0.0/24 \\
--cluster-signing-cert-file=/opt/kubernetes/ssl/ca.pem \\
--cluster-signing-key-file=/opt/kubernetes/ssl/ca-key.pem  \\
--root-ca-file=/opt/kubernetes/ssl/ca.pem \\
--service-account-private-key-file=/opt/kubernetes/ssl/ca-key.pem \\
--cluster-signing-duration=87600h0m0s"
EOF#––leader-elect:當該組件啟動多個時,自動選舉(HA)
#-–kubeconfig:連接 apiserver 用的配置文件,用于識別 k8s 集群
#--cluster-cidr=10.244.0.0/16:pod資源的網段,需與pod網絡插件的值設置一致。通常,Flannel網絡插件的默認為10.244.0.0/16,Calico插件的默認值為192.168.0.0/16
#--cluster-signing-cert-file/–-cluster-signing-key-file:自動為kubelet頒發證書的CA,與apiserver保持一致。指定簽名的CA機構根證書,用來簽名為 TLS BootStrapping 創建的證書和私鑰
#--root-ca-file:指定根CA證書文件路徑,用來對 kube-apiserver 證書進行校驗,指定該參數后,才會在 Pod 容器的 ServiceAccount 中放置該 CA 證書文件
#--experimental-cluster-signing-duration:設置為 TLS BootStrapping 簽署的證書有效時間為10年,默認為1年##生成kube-controller-manager證書
cd /opt/k8s/k8s-cert/
#創建證書請求文件
cat > kube-controller-manager-csr.json << EOF
{"CN": "system:kube-controller-manager","hosts": [],"key": {"algo": "rsa","size": 2048},"names": [{"C": "CN","L": "BeiJing", "ST": "BeiJing","O": "system:masters","OU": "System"}]
}
EOF#生成證書
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager#生成kubeconfig文件
KUBE_CONFIG="/opt/kubernetes/cfg/kube-controller-manager.kubeconfig"
KUBE_APISERVER="https://192.168.1.11:6443"kubectl config set-cluster kubernetes \--certificate-authority=/opt/kubernetes/ssl/ca.pem \--embed-certs=true \--server=${KUBE_APISERVER} \--kubeconfig=${KUBE_CONFIG}
kubectl config set-credentials kube-controller-manager \--client-certificate=./kube-controller-manager.pem \--client-key=./kube-controller-manager-key.pem \--embed-certs=true \--kubeconfig=${KUBE_CONFIG}
kubectl config set-context default \--cluster=kubernetes \--user=kube-controller-manager \--kubeconfig=${KUBE_CONFIG}
kubectl config use-context default --kubeconfig=${KUBE_CONFIG}##創建 kube-controller-manager.service 服務管理文件
cat >/usr/lib/systemd/system/kube-controller-manager.service <<EOF
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/kubernetes/kubernetes[Service]
EnvironmentFile=-/opt/kubernetes/cfg/kube-controller-manager
ExecStart=/opt/kubernetes/bin/kube-controller-manager \$KUBE_CONTROLLER_MANAGER_OPTS
Restart=on-failure[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable kube-controller-manager
systemctl restart kube-controller-manager

scheduler.sh

#!/bin/bash
##創建 kube-scheduler 啟動參數配置文件
MASTER_ADDRESS=$1cat >/opt/kubernetes/cfg/kube-scheduler <<EOF
KUBE_SCHEDULER_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--leader-elect=true \\
--kubeconfig=/opt/kubernetes/cfg/kube-scheduler.kubeconfig \\
--bind-address=127.0.0.1"
EOF#-–kubeconfig:連接 apiserver 用的配置文件,用于識別 k8s 集群
#--leader-elect=true:當該組件啟動多個時,自動啟動 leader 選舉
#k8s中Controller-Manager和Scheduler的選主邏輯:k8s中的etcd是整個集群所有狀態信息的存儲,涉及數據的讀寫和多個etcd之間數據的同步,對數據的一致性要求嚴格,所以使用較復雜的 raft 算法來選擇用于提交數據的主節點。而 apiserver 作為集群入口,本身是無狀態的web服務器,多個 apiserver 服務之間直接負載請求并不需要做選主。Controller-Manager 和 Scheduler 作為任務類型的組件,比如 controller-manager 內置的 k8s 各種資源對象的控制器實時的 watch apiserver 獲取對象最新的變化事件做期望狀態和實際狀態調整,調度器watch未綁定節點的pod做節點選擇,顯然多個這些任務同時工作是完全沒有必要的,所以 controller-manager 和 scheduler 也是需要選主的,但是選主邏輯和 etcd 不一樣的,這里只需要保證從多個 controller-manager 和 scheduler 之間選出一個 leader 進入工作狀態即可,而無需考慮它們之間的數據一致和同步。##生成kube-scheduler證書
cd /opt/k8s/k8s-cert/
#創建證書請求文件
cat > kube-scheduler-csr.json << EOF
{"CN": "system:kube-scheduler","hosts": [],"key": {"algo": "rsa","size": 2048},"names": [{"C": "CN","L": "BeiJing","ST": "BeiJing","O": "system:masters","OU": "System"}]
}
EOF#生成證書
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-scheduler-csr.json | cfssljson -bare kube-scheduler#生成kubeconfig文件
KUBE_CONFIG="/opt/kubernetes/cfg/kube-scheduler.kubeconfig"
KUBE_APISERVER="https://192.168.1.11:6443"kubectl config set-cluster kubernetes \--certificate-authority=/opt/kubernetes/ssl/ca.pem \--embed-certs=true \--server=${KUBE_APISERVER} \--kubeconfig=${KUBE_CONFIG}
kubectl config set-credentials kube-scheduler \--client-certificate=./kube-scheduler.pem \--client-key=./kube-scheduler-key.pem \--embed-certs=true \--kubeconfig=${KUBE_CONFIG}
kubectl config set-context default \--cluster=kubernetes \--user=kube-scheduler \--kubeconfig=${KUBE_CONFIG}
kubectl config use-context default --kubeconfig=${KUBE_CONFIG}##創建 kube-scheduler.service 服務管理文件
cat >/usr/lib/systemd/system/kube-scheduler.service <<EOF
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/kubernetes/kubernetes[Service]
EnvironmentFile=-/opt/kubernetes/cfg/kube-scheduler
ExecStart=/opt/kubernetes/bin/kube-scheduler \$KUBE_SCHEDULER_OPTS
Restart=on-failure[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable kube-scheduler
systemctl restart kube-scheduler

k8s-sert.sh

#!/bin/bash
cat > ca-config.json <<EOF
{"signing": {"default": {"expiry": "87600h"},"profiles": {"kubernetes": {"expiry": "87600h","usages": ["signing","key encipherment","server auth","client auth"]}}}
}
EOF#生成CA證書和私鑰(根證書和私鑰)
cat > ca-csr.json <<EOF
{"CN": "kubernetes","key": {"algo": "rsa","size": 2048},"names": [{"C": "CN","L": "Beijing","ST": "Beijing","O": "k8s","OU": "System"}]
}
EOFcfssl gencert -initca ca-csr.json | cfssljson -bare ca -#-----------------------
#生成 apiserver 的證書和私鑰(apiserver和其它k8s組件通信使用)
#hosts中將所有可能作為 apiserver 的 ip 添加進去,后面 keepalived 使用的 VIP 也要加入
cat > apiserver-csr.json <<EOF
{"CN": "kubernetes","hosts": ["10.0.0.1","127.0.0.1","192.168.1.11","192.168.1.22","192.168.1.100","192.168.1.55","192.168.1.66","kubernetes","kubernetes.default","kubernetes.default.svc","kubernetes.default.svc.cluster","kubernetes.default.svc.cluster.local"],"key": {"algo": "rsa","size": 2048},"names": [{"C": "CN","L": "BeiJing","ST": "BeiJing","O": "k8s","OU": "System"}]
}
EOFcfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes apiserver-csr.json | cfssljson -bare apiserver#-----------------------
#生成 kubectl 連接集群的證書和私鑰,具有admin權限
cat > admin-csr.json <<EOF
{"CN": "admin","hosts": [],"key": {"algo": "rsa","size": 2048},"names": [{"C": "CN","L": "BeiJing","ST": "BeiJing","O": "system:masters","OU": "System"}]
}
EOFcfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes admin-csr.json | cfssljson -bare admin#-----------------------
#生成 kube-proxy 的證書和私鑰
cat > kube-proxy-csr.json <<EOF
{"CN": "system:kube-proxy","hosts": [],"key": {"algo": "rsa","size": 2048},"names": [{"C": "CN","L": "BeiJing","ST": "BeiJing","O": "k8s","OU": "System"}]
}
EOFcfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-proxy-csr.json | cfssljson -bare kube-proxy
chmod +x *.sh
mkdir -p /opt/kubernetes/{bin,cfg,ssl,logs}
mkdir /opt/k8s/k8s-cert
mv /opt/k8s/k8s-cert.sh /opt/k8s/k8s-cert
cd /opt/k8s/k8s-cert/
./k8s-cert.sh

cp ca*pem apiserver*pem /opt/kubernetes/ssl/
cd /opt/k8s/
tar zxvf kubernetes-server-linux-amd64.tar.gz
cd /opt/k8s/kubernetes/server/bin
cp kube-apiserver kubectl kube-controller-manager kube-scheduler /opt/kubernetes/bin/
ln -s /opt/kubernetes/bin/* /usr/local/bin/
cd /opt/k8s/
vim token.sh
#!/bin/bash
#獲取隨機數前16個字節內容,以十六進制格式輸出,并刪除其中空格
BOOTSTRAP_TOKEN=$(head -c 16 /dev/urandom | od -An -t x | tr -d ' ') 
#生成 token.csv 文件,按照 Token序列號,用戶名,UID,用戶組 的格式生成
cat > /opt/kubernetes/cfg/token.csv <<EOF
${BOOTSTRAP_TOKEN},kubelet-bootstrap,10001,"system:kubelet-bootstrap"
EOF
chmod +x token.sh
./token.sh
cat /opt/kubernetes/cfg/token.csv
cd /opt/k8s/
./apiserver.sh 192.168.1.11 https://192.168.1.11:2379,https://192.168.1.33:2379,https://192.168.1.44:2379
ps aux | grep kube-apiserver
netstat -natp | grep 6443
cd /opt/k8s/
#啟動 scheduler 服務
./scheduler.sh
ps aux | grep kube-scheduler
#啟動 controller-manager 服務
./controller-manager.sh
ps aux | grep kube-controller-manager
#生成kubectl連接集群的kubeconfig文件
./admin.sh
#通過kubectl工具查看當前集群組件狀態
kubectl get cs

部署node:

在所有node操作

創建kubernetes工作目錄

mkdir -p /opt/kubernetes/{bin,cfg,ssl,logs}

準備kubelet.sh、proxy.sh

kubelet.sh

#!/bin/bashNODE_ADDRESS=$1
DNS_SERVER_IP=${2:-"10.0.0.2"}#創建 kubelet 啟動參數配置文件
cat >/opt/kubernetes/cfg/kubelet <<EOF
KUBELET_OPTS="--logtostderr=false \\
--v=2 \\
--log-dir=/opt/kubernetes/logs \\
--hostname-override=${NODE_ADDRESS} \\
--network-plugin=cni \\
--kubeconfig=/opt/kubernetes/cfg/kubelet.kubeconfig \\
--bootstrap-kubeconfig=/opt/kubernetes/cfg/bootstrap.kubeconfig \\
--config=/opt/kubernetes/cfg/kubelet.config \\
--cert-dir=/opt/kubernetes/ssl \\
--pod-infra-container-image=registry.cn-hangzhou.aliyuncs.com/google-containers/pause-amd64:3.0"
EOF#--hostname-override:指定kubelet節點在集群中顯示的主機名或IP地址,默認使用主機hostname;kube-proxy和kubelet的此項參數設置必須完全一致
#--network-plugin:啟用CNI
#--kubeconfig:指定kubelet.kubeconfig文件位置,當前為空路徑,會自動生成,用于如何連接到apiserver,里面含有kubelet證書,master授權完成后會在node節點上生成 kubelet.kubeconfig 文件
#--bootstrap-kubeconfig:指定連接 apiserver 的 bootstrap.kubeconfig 文件
#--config:指定kubelet配置文件的路徑,啟動kubelet時將從此文件加載其配置
#--cert-dir:指定master頒發的kubelet證書生成目錄
#--pod-infra-container-image:指定Pod基礎容器(Pause容器)的鏡像。Pod啟動的時候都會啟動一個這樣的容器,每個pod之間相互通信需要Pause的支持,啟動Pause需要Pause基礎鏡像#----------------------
#創建kubelet配置文件(該文件實際上就是一個yml文件,語法非常嚴格,不能出現tab鍵,冒號后面必須要有空格,每行結尾也不能有空格)
cat >/opt/kubernetes/cfg/kubelet.config <<EOF
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
address: ${NODE_ADDRESS}
port: 10250
readOnlyPort: 10255
cgroupDriver: cgroupfs
clusterDNS:
- ${DNS_SERVER_IP} 
clusterDomain: cluster.local
failSwapOn: false
authentication:anonymous:enabled: true
EOF#PS:當命令行參數與此配置文件(kubelet.config)有相同的值時,就會覆蓋配置文件中的該值。#----------------------
#創建 kubelet.service 服務管理文件
cat >/usr/lib/systemd/system/kubelet.service <<EOF
[Unit]
Description=Kubernetes Kubelet
After=docker.service
Requires=docker.service[Service]
EnvironmentFile=/opt/kubernetes/cfg/kubelet
ExecStart=/opt/kubernetes/bin/kubelet \$KUBELET_OPTS
Restart=on-failure
KillMode=process[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable kubelet
systemctl restart kubelet

proxy.sh

#!/bin/bashNODE_ADDRESS=$1#創建 kube-proxy 啟動參數配置文件
cat >/opt/kubernetes/cfg/kube-proxy <<EOF
KUBE_PROXY_OPTS="--logtostderr=true \\
--v=4 \\
--hostname-override=${NODE_ADDRESS} \\
--cluster-cidr=172.17.0.0/16 \\
--proxy-mode=ipvs \\
--kubeconfig=/opt/kubernetes/cfg/kube-proxy.kubeconfig"
EOF#--hostnameOverride: 參數值必須與 kubelet 的值一致,否則 kube-proxy 啟動后會找不到該 Node,從而不會創建任何 ipvs 規則
#--cluster-cidr:指定 Pod 網絡使用的聚合網段,Pod 使用的網段和 apiserver 中指定的 service 的 cluster ip 網段不是同一個網段。 kube-proxy 根據 --cluster-cidr 判斷集群內部和外部流量,指定 --cluster-cidr 選項后 kube-proxy 才會對訪問 Service IP 的請求做 SNAT,即來自非 Pod 網絡的流量被當成外部流量,訪問 Service 時需要做 SNAT。
#--proxy-mode:指定流量調度模式為ipvs模式,可添加--ipvs-scheduler選項指定ipvs調度算法(rr|wrr|lc|wlc|lblc|lblcr|dh|sh|sed|nq)
#--kubeconfig: 指定連接 apiserver 的 kubeconfig 文件	#----------------------
#創建 kube-proxy.service 服務管理文件
cat >/usr/lib/systemd/system/kube-proxy.service <<EOF
[Unit]
Description=Kubernetes Proxy
After=network.target[Service]
EnvironmentFile=-/opt/kubernetes/cfg/kube-proxy
ExecStart=/opt/kubernetes/bin/kube-proxy \$KUBE_PROXY_OPTS
Restart=on-failure[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable kube-proxy
systemctl restart kube-proxy
chmod +x kubelet.sh proxy.sh

在 master01 節點上操作

cd /opt/k8s/kubernetes/server/bin
scp kubelet kube-proxy root@192.168.1.33:/opt/kubernetes/bin/
scp kubelet kube-proxy root@192.168.1.44:/opt/kubernetes/bin/
mkdir /opt/k8s/kubeconfig
cd /opt/k8s/kubeconfig

在/opt/k8s/kubeconfig/目錄下準備kubeconfig.sh

kubeconfig.sh

#!/bin/bash
#example: kubeconfig 192.168.1.11 /opt/k8s/k8s-cert/
#創建bootstrap.kubeconfig文件
#該文件中內置了 token.csv 中用戶的 Token,以及 apiserver CA 證書;kubelet 首次啟動會加載此文件,使用 apiserver CA 證書建立與 apiserver 的 TLS 通訊,使用其中的用戶 Token 作為身份標識向 apiserver 發起 CSR 請求BOOTSTRAP_TOKEN=$(awk -F ',' '{print $1}' /opt/kubernetes/cfg/token.csv)
APISERVER=$1
SSL_DIR=$2export KUBE_APISERVER="https://$APISERVER:6443"# 設置集群參數
kubectl config set-cluster kubernetes \--certificate-authority=$SSL_DIR/ca.pem \--embed-certs=true \--server=${KUBE_APISERVER} \--kubeconfig=bootstrap.kubeconfig
#--embed-certs=true:表示將ca.pem證書寫入到生成的bootstrap.kubeconfig文件中# 設置客戶端認證參數,kubelet 使用 bootstrap token 認證
kubectl config set-credentials kubelet-bootstrap \--token=${BOOTSTRAP_TOKEN} \--kubeconfig=bootstrap.kubeconfig# 設置上下文參數
kubectl config set-context default \--cluster=kubernetes \--user=kubelet-bootstrap \--kubeconfig=bootstrap.kubeconfig# 使用上下文參數生成 bootstrap.kubeconfig 文件
kubectl config use-context default --kubeconfig=bootstrap.kubeconfig#----------------------#創建kube-proxy.kubeconfig文件
# 設置集群參數
kubectl config set-cluster kubernetes \--certificate-authority=$SSL_DIR/ca.pem \--embed-certs=true \--server=${KUBE_APISERVER} \--kubeconfig=kube-proxy.kubeconfig# 設置客戶端認證參數,kube-proxy 使用 TLS 證書認證
kubectl config set-credentials kube-proxy \--client-certificate=$SSL_DIR/kube-proxy.pem \--client-key=$SSL_DIR/kube-proxy-key.pem \--embed-certs=true \--kubeconfig=kube-proxy.kubeconfig# 設置上下文參數
kubectl config set-context default \--cluster=kubernetes \--user=kube-proxy \--kubeconfig=kube-proxy.kubeconfig# 使用上下文參數生成 kube-proxy.kubeconfig 文件
kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig
chmod +x kubeconfig.sh
./kubeconfig.sh 192.168.1.11 /opt/k8s/k8s-cert/
scp bootstrap.kubeconfig kube-proxy.kubeconfig root@192.168.1.33:/opt/kubernetes/cfg/
scp bootstrap.kubeconfig kube-proxy.kubeconfig root@192.168.1.44:/opt/kubernetes/cfg/
kubectl create clusterrolebinding kubelet-bootstrap --clusterrole=system:node-bootstrapper --user=kubelet-bootstrap若執行失敗,可先給kubectl綁定默認cluster-admin管理員集群角色,授權集群操作權限
kubectl create clusterrolebinding cluster-system-anonymous --clusterrole=cluster-admin --user=system:anonymous

在node01節點操作

cd /opt/
./kubelet.sh 192.168.1.33
ps aux | grep kubelet

在 master01 節點上操作

kubectl get csr

kubectl certificate approve node-csr-duiobEzQ0R93HsULoS9NT9JaQylMmid_nBF3Ei3NtFEkubectl get csr

在 node01 節點上操作

for i in $(ls /usr/lib/modules/$(uname -r)/kernel/net/netfilter/ipvs|grep -o "^[^.]*");do echo $i; /sbin/modinfo -F filename $i >/dev/null 2>&1 && /sbin/modprobe $i;done
cd /opt/
./proxy.sh 192.168.1.33
ps aux | grep kube-proxy

部署 Calico網絡:

在 master01 節點上操作

準備calico.yaml 文件到 /opt/k8s 目錄中

           # - name: CALICO_IPV4POOL_CIDR#   value: "10.244.0.0/16"

修改里面定義 Pod 的網絡(CALICO_IPV4POOL_CIDR),需與前面 kube-controller-manager 配置文件指定的 cluster-cidr 網段一樣

kubectl apply -f calico.yaml
kubectl get pods -n kube-system
kubectl get nodes

部署node02:

在node01操作

cd /opt/
scp kubelet.sh proxy.sh root@192.168.1.44:/opt/
scp -r /opt/cni root@192.168.1.44:/opt/

在node02操作

cd /opt/
chmod +x kubelet.sh
./kubelet.sh 192.168.1.44

在 master01 節點上操作

kubectl get csr
kubectl certificate approve node-csr-nh4DGjA-xNcE_dJ0blI6HTgz-XcqTkD5MFiGQQ9mEoQ
kubectl get csr

在 node02 節點上操作

for i in $(ls /usr/lib/modules/$(uname -r)/kernel/net/netfilter/ipvs|grep -o "^[^.]*");do echo $i; /sbin/modinfo -F filename $i >/dev/null 2>&1 && /sbin/modprobe $i;done
cd /opt/
chmod +x proxy.sh
./proxy.sh 192.168.1.44

在 master01 節點上操作

kubectl get nodes

部署 CoreDNS

在所有 node 節點上操作

上傳 coredns.tar 到 /opt 目錄中

cd /opt
docker load -i coredns.tar

在 master01 節點上操作

上傳 coredns.yaml 文件到 /opt/k8s 目錄中,部署 CoreDNS?

cd /opt/k8s
kubectl apply -f coredns.yaml
kubectl get pods -n kube-system 

DNS 解析測試

kubectl run -it --rm dns-test --image=busybox:1.28.4 sh

如果出現以下報錯
[root@master01 k8s]# kubectl run -it ?--image=busybox:1.28.4 sh
If you don't see a command prompt, try pressing enter.
Error attaching, falling back to logs: unable to upgrade connection: Forbidden (user=system:anonymous, verb=create, resource=nodes, subresource=proxy)
Error from server (Forbidden): Forbidden (user=system:anonymous, verb=get, resource=nodes, subresource=proxy) ( pods/log sh)

需要添加 rbac的權限 ?直接使用kubectl綁定 ?clusteradmin 管理員集群角色 ?授權操作權限

[root@master01 k8s]# kubectl create clusterrolebinding cluster-system-anonymous --clusterrole=cluster-admin --user=system:anonymous
clusterrolebinding.rbac.authorization.k8s.io/cluster-system-anonymous created

master02 節點部署 :

從 master01 節點上拷貝證書文件、各master組件的配置文件和服務管理文件到 master02 節點

scp -r /opt/etcd/ root@192.168.1.22:/opt/
scp -r /opt/kubernetes/ root@192.168.1.22:/opt
scp -r /root/.kube root@192.168.1.22:/root
scp /usr/lib/systemd/system/{kube-apiserver,kube-controller-manager,kube-scheduler}.service root@192.168.1.22:/usr/lib/systemd/system/

在master02上修改配置文件kube-apiserver中的IP

vim /opt/kubernetes/cfg/kube-apiserver
--etcd-servers=https://192.168.1.11:2379,https://192.168.1.33:2379,https://192.168.1.44:2379 \
--bind-address=192.168.1.22 \
--secure-port=6443 \
--advertise-address=192.168.1.22 \
--allow-privileged=true \

在 master02 節點上啟動各服務并設置開機自啟

systemctl start kube-apiserver.service
systemctl enable kube-apiserver.service
systemctl start kube-controller-manager.service
systemctl enable kube-controller-manager.service
systemctl start kube-scheduler.service
systemctl enable kube-scheduler.service

查看node節點狀態

ln -s /opt/kubernetes/bin/* /usr/local/bin/
kubectl get nodes

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/12750.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/12750.shtml
英文地址,請注明出處:http://en.pswp.cn/web/12750.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

渲染農場是什么意思?瑞云渲染為你解答

渲染農場是一種通過集合多臺計算機的計算能力來加速圖像渲染過程的系統。它尤其適用于動畫、電影特效和高端視覺效果的制作&#xff0c;這些領域通常需要處理非常復雜和計算密集型的渲染任務。 渲染農場就是一大群電腦&#xff0c;他們一起可以快速渲染出漂亮的圖像。在做動畫片…

客觀需求驗證的常見5大步驟(實施版)

我們在挖掘用戶需求時&#xff0c;往往容易犯偽需求或需求錯位等問題&#xff0c;因此需要進行客觀需求驗證。通過客觀的驗證&#xff0c;我們可以有效減少主觀判斷誤差問題&#xff0c;確保需求的準確性&#xff0c;從而降低需求變更和項目風險的概率&#xff0c;減少開發成本…

LeetCode算法題:11. 盛最多水的容器(Java)(雙指針問題總結)

給定一個長度為 n 的整數數組 height 。有 n 條垂線&#xff0c;第 i 條線的兩個端點是 (i, 0) 和 (i, height[i]) 。 找出其中的兩條線&#xff0c;使得它們與 x 軸共同構成的容器可以容納最多的水。 返回容器可以儲存的最大水量。 提示&#xff1a; n height.length2 <…

第十四屆藍橋杯大賽軟件賽國賽C/C++ 大學 B 組 數三角

//枚舉頂點。 //不存在等邊三角形 #include<bits/stdc.h> using namespace std; #define int long long const int n2e311; int a,b,c,l[n],r[n]; signed main() {ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);cin>>a;for(int i1;i<a;i){cin>>…

UE4_環境_局部霧化效果

學習筆記&#xff0c;不喜勿噴&#xff01;侵權立刪&#xff01;祝愿大家生活越來越好&#xff01; 本文重點介紹下材質節點SphereMask節點在體積霧中的使用方法。 一、球體遮罩SphereMask材質節點介紹&#xff1a; 球體蒙版&#xff08;SphereMask&#xff09; 表達式根據距…

【筆記】Android Studio 版本信息

Android Studio Jellyfish | 2023.3.1 | Android Developers Android Studio 是開發 Android 應用的官方 IDE&#xff0c;包含構建 Android 應用所需的所有功能。 AS與AGP版本適用關系 AGP(Android Gradle plugin) Android gradle插件 Androdi Studio versionRequired AG…

2024紅帽全球峰會:CEO行業洞察分享

作為全球IT領域一年一度的行業盛宴&#xff0c;2024紅帽全球峰會于近日盛大召開。生成式AI與大模型是當前IT行業最受關注的熱點話題&#xff0c;而紅帽在生成式AI與大模型領域的最新動作&#xff0c;也理所當然地成為了本屆峰會觀眾目光聚集的焦點。 作為世界領先的開源解決方案…

使用vcpkg與json文件自動安裝項目依賴庫

說明 本文記錄自己使用vcpkg.json文件自動安裝依賴庫并完成編譯的全過程。 關于vcpkg是什么這里就不多詳細解釋&#xff0c;可以看一下專門的介紹及安裝的文章&#xff0c;總之了解這是一個C的包管理工具就可以了。 流程 下面介紹從GitHub上克隆C項目以及為這個項目安裝所需…

二叉樹的常見操作

建立樹 復制二叉樹 計算深度 計算總結點數 計算葉子結點數

OpenHarmony標準設備應用開發(二)——布局、動畫與音樂

本章是 OpenHarmony 標準設備應用開發的第二篇文章。我們通過知識體系新開發的幾個基于 OpenHarmony3.1 Beta 標準系統的樣例&#xff1a;分布式音樂播放、傳炸彈、購物車等樣例&#xff0c;分別介紹下音樂播放、顯示動畫、動畫轉場&#xff08;頁面間轉場&#xff09;三個進階…

AI工具的熱門與卓越:揭示AI技術的實際應用和影響

文章目錄 每日一句正能量前言常用AI工具創新AI應用個人體驗分享后記 每日一句正能量 我們在我們的勞動過程中學習思考&#xff0c;勞動的結果&#xff0c;我們認識了世界的奧妙&#xff0c;于是我們就真正來改變生活了。 前言 隨著人工智能&#xff08;AI&#xff09;技術的快…

深度剖析MyBatis的二級緩存

二級緩存的原理 MyBatis 二級緩存的原理是什么&#xff1f; 二級緩存的原理和一級緩存一樣&#xff0c;第一次查詢會將數據放到 緩存 中&#xff0c;然后第二次查詢直接去緩存讀取。但是一級緩存是基于 SqlSession 的&#xff0c;二級緩存是基于 mapper 的 namespace 的。也就是…

關于API接口的自述

在實際工作中&#xff0c;我們需要經常跟第三方平臺打交道&#xff0c;可能會對接第三方平臺API接口&#xff0c;或者提供API接口給第三方平臺調用。 那么問題來了&#xff0c;如果設計一個優雅的API接口&#xff0c;能夠滿足&#xff1a;安全性、可重復調用、穩定性、好定位問…

Qt運行時,如何設置第一個聚焦的控件

問題&#xff1a;Qt第一個聚焦的控件&#xff0c;如何自行設置&#xff1f; 嘗試&#xff1a; 1.在代碼中設置 lineEdit->setFocus() 。無效&#xff01; 2.Qt Designer–打開form1.ui–菜單欄下一行–Edit Tab Order–按順序點擊–菜單欄下一行–Edit Widgets–退出。無效…

為什么做了功能測試還要做接口測試

接口測試與功能測試不是重復的測試,而是互為補充的測試策略。 在軟件測試領域,接口測試和功能測試被視為質量保證過程中至關重要的組成部分。盡管它們之間存在部分重復,但更多的情況下,它們相輔相成,各自發揮著獨特的作用。本文將探討接口測試與功能測試之間的關系,以及它…

【easyX】動手輕松掌握easyX 1

01 簡單繪圖 在這個程序中&#xff0c;我們先初始化繪圖窗口。其次&#xff0c;簡單繪制兩條線。 #include <graphics.h>//繪圖庫頭文件 #include <stdio.h> int main() {initgraph(640, 480);//初始化640?480繪圖屏幕line(200, 240, 440, 240);//畫線(200,240)…

MySQL是如何選擇索引的?

2.3.5. 索引選擇 MySQL是如何選擇索引的&#xff1f; 優化器決定了具體某一索引的選擇&#xff0c;也就是常說的執行計劃。而優化器的選擇是基于成本&#xff08;cost&#xff09;&#xff0c;哪個索引的成本越低&#xff0c;優先使用哪個索引。 SQL 優化器會分析所有可能的執…

Python操作鼠標鍵盤和爬蟲

一.pyautogui 庫 pyautogui 是一個 Python 庫&#xff0c;允許控制鼠標和鍵盤。可以通過它編寫 Python 腳本來自動執行各種任務&#xff0c;例如點擊按鈕、輸入文本、移動鼠標等。這個庫非常適合用來編寫自動化腳本來完成重復性的工作&#xff0c;比如網頁表單填寫、屏幕截圖、…

STC8增強型單片機開發——定時器Timer

一、定時器 定時器是一種計時裝置&#xff0c;通常由一個晶體振蕩器提供時鐘信號&#xff0c;可以計時一定的時間后執行相應的操作。在單片機中&#xff0c;定時器一般是由計數器和時鐘源組成的&#xff0c;可以用來產生一定時間間隔的中斷信號&#xff0c;或者用于測量輸入信號…

開放式運動耳機哪款好用?五款高性能值得信賴產品推薦

身為戶外運動的達人&#xff0c;我發現開放式運動耳機簡直是咱們運動時的最佳拍檔&#xff0c;不管是跑步還是健身&#xff0c;開放式運動耳機最為舒適&#xff0c;它的妙處就在于不用塞進耳朵&#xff0c;這樣既安全又衛生&#xff0c;戶外動起來更放心。但市面上好壞參半&…