EKS Blueprints WorkshopのArgo CDの章の実装を調べた
EKS Blueprints for Terraformのworkshopをやってみました。面白かったのですが、「なんかできた!」くらいの理解に留まったので、どのように実現しているのかもう少し調べてみました。特にArgo CDが登場する章が理解できていなかったので、そこを調べました。
この章でやっていることのおさらい
端的に言うと、この章では各種addonをデプロイしています。
ここでいうaddonの定義は、クラスターに特定の機能を追加する一連のコンポーネントをまとめたもののことです。EKSの機能でいうところのアドオンのことはEKS managed addonと呼んでいます。単にaddonとだけ言うと、これらEKS managed addonと、そうでないものを併せた総称として扱われています。
今回この章ではaddonとして以下がデプロイされます。この中でEKS managed addonなのはAmazon EBS CSI ドライバーです。
- Argo CD
- AWS Load Balancer Controller
- Amazon EBS CSI ドライバー
- AWS for Fluent Bit
- Metrics Server
そして、EKS managed addonとArgo CDはTerraformで直接デプロイ、そうでないaddonはTerraformがArgo CDのapplicationをデプロイし、そのArgo CDのapplicationが各addonのHelm releaseをデプロイしています。
Argo CDのapplicationというのは、ソースとなるGitリポジトリと、デプロイ先となるクラスターについての情報をまとめているリソースです。マニフェストファイルで作成する場合は以下のようなものになります。
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: guestbook namespace: argocd spec: project: default source: repoURL: https://github.com/argoproj/argocd-example-apps.git targetRevision: HEAD path: guestbook destination: server: https://kubernetes.default.svc namespace: guestbook
ですので、こういったapplicationの定義がどこかでされている筈です。どこでどのようにされているのか調べていきましょう。
出発点となるコード
workshopのページから引用します。
locals { (略) #--------------------------------------------------------------- # ARGOCD ADD-ON APPLICATION #--------------------------------------------------------------- addon_application = { path = "chart" repo_url = "https://github.com/[ YOUR GITHUB USER HERE ]/eks-blueprints-add-ons.git" add_on_application = true } #--------------------------------------------------------------- # ARGOCD WORKLOAD APPLICATION #--------------------------------------------------------------- workload_application = { path = "envs/dev" repo_url = "https://github.com/[ YOUR GITHUB USER HERE ]/eks-blueprints-workloads.git" add_on_application = false } (略) }
(略) module "kubernetes_addons" { source = "github.com/aws-ia/terraform-aws-eks-blueprints?ref=v4.12.2/modules/kubernetes-addons" eks_cluster_id = module.eks_blueprints.eks_cluster_id #--------------------------------------------------------------- # ARGO CD ADD-ON #--------------------------------------------------------------- enable_argocd = true argocd_manage_add_ons = true # Indicates that Argo CD is responsible for managing/deploying Add-ons. argocd_applications = { addons = local.addon_application #workloads = local.workload_application #We comment it for now } argocd_helm_config = { set = [ { name = "server.service.type" value = "LoadBalancer" } ] } #--------------------------------------------------------------- # ADD-ONS - You can add additional addons here # https://aws-ia.github.io/terraform-aws-eks-blueprints/add-ons/ #--------------------------------------------------------------- enable_aws_load_balancer_controller = true enable_karpenter = false enable_amazon_eks_aws_ebs_csi_driver = true enable_aws_for_fluentbit = true enable_metrics_server = true }
Terraform moduleの構成
main.tf
の中でEKS Blueprintsの sub moduleであるkubernetes-addons
を呼んでいます。このkubernetes-addons
から更に別のsub moduleを呼んでいて、それがさらに別のsub moduleを…とmoduleが入れ子状態になっています。まずこの点を整理します。
- kubernetes-addonsは各addonのハブになるmoduleで、このmoduleを呼んで
enable xxx
というattributeにtrueを渡せばそのxxxのaddonのmoduleを呼んでくれる、というようなmoduleです。 - argocd,aws-load-balancer-controller,aws-for-fluentbit,aws-ebs-csi-driverは言わずもがな、それぞれのaddonを提供するmoduleですね。
- ※ metrics-server moduleも呼ばれていますが、作るリソースが無いので上記図では割愛しています。
- helm-addonは、helm経由でaddonを提供する場合に利用される汎用moduleです。
- irsaはIRSA(IAM Role for Service Account)を利用する場合に関連リソースを作成するmoduleです。IRSAについては以下をどうぞ。
本題: 実装解説
kubernetes-addons module
前述の通り、kubernetes-addonsは各addonのハブになるmoduleです。以下main.tf
のハイライトした行でそれぞれのaddonが有効化されるように指定されています。
(略) module "kubernetes_addons" { source = "github.com/aws-ia/terraform-aws-eks-blueprints?ref=v4.12.2/modules/kubernetes-addons" eks_cluster_id = module.eks_blueprints.eks_cluster_id #--------------------------------------------------------------- # ARGO CD ADD-ON #--------------------------------------------------------------- enable_argocd = true argocd_manage_add_ons = true # Indicates that Argo CD is responsible for managing/deploying Add-ons. argocd_applications = { addons = local.addon_application #workloads = local.workload_application #We comment it for now } argocd_helm_config = { set = [ { name = "server.service.type" value = "LoadBalancer" } ] } #--------------------------------------------------------------- # ADD-ONS - You can add additional addons here # https://aws-ia.github.io/terraform-aws-eks-blueprints/add-ons/ #--------------------------------------------------------------- enable_aws_load_balancer_controller = true enable_karpenter = false enable_amazon_eks_aws_ebs_csi_driver = true enable_aws_for_fluentbit = true enable_metrics_server = true }
Argo CDの場合、このenable_argocd
がkubernetes-addonsの中で以下のように使われていて、argocd moduleが呼ばれています。
module "argocd" { count = var.enable_argocd ? 1 : 0 source = "./argocd" helm_config = var.argocd_helm_config applications = var.argocd_applications addon_config = { for k, v in local.argocd_addon_config : k => v if v != null } addon_context = local.addon_context }
他のaddonに関しても同様で、各addonのmodule呼び出しのコードに count = var.enable_xxx ? 1 : 0
が書かれていることで、addonのデプロイ/非デプロイの分岐が実現されています。
aws-load-balancer-controller module
ここで一度aws-load-balancer-controller moduleに注目してみましょう。argocdと同様kubernetes-addonsから呼ばれています。
ここで注目なのは、manage_via_gitops
attributeです。
module "aws_load_balancer_controller" { count = var.enable_aws_load_balancer_controller ? 1 : 0 source = "./aws-load-balancer-controller" helm_config = var.aws_load_balancer_controller_helm_config manage_via_gitops = var.argocd_manage_add_ons addon_context = merge(local.addon_context, { default_repository = local.amazon_container_image_registry_uris[data.aws_region.current.name] }) }
ここ(manage_via_gitops
)で参照されているvar.argocd_manage_add_ons
は最初のコードmain.tf
の中でkubernetes-addonsを呼ぶ際に使われたargocd_manage_add_ons
attributeです。main.tf
を再掲します。
(略) module "kubernetes_addons" { source = "github.com/aws-ia/terraform-aws-eks-blueprints?ref=v4.12.2/modules/kubernetes-addons" eks_cluster_id = module.eks_blueprints.eks_cluster_id #--------------------------------------------------------------- # ARGO CD ADD-ON #--------------------------------------------------------------- enable_argocd = true argocd_manage_add_ons = true # Indicates that Argo CD is responsible for managing/deploying Add-ons. (略) }
aws-load-balancer-controller moduleの中に進みます。渡されたmanage_via_gitops
がどう使われているか見てみましょう。
module "helm_addon" { source = "../helm-addon" manage_via_gitops = var.manage_via_gitops set_values = local.set_values helm_config = local.helm_config irsa_config = local.irsa_config addon_context = var.addon_context }
さらに helm-addon moduleに渡されていますね。helm-addon moduleの中でmanage_via_gitops
がどう使われているか見てみましょう。
resource "helm_release" "addon" { count = var.manage_via_gitops ? 0 : 1 name = var.helm_config["name"] repository = try(var.helm_config["repository"], null) chart = var.helm_config["chart"] version = try(var.helm_config["version"], null) timeout = try(var.helm_config["timeout"], 1200) values = try(var.helm_config["values"], null) (略)
helm_release
リソースのcountで使われています。つまりこのmanage_via_gitops
がtrue
だった場合helm_release
リソースはつくられないと言うことです!実際terraform plan
コマンドの結果のなかに AWS Load Balancer Controllerのhelm releaseは無く、IRSA関連のリソースのみが含まれていました。
# module.kubernetes_addons.module.aws_load_balancer_controller[0].aws_iam_policy.aws_load_balancer_controller will be created + resource "aws_iam_policy" "aws_load_balancer_controller" { + arn = (known after apply) + description = "Allows lb controller to manage ALB and NLB" + id = (known after apply) + name = (known after apply) + path = "/" + policy = jsonencode( { + Statement = [ + { + Action = "iam:CreateServiceLinkedRole" + Condition = { + StringEquals = { + "iam:AWSServiceName" = "elasticloadbalancing.amazonaws.com" } } + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = [ + "elasticloadbalancing:DescribeTargetHealth", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeTargetGroupAttributes", + "elasticloadbalancing:DescribeTags", + "elasticloadbalancing:DescribeSSLPolicies", + "elasticloadbalancing:DescribeRules", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeListenerCertificates", + "ec2:GetCoipPoolUsage", + "ec2:DescribeVpcs", + "ec2:DescribeVpcPeeringConnections", + "ec2:DescribeTags", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeInternetGateways", + "ec2:DescribeInstances", + "ec2:DescribeCoipPools", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeAddresses", + "ec2:DescribeAccountAttributes", ] + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = [ + "wafv2:GetWebACLForResource", + "wafv2:GetWebACL", + "wafv2:DisassociateWebACL", + "wafv2:AssociateWebACL", + "waf-regional:GetWebACLForResource", + "waf-regional:GetWebACL", + "waf-regional:DisassociateWebACL", + "waf-regional:AssociateWebACL", + "shield:GetSubscriptionState", + "shield:DescribeProtection", + "shield:DeleteProtection", + "shield:CreateProtection", + "iam:ListServerCertificates", + "iam:GetServerCertificate", + "cognito-idp:DescribeUserPoolClient", + "acm:ListCertificates", + "acm:DescribeCertificate", ] + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = [ + "ec2:RevokeSecurityGroupIngress", + "ec2:AuthorizeSecurityGroupIngress", ] + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = "ec2:CreateSecurityGroup" + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = "ec2:CreateTags" + Condition = { + Null = { + "aws:RequestTag/elbv2.k8s.aws/cluster" = "false" } + StringEquals = { + "ec2:CreateAction" = "CreateSecurityGroup" } } + Effect = "Allow" + Resource = "arn:aws:ec2:*:*:security-group/*" + Sid = "" }, + { + Action = [ + "ec2:DeleteTags", + "ec2:CreateTags", ] + Condition = { + Null = { + "aws:ResourceTag/ingress.k8s.aws/cluster" = "false" } } + Effect = "Allow" + Resource = "arn:aws:ec2:*:*:security-group/*" + Sid = "" }, + { + Action = [ + "elasticloadbalancing:RemoveTags", + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:AddTags", ] + Condition = { + Null = { + "aws:ResourceTag/ingress.k8s.aws/cluster" = "false" } } + Effect = "Allow" + Resource = [ + "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*", ] + Sid = "" }, + { + Action = [ + "ec2:DeleteTags", + "ec2:CreateTags", ] + Condition = { + Null = { + "aws:RequestTag/elbv2.k8s.aws/cluster" = "true" + "aws:ResourceTag/elbv2.k8s.aws/cluster" = "false" } } + Effect = "Allow" + Resource = "arn:aws:ec2:*:*:security-group/*" + Sid = "" }, + { + Action = [ + "ec2:RevokeSecurityGroupIngress", + "ec2:DeleteSecurityGroup", + "ec2:AuthorizeSecurityGroupIngress", ] + Condition = { + Null = { + "aws:ResourceTag/elbv2.k8s.aws/cluster" = "false" } } + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = [ + "elasticloadbalancing:CreateTargetGroup", + "elasticloadbalancing:CreateLoadBalancer", ] + Condition = { + Null = { + "aws:RequestTag/elbv2.k8s.aws/cluster" = "false" } } + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = [ + "elasticloadbalancing:DeleteRule", + "elasticloadbalancing:DeleteListener", + "elasticloadbalancing:CreateRule", + "elasticloadbalancing:CreateListener", ] + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = [ + "elasticloadbalancing:RemoveTags", + "elasticloadbalancing:AddTags", ] + Condition = { + Null = { + "aws:RequestTag/elbv2.k8s.aws/cluster" = "true" + "aws:ResourceTag/elbv2.k8s.aws/cluster" = "false" } } + Effect = "Allow" + Resource = [ + "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*", ] + Sid = "" }, + { + Action = [ + "elasticloadbalancing:RemoveTags", + "elasticloadbalancing:AddTags", ] + Effect = "Allow" + Resource = [ + "arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*", ] + Sid = "" }, + { + Action = [ + "elasticloadbalancing:SetSubnets", + "elasticloadbalancing:SetSecurityGroups", + "elasticloadbalancing:SetIpAddressType", + "elasticloadbalancing:ModifyTargetGroupAttributes", + "elasticloadbalancing:ModifyTargetGroup", + "elasticloadbalancing:ModifyLoadBalancerAttributes", + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:DeleteLoadBalancer", ] + Condition = { + Null = { + "aws:ResourceTag/elbv2.k8s.aws/cluster" = "false" } } + Effect = "Allow" + Resource = "*" + Sid = "" }, + { + Action = [ + "elasticloadbalancing:RegisterTargets", + "elasticloadbalancing:DeregisterTargets", ] + Effect = "Allow" + Resource = "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*" + Sid = "" }, + { + Action = [ + "elasticloadbalancing:SetWebAcl", + "elasticloadbalancing:RemoveListenerCertificates", + "elasticloadbalancing:ModifyRule", + "elasticloadbalancing:ModifyListener", + "elasticloadbalancing:AddListenerCertificates", ] + Effect = "Allow" + Resource = "*" + Sid = "" }, ] + Version = "2012-10-17" } ) + policy_id = (known after apply) + tags_all = (known after apply) } # module.kubernetes_addons.module.aws_load_balancer_controller[0].module.helm_addon.module.irsa[0].aws_iam_role.irsa[0] will be created + resource "aws_iam_role" "irsa" { + arn = (known after apply) + assume_role_policy = (known after apply) + create_date = (known after apply) + description = "AWS IAM Role for the Kubernetes service account aws-load-balancer-controller-sa." + force_detach_policies = true + id = (known after apply) + managed_policy_arns = (known after apply) + max_session_duration = 3600 + name = (known after apply) + name_prefix = (known after apply) + path = "/" + tags_all = (known after apply) + unique_id = (known after apply) + inline_policy { + name = (known after apply) + policy = (known after apply) } } # module.kubernetes_addons.module.aws_load_balancer_controller[0].module.helm_addon.module.irsa[0].aws_iam_role_policy_attachment.irsa[0] will be created + resource "aws_iam_role_policy_attachment" "irsa" { + id = (known after apply) + policy_arn = (known after apply) + role = (known after apply) } # module.kubernetes_addons.module.aws_load_balancer_controller[0].module.helm_addon.module.irsa[0].kubernetes_service_account_v1.irsa[0] will be created + resource "kubernetes_service_account_v1" "irsa" { + automount_service_account_token = true + default_secret_name = (known after apply) + id = (known after apply) + metadata { + annotations = (known after apply) + generation = (known after apply) + name = "aws-load-balancer-controller-sa" + namespace = "kube-system" + resource_version = (known after apply) + uid = (known after apply) } }
AWS Load Balancer ControllerのmoduleなのにAWS Load Balancer Controllerのリソースが作成されないのは奇妙ですね。これは、先に説明したとおり「TerraformがArgo CDのapplicationをデプロイし、そのArgo CDのapplicationが各addonのHelm releaseをデプロイしている」からです。つまりTerraformはAWS Load Balancer Controllerをデプロイしないんです。あとでargocd moduleも見るのでそこで確認しましょう。
IRSA周りのリソースだけArgo CDを介さずTerraformが直接作成する理由は、これらIAMロールなどのIRSA周りのリソースがAWSのリソースだからだと思います。k8sのリソースではないのでArgo CDの管理対象外だということです。
- Service Accountはk8sリソースですが、irsa moduleとしてまとめている中の一部なので、Terraformから直接デプロイすることにしたのだと思います。
- ACK(AWS Controllers for Kubernetes)などを使えばAWSリソースもk8sマニフェストからデプロイできると思いますが…まあ今の構成の方がシンプルですよね。
aws-for-fluentbit module
aws-load-balancer-controller moduleと同じなので割愛します。つまりIRSA周りのリソースが作られて、肝心のAWS for Fluent BitのHelm releaseは作成されません。(厳密に言うとその他に、CloudWatch Logs Log Groupなども作られます。)
metrics-server module
こちらはirsa moduleを呼んでおらず、helm-addon moduleを呼ぶのみです。かつその際aws-load-balancer-controller やaws-for-fluentbit moduleと同様manage_via_gitops
がtrueの場合はHelm releaseも作成しないので、何も作成するリソースがありません!
aws-ebs-csi-driver module
こちらは EKS managed addonなのでArgo CDは介さずTerraformからaws_eks_addon
リソースを使って直接デプロイしています。
resource "aws_eks_addon" "aws_ebs_csi_driver" { count = var.enable_amazon_eks_aws_ebs_csi_driver && !var.enable_self_managed_aws_ebs_csi_driver ? 1 : 0 cluster_name = var.addon_context.eks_cluster_id addon_name = local.name addon_version = try(var.addon_config.addon_version, data.aws_eks_addon_version.this.version) resolve_conflicts = try(var.addon_config.resolve_conflicts, "OVERWRITE") service_account_role_arn = local.create_irsa ? module.irsa_addon[0].irsa_iam_role_arn : try(var.addon_config.service_account_role_arn, null) preserve = try(var.addon_config.preserve, true) tags = merge( var.addon_context.tags, try(var.addon_config.tags, {}) ) }
EBS CSI ドライバーで使うIRSA関連のリソースも作成します。
argocd module
いよいよargocd moduleです。
Argo CD自体のHelm リリース
まず、他のmoduleと同様Argo CDそのものHelmリリースがhelm-addon moduleを使って作成されます。
module "helm_addon" { source = "../helm-addon" helm_config = local.helm_config addon_context = var.addon_context depends_on = [kubernetes_namespace_v1.this] }
addon達のapplicationは?
他のaddonのapplication達はどこで作られているのでしょうか?答えは以下helm_release.argocd_application
です。
# --------------------------------------------------------------------------------------------------------------------- # Argo CD App of Apps Bootstrapping (Helm) # --------------------------------------------------------------------------------------------------------------------- resource "helm_release" "argocd_application" { for_each = { for k, v in var.applications : k => merge(local.default_argocd_application, v) if merge(local.default_argocd_application, v).type == "helm" } name = each.key chart = "${path.module}/argocd-application/helm" version = "1.0.0" namespace = local.helm_config["namespace"] # Application Meta. set { name = "name" value = each.key type = "string" } set { name = "project" value = each.value.project type = "string" } # Source Config. set { name = "source.repoUrl" value = each.value.repo_url type = "string" } set { name = "source.targetRevision" value = each.value.target_revision type = "string" } set { name = "source.path" value = each.value.path type = "string" } set { name = "source.helm.releaseName" value = each.key type = "string" } set { name = "source.helm.values" value = yamlencode(merge( { repoUrl = each.value.repo_url }, each.value.values, local.global_application_values, each.value.add_on_application ? var.addon_config : {} )) type = "auto" } # Destination Config. set { name = "destination.server" value = each.value.destination type = "string" } depends_on = [module.helm_addon] }
まずは冒頭でfor_each = { for k, v in var.applications : k => merge(local.default_argocd_application, v) if merge(local.default_argocd_application, v).type == "helm" }
とfor_eachが書かれていることに注目です。
ここで使われているvar.applications
は、最初のmain.tf
でkubernetes-addons moduleを呼び出す際に指定しているargocd_applications
の値です。
(略) module "kubernetes_addons" { source = "github.com/aws-ia/terraform-aws-eks-blueprints?ref=v4.12.2/modules/kubernetes-addons" eks_cluster_id = module.eks_blueprints.eks_cluster_id #--------------------------------------------------------------- # ARGO CD ADD-ON #--------------------------------------------------------------- enable_argocd = true argocd_manage_add_ons = true # Indicates that Argo CD is responsible for managing/deploying Add-ons. argocd_applications = { addons = local.addon_application #workloads = local.workload_application #We comment it for now } argocd_helm_config = { set = [ { name = "server.service.type" value = "LoadBalancer" } ] } #--------------------------------------------------------------- # ADD-ONS - You can add additional addons here # https://aws-ia.github.io/terraform-aws-eks-blueprints/add-ons/ #--------------------------------------------------------------- enable_aws_load_balancer_controller = true enable_karpenter = false enable_amazon_eks_aws_ebs_csi_driver = true enable_aws_for_fluentbit = true enable_metrics_server = true }
そしてそのargocd_applications
値の中でaddons = local.addon_application
と指定されています。このlocal.addon_application
の値はlocal.tf
で定義されている以下です。
locals { (略) #--------------------------------------------------------------- # ARGOCD ADD-ON APPLICATION #--------------------------------------------------------------- addon_application = { path = "chart" repo_url = "https://github.com/[ YOUR GITHUB USER HERE ]/eks-blueprints-add-ons.git" add_on_application = true } (略) }
話を先程のfor_eachに戻します。
for_each = { for k, v in var.applications : k => merge(local.default_argocd_application, v) if merge(local.default_argocd_application, v).type == "helm" }
と書かれていて、ループする値 var.applications
は
{ addons = local.addon_application #workloads = local.workload_application #We comment it for now }
なので、ループ数は1ですね。(workloadsはコメントアウトされています。)
また、if merge(local.default_argocd_application, v).type == "helm"
という条件がついています。local.default_argocd_application
の定義は以下のようになっています。
default_argocd_application = { namespace = local.helm_config["namespace"] target_revision = "HEAD" destination = "https://kubernetes.default.svc" project = "default" values = {} type = "helm" add_on_application = false }
local.default_argocd_application.type
の値はhelm
ですね。なのでこのif文は真になり、やはりループ数は1です。
このargocd moduleはHelmのアプリとKustomizeのアプリに対応していて、Helmのアプリの場合のみこのhelm_release.argocd_application
が作成されるということです。(Kustomizeアプリの場合はひとつ下のkubectl_manifest.argocd_kustomize_application
が作成されます。)
addonは3つあるはずなのにapplicationはひとつだけ?
さて、ループ数が1ということなので、作成されるArgo CDのアプリケーションは一つということになります。前述のとおり、Argo CD経由でデプロイされるaddonはAWS Load Balancer Controller、AWS for Fluent Bit、Metrics Serverと3つあるはずなのにおかしいですね。この点については後ほど説明します。
まずはapplicationのHelmチャートのコードを確認します。applicationのtemplateはこれで、これに先程のhelm_release.argocd_application
リソースで指定しているsetの内容を反映すると、以下のようになります。
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: addons namespace: argocd # setではなく`helm_release.argocd_application.namespace`で指定されている finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: "https://github.com/[ YOUR GITHUB USER HERE ]/eks-blueprints-add-ons.git" # https://github.com/aws-samples/eks-blueprints-add-ons をforkしたもの targetRevision: HEAD path: chart helm: values: | "account": "012345678901" "awsForFluentBit": "enable": "true" "logGroupName": "/eks-blueprint/worker-fluentbit-logs" "serviceAccountName": "aws-for-fluent-bit-sa" "awsLoadBalancerController": "enable": "true" "serviceAccountName": "aws-load-balancer-controller-sa" "clusterName": "eks-blueprint" "metricsServer": "enable": "true" "region": "ap-northeast-1" "repoUrl": "https://github.com/[ YOUR GITHUB USER HERE ]/eks-blueprints-add-ons.git" destination: server: https://kubernetes.default.svc namespace: argocd syncPolicy: automated: allowEmpty: false prune: true selfHeal: true retry: backoff: duration: "10s" factor: 2 maxDuration: "3m" syncOptions: - "Validate=false" # disables resource validation (equivalent to 'kubectl apply --validate=false') ( true by default ) - "CreateNamespace=true" # Namespace Auto-Creation ensures that namespace specified as the application destination exists in the destination cluster. - "PrunePropagationPolicy=foreground" # Supported policies are background, foreground and orphan. - "PruneLast=true" # Allow the ability for resource pruning to happen as a final, implicit wave of a sync operation
https://github.com/aws-samples/eks-blueprints-add-ons(厳密にはこのリポジトリをフォークしたリポジトリ) の chart/
以下がsourceになるので、このHelm Chartがデプロイされます。
上記applicationのマニフェストファイルの中で注目すべきは spec.source.helm.values
以下です。
(略) spec: project: default source: repoURL: "https://github.com/[ YOUR GITHUB USER HERE ]/eks-blueprints-add-ons.git" # https://github.com/aws-samples/eks-blueprints-add-ons をforkしたもの targetRevision: HEAD path: "chart" helm: values: | "account": "012345678901" "awsForFluentBit": "enable": "true" "logGroupName": "/eks-blueprint/worker-fluentbit-logs" "serviceAccountName": "aws-for-fluent-bit-sa" "awsLoadBalancerController": "enable": "true" "serviceAccountName": "aws-load-balancer-controller-sa" "clusterName": "eks-blueprint" "metricsServer": "enable": "true" "region": "ap-northeast-1" "repoUrl": "https://github.com/[ YOUR GITHUB USER HERE ]/eks-blueprints-add-ons.git" (略)
察しの言い方ならなんとなくわかったかもしれませんが、このapplication (addons application)は各addon(AWS Load Balancer Controller、AWS for Fluent Bit、Metrics Server)達をまとめるapplicationであり、配下にこの3addonsがデプロイされます。所謂 App Of Apps Patternです。Terraformからデプロイするapplicationはひとつだけですが、そのapplicationが3つのapplicationをデプロイする、ということです。
https://github.com/aws-samples/eks-blueprints-add-ons の中をチェックしていきましょう。
{{- if and (.Values.awsLoadBalancerController) (.Values.awsLoadBalancerController.enable) -}} apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: aws-load-balancer-controller namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: {{ .Values.repoUrl }} path: add-ons/aws-load-balancer-controller targetRevision: {{ .Values.targetRevision }} helm: values: | aws-load-balancer-controller: clusterName: {{ .Values.clusterName }} region: {{ .Values.region }} serviceAccount: name: {{ .Values.awsLoadBalancerController.serviceAccountName }} create: false {{- toYaml .Values.awsLoadBalancerController | nindent 10 }} destination: server: https://kubernetes.default.svc namespace: kube-system syncPolicy: automated: prune: true retry: limit: 1 backoff: duration: 5s factor: 2 maxDuration: 1m {{- end -}}
templates
以下のフォルダに上記のようなaddonごとのファイルが配置されており、それぞれ冒頭でif文が設定されています。valueで対応するaddonのフラグをtrueにしたときだけそのaddonのArogoCD applicationがデプロイされるということですね。このArogoCD applicationでデプロイされるAWS Load Balancer ControllerのHelm Chartは同じリポジトリ内のこれです。
ようやくaddonのHelm Chartまで辿り着きました!? AWS for Fluent BitとMetrics Serverも同様の方法でデプロイされます。
spec.source.helm.values値の作り方
少し話を戻して、先程のspec.source.helm.values
以下の値をどの様に作っているかも確認しましょう。該当のソースコードは以下のset
です。
set { name = "source.helm.values" value = yamlencode(merge( { repoUrl = each.value.repo_url }, each.value.values, local.global_application_values, each.value.add_on_application ? var.addon_config : {} )) type = "auto" }
最初のrepoUrl = each.value.repo_url
のeach.value.repo_url
はlocals.tf
で定義しているlocal.addon_application.repo_url
です。
addon_application = { path = "chart" repo_url = "https://github.com/[ YOUR GITHUB USER HERE ]/eks-blueprints-add-ons.git" add_on_application = true }
その下のeach.value.values
は、この値が使われるので空です。
次のlocal.global_application_values
は以下です。クラスターの基本的な情報がkubernetes-addons moduleより渡されてきています。
global_application_values = { region = var.addon_context.aws_region_name account = var.addon_context.aws_caller_identity_account_id clusterName = var.addon_context.eks_cluster_id }
最後の each.value.add_on_application ? var.addon_config : {}
です。each.value.add_on_application
はlocals.tf
で定義しているlocal.addon_application.add_on_application
が使われるので trueで、つまりvar.addon_config
が使われます。var.addon_config
の値はkubernetes-addons moduleがargocd moduleを呼び出す際に指定した以下の部分です。
module "argocd" { count = var.enable_argocd ? 1 : 0 source = "./argocd" helm_config = var.argocd_helm_config applications = var.argocd_applications addon_config = { for k, v in local.argocd_addon_config : k => v if v != null } addon_context = local.addon_context }
ここで使われているlocal.argocd_addon_config
はこんな定義になっています。
# Configuration for managing add-ons via Argo CD. argocd_addon_config = { agones = var.enable_agones ? module.agones[0].argocd_gitops_config : null awsEfsCsiDriver = var.enable_aws_efs_csi_driver ? module.aws_efs_csi_driver[0].argocd_gitops_config : null awsFSxCsiDriver = var.enable_aws_fsx_csi_driver ? module.aws_fsx_csi_driver[0].argocd_gitops_config : null awsForFluentBit = var.enable_aws_for_fluentbit ? module.aws_for_fluent_bit[0].argocd_gitops_config : null awsLoadBalancerController = var.enable_aws_load_balancer_controller ? module.aws_load_balancer_controller[0].argocd_gitops_config : null certManager = var.enable_cert_manager ? module.cert_manager[0].argocd_gitops_config : null clusterAutoscaler = var.enable_cluster_autoscaler ? module.cluster_autoscaler[0].argocd_gitops_config : null corednsAutoscaler = var.enable_amazon_eks_coredns && var.enable_coredns_autoscaler && length(var.coredns_autoscaler_helm_config) > 0 ? module.coredns_autoscaler[0].argocd_gitops_config : null grafana = var.enable_grafana ? module.grafana[0].argocd_gitops_config : null ingressNginx = var.enable_ingress_nginx ? module.ingress_nginx[0].argocd_gitops_config : null keda = var.enable_keda ? module.keda[0].argocd_gitops_config : null metricsServer = var.enable_metrics_server ? module.metrics_server[0].argocd_gitops_config : null ondat = var.enable_ondat ? module.ondat[0].argocd_gitops_config : null prometheus = var.enable_prometheus ? module.prometheus[0].argocd_gitops_config : null sparkHistoryServer = var.enable_spark_history_server ? module.spark_history_server[0].argocd_gitops_config : null sparkOperator = var.enable_spark_k8s_operator ? module.spark_k8s_operator[0].argocd_gitops_config : null tetrateIstio = var.enable_tetrate_istio ? module.tetrate_istio[0].argocd_gitops_config : null traefik = var.enable_traefik ? module.traefik[0].argocd_gitops_config : null vault = var.enable_vault ? module.vault[0].argocd_gitops_config : null vpa = var.enable_vpa ? module.vpa[0].argocd_gitops_config : null yunikorn = var.enable_yunikorn ? module.yunikorn[0].argocd_gitops_config : null argoRollouts = var.enable_argo_rollouts ? module.argo_rollouts[0].argocd_gitops_config : null karpenter = var.enable_karpenter ? module.karpenter[0].argocd_gitops_config : null kubernetesDashboard = var.enable_kubernetes_dashboard ? module.kubernetes_dashboard[0].argocd_gitops_config : null awsCloudWatchMetrics = var.enable_aws_cloudwatch_metrics ? module.aws_cloudwatch_metrics[0].argocd_gitops_config : null externalDns = var.enable_external_dns ? module.external_dns[0].argocd_gitops_config : null velero = var.enable_velero ? module.velero[0].argocd_gitops_config : null promtail = var.enable_promtail ? module.promtail[0].argocd_gitops_config : null calico = var.enable_calico ? module.calico[0].argocd_gitops_config : null kubecost = var.enable_kubecost ? module.kubecost[0].argocd_gitops_config : null smb_csi_driver = var.enable_smb_csi_driver ? module.smb_csi_driver[0].argocd_gitops_config : null chaos_mesh = var.enable_chaos_mesh ? module.chaos_mesh[0].argocd_gitops_config : null cilium = var.enable_cilium ? module.cilium[0].argocd_gitops_config : null gatekeeper = var.enable_gatekeeper ? module.gatekeeper[0].argocd_gitops_config : null kyverno = var.enable_kyverno ? { enable = true } : null kyverno_policies = var.enable_kyverno ? { enable = true } : null kyverno_policy_reporter = var.enable_kyverno ? { enable = true } : null nvidiaDevicePlugin = var.enable_nvidia_device_plugin ? module.nvidia_device_plugin[0].argocd_gitops_config : null }
kubernetes-addons moduleを呼ぶときに使った、 enable_xxxのvariablesを使って、trueのときは各moduleのargocd_gitops_config
outputを返しています。ではaws-load-balancer-controller moduleでこのoutputがどの様に定義されているか確認します。
output "argocd_gitops_config" { description = "Configuration used for managing the add-on with Argo CD" value = var.manage_via_gitops ? local.argocd_gitops_config : null }
argocd_gitops_config = { enable = true serviceAccountName = local.service_account_name }
ようやく spec.source.helm.values
に書かれていたものを見つけることができました!
まとめ
EKS Blueprints for Terraform Workshopの Argo CDについての章の実装を確認してみました。Terraform → Argo CD → Helm release → Addonという構成がまず難しいのに加えて、それを実現しているTerraformコードの構造も複雑で、さらにApp of appsの概念も加わり…という感じでなかなか骨が折れました。が良い勉強になりました。