EKS-Fuckups – und was wir daraus lernen

Einen vollständig verwalteten Kubernetes-Service wie EKS zu nutzen, ist eine gute Idee. Einiges an Komplexität und Aufwand im Betrieb wird dadurch überflüssig oder auf ein Mindestmaß reduziert.

Natürlich gibt es auch Stolpersteine. Und Dinge, die man so nicht auf dem Schirm hatte. Was haben wir erfahren? Welchen Herausforderungen standen wir schon gegenüber?

Einige Einsichten möchten wir in diesem Post mit euch teilen. Und: Ein paar praktische Tipps gibts am Ende des Blogartikels auch noch.

Rückblick

Zugegeben: auch ohne EKS ist Kubernetes allein schon anspruchsvoll. Was typischerweise bei EKS im Speziellen schiefgehen kann, zeigt unsere TOP-5 Liste:

Zu kleine VPC Subnets

Ein typisches VPC besteht aus jeweils mindestens drei Public und drei Private Subnets. Doch warum eigentlich drei? Das entspricht der Anzahl der sogenannten Availability Zones (= „AZs“) in einer Region, z. B. „Frankfurt (eu-central-1)“. Andere Regionen (z. B. Virginia) haben sogar sechs!

Das Problem: in ein typisches „/24er“ Subnetz passen 254 nutzbare IP-Adressen. Multipliziert mit drei klingt das erstmal ausreichend. Pods, die kommen und gehen, verbrauchen aber stets immer mindestens eine IP-Adresse aus dem jeweiligen Subnetz. Dazu kommt noch ein dreißig-sekündiger „ENI Cooldown Cache“ – erst hiernach wird die verwendete IP-Adresse wieder freigegeben. Das kann bei sehr vielen, kurz laufenden Jobs (z. B. Kubernetes CronJobs) schnell mal zum Engpass werden. 

Unser Tipp: Plant eure Subnetze gleich zu Beginn ruhig großzügig ein. Mindestens ein „/21er“ Netz sollte es pro AZ schon sein. Beachtet auch die Limitierung der jeweiligen Instanztypen hinsichtlich maximal möglicher Pods! Auf GitHub gibts ein kleines Shell Skript, das euch bei der Planung und Berechnung hilft.

PVC sind an AZ gebunden

Quasi ein Folgethema zum ersten Punkt: Elastic Block Storage (kurz: EBS) ist die virtuelle Festplatte eurer EC2-Instanzen. Verwendet ihr z. B. Kubernetes StatefulSets und wird der Pod hierfür in einer bestimmten AZ gestartet (Beispiel: eu-central-1b), dann bleibt das Persistent Volume („PV“) auch immer in dieser AZ (selbst nach einem Pod-Restart). Damit büßt man gewissermaßen ein wenig Kubernetes-Magic ein, da Pods u. U. nicht immer gleichmäßig über alle AZ verteilt werden. 

Unser Tipp: Wenn möglich, keine oder nur wenige Persistent Volumes verwenden. Schaut doch stattdessen einmal, was es „drumrum“ an ausgelagerten Plattform-Diensten wie z. B. RDS für eure Postgres-Datenbanken oder ElastiCache für das Redis-Cluster. Dann ist zumindest mal das Persistenz-Problem aus dem Cluster!

EKS Cluster als IAM User erstellt

Damit hat praktisch nur eine Person programmatischen Zugriff auf das neue Cluster. Natürlich könnt ihr später noch weitere IAM-User oder -Rollen hinzufügen, problematisch wird es aber dann, wenn ihr das eben nicht getan habt und die Person das Unternehmen verläßt …

Unser Tipp: Erstellt das Cluster mit einer IAM-Rolle. Hier greift das Konzept der IAM Assumed Roles. Ausserdem unterstützt EKS auch OpenID Connect Provider, worüber ihr die Authentifizierung der Nutzer realisieren könnt.

Updates versäumt

Amazon bietet in der Regel vier stabile Kubernetes-Versionen zeitgleich an. Nicht mehr unterstützte, also zu alte Versionen, werden nach Ankündigung dann irgendwann zwangsaktualisiert. Man ist also besser vorbereitet und aktualisiert spätestens alle vier Monate:

  • Control Plane
  • Worker Nodes
  • EKS Addons

Unser Tipp: Bleibt mit dem Amazon-EKS-Release-Kalender stets auf dem Laufenden und wartet nicht zu lange auf die nächste Version. Ein nicht-produktiv-Cluster ist immer zu empfehlen, um die Update-Prozedur vorab zu proben.

Kein IRSA / zu großzügige Berechtigungen

Hinter dem Akronym „IRSA“ verbirgt sich das Konzept, IAM-Rollen auf der einen Seite mit Kubernetes ServiceAccounts auf der anderen Seite zu verbinden. Hiermit können Pods, also die kleinste Einheit in Kubernetes, mit minimalen Berechtigungen ausgestattet werden, um auf AWS-Dienste wie S3 oder SQS zuzugreifen.

„Least Privilege“ zahlt sich also auch hier in jedem Fall aus, da ihr nicht mehr ganze Worker Nodes mit allen möglichen IAM-Berechtigungen bestücken müsst. Kurz: ein Pod wird einem ServiceAccount zugeordnet, der wiederum mit einer IAM-Rolle annotiert ist. Diese Rolle enthält Verweise auf eine oder mehrere IAM Policies. Und das praktische daran: die Zuweisung der Berechtigungen passiert automatisch im Hintergrund und sicher mittels Web Identity Tokens. 

Unser Tipp: Folgt konsequent dem IRSA-Ansatz, um wirklich nur nötige Berechtigungen zu erteilen, wenn ihr AWS-Services aus Kubernetes heraus nutzen wollt. Und: Tools wie popeye helfen euch auch dabei, bestehende Workloads zu überprüfen.

Gibt’s da nicht was von Terraform?

Zum Abschluss noch ein konkretes, wenn auch kompaktes Beispiel: damit erstellen wir einen EKS-Cluster in der aktuellen Version 1.24 und das notwendige VPC gleich mit. Bonus: wir bauen ein bisschen Security mit ein (IMDSv2 enforced) und zeigen, wie man moderne gp3 EBS Storages vorbereitet:

data "aws_availability_zones" "available" {}
 
locals {
  name   = "eks-demo"
  region = var.aws_region
 
  vpc_cidr = "10.0.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)
 
  tags = {
    Name = local.name
  }
}
 
data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_id
}
 
data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_id
}
 
provider "helm" {
  kubernetes {
    host                   = data.aws_eks_cluster.cluster.endpoint
    cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data)
    token                  = data.aws_eks_cluster_auth.cluster.token
  }
}
 
provider "kubernetes" {
  host                   = data.aws_eks_cluster.cluster.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data)
  token                  = data.aws_eks_cluster_auth.cluster.token
}
 
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 3.0"
 
  name = local.name
  cidr = local.vpc_cidr
 
  azs             = local.azs
  public_subnets  = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 5, k)]
  private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 5, k + 3)]
 
  enable_nat_gateway   = true
  single_nat_gateway   = true
  enable_dns_hostnames = true
 
  manage_default_network_acl    = true
  default_network_acl_tags      = { Name = "${local.name}-default" }
  manage_default_route_table    = true
  default_route_table_tags      = { Name = "${local.name}-default" }
  manage_default_security_group = true
  default_security_group_tags   = { Name = "${local.name}-default" }
 
  public_subnet_tags = {
    "Name"                                = "${local.name}-public"
    "kubernetes.io/cluster/${local.name}" = "shared"
    "kubernetes.io/role/elb"              = 1
  }
 
  private_subnet_tags = {
    "Name"                                = "${local.name}-private"
    "kubernetes.io/cluster/${local.name}" = "shared"
    "kubernetes.io/role/internal-elb"     = 1
  }
 
  tags = local.tags
}
 
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 18.0"
 
  cluster_name    = local.name
  cluster_version = "1.24"
 
  cluster_endpoint_private_access = true
  cluster_endpoint_public_access  = true
 
  cluster_addons = {
    coredns = {
      resolve_conflicts = "OVERWRITE"
    }
    kube-proxy = {}
    vpc-cni = {
      resolve_conflicts = "OVERWRITE"
    }
    aws-ebs-csi-driver = {
      service_account_role_arn = module.iam_assumable_role_ebs_csi.iam_role_arn
      resolve_conflicts        = "OVERWRITE"
    }
  }
 
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets
 
  eks_managed_node_group_defaults = {
    ami_type = "AL2_x86_64"
 
    block_device_mappings = {
      xvda = {
        device_name = "/dev/xvda"
        ebs = {
          delete_on_termination = true
          encrypted             = true
          volume_size           = 50
          volume_type           = "gp3"
        }
 
      }
    }
 
    metadata_options = {
      http_endpoint               = "enabled"
      http_put_response_hop_limit = 2
      http_tokens                 = "required"
    }
 
    iam_role_attach_cni_policy = true
  }
 
  eks_managed_node_groups = {
    on_demand_t3_large = {
      min_size     = 3
      max_size     = 6
      desired_size = 3
 
      instance_types = ["t3.large"]
      capacity_type  = "ON_DEMAND"
      labels         = { nodegroup = "on_demand_t3_large" }
      update_config = {
        max_unavailable_percentage = 50
      }
    },
  }
 
  # Fargate Profile(s)
  fargate_profiles = {
    default = {
      name = "fargate"
      selectors = [
        {
          namespace = "fargate"
        }
      ]
    }
  }
 
  # aws-auth configmap
  manage_aws_auth_configmap = true
 
  tags = local.tags
}
 
module "iam_assumable_role_ebs_csi" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc"
  version = "~> 5.2.0"
 
  create_role                   = true
  role_name                     = "ebs-csi"
  provider_url                  = replace(module.eks.cluster_oidc_issuer_url, "https://", "")
  role_policy_arns              = ["arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"]
  oidc_fully_qualified_subjects = ["system:serviceaccount:kube-system:ebs-csi-controller-sa"]
 
  tags = merge(
    local.tags,
    {
      Name   = "ebs-csi"
    }
  )
 
}

Ausblick, oder: was wir daraus lernen

Don’t click. Just Code! Oder anders ausgedrückt: wir setzen auf Infrastructure as Code, vorzugsweise mit Terraform; hierfür existieren viele erprobte, reife Module, die die Verwaltung der AWS-Infrastruktur zum Kinderspiel machen. Unter anderen setzen wir Module für VPC, IAM, EKS und S3 ein. EKS Addons (u. a. CoreDNS oder EBS-CSI) lassen sich damit ebenso einfach verwalten wie weitere, essentielle Helm Charts, z. B. für den ALB IngressController, den Metrics-Server oder ExternalDNS. 

Plant euer EKS Setup in punkto VPC und Netzwerk gründlich. Ein späterer Umbau ist zwar möglich, aber nicht immer ohne Downtimes realisierbar. Skripte wie max-pods-calculator.sh helfen euch, die passenden EC2-Instanzen für euren Cluster auszuwählen. 

Nicht nur Software & Code kann man testen: Infrastruktur sollte hierbei nicht aussen vor bleiben. Habt ihr schon einmal Tools wie chef/inspecbats/detik oder cnspec in euren CI-Prozess integriert? 

EKS bietet neben on-demand on Spot Instanz basierten Deployment Modellen auch Fargate als Serverless-Option an: hier braucht ihr euch nicht um Server (EC2 Instanzen) und Kapazitäten zu kümmern. 

Nicht zuletzt lohnt immer ein Blick auf EKS Best Practices: neben Security und Performance werden dort weitere operative Themen umfangreich behandelt.

Für neue Blogupdates anmelden: