Skip to content
This repository was archived by the owner on Sep 30, 2020. It is now read-only.

Commit ea799be

Browse files
Allow major Etcd upgrades with safe roll-back (#1773)
* Safer major etcd upgrades by spinning up new etcds and preforming a migration (copy of all kubernetes data) Simialr to the approach used during the stack migration but this time the major/minor version of etcd is used to control migration e.g 3.2.x -> 3.3.x will cause a migration. It is safer because should the CF roll fail the previous etcd's should still be available to fall-back to. Bring all new etcds up at same time during a migration. Correct looking up of configsets now that the instance name has changed. When an etcd has an attached NIC use that address rather than the machine's private dnsname Update Etcd to 3.3.17 release Fix etcdadm so that it can still detect cluster healthy now written to stderr Update etcd migration to respect keys with leases Fix building etcd endpoints where the interfaces are listed in different orders Move to a two export process for retrieving keys and values using etcdctl 'json' export type for key/value data, and then again using its 'fields' export type in order to successfully extract key/lease data. Process the two files back together with a nod to performance. * Fix tests by specifying etcdversion (I added some code to throw an error if the default etcd version hadn't been correctly linked into the binary - which it isn't during testing). * Only announce the migration if the lookup of etcd endpoints have succeeded.
1 parent 6971be4 commit ea799be

File tree

11 files changed

+318
-66
lines changed

11 files changed

+318
-66
lines changed

build

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ TAG=$(git describe --exact-match --abbrev=0 --tags "${COMMIT}" 2> /dev/null || t
66
BRANCH=$(git branch | grep \* | cut -d ' ' -f2 | sed -e 's/[^a-zA-Z0-9+=._:/-]*//g' || true)
77
OUTPUT_PATH=${OUTPUT_PATH:-"bin/kube-aws"}
88
VERSION=""
9-
ETCD_VERSION="v3.2.26"
9+
ETCD_VERSION="v3.3.17"
1010
KUBERNETES_VERSION="v1.15.5"
1111

1212
if [ -z "$TAG" ]; then

builtin/files/etcdadm/etcdadm

+162-18
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ config_etcd_endpoints() {
8585
echo "${ETCD_ENDPOINTS}"
8686
}
8787

88-
etcd_version=${ETCD_VERSION:-v3.2.13}
88+
etcd_version=${ETCD_VERSION:-v3.3.17}
8989
etcd_aci_url="https://github.com/coreos/etcd/releases/download/$etcd_version/etcd-$etcd_version-linux-amd64.aci"
9090

9191
member_count="${ETCDADM_MEMBER_COUNT:?missing required env}"
@@ -783,7 +783,7 @@ member_data_dir() {
783783
}
784784

785785
member_etcdctl() {
786-
raw_etcdctl etcdctl --endpoints "$(member_client_url)" ${*}
786+
raw_etcdctl etcdctl --endpoints $(member_client_url) $@
787787
}
788788

789789
raw_etcdctl() {
@@ -801,14 +801,8 @@ raw_etcdctl() {
801801
docker_opts+=(--volume=${credentials}:${credentials})
802802
fi
803803

804-
_run_as_root docker run ${docker_opts[*]} \
805-
--env ETCDCTL_API=3 \
806-
--network=host \
807-
--volume="$(member_host_snapshots_dir_path)":/"$(member_snapshots_dir_name)" \
808-
--volume="$(member_data_dir)":/var/lib/etcd \
809-
--volume "$(member_snapshots_dir_name)":"$(member_host_snapshots_dir_path)" \
810-
quay.io/coreos/etcd:$etcd_version \
811-
${*}
804+
local command=$(echo "docker run ${docker_opts[@]} --env ETCDCTL_API=3 --network=host --volume=$(member_host_snapshots_dir_path):/$(member_snapshots_dir_name) --volume=$(member_data_dir):/var/lib/etcd --volume=$(member_snapshots_dir_name):$(member_host_snapshots_dir_path) quay.io/coreos/etcd:$etcd_version $@ 2>&1" | tr '\n' ' ')
805+
bash -c "${command}" 2>&1
812806
}
813807

814808
member_is_healthy() {
@@ -874,23 +868,173 @@ export_kubernetes_registry() {
874868
local file_name=$1
875869
echo "Exporting kubernetes objects to $(member_host_snapshots_dir_path)/$file_name"
876870
if cluster_is_healthy; then
877-
(member_etcdctl get '/registry' --prefix --write-out="json") | jq -r '.kvs[] | "\(.key):\(.value)"' >"$(member_host_snapshots_dir_path)/$file_name"
871+
export_key_values $file_name
872+
process_export_files $file_name
878873
else
879874
_panic 'cluster is not healthy, can not export keys from an unhealthy cluster'
880875
fi
876+
echo "FINISHED exporting kubernetes object registry, ready to import"
877+
}
878+
879+
export_key_values() {
880+
local file_name=$1
881+
882+
echo "exporting objects from original etcds under path /registry..."
883+
# we have to export twice (unfortunately)
884+
# 'json' output mangles the lease ids
885+
# 'fields' mangles the values
886+
# we will process both of these files in order to properly extract key/value/lease information
887+
member_etcdctl get '/registry' --prefix --write-out="fields" >"$(member_host_snapshots_dir_path)/${file_name}.fields"
888+
member_etcdctl get '/registry' --prefix --write-out="json" | jq -r '.kvs[] | "\(.key)|\(.value)"' >"$(member_host_snapshots_dir_path)/${file_name}.kv"
889+
echo "FINISHED exporting objects!"
890+
}
891+
892+
# In order to improve performance, we process the exported data so that we can import it in batches of lease ttl
893+
# Each etcdctl docker container and so eats time so we want to process imports in as big batches as possible.
894+
process_export_files() {
895+
local file=$1
896+
897+
create_lease_lookup "${file}.fields"
898+
create_import_files "${file}.kv" "${file}.fields"
899+
}
900+
901+
# create_lease_lookup, takes a fields type export from etcdctl and creates a crude lookup structure using the last 3 chars the key as cache buckets
902+
# the reasoning behind this is to prevent the creation of the sorted key/value files from having to grep through a single large file for every key/value.
903+
# NOTE: it was impossible to use the lease as extracted from the etcdctl json output because it mangles the lease number by using floats and rounding
904+
# fields output does not exhibit this issue.
905+
create_lease_lookup() {
906+
local file=$1
907+
local cachepath="${file}_lease_cache"
908+
local key lease scratch
909+
local cache1 cache2 cache3
910+
911+
# "Key" : "/registry/services/specs/kube-system/tiller-deploy"
912+
# "CreateRevision" : 381
913+
# "ModRevision" : 381
914+
# "Version" : 1
915+
# "Value" : "k8s\x00\n\r\n\x02v1\x12\aService\x12\xf4\x04\n\x82\x04\n\rtiller-deploy\x12\x00\x1a\vkube-system\"\x00*$0fca6c7b-ffd2-11e9-a0f8-02a978d289c02\x008\x00B\b\b\xb0\xf8\x85\xee\x05\x10\x00Z\v\n\x03app\x12\x04helmZ\x0e\n\x04name\x12\x06tillerb\x8c\x03\n0kubectl.kubernetes.io/last-applied-configuration\x12\xd7\x02{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":null,\"labels\":{\"app\":\"helm\",\"name\":\"tiller\"},\"name\":\"tiller-deploy\",\"namespace\":\"kube-system\"},\"spec\":{\"ports\":[{\"name\":\"tiller\",\"port\":44134,\"targetPort\":\"tiller\"}],\"selector\":{\"app\":\"helm\",\"name\":\"tiller\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\nz\x00\x12i\n!\n\x06tiller\x12\x03TCP\x18\xe6\xd8\x02\"\f\b\x01\x10\x00\x1a\x06tiller(\x00\x12\v\n\x03app\x12\x04helm\x12\x0e\n\x04name\x12\x06tiller\x1a\f192.168.5.55\"\tClusterIP:\x04NoneB\x00R\x00Z\x00`\x00h\x00\x1a\x02\n\x00\x1a\x00\"\x00"
916+
# "Lease" : 0
917+
echo "writing lease assignment to lease cache ${cachepath}..."
918+
while read -u 10 key
919+
do
920+
if [[ "$key" =~ ^\"Key\"\ : ]]; then
921+
key=${key#\"Key\" : \"}
922+
key=${key%\"}
923+
read -u 10 scratch # CreateRevision
924+
read -u 10 scratch # ModRevision
925+
read -u 10 scratch # Version
926+
read -u 10 scratch # Value
927+
read -u 10 lease
928+
lease=${lease#\"Lease\" : }
929+
lease=$(printf "%0x" ${lease}) # we want the hex encoding of the lease id
930+
931+
# use last 3 characters of key as crude cache buckets
932+
cache1=${key: -1:1}
933+
cache2=${key: -2:1}
934+
cache3=${key: -3:1}
935+
if [[ "${lease}" != "0" ]]; then
936+
mkdir -p "$(member_host_snapshots_dir_path)/${cachepath}/${cache1}/${cache2}/${cache3}"
937+
echo "${key}|${lease}" >>"$(member_host_snapshots_dir_path)/${cachepath}/${cache1}/${cache2}/${cache3}/leases"
938+
fi
939+
fi
940+
done 10<"$(member_host_snapshots_dir_path)/${file}"
941+
echo "FINISHED populating the lease cache!"
942+
}
943+
944+
create_import_files() {
945+
local kvfile=$1
946+
local leasefile=$2
947+
local cachepath="${leasefile}_lease_cache"
948+
local ttlfile="${leasefile}.ttls"
949+
local sortedfile="${kvfile}.sorted"
950+
local key value ttl leased destfile
951+
952+
echo "writing import files..."
953+
while read -u 10 l
954+
do
955+
key=$(echo "${l%%|*}" | base64 -d)
956+
value="${l##*|}"
957+
958+
# use last 3 characters of key as cache buckets
959+
cache1=${key: -1:1}
960+
cache2=${key: -2:1}
961+
cache3=${key: -3:1}
962+
963+
destfile="$(member_host_snapshots_dir_path)/${sortedfile}.nolease"
964+
if [ -d "$(member_host_snapshots_dir_path)/${cachepath}/${cache1}/${cache2}/${cache3}" ]; then
965+
if leased=$(grep "^${key}|" "$(member_host_snapshots_dir_path)/${cachepath}/${cache1}/${cache2}/${cache3}/leases"); then
966+
leased="${leased##*|}"
967+
if ! ttl=$(grep "^${leased} " "$(member_host_snapshots_dir_path)/${ttlfile}" | awk '{print $2}'); then
968+
ttl=$(lookup_lease_ttl "${leased}" "$(member_host_snapshots_dir_path)/${ttlfile}")
969+
fi
970+
destfile="$(member_host_snapshots_dir_path)/${sortedfile}.leased.${ttl}"
971+
fi
972+
fi
973+
echo "${key}" >>"${destfile}"
974+
echo "${value}" >>"${destfile}"
975+
done 10<"$(member_host_snapshots_dir_path)/${kvfile}"
976+
echo "FINISHED writing import files!"
881977
}
882978

883-
import_kubernetes_registry() {
884-
local file_name=$1
885-
echo "Importing kubernetes objects to $(member_host_snapshots_dir_path)/$file_name"
886-
if [[ ! -f "$(member_host_snapshots_dir_path)/$file_name" ]]; then
887-
_panic "Can't import objects, file not found: $(member_host_snapshots_dir_path)/$file_name"
979+
lookup_lease_ttl() {
980+
local lease=$1
981+
local cachepath=$2
982+
local ttl
983+
984+
result="$(member_etcdctl lease timetolive ${lease} | grep -v "already expired" 2>&1)"
985+
if [[ -n "${result}" ]]; then
986+
ttl=${result##*remaining(}
987+
ttl=${ttl%%s)}
988+
else
989+
ttl="0"
888990
fi
991+
echo "${ttl}"
992+
echo "${lease} ${ttl}" >>${cachepath}
993+
}
994+
995+
import_kubernetes_registry() {
996+
local kvfile=$1
997+
local standard="${1}.kv.sorted.nolease"
998+
local leased="${1}.kv.sorted.leased."
999+
8891000
if cluster_is_healthy; then
890-
raw_etcdctl /bin/sh -c 'while read -u 10 l; do k=$(echo "${l%%:*}" | base64 -d); v=${l##*:}; echo "saving $k"; echo -e "$v" | base64 -d | etcdctl --endpoints='$(member_client_url)' put "$k"; done 10<'$(member_snapshots_dir_name)/$file_name
1001+
echo "Importing standard kubernetes objects from $(member_host_snapshots_dir_path)/${standard}"
1002+
if [[ ! -f "$(member_host_snapshots_dir_path)/${standard}" ]]; then
1003+
_panic "Can't import objects, file not found: $(member_host_snapshots_dir_path)/${standard}"
1004+
fi
1005+
echo "importing keys from ${standard} ..."
1006+
import_keyfile "${standard}" ""
1007+
1008+
for file in $(member_host_snapshots_dir_path)/${leased}*
1009+
do
1010+
if [[ "$file" != "$(member_host_snapshots_dir_path)/${leased}*" ]]; then
1011+
local shortfile=$(basename $file)
1012+
local ttl=${shortfile##*.}
1013+
echo "importing leased keys from ${shortfile} ..."
1014+
import_keyfile "${shortfile}" "${ttl}"
1015+
fi
1016+
done
8911017
else
8921018
_panic 'cluster is not healthy, can not import keys to an unhealthy cluster'
8931019
fi
1020+
echo "FINISHED importing kubernetes registry, migration complete!"
1021+
}
1022+
1023+
import_keyfile() {
1024+
local file_name=$1
1025+
local ttl=$2
1026+
1027+
if [[ -n "${ttl}" ]]; then
1028+
id=$(member_etcdctl lease grant ${ttl} | awk '{print $2}')
1029+
command="while read -u 10 k; do read -u 10 v; echo \"saving \$k with lease=${id} ttl=${ttl}\"; echo \"\$v\" | base64 -d | etcdctl --endpoints='$(member_client_url)' put \$k --lease=${id}; done 10<$(member_snapshots_dir_name)/$file_name"
1030+
echo "command is $command"
1031+
raw_etcdctl /bin/sh -c "'${command}'" 2>&1
1032+
else
1033+
command="while read -u 10 k; do read -u 10 v; echo \"saving \$k\"; echo \"\$v\" | base64 -d | etcdctl --endpoints='$(member_client_url)' put \$k; done 10<$(member_snapshots_dir_name)/$file_name"
1034+
echo "command is $command"
1035+
raw_etcdctl /bin/sh -c "'${command}'" 2>&1
1036+
fi
1037+
echo "FINISHED importing ${file_name}!"
8941038
}
8951039

8961040
etcdadm_main() {
@@ -943,4 +1087,4 @@ etcdadm_main() {
9431087
if [[ "$0" == *etcdadm ]]; then
9441088
etcdadm_main "$@"
9451089
exit $?
946-
fi
1090+
fi

builtin/files/stack-templates/etcd.json.tmpl

+6-17
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@
338338
"PropagateAtLaunch": "true",
339339
"Value": "{{ $.Etcd.Version }}"
340340
},
341+
{
342+
"Key": "kube-aws:etcd_upgrade_group",
343+
"PropagateAtLaunch": "true",
344+
"Value": "{{ $etcdInstance.MajorMinorVersion }}"
345+
},
341346
{
342347
"Key": "Name",
343348
"PropagateAtLaunch": "true",
@@ -515,7 +520,7 @@
515520
}
516521
},
517522
"DependsOn": [
518-
{{if $.StackExists}}{{if $etcdIndex}}"{{$.Etcd.LogicalName}}{{sub $etcdIndex 1}}",{{end}}{{end}}
523+
{{if $.StackExists}}{{if not $.EtcdMigrationEnabled }}{{if $etcdIndex}}"{{$etcdInstance.LogicalNameForIndex (sub $etcdIndex 1)}}",{{end}}{{end}}{{end}}
519524
{{if $etcdInstance.EIPManaged}}
520525
"{{$etcdInstance.EIPLogicalName}}",
521526
{{end}}
@@ -588,22 +593,6 @@
588593
{{end}}
589594
},
590595
"Outputs": {
591-
{{range $index, $etcdInstance := $.EtcdNodes}}
592-
{{if $etcdInstance.EIPManaged}}
593-
"{{$etcdInstance.EIPLogicalName}}": {
594-
"Description": "The EIP for etcd node {{$index}}",
595-
"Value": {{$etcdInstance.EIPRef}},
596-
"Export": { "Name" : {"Fn::Sub": "${AWS::StackName}-{{$etcdInstance.EIPLogicalName}}" }}
597-
},
598-
{{end}}
599-
{{if $etcdInstance.NetworkInterfaceManaged}}
600-
"{{$etcdInstance.NetworkInterfacePrivateIPLogicalName}}": {
601-
"Description": "The private IP for etcd node {{$index}}",
602-
"Value": {{$etcdInstance.NetworkInterfacePrivateIPRef}},
603-
"Export": { "Name" : {"Fn::Sub": "${AWS::StackName}-{{$etcdInstance.NetworkInterfacePrivateIPLogicalName}}" }}
604-
},
605-
{{end}}
606-
{{end}}
607596
"StackName": {
608597
"Description": "The name of this stack which is used by node pool stacks to import outputs from this stack",
609598
"Value": { "Ref": "AWS::StackName" }

builtin/files/userdata/cloud-config-etcd

+16-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{{ define "instance-script" -}}
22
{{- $S3URI := self.Parts.s3.Asset.S3URL -}}
33
echo '{{.EtcdIndexEnvVarName}}={{extra.etcdIndex}}' >> {{.EtcdNodeEnvFileName}}
4+
echo 'CLUSTER_LOGICAL_NAME={{.Etcd.Cluster.LogicalName }}' >> {{.EtcdNodeEnvFileName}}
45
. /etc/environment
56
export COREOS_PRIVATE_IPV4 COREOS_PRIVATE_IPV6 COREOS_PUBLIC_IPV4 COREOS_PUBLIC_IPV6
67
REGION=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.region')
@@ -223,21 +224,22 @@ coreos:
223224
EnvironmentFile=-/etc/etcd-environment
224225
EnvironmentFile=/var/run/coreos/etcdadm-environment-migration
225226
ExecStartPre=/opt/bin/etcdadm cluster-is-healthy
226-
ExecStartPre=/bin/bash -c "\
227+
ExecStart=/bin/bash -c "\
227228
if /opt/bin/etcdadm member-is-leader; then \
228-
/opt/bin/etcdadm migration-export-kube-state existing-state-file.json && \
229-
mv /var/run/coreos/etcdadm/snapshots/existing-state-file.json /var/run/coreos/etcdadm/snapshots/exported-state-file.json; \
229+
/opt/bin/etcdadm migration-export-kube-state migration && \
230+
touch /var/run/coreos/etcdadm/snapshots/export-complete && \
231+
/bin/sleep 3600; \
230232
else \
231-
touch /var/run/coreos/etcdadm/snapshots/exported-state-file.json; \
233+
touch /var/run/coreos/etcdadm/snapshots/export-complete && \
234+
/bin/sleep 3600; \
232235
fi"
233-
ExecStart=/bin/sleep 3600
234-
TimeoutStartSec=900
236+
TimeoutStartSec=infinity
235237
- name: import-existing-etcd-state.path
236238
enable: true
237239
command: start
238240
content: |
239241
[Path]
240-
PathExists=/var/run/coreos/etcdadm/snapshots/exported-state-file.json
242+
PathExists=/var/run/coreos/etcdadm/snapshots/export-complete
241243

242244
[Install]
243245
WantedBy=default.target
@@ -258,10 +260,9 @@ coreos:
258260
EnvironmentFile=/var/run/coreos/etcdadm-environment
259261
ExecStartPre=/usr/bin/systemctl is-active export-existing-etcd-state.service
260262
ExecStartPre=/opt/bin/etcdadm cluster-is-healthy
261-
ExecStartPre=/bin/bash -c 'if [[ -s "/var/run/coreos/etcdadm/snapshots/exported-state-file.json" ]]; then \
262-
/opt/bin/etcdadm migration-import-kube-state exported-state-file.json; fi'
263-
ExecStart=/bin/sleep 3600
264-
TimeoutStartSec=900
263+
ExecStart=/bin/bash -c 'if [[ -s "/var/run/coreos/etcdadm/snapshots/migration.kv" ]]; then \
264+
/opt/bin/etcdadm migration-import-kube-state migration && rm -f /var/run/coreos/etcdadm/snapshots/export-complete; fi; /bin/sleep 3600'
265+
TimeoutStartSec=infinity
265266
{{ end -}}
266267
- name: etcdadm-reconfigure.service
267268
enable: true
@@ -575,7 +576,7 @@ write_files:
575576
content: |
576577
#!/bin/bash -vxe
577578

578-
cfn-init -v -c "etcd-server" --region {{.Region}} --resource {{.Etcd.LogicalName}}${{.EtcdIndexEnvVarName}} --stack "${{.StackNameEnvVarName}}"
579+
cfn-init -v -c "etcd-server" --region {{.Region}} --resource ${CLUSTER_LOGICAL_NAME}i${{.EtcdIndexEnvVarName}} --stack "${{.StackNameEnvVarName}}"
579580

580581
- path: /opt/bin/attach-etcd-volume
581582
owner: root:root
@@ -851,6 +852,7 @@ write_files:
851852
--uuid-file-save=/var/run/coreos/$1.uuid \
852853
--set-env={{.StackNameEnvVarName}}=${{.StackNameEnvVarName}} \
853854
--set-env={{.EtcdIndexEnvVarName}}=${{.EtcdIndexEnvVarName}} \
855+
--set-env=CLUSTER_LOGICAL_NAME=$CLUSTER_LOGICAL_NAME \
854856
--net=host \
855857
--trust-keys-from-https \
856858
{{.AWSCliImage.Options}}{{.AWSCliImage.RktRepo}} --exec=/opt/bin/$1 -- $2
@@ -920,12 +922,13 @@ write_files:
920922
--uuid-file-save=/var/run/coreos/cfn-signal.uuid \
921923
--set-env={{.StackNameEnvVarName}}=${{.StackNameEnvVarName}} \
922924
--set-env={{.EtcdIndexEnvVarName}}=${{.EtcdIndexEnvVarName}} \
925+
--set-env=CLUSTER_LOGICAL_NAME=$CLUSTER_LOGICAL_NAME \
923926
--net=host \
924927
--trust-keys-from-https \
925928
{{.AWSCliImage.Options}}{{.AWSCliImage.RktRepo}} --exec=/bin/bash -- \
926929
-vxec \
927930
'
928-
cfn-signal -e 0 --region {{.Region}} --resource {{.Etcd.LogicalName}}${{.EtcdIndexEnvVarName}} --stack "${{.StackNameEnvVarName}}"
931+
cfn-signal -e 0 --region {{.Region}} --resource ${CLUSTER_LOGICAL_NAME}i${{.EtcdIndexEnvVarName}} --stack "${{.StackNameEnvVarName}}"
929932
'
930933

931934
rkt rm --uuid-file=/var/run/coreos/cfn-signal.uuid || :

pkg/api/etcd.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type Etcd struct {
3030
UnknownKeys `yaml:",inline"`
3131
}
3232

33-
var ETCD_VERSION string = "v99.99"
33+
var ETCD_VERSION string = ""
3434

3535
type EtcdDisasterRecovery struct {
3636
Automated bool `yaml:"automated,omitempty"`
@@ -47,6 +47,9 @@ type EtcdSnapshot struct {
4747

4848
func NewDefaultEtcd() Etcd {
4949
return Etcd{
50+
Cluster: EtcdCluster{
51+
Version: ETCD_VERSION,
52+
},
5053
EC2Instance: EC2Instance{
5154
Count: 1,
5255
InstanceType: "t2.medium",
@@ -185,5 +188,8 @@ func (e Etcd) Version() string {
185188
if e.Cluster.Version != "" {
186189
return e.Cluster.Version
187190
}
191+
if ETCD_VERSION == "" {
192+
panic("The default version of Etcd has not been properly set by the build process, please fix or use another version")
193+
}
188194
return ETCD_VERSION
189195
}

0 commit comments

Comments
 (0)