동기
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) 생성은 옵션으로 빠졌다:
# 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 구성을 하는게 좋겠지만.
name 과 server 값을 사용하여 클러스터를 식별하고 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_repo → addons_repo_url
•
gitops_addons_basepath → addons_repo_basepath
•
gitops_addons_path → addons_repo_path
•
gitops_addons_revision → addons_repo_revision
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.
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으로 주는 것도 방법이다.