Skip to content

Disaster Recovery

This document details disaster recovery procedures for Welkin. These procedures must be executed by the administrator. Most commands found in these instructions are expected to be run from the compliantkubernetes-apps repository.

Compliance Need

Disaster recovery is mandated by several regulations and information security standards. For example, in ISO 27001:2013, the annexes that mostly concerns disaster recovery are:

Off-site backups

Backups can be set up to be replicated off-site using CronJobs.

If these are encrypted then these off-site backups must first be restored themselves before they can be used to restore other services.

If these are unencrypted then these off-site backups can be used directly to restore other services by reconfiguring which object storage service they are using.

See the instructions in compliantkubernetes-apps for how to restore off-site backups.

When a new region/Infrastructure Provider is used

  • Configure and set base ck8s-configs:

sc-config.yaml:

harbor.persistence.swift.*,objectStorage.sync.*

common-config.yaml:

objectStorage.s3.region, objectStorage.s3.regionEndpoint

secrets.yaml:

dex.connectors.*, harbor.persistence.swift.username, harbor.persistence.swift.password, objectStorage.s3.accessKey, objectStorage.s3.secretKey

.state/s3cfg.ini:

access_key, secret_key, host_base, host_bucket

  • Configure and set custom ck8s-configs:

Examples can be files containing Identity Provider, Infrastructure Provider, or DNS critical information.

OpenSearch

Backup

OpenSearch is set up to store backups in an S3 bucket. There is a CronJob called opensearch-backup in the cluster that is invoking the snapshot process in OpenSearch.

To take a snapshot on-demand, execute:

./bin/ck8s ops kubectl sc -n opensearch-system create job --from=cronjob/opensearch-backup <name-of-job>

Optional: Start new cluster from snapshot

Note

Only perform the steps in this section if you are starting a new cluster from a snapshot. Otherwise, skip ahead to the Restore section.

Before you install OpenSearch you should disable the initial index creation to make the restore process leaner by setting the following configuration option:

opensearch.createIndices: false

Install the OpenSearch suite:

./bin/ck8s ops helmfile sc -l app=opensearch apply

Wait for the installation to complete.

After the installation, continue to the Restore section to proceed with the restore. If you want to restore all indices, use the following indices variable:

indices="kubernetes-*,kubeaudit-*,other-*,authlog-*"

Note

This process assumes that you are using the same S3 bucket as your previous cluster. If you aren't:

  • Register a new S3 snapshot repository to the old bucket as described here
  • Use the newly registered snapshot repository in the restore process

Restore

Set the following variables:

  1. OpenSearch user with permissions to manage snapshots, usually admin
  2. The password for the above user
  3. The URL to OpenSearch
user=admin
password=$(sops -d ${CK8S_CONFIG_PATH}/secrets.yaml | yq4 '.opensearch.adminPassword')
os_url=https://opensearch.$(yq4 '.global.opsDomain' ${CK8S_CONFIG_PATH}/common-config.yaml)

Restoring from off-site backup

  • To restore from an encrypted off-site backup:

    First import the backup into the main S3 service and register the restored bucket as a new snapshot repository:

    curl -kL -u "${user}:${password}" -X PUT "${os_url}/_snapshot/backup-repository?pretty" -H 'Content-Type: application/json' -d'
    {
      "type": "s3",
      "settings": {
        "bucket": "<restored-bucket>",
        "readonly": true
      }
    }
    '
    
    Then restore from this snapshot repository (backup-repository) in OpenSearch.

  • To restore from an unencrypted off-site backup:

    Configure the remote and bucket as the main S3 service and bucket for apps and OpenSearch respectively, then update the OpenSearch Helm releases and perform the restore. It is recommended to either suspend or remove the OpenSearch backup CronJob to prevent it from running while restoring.

    Remember to revert to the regular S3 service afterwards and reactivate the backup CronJob!
    Replace the previous snapshot repository if it is unusable.

List snapshot repositories:

# Simple curl -kL -u "${user}:${password}" "${os_url}/_cat/repositories?v"
id                   type
opensearch-snapshots   s3

# Detailed curl -kL -u "${user}:${password}" "${os_url}/_snapshot/?pretty"
{
  "opensearch-snapshots" : {
    "type" : "s3",
    "settings" : {
      "bucket" : "opensearch-backup",
      "client" : "default"
    }
  }
}

List available snapshots:

snapshot_repo=<name/id from previous step>

# Simple curl -kL -u "${user}:${password}" "${os_url}/_cat/snapshots/${snapshot_repo}?v&s=id"
id                         status start_epoch start_time end_epoch  end_time duration indices successful_shards failed_shards total_shards
snapshot-20211231_120002z SUCCESS 1640952003  12:00:03   1640952082 12:01:22     1.3m      54                54             0           54
snapshot-20220101_000003z SUCCESS 1640995203  00:00:03   1640995367 00:02:47     2.7m      59                59             0           59
snapshot-20220101_120002z SUCCESS 1641038403  12:00:03   1641038533 12:02:13     2.1m      57                57             0           57
...

# Detailed list of all snapshots
curl -kL -u "${user}:${password}" "${os_url}/_snapshot/${snapshot_repo}/_all?pretty"

# Detailed list of specific snapshot curl -kL -u "${user}:${password}" "${os_url}/_snapshot/${snapshot_repo}/snapshot-20220104_120002z?pretty"
{{
  "snapshots" : [
    {
      "snapshot" : "snapshot-20220104_120002z",
      "uuid" : "oClQdNAyTeiEmZb5dVh0SQ",
      "version_id" : 135238127,
      "version" : "1.2.3",
      "indices" : [
        "authlog-default-2021.12.20-000001",
        "authlog-default-2021.12.30-000011",
        "authlog-default-2022.01.03-000015",
        "other-default-2021.12.30-000011",
        ...
      ],
      "data_streams" : [ ],
      "include_global_state" : false,
      "state" : "SUCCESS",
      "start_time" : "2022-01-04T12:00:02.596Z",
      "start_time_in_millis" : 1641297602596,
      "end_time" : "2022-01-04T12:01:07.833Z",
      "end_time_in_millis" : 1641297667833,
      "duration_in_millis" : 65237,
      "failures" : [ ],
      "shards" : {
        "total" : 66,
        "failed" : 0,
        "successful" : 66
      }
    }
  ]
}

You usually select the latest snapshot containing the indices you want to restore. Restore one or multiple indices from a snapshot

Note

You cannot restore a write index (the latest index) if you already have a write index connected to the same index alias (which will happen if you have started to receive logs).

snapshot_name=<Snapshot name from previous step>
# Use "-.*" if index per namespace is enabled
indices="kubernetes-*,kubeaudit-*,other-*,authlog-*"

curl -kL -u "${user}:${password}" -X POST "${os_url}/_snapshot/${snapshot_repo}/${snapshot_name}/_restore?pretty" -H 'Content-Type: application/json' -d'
{
  "indices": "'${indices}'"
}
'

Read the documentation to see the API, all parameters and their explanations.

Restoring OpenSearch Dashboards data

Data in OpenSearch Dashboards (saved searches, visualizations, dashboards, etc) is stored in the indices pointed to by the alias .kibana. To restore that data you first need to delete the index and then do a restore.

This will overwrite anything in the current .kibana_x index. If there is something new that should be saved, then export the saved objects and import them after the restore.

There can be multiple .kibana indices in OpenSearch, the current index should be the one you want to restore. To view your dashboard indices, follow these steps.

snapshot_name=<Snapshot name from previous step>

curl -kL -u "${user}:${password}" -X GET ${os_url}'/.kibana*?pretty' | jq 'keys'

If multiple .kibana_x indices show up, run this to see the index that the alias is currently looking at.

curl -kL -u "${user}:${password}" -X GET ${os_url}'/_alias/.kibana*?pretty' | jq 'keys'

Make sure that the index you want to restore also exists on the snapshot. (May be an issue if you are using an old snapshot)

curl -kL -u "${user}:${password}" -X GET "${os_url}/_snapshot/${snapshot_repo}/${snapshot_name}?pretty" | jq '.snapshots[].indices' | grep .kibana

Note

If you visit the "<os_url>/app/dashboards" page in the OpenSearch GUI after deleting the index and before restoring the index, another empty index .kibana will be created. You need to delete this manually, which can be done with:

curl -kL -u "${user}:${password}" -X DELETE "${os_url}/.kibana?pretty"
index_to_restore=<Index name from previous step>

curl -kL -u "${user}:${password}" -X DELETE "${os_url}/${index_to_restore}?pretty"

curl -kL -u "${user}:${password}" -X POST "${os_url}/_snapshot/${snapshot_repo}/${snapshot_name}/_restore?pretty" -H 'Content-Type: application/json' -d'
{
  "indices": "'${index_to_restore}'"
}
'

Harbor

Backup

Harbor is set up to store backups of the database in an S3 bucket (note that this does not include the actual images, since those are already stored in S3 by default). There is a CronJob called harbor-backup-cronjob in the cluster that is taking a database dump and uploading it to a S3 bucket.

To take a backup on-demand, execute

./bin/ck8s ops kubectl sc -n harbor create job --from=cronjob/harbor-backup-cronjob <name-of-job>

Restore

Restoring from off-site backup

Since Harbor stores both database backups and images in the same bucket it is recommended to restore the off-site backup into the main S3 service first, reconfigure Harbor to use it, then restore the database from it.

Instructions for how to restore Harbor can be found in compliantkubernetes-apps: https://github.com/elastisys/compliantkubernetes-apps/blob/main/restore/harbor/README.md

Velero

These instructions focuses on backups for the Workload Cluster using the Velero CLI. For instructions on using Velero in the Management Cluster see the Grafana section.

Note

The steps for running Velero in this document assumes v0.39 or later of Welkin, as the Velero CLI is now part of the ck8s ops CLI making it easier to operate on both clusters with Velero without having to manually set KUBECONFIG, as long as CK8S_CONFIG_PATH points to the correct environment.

Read more about Velero here.

Note

This documentation uses the Velero CLI, as opposed to Velero CRDs, since that is what is encouraged by upstream documentation.

Backup

Velero is set up to take daily backups and store them in an S3 bucket. The daily backup will not take backups of everything in a Kubernetes cluster, it will instead look for certain labels and annotations. Read more about those labels and annotations here.

It is also possible to take on-demand backups. Then you can freely chose what to backup and do not have to base it on the same labels. Here is a basic example of how to use Velero to take a backup of all Kubernetes resources (though not the data in the volumes by default):

./bin/ck8s ops velero wc backup create manual-backup

If you want to create a backup from existing schedule you can run the following:

./bin/ck8s ops velero wc backup create --from-schedule velero-daily-backup --wait

Tip

Check which arguments you can use by running ./bin/ck8s ops velero wc backup create --help.

Restore

Note

If you are restoring an environment under a new domain name then there is a possibility to reconfigure image references with Velero, but other resources that might contain domain names such as Ingresses, ConfigMaps and Secrets must be updated manually.

If you are restoring an environment and want or need to change the StorageClass of PersistentVolumes then it is possible to configure a StorageClass mapping, see the Velero documentation.

Restoring from a backup with Velero is meant to be a type of disaster recovery. Velero will not overwrite existing Resources when restoring. As such, if you want to restore the state of a Resource that is still running, the Resource must be deleted first.

To restore the state from the latest daily backup, run:

./bin/ck8s ops velero wc restore create --from-schedule velero-daily-backup --wait

Tip

Use ./bin/ck8s ops velero wc restore create --help to see available flags and some examples. If a backup has a status of PartiallyFailed, the argument --allow-partially-failed can be used to restore from such a backup. If a backup or restore gets stuck or has other issues, refer to this guide.

This command will wait until the restore has finished. You can also do partial restorations, e.g. just restoring one namespace, by using different arguments. You can also restore from manual backups by using the flag --from-backup <backup-name>

Persistent Volumes are only restored if a Pod with the backup annotation is restored. Multiple Pods can have an annotation for the same Persistent Volume. When restoring the Persistent Volume it will overwrite any existing files with the same names as the files to be restored. Any other files will be left as they were before the restoration started. So a restore will not wipe the volume clean and then restore. If a clean wipe is the desired behavior, then the volume must be wiped manually before restoring.

Example restoring a volume in WaitForFirstConsumer mode

Restoring volumes with the volume binding mode WaitForFirstConsumer requires some extra steps, as Velero will not restore the data until a Pod binds the volume.

So by either manually creating a Pod of the kind that normally binds the volume or creating a special purpose Pod that only binds the volume, we can let Velero complete the restoration.

For the second case, the following method can be used to get the restore to proceed to completion without starting the actual pods that use them.

Check if there are any volumes stuck in Pending.

kubectl get pvc -A | sed -n '1p;/Pending/p'

If there were no stuck volumes then you are done.

Save the following as: volume-restorer-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: volume-restore-helper
  namespace: default
spec:
  automountServiceAccountToken: false
  containers:
    - image: busybox:stable
      imagePullPolicy: IfNotPresent
      name: sleeper
      resources:
        limits:
          cpu: 10m
          memory: 5Mi
        requests:
          cpu: 10m
          memory: 5Mi
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop:
            - ALL
        privileged: false
        runAsGroup: 1
        runAsNonRoot: true
        runAsUser: 10000
        runAsGroup: 10000
        seccompProfile:
          type: RuntimeDefault
      volumeMounts:
        # Adjust this `mountPath` to match the real pod.
        - mountPath: /mnt
          name: restore-me
      args: # Do nothing while Velero restores data.
        - sleep
        - inf
  tolerations:
    - effect: NoExecute
      key: node.kubernetes.io/not-ready
      operator: Exists
      tolerationSeconds: 300
    - effect: NoExecute
      key: node.kubernetes.io/unreachable
      operator: Exists
      tolerationSeconds: 300
  volumes:
    - name: restore-me
      persistentVolumeClaim:
        claimName: some-data # Enter the volume you want to restore
  securityContext:
    fsGroup: 10000
  1. Fill in the volume based on kubectl get pvc or kubectl get pv.
  2. Adjust the mountPath to match what the original Pod uses.
  3. Create Pod using kubectl apply -f volume-restorer-pod.yaml.
  4. Watch velero restore get for Velero to finish.
  5. Finally, delete the restorer Pod using kubectl delete -f volume-restorer-pod.yaml.

Example restoring a partially failed backup

A backup that has status PartiallyFailed can be restored by using --allow-partially-failed flag:

./bin/ck8s ops velero wc restore create <restore-name> --allow-partially-failed --from-schedule velero-daily-backup --wait

Example restoring a single resource

You can explore a Completed backup as follows

./bin/ck8s ops velero wc backup describe --details <name-of-backup>

and you can then use the following to handpick resources from the backup you want restored:

./bin/ck8s ops velero wc restore create <restore-name>  --include-resources pod,volume --from-backup <backup-name> --include-namespaces <namespace-name> --selector <resource-selector> --wait

Restore from off-site backup

  • Restoring from encrypted off-site backup:

Recover the encrypted bucket into the main S3 service and reconfigure Velero to use this bucket, then follow the regular instructions.

The references in Kubernetes might need to be deleted so Velero can resync from the bucket:

# Note that this is only backup metadata
./bin/ck8s ops kubectl sc -n velero delete backups.velero.io --all

./bin/ck8s ops kubectl wc -n velero delete backups.velero.io --all
  • Restoring from unencrypted off-site backup:

To recover directly from off-site backup the backup-location must be reconfigured:

export CLUSTER="<sc|wc>"
export S3_BUCKET="<off-site-s3-bucket>" # Do not include s3:// prefix
export S3_PREFIX="<service-cluster|workload-cluster>"
export S3_ACCESS_KEY=$(sops -d --extract '["objectStorage"]["sync"]["s3"]["accessKey"]' "$CK8S_CONFIG_PATH/secrets.yaml")
export S3_SECRET_KEY=$(sops -d --extract '["objectStorage"]["sync"]["s3"]["secretKey"]' "$CK8S_CONFIG_PATH/secrets.yaml")
export S3_REGION=$(yq r "$CK8S_CONFIG_PATH/sc-config.yaml" "objectStorage.sync.s3.region")
export S3_ENDPOINT=$(yq r "$CK8S_CONFIG_PATH/sc-config.yaml" "objectStorage.sync.s3.regionEndpoint")
export S3_PATH_STYLE=$(yq r "$CK8S_CONFIG_PATH/sc-config.yaml" "objectStorage.sync.s3.forcePathStyle")

# Delete backups from default backup location, note that this is only the backup metadata
./bin/ck8s ops kubectl "${CLUSTER}" -n velero delete backups.velero.io --all

# Delete default backup location
./bin/ck8s ops velero "${CLUSTER}" backup-location delete default

# Create off-site credentials
kubectl -n velero create secret generic velero-backup \
  --from-literal=cloud="$(echo -e "[default]\naws_access_key_id: ${S3_ACCESS_KEY}\naws_secret_access_key: ${S3_SECRET_KEY}\n")"

# Create off-site backup location
./bin/ck8s ops velero "${CLUSTER}" backup-location create backup \
    --access-mode ReadOnly \
    --provider aws \
    --bucket "${S3_BUCKET}" \
    --prefix "${S3_PREFIX}" \
    --config="region=${S3_REGION},s3Url=${S3_ENDPOINT},s3ForcePathStyle=${S3_PATH_STYLE}" \
    --credential=velero-backup=cloud

Check that the backup-location becomes available:

$ velero backup-location get
NAME     PROVIDER   BUCKET/PREFIX       PHASE       LAST VALIDATED   ACCESS MODE   DEFAULT
backup   aws        <bucket>/<prefix>   Available   <timestamp>      ReadOnly

Then check that the backups becomes available using:

./bin/ck8s ops velero "${CLUSTER}" backup get

When they are available restore one of them using:

./bin/ck8s ops velero "${CLUSTER}" restore create <name-of-restore> --from-backup <name-of-backup>

After the restore is complete Velero should be reconfigured to use the main S3 service again, with a new bucket if the previous one is unusable. Updating or syncing the Helm chart:

./bin/ck8s ops helmfile "${CLUSTER}" -f helmfile -l app=velero -i apply

The secret and the backup metadata from the off-site backups can be deleted:

./bin/ck8s ops kubectl "${CLUSTER}" -n velero delete secret velero-backup
./bin/ck8s ops kubectl "${CLUSTER}" -n velero delete backups.velero.io --all
./bin/ck8s ops kubectl "${CLUSTER}" -n velero delete backupstoragelocations.velero.io backup

Grafana

This section refers to the Management cluster and specifically to the user Grafana, not the ops Grafana.

Backup

Backups of Grafana dashboards created by Application Developers are included in the daily Velero backup in the Management Cluster. We then include the Grafana deployment, Pod, and PVC (including the data). Manual backups can be taken using Velero (include the same resources).

To manually create a backup run:

./bin/ck8s ops velero sc backup create --from-schedule velero-daily-backup --wait

Restore

To restore the Grafana backup you must:

  • Have Grafana installed
  • Delete the Grafana deployment, PVC and PV
./bin/ck8s ops kubectl sc delete deploy -n monitoring user-grafana
./bin/ck8s ops kubectl sc delete pvc -n monitoring user-grafana
  • Restore the Velero backup
./bin/ck8s ops velero sc restore create --from-schedule velero-daily-backup --wait