restore-from-backup.sh 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. #!/usr/bin/env bash
  2. # Copyright 2016 The Kubernetes Authors.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. # This script performs disaster recovery of etcd from the backup data.
  16. # Assumptions:
  17. # - backup was done using etcdctl command:
  18. # a) in case of etcd2
  19. # $ etcdctl backup --data-dir=<dir>
  20. # produced .snap and .wal files
  21. # b) in case of etcd3
  22. # $ etcdctl --endpoints=<address> snapshot save
  23. # produced .db file
  24. # - version.txt file is in the current directory (if it isn't it will be
  25. # defaulted to "3.0.17/etcd3"). Based on this file, the script will
  26. # decide to which version we are restoring (procedures are different
  27. # for etcd2 and etcd3).
  28. # - in case of etcd2 - *.snap and *.wal files are in current directory
  29. # - in case of etcd3 - *.db file is in the current directory
  30. # - the script is run as root
  31. # - for event etcd, we only support clearing it - to do it, you need to
  32. # set RESET_EVENT_ETCD=true env var.
  33. set -o errexit
  34. set -o nounset
  35. set -o pipefail
  36. # Version file contains information about current version in the format:
  37. # <etcd binary version>/<etcd api mode> (e.g. "3.0.12/etcd3").
  38. #
  39. # If the file doesn't exist we assume "3.0.17/etcd3" configuration is
  40. # the current one and create a file with such configuration.
  41. # The restore procedure is chosen based on this information.
  42. VERSION_FILE="version.txt"
  43. # Make it possible to overwrite version file (or default version)
  44. # with VERSION_CONTENTS env var.
  45. if [ -n "${VERSION_CONTENTS:-}" ]; then
  46. echo "${VERSION_CONTENTS}" > "${VERSION_FILE}"
  47. fi
  48. if [ ! -f "${VERSION_FILE}" ]; then
  49. echo "3.0.17/etcd3" > "${VERSION_FILE}"
  50. fi
  51. VERSION_CONTENTS="$(cat ${VERSION_FILE})"
  52. ETCD_VERSION="$(echo "$VERSION_CONTENTS" | cut -d '/' -f 1)"
  53. ETCD_API="$(echo "$VERSION_CONTENTS" | cut -d '/' -f 2)"
  54. # Name is used only in case of etcd3 mode, to appropriate set the metadata
  55. # for the etcd data.
  56. # NOTE: NAME HAS TO BE EQUAL TO WHAT WE USE IN --name flag when starting etcd.
  57. NAME="${NAME:-etcd-$(hostname)}"
  58. INITIAL_CLUSTER="${INITIAL_CLUSTER:-${NAME}=http://localhost:2380}"
  59. INITIAL_ADVERTISE_PEER_URLS="${INITIAL_ADVERTISE_PEER_URLS:-http://localhost:2380}"
  60. # Port on which etcd is exposed.
  61. etcd_port=2379
  62. event_etcd_port=4002
  63. # Wait until both etcd instances are up
  64. wait_for_etcd_up() {
  65. port=$1
  66. # TODO: As of 3.0.x etcd versions, all 2.* and 3.* versions return
  67. # {"health": "true"} on /health endpoint in healthy case.
  68. # However, we should come with a regex for it to avoid future break.
  69. health_ok="{\"health\": \"true\"}"
  70. for _ in $(seq 120); do
  71. # TODO: Is it enough to look into /health endpoint?
  72. health=$(curl --silent "http://127.0.0.1:${port}/health")
  73. if [ "${health}" == "${health_ok}" ]; then
  74. return 0
  75. fi
  76. sleep 1
  77. done
  78. return 1
  79. }
  80. # Wait until apiserver is up.
  81. wait_for_cluster_healthy() {
  82. for _ in $(seq 120); do
  83. cs_status=$(kubectl get componentstatuses -o template --template='{{range .items}}{{with index .conditions 0}}{{.type}}:{{.status}}{{end}}{{"\n"}}{{end}}') || true
  84. componentstatuses=$(echo "${cs_status}" | grep -c 'Healthy:') || true
  85. healthy=$(echo "${cs_status}" | grep -c 'Healthy:True') || true
  86. if [ "${componentstatuses}" -eq "${healthy}" ]; then
  87. return 0
  88. fi
  89. sleep 1
  90. done
  91. return 1
  92. }
  93. # Wait until etcd and apiserver pods are down.
  94. wait_for_etcd_and_apiserver_down() {
  95. for _ in $(seq 120); do
  96. etcd=$(docker ps | grep -c etcd-server)
  97. apiserver=$(docker ps | grep -c apiserver)
  98. # TODO: Theoretically it is possible, that apiserver and or etcd
  99. # are currently down, but Kubelet is now restarting them and they
  100. # will reappear again. We should avoid it.
  101. if [ "${etcd}" -eq "0" ] && [ "${apiserver}" -eq "0" ]; then
  102. return 0
  103. fi
  104. sleep 1
  105. done
  106. return 1
  107. }
  108. # Move the manifest files to stop etcd and kube-apiserver
  109. # while we swap the data out from under them.
  110. MANIFEST_DIR="/etc/kubernetes/manifests"
  111. MANIFEST_BACKUP_DIR="/etc/kubernetes/manifests-backups"
  112. mkdir -p "${MANIFEST_BACKUP_DIR}"
  113. echo "Moving etcd(s) & apiserver manifest files to ${MANIFEST_BACKUP_DIR}"
  114. # If those files were already moved (e.g. during previous
  115. # try of backup) don't fail on it.
  116. mv "${MANIFEST_DIR}/kube-apiserver.manifest" "${MANIFEST_BACKUP_DIR}" || true
  117. mv "${MANIFEST_DIR}/etcd.manifest" "${MANIFEST_BACKUP_DIR}" || true
  118. mv "${MANIFEST_DIR}/etcd-events.manifest" "${MANIFEST_BACKUP_DIR}" || true
  119. # Wait for the pods to be stopped
  120. echo "Waiting for etcd and kube-apiserver to be down"
  121. if ! wait_for_etcd_and_apiserver_down; then
  122. # Couldn't kill etcd and apiserver.
  123. echo "Downing etcd and apiserver failed"
  124. exit 1
  125. fi
  126. read -rsp $'Press enter when all etcd instances are down...\n'
  127. # Create the sort of directory structure that etcd expects.
  128. # If this directory already exists, remove it.
  129. BACKUP_DIR="/var/tmp/backup"
  130. rm -rf "${BACKUP_DIR}"
  131. if [ "${ETCD_API}" == "etcd2" ]; then
  132. echo "Preparing etcd backup data for restore"
  133. # In v2 mode, we simply copy both snap and wal files to a newly created
  134. # directory. After that, we start etcd with --force-new-cluster option
  135. # that (according to the etcd documentation) is required to recover from
  136. # a backup.
  137. echo "Copying data to ${BACKUP_DIR} and restoring there"
  138. mkdir -p "${BACKUP_DIR}/member/snap"
  139. mkdir -p "${BACKUP_DIR}/member/wal"
  140. # If the cluster is relatively new, there can be no .snap file.
  141. mv ./*.snap "${BACKUP_DIR}/member/snap/" || true
  142. mv ./*.wal "${BACKUP_DIR}/member/wal/"
  143. # TODO(jsz): This won't work with HA setups (e.g. do we need to set --name flag)?
  144. echo "Starting etcd ${ETCD_VERSION} to restore data"
  145. if ! image=$(docker run -d -v ${BACKUP_DIR}:/var/etcd/data \
  146. --net=host -p ${etcd_port}:${etcd_port} \
  147. "k8s.gcr.io/etcd:${ETCD_VERSION}" /bin/sh -c \
  148. "/usr/local/bin/etcd --data-dir /var/etcd/data --force-new-cluster"); then
  149. echo "Docker container didn't started correctly"
  150. exit 1
  151. fi
  152. echo "Container ${image} created, waiting for etcd to report as healthy"
  153. if ! wait_for_etcd_up "${etcd_port}"; then
  154. echo "Etcd didn't come back correctly"
  155. exit 1
  156. fi
  157. # Kill that etcd instance.
  158. echo "Etcd healthy - killing ${image} container"
  159. docker kill "${image}"
  160. elif [ "${ETCD_API}" == "etcd3" ]; then
  161. echo "Preparing etcd snapshot for restore"
  162. mkdir -p "${BACKUP_DIR}"
  163. echo "Copying data to ${BACKUP_DIR} and restoring there"
  164. number_files=$(find . -maxdepth 1 -type f -name "*.db" | wc -l)
  165. if [ "${number_files}" -ne "1" ]; then
  166. echo "Incorrect number of *.db files - expected 1"
  167. exit 1
  168. fi
  169. mv ./*.db "${BACKUP_DIR}/"
  170. snapshot="$(ls ${BACKUP_DIR})"
  171. # Run etcdctl snapshot restore command and wait until it is finished.
  172. # setting with --name in the etcd manifest file and then it seems to work.
  173. if ! docker run -v ${BACKUP_DIR}:/var/tmp/backup --env ETCDCTL_API=3 \
  174. "k8s.gcr.io/etcd:${ETCD_VERSION}" /bin/sh -c \
  175. "/usr/local/bin/etcdctl snapshot restore ${BACKUP_DIR}/${snapshot} --name ${NAME} --initial-cluster ${INITIAL_CLUSTER} --initial-advertise-peer-urls ${INITIAL_ADVERTISE_PEER_URLS}; mv /${NAME}.etcd/member /var/tmp/backup/"; then
  176. echo "Docker container didn't started correctly"
  177. exit 1
  178. fi
  179. rm -f "${BACKUP_DIR}/${snapshot}"
  180. fi
  181. # Also copy version.txt file.
  182. cp "${VERSION_FILE}" "${BACKUP_DIR}"
  183. export MNT_DISK="/mnt/disks/master-pd"
  184. # Save the corrupted data (clean directory if it is already non-empty).
  185. rm -rf "${MNT_DISK}/var/etcd-corrupted"
  186. mkdir -p "${MNT_DISK}/var/etcd-corrupted"
  187. echo "Saving corrupted data to ${MNT_DISK}/var/etcd-corrupted"
  188. mv /var/etcd/data "${MNT_DISK}/var/etcd-corrupted"
  189. # Replace the corrupted data dir with the restored data.
  190. echo "Copying restored data to /var/etcd/data"
  191. mv "${BACKUP_DIR}" /var/etcd/data
  192. if [ "${RESET_EVENT_ETCD:-}" == "true" ]; then
  193. echo "Removing event-etcd corrupted data"
  194. EVENTS_CORRUPTED_DIR="${MNT_DISK}/var/etcd-events-corrupted"
  195. # Save the corrupted data (clean directory if it is already non-empty).
  196. rm -rf "${EVENTS_CORRUPTED_DIR}"
  197. mkdir -p "${EVENTS_CORRUPTED_DIR}"
  198. mv /var/etcd/data-events "${EVENTS_CORRUPTED_DIR}"
  199. fi
  200. # Start etcd and kube-apiserver again.
  201. echo "Restarting etcd and apiserver from restored snapshot"
  202. mv "${MANIFEST_BACKUP_DIR}"/* "${MANIFEST_DIR}/"
  203. rm -rf "${MANIFEST_BACKUP_DIR}"
  204. # Verify that etcd is back.
  205. echo "Waiting for etcd to come back"
  206. if ! wait_for_etcd_up "${etcd_port}"; then
  207. echo "Etcd didn't come back correctly"
  208. exit 1
  209. fi
  210. # Verify that event etcd is back.
  211. echo "Waiting for event etcd to come back"
  212. if ! wait_for_etcd_up "${event_etcd_port}"; then
  213. echo "Event etcd didn't come back correctly"
  214. exit 1
  215. fi
  216. # Verify that kube-apiserver is back and cluster is healthy.
  217. echo "Waiting for apiserver to come back"
  218. if ! wait_for_cluster_healthy; then
  219. echo "Apiserver didn't come back correctly"
  220. exit 1
  221. fi
  222. echo "Cluster successfully restored!"