Deploying and Managing ArgoCD Using Terraform on Kubernetes

ArgoCD is a powerful GitOps continuous delivery tool for Kubernetes that enables declarative management of application deployments. By using Terraform, we can automate the deployment and management of ArgoCD, ensuring an infrastructure-as-code (IaC) approach for Kubernetes GitOps workflows.This guide will cover the step-by-step process of deploying ArgoCD using Terraform on a Kubernetes cluster, along with best practices for managing its configuration and lifecycle.

ArgoCD Terraform Deployment

Video Tutorial

View Source Code

Prerequisites

  • Terraform (v1.0+)
  • Kubernetes cluster
  • kubectl configured
  • Helm provider (optional)
  • Git repository access
  • Basic understanding of Terraform and ArgoCD

Project Structure

argocd-terraform
├── README.md
├── environments
│   ├── dev
│   │   ├── argocd-apps
│   │   │   ├── README.md
│   │   │   ├── backend.tf
│   │   │   ├── main.tf
│   │   │   ├── outputs.tf
│   │   │   ├── providers.tf
│   │   │   ├── terraform.tfvars
│   │   │   └── variables.tf
│   │   └── argocd-server
│   │       ├── README.md
│   │       ├── backend.tf
│   │       ├── main.tf
│   │       ├── outputs.tf
│   │       ├── providers.tf
│   │       ├── terraform.tfvars
│   │       └── variables.tf
│   ├── production
│   └── staging
└── modules
    ├── applications
    │   ├── README.md
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── argocd
        ├── README.md
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

Step 1: Initialize Terraform and Configure Kubernetes Provider

providers.tf

terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.23.0"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.11.0"
    }
    null = {
      source  = "hashicorp/null"
      version = "~> 3.2.0"
    }
  }
}

provider "kubernetes" {
  config_path = var.kubeconfig_path
}

provider "helm" {
  kubernetes {
    config_path = var.kubeconfig_path
  }
}

versions.tf

terraform {
  required_version = ">= 1.0.0"
}

Step 2: ArgoCD Server Module

modules/argocd/main.tf

resource "kubernetes_namespace" "argocd" {
  metadata {
    name = var.namespace
    labels = {
      "app.kubernetes.io/managed-by" = "terraform"
    }
  }
}

resource "helm_release" "argocd" {
  name       = "argocd"
  repository = "https://argoproj.github.io/argo-helm"
  chart      = "argo-cd"
  version    = var.argocd_version
  namespace  = kubernetes_namespace.argocd.metadata[0].name

  values = [
    yamlencode({
      server = {
        extraArgs = ["--insecure"]
        service = {
          type = var.service_type
        }
        ingress = {
          enabled = var.ingress_enabled
          hosts   = var.ingress_hosts
          tls     = var.ingress_tls
        }
      }
      dex = {
        enabled = var.dex_enabled
      }
      notifications = {
        enabled = var.notifications_enabled
      }
      ha = {
        enabled = var.ha_enabled
      }
    })
  ]
}

modules/argocd/variables.tf

variable "namespace" {
  description = "Namespace for ArgoCD installation"
  type        = string
  default     = "argocd"
}

variable "argocd_version" {
  description = "Version of ArgoCD Helm chart"
  type        = string
  default     = "5.46.7"
}

variable "service_type" {
  description = "Service type for ArgoCD server"
  type        = string
  default     = "ClusterIP"
}

variable "ingress_enabled" {
  description = "Enable ingress for ArgoCD server"
  type        = bool
  default     = false
}

variable "ingress_hosts" {
  description = "Ingress hosts for ArgoCD server"
  type        = list(string)
  default     = []
}

variable "ingress_tls" {
  description = "TLS configuration for ingress"
  type        = list(map(string))
  default     = []
}

variable "dex_enabled" {
  description = "Enable Dex for SSO"
  type        = bool
  default     = false
}

variable "notifications_enabled" {
  description = "Enable notifications"
  type        = bool
  default     = false
}

variable "ha_enabled" {
  description = "Enable high availability"
  type        = bool
  default     = false
}

variable "admin_password" {
  description = "Admin password for ArgoCD"
  type        = string
  sensitive   = true
}

variable "server_secretkey" {
  description = "Server secret key"
  type        = string
  sensitive   = true
}

Step 3: Applications Module

modules/applications/main.tf

resource "kubernetes_manifest" "application" {
  for_each = var.applications

  manifest = {
    apiVersion = "argoproj.io/v1alpha1"
    kind       = "Application"
    metadata = {
      name      = each.key
      namespace = var.argocd_namespace
    }
    spec = {
      project = each.value.project
      source = {
        repoURL        = each.value.repo_url
        targetRevision = each.value.target_revision
        path           = each.value.path
      }
      destination = {
        server    = each.value.destination_server
        namespace = each.value.destination_namespace
      }
      syncPolicy = {
        automated = {
          prune       = each.value.sync_prune
          selfHeal    = each.value.sync_self_heal
        }
        syncOptions = [
          "CreateNamespace=true"
        ]
      }
    }
  }
}

modules/applications/variables.tf

variable "argocd_namespace" {
  description = "ArgoCD namespace"
  type        = string
}

variable "applications" {
  description = "Map of applications to create"
  type = map(object({
    project               = string
    repo_url             = string
    target_revision      = string
    path                 = string
    destination_server   = string
    destination_namespace = string
    sync_prune          = bool
    sync_self_heal      = bool
  }))
}

variable "helm_release_name" {
  description = "Name of the ArgoCD Helm release to depend on"
  type        = string
}

Step 4: Deployments Cluster ArgoCD Server Configuration

main.tf

module "argocd" {
  source = "../modules/argocd"

  namespace             = var.argocd_namespace
  argocd_version        = var.argocd_version
  service_type          = var.service_type
  ingress_enabled       = var.ingress_enabled
  ingress_hosts         = var.ingress_hosts
  ingress_tls           = var.ingress_tls
  dex_enabled           = var.dex_enabled
  notifications_enabled = var.notifications_enabled
  ha_enabled            = var.ha_enabled
  admin_password        = var.admin_password
  server_secretkey      = var.server_secretkey
}

variables.tf

variable "kubeconfig_path" {
  description = "Path to kubeconfig file"
  type        = string
}

variable "argocd_namespace" {
  description = "Namespace for ArgoCD installation"
  type        = string
  default     = "argocd"
}

variable "argocd_version" {
  description = "Version of ArgoCD Helm chart"
  type        = string
  default     = "5.46.7"
}

variable "service_type" {
  description = "Service type for ArgoCD server"
  type        = string
  default     = "ClusterIP"
}

variable "ingress_enabled" {
  description = "Enable ingress for ArgoCD server"
  type        = bool
  default     = false
}

variable "ingress_hosts" {
  description = "Ingress hosts for ArgoCD server"
  type        = list(string)
  default     = []
}

variable "ingress_tls" {
  description = "TLS configuration for ingress"
  type        = list(map(string))
  default     = []
}

variable "applications" {
  description = "Map of applications to create"
  type = map(object({
    project               = string
    repo_url              = string
    target_revision       = string
    path                  = string
    destination_server    = string
    destination_namespace = string
    sync_prune            = bool
    sync_self_heal        = bool
  }))
  default = {}
}

variable "dex_enabled" {
  description = "Enable Dex authentication"
  type        = bool
  default     = false
}

variable "notifications_enabled" {
  description = "Enable notifications"
  type        = bool
  default     = false
}
variable "ha_enabled" {
  description = "Enable HA mode"
  type        = bool
  default     = false
}
variable "admin_password" {
  description = "Admin password for ArgoCD"
  type        = string
  default     = "password"
}
variable "server_secretkey" {
  description = "Secret key for ArgoCD server"
  type        = string
  default     = "secretkey"
}

outputs.tf

output "argocd_namespace" {
  description = "Namespace where ArgoCD is installed"
  value       = module.argocd.namespace
}

output "argocd_server_service" {
  description = "ArgoCD server service details"
  value       = module.argocd.server_service
  sensitive   = true
}
output "helm_release_name" {
  description = "Name of the ArgoCD Helm release"
  value       = module.argocd.helm_release_name
}

backend.tf

terraform {
  backend "s3" {
    bucket = "your-s3-bucket-name"
    key    = "terraform/state/dev/argocd-server/terraform.tfstate"
    region = "your-region"
  }
}

Usage Example

terraform.tfvars

kubeconfig_path       = "~/.kube/config"
argocd_namespace      = "argocd"
service_type          = "LoadBalancer"
ingress_enabled       = true
ingress_hosts         = ["argocd.example.com"]
dex_enabled           = true
notifications_enabled = true
ha_enabled            = true
admin_password        = "password"
server_secretkey      = "secretkey"

Step 5: Development Cluster ArgoCD Applications Configuration

main.tf


resource "null_resource" "wait_for_crds" {
  depends_on = [var.helm_release_name]

  provisioner "local-exec" {
    command = <<EOF
      while ! kubectl get crd applications.argoproj.io &>/dev/null; do
        echo "Waiting for ArgoCD CRDs to be installed..."
        sleep 10
      done
    EOF
  }
}

module "applications" {
  source     = "../modules/applications"
  depends_on = [null_resource.wait_for_crds]

  argocd_namespace  = var.argocd_namespace
  helm_release_name = var.helm_release_name
  applications      = var.applications
}

variables.tf

variable "kubeconfig_path" {
  description = "Path to kubeconfig file"
  type        = string
}

variable "argocd_namespace" {
  description = "Namespace for ArgoCD installation"
  type        = string
  default     = "argocd"
}

variable "argocd_version" {
  description = "Version of ArgoCD Helm chart"
  type        = string
  default     = "5.46.7"
}

variable "helm_release_name" {
  description = "Name of the ArgoCD Helm release to depend on"
  type        = string
}

variable "applications" {
  description = "Map of applications to create"
  type = map(object({
    project               = string
    repo_url              = string
    target_revision       = string
    path                  = string
    destination_server    = string
    destination_namespace = string
    sync_prune            = bool
    sync_self_heal        = bool
  }))
  default = {}
}

outputs.tf

output "applications" {
  description = "Created applications"
  value       = module.applications.applications
}

backend.tf

terraform {
  backend "s3" {
    bucket = "your-s3-bucket-name"
    key    = "terraform/state/dev/argocd-apps/terraform.tfstate"
    region = "your-region"
  }
}

Usage Example

terraform.tfvars

kubeconfig_path       = "~/.kube/config"
argocd_namespace      = "argocd"
helm_release_name = "argocd"
applications = {
  "guestbook" = {
    project               = "default"
    repo_url              = "https://github.com/argoproj/argocd-example-apps.git"
    target_revision       = "HEAD"
    path                  = "guestbook"
    destination_server    = "https://kubernetes.default.svc"
    destination_namespace = "guestbook"
    sync_prune            = true
    sync_self_heal        = true
  }
}

Step 6: Access ArgoCD UI

6.1 Get Initial Admin Password

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

6.2 Access the UI

Option 1: Port Forwarding

kubectl port-forward svc/argocd-server -n argocd 8080:443

Best Practices

  1. Secret Management

    • Use Vault or AWS Secrets Manager for sensitive values
    • Never commit secrets to version control
    • Utilize Terraform’s sensitive attribute
  2. State Management

    • Use remote state storage (S3, GCS, Azure Storage)
    • Enable state locking
    • Implement state encryption
  3. Module Organization

    • Keep modules focused and single-purpose
    • Use consistent naming conventions
    • Document all variables and outputs
  4. Security

    • Enable RBAC
    • Configure network policies
    • Use TLS for ingress
    • Implement least privilege access
  5. High Availability

    • Enable HA mode for production
    • Configure proper resource requests/limits
    • Use node affinity and anti-affinity rules

Troubleshooting

Common Issues

  1. Helm Provider Issues

    # Verify Helm repository
    helm repo add argo https://argoproj.github.io/argo-helm
    helm repo update
  2. Application Sync Issues

    # Check application status
    kubectl get applications -n argocd
    kubectl describe application <name> -n argocd
  3. Authentication Issues

    # Verify secrets
    kubectl get secrets -n argocd
    kubectl describe secret argocd-secret -n argocd

Maintenance

Upgrading ArgoCD

  1. Update the argocd_version variable
  2. Run Terraform plan and apply
  3. Verify the upgrade:
    kubectl get pods -n argocd
    argocd version

Backup and Restore

  1. Backup State

    terraform show -json > terraform_state_backup.json
  2. Backup ArgoCD

    kubectl get applications -n argocd -o yaml > applications_backup.yaml
    kubectl get appprojects -n argocd -o yaml > projects_backup.yaml

Additional Resources