SMS Blog

Getting Started with Amazon EKS Backups Using Velero and Terraform

Amazon EKS provides a robust, managed Kubernetes experience, but a critical piece of the puzzle remains your responsibility: disaster recovery and data protection. A running cluster is not a backup. Configuration errors, accidental deletions, or even region-wide outages can happen. Without a solid backup strategy, you risk significant downtime and data loss.

This blog provides an introductory walkthrough for implementing a backup and restore solution for your EKS clusters using two powerful open-source tools: Velero for orchestrating backups and Terraform for automating the entire infrastructure setup.

We will cover:

  • Why backing up EKS is non-negotiable.
  • An introduction to Velero.
  • A step-by-step guide to deploying Velero on EKS using Terraform, including creating the S3 bucket and securing access with EKS Pod Identity.
  • How to create on-demand and scheduled backups.
  • How to validate backups once they’re created.
  • How to restore a backup to an existing cluster or a completely new one.

Why You Need to Backup EKS Clusters

Before we dive into the “how,” let’s establish the “why.” You need a backup strategy for your EKS cluster for several critical reasons:

  • Disaster Recovery: If your cluster suffers a catastrophic failure or an entire AWS region goes down, a backup is your only path to recovery.
  • Data Protection for Stateful Apps: If you run databases, message queues, or other stateful services on your EKS cluster, you must back up their Persistent Volumes (PVs). Velero integrates with AWS to create EBS snapshots of your PVs, ensuring your application data is safe.
  • Cluster Migrations & Upgrades: Backups are the safest way to migrate applications from one EKS cluster to another or to create a clone of your production environment for testing a major Kubernetes upgrade.
  • Protection Against Human Error: The most common cause of outages is human error. A kubectl delete command issued against the wrong namespace can be devastating. Velero allows you to quickly roll back to a known good state.

What is Velero?

Velero is an open-source tool designed to safely back up and restore Kubernetes cluster resources and persistent volumes. It works by:

  1. Backing up Cluster Objects: It queries the Kubernetes API server and saves the YAML definitions of your resources (Deployments, Services, ConfigMaps, etc.) into a compressed tarball, which it then stores in an object storage bucket, like Amazon S3.
  2. Snapshotting Persistent Volumes: For Persistent Volumes backed by a supported provider, Velero calls the provider’s API to create a snapshot. On AWS, this means creating an EBS snapshot of the volume attached to your pod.

This two-pronged approach ensures you can fully reconstruct both your application’s state and its underlying data.

Deploying Velero with Terraform

Now for the core of our setup. We will use Terraform to provision all the necessary AWS resources and then deploy Velero using the official Helm chart.

Prerequisites:

  • Terraform CLI installed.
  • kubectl installed and configured to access your EKS cluster.
  • Helm CLI installed (for understanding, though Terraform will manage the release).
  • An existing EKS cluster.
  • The EKS Pod Identity Agent add-on installed and active on your cluster. This is the modern, recommended way to handle IAM permissions for EKS workloads.
    NOTE: For a brief overview of EKS Pod Identity including deployment directions refer to the blog post Leveraging EKS Pod Identity on AWS.

Securing Access with EKS Pod Identity

Velero can and should be configured using EKS Pod Identity. As the successor to the older IAM Roles for Service Accounts (IRSA), it is the current AWS best practice for granting pods permissions to AWS resources.

With EKS Pod Identity, we create a dedicated IAM role that Velero’s pod can assume. This is far more secure than creating and managing static IAM user access keys. The EKS Pod Identity agent on your cluster nodes automatically handles vending temporary credentials to the pod, adhering to the principle of least privilege.

Our Terraform code will create:

  • An S3 bucket to store the backup files.
  • An IAM policy with the exact permissions Velero needs.
  • An IAM role with the correct trust policy for EKS Pod Identity.
  • An EKS Pod Identity Association to link the role to the Velero service account.

Create a file named velero.tf and add the following code.

# Configure the AWS Provider
provider "aws" {
  region = "us-east-1" # Change to your EKS cluster's region
}

# Configure the Helm Provider
provider "helm" {
  kubernetes = {
    config_path = "~/.kube/config" # Or your specific kubeconfig path
  }
}

# Assumes you have an existing EKS cluster with the Pod Identity Agent add-on enabled.
data "aws_eks_cluster" "cluster" {
  name = "my-eks-cluster" # <-- IMPORTANT: Change to your cluster name
}

# A unique name for our backup bucket
resource "random_pet" "bucket_name" {
  length = 2
}

# 1. S3 bucket for Velero backups
resource "aws_s3_bucket" "velero_backups" {
  bucket = "${data.aws_eks_cluster.cluster.name}-velero-backups-${random_pet.bucket_name.id}"

  tags = {
    Name = "Velero Backups for ${data.aws_eks_cluster.cluster.name}"
  }
}

# Block all public access to the backup bucket
resource "aws_s3_bucket_public_access_block" "velero_backups" {
  bucket = aws_s3_bucket.velero_backups.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# 2. The IAM Policy: Permissions for Velero
resource "aws_iam_policy" "velero_policy" {
  name        = "${data.aws_eks_cluster.cluster.name}-VeleroPolicy"
  description = "IAM policy for Velero to access S3 and EC2 snapshots"

  # The policy document grants permissions for S3 operations on the backup bucket
  # and EC2 operations for creating and managing EBS snapshots.
  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Effect" : "Allow",
        "Action" : [
          "ec2:DescribeVolumes",
          "ec2:DescribeSnapshots",
          "ec2:CreateTags",
          "ec2:CreateVolume",
          "ec2:CreateSnapshot",
          "ec2:DeleteSnapshot"
        ],
        "Resource" : "*"
      },
      {
        "Effect" : "Allow",
        "Action" : [
          "s3:GetObject",
          "s3:DeleteObject",
          "s3:PutObject",
          "s3:AbortMultipartUpload",
          "s3:ListMultipartUploadParts"
        ],
        "Resource" : [
          "${aws_s3_bucket.velero_backups.arn}/*"
        ]
      },
      {
        "Effect" : "Allow",
        "Action" : [
          "s3:ListBucket"
        ],
        "Resource" : [
          aws_s3_bucket.velero_backups.arn
        ]
      }
    ]
  })
}

# 3. The IAM Role for Velero (for EKS Pod Identity)
resource "aws_iam_role" "velero_role" {
  name = "${data.aws_eks_cluster.cluster.name}-VeleroRole"

  # The fix is in this trust policy
  assume_role_policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "pods.eks.amazonaws.com"
        },
        "Action" : [
          "sts:AssumeRole",
          "sts:TagSession"
        ]
      }
    ]
  })
}

# Attach the policy to the role
resource "aws_iam_role_policy_attachment" "velero_attach" {
  role       = aws_iam_role.velero_role.name
  policy_arn = aws_iam_policy.velero_policy.arn
}

# 4. Create the Pod Identity Association
# This resource links the IAM role to the Velero service account.
resource "aws_eks_pod_identity_association" "velero" {
  cluster_name    = data.aws_eks_cluster.cluster.name
  namespace       = "velero"
  service_account = "velero" # This is the default service account name created by the Helm chart
  role_arn        = aws_iam_role.velero_role.arn

  depends_on = [
    aws_iam_role_policy_attachment.velero_attach
  ]
}

# 5. The Deployment: Velero Helm Chart
resource "helm_release" "velero" {
  name             = "velero"
  repository       = "https://vmware-tanzu.github.io/helm-charts"
  chart            = "velero"
  namespace        = "velero"
  create_namespace = true
  version          = "10.0.4" # Use a specific, recent version

  values = [
    yamlencode({
      # For EKS Pod Identity, we don't need annotations.
      # The aws_eks_pod_identity_association resource handles linking the role.
      serviceAccount = {
        server = {
          create = true,
          name   = "velero"
        }
      }

      # Tell Velero we are not providing a static secret
      credentials = {
        useSecret = false
      }

      # Configuration for the backup storage location
      configuration = {
        backupStorageLocation = [
          {
            name   = "default",
            provider = "aws"
            bucket = aws_s3_bucket.velero_backups.bucket,
            config = {
              region = "us-east-1" # <-- Change to your EKS cluster's region
            }
          }
        ],
        # volumeSnapshotLocation must be an array
        volumeSnapshotLocation = [
          {
            name     = "default",
            provider = "aws",
            config = {
              region = "us-east-1" # <-- Change to your EKS cluster's region
            }
          }
        ]
      }

      # Install the Velero AWS plugin and enable the node-agent for volume snapshots
      initContainers = [
        {
          name  = "velero-plugin-for-aws",
          image = "velero/velero-plugin-for-aws:v1.12.0",
          volumeMounts = [
            {
              mountPath = "/target",
              name      = "plugins"
            }
          ]
        }
      ]

      # Enable snapshots by default
      snapshotsEnabled = true
    })
  ]

  # Velero depends on the pod identity association being created
  depends_on = [
    aws_eks_pod_identity_association.velero
  ]
}

Now, run Terraform to apply this configuration:

Initialize Terraform

terraform init

Apply the configuration

terraform apply --auto-approve

After a few minutes, Terraform will provision the S3 bucket, IAM resources, associate the identity, and deploy Velero to your cluster. You can verify the installation by checking for pods in the velero namespace:

kubectl get pods -n velero
 NAME                      READY   STATUS    RESTARTS   AGE
 velero-785d4596b6-abcde   1/1     Running   0          2m

How Backups Are Created

Now that Velero is installed, you need to install the Velero CLI to interact with it. Follow the official instructions to install it. You can create backups that are scoped to a namespace or that cover the entire cluster.

Backing Up a Specific Namespace

This is useful for backing up a single application or a team’s resources.

Create a backup of only the ‘my-app’ namespace

velero backup create my-app-backup --include-namespaces my-app

Check the status of the backup

velero backup describe my-app-backup

Backing Up the Entire Cluster

For a full disaster recovery backup, you can back up all resources. It’s a best practice to exclude namespaces that contain transient or cluster-specific operational data that you wouldn’t want to restore, like kube-system or Velero’s own namespace.

Create a backup of the entire cluster, excluding some system namespaces

velero backup create full-cluster-backup --exclude-namespaces kube-system,velero

Check the status

velero backup describe full-cluster-backup --details

Creating a Scheduled Backup

For production, you’ll want scheduled backups. This command creates a backup of the production-db and monitoring namespaces every day at 3 AM.

velero schedule create daily-prod-backup
  --schedule="0 3 * * *"
  --include-namespaces production-db,monitoring
  --ttl 336h0m0s # Set a Time-To-Live (TTL) of 14 days (336 hours)

Similarly, you can schedule a daily full-cluster backup for disaster recovery purposes.

Schedule a full cluster backup to run every night at 1 AM

velero schedule create daily-full-cluster-backup
  --schedule="0 1 * * *"
  --exclude-namespaces kube-system,velero
  --ttl 720h0m0s # Set a Time-To-Live (TTL) of 30 days

The backup process will start. Velero will save the object definitions to S3 and trigger EBS snapshots for any PVs in the selected scope.

Managing Schedules with Infrastructure as Code

While using the Velero CLI is great for ad-hoc tasks, in a production environment it is better to manage your backup schedules declaratively, just like any other piece of your infrastructure. This aligns with GitOps principles, where your Git repository is the single source of truth.

When Velero is installed, it adds Custom Resource Definitions (CRDs) to your cluster, including one for a Schedule. This allows you to define a backup schedule in a YAML or JSON manifest and apply it with kubectl or, even better, manage it with Terraform.

Why use IaC for Schedules?

  • Declarative State: Your backup strategy is defined in code. If someone manually deletes a schedule from the cluster, your automation (e.g., a terraform apply) will detect the drift and recreate it, enforcing the desired state.
  • Version Control & Auditing: Every change to your backup schedule (frequency, scope, TTL) is captured in your Git history. This is crucial for auditing and understanding how your backup strategy has evolved.
  • Consistency: It ensures your backup schedules are managed with the same rigor and process (e.g., pull requests) as your application deployments.

Here is how you can define the same daily full-cluster backup schedule using the kubernetes_manifest resource in Terraform. Add this to your velero.tf file:

resource "kubernetes_manifest" "daily_full_cluster_schedule" {
  # This depends on the Helm release to ensure the Velero CRDs exist before we try to create a schedule.
  depends_on = [helm_release.velero]
  manifest = {
    "apiVersion" = "velero.io/v1"
    "kind"       = "Schedule"
    "metadata" = {
      "name"      = "daily-full-cluster-from-tf"
      "namespace" = "velero"
    }
    "spec" = {
      "schedule" = "0 1 * * *" # Daily at 1 AM
      "template" = {
        "includedNamespaces" = ["*"]
        "excludedNamespaces" = ["kube-system", "velero"]
        "storageLocation"    = "default"
        "ttl"                = "720h0m0s" # 30 days
      }
    }
  }
}

How to Validate Your Velero Backups

A backup strategy is incomplete without validation. You must regularly verify that your backups are viable and can be successfully restored. Here is a step-by-step process for inspecting your backups.

Step 1: Check the Backup Status

The first and simplest check is to see if Velero reports the backup as Completed without any errors.

List all backups and their status

velero backup get
# Example Output:
 NAME                     STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
 full-cluster-backup      Completed   0        0          2025-06-11T20:15:00Z            29d       default            <none>
 failing-backup           Failed      1        3          2025-06-11T19:00:00Z            29d       default            <none>

Look for a STATUS of Completed and ERRORS equal to 0. A Failed status requires immediate investigation.

Step 2: Describe the Backup in Detail

This is the most important command for manual inspection. It shows you exactly what Velero captured. Use the --details flag to see a complete list of every resource included in the backup file.

# Describe a specific backup and list all backed-up resources
velero backup describe full-cluster-backup --details

In the output, you will see sections for Persistent Volumes, Resources, and a count of items backed up. Skim this list to ensure the critical resources you expected to be backed up (like Deployments, StatefulSets, PVCs, ConfigMaps, etc.) are present.

Step 3: Check the Backup Logs

If a backup has errors or warnings, or if you just want to be extra thorough, check the logs for that specific backup process. This will show you the step-by-step actions Velero took and any issues it encountered.

# Get the logs for a specific backup
velero backup logs full-cluster-backup

Look for any lines with level=error or level=warning. An error might indicate that Velero couldn’t access a resource or that a volume snapshot failed.

While these inspection steps are crucial, the only way to be 100% certain a backup is valid is to perform a restore. The following sections will discuss in detail how to restore your backups to validate them and recover from disaster.

Restore Overview

Creating backups is only half the process; the true value lies in being able to restore them effectively. Velero provides a flexible and powerful set of restore capabilities to handle a variety of scenarios, from recovering a single deleted resource to migrating an entire cluster. The following sections provide detailed, hands-on examples for these common use cases:

  • Restoring to Your Existing Cluster: This covers the most frequent use case—recovering from accidental deletions or configuration errors within the same EKS cluster. We’ll explore granular restores of specific resources, full namespaces, and even the entire cluster.
  • Restoring to a New Cluster: This is your guide for disaster recovery and cluster migrations. We’ll walk through the process of taking a backup from one cluster and restoring it to a completely new one, including the necessary setup.
  • Advanced Restores: This section details how to use Velero’s remapping features to restore resources into different namespaces, a key technique for cloning production environments for testing and development.

Restoring to Your Existing Cluster

This is your primary disaster recovery scenario. Imagine a developer accidentally deleted resources. Velero provides granular restore capabilities.

Scenario 1: Restore an Entire Cluster

This is the “big red button” for a major incident where multiple namespaces were affected.

# Create a restore from the full-cluster-backup
velero restore create --from-backup full-cluster-backup
# Check the status of the restore
velero restore describe <RESTORE_NAME> # The name is generated automatically, find it with `velero restore get`

Scenario 2: Restore an Entire Namespace

This is the most common use case, recovering from an accidental kubectl delete namespace my-app.

# Let's simulate the disaster
kubectl delete namespace my-app
# Create a restore from the specific backup for that app
velero restore create --from-backup my-app-backup --include-namespaces my-app
# Wait for the restore to complete and verify
velero restore get
kubectl get pods -n my-app # Your pods should be coming back online!

Scenario 3: Restore a Specific Resource Type in a Namespace

This is useful for a more surgical fix, like restoring only the Deployments and ConfigMaps in the my-app namespace without touching other resources.

# Let's say we only deleted a specific deployment
kubectl delete deployment my-app-deployment -n my-app
# Restore ONLY deployments and configmaps within the my-app namespace from the backup
velero restore create --from-backup my-app-backup
  --include-namespaces my-app
  --include-resources deployments,configmaps

Restoring to a New Cluster (Migration)

This process is perfect for migrating to a new EKS cluster or recovering in a different AWS region.

Prepare the New Cluster

On the new EKS cluster, you must first enable the EKS Pod Identity Agent add-on. Then, run the exact same Terraform setup (velero.tf) that you ran on the original cluster. This is critical. It ensures the new Velero instance:

  • Points to the same S3 backup bucket.
  • Has an IAM role and Pod Identity Association with the same permissions to read from that bucket and create volumes from EBS snapshots.

Check for Existing Backups

Once Velero is running on the new cluster, it will automatically sync with the S3 bucket. You should be able to see the backups created by the old cluster.

# On the NEW cluster
velero backup get
# NAME                  STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
# my-app-backup         Completed   0        0          2025-06-11T18:30:00Z            29d       default            <none>
# full-cluster-backup   Completed   0        0          2025-06-11T18:45:00Z            29d       default            <none>

Restore to the New Cluster

The restore command is identical. You’re simply running it on the new cluster.

# On the NEW cluster, restore the desired backup
velero restore create --from-backup my-app-backup
# Verify the resources are created in the new cluster
kubectl get all -n my-app

Your application, along with its persistent data restored from EBS snapshots, will now be running on the new cluster.

Advanced Restores: Remapping and Modifying Resources

Velero’s capabilities extend beyond simple one-to-one restores. You can also manipulate resources as they are restored, which is incredibly useful for tasks like cloning an environment for testing or adapting to changes in a new cluster.

Restoring to a Different Namespace

A common requirement is to restore a production backup into a new namespace to create a staging or development environment. This allows you to test changes against a realistic dataset without affecting production. The --namespace-mappings flag makes this easy.

Restore resources from the ‘prod-ns’ namespace into the ‘staging-ns’ namespace

velero restore create prod-to-staging-clone --from-backup daily-prod-backup
  --namespace-mappings prod-ns:staging-ns

This command tells Velero to find all resources in the backup that were in the prod-ns namespace and create them in the staging-ns namespace instead.

Modifying Resources on Restore

For more complex transformations, such as changing a resource’s storage class, removing labels, or other in-depth modifications, Velero uses restore hooks. These are pods that run custom logic either before a resource is restored in order to modify it or after it is restored in order to perform post-restore actions.

Implementing restore hooks is an advanced topic that involves creating custom container images with your logic. For detailed examples and instructions, refer to the official Velero Restore Hooks documentation.

Conclusion

Protecting your Kubernetes workloads is not an optional extra; it is a fundamental operational responsibility. By combining the power of Velero with the declarative automation of Terraform, you can create a robust, repeatable, and secure backup and recovery system for your EKS clusters.

Following the steps in this guide, you have built a solution that leverages modern AWS best practices like EKS Pod Identity and provides the flexibility to handle everything from minor rollbacks to full-scale cluster migrations. With scheduled backups running, you gain the peace of mind that comes from knowing your application state and data are protected, allowing you to focus on building and innovating.

Picture of Rob Stewart

Rob Stewart

Rob Stewart has over 25 years of experience driving technology innovation. As a cloud architect at SMS, he spearheads the design and implementation of cutting-edge cloud solutions for government and private sector customers, unlocking efficiency and scalability. Prior to SMS, Rob led a global team developing a learning management platform deployed on AWS and was instrumental in driving the adoption of modern devops practices resulting in a dramatic increase in the consistency of software delivery. He is an accredited expert in cloud technologies, with multiple AWS, Azure and Kubernetes certifications. In his free time, he enjoys spending time with his family and two cats. View Rob's LinkedIn

Leave a Reply

Your email address will not be published. Required fields are marked *