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のページから引用します。
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が入れ子状態になっています。まずこの点を整理します。
- 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が有効化されるように指定されています。
再掲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_gitops
がtrue
だった場合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_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の概念も加わり…という感じでなかなか骨が折れました。が良い勉強になりました。