Search

EKS GitOps Bridge

동기

EKS 클러스터를 운영하기 위해 IaC로써 테라폼을 많이 사용한다. 또 EKS 클러스터 내부의 쿠버네티스 리소스는 ArgoCD를 사용하 GitOps로 관리하는 것이 일반적이다. GitOps Bridge는 이 두 부분을 연결했다고 볼 수 있다. 테라폼으로 클러스터 설치 후, 절차적/명령적인 수작업을 통한 GitOps 구축이 아니라, ArgoCD와 Applications에 대한 IaC를 통한 부트스트래핑이라 할 수 있다.
이처럼 러프한 개념은 계속 생각해 왔지만, 구체적으로 그리고 어떻게 “잘” 만들 수 있을까 고민하고 있었다. 잘하는 방법 중 하나는 많이 쓰는 공개된 것을 가져다 쓰는 것이다. 따라서 gitops bridge pattern이란 것을 품고 있는 eks blueprint for terraform 패턴 중 하나를 쓰기로 했다:
처음에 상상한것 보단 좀 더 상세한 아키텍처이지만, 어떻게 gitops bridge로 bootstrap을 했는지 더 자세히 알 필요가 있었다. 현재(v5.0.0) eks blueprints for terraform의 gitops pattern을 따라했을 때, 기본으로 설치되는 애드온인, aws-load-balancer-controller, 을 통해 알아 본다.

스택 설치

# terraform.tfvars region = "ap-northeast-2" # kubernetes_version = "1.24" enable_gitops_auto_addons = true # default # addons = { # enable_aws_load_balancer_controller = true # enable_metrics_server = true # }
Ruby
복사
루트 모듈의 기본 변수 중 편하게 또는 제약사항에 맞춰 몇 가지 변경한다. 리전을 가까운 곳 그리고 나의 경우 현재 운영하는 쿠버네티스 버전과 맞추어 워크로드 객체를 생성하더라도 API 버전 문제가 없도록 맞추었다.
enable_gitops_auto_addons 를 활성화하여(기본 false) 기본적인 애드온 두 개를 설치한다. 아래 주석처리한 변수는 기본 값이라 써주지 않아도 두 애드온을 설치한다.
$ tf init $ tf apply -target="module.vpc" -auto-approve $ tf apply -target="module.eks" -auto-approve $ tf apply -auto-approve
Shell
복사
그리고 지시대로 테라폼 코드를 적용한다. eks blueprints for terraform은 이렇게 타겟을 나눠 스택으로 실행한다. 이것은 테라폼의 철학이 아니라고 aws에서도 인지하고 알려주었다. 전에 쓴 글과 같이 참고해보자:
아무튼 이처럼 apply하면 module.eks 와 나머지, module.eks_blueprints_addons 그리고 module.gitops_bridge_bootstrap 을 설치한 것이다. 여기서 gitops bridge bootstrapping에 핵심이 되는 eks_blueprints_addons과 gitops_bridge(gitops-bridge-argocd-bootstrap-terraform)에 대해 더 자세히 보자.

eks_blueprints_addons

Create AWS resources needed for addons (IRSA roles …) and expose them to metadata
크게 두 가지 일을 한다. 설명대로 애드온에 필요한 AWS 리소스와 그 메타데이터를 만든다.
애드온은 helm release이다. aws-load-balacner-controller 처럼 aws가 만들거나 또는 그와 상관 없이 오픈소스 버전의, eks에서 애드온으로 사용할 수 있는 컨트롤러의 helm release를 설치한다. 테라폼 모듈 aws-ia/eks-blueprints-addons에서 변수 enable_* 로 제어된다:
애드온에 필요한 리소스 중 대표적으로 IRSA가 있다. IRSA 권한이 필요한 애드온의 aws_iam_policy_document 데이터 소스를 참조하는 것을 볼 수 있다:
그전엔 aws-load-balancer|efs-csi-controller, karpenter 정도 설치할 땐 각각 테라폼 모듈을 사용했는데, 이 부분을 각각의 enable_* 변수로 이 모듈에선 추상화하고 있다.
그리곤 모듈에서 나온 출력, IRSA의 arn 같은 것을 애드온 helm의 values로 넣어 주었다. 이 부분을 메타데이터로 넘겨준다:
aws-load-balancer-controller로 예를 들면, 자식 모듈 전체를 출력하여 irsa arn(aws_load_balancer_controller_iam_role_arn)도 접근할 수 있다.
원래는 이 모듈에서 메타데이터 뿐만 아니라 애드온 자체도 설치했지만, 이를 argocd 같은 gitops 도구에 위임하기 위해 애드온에 해당하는 쿠버네티스 리소스(helm) 생성은 옵션으로 빠졌다:
209
pull
# variables.tf ... ################################################################################ # GitOps Bridge ################################################################################ variable "create_kubernetes_resources" { description = "Create Kubernetes resource with Helm or Kubernetes provider" type = bool default = true } # main.tf ... module "aws_load_balancer_controller" { ... create = var.enable_aws_load_balancer_controller # Disable helm release create_release = var.create_kubernetes_resources ... } ...
Ruby
복사
이런식으로 테라폼 helm_release로 만들지 않는다(모듈에서 helm release도 만들어 주는 줄은 처음 알았다).
gitops bridge 패턴에서 이 eks_blueprints_addons 모듈의 역할은:
“애드온 helm release를 제외한”, IRSA와 같은, 나머지 aws 리소스를 만들고
그 중 애드온 생성에 필요한 메타데이터를 넘긴다.

gitops_bridge_bootstrap

실질적인 gitops bridge를 담당하는 이 모듈은 앞 eks_blueprints_addons의 결과로 나온 메타데이터를 받아, secret과 argocd를 만든다. 그리고 우린 루트 모듈의 enable_gitops_auto_addons 를 활성화하여 applicationset을 직접 만들지 않는다:
❯ tf state list | grep gitops_bridge_bootstrap module.gitops_bridge_bootstrap.helm_release.argocd[0] module.gitops_bridge_bootstrap.helm_release.bootstrap["addons"] module.gitops_bridge_bootstrap.kubernetes_secret_v1.cluster[0]
Ruby
복사
bootstrap(applicationset)은 애드온에 대해서만 만들었다. 메타데이터는 secret에 어노테이션으로 붙는다:
❯ k -n argocd get secret in-cluster -oyaml | yq .metadata.annotations addons_repo_basepath: argocd/ addons_repo_path: bootstrap/control-plane/addons addons_repo_revision: main addons_repo_url: https://github.com/aws-samples/eks-blueprints-add-ons aws_account_id: "<redacted>" aws_cluster_name: getting-started-gitops aws_load_balancer_controller_iam_role_arn: arn:aws:iam::<redacted>:role/alb-controller-20240108072427046000000001 aws_load_balancer_controller_namespace: kube-system aws_load_balancer_controller_service_account: aws-load-balancer-controller-sa aws_region: ap-northeast-2 aws_vpc_id: vpc-<redacted> cluster_name: in-cluster environment: dev
YAML
복사
애드온에서 이걸 어떻게 참조하는지 이따 확인해볼 것이다. 그리고 secret엔 레이블도 있다:
❯ k -n argocd get secret in-cluster -oyaml | yq .metadata.labels argocd.argoproj.io/secret-type: cluster aws_cluster_name: getting-started-gitops cluster_name: in-cluster enable_ack_apigatewayv2: "false" enable_ack_dynamodb: "false" enable_ack_emrcontainers: "false" enable_ack_eventbridge: "false" enable_ack_prometheusservice: "false" enable_ack_rds: "false" enable_ack_s3: "false" enable_ack_sfn: "false" enable_argo_events: "false" enable_argo_rollouts: "false" enable_argo_workflows: "false" enable_argocd: "true" enable_aws_cloudwatch_metrics: "false" enable_aws_ebs_csi_resources: "false" enable_aws_efs_csi_driver: "false" enable_aws_for_fluentbit: "false" enable_aws_fsx_csi_driver: "false" enable_aws_gateway_api_controller: "false" enable_aws_load_balancer_controller: "true" enable_aws_node_termination_handler: "false" enable_aws_privateca_issuer: "false" enable_aws_secrets_store_csi_driver_provider: "false" enable_cert_manager: "false" enable_cluster_autoscaler: "false" enable_cluster_proportional_autoscaler: "false" enable_external_dns: "false" enable_external_secrets: "false" enable_fargate_fluentbit: "false" enable_gatekeeper: "false" enable_gpu_operator: "false" enable_ingress_nginx: "false" enable_karpenter: "false" enable_kube_prometheus_stack: "false" enable_kyverno: "false" enable_metrics_server: "true" enable_prometheus_adapter: "false" enable_secrets_store_csi_driver: "false" enable_velero: "false" enable_vpa: "false" environment: dev kubernetes_version: "1.24"
YAML
복사
대부분 루트 모듈 enable_* 변수를 그대로 가져와, 애드온을 만들지 말지 결정한다고 미리 유추해 볼 수 있다. 그리고 몇몇 클러스터 또는 애드온/워크로드 관련 “정보”(앞의 메타데이터랑 헷갈리기 때문에 정보라 씀) 있다(aws_cluster_name, cluster_name, environment, kubernetes_version)
secret의 내용도 클러스터 관련 정보이다:
❯ k view-secret in-cluster -n argocd name in-cluster ❯ k view-secret in-cluster -n argocd server https://kubernetes.default.svc ❯ k view-secret in-cluster -n argocd config { "tlsClientConfig": { "insecure": false } }
YAML
복사
이것의 역순으로 어떻게 만들어졌고(어디에서 가져왔고), 모듈 내의 어떤걸 만들 때 참조하는지 확인해보면 bridge가 완성된다.
# variables.tf variable "cluster" { description = "argocd cluster secret" type = any default = null } # main.tf locals { cluster_name = try(var.cluster.cluster_name, "in-cluster") ... config = <<-EOT { "tlsClientConfig": { "insecure": false } } EOT argocd = { ... stringData = { name = local.cluster_name server = try(var.cluster.server, "https://kubernetes.default.svc") config = try(var.cluster.config, local.config) } } ... resource "kubernetes_secret_v1" "cluster" { count = var.create && (var.cluster != null) ? 1 : 0 ... data = local.argocd.stringData }
Ruby
복사
cluster 변수가 기본 값이 비었기 때문에(null), locals에서 참조할 때의 기본 값이 쓰인다:
name=in-cluster
server=https://kubernetes.default.svc
config={ "tlsClientConfig": { "insecure": false } }
기본 값을 쓸 수 있는 이유는 argocd가 대상 클러스터 안에 설치 되기 때문이다(클러스터 외부에 두거나 허브-스포크로 여러 클러스터에 멀티 테넌시 구성을 한다면 대상 클러스터로 값을 바꿀 것이다). insecure로 구성한 것도 이런 이유에서다. 물론 zero trust 구성을 하는게 좋겠지만.
그리고 이 정보는 argocd의 applicationset의 cluster generator에도 쓰이는 정보, argocd cluster secret, 이다:
nameserver 값을 사용하여 클러스터를 식별하고 applicationset을 배포할 곳을 지정한다:
apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: cluster-addons namespace: argocd spec: syncPolicy: preserveResourcesOnDeletion: true generators: - clusters: {} # Automatically use all clusters defined within Argo CD template: ... destination: namespace: argocd name: '{{name}}' # 'name' field of the Secret syncPolicy: automated: {}
YAML
복사
enable_gitops_auto_addons 으로 적용된 applicationset이다. 루트 모듈의 테라폼 출력 access_argocd 를 통해 argocd ui에서 확인하자(나온 lb 주소가 http이기 때문에 크롬에선 고급 옵션으로 뚫고 들어가자):
❯ tf output access_argocd <<EOT export KUBECONFIG="/tmp/getting-started-gitops" aws eks --region ap-northeast-2 update-kubeconfig --name getting-started-gitops echo "ArgoCD Username: admin" echo "ArgoCD Password: $(kubectl get secrets argocd-initial-admin-secret -n argocd --template="{{index .data.password | base64decode}}")" echo "ArgoCD URL: https://$(kubectl get svc -n argocd argo-cd-argocd-server -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')" EOT
Shell
복사
addon 으로 시작하는 몇개 app과 cluster-addons 가 있다:
secret의 name 값이 템플릿 된걸 볼 수 있다.
나머지 addon-* application들은 이 cluster-addons 에 의해 연쇄적으로 만들어진 것이다. 그러기 위한 정보는 source.spec 에 있는데, 매니페스트를 확인하면 secret 어노테이션으로부터 가져온다.
apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: cluster-addons namespace: argocd spec: ... generators: - clusters: {} template: metadata: name: cluster-addons spec: project: default source: repoURL: '{{metadata.annotations.addons_repo_url}}' path: '{{metadata.annotations.addons_repo_basepath}}{{metadata.annotations.addons_repo_path}}' targetRevision: '{{metadata.annotations.addons_repo_revision}}' ...
YAML
복사
이 값들은 루트 모듈의 변수로써도 구성이 가능하다:
gitops_addons_org , gitops_addons_repoaddons_repo_url
gitops_addons_basepathaddons_repo_basepath
gitops_addons_pathaddons_repo_path
gitops_addons_revisionaddons_repo_revision
aws-load-balancer-controller 것을 예시로 보면(나머지도 비슷하다) merge generator를 쓴다:
appset의 또 다른 generator인 merge generator는 중첩된 generator 구조로써, 첫번째 generator가 base 뒤따르는 generator 부터 조건에 따라 mergeKeys 기반으로 base의 파라미터를 병합하여 만든다. 위 argocd 문서 예제로 이해해볼 수 있다.
apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: addons-aws-load-balancer-controller spec: ... generators: - merge: mergeKeys: [server] generators: - clusters: # 1. base values: addonChart: aws-load-balancer-controller # anything not staging or prod use this version addonChartVersion: 1.6.0 addonChartRepository: https://aws.github.io/eks-charts selector: matchExpressions: - key: akuity.io/argo-cd-cluster-name operator: NotIn values: [in-cluster] - key: enable_aws_load_balancer_controller operator: In values: ['true'] - clusters: # 2. for staging selector: matchLabels: environment: staging values: addonChartVersion: 1.6.0 - clusters: # 3. for prod selector: matchLabels: environment: prod values: addonChartVersion: 1.6.0 ...
YAML
복사
aws-load-balancer-controller의 경우 기본적으로 차트에 대한 정보(이름, url, 버전)을 주고 staging이나 prod에서 차트 버전을 오버라이드 할 수 있게 구성해놨다. 여기서 중요한건 base generator의 cluster generator label selector가 enable_aws_load_balancer_controller=true 로 필터한다는 점이다(akuity.io의 레이블은 managed argocd 서비스의 것으로 보인다. 이를 통해 gitops bridge 구성 시 충돌을 피하려는 목적인 듯, 지금 중요치 않다). argocd cluster secret in-cluster의 레이블에 루트 모듈 변수부터 넘겨준 enable_* 을 달고 있는 이유이다.
apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: addons-aws-load-balancer-controller spec: ... template: metadata: name: addon-{{name}}-{{values.addonChart}} # 1 spec: project: default sources: - repoURL: '{{metadata.annotations.addons_repo_url}}' # 2 targetRevision: '{{metadata.annotations.addons_repo_revision}}' # 2 ref: values - helm: releaseName: '{{values.addonChart}}' ignoreMissingValueFiles: true valueFiles: - $values/{{metadata.annotations.addons_repo_basepath}}charts/addons/{{values.addonChart}}/values.yaml # 3 - $values/{{metadata.annotations.addons_repo_basepath}}environments/{{metadata.labels.environment}}/addons/{{values.addonChart}}/values.yaml # 3 - $values/{{metadata.annotations.addons_repo_basepath}}clusters/{{name}}/addons/{{values.addonChart}}/values.yaml # 3 values: | clusterName: {{metadata.annotations.aws_cluster_name}} serviceAccount: name: {{metadata.annotations.aws_load_balancer_controller_service_account}} annotations: eks.amazonaws.com/role-arn: {{metadata.annotations.aws_load_balancer_controller_iam_role_arn}} # 4 ...
YAML
복사
1.
cluster generator로 appset의 이름 규칙을 알 수 있다.
2.
values를 참조하기 위한 정보를 넘겨주는데, 같은 레포를 보고 있다. multi sources를 쓰기 때문에 argocd 버전이 2.6+는 되어야 한다.
3.
values를 세 단계로 사용한다. chart, env, cluster. 비록 in-cluster argocd이지만 여러 클러스터 구성을 염두에 두고 만든 것이라 실제로 적용할 때 참고할만하다.
4.
inline values로 irsa arn을 붙여준다.
큰 그림만 보곤 이 부분이 잘 안보였기 때문에 코드를 샅샅이 보았다. 또 appset을 통해 어떻게 구현하는지에 대한 자세한 내용, enable 플래그를 secret 레이블을 통해 받는다거나, env 파이프라인 같은 여러 클러스터에 구성 시 참고할 디렉토리 구조 등을 알아야 했다. aws-samples 것을 그대로 사용할 수도 없기에 특히 appset 부분은 직접 만들어야 한다. 모든 애드온이 선택적으로 필요한 것은 아니니.
cluster-addons의 여러 애드온 중 aws-load-balancer-controller의 appset이 설치된 걸 확인할 수 있고
그 appset으로 만든 app도 확인할 수 있다:
자세한 그림으로 정리하면… (그릴 것)
그리고 aws-samples/eks-blueprints-add-ons/argocd를 참고하여 구성한 appset 디렉토리 구조도 공개할 것이다.
(만들고 링크할 것)

습득 교훈

gitops bridge 패턴은
argocd 같은 gitops 툴을 사용하여
eks 클러스터 내의 워크로드(+ 애드온)을
클러스터 부트스트랩 시 연결(같이 설치)하는 것이다.
argocd appset의 cluster generator 패턴은 cluster secret을 활용한다.
클러스터 식별과 연결을 위해 data를 사용한다.
추가 정보는 메타데이터의 어노테이션이나 레이블을 사용한다.
어노테이션: eks의 경우 irsa 같이 필요한 클러스터 외부(aws) 리소스 정보
레이블: appset에서 애드온 설치 여부 플래그
여러 클러스터에 대한 워크로드 부트스트랩으로써 argocd merge generator를 사용한다.
source가 helm일 때 종류마다(e.g. 클러스터, env, 리전, …) values를 오버라이드 하여 분기할 수 있다.
공통으로 또는 꼭 들어가야 하는 values는 inline으로 주는 것도 방법이다.