#!/bin/bash
set -eo pipefail
declare -x TARGET_TEAR
declare -x cmd
dir=/tmp/etcdcert
KUBE_CERT_PATH=/etc/kubernetes/pki
ETCD_CERT_DIR=/var/lib/etcd/cert
ETCD_HOSTS=""
currentDir="$PWD"
# 更新K8s证书,根据集群Region替换下面cn-hangzhou的默认镜像地域。
function get_etcdhosts() {
name1=$(find "$ETCD_CERT_DIR" -name '*-name-1.pem' -exec basename {} \; | sed 's/-name-1.pem//g')
name2=$(find "$ETCD_CERT_DIR" -name '*-name-2.pem' -exec basename {} \; | sed 's/-name-2.pem//g')
name3=$(find "$ETCD_CERT_DIR" -name '*-name-3.pem' -exec basename {} \; | sed 's/-name-3.pem//g')
echo "hosts: $name1 $name2 $name3"
ETCD_HOSTS="$name1 $name2 $name3"
}
function gencerts() {
echo "generate ssl cert ..."
rm -rf $dir
mkdir -p "$dir"
local hosts
hosts=$(echo $ETCD_HOSTS | tr -s " " ",")
echo "-----generate ca"
echo '{"CN":"CA","key":{"algo":"rsa","size":2048}, "ca": {"expiry": "438000h"}}' |
cfssl gencert -initca - | cfssljson -bare $dir/ca -
echo '{"signing":{"default":{"expiry":"438000h","usages":["signing","key encipherment","server auth","client auth"]}}}' >$dir/ca-config.json
echo "-----generate etcdserver"
export ADDRESS=$hosts,ext1.example.com,coreos1.local,coreos1,127.0.0.1
export NAME=etcd-server
echo '{"CN":"'$NAME'","hosts":[""],"key":{"algo":"rsa","size":2048}}' |
cfssl gencert -config=$dir/ca-config.json -ca=$dir/ca.pem -ca-key=$dir/ca-key.pem -hostname="$ADDRESS" - | cfssljson -bare $dir/$NAME
export ADDRESS=
export NAME=etcd-client
echo '{"CN":"'$NAME'","hosts":[""],"key":{"algo":"rsa","size":2048}}' |
cfssl gencert -config=$dir/ca-config.json -ca=$dir/ca.pem -ca-key=$dir/ca-key.pem -hostname="$ADDRESS" - | cfssljson -bare $dir/$NAME
# gen peer-ca
echo "-----generate peer certificates"
echo '{"CN":"Peer-CA","key":{"algo":"rsa","size":2048}, "ca": {"expiry": "438000h"}}' | cfssl gencert -initca - | cfssljson -bare $dir/peer-ca -
echo '{"signing":{"default":{"expiry":"438000h","usages":["signing","key encipherment","server auth","client auth"]}}}' >$dir/peer-ca-config.json
i=0
for host in $ETCD_HOSTS; do
((i = i + 1))
export MEMBER=${host}-name-$i
echo '{"CN":"'${MEMBER}'","hosts":[""],"key":{"algo":"rsa","size":2048}}' |
cfssl gencert -ca=$dir/peer-ca.pem -ca-key=$dir/peer-ca-key.pem -config=$dir/peer-ca-config.json -profile=peer \
-hostname="$hosts,${MEMBER}.local,${MEMBER}" - | cfssljson -bare $dir/${MEMBER}
done
# 制作bundle ca
cat $KUBE_CERT_PATH/etcd/ca.pem >>$dir/bundle_ca.pem
cat $ETCD_CERT_DIR/ca.pem >>$dir/bundle_ca.pem
cat $dir/ca.pem >>$dir/bundle_ca.pem
# 制作bundle peer-ca
cat $ETCD_CERT_DIR/peer-ca.pem >$dir/bundle_peer-ca.pem
cat $dir/peer-ca.pem >>$dir/bundle_peer-ca.pem
current_year=$(date +%Y)
TARGET_TEAR=$((TARGET_TEAR + 50))
# chown
chown -R etcd:etcd $dir
chmod 0644 $dir/*
}
function etcd_client_urls() {
local etcd_hosts=()
for ip in "${ETCD_HOSTS[@]}"; do
etcd_hosts+=("https://$ip:2379")
done
local result=$(
IFS=','
echo "${etcd_hosts[*]}"
)
echo "$result"
}
function check_cert_files_exist() {
REQUIRED_CERTS=("ca.pem" "etcd-server-key.pem" "etcd-server.pem" "peer-ca-key.pem" "peer-ca.pem")
if [ ! -d "$ETCD_CERT_DIR" ]; then
echo "Error: Directory $ETCD_CERT_DIR does not exist"
exit 1
fi
for cert_file in "${REQUIRED_CERTS[@]}"; do
if [ ! -f "$ETCD_CERT_DIR/$cert_file" ]; then
echo "Error: File $ETCD_CERT_DIR/$cert_file does not exist"
exit 1
fi
done
echo "All required certificate files exist"
}
function check_etcd_cluster_ready() {
local etcd_endpoints=()
for ip in $ETCD_HOSTS; do
etcd_endpoints+=("https://$ip:2379")
done
ready=0
for i in $(seq 300); do
for idx in "${!etcd_endpoints[@]}"; do
endpoint="${etcd_endpoints[$idx]}"
local health_output=$(ETCDCTL_API=3 etcdctl --cacert=/var/lib/etcd/cert/ca.pem --cert=/var/lib/etcd/cert/etcd-server.pem --key=/var/lib/etcd/cert/etcd-server-key.pem --endpoints "$endpoint" endpoint health --command-timeout=1s 2>&1)
if echo "$health_output" | grep -q "successfully committed proposal"; then
unset 'etcd_endpoints[$idx]'
else
echo "etcdctl result: ${health_output}"
echo "$endpoint is not ready"
fi
done
# shellcheck disable=SC2199
if [[ -z "${etcd_endpoints[@]}" ]]; then
echo "ETCD cluster is ready"
ready=1
break
fi
printf "wait etcd cluster to be ready, retry %d after 1s,total 300s \n" "$i"
done
}
function check_container_runtime() {
if command -v dockerd &>/dev/null && ps aux | grep -q "[d]ockerd"; then
cmd=docker
elif command -v containerd &>/dev/null && ps aux | grep -q "[c]ontainerd"; then
cmd=crictl
else
echo "Neither Dockerd nor Containerd is installed or running."
exit 1
fi
}
function rotate_etcd_ca() {
for ADDR in $ETCD_HOSTS; do
echo "update etcd CA on node $ADDR"
scp -o StrictHostKeyChecking=no $dir/bundle_ca.pem root@$ADDR:$ETCD_CERT_DIR/ca.pem
scp -o StrictHostKeyChecking=no $dir/bundle_ca.pem root@$ADDR:$KUBE_CERT_PATH/etcd/ca.pem
scp -o StrictHostKeyChecking=no $dir/etcd-client.pem root@$ADDR:$KUBE_CERT_PATH/etcd/etcd-client.pem
scp -o StrictHostKeyChecking=no $dir/etcd-client-key.pem root@$ADDR:$KUBE_CERT_PATH/etcd/etcd-client-key.pem
scp -o StrictHostKeyChecking=no $dir/bundle_peer-ca.pem root@$ADDR:$ETCD_CERT_DIR/peer-ca.pem
ssh -o StrictHostKeyChecking=no root@$ADDR chown -R etcd:etcd $ETCD_CERT_DIR
ssh -o StrictHostKeyChecking=no root@$ADDR chmod 0644 $ETCD_CERT_DIR/*
echo "restart etcd on node $ADDR"
ssh -o StrictHostKeyChecking=no root@$ADDR systemctl restart etcd
echo "etcd on node $ADDR restarted"
# 校验etcd是否启动成功,校验集群是否正常
echo "check connectivity for etcd nodes"
check_etcd_cluster_ready
echo "end to check connectivity for etcd nodes"
restart_one_apiserver $ADDR
echo "apiserver on node $ADDR restarted"
done
}
function rotate_etcd_certs() {
for ADDR in $ETCD_HOSTS; do
echo "update etcd peer certs on node $ADDR"
scp -o StrictHostKeyChecking=no \
$dir/{peer-ca-key.pem,etcd-server.pem,etcd-server-key.pem,etcd-client.pem,etcd-client-key.pem,ca-key.pem,*-name*.pem} root@$ADDR:$ETCD_CERT_DIR/
ssh -o StrictHostKeyChecking=no root@$ADDR chown -R etcd:etcd $ETCD_CERT_DIR
ssh -o StrictHostKeyChecking=no root@$ADDR \
chmod 0400 $ETCD_CERT_DIR/{peer-ca-key.pem,etcd-server.pem,etcd-server-key.pem,etcd-client.pem,etcd-client-key.pem,ca-key.pem,*-name*.pem}
echo "restart etcd on node $ADDR"
ssh -o StrictHostKeyChecking=no root@$ADDR systemctl restart etcd
echo "etcd on node $ADDR restarted"
echo "check connectivity for etcd nodes"
check_etcd_cluster_ready
echo "end to check connectivity for etcd nodes"
done
}
function recover_etcd_ca() {
# Update certs on etcd nodes.
for ADDR in $ETCD_HOSTS; do
echo "replace etcd CA on node $ADDR"
scp -o StrictHostKeyChecking=no $dir/ca.pem root@$ADDR:$ETCD_CERT_DIR/ca.pem
scp -o StrictHostKeyChecking=no $dir/ca.pem root@$ADDR:$KUBE_CERT_PATH/etcd/ca.pem
scp -o StrictHostKeyChecking=no $dir/ca.pem root@$ADDR:$KUBE_CERT_PATH/etcd/ca.pem
scp -o StrictHostKeyChecking=no $dir/peer-ca.pem root@$ADDR:$ETCD_CERT_DIR/peer-ca.pem
ssh -o StrictHostKeyChecking=no root@$ADDR chown -R etcd:etcd $ETCD_CERT_DIR
echo "restart apiserver on node $ADDR"
restart_one_apiserver $ADDR
echo "apiserver on node $ADDR restarted"
echo "restart etcd on node $ADDR"
ssh -o StrictHostKeyChecking=no root@$ADDR systemctl restart etcd
echo "etcd on node $ADDR restarted"
echo "check connectivity for etcd nodes"
check_etcd_cluster_ready
echo "end to check connectivity for etcd nodes"
sleep 5
done
}
function recover_etcd_client_ca() {
# Update certs on etcd nodes.
for ADDR in $ETCD_HOSTS; do
echo "replace etcd CA on node $ADDR"
scp -o StrictHostKeyChecking=no $dir/ca.pem root@$ADDR:$KUBE_CERT_PATH/etcd/ca.pem
scp -o StrictHostKeyChecking=no $dir/ca.pem root@$ADDR:$KUBE_CERT_PATH/etcd/ca.pem
done
}
function renew_k8s_certs() {
# try to get region id from meta-server if not given in parameter
META_REGION=$(get_region_id)
if [[ -z "$REGION" ]]; then
if [[ -z "$META_REGION" ]]; then
echo "failed to get region id from ECS meta-server, please enter the region parameter."
return 1
fi
REGION=$META_REGION
elif [[ -n "${META_REGION}" && "$REGION" != "$META_REGION" ]] ; then
echo "switch to use local region id $META_REGION"
REGION=$META_REGION
fi
# Update certs for k8s components and kubeconfig
for ADDR in $ETCD_HOSTS; do
echo "renew k8s components cert on node $ADDR"
#compatible containerd
set +e
IMAGE="registry.$REGION.aliyuncs.com/acs/etcd-rotate:v2.0.0"
if is_vpc; then
IMAGE="registry-vpc.$REGION.aliyuncs.com/acs/etcd-rotate:v2.0.0"
fi
echo "will pull rotate image $IMAGE"
ssh -o StrictHostKeyChecking=no root@$ADDR docker run --privileged=true -v /:/alicoud-k8s-host --pid host --net host \
$IMAGE /renew/upgrade-k8s.sh --role master
ssh -o StrictHostKeyChecking=no root@$ADDR ctr image pull $IMAGE
ssh -o StrictHostKeyChecking=no root@$ADDR ctr run --privileged=true --mount type=bind,src=/,dst=/alicoud-k8s-host,options=rbind:rw \
--net-host $IMAGE cert-rotate /renew/upgrade-k8s.sh --role master
set -e
echo "finished renew k8s components cert on $ADDR"
done
}
function get_region_id() {
set +e; # close error out
local TOKEN=`curl -X PUT "http://100.100.100.200/latest/api/token" \
-H "X-aliyun-ecs-metadata-token-ttl-seconds:900"`
local path=100.100.100.200/latest/meta-data/region-id
for (( i=0; i<3; i++));
do
response=$(curl -H "X-aliyun-ecs-metadata-token: $TOKEN" --retry 1 --retry-delay 5 -sSL $path)
if [[ $? -gt 0 || "x$response" == "x" ]];
then
sleep 2; continue
fi
if echo "$response"|grep -E "<title>.*</title>" >/dev/null;
then
sleep 3; continue
fi
echo "$response"
# return from metadata succeed.
set -e; return
done
set -e # open error out
# function will return empty string when failed
}
function is_vpc() {
# Execute the curl command and capture the network-type from ECS meta-server
local TOKEN=`curl -X PUT "http://100.100.100.200/latest/api/token" \
-H "X-aliyun-ecs-metadata-token-ttl-seconds:900"`
response=$(curl -H "X-aliyun-ecs-metadata-token: $TOKEN" -s http://100.100.100.200/latest/meta-data/network-type)
if [ "$response" = "vpc" ]; then
return 0
else
return 1
fi
}
function generate_cm() {
echo "generate status configmap"
cat <<-"EOF" >/tmp/ack-rotate-etcd-ca-cm.yaml.tpl
apiVersion: v1
kind: ConfigMap
metadata:
name: ack-rotate-etcd-status
namespace: kube-system
data:
status: "success"
hosts: "$hosts"
EOF
sed -e "s#\$hosts#$ETCD_HOSTS#" /tmp/ack-rotate-etcd-ca-cm.yaml.tpl | kubectl apply -f -
}
function restart_one_apiserver() {
ADDR=$1
if [[ -z "${ADDR}" ]]; then
printf "ADDR is empty,exit."
exit 1
fi
printf "restart apiserver on node %s\n" "${ADDR}"
scp -o StrictHostKeyChecking=no "${currentDir}"/restart-apiserver.sh root@"${ADDR}":/tmp/restart-apiserver.sh
ssh -e none -o StrictHostKeyChecking=no root@"${ADDR}" chmod +x /tmp/restart-apiserver.sh
ssh -e none -o StrictHostKeyChecking=no root@"${ADDR}" bash /tmp/restart-apiserver.sh
}
while
[[ $# -gt 0 ]]
do
key="$1"
case $key in
--region)
export REGION=$2
shift
;;
*)
echo "unknown option [$key]"
exit 1
;;
esac
shift
done
get_etcdhosts
echo "${ETCD_HOSTS[@]}"
check_container_runtime
# Update certs on etcd nodes.
echo "---restart runtime and kubelet on master nodes---"
for ADDR in $ETCD_HOSTS; do
if [ "$cmd" == "docker" ]; then
echo "restart docker on node $ADDR"
ssh -o StrictHostKeyChecking=no root@$ADDR systemctl restart docker
fi
ssh -e none -o StrictHostKeyChecking=no root@"${ADDR}" systemctl restart kubelet
done
sleep 5
echo "---end to restart runtime and kubelet on master nodes---"
echo "---renew k8s components certs---"
renew_k8s_certs
echo "---end to renew k8s components certs---"
echo "---check cert files exist---"
check_cert_files_exist
echo "---end to check cert files exist---"
echo "---check connectivity for etcd nodes---"
check_etcd_cluster_ready
echo "---end to check connectivity for etcd nodes---"
# Update certs on etcd nodes.
for ADDR in $ETCD_HOSTS; do
scp -o StrictHostKeyChecking=no restart-apiserver.sh root@$ADDR:/tmp/restart-apiserver.sh
ssh -o StrictHostKeyChecking=no root@$ADDR chmod +x /tmp/restart-apiserver.sh
done
gencerts
echo "---rotate etcd ca and etcd client ca---"
rotate_etcd_ca
echo "---end to rotate etcd ca and etcd client ca---"
echo "---rotate etcd peer and certs---"
rotate_etcd_certs
echo "---end to rotate etcd peer and certs---"
echo "check etcd cluster ready"
check_etcd_cluster_ready
echo "---replace etcd ca---"
recover_etcd_ca
echo "---end to replace etcd ca---"
generate_cm
echo "etcd CA and certs have succesfully rotated!"