Infrastructure as Code¶
Terraform is being used to provision Infrastructure on AWS.
We make heavy use of upstream modules, such as github.com/terraform-aws-modules.
All Terraform files are located in the infra directory:
> tree -d
.
├── bootstrap
├── clusters # tfvar files per cluster
├── files # static files (templates, ...)
├── vars # common tfvar files
└── *.tf # Terraform files
Provisioning a new Cluster¶
In order to provision a new or an existing cluster, you would have to add a corresponding ./clusters/<cluster-name>.tfvars file.
The following variable set is supported:
# infra/clusters/dev.tfvars
region = "ap-southeast-2"
env = "dev"
eks = {
kubernetes_version = "1.33"
cidr = "10.0.0.0/16"
min_size = 2
max_size = 10
instance_types = ["t3.medium"]
}
argocd = {
enabled = true
apply_root_app = false
}
access = {
"constellation_admin" = {
principal_arn = "arn:aws:iam::799468650620:role/aws-reserved/sso.amazonaws.com/eu-west-1/AWSReservedSSO_ConstellationAdmin_64b9f879eabd03dc"
policies = {
"cluster_admin" = {
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope = "cluster"
}
}
}
"constellation_engineering" = {
principal_arn = "arn:aws:iam::799468650620:role/aws-reserved/sso.amazonaws.com/eu-west-1/AWSReservedSSO_ConstellationEngineering_9dda2a0a0849d418"
policies = {
"read_only" = {
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminViewPolicy"
access_scope = "cluster"
}
}
}
}
rds = {
enabled = true
engine = "postgres"
version = "17"
family = "postgres17"
port = 5432
instances = 1
instance_class = "db.t3.medium"
allocated_storage = 5
}
irsa = {
forge = {
namespace = "*"
service_account_name = "forge"
iam_policy = {
"ECRDescribeImages" = {
actions = [
"ecr:DescribeImages",
"secretsmanager:DescribeSecret"
]
resources = ["*"]
}
}
sub_condition = "StringLike"
}
cilium = {
namespace = "kube-system"
service_account_name = "cilium-operator"
iam_policy = {
"CiliumENIAndPrefixDelegation" = {
actions = [
"ec2:*"
]
resources = ["*"]
}
}
}
grafana_cloudwatch = {
namespace = "monitoring"
service_account_name = "kube-prometheus-stack-grafana"
iam_policy = {
# https://github.com/monitoringartist/grafana-aws-cloudwatch-dashboards?tab=readme-ov-file
"AWSBilling" = {
actions = [
"cloudwatch:ListMetrics",
"cloudwatch:GetMetricStatistics",
"cloudwatch:GetMetricData",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:GetLogEvents",
"logs:FilterLogEvents"
]
resources = ["*"]
}
}
}
cluster_autoscaler = {
namespace = "kube-system"
service_account_name = "cluster-autoscaler-aws-cluster-autoscaler"
iam_policy = {
# https://docs.aws.amazon.com/eks/latest/best-practices/cas.html
"ClusterAutoscalerAll" = {
actions = [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:DescribeTags",
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup",
"ec2:DescribeLaunchTemplateVersions",
"ec2:DescribeInstanceTypes",
"eks:DescribeNodegroup"
]
resources = ["*"]
}
}
}
external_secrets_operator = {
namespace = "external-secrets"
service_account_name = "external-secrets"
iam_policy = {
"ExternalSecretsOperatorAll" = {
actions = [
"secretsmanager:ListSecrets",
"secretsmanager:BatchGetSecretValue",
"secretsmanager:GetResourcePolicy",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds",
]
resources = ["*"]
}
"Renovate" = {
actions = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:DescribeImages",
"ecr:DescribeRepositories",
"ecr:ListImages"
]
resources = ["*"]
}
}
}
clearcomply-backend = {
namespace = "app-clearcomply"
service_account_name = "clearcomply-backend"
iam_policy = {
"S3Access" = {
actions = [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:PutObject",
"s3:PutObjectTagging",
"s3:DeleteObject",
"s3:ListBucket",
"s3:ListBucketVersions"
]
resources = [
"arn:aws:s3:::clearcomply-policies-dev",
"arn:aws:s3:::clearcomply-policies-dev/*"
]
}
}
}
}
irsa_clearroute_account = {
external_dns = {
namespace = "external-dns"
service_account_name = "external-dns"
iam_policy = {
# https://kubernetes-sigs.github.io/external-dns/latest/docs/tutorials/aws/#iam-policy
"ChangeResourceRecordSets" = {
actions = ["route53:ChangeResourceRecordSets"]
resources = ["arn:aws:route53:::hostedzone/*"]
}
"Route53Records" = {
actions = [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ListTagsForResources"
]
resources = ["*"]
}
}
}
cert_manager = {
namespace = "cert-manager"
service_account_name = "cert-manager"
iam_policy = {
"ChangeResourceRecordSets" = {
actions = [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
]
resources = ["arn:aws:route53:::hostedzone/*"]
conditions = {
"ForAllValues:StringEquals" = {
variable = "route53:ChangeResourceRecordSetsRecordTypes"
values = ["TXT"]
}
}
}
"ListHostedZones" = {
actions = [
"route53:ListHostedZones",
"route53:ListHostedZonesByName",
"route53:GetChange",
"route53:GetHostedZone",
]
resources = ["*"]
}
}
}
}
When providing (& merging) a PR the Github Action pipelines, will automatically pick up any changes and apply them.
A Cluster provisioning, includes:
- VPC
- EKS
- RDS
- App specific RDS DB Credentials for each app in AWS secrets manager
- IAM settings (especially IRSA)
- Route53 settings
- ECRs for each app (if the app is a
github.com/clear-routerepository)
Furthermore ArgoCD using helm will be installed on the EKS cluster.
GitOps Bridge: Handover from Terraform to ArgoCD¶
We aim to have a clear separation between Terraform managed resources and Kubernetes (EKS) managed resources. We strictly want to avoid managing Kubernetes manifests or Helm Charts using Terraform, as the Kubernetes & ArgoCD Reconciliation loop conflicts with Terraforms ad-hoc driven approach.
However, sometimes ArgoCD needs values that are Terraform managed. This "gap"-problem is called the GitOps bridge.
In order to securely pass any computed values to ArgoCD, during Cluster provisioning an ArgoCD Cluster Secret is created, that contains all required Terraform values as annotations. Those annotations can then be consumed in ArgoCD with an ApplicationSet of type Cluster Generator using {{ metadata.annotation.<annotation>}}.
Here is a short example of the ExternalDNS ArgoCD App:
# applications/argocd/dev/external-dns.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: external-dns
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
generators:
- clusters:
selector:
matchLabels:
argocd.argoproj.io/secret-type: cluster
template:
metadata:
name: external-dns
spec:
project: default
sources:
- repoURL: https://kubernetes-sigs.github.io/external-dns/
chart: external-dns
targetRevision: 1.19.0
helm:
values: |
provider:
name: aws
txtOwnerId: {{ metadata.annotations.cluster_name }}
domainFilters:
- {{ metadata.annotations.cluster_name }}.{{ metadata.annotations.tld }}
env:
- name: AWS_DEFAULT_REGION
value: {{ metadata.annotations.region }}
tolerations:
- key: workload-tier
operator: Equal
effect: NoSchedule
value: baseline
nodeSelector:
workload-tier: baseline
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: {{ metadata.annotations.external_dns_role_arn }}
destination:
server: https://kubernetes.default.svc
namespace: external-dns
syncPolicy:
syncOptions:
- CreateNamespace=true
- SkipDryRunOnMissingResource=true
automated:
prune: true
selfHeal: true
retry:
limit: 5
backoff:
duration: 5s
maxDuration: 3m0s
factor: 2