EKS Blueprints WorkshopのArgo CDの章の実装を調べた

2022.10.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

EKS Blueprints for Terraformworkshopをやってみました。面白かったのですが、「なんかできた!」くらいの理解に留まったので、どのように実現しているのかもう少し調べてみました。特に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のページから引用します。

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
  }

  #---------------------------------------------------------------
  # 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
  }

  (略)
}

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.

  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が入れ子状態になっています。まずこの点を整理します。

modules.png

本題: 実装解説

kubernetes-addons module

前述の通り、kubernetes-addonsは各addonのハブになるmoduleです。以下main.tfのハイライトした行でそれぞれのaddonが有効化されるように指定されています。

再掲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.

  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を再掲します。

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_gitopstrueだった場合helm_releaseリソースはつくられないと言うことです!実際terraform planコマンドの結果のなかに AWS Load Balancer Controllerのhelm releaseは無く、IRSA関連のリソースのみが含まれていました。

terraform plan結果から`module.aws_load_balancer_controller`を含むリソースを抜粋したもの

  # 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の値です。

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.

  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で定義されている以下です。

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_urleach.value.repo_urllocals.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_applicationlocals.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_configoutputを返しています。では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の概念も加わり…という感じでなかなか骨が折れました。が良い勉強になりました。