diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f66b0ae8..3d4e33f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,10 +40,11 @@ jobs: # Docs for this method: # https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable run: | - INSTANCE_NAME="$(echo ci-${{ github.event.pull_request.number }}-${GITHUB_RUN_ID})" + INSTANCE_NAME="$(echo ci-${{ github.event.pull_request.number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT})" if [[ "$(echo -n ${INSTANCE_NAME} | wc -c)" -lt "64" ]]; then - INSTANCE_NAME=${INSTANCE_NAME}-special-extra-long-padding-for-real-test-case-because-ssb-needs-it - INSTANCE_NAME=$(head -c 64 <<< $INSTANCE_NAME) + INSTANCE_NAME="${INSTANCE_NAME}-a-long-suffix-to-ensure-we-are-generating-ids-that-are-short-enough-for-underlying-identifiers" + INSTANCE_NAME="${INSTANCE_NAME//-}" + INSTANCE_NAME=$(head -c 64 <<< "$INSTANCE_NAME") fi echo "INSTANCE_NAME=${INSTANCE_NAME}" | tee -a $GITHUB_ENV diff --git a/.gitignore b/.gitignore index a413f98d..a9dfbff1 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,5 @@ kubeconfig-* *.swp terraform.log *.binding.json +terraform/modules/provision-aws/provider-*.tf +terraform/modules/provision-aws/k8s-*.tf diff --git a/eks-service-definition.yml b/eks-service-definition.yml index c2a03355..348da6d1 100644 --- a/eks-service-definition.yml +++ b/eks-service-definition.yml @@ -117,23 +117,41 @@ provision: details: The cluster token for authentication of the eks cluster user template_refs: - admin-account: terraform/modules/provision/admin-account.tf - crds: terraform/modules/provision/crds.tf - clusterdns: terraform/modules/provision/cluster-dns.tf - data: terraform/modules/provision/data.tf - eks: terraform/modules/provision/eks.tf - external-dns: terraform/modules/provision/external-dns.tf - ingress: terraform/modules/provision/ingress.tf - logging: terraform/modules/provision/logging.tf - main: terraform/modules/provision/main.tf - outputs: terraform/modules/provision/outputs.tf - providers-aws: terraform/modules/provision/providers/aws.tf - providers-kubernetes: terraform/modules/provision/providers/kubernetes.tf - providers-helm: terraform/modules/provision/providers/helm.tf - rbac: terraform/modules/provision/rbac.tf - variables: terraform/modules/provision/variables.tf - vpc: terraform/modules/provision/vpc.tf - persistent-storage: terraform/modules/provision/persistent-storage.tf + + # These files make up the provision module (AWS resources) + cluster-dns: terraform/modules/provision-aws/cluster-dns.tf + crds: terraform/modules/provision-aws/crds.tf + data: terraform/modules/provision-aws/data.tf + eks: terraform/modules/provision-aws/eks.tf + external-dns: terraform/modules/provision-aws/external-dns.tf + ingress: terraform/modules/provision-aws/ingress.tf + main: terraform/modules/provision-aws/main.tf + outputs: terraform/modules/provision-aws/outputs.tf + persistent-storage: terraform/modules/provision-aws/persistent-storage.tf + variables: terraform/modules/provision-aws/variables.tf + versions: terraform/modules/provision-aws/versions.tf + vpc: terraform/modules/provision-aws/vpc.tf + + # Since these modules are being used as a root module in the brokerpak, + # these files add the necessary provider configuration. + provider-aws: terraform/modules/provision-aws/providers/provider-aws.tf + provider-kubernetes: terraform/modules/provision-aws/providers/provider-kubernetes.tf + provider-helm: terraform/modules/provision-aws/providers/provider-helm.tf + + # This file sets provision-k8s locals based on internal/output values from provision-aws + k8s-locals: terraform/modules/provision-aws/locals/k8s-locals.tf + + # These files make up the provision module (k8s/helm resources) + k8s-admin-account: terraform/modules/provision-k8s/k8s-admin-account.tf + k8s-external-dns: terraform/modules/provision-k8s/k8s-external-dns.tf + k8s-logging: terraform/modules/provision-k8s/k8s-logging.tf + k8s-outputs: terraform/modules/provision-k8s/k8s-outputs.tf + k8s-persistent-storage: terraform/modules/provision-k8s/k8s-persistent-storage.tf + + # EXPLICITLY OMITTED; these are only useful for submodule usage + # terraform/modules/provision-aws/standalone-outputs.tf + # terraform/modules/provision-k8s/standalone-variables.tf + # terraform/modules/provision-k8s/standalone-versions.tf bind: plan_inputs: [] @@ -181,11 +199,15 @@ bind: type: string details: The CA data for the kubernetes endpoint template_refs: - main: terraform/modules/bind/main.tf - data: terraform/modules/bind/data.tf - outputs: terraform/modules/bind/outputs.tf - providers: terraform/modules/bind/providers/kubernetes.tf - variables: terraform/modules/bind/variables.tf + data: terraform/modules/bind/data.tf + main: terraform/modules/bind/main.tf + outputs: terraform/modules/bind/outputs.tf + variables: terraform/modules/bind/variables.tf + versions: terraform/modules/bind/versions.tf + + # Since this module is being used as a root module in the brokerpak, this file adds the necessary provider + provider-kubernetes: terraform/modules/bind/providers/provider-kubernetes.tf + examples: - name: Demonstrate provisioning and binding the "raw" plan description: | diff --git a/terraform/bind-providers.tf b/terraform/bind-providers.tf index be992a34..3975f73b 100644 --- a/terraform/bind-providers.tf +++ b/terraform/bind-providers.tf @@ -1,6 +1,6 @@ provider "kubernetes" { alias = "bind" - host = module.provision.server - cluster_ca_certificate = base64decode(module.provision.certificate_authority_data) - token = module.provision.token + host = data.aws_eks_cluster.cluster.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) + token = module.provision-k8s.token } diff --git a/terraform/main.tf b/terraform/main.tf index 9ea5b8e5..69a0b490 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -6,6 +6,8 @@ terraform { # configuration pointing specifically at the us-east-1 region. We are # required to set up KMS keys in that region in order for them to be usable # for setting up a DNSSEC KSK in Route53. + # Documentation on using aliased providers in a module: + # https://www.terraform.io/language/modules/develop/providers#provider-aliases-within-modules aws = { source = "hashicorp/aws" version = "~> 3.63" @@ -15,7 +17,7 @@ terraform { } output "domain_name" { - value = module.provision.domain_name + value = module.provision-aws.domain_name } output "certificate_authority_data" { @@ -87,13 +89,11 @@ variable "write_kubeconfig" { default = false } -module "provision" { - source = "./modules/provision" +module "provision-aws" { + source = "./modules/provision-aws" providers = { aws = aws aws.dnssec-key-provider = aws.dnssec-key-provider - kubernetes = kubernetes.provision - helm = helm.provision } instance_name = var.instance_name labels = var.labels @@ -107,6 +107,23 @@ module "provision" { zone = var.zone } +module "provision-k8s" { + source = "./modules/provision-k8s" + providers = { + aws = aws + kubernetes = kubernetes.provision + helm = helm.provision + } + certificate_authority_data = module.provision-aws.certificate_authority_data + domain = module.provision-aws.domain_name + instance_name = var.instance_name + persistent_storage_key_id = module.provision-aws.persistent_storage_key_id + region = var.region + server = module.provision-aws.server + zone_id = module.provision-aws.zone_id + zone_role_arn = module.provision-aws.zone_role_arn +} + module "bind" { source = "./modules/bind" providers = { @@ -114,6 +131,6 @@ module "bind" { } instance_name = var.instance_name depends_on = [ - module.provision + module.provision-k8s ] } diff --git a/terraform/modules/bind/data.tf b/terraform/modules/bind/data.tf index b7407367..9154b155 100644 --- a/terraform/modules/bind/data.tf +++ b/terraform/modules/bind/data.tf @@ -6,11 +6,6 @@ locals { data "aws_eks_cluster" "main" { name = local.cluster_name } - -data "aws_eks_cluster_auth" "main" { - name = local.cluster_name -} - # Read in the generated secret for the service account data "kubernetes_secret" "secret" { metadata { diff --git a/terraform/modules/bind/main.tf b/terraform/modules/bind/main.tf index 60eb6677..8c85231d 100644 --- a/terraform/modules/bind/main.tf +++ b/terraform/modules/bind/main.tf @@ -11,7 +11,8 @@ resource "kubernetes_service_account" "account" { } } -# Bind the namespace-admin role to the service account +# Make the service account an admin within the namespace. See +# https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles. resource "kubernetes_role_binding" "binding" { metadata { name = "${kubernetes_service_account.account.metadata[0].name}-namespace-admin-role-binding" @@ -20,8 +21,8 @@ resource "kubernetes_role_binding" "binding" { role_ref { api_group = "rbac.authorization.k8s.io" - kind = "Role" - name = "namespace-admin" + kind = "ClusterRole" + name = "admin" } subject { diff --git a/terraform/modules/bind/providers/kubernetes.tf b/terraform/modules/bind/providers/provider-kubernetes.tf similarity index 100% rename from terraform/modules/bind/providers/kubernetes.tf rename to terraform/modules/bind/providers/provider-kubernetes.tf diff --git a/terraform/modules/provision/2048_fixture.yml b/terraform/modules/provision-aws/2048_fixture.yml similarity index 100% rename from terraform/modules/provision/2048_fixture.yml rename to terraform/modules/provision-aws/2048_fixture.yml diff --git a/terraform/modules/provision/Dockerfile b/terraform/modules/provision-aws/Dockerfile similarity index 100% rename from terraform/modules/provision/Dockerfile rename to terraform/modules/provision-aws/Dockerfile diff --git a/terraform/modules/provision/README.md b/terraform/modules/provision-aws/README.md similarity index 82% rename from terraform/modules/provision/README.md rename to terraform/modules/provision-aws/README.md index b3b4bea7..0ff081fe 100644 --- a/terraform/modules/provision/README.md +++ b/terraform/modules/provision-aws/README.md @@ -22,12 +22,18 @@ the broker context here. docker build -t eks-provision:latest . ``` +1. Symlink the various files that make this module self-contained into this directory: + + ```bash + ln -s providers/* locals/* ../provision-k8s/k8s-* . + ``` + 1. Then, start a shell inside a container based on this image. The parameters here carry some of your environment variables into that shell, and ensure that you'll have permission to remove any files that get created. ```bash - $ docker run -v `pwd`:`pwd` -w `pwd` -e HOME=`pwd` --user $(id -u):$(id -g) -e TERM -it --rm -e AWS_SECRET_ACCESS_KEY -e AWS_ACCESS_KEY_ID -e AWS_DEFAULT_REGION eks-provision:latest + $ docker run -v `pwd`/..:`pwd`/.. -w `pwd` -e HOME=`pwd` --user $(id -u):$(id -g) -e TERM -it --rm -e AWS_SECRET_ACCESS_KEY -e AWS_ACCESS_KEY_ID -e AWS_DEFAULT_REGION eks-provision:latest [within the container] terraform init diff --git a/terraform/modules/provision/cluster-dns.tf b/terraform/modules/provision-aws/cluster-dns.tf similarity index 100% rename from terraform/modules/provision/cluster-dns.tf rename to terraform/modules/provision-aws/cluster-dns.tf diff --git a/terraform/modules/provision/crds.tf b/terraform/modules/provision-aws/crds.tf similarity index 100% rename from terraform/modules/provision/crds.tf rename to terraform/modules/provision-aws/crds.tf diff --git a/terraform/modules/provision/data.tf b/terraform/modules/provision-aws/data.tf similarity index 100% rename from terraform/modules/provision/data.tf rename to terraform/modules/provision-aws/data.tf diff --git a/terraform/modules/provision/eks.tf b/terraform/modules/provision-aws/eks.tf similarity index 88% rename from terraform/modules/provision/eks.tf rename to terraform/modules/provision-aws/eks.tf index 500e5706..81796886 100644 --- a/terraform/modules/provision/eks.tf +++ b/terraform/modules/provision-aws/eks.tf @@ -1,5 +1,4 @@ locals { - # Prevent provisioning if the necessary CLI binaries aren't present cluster_name = "k8s-${substr(sha256(var.instance_name), 0, 16)}" cluster_version = "1.21" kubeconfig = "kubeconfig-${local.cluster_name}" @@ -99,6 +98,18 @@ module "eks" { max_size = var.mng_max_capacity min_size = var.mng_min_capacity + block_device_mappings = { + xvda = { + device_name = "/dev/xvda" + ebs = { + volume_size = 20 + encrypted = true + kms_key_id = aws_kms_key.ebs-key.arn + delete_on_termination = true + } + } + } + instance_types = var.mng_instance_types capacity_type = "ON_DEMAND" } @@ -132,6 +143,28 @@ resource "aws_iam_role_policy_attachment" "ebs-usage" { role = each.value.iam_role_name } +# --------------------------------------------- +# Logging Policy for the pod execution IAM role +# --------------------------------------------- +resource "aws_iam_policy" "pod-logging" { + name = "${local.cluster_name}-pod-logging" + policy = <<-EOF + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:DescribeLogStreams", + "logs:PutLogEvents" + ], + "Resource": "*" + }] + } + EOF +} + # Generate a kubeconfig file for use in provisioners data "template_file" "kubeconfig" { diff --git a/terraform/modules/provision-aws/external-dns.tf b/terraform/modules/provision-aws/external-dns.tf new file mode 100644 index 00000000..ce991612 --- /dev/null +++ b/terraform/modules/provision-aws/external-dns.tf @@ -0,0 +1,59 @@ +# Modeled after an example here: +# https://tech.polyconseil.fr/external-dns-helm-terraform.html + +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "external_dns" { + name = "${local.cluster_name}-external-dns" + tags = var.labels + assume_role_policy = <<-EOF + { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${module.eks.oidc_provider}" + }, + "Condition": { + "StringEquals": { + "${module.eks.oidc_provider}:sub": "system:serviceaccount:kube-system:external-dns" + } + } + } + ] + } + EOF +} + +resource "aws_iam_role_policy" "external_dns" { + name_prefix = "${local.cluster_name}-external-dns" + role = aws_iam_role.external_dns.name + policy = <<-EOF + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "route53:ChangeResourceRecordSets" + ], + "Resource": [ + "arn:aws:route53:::hostedzone/${aws_route53_zone.cluster.zone_id}" + ] + }, + { + "Effect": "Allow", + "Action": [ + "route53:ListHostedZones", + "route53:ListResourceRecordSets" + ], + "Resource": [ + "*" + ] + } + ] + } + EOF +} diff --git a/terraform/modules/provision/ingress.tf b/terraform/modules/provision-aws/ingress.tf similarity index 96% rename from terraform/modules/provision/ingress.tf rename to terraform/modules/provision-aws/ingress.tf index a5d687dd..af7b2aa3 100644 --- a/terraform/modules/provision/ingress.tf +++ b/terraform/modules/provision-aws/ingress.tf @@ -47,6 +47,8 @@ resource "helm_release" "ingress_nginx" { dynamic "set" { for_each = { + "aws_iam_role_arn" = module.aws_load_balancer_controller.aws_iam_role_arn + "controller.ingressClassResource.default" = true "controller.service.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-scheme" = "internet-facing", "controller.service.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-ssl-ports" = "https", "controller.service.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-ssl-cert" = aws_acm_certificate.cert.arn, @@ -87,7 +89,6 @@ resource "helm_release" "ingress_nginx" { "clusterName" = module.eks.cluster_id, "region" = local.region, "vpcId" = module.vpc.vpc_id, - "aws_iam_role_arn" = module.aws_load_balancer_controller.aws_iam_role_arn } content { name = set.key diff --git a/terraform/modules/provision-aws/locals/k8s-locals.tf b/terraform/modules/provision-aws/locals/k8s-locals.tf new file mode 100644 index 00000000..6b2e9170 --- /dev/null +++ b/terraform/modules/provision-aws/locals/k8s-locals.tf @@ -0,0 +1,7 @@ +locals { + certificate_authority_data = data.aws_eks_cluster.main.certificate_authority[0].data + persistent_storage_key_id = aws_kms_key.ebs-key.key_id + server = data.aws_eks_cluster.main.endpoint + zone_id = aws_route53_zone.cluster.zone_id + zone_role_arn = aws_iam_role.external_dns.arn +} \ No newline at end of file diff --git a/terraform/modules/provision/main.tf b/terraform/modules/provision-aws/main.tf similarity index 89% rename from terraform/modules/provision/main.tf rename to terraform/modules/provision-aws/main.tf index 67ec085d..0a2dcfae 100644 --- a/terraform/modules/provision/main.tf +++ b/terraform/modules/provision-aws/main.tf @@ -16,7 +16,7 @@ # can to avoid that. resource "null_resource" "prerequisite_binaries_present" { provisioner "local-exec" { - interpreter = ["/bin/sh", "-c"] - command = "type -p aws-iam-authenticator git helm kubectl" + interpreter = ["/bin/bash", "-c"] + command = "type -a aws-iam-authenticator git helm kubectl" } } diff --git a/terraform/modules/provision-aws/outputs.tf b/terraform/modules/provision-aws/outputs.tf new file mode 100644 index 00000000..e910bd16 --- /dev/null +++ b/terraform/modules/provision-aws/outputs.tf @@ -0,0 +1,4 @@ +output "domain_name" { value = local.domain } +output "server" { value = data.aws_eks_cluster.main.endpoint } +output "certificate_authority_data" { value = data.aws_eks_cluster.main.certificate_authority[0].data } +output "cluster-id" { value = data.aws_eks_cluster.main.id } diff --git a/terraform/modules/provision/persistent-storage.tf b/terraform/modules/provision-aws/persistent-storage.tf similarity index 91% rename from terraform/modules/provision/persistent-storage.tf rename to terraform/modules/provision-aws/persistent-storage.tf index 5a5f67f7..1ea41e93 100644 --- a/terraform/modules/provision/persistent-storage.tf +++ b/terraform/modules/provision-aws/persistent-storage.tf @@ -191,7 +191,7 @@ locals { } # Default KMS Key Delegation is not enough since it restricts -# the 'Pricipal' to only the root account. +# the 'Principal' to only the root account. # This policy allows the EBS CSI to create additional keys # for each new EBS volume based on this root key. resource "aws_kms_key" "ebs-key" { @@ -204,17 +204,3 @@ resource "aws_iam_policy" "ebs-usage" { name_prefix = "${local.cluster_name}-ebs-policy" policy = replace(local.ebs_policy, "", aws_kms_key.ebs-key.arn) } - -resource "kubernetes_storage_class" "ebs-sc" { - metadata { - name = "ebs-sc" - } - parameters = { - encrypted = "true" - kmsKeyId = aws_kms_key.ebs-key.key_id - } - # Storage provisioner retrieved from - # https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html - storage_provisioner = "kubernetes.io/aws-ebs" - allow_volume_expansion = true -} diff --git a/terraform/modules/provision/providers/aws.tf b/terraform/modules/provision-aws/providers/provider-aws.tf similarity index 100% rename from terraform/modules/provision/providers/aws.tf rename to terraform/modules/provision-aws/providers/provider-aws.tf diff --git a/terraform/modules/provision/providers/helm.tf b/terraform/modules/provision-aws/providers/provider-helm.tf similarity index 100% rename from terraform/modules/provision/providers/helm.tf rename to terraform/modules/provision-aws/providers/provider-helm.tf diff --git a/terraform/modules/provision/providers/kubernetes.tf b/terraform/modules/provision-aws/providers/provider-kubernetes.tf similarity index 100% rename from terraform/modules/provision/providers/kubernetes.tf rename to terraform/modules/provision-aws/providers/provider-kubernetes.tf diff --git a/terraform/modules/provision-aws/standalone-outputs.tf b/terraform/modules/provision-aws/standalone-outputs.tf new file mode 100644 index 00000000..25a6fc66 --- /dev/null +++ b/terraform/modules/provision-aws/standalone-outputs.tf @@ -0,0 +1,7 @@ +# This file contains the outputs necessary when the directory is +# used as a standalone module. Leave it out if you're combining this directory +# with the provision-k8s module. + +output "persistent_storage_key_id" { value = aws_kms_key.ebs-key.key_id } +output "zone_id" { value = aws_route53_zone.cluster.zone_id } +output "zone_role_arn" { value = aws_iam_role.external_dns.arn } diff --git a/terraform/modules/provision/terraform.tfvars-template b/terraform/modules/provision-aws/terraform.tfvars-template similarity index 100% rename from terraform/modules/provision/terraform.tfvars-template rename to terraform/modules/provision-aws/terraform.tfvars-template diff --git a/terraform/modules/provision/variables.tf b/terraform/modules/provision-aws/variables.tf similarity index 100% rename from terraform/modules/provision/variables.tf rename to terraform/modules/provision-aws/variables.tf diff --git a/terraform/modules/provision/versions.tf b/terraform/modules/provision-aws/versions.tf similarity index 100% rename from terraform/modules/provision/versions.tf rename to terraform/modules/provision-aws/versions.tf diff --git a/terraform/modules/provision/vpc.tf b/terraform/modules/provision-aws/vpc.tf similarity index 68% rename from terraform/modules/provision/vpc.tf rename to terraform/modules/provision-aws/vpc.tf index 402b7218..49ffb6e1 100644 --- a/terraform/modules/provision/vpc.tf +++ b/terraform/modules/provision-aws/vpc.tf @@ -12,9 +12,11 @@ module "vpc" { name = "eks-vpc" cidr = "10.31.0.0/16" - azs = data.aws_availability_zones.available.names - private_subnets = ["10.31.1.0/24", "10.31.2.0/24", "10.31.3.0/24"] - public_subnets = ["10.31.101.0/24", "10.31.102.0/24", "10.31.103.0/24"] + azs = data.aws_availability_zones.available.names + # These subnets represent AZs us-west-2a, us-west-2b, and us-west-2c + # This gives us 8187 IP addresses that can be given to nodes and (via the VPC-CNI add-on) pods. + private_subnets = ["10.31.0.0/19", "10.31.32.0/19", "10.31.64.0/19"] + public_subnets = ["10.31.128.0/19", "10.31.160.0/19", "10.31.192.0/19"] enable_nat_gateway = true single_nat_gateway = true diff --git a/terraform/modules/provision/admin-account.tf b/terraform/modules/provision-k8s/k8s-admin-account.tf similarity index 76% rename from terraform/modules/provision/admin-account.tf rename to terraform/modules/provision-k8s/k8s-admin-account.tf index 95aca547..3b5a6ce4 100644 --- a/terraform/modules/provision/admin-account.tf +++ b/terraform/modules/provision-k8s/k8s-admin-account.tf @@ -50,15 +50,15 @@ data "template_file" "admin_kubeconfig" { token: ${data.kubernetes_secret.secret.data.token} clusters: - cluster: - certificate-authority-data: ${data.aws_eks_cluster.main.certificate_authority[0].data} - server: ${data.aws_eks_cluster.main.endpoint} - name: ${data.aws_eks_cluster.main.name} + certificate-authority-data: ${local.certificate_authority_data} + server: ${local.server} + name: ${local.cluster_name} contexts: - context: - cluster: ${data.aws_eks_cluster.main.name} + cluster: ${local.cluster_name} namespace: "kube-system" user: ${kubernetes_service_account.admin.metadata[0].name} - name: ${data.aws_eks_cluster.main.name}-kube-system-${kubernetes_service_account.admin.metadata[0].name} - current-context: ${data.aws_eks_cluster.main.name}-kube-system-${kubernetes_service_account.admin.metadata[0].name} + name: ${local.cluster_name}-kube-system-${kubernetes_service_account.admin.metadata[0].name} + current-context: ${local.cluster_name}-kube-system-${kubernetes_service_account.admin.metadata[0].name} EOF } diff --git a/terraform/modules/provision/external-dns.tf b/terraform/modules/provision-k8s/k8s-external-dns.tf similarity index 50% rename from terraform/modules/provision/external-dns.tf rename to terraform/modules/provision-k8s/k8s-external-dns.tf index 0f783d27..8a3f354a 100644 --- a/terraform/modules/provision/external-dns.tf +++ b/terraform/modules/provision-k8s/k8s-external-dns.tf @@ -1,69 +1,12 @@ # Modeled after an example here: # https://tech.polyconseil.fr/external-dns-helm-terraform.html -data "aws_caller_identity" "current" {} - -resource "aws_iam_role" "external_dns" { - name = "${local.cluster_name}-external-dns" - tags = var.labels - assume_role_policy = <<-EOF - { - "Version": "2012-10-17", - "Statement": [ - { - "Action": "sts:AssumeRoleWithWebIdentity", - "Effect": "Allow", - "Principal": { - "Federated": "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${module.eks.oidc_provider}" - }, - "Condition": { - "StringEquals": { - "${module.eks.oidc_provider}:sub": "system:serviceaccount:kube-system:external-dns" - } - } - } - ] - } - EOF -} - -resource "aws_iam_role_policy" "external_dns" { - name_prefix = "${local.cluster_name}-external-dns" - role = aws_iam_role.external_dns.name - policy = <<-EOF - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "route53:ChangeResourceRecordSets" - ], - "Resource": [ - "arn:aws:route53:::hostedzone/${aws_route53_zone.cluster.zone_id}" - ] - }, - { - "Effect": "Allow", - "Action": [ - "route53:ListHostedZones", - "route53:ListResourceRecordSets" - ], - "Resource": [ - "*" - ] - } - ] - } - EOF -} - resource "kubernetes_service_account" "external_dns" { metadata { name = "external-dns" namespace = "kube-system" annotations = { - "eks.amazonaws.com/role-arn" = aws_iam_role.external_dns.arn + "eks.amazonaws.com/role-arn" = local.zone_role_arn } } automount_service_account_token = true @@ -113,24 +56,31 @@ resource "helm_release" "external_dns" { namespace = kubernetes_service_account.external_dns.metadata.0.namespace wait = true atomic = true - repository = "https://charts.bitnami.com/bitnami" + repository = "https://kubernetes-sigs.github.io/external-dns" chart = "external-dns" - version = "4.9.3" + version = "1.7.1" + + values = [ + <<-EOF + env: + - name: AWS_DEFAULT_REGION + value: ${local.region} + extraArgs: + - --zone-id-filter=${local.zone_id} + - --fqdn-template={{.Name}}.${local.domain} + EOF + ] + dynamic "set" { for_each = { "rbac.create" = false "serviceAccount.create" = false "serviceAccount.name" = kubernetes_service_account.external_dns.metadata.0.name - "rbac.pspEnabled" = false - "name" = "${local.cluster_name}-external-dns" "provider" = "aws" "policy" = "sync" "logLevel" = "info" "sources" = "{ingress}" - "aws.zoneType" = "" "txtPrefix" = "edns-" - "aws.region" = data.aws_region.current.name - "fqdnTemplates" = "\\{\\{.Name\\}\\}.${local.domain}" } content { name = set.key diff --git a/terraform/modules/provision/logging.tf b/terraform/modules/provision-k8s/k8s-logging.tf similarity index 65% rename from terraform/modules/provision/logging.tf rename to terraform/modules/provision-k8s/k8s-logging.tf index 73a51d74..bd998329 100644 --- a/terraform/modules/provision/logging.tf +++ b/terraform/modules/provision-k8s/k8s-logging.tf @@ -1,25 +1,3 @@ -# --------------------------------------------- -# Logging Policy for the pod execution IAM role -# --------------------------------------------- -resource "aws_iam_policy" "pod-logging" { - name = "${local.cluster_name}-pod-logging" - policy = <<-EOF - { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "logs:CreateLogStream", - "logs:CreateLogGroup", - "logs:DescribeLogStreams", - "logs:PutLogEvents" - ], - "Resource": "*" - }] - } - EOF -} - # --------------------------------------------------------------------------------------------- # Logging by fluentbit requires namespace aws-observability and a configmap in Kubernetes # --------------------------------------------------------------------------------------------- diff --git a/terraform/modules/provision/outputs.tf b/terraform/modules/provision-k8s/k8s-outputs.tf similarity index 59% rename from terraform/modules/provision/outputs.tf rename to terraform/modules/provision-k8s/k8s-outputs.tf index b13b020c..4b14b83f 100644 --- a/terraform/modules/provision/outputs.tf +++ b/terraform/modules/provision-k8s/k8s-outputs.tf @@ -1,7 +1,3 @@ -output "domain_name" { value = local.domain } -output "server" { value = data.aws_eks_cluster.main.endpoint } -output "certificate_authority_data" { value = data.aws_eks_cluster.main.certificate_authority[0].data } -output "cluster-id" { value = data.aws_eks_cluster.main.id } output "token" { value = data.kubernetes_secret.secret.data.token description = "A cluster-admin token for use in constructing your own kubernetes configuration. NOTE: Do _not_ use this token when configuring the required_provider or you'll get a dependency cycle. Instead use exec with the same AWS credentials that were used for the required_providers aws provider." diff --git a/terraform/modules/provision-k8s/k8s-persistent-storage.tf b/terraform/modules/provision-k8s/k8s-persistent-storage.tf new file mode 100644 index 00000000..a024b574 --- /dev/null +++ b/terraform/modules/provision-k8s/k8s-persistent-storage.tf @@ -0,0 +1,14 @@ + +resource "kubernetes_storage_class" "ebs-sc" { + metadata { + name = "ebs-sc" + } + parameters = { + encrypted = "true" + kmsKeyId = local.persistent_storage_key_id + } + # Storage provisioner retrieved from + # https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html + storage_provisioner = "kubernetes.io/aws-ebs" + allow_volume_expansion = true +} diff --git a/terraform/modules/provision-k8s/standalone-variables.tf b/terraform/modules/provision-k8s/standalone-variables.tf new file mode 100644 index 00000000..678b8471 --- /dev/null +++ b/terraform/modules/provision-k8s/standalone-variables.tf @@ -0,0 +1,55 @@ +# This file contains the variable definitions necessary when the directory is +# used as a standalone module. Leave it out if you're combining this directory +# with the provision-aws module. + +# The certificate_authority_data for the k8s instance +variable "certificate_authority_data" { + type = string +} + +# The domain suffix to use for all DNS entries +variable "domain" { + type = string +} + +# The name of the k8s instance we're setting up +variable "instance_name" { + type = string +} + +# ARN for the key used for EBS volumes +variable "persistent_storage_key_id" { + type = string +} + +# AWS Region +variable "region" { + type = string +} + +# The server for the k8s API +variable "server" { + type = string +} + +# The ID of the Route53 zone where external DNS records for ingresses should be +# maintained +variable "zone_id" { + type = string +} + +# The ARN of an IAM role that is able to manipulate records in the Route53 zone_id. +variable "zone_role_arn" { + type = string +} + +locals { + certificate_authority_data = var.certificate_authority_data + cluster_name = "k8s-${substr(sha256(var.instance_name), 0, 16)}" + domain = var.domain + persistent_storage_key_id = var.persistent_storage_key_id + region = var.region + server = var.server + zone_id = var.zone_id + zone_role_arn = var.zone_role_arn +} diff --git a/terraform/modules/provision-k8s/standalone-versions.tf b/terraform/modules/provision-k8s/standalone-versions.tf new file mode 100644 index 00000000..8ef41287 --- /dev/null +++ b/terraform/modules/provision-k8s/standalone-versions.tf @@ -0,0 +1,23 @@ +# This file contains the version definitions necessary when the directory is +# used as a standalone module. Leave it out if you're combining this directory +# with the provision-aws module + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.63" + } + + helm = { + source = "hashicorp/helm" + version = "~>2.4" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~>2.7" + } + + } + required_version = "~> 1.1" +} diff --git a/terraform/modules/provision/rbac.tf b/terraform/modules/provision/rbac.tf deleted file mode 100644 index 3a7c54be..00000000 --- a/terraform/modules/provision/rbac.tf +++ /dev/null @@ -1,15 +0,0 @@ - -# Create a namespace-level admin role for each namespace -resource "kubernetes_role" "namespace_admin" { - # TODO: create one of these in every requested namespace - metadata { - name = "namespace-admin" - namespace = "default" - } - - rule { - api_groups = ["*"] - resources = ["*"] - verbs = ["*"] - } -} diff --git a/terraform/provision-providers.tf b/terraform/provision-providers.tf index 6117271b..da16caab 100644 --- a/terraform/provision-providers.tf +++ b/terraform/provision-providers.tf @@ -6,38 +6,29 @@ variable "aws_secret_access_key" { type = string } +# Trying to put the data source *outside* the module that provisions the cluster here +# Ref https://github.com/terraform-aws-modules/terraform-aws-eks/issues/911#issuecomment-906190150 +data "aws_eks_cluster" "cluster" { + name = module.provision-aws.cluster-id +} + +data "aws_eks_cluster_auth" "cluster" { + name = module.provision-aws.cluster-id +} + provider "kubernetes" { alias = "provision" - host = module.provision.server - cluster_ca_certificate = base64decode(module.provision.certificate_authority_data) - - exec { - api_version = "client.authentication.k8s.io/v1alpha1" - args = ["token", "--cluster-id", module.provision.cluster-id] - command = "aws-iam-authenticator" - env = { - AWS_ACCESS_KEY_ID = var.aws_access_key_id, - AWS_SECRET_ACCESS_KEY = var.aws_secret_access_key - } - } + host = data.aws_eks_cluster.cluster.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) + token = data.aws_eks_cluster_auth.cluster.token } provider "helm" { alias = "provision" debug = true kubernetes { - host = module.provision.server - cluster_ca_certificate = base64decode(module.provision.certificate_authority_data) - - exec { - api_version = "client.authentication.k8s.io/v1alpha1" - args = ["token", "--cluster-id", module.provision.cluster-id] - command = "aws-iam-authenticator" - env = { - AWS_ACCESS_KEY_ID = var.aws_access_key_id, - AWS_SECRET_ACCESS_KEY = var.aws_secret_access_key - } - } + host = data.aws_eks_cluster.cluster.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) + token = data.aws_eks_cluster_auth.cluster.token } } - diff --git a/test.sh b/test.sh index abd37ff4..a6d213d1 100755 --- a/test.sh +++ b/test.sh @@ -14,13 +14,14 @@ if [[ -z ${1+x} ]] ; then exit 1 fi -SERVICE_INFO="$(cat $1 | jq -r .credentials)" +SERVICE_INFO="$(jq -r .credentials < "$1")" # Set up the kubeconfig -export KUBECONFIG=$(mktemp) -echo "$SERVICE_INFO" | jq -r '.kubeconfig' > ${KUBECONFIG} -export DOMAIN_NAME=$(echo "$SERVICE_INFO" | jq -r '.domain_name') - +KUBECONFIG=$(mktemp) +export KUBECONFIG +echo "$SERVICE_INFO" | jq -r '.kubeconfig' > "${KUBECONFIG}" +DOMAIN_NAME=$(echo "$SERVICE_INFO" | jq -r '.domain_name') +export DOMAIN_NAME echo "To work directly with the instance:" echo "export KUBECONFIG=${KUBECONFIG}" @@ -29,20 +30,112 @@ echo "Running tests..." # Test 1 echo "Deploying the test fixture..." -kubectl apply -f terraform/modules/provision/2048_fixture.yml - -echo "Waiting 3 minutes for the workload to start and the DNS entry to be created..." -sleep 180 - -export TEST_HOST=ingress-2048.${DOMAIN_NAME} +export SUBDOMAIN=subdomain-2048 +export TEST_HOST=${SUBDOMAIN}.${DOMAIN_NAME} export TEST_URL=https://${TEST_HOST} -echo -n "Testing that the ingress is resolvable via SSL, and that it's properly pointing at the 2048 app..." -(curl --silent --show-error ${TEST_URL} | fgrep '2048' > /dev/null) -if [[ $? == 0 ]]; then echo PASS; else retval=1; echo FAIL; fi +cat <<-TESTFIXTURE | kubectl apply -f - +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-2048 +spec: + selector: + matchLabels: + app.kubernetes.io/name: app-2048 + replicas: 2 + template: + metadata: + labels: + app.kubernetes.io/name: app-2048 + spec: + containers: + - image: alexwhen/docker-2048 + imagePullPolicy: Always + name: app-2048 + ports: + - containerPort: 80 + securityContext: + allowPrivilegeEscalation: false +--- +apiVersion: v1 +kind: Service +metadata: + name: service-2048 +spec: + ports: + - port: 80 + targetPort: 80 + protocol: TCP + type: ClusterIP + selector: + app.kubernetes.io/name: app-2048 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ${SUBDOMAIN} + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + # We want TTL to be quick in case we want to run tests in quick succession + external-dns.alpha.kubernetes.io/ttl: "30" +spec: + rules: + - host: ${TEST_HOST} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: service-2048 + port: + number: 80 +TESTFIXTURE + +# We set the ingress to appear at a subdomain. It will take a minute for +# external-dns to make the Route 53 entry for that subdomain, and for that +# record to propagate. By waiting here, we are testing that both the +# ingress-nginx controller and external-dns are working correctly. +# +# Notes: +# - host and dig are not available in the CSB container, but nslookup is. +# - We have found that the propagation speed for both the CNAME and DS record +# can be v-e-r-y s-l-o-w and depend a lot on your DNS provider. Which is why +# we've set the timeout to 30 minutes here. +echo -n "Waiting up to 1800 seconds for the ${TEST_HOST} subdomain to be resolvable..." +time=0 +while true; do + # I'm not crazy about this test but I can't think of a better one. + + if (nslookup -type=CNAME "$TEST_HOST" | grep -q "canonical name ="); then + echo PASS + break + elif [[ $time -gt 1800 ]]; then + retval=1; echo FAIL; break; + fi + time=$((time+5)) + sleep 5 + echo -ne "\r($time seconds) ..." + +done echo "You can try the fixture yourself by visiting:" -echo ${TEST_URL} +echo "${TEST_URL}" + +echo -n "Waiting up to 600 seconds for the ingress to respond with the expected content via SSL..." +time=0 +while true; do + if (curl --silent --show-error "${TEST_URL}" | grep -F '2048'); then + echo PASS; break; + elif [[ $time -gt 600 ]]; then + retval=1; echo FAIL; break; + fi + time=$((time+5)) + sleep 5 + echo -ne "\r($time seconds) ..." +done # timeout(): Test whether a command finishes before a deadline # Usage: @@ -54,11 +147,11 @@ echo ${TEST_URL} # http://blog.mediatribe.net/fr/node/72/index.html function timeout () { local timeout=${TIMEOUT_DEADLINE_SECS:-65} - "$@" & - sleep ${timeout} + "$@" 2>/dev/null & + sleep "${timeout}" # If the process has already exited, kill returns a non-zero exit status If # the process hasn't already exited, kill returns a zero exit status - if kill $! # 2> /dev/null + if kill $! > /dev/null 2>&1 then # The command was still running at the deadline and had to be killed echo "The command did NOT exit within ${timeout} seconds." @@ -73,13 +166,33 @@ function timeout () { # or the process is killed. timeout() will complain if it takes longer than 65 # seconds to end on its own. echo -n "Testing that connections are closed after 60s of inactivity... " -(timeout openssl s_client -quiet -connect ${TEST_HOST}:443 2> /dev/null) -if [[ $? == 0 ]]; then echo PASS; else retval=1; echo FAIL; fi - -echo -n "Testing DNSSSEC configuration is valid... " -dnssec_validates=$(delv @8.8.8.8 ${DOMAIN_NAME} +yaml | grep -o '\s*\- fully_validated:' | wc -l) -if [[ $dnssec_validated != 0 ]]; then echo PASS; else retval=1; echo FAIL; fi +if (timeout openssl s_client -quiet -connect "${TEST_HOST}":443); then + echo PASS; +else + retval=1; + echo FAIL; +fi +# We are explicitly disabling the followiung DNSSEC configuration validity test +# until we can do it without relying on unknown intermediate resolver support +# for DNSSEC. See issue here: +# https://github.com/gsa/data.gov/issues/3751 + +# echo -n "Waiting up to 600 seconds for the DNSSEC chain-of-trust to be validated... " +# time=0 +# while true; do +# if [[ $(delv "${DOMAIN_NAME}" +yaml | grep -o '\s*\- fully_validated:' | wc -l) != 0 ]]; then +# echo PASS; +# break; +# elif [[ $time -gt 600 ]]; then +# retval=1; +# echo FAIL; +# break; +# fi +# time=$((time+5)) +# sleep 5 +# echo -ne "\r($time seconds) ..." +# done # Test 2 - ebs dynamic provisioning echo -n "Provisioning PV resources... " @@ -91,7 +204,7 @@ kubectl wait --for=condition=ready --timeout=600s pod ebs-app sleep 10 echo -n "Verify pod can write to EFS volume..." -if [[ $(kubectl exec -ti ebs-app -- cat /data/out.txt | grep "Pod was here!") ]]; then +if (kubectl exec -ti ebs-app -- cat /data/out.txt | grep -q "Pod was here!"); then echo PASS else retval=1 @@ -100,6 +213,7 @@ fi # Cleanup -rm ${KUBECONFIG} - +rm "${KUBECONFIG}" +echo "You can reset your terminal without losing backscroll by running: stty sane" exit $retval +