Translate

четвер, 10 жовтня 2024 р.

Kubernetes. Part IX: EKS Addons/Controllers

Минулого разу ми підняли Kubernetes кластер в AWS із LB контролером, а сьогодні поговоримо про те, як зробити досвід роботи із EKS ще більш повноцінним. Виконаємо установку контролерів/аддонів, що надають додаткові зручності та спрощують обслуговування кластеру. Як і минулого разу, код буде доступний у моєму репозиторію і описаний на останньому, на момент написання статті, Тераформі версії 1.9. Окрім того потрібно мати працюючий EKS кластер, хоч самі контролери (окрім спеціальних) в більшості випадків працюватимуть і на інших реалізаціях Kubernetes в cloud чи навіть bare-metal. Для простоти розуміння надалі будемо вважати, що кластер перебуває у стані, що описаний в попередній статті, тобто присутні працюючі awscli, EKS, AWS LB Controller.

Ця стаття не про те як потрібно організовувати код Terraform і якими правильними враперами його потрібно обкласти, а лише представлений якомога простіший опис інсталяції всіх необхідних ресурсів.  

1. EXTERNAL DNS

Контролер, що слідкує за K8s сервісами та інгресами, та у разі необхідності створює записи на стороні DNS-провайдера. У нашому випадку це буде AWS Route53, проте ExternalDNS також підтримує роботу із багатьма іншими рішеннями як cloud-hosted (AzureDNS, CloudFlare чи DigitalOcean), так і з деякими self-hosted (CoreDNS, PowerDNS, Bind, Windows DNS). Із повним переліком можна ознайомитись за цим посиланням. Перейдемо до опису його установки:


$ cd ../addons/external-dns
$ cat main.tf


data "terraform_remote_state" "eks" {
  backend = "s3"

  config = {
    bucket = "my-tf-state-2023-06-01"
    key    = "my-eks.tfstate"
    region = "us-east-1"
  }
}

data "aws_route53_zone" "this" {
  for_each = toset(var.route53_zone_ids)

  zone_id = each.key
}

locals {
  route53_zone_arns = [for k, v in data.aws_route53_zone.this : v.arn]
}

module "external_dns_pod_identity" {
  source  = "terraform-aws-modules/eks-pod-identity/aws"
  version = "v1.4.1"

  name = "external-dns"

  attach_external_dns_policy = true
  # Array like ["arn:aws:route53:::hostedzone/Z04055631FR4AIE80F1GK",
  #             "arn:aws:route53:::hostedzone/Z056915018W8C59H17J0X"]
  external_dns_hosted_zone_arns = local.route53_zone_arns

  # Pod Identity Associations
  association_defaults = {
    namespace       = "kube-system"
    service_account = "external-dns"
  }

  associations = {
    one = {
      cluster_name = data.terraform_remote_state.eks.outputs.cluster_name
    }
  }
}

resource "helm_release" "this" {
  name       = "external-dns"
  repository = "oci://registry-1.docker.io/bitnamicharts"
  chart      = "external-dns"
  version    = var.helm_package_version
  namespace  = "kube-system"

  set {
    name  = "serviceAccount.create"
    value = "true"
  }

  set {
    name  = "provider"
    value = "aws"
  }

  set {
    name  = "txtOwnerId"
    value = "External-dns addon of ${data.terraform_remote_state.eks.outputs.cluster_name} EKS cluster"
  }

  set {
    name = "zoneIdFilters"
    # list like "{Z04055631FR4AIE80F1GK,Z056915018W8C59H17J0X}"
    value = format("{%s}", join(",", var.route53_zone_ids))
  }

  # it removes unused domains after removing related resources
  set {
    name  = "policy"
    value = "sync"
  }
}

$ cat variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "credentials" {
  default     = ["~/.aws/credentials"]
  description = "where your access and secret_key are stored, you create the file when you run the aws config"
}

variable "helm_package_version" {
  description = "Version of the helm package."
  type        = string
  default     = "8.3.8"
}

variable "route53_zone_ids" {
  description = "Route53 zone IDs to be served by external-dns addon."
  type        = list(string)
  default     = ["Z04055631FR4AIE80F1GK", "Z056915018W8C59H17J0X"]
}

Зроблю певні пояснення до коду:
  • у main.tf ми описуємо використання стейта раніше створеного EKS-кластеру, адже його дані необхідні для ролі, конфігурації самого аддону та асоціації ролі для нього через pod identity
  • за допомогою стороннього модуля створимо IAM роль та її асоціацію через pod identity, котра буде прив'язана до імені сервіс акаунту та простору імен, в котрому він розміщений. Робота додатку через IRSA також можлива і приведена в файлі main.tf_irsa
  • і останній крок - установка Helm-чарту ExternalDNS в kube-system простір імен. Якось так склалось, що я використовую чарт від Bitnami проте є також офіційний, котрий бажано і використовувати
  • також не забуваємо виправити наведені жирним рядки на власні

Запустимо цей terraform-код:

$ terraform init
$ terraform apply


Після установки перевіримо чи працює ExternalDNS (не забуваємо підставити свою назву кластера):

$ aws eks update-kubeconfig --region us-east-1 --name my-eks-NbP3tleo

$ kubectl get pods -n kube-system | grep external-dns
external-dns-55c57c5b9-wxzl2   1/1     Running   0          9m34s

$ kubectl -n kube-system logs -f deploy/external-dns

time="2023-11-25T16:13:31Z" level=info msg="Applying provider record filter for domains: [example1.com. .example1.com. example2.com. .example2.com.]"
time="2023-11-25T16:13:31Z" level=info msg="All records are already up to date"


ExternalDNS працює як із Kubernetes сервісами, так із інгресами. Цього разі розглянемо створення DNS-записів для інгресу із залученням додаткової анотації:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
  name: echoserver
---
apiVersion: v1
kind: Service
metadata:
  name: echoserver
  namespace: echoserver
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: echoserver
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echoserver
  namespace: echoserver
spec:
  selector:
    matchLabels:
      app: echoserver
  replicas: 2
  template:
    metadata:
      labels:
        app: echoserver
    spec:
      containers:
      - image: k8s.gcr.io/e2e-test-images/echoserver:2.5
        imagePullPolicy: Always
        name: echoserver
        ports:
        - containerPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: echoserver
  namespace: echoserver
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/group.name: my-group
    external-dns.alpha.kubernetes.io/hostname: echoserver.example1.com
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /
            pathType: Exact
            backend:
              service:
                name: echoserver
                port:
                  number: 8080
EOF

namespace/echoserver created
service/echoserver created
deployment.apps/echoserver created
ingress.networking.k8s.io/echoserver created

Логи:

$ kubectl -n kube-system logs -f deploy/external-dns

...
time="2023-11-25T16:25:37Z" level=info msg="Desired change: CREATE cname-echoserver.example1.com TXT [Id: /hostedzone/ZONE_ID_1]"
time="2023-11-25T16:25:37Z" level=info msg="Desired change: CREATE echoserver.example1.com A [Id: /hostedzone/ZONE_ID_1]"
time="2023-11-25T16:25:37Z" level=info msg="Desired change: CREATE echoserver.example1.com TXT [Id: /hostedzone/ZONE_ID_1]"
time="2023-11-25T16:25:37Z" level=info msg="3 record(s) in zone example1.com. [Id: /hostedzone/ZONE_ID_1] were successfully updated"

$ kubectl get ing -n echoserver
NAME         CLASS   HOSTS   ADDRESS                                                         PORTS   AGE
echoserver   alb     *       k8s-mygroup-064002fe58-2021024154.us-east-1.elb.amazonaws.com   80      9m1s


В Route53 буде створено alias-запис echoserver.example1.com, що буде вести на IP-адреси домену ALB k8s-mygroup-064002fe58-2021024154.us-east-1.elb.amazonaws.com за яким і буде знаходитись поди echoserver.

Відповідно за адресою http://echoserver.example1.com має відкриватись сторінка нашого додатку.

За замовчуванням externalDNS дивиться як в додаткові анотації, так і в hosts поле інгресу. І вже на основі цих даних створює DNS-записи. Якщо ж виставити значення додаткової анотації external-dns.alpha.kubernetes.io/ingress-hostname-source в значення defined-hosts-only чи annotation-only, то будуть читатись лише hosts поле або анотації відповідно.

ExternalDNS використовує додаткові TXT-записи в зоні для того, щоб трекати власноруч створені записи, проте підтримуються і інші сховища для зберігання сервісних даних, наприклад DynamoDB. Створені TXT записи в зоні Route53 можна побачити на скріншоті приведеному вище.

Посилання:
https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/aws.md
https://repost.aws/knowledge-center/eks-set-up-externaldns
https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.5/guide/integrations/external_dns
https://tech.polyconseil.fr/external-dns-helm-terraform.html


2. EBS CSI DRIVER

 
Драйвер, що необхідний для використання AWS EBS у якості сховища для томів Kubernetes. Файли на диску кожного поду по-замовчуванню є ефемерними, тобто втрачатимуться після кожного перевантаження чи зупинки поду. Якщо ж додаток зберігає стан своєї роботи, то цього буде недостатньо, адже втрата даних буде критичною. Тож такий додаток має змонтувати знову той самий EBS/Kubernetes Volume у разі якихось перевантажень, оновлень чи інших змін/негараздів.

Дистрибуцією EBS CSI драйвера займається сам AWS, проте IAM-роль для його роботи потрібно створювати власноруч, що доволі дивно.

$ cd ../ebs-csi
$ cat main.tf


data "terraform_remote_state" "eks" {
  backend = "s3"

  config = {
    bucket = "my-tf-state-2023-06-01"
    key    = "my-eks.tfstate"
    region = "us-east-1"
  }
}

module "aws_ebs_csi_pod_identity" {
  source  = "terraform-aws-modules/eks-pod-identity/aws"
  version = "v1.5.0"

  name = "aws-ebs-csi"

  attach_aws_ebs_csi_policy = true
  # if you need encryption (you have to need it)
  # aws_ebs_csi_kms_arns      = ["arn:aws:kms:*:*:key/1234abcd-12ab-34cd-56ef-1234567890ab"]

  association_defaults = {
    namespace       = "kube-system"
    service_account = "ebs-csi-controller-sa"
  }

  associations = {
    one = {
      cluster_name = data.terraform_remote_state.eks.outputs.cluster_name
    }
  }
}

resource "aws_eks_addon" "this" {
  cluster_name             = data.terraform_remote_state.eks.outputs.cluster_name
  addon_name               = "aws-ebs-csi-driver"
  addon_version            = var.addon_version
  # service_account_role_arn = module.irsa_role.iam_role_arn
}

data "kubectl_path_documents" "storage_class" {
  pattern = "templates/sc.tpl.yaml"
  vars = {
    # Produces string like "env_name=development,region=us-east-1,team=devops"
    tag_specifications = join(",", [for key, value in var.volume_tags : "${key}=${value}"])
  }
}

resource "kubectl_manifest" "storage_class" {
  count     = length(data.kubectl_path_documents.storage_class.documents)
  yaml_body = element(data.kubectl_path_documents.storage_class.documents, count.index)
}


Та перелік змінних, що будуть відправлені на основний код:

$ cat variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "credentials" {
  default     = ["~/.aws/credentials"]
  description = "where your access and secret_key are stored, you create the file when you run the aws config"
}

variable "addon_version" {
  type        = string
  description = "Version of EKS addon."
  # you can use "latest" here
  default     = "v1.34.0-eksbuild.1"
}

variable "volume_tags" {
  type        = map(any)
  description = "Volume tags."
  # Can't be changed w/o recreating of StorageClass K8s object
  default = {
    env_name = "development"
    region   = "us-east-1"
    team     = "devops"
  }

}


Традиційно прокоментую логіку, хоч вона не сильно відрізніється від попереднього аддону:

  • вичитуємо стейт створеного EKS кластеру
  • створюємо IAM роль та pod identity асоціацію для ServiceAccount-а. Приклад зі створенням IRSA приведений у файлі main.tf_irsa і для його роботи потрібен робочий OIDC провайдер
  • terraform ресурс aws_eks_addon активує addon/драйвер до нашого EKS кластеру через AWS API
  • kubectl_manifest створить новий StorageClass, котрий описаний за відносною адресою templates/sc.yaml.tpl. Він виступає у якості шаблону для майбутніх томів

Запустимо Тераформ код:

$ terraform init
$ terraform apply

$ kubectl get StorageClass

NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
ebs-generic     ebs.csi.aws.com         Delete          WaitForFirstConsumer   false                  91s
gp2 (default)   kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  7h42m


Перший StorageClass створений нами, а наступний - це in-tree реалізація EBS драйвера в межах кодової бази Kubernetes. Він активується одразу із встановленням кластеру. Наразі він вже не підтримується, але і поки планів по його видаленню немає.

$ kubectl get sc ebs-generic -o yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      ...
  creationTimestamp: "2023-11-25T23:17:42Z"
  name: ebs-generic
  resourceVersion: "94073"
  uid: 8441148f-6a7c-4c79-abdd-724c2be2b124
parameters:
  allowAutoIOPSPerGBIncrease: "true"
  csi.storage.k8s.io/fstype: ext4
  encrypted: "true"
  iopsPerGB: "3"
  tagSpecification_0: env_name=development
  tagSpecification_1: region=us-east-1
  tagSpecification_2: team=devops
  throughput: "250"
  type: gp3
provisioner: ebs.csi.aws.com
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

Перевіримо чи він справді працює. Для цього створимо об'єкт PersistentVolumeClaim та підключимо його до поду:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim-2
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ebs-generic
  resources:
    requests:
      storage: 2Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: app-2
spec:
  containers:
  - name: app-2
    image: centos
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: ebs-claim-2
EOF


Чи справді том підключений до поду:

$ kubectl get pv

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                 STORAGECLASS   REASON   AGE
pvc-8ef428ae-ba63-4f4e-9746-7488c8f8dd07   2Gi        RWO            Delete           Bound    default/ebs-claim-2   ebs-generic             37s

$ kubectl describe pv pvc-8ef428ae-ba63-4f4e-9746-7488c8f8dd07

$ kubectl exec -it app-2 -- cat /data/out.txt

неділя, 26 листопада 2023 00:01:54 +0000
неділя, 26 листопада 2023 00:01:54 +0000
неділя, 26 листопада 2023 00:01:54 +0000
неділя, 26 листопада 2023 00:01:54 +0000

$ kubectl exec -it app-2 -- df
Filesystem     1K-blocks    Used Available Use% Mounted on
overlay         20959212 6004804  14954408  29% /
tmpfs              65536       0     65536   0% /dev
tmpfs             985256       0    985256   0% /sys/fs/cgroup
/dev/nvme1n1     1992552      28   1976140   1% /data
/dev/nvme0n1p1  20959212 6004804  14954408  29% /etc/hosts
shm                65536       0     65536   0% /dev/shm
tmpfs            1483088      12   1483076   1% /run/secrets/kubernetes.io/serviceaccount
tmpfs             985256       0    985256   0% /proc/acpi
tmpfs             985256       0    985256   0% /sys/firmware


EBS CSI контролер може бути запущений на Fargate-воркерах, проте атачити диски можна буде лише до подів, що працюють на EC2-інстансах. Це варто мати на увазі.

За наступним посиланням можна подивитись приклади роботи із EBS вольюмами в EKS, а саме зміна його розміру, зняття снепшоту, робота із OS Windows та інше.

Посилання:
https://github.com/kubernetes-sigs/aws-ebs-csi-driver/tree/master/docs
https://github.com/kubernetes-sigs/aws-ebs-csi-driver/tree/master/examples/kubernetes/dynamic-provisioning
https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html
https://docs.aws.amazon.com/eks/latest/userguide/ebs-sample-app.html
https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/modify-volume.md


3. METRICS SERVER

Джерело метрик контейнерів для цілей автоматичного масштабування. Цей аддон збирає ресурсні метрики від Kubelet та надає їх через Metrics API для їхнього використання Horizontal Pod та Vertical Pod скейлерами.

Metrics Server
не можна використовувати для інших цілей, окрім зазначених, наприклад моніторингу. Для цього є kube-state-metrics, котрий зазвичай встановлюється разом із Prometheus оператором.

Код його установки для EKS є доволі простим, адже не потребує додаткових ролей:

$ cd ../metrics-server
$ cat main.tf


data "terraform_remote_state" "eks" {
  backend = "s3"

  config = {
    bucket = "my-tf-state-2023-06-01"
    key    = "my-eks.tfstate"
    region = "us-east-1"
  }
}

resource "helm_release" "this" {
  name       = "metrics-servers"
  repository = "https://kubernetes-sigs.github.io/metrics-server/"
  chart      = "metrics-server"
  version    = var.helm_package_version
  namespace  = var.namespace

  set {
    name  = "replicas"
    value = 2
  }
}


Metrics Server на відміну від ExternalDNS, підтримує роботу в HA конфігурації.

$ cat variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "credentials" {
  default     = ["~/.aws/credentials"]
  description = "where your access and secret_key are stored, you create the file when you run the aws config"
}

variable "helm_package_version" {
  description = "Helm package version."
  type        = string
  default     = "3.12.1"
}

variable "namespace" {
  description = "Namespace service will be deployed to."
  type        = string
  default     = "kube-system"
}


Застосуємо код:

$ terraform init
$ terraform apply


Metrics Server імплементує також вивід команди kubectl top. Тож якщо вона працює - значить аддон встановлено коректно:

$ kubectl top nodes

NAME                                          CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-196-0-139.eu-west-1.compute.internal    125m         3%     2831Mi          41%
ip-10-196-18-125.eu-west-1.compute.internal   213m         5%     2534Mi          37%
ip-10-196-19-22.eu-west-1.compute.internal    62m          1%     1945Mi          28%

$ kubectl top pod metrics-server-76494d95bd-fln5c -n kube-system

NAME                              CPU(cores)   MEMORY(bytes)
metrics-server-76494d95bd-fln5c   5m           41Mi



4. EXTERNAL SECRETS OPERATOR

Це оператор, котрий інтегрує системи управління та зберігання секретів на кшталт AWS Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault та багато інших. Оператор вичитує інформацію зі сторонніх API та автоматично інжектить їх як звичайні Kubernetes секрети.


 

External Secrets Operator імплементує додаткові CRD примітиви: ExternalSecret, SecretStore та ClusterSecretStore, що реалізують дружні до користувача абстракції над сторонніми сервісами. Детальніше про їх використання поговоримо після установки оператора.

Традиційно скористаємось Тераформом і офіційним helm-чартом:

$ cd ../external-secrets
$ cat main.tf


data "terraform_remote_state" "eks" {
  backend = "s3"

  config = {
    bucket = "my-tf-state-2023-06-01"
    key    = "my-eks.tfstate"
    region = "us-east-1"
  }
}


module "external_secrets_pod_identity" {
  source  = "terraform-aws-modules/eks-pod-identity/aws"
  version = "v1.5.0"

  name = "external-secrets"

  attach_external_secrets_policy        = true
  external_secrets_ssm_parameter_arns   = var.external_secrets_ssm_parameter_arns
  external_secrets_secrets_manager_arns = var.external_secrets_secrets_manager_arns
  external_secrets_kms_key_arns         = var.external_secrets_kms_key_arns
  external_secrets_create_permission    = true

  association_defaults = {
    namespace       = "kube-system"
    service_account = "external-secrets"
  }

  associations = {
    one = {
      cluster_name = data.terraform_remote_state.eks.outputs.cluster_name
    }
  }
}

resource "helm_release" "external_secrets" {
  name       = "external-secrets"
  repository = "https://charts.external-secrets.io"
  chart      = "external-secrets"
  version    = var.helm_package_version
  namespace  = "kube-system"

  set {
    name  = "serviceAccount.create"
    value = "true"
  }
}


IRSA-варіант коду знаходиться в main.tf_irsa, проте ми традиційно будемо користуватись pod identity асоціацією ролі.

$ cat variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "credentials" {
  default     = ["~/.aws/credentials"]
  description = "where your access and secret_key are stored, you create the file when you run the aws config"
}

variable "helm_package_version" {
  type        = string
  description = "Version of the helm package."
  default     = "0.10.3"
}

variable "external_secrets_ssm_parameter_arns" {
  type        = list(any)
  description = "Parameter Store arns list."
  default     = ["arn:aws:ssm:us-east-1:*:parameter/dev/*"]
}

variable "external_secrets_secrets_manager_arns" {
  type        = list(any)
  description = "Secrets manager arns list."
  default     = ["arn:aws:secretsmanager:us-east-1:*:secret:dev/*"]
}

variable "external_secrets_kms_key_arns" {
  type        = list(any)
  description = "KMS keys arns for decoding."
  default     = ["arn:aws:kms:us-east-1:*:key/9a04d3c9-1589-42b3-9dca-296b6a51c695"]
}


Тут варто виправити на свої значення KMS-ключа (цей приклад із дефолтним ключем, котрий надає AWS, і тут варто ввести ключ свого акаунту) та адресу секретів. Логіка шляху dev/* в тому, що оператор зможе читати лише секрети під цією маскою.

Застосуємо код:

$ terraform init
$ terraform apply


Створимо тестовий секрет dev/secret в AWS Secrets Manager:


Пересвідчившись, що оператор працює коректно, створимо ClusterSecretStore із описом сервісу, що будемо використовувати для читання секретів:

$ kubectl apply -f - <<EOF
kind: ClusterSecretStore
apiVersion: external-secrets.io/v1beta1
metadata:
  name: global-secret-store
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
EOF

Можна скористатись також і SecretStore CRD, проте він працює лише в межах неймспейса, в якому був створений.

Такий опис провайдера aws буде працювати як для IRSA, так і для pod identity. Наступний же буде працювати лише для IRSA, бо під капотом він використовує aws.AssumeRoleWithWebIdentity() виклик:

$ kubectl apply -f - <<EOF
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: global-secret-store-jwt
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets
            namespace: kube-system
EOF


Окрім цього можна використовувати у якості доступу access-key та aws-secret, що може бути корисним у разі відсутності можливості використання ролі. Про це можна почитати детальніше за наступним посиланням.

Перевіримо чи створився global-secret-store:

$ kubectl get clustersecretstore -A
NAME                  AGE   STATUS   CAPABILITIES   READY
global-secret-store   18h   Valid    ReadWrite      True


Спробуємо прочитати записані поля секрету dev/secret:

$ kubectl apply -f - <<EOF
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: some-svc-secret
  namespace: default
spec:
  refreshInterval: 5m
  secretStoreRef:
    name: global-secret-store
    kind: ClusterSecretStore
  target:
    name: some-svc-secret-es # secret name (will be mirrored as plain secret to K8s)
    creationPolicy: Owner
  data:
  - secretKey: password
    remoteRef:
      key: dev/secret # secret name (on AWS side)
      property: password # secret key name (on AWS side)
  - secretKey: username
    remoteRef:
      key: "dev/secret" # secret name (on AWS side)
      property: username # secret key name (on AWS side)
EOF


$ kubectl get es
NAME                    STORE                 REFRESH INTERVAL   STATUS         READY
some-svc-secret   global-secret-store   20s                SecretSynced   True


Перевіримо чи поля password та username були відображені як Kubernetes секрет, котрий було вказано в ExternalSecret:

$ kubectl get secret
NAME                       TYPE     DATA   AGE
...
some-svc-secret-es   Opaque   2      6m18s


$ kubectl edit secret some-svc-secret-es


Посилання:

https://external-secrets.io/main/
https://external-secrets.io/latest/introduction/faq/
https://cloudhero.io/getting-started-with-external-secrets-operator-on-kubernetes-using-aws-secrets-manager/
https://aws.amazon.com/blogs/containers/leverage-aws-secrets-stores-from-eks-fargate-with-external-secrets-operator/
https://www.giantswarm.io/blog/manage-kubernetes-secrets-using-aws-secrets-manager
https://github.com/external-secrets/external-secrets/issues/2951
https://github.com/external-secrets/external-secrets/issues/478#issuecomment-964413129


5. AWS SECRETS AND CONFIGURATION PROVIDER (ASCP)

На відміну від External Secrets Operator (ESO), окрім синхронізації із K8s секретами, цей спосіб також дозволяє відображати AWS Secrets Manager/Parameter Store секрети як файли файлової системи, що змонтовані в под EKS кластеру. ASCP працює лише із EC2 нод групами і не підтримує Fargate. Функціонал автоматичних ротацій змонтованих секретів також присутній, але він наразі перебуває в статусі альфа.

ASCP для своєї роботи потребує Kubernetes Secrets Store CSI Driver. Останній в свою чергу підтримує і інші провайдери секретів, окрім AWS.

ASCP працює лише із IRSA і модуля для її установки цього разу ніхто не написав:

$ cd ../ascp
$ cat main.tf


data "terraform_remote_state" "eks" {
  backend = "s3"

  config = {
    bucket = "my-tf-state-2023-06-01"
    key    = "my-eks.tfstate"
    region = "us-east-1"
  }
}

resource "aws_iam_policy" "this" {
  name        = "eks-ascp-${data.terraform_remote_state.eks.outputs.cluster_name}-${var.region}"
  path        = "/"
  description = "AWS ASCP controller IAM policy."
  policy      = file(format("%s/templates/policy.json", path.module))
}

resource "aws_iam_role" "this" {
  name = "eks-ascp-${data.terraform_remote_state.eks.outputs.cluster_name}-${var.region}"
  assume_role_policy = templatefile("${path.module}/templates/assume_policy.json_tpl",
    {
      oidc_provider     = data.terraform_remote_state.eks.outputs.oidc_provider,
      oidc_provider_arn = data.terraform_remote_state.eks.outputs.oidc_provider_arn
  })
}

resource "aws_iam_role_policy_attachment" "attach" {
  role       = aws_iam_role.this.name
  policy_arn = aws_iam_policy.this.arn
}

resource "helm_release" "csi_secrets_store" {
  name       = "csi-secrets-store"
  repository = "https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts"
  chart      = "secrets-store-csi-driver"
  version    = var.csi_secrets_store_helm_version
  namespace  = "kube-system"
}

# AWS Secrets and Configuration Provider (ASCP)
resource "helm_release" "aws_secrets_manager" {
  name       = "secrets-store-csi-driver-provider-aws"
  repository = "https://aws.github.io/secrets-store-csi-driver-provider-aws"
  chart      = "secrets-store-csi-driver-provider-aws"
  version    = var.aws_secrets_manager_helm_version
  namespace  = "kube-system"
}


Тут ми встановили 2 хелм чарти та написали для них trusted policy:

$ cat templates/assume_policy.json_tpl

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "${oidc_provider_arn}"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    
                    "${oidc_provider}:aud": "sts.amazonaws.com",
                    "${oidc_provider}:sub": "system:serviceaccount:default:app-ascp-sa"
                }
            }
        }
    ]
}


Тобто сервіс аккаунт додатку, котрий потребуватиме секрет, має лежати в default неймспейсі і мати назву app-ascp-sa. Звісно це можна змінювати на власний розсуд, додавати маски і цим робити правила більш загальними. IAM полісі буде виглядати так:

$ cat templates/policy.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret"
            ],
            "Resource": [
                "arn:aws:secretsmanager:us-east-1:*:secret:dev/*",
            ]
        }
    ]
}

$ cat variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "credentials" {
  default     = ["~/.aws/credentials"]
  description = "where your access and secret_key are stored, you create the file when you run the aws config."
}

variable "csi_secrets_store_helm_version" {
  default     = "1.4.5"
  description = "Helm package version of CSI Secret Store"
}

variable "aws_secrets_manager_helm_version" {
  default     = "0.3.9"
  description = "Helm package version of AWS Secret Manager"
}


Цього разу ми дозволили читання подом всіх секретів із маскою secret:dev/* в регіоні us-east-1.

Застосуємо код:

$ terraform init
$ terraform apply


Створимо тестовий секрет dev/secret в AWS Secrets Manager, абсолютно аналогічний як в попередньому прикладі із ESO, та перейдемо до дій.



$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-ascp
spec:
  selector:
    matchLabels:
      app: app-ascp
  replicas: 1
  template:
    metadata:
      labels:
        app: app-ascp
    spec:
      containers:
      - image: centos
        command: ["/bin/sh"]
        args: ["-c", "sleep 1800"]
        name: app-ascp
        volumeMounts:
        - name: secrets-store-inline
          mountPath: "/conf/"
          readOnly: true
      serviceAccountName: app-ascp-sa
      volumes:
      - name: secrets-store-inline
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: "app-ascp-aws-secret"
---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: app-ascp-aws-secret
spec:
  provider: aws
  parameters:
    objects: |
        - objectName: "dev/secret"
          objectType: "secretsmanager"
          objectAlias: "secret.yaml"
---
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    # your role needs to be here
    eks.amazonaws.com/role-arn: arn:aws:iam::789248082627:role/eks-ascp-my-eks-yXHoHQxx-us-east-1
  name: app-ascp-sa
EOF
---
deployment.apps/app-ascp created
secretproviderclass.secrets-store.csi.x-k8s.io/app-ascp-aws-secret created
serviceaccount/app-ascp-sa created

 

Наведену роль необхідно змінити на власну. Тепер спробуємо знайти та переглянути наш секрет:

$ kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
app-ascp-695cbf7f8c-tg22q   1/1     Running   0          44s

$ kubectl exec -it app-ascp-695cbf7f8c-tg22q -- ls -la /conf/
total 4
drwxrwxrwt 2 root root 60 Oct  8 13:23 .
drwxr-xr-x 1 root root 29 Oct  8 13:23 ..
-rw-r--r-- 1 root root 42 Oct  8 13:23 secret.yaml

$ kubectl exec -it app-ascp-695cbf7f8c-tg22q -- cat /conf/secret.yaml
{"username":"admin","password":"admin123"}%


Посилання:

https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_csi_driver.html
https://secrets-store-csi-driver.sigs.k8s.io/

Немає коментарів:

Дописати коментар