From 0c89f57ac50270752a767e6bfd7919a1c4a79f55 Mon Sep 17 00:00:00 2001 From: 3u13r Date: Tue, 17 Oct 2023 15:46:15 +0200 Subject: [PATCH] Support internal load balancers (#2388) * arch: support internal lb on Azure * arch: support internal lb on GCP * helm: remove lb svc from verify deployment * arch: support internal lb on AWS * terraform: add jump hosts for internal lb * cli: expose internalLoadBalancer in config * ci: add e2e-manual-internal * add in-cluster endpoint to terraform output --- .../actions/constellation_create/action.yml | 9 + .github/actions/e2e_test/action.yml | 3 + .../workflows/e2e-test-manual-internal.yml | 227 ++++++++++ bootstrapper/cmd/bootstrapper/main.go | 4 + cli/internal/cloudcmd/tfvars.go | 20 +- cli/internal/helm/BUILD.bazel | 1 - .../templates/loadbalancer-service.yaml | 18 - .../verification-service/values.schema.json | 15 +- cli/internal/helm/overrides.go | 3 +- cli/internal/state/state.go | 6 +- cli/internal/state/state_doc.go | 51 +-- cli/internal/terraform/BUILD.bazel | 11 + cli/internal/terraform/terraform.go | 18 +- cli/internal/terraform/terraform/aws/main.tf | 21 +- .../terraform/aws/modules/jump_host/main.tf | 59 +++ .../terraform/aws/modules/jump_host/output.tf | 3 + .../aws/modules/jump_host/variables.tf | 29 ++ .../aws/modules/load_balancer_target/main.tf | 11 +- .../terraform/terraform/aws/outputs.tf | 19 +- .../terraform/terraform/aws/variables.tf | 6 + .../terraform/azure/.terraform.lock.hcl | 23 ++ .../terraform/terraform/azure/main.tf | 44 +- .../terraform/azure/modules/jump_host/main.tf | 85 ++++ .../azure/modules/jump_host/outputs.tf | 3 + .../azure/modules/jump_host/variables.tf | 29 ++ .../terraform/terraform/azure/outputs.tf | 21 +- .../terraform/terraform/azure/variables.tf | 6 + cli/internal/terraform/terraform/gcp/main.tf | 70 +++- .../modules/internal_load_balancer/main.tf | 72 ++++ .../internal_load_balancer/variables.tf | 54 +++ .../terraform/gcp/modules/jump_host/main.tf | 73 ++++ .../gcp/modules/jump_host/outputs.tf | 3 + .../gcp/modules/jump_host/variables.tf | 30 ++ .../terraform/terraform/gcp/outputs.tf | 23 +- .../terraform/terraform/gcp/variables.tf | 6 + cli/internal/terraform/terraform_test.go | 11 +- cli/internal/terraform/variables.go | 10 + cli/internal/terraform/variables_test.go | 11 +- go.sum | 2 + hack/go.sum | 2 + internal/cloud/aws/aws.go | 56 +-- internal/cloud/aws/aws_test.go | 388 +++++++----------- internal/cloud/azure/BUILD.bazel | 4 + internal/cloud/azure/azure.go | 122 +++++- internal/config/config.go | 9 + internal/config/config_doc.go | 31 +- 46 files changed, 1310 insertions(+), 412 deletions(-) create mode 100644 .github/workflows/e2e-test-manual-internal.yml delete mode 100644 cli/internal/helm/charts/edgeless/constellation-services/charts/verification-service/templates/loadbalancer-service.yaml create mode 100644 cli/internal/terraform/terraform/aws/modules/jump_host/main.tf create mode 100644 cli/internal/terraform/terraform/aws/modules/jump_host/output.tf create mode 100644 cli/internal/terraform/terraform/aws/modules/jump_host/variables.tf create mode 100644 cli/internal/terraform/terraform/azure/modules/jump_host/main.tf create mode 100644 cli/internal/terraform/terraform/azure/modules/jump_host/outputs.tf create mode 100644 cli/internal/terraform/terraform/azure/modules/jump_host/variables.tf create mode 100644 cli/internal/terraform/terraform/gcp/modules/internal_load_balancer/main.tf create mode 100644 cli/internal/terraform/terraform/gcp/modules/internal_load_balancer/variables.tf create mode 100644 cli/internal/terraform/terraform/gcp/modules/jump_host/main.tf create mode 100644 cli/internal/terraform/terraform/gcp/modules/jump_host/outputs.tf create mode 100644 cli/internal/terraform/terraform/gcp/modules/jump_host/variables.tf diff --git a/.github/actions/constellation_create/action.yml b/.github/actions/constellation_create/action.yml index 7864523bb..d1cc60529 100644 --- a/.github/actions/constellation_create/action.yml +++ b/.github/actions/constellation_create/action.yml @@ -47,6 +47,9 @@ inputs: refStream: description: "Reference and stream of the image in use" required: false + internalLoadBalancer: + description: "Whether to use an internal load balancer for the control plane" + required: false outputs: kubeconfig: @@ -115,6 +118,12 @@ runs: run: | yq eval -i '(.debugCluster) = true' constellation-conf.yaml + - name: Enable internalLoadBalancer flag + if: inputs.internalLoadBalancer == 'true' + shell: bash + run: | + yq eval -i '(.internalLoadBalancer) = true' constellation-conf.yaml + # Uses --force flag since the CLI currently does not have a pre-release version and is always on the latest released version. # However, many of our pipelines work on prerelease images. Thus the used images are newer than the CLI's version. # This makes the version validation in the CLI fail. diff --git a/.github/actions/e2e_test/action.yml b/.github/actions/e2e_test/action.yml index 9e8f5f87c..4931ea561 100644 --- a/.github/actions/e2e_test/action.yml +++ b/.github/actions/e2e_test/action.yml @@ -74,6 +74,8 @@ inputs: default: "false" azureSNPEnforcementPolicy: description: "Enable security policy for the cluster." + internalLoadBalancer: + description: "Enable internal load balancer for the cluster." outputs: kubeconfig: @@ -253,6 +255,7 @@ runs: azureClusterCreateCredentials: ${{ inputs.azureClusterCreateCredentials }} kubernetesVersion: ${{ inputs.kubernetesVersion }} refStream: ${{ inputs.refStream }} + internalLoadBalancer: ${{ inputs.internalLoadBalancer }} - name: Deploy log- and metrics-collection (Kubernetes) id: deploy-logcollection diff --git a/.github/workflows/e2e-test-manual-internal.yml b/.github/workflows/e2e-test-manual-internal.yml new file mode 100644 index 000000000..dc57d5273 --- /dev/null +++ b/.github/workflows/e2e-test-manual-internal.yml @@ -0,0 +1,227 @@ +name: e2e test manual internal LB + +on: + workflow_dispatch: + inputs: + nodeCount: + description: "Number of nodes to use in the cluster. Given in format `:`." + default: "3:2" + type: string + cloudProvider: + description: "Which cloud provider to use." + type: choice + options: + - "gcp" + - "azure" + - "aws" + default: "azure" + required: true + test: + description: "The test to run." + type: choice + options: + - "sonobuoy quick" + - "sonobuoy full" + - "autoscaling" + - "lb" + - "perf-bench" + - "verify" + - "recover" + - "malicious join" + - "nop" + required: true + kubernetesVersion: + description: "Kubernetes version to create the cluster from." + default: "1.27" + required: true + cliVersion: + description: "Version of a released CLI to download. Leave empty to build the CLI from the checked out ref." + type: string + default: "" + required: false + imageVersion: + description: "Full name of OS image (CSP independent image version UID). Leave empty for latest debug image on main." + type: string + default: "" + required: false + workflow_call: + inputs: + nodeCount: + description: "Number of nodes to use in the cluster. Given in format `:`." + default: "3:2" + type: string + cloudProvider: + description: "Which cloud provider to use." + type: string + required: true + test: + description: "The test to run." + type: string + required: true + kubernetesVersion: + description: "Kubernetes version to create the cluster from." + type: string + required: true + cliVersion: + description: "Version of a released CLI to download. Leave empty to build the CLI from the checked out ref." + type: string + default: "" + required: false + imageVersion: + description: "Full name of OS image (CSP independent image version UID). Leave empty for latest debug image on main." + type: string + default: "" + required: false + +jobs: + split-nodeCount: + name: Split nodeCount + runs-on: ubuntu-22.04 + permissions: + id-token: write + contents: read + outputs: + workerNodes: ${{ steps.split-nodeCount.outputs.workerNodes }} + controlPlaneNodes: ${{ steps.split-nodeCount.outputs.controlPlaneNodes }} + steps: + - name: Split nodeCount + id: split-nodeCount + shell: bash + run: | + nodeCount="${{ inputs.nodeCount }}" + workerNodes="${nodeCount##*:}" + controlPlaneNodes="${nodeCount%%:*}" + + if [[ -z "${workerNodes}" ]] || [[ -z "{controlPlaneNodes}" ]]; then + echo "Invalid nodeCount input: '${nodeCount}'." + exit 1 + fi + + echo "workerNodes=${workerNodes}" | tee -a "$GITHUB_OUTPUT" + echo "controlPlaneNodes=${controlPlaneNodes}" | tee -a "$GITHUB_OUTPUT" + + find-latest-image: + name: Select image + runs-on: ubuntu-22.04 + permissions: + id-token: write + contents: read + outputs: + image: ${{ steps.find-latest-image.outputs.output }}${{ steps.check-input.outputs.image }} + steps: + - name: Check input + id: check-input + shell: bash + run: | + if [[ -z "${{ inputs.imageVersion }}" ]]; then + echo "Using latest debug image from main." + exit 0 + else + echo "image=${{ inputs.imageVersion }}" | tee -a "$GITHUB_OUTPUT" + fi + + - name: Checkout head + if: inputs.imageVersion == '' + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || '' }} + + - name: Login to AWS + if: inputs.imageVersion == '' + uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 + with: + role-to-assume: arn:aws:iam::795746500882:role/GithubConstellationVersionsAPIRead + aws-region: eu-central-1 + + - name: Find latest image + id: find-latest-image + if: inputs.imageVersion == '' + uses: ./.github/actions/versionsapi + with: + command: latest + ref: main + stream: debug + + - name: Is debug image? + id: isDebugImage + shell: bash + run: | + case "${{ inputs.imageVersion }}" in + "") + ;; + *"/stream/debug/"*) + ;; + *) + echo "Only debug images are supported for internal LB tests." + exit 1 + ;; + esac + + e2e-test-manual: + runs-on: ubuntu-22.04 + permissions: + id-token: write + checks: write + contents: read + packages: write + needs: [find-latest-image, split-nodeCount] + if: always() && !cancelled() + steps: + - name: Install basic tools (macOS) + if: runner.os == 'macOS' + shell: bash + run: brew install coreutils kubectl bash terraform + + - name: Checkout head + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || '' }} + + - name: Run manual E2E test + id: e2e_test + uses: ./.github/actions/e2e_test + with: + workerNodesCount: ${{ needs.split-nodeCount.outputs.workerNodes }} + controlNodesCount: ${{ needs.split-nodeCount.outputs.controlPlaneNodes }} + cloudProvider: ${{ inputs.cloudProvider }} + gcpProject: ${{ secrets.GCP_E2E_PROJECT }} + gcpClusterCreateServiceAccount: "constellation-e2e-cluster@constellation-331613.iam.gserviceaccount.com" + gcpIAMCreateServiceAccount: "constellation-iam-e2e@constellation-331613.iam.gserviceaccount.com" + gcpInClusterServiceAccountKey: ${{ secrets.GCP_CLUSTER_SERVICE_ACCOUNT }} + test: ${{ inputs.test }} + kubernetesVersion: ${{ inputs.kubernetesVersion }} + awsOpenSearchDomain: ${{ secrets.AWS_OPENSEARCH_DOMAIN }} + awsOpenSearchUsers: ${{ secrets.AWS_OPENSEARCH_USER }} + awsOpenSearchPwd: ${{ secrets.AWS_OPENSEARCH_PWD }} + osImage: ${{ needs.find-latest-image.outputs.image }} + cliVersion: ${{ inputs.cliVersion }} + isDebugImage: true + buildBuddyApiKey: ${{ secrets.BUILDBUDDY_ORG_API_KEY }} + azureClusterCreateCredentials: ${{ secrets.AZURE_E2E_CLUSTER_CREDENTIALS }} + azureIAMCreateCredentials: ${{ secrets.AZURE_E2E_IAM_CREDENTIALS }} + registry: ghcr.io + githubToken: ${{ secrets.GITHUB_TOKEN }} + cosignPassword: ${{ secrets.COSIGN_PASSWORD }} + cosignPrivateKey: ${{ secrets.COSIGN_PRIVATE_KEY }} + fetchMeasurements: ${{ contains(needs.find-latest-image.outputs.image, '/stream/stable/') }} + internalLoadBalancer: true + + - name: Always terminate cluster + if: always() + uses: ./.github/actions/constellation_destroy + with: + kubeconfig: ${{ steps.e2e_test.outputs.kubeconfig }} + + - name: Always delete IAM configuration + if: always() + uses: ./.github/actions/constellation_iam_destroy + with: + cloudProvider: ${{ inputs.cloudProvider }} + azureCredentials: ${{ secrets.AZURE_E2E_IAM_CREDENTIALS }} + gcpServiceAccount: "constellation-iam-e2e@constellation-331613.iam.gserviceaccount.com" + + - name: Always upload Terraform logs + if: always() + uses: ./.github/actions/upload_terraform_logs + with: + artifactNameSuffix: ${{ steps.e2e_test.outputs.namePrefix }} diff --git a/bootstrapper/cmd/bootstrapper/main.go b/bootstrapper/cmd/bootstrapper/main.go index aab7c440a..f2f600823 100644 --- a/bootstrapper/cmd/bootstrapper/main.go +++ b/bootstrapper/cmd/bootstrapper/main.go @@ -125,6 +125,10 @@ func main() { if err != nil { log.With(zap.Error(err)).Fatalf("Failed to set up cloud logger") } + if err := metadata.PrepareControlPlaneNode(ctx, log); err != nil { + log.With(zap.Error(err)).Fatalf("Failed to prepare Azure control plane node") + } + metadataAPI = metadata clusterInitJoiner = kubernetes.New( "azure", k8sapi.NewKubernetesUtil(), &k8sapi.KubdeadmConfiguration{}, kubectl.NewUninitialized(), diff --git a/cli/internal/cloudcmd/tfvars.go b/cli/internal/cloudcmd/tfvars.go index c2e08f452..9d01926c8 100644 --- a/cli/internal/cloudcmd/tfvars.go +++ b/cli/internal/cloudcmd/tfvars.go @@ -103,6 +103,7 @@ func awsTerraformVars(conf *config.Config, imageRef string) *terraform.AWSCluste Debug: conf.IsDebugCluster(), EnableSNP: conf.GetAttestationConfig().GetVariant().Equal(variant.AWSSEVSNP{}), CustomEndpoint: conf.CustomEndpoint, + InternalLoadBalancer: conf.InternalLoadBalancer, } } @@ -143,6 +144,7 @@ func azureTerraformVars(conf *config.Config, imageRef string) *terraform.AzureCl UserAssignedIdentity: conf.Provider.Azure.UserAssignedIdentity, ResourceGroup: conf.Provider.Azure.ResourceGroup, CustomEndpoint: conf.CustomEndpoint, + InternalLoadBalancer: conf.InternalLoadBalancer, } vars = normalizeAzureURIs(vars) @@ -172,14 +174,15 @@ func gcpTerraformVars(conf *config.Config, imageRef string) *terraform.GCPCluste } } return &terraform.GCPClusterVariables{ - Name: conf.Name, - NodeGroups: nodeGroups, - Project: conf.Provider.GCP.Project, - Region: conf.Provider.GCP.Region, - Zone: conf.Provider.GCP.Zone, - ImageID: imageRef, - Debug: conf.IsDebugCluster(), - CustomEndpoint: conf.CustomEndpoint, + Name: conf.Name, + NodeGroups: nodeGroups, + Project: conf.Provider.GCP.Project, + Region: conf.Provider.GCP.Region, + Zone: conf.Provider.GCP.Zone, + ImageID: imageRef, + Debug: conf.IsDebugCluster(), + CustomEndpoint: conf.CustomEndpoint, + InternalLoadBalancer: conf.InternalLoadBalancer, } } @@ -218,6 +221,7 @@ func openStackTerraformVars(conf *config.Config, imageRef string) *terraform.Ope Debug: conf.IsDebugCluster(), NodeGroups: nodeGroups, CustomEndpoint: conf.CustomEndpoint, + InternalLoadBalancer: conf.InternalLoadBalancer, } } diff --git a/cli/internal/helm/BUILD.bazel b/cli/internal/helm/BUILD.bazel index a93787d05..c70f18430 100644 --- a/cli/internal/helm/BUILD.bazel +++ b/cli/internal/helm/BUILD.bazel @@ -252,7 +252,6 @@ go_library( "charts/edgeless/constellation-services/charts/verification-service/.helmignore", "charts/edgeless/constellation-services/charts/verification-service/Chart.yaml", "charts/edgeless/constellation-services/charts/verification-service/templates/daemonset.yaml", - "charts/edgeless/constellation-services/charts/verification-service/templates/loadbalancer-service.yaml", "charts/edgeless/constellation-services/charts/verification-service/templates/nodeport-service.yaml", "charts/edgeless/constellation-services/charts/verification-service/values.schema.json", "charts/edgeless/constellation-services/charts/verification-service/values.yaml", diff --git a/cli/internal/helm/charts/edgeless/constellation-services/charts/verification-service/templates/loadbalancer-service.yaml b/cli/internal/helm/charts/edgeless/constellation-services/charts/verification-service/templates/loadbalancer-service.yaml deleted file mode 100644 index 8e2e45aaf..000000000 --- a/cli/internal/helm/charts/edgeless/constellation-services/charts/verification-service/templates/loadbalancer-service.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: verify - namespace: {{ .Release.Namespace }} -spec: - allocateLoadBalancerNodePorts: false - externalIPs: - - {{ .Values.loadBalancerIP | quote }} - loadBalancerClass: constellation - ports: - - name: grpc - port: {{ .Values.grpcNodePort }} - protocol: TCP - targetPort: {{ .Values.grpcContainerPort }} - selector: - k8s-app: verification-service - type: LoadBalancer diff --git a/cli/internal/helm/charts/edgeless/constellation-services/charts/verification-service/values.schema.json b/cli/internal/helm/charts/edgeless/constellation-services/charts/verification-service/values.schema.json index 56538fe4a..7cc900e67 100644 --- a/cli/internal/helm/charts/edgeless/constellation-services/charts/verification-service/values.schema.json +++ b/cli/internal/helm/charts/edgeless/constellation-services/charts/verification-service/values.schema.json @@ -4,21 +4,22 @@ "image": { "description": "Container image to use for the spawned pods.", "type": "string", - "examples": ["ghcr.io/edgelesssys/constellation/join-service:latest"] - }, - "loadBalancerIP": { - "description": "IP of the k8s LB service", - "type": "string" + "examples": [ + "ghcr.io/edgelesssys/constellation/join-service:latest" + ] }, "attestationVariant": { "description": "Attestation variant to use for aTLS connections.", "type": "string", - "examples": ["azure-sev-snp", "azure-trusted-launch", "gcp-sev-es"] + "examples": [ + "azure-sev-snp", + "azure-trusted-launch", + "gcp-sev-es" + ] } }, "required": [ "image", - "loadBalancerIP", "attestationVariant" ], "title": "Values", diff --git a/cli/internal/helm/overrides.go b/cli/internal/helm/overrides.go index 836b6e977..caf56e8cb 100644 --- a/cli/internal/helm/overrides.go +++ b/cli/internal/helm/overrides.go @@ -42,7 +42,7 @@ func extraCiliumValues(provider cloudprovider.Provider, conformanceMode bool, ou } } - extraVals["k8sServiceHost"] = output.ClusterEndpoint + extraVals["k8sServiceHost"] = output.InClusterEndpoint extraVals["k8sServicePort"] = constants.KubernetesPort if provider == cloudprovider.GCP { extraVals["ipv4NativeRoutingCIDR"] = output.GCP.IPCidrPod @@ -62,7 +62,6 @@ func extraConstellationServicesValues( } extraVals["verification-service"] = map[string]any{ "attestationVariant": cfg.GetAttestationConfig().GetVariant().String(), - "loadBalancerIP": output.ClusterEndpoint, } extraVals["konnectivity"] = map[string]any{ "loadBalancerIP": output.ClusterEndpoint, diff --git a/cli/internal/state/state.go b/cli/internal/state/state.go index b4409eeee..7671acf90 100644 --- a/cli/internal/state/state.go +++ b/cli/internal/state/state.go @@ -89,9 +89,13 @@ type Infrastructure struct { // Unique identifier the cluster's cloud resources are tagged with. UID string `yaml:"uid"` // description: | - // Endpoint the cluster can be reached at. + // Endpoint the cluster can be reached at. This is the endpoint that is being used by the CLI. ClusterEndpoint string `yaml:"clusterEndpoint"` // description: | + // The Cluster uses to reach itself. This might differ from the ClusterEndpoint in case e.g., + // an internal load balancer is used. + InClusterEndpoint string `yaml:"inClusterEndpoint"` + // description: | // Secret used to authenticate the bootstrapping node. InitSecret HexBytes `yaml:"initSecret"` // description: | diff --git a/cli/internal/state/state_doc.go b/cli/internal/state/state_doc.go index ff9455e98..48aa8a29c 100644 --- a/cli/internal/state/state_doc.go +++ b/cli/internal/state/state_doc.go @@ -74,7 +74,7 @@ func init() { FieldName: "infrastructure", }, } - InfrastructureDoc.Fields = make([]encoder.Doc, 7) + InfrastructureDoc.Fields = make([]encoder.Doc, 8) InfrastructureDoc.Fields[0].Name = "uid" InfrastructureDoc.Fields[0].Type = "string" InfrastructureDoc.Fields[0].Note = "" @@ -83,33 +83,38 @@ func init() { InfrastructureDoc.Fields[1].Name = "clusterEndpoint" InfrastructureDoc.Fields[1].Type = "string" InfrastructureDoc.Fields[1].Note = "" - InfrastructureDoc.Fields[1].Description = "Endpoint the cluster can be reached at." - InfrastructureDoc.Fields[1].Comments[encoder.LineComment] = "Endpoint the cluster can be reached at." - InfrastructureDoc.Fields[2].Name = "initSecret" - InfrastructureDoc.Fields[2].Type = "HexBytes" + InfrastructureDoc.Fields[1].Description = "Endpoint the cluster can be reached at. This is the endpoint that is being used by the CLI." + InfrastructureDoc.Fields[1].Comments[encoder.LineComment] = "Endpoint the cluster can be reached at. This is the endpoint that is being used by the CLI." + InfrastructureDoc.Fields[2].Name = "inClusterEndpoint" + InfrastructureDoc.Fields[2].Type = "string" InfrastructureDoc.Fields[2].Note = "" - InfrastructureDoc.Fields[2].Description = "Secret used to authenticate the bootstrapping node." - InfrastructureDoc.Fields[2].Comments[encoder.LineComment] = "Secret used to authenticate the bootstrapping node." - InfrastructureDoc.Fields[3].Name = "apiServerCertSANs" - InfrastructureDoc.Fields[3].Type = "[]string" + InfrastructureDoc.Fields[2].Description = "The Cluster uses to reach itself. This might differ from the ClusterEndpoint in case e.g.,\nan internal load balancer is used." + InfrastructureDoc.Fields[2].Comments[encoder.LineComment] = "The Cluster uses to reach itself. This might differ from the ClusterEndpoint in case e.g.," + InfrastructureDoc.Fields[3].Name = "initSecret" + InfrastructureDoc.Fields[3].Type = "HexBytes" InfrastructureDoc.Fields[3].Note = "" - InfrastructureDoc.Fields[3].Description = "description: |\n List of Subject Alternative Names (SANs) to add to the Kubernetes API server certificate.\n If no SANs should be added, this field can be left empty.\n" - InfrastructureDoc.Fields[3].Comments[encoder.LineComment] = "description: |" - InfrastructureDoc.Fields[4].Name = "name" - InfrastructureDoc.Fields[4].Type = "string" + InfrastructureDoc.Fields[3].Description = "Secret used to authenticate the bootstrapping node." + InfrastructureDoc.Fields[3].Comments[encoder.LineComment] = "Secret used to authenticate the bootstrapping node." + InfrastructureDoc.Fields[4].Name = "apiServerCertSANs" + InfrastructureDoc.Fields[4].Type = "[]string" InfrastructureDoc.Fields[4].Note = "" - InfrastructureDoc.Fields[4].Description = "Name used in the cluster's named resources." - InfrastructureDoc.Fields[4].Comments[encoder.LineComment] = "Name used in the cluster's named resources." - InfrastructureDoc.Fields[5].Name = "azure" - InfrastructureDoc.Fields[5].Type = "Azure" + InfrastructureDoc.Fields[4].Description = "description: |\n List of Subject Alternative Names (SANs) to add to the Kubernetes API server certificate.\n If no SANs should be added, this field can be left empty.\n" + InfrastructureDoc.Fields[4].Comments[encoder.LineComment] = "description: |" + InfrastructureDoc.Fields[5].Name = "name" + InfrastructureDoc.Fields[5].Type = "string" InfrastructureDoc.Fields[5].Note = "" - InfrastructureDoc.Fields[5].Description = "Values specific to a Constellation cluster running on Azure." - InfrastructureDoc.Fields[5].Comments[encoder.LineComment] = "Values specific to a Constellation cluster running on Azure." - InfrastructureDoc.Fields[6].Name = "gcp" - InfrastructureDoc.Fields[6].Type = "GCP" + InfrastructureDoc.Fields[5].Description = "Name used in the cluster's named resources." + InfrastructureDoc.Fields[5].Comments[encoder.LineComment] = "Name used in the cluster's named resources." + InfrastructureDoc.Fields[6].Name = "azure" + InfrastructureDoc.Fields[6].Type = "Azure" InfrastructureDoc.Fields[6].Note = "" - InfrastructureDoc.Fields[6].Description = "Values specific to a Constellation cluster running on GCP." - InfrastructureDoc.Fields[6].Comments[encoder.LineComment] = "Values specific to a Constellation cluster running on GCP." + InfrastructureDoc.Fields[6].Description = "Values specific to a Constellation cluster running on Azure." + InfrastructureDoc.Fields[6].Comments[encoder.LineComment] = "Values specific to a Constellation cluster running on Azure." + InfrastructureDoc.Fields[7].Name = "gcp" + InfrastructureDoc.Fields[7].Type = "GCP" + InfrastructureDoc.Fields[7].Note = "" + InfrastructureDoc.Fields[7].Description = "Values specific to a Constellation cluster running on GCP." + InfrastructureDoc.Fields[7].Comments[encoder.LineComment] = "Values specific to a Constellation cluster running on GCP." GCPDoc.Type = "GCP" GCPDoc.Comments[encoder.LineComment] = "GCP describes the infra state related to GCP." diff --git a/cli/internal/terraform/BUILD.bazel b/cli/internal/terraform/BUILD.bazel index 2078d35b9..f1d394cdc 100644 --- a/cli/internal/terraform/BUILD.bazel +++ b/cli/internal/terraform/BUILD.bazel @@ -73,6 +73,17 @@ go_library( "terraform/iam/aws/.terraform.lock.hcl", "terraform/iam/azure/.terraform.lock.hcl", "terraform/iam/gcp/.terraform.lock.hcl", + "terraform/gcp/modules/internal_load_balancer/main.tf", + "terraform/gcp/modules/internal_load_balancer/variables.tf", + "terraform/gcp/modules/jump_host/main.tf", + "terraform/gcp/modules/jump_host/outputs.tf", + "terraform/gcp/modules/jump_host/variables.tf", + "terraform/aws/modules/jump_host/main.tf", + "terraform/aws/modules/jump_host/output.tf", + "terraform/aws/modules/jump_host/variables.tf", + "terraform/azure/modules/jump_host/main.tf", + "terraform/azure/modules/jump_host/variables.tf", + "terraform/azure/modules/jump_host/outputs.tf", ], importpath = "github.com/edgelesssys/constellation/v2/cli/internal/terraform", visibility = ["//cli:__subpackages__"], diff --git a/cli/internal/terraform/terraform.go b/cli/internal/terraform/terraform.go index 84a1ac17d..4ed951d62 100644 --- a/cli/internal/terraform/terraform.go +++ b/cli/internal/terraform/terraform.go @@ -181,11 +181,20 @@ func (c *Client) ShowInfrastructure(ctx context.Context, provider cloudprovider. return state.Infrastructure{}, errors.New("terraform show: no values returned") } - ipOutput, ok := tfState.Values.Outputs["ip"] + outOfClusterEndpointOutput, ok := tfState.Values.Outputs["out_of_cluster_endpoint"] if !ok { - return state.Infrastructure{}, errors.New("no IP output found") + return state.Infrastructure{}, errors.New("no out_of_cluster_endpoint output found") } - ip, ok := ipOutput.Value.(string) + outOfClusterEndpoint, ok := outOfClusterEndpointOutput.Value.(string) + if !ok { + return state.Infrastructure{}, errors.New("invalid type in IP output: not a string") + } + + inClusterEndpointOutput, ok := tfState.Values.Outputs["in_cluster_endpoint"] + if !ok { + return state.Infrastructure{}, errors.New("no in_cluster_endpoint output found") + } + inClusterEndpoint, ok := inClusterEndpointOutput.Value.(string) if !ok { return state.Infrastructure{}, errors.New("invalid type in IP output: not a string") } @@ -231,7 +240,8 @@ func (c *Client) ShowInfrastructure(ctx context.Context, provider cloudprovider. } res := state.Infrastructure{ - ClusterEndpoint: ip, + ClusterEndpoint: outOfClusterEndpoint, + InClusterEndpoint: inClusterEndpoint, APIServerCertSANs: apiServerCertSANs, InitSecret: []byte(secret), UID: uid, diff --git a/cli/internal/terraform/terraform/aws/main.tf b/cli/internal/terraform/terraform/aws/main.tf index 62c697e26..0968eb143 100644 --- a/cli/internal/terraform/terraform/aws/main.tf +++ b/cli/internal/terraform/terraform/aws/main.tf @@ -51,6 +51,9 @@ locals { tags = { constellation-uid = local.uid, } + + in_cluster_endpoint = aws_lb.front_end.dns_name + out_of_cluster_endpoint = var.internal_load_balancer && var.debug ? module.jump_host[0].ip : local.in_cluster_endpoint } resource "random_id" "uid" { @@ -84,14 +87,14 @@ resource "aws_eip" "lb" { # in a future version to support all availability zones in the chosen region # This should only be done after we migrated to DNS-based addressing for the # control-plane. - for_each = toset([var.zone]) + for_each = var.internal_load_balancer ? [] : toset([var.zone]) domain = "vpc" tags = merge(local.tags, { "constellation-ip-endpoint" = each.key == var.zone ? "legacy-primary-zone" : "additional-zone" }) } resource "aws_lb" "front_end" { name = "${local.name}-loadbalancer" - internal = false + internal = var.internal_load_balancer load_balancer_type = "network" tags = local.tags security_groups = [aws_security_group.security_group.id] @@ -106,7 +109,7 @@ resource "aws_lb" "front_end" { for_each = toset([var.zone]) content { subnet_id = module.public_private_subnet.public_subnet_id[subnet_mapping.key] - allocation_id = aws_eip.lb[subnet_mapping.key].id + allocation_id = var.internal_load_balancer ? "" : aws_eip.lb[subnet_mapping.key].id } } enable_cross_zone_load_balancing = true @@ -206,6 +209,17 @@ module "instance_group" { ) } +module "jump_host" { + count = var.internal_load_balancer && var.debug ? 1 : 0 + source = "./modules/jump_host" + base_name = local.name + subnet_id = module.public_private_subnet.public_subnet_id[var.zone] + lb_internal_ip = aws_lb.front_end.dns_name + ports = [for port in local.load_balancer_ports : port.port] + iam_instance_profile = var.iam_instance_profile_worker_nodes + security_group_id = aws_security_group.security_group.id +} + # TODO(31u3r): Remove once 2.12 is released moved { from = module.load_balancer_target_konnectivity @@ -241,3 +255,4 @@ moved { from = module.load_balancer_target_bootstrapper to = module.load_balancer_targets["bootstrapper"] } + diff --git a/cli/internal/terraform/terraform/aws/modules/jump_host/main.tf b/cli/internal/terraform/terraform/aws/modules/jump_host/main.tf new file mode 100644 index 000000000..ad5d24e23 --- /dev/null +++ b/cli/internal/terraform/terraform/aws/modules/jump_host/main.tf @@ -0,0 +1,59 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.17.0" + } + } +} + + +data "aws_ami" "ubuntu" { + most_recent = true + owners = ["099720109477"] # Canonical + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] + } +} + +resource "aws_instance" "jump_host" { + ami = data.aws_ami.ubuntu.id + instance_type = "c5a.large" + associate_public_ip_address = true + + iam_instance_profile = var.iam_instance_profile + subnet_id = var.subnet_id + security_groups = [var.security_group_id] + + tags = { + "Name" = "${var.base_name}-jump-host" + } + + user_data = < port } + for_each = var.internal_load_balancer ? {} : { for port in local.control_plane_named_ports : port.name => port } source = "./modules/loadbalancer" name = local.name backend_port_name = each.value.name port = each.value.port health_check = each.value.health_check backend_instance_groups = local.control_plane_instance_groups - ip_address = google_compute_global_address.loadbalancer_ip.self_link + ip_address = google_compute_global_address.loadbalancer_ip[0].self_link frontend_labels = merge(local.labels, { constellation-use = each.value.name }) } +module "loadbalancer_internal" { + for_each = var.internal_load_balancer ? { for port in local.control_plane_named_ports : port.name => port } : {} + source = "./modules/internal_load_balancer" + name = local.name + backend_port_name = each.value.name + port = each.value.port + health_check = each.value.health_check + backend_instance_group = local.control_plane_instance_groups[0] + ip_address = google_compute_address.loadbalancer_ip_internal[0].self_link + frontend_labels = merge(local.labels, { constellation-use = each.value.name }) + + region = var.region + network = google_compute_network.vpc_network.id + backend_subnet = google_compute_subnetwork.ilb_subnet[0].id +} + +module "jump_host" { + count = var.internal_load_balancer && var.debug ? 1 : 0 + source = "./modules/jump_host" + base_name = local.name + zone = var.zone + subnetwork = google_compute_subnetwork.vpc_subnetwork.id + labels = local.labels + lb_internal_ip = google_compute_address.loadbalancer_ip_internal[0].address + ports = [for port in local.control_plane_named_ports : port.port] +} moved { from = module.loadbalancer_boot to = module.loadbalancer_public["bootstrapper"] @@ -210,11 +269,6 @@ moved { to = module.loadbalancer_public["recovery"] } -moved { - from = module.loadbalancer_join - to = module.loadbalancer_public["join"] -} - moved { from = module.loadbalancer_debugd[0] to = module.loadbalancer_public["debugd"] diff --git a/cli/internal/terraform/terraform/gcp/modules/internal_load_balancer/main.tf b/cli/internal/terraform/terraform/gcp/modules/internal_load_balancer/main.tf new file mode 100644 index 000000000..00ed5a0a7 --- /dev/null +++ b/cli/internal/terraform/terraform/gcp/modules/internal_load_balancer/main.tf @@ -0,0 +1,72 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "4.83.0" + } + } +} + +locals { + name = "${var.name}-${var.backend_port_name}" +} + +resource "google_compute_region_health_check" "health" { + name = local.name + region = var.region + check_interval_sec = 1 + timeout_sec = 1 + + dynamic "tcp_health_check" { + for_each = var.health_check == "TCP" ? [1] : [] + content { + port = var.port + } + } + + dynamic "https_health_check" { + for_each = var.health_check == "HTTPS" ? [1] : [] + content { + host = "" + port = var.port + request_path = "/readyz" + } + } +} + +resource "google_compute_region_backend_service" "backend" { + name = local.name + protocol = "TCP" + load_balancing_scheme = "INTERNAL_MANAGED" + health_checks = [google_compute_region_health_check.health.id] + port_name = var.backend_port_name + timeout_sec = 240 + region = var.region + + backend { + group = var.backend_instance_group + balancing_mode = "UTILIZATION" + capacity_scaler = 1.0 + } +} + +resource "google_compute_region_target_tcp_proxy" "proxy" { + name = local.name + region = var.region + backend_service = google_compute_region_backend_service.backend.id +} + +# forwarding rule +resource "google_compute_forwarding_rule" "forwarding" { + name = local.name + network = var.network + subnetwork = var.backend_subnet + region = var.region + ip_address = var.ip_address + ip_protocol = "TCP" + load_balancing_scheme = "INTERNAL_MANAGED" + port_range = var.port + allow_global_access = true + target = google_compute_region_target_tcp_proxy.proxy.id + labels = var.frontend_labels +} diff --git a/cli/internal/terraform/terraform/gcp/modules/internal_load_balancer/variables.tf b/cli/internal/terraform/terraform/gcp/modules/internal_load_balancer/variables.tf new file mode 100644 index 000000000..4ba586426 --- /dev/null +++ b/cli/internal/terraform/terraform/gcp/modules/internal_load_balancer/variables.tf @@ -0,0 +1,54 @@ +variable "name" { + type = string + description = "Base name of the load balancer." +} + +variable "region" { + type = string + description = "The region where the load balancer will be created." +} + +variable "network" { + type = string + description = "The network to which all network resources will be attached." +} + +variable "backend_subnet" { + type = string + description = "The subnet to which all backend network resources will be attached." +} + +variable "health_check" { + type = string + description = "The type of the health check. 'HTTPS' or 'TCP'." + validation { + condition = contains(["HTTPS", "TCP"], var.health_check) + error_message = "Health check must be either 'HTTPS' or 'TCP'." + } +} + +variable "port" { + type = string + description = "The port on which to listen for incoming traffic." +} + +variable "backend_port_name" { + type = string + description = "Name of backend port. The same name should appear in the instance groups referenced by this service." +} + +variable "backend_instance_group" { + type = string + description = "The URL of the instance group resource from which the load balancer will direct traffic." +} + +variable "ip_address" { + type = string + description = "The IP address that this forwarding rule serves." +} + +variable "frontend_labels" { + type = map(string) + default = {} + description = "Labels to apply to the forwarding rule." +} diff --git a/cli/internal/terraform/terraform/gcp/modules/jump_host/main.tf b/cli/internal/terraform/terraform/gcp/modules/jump_host/main.tf new file mode 100644 index 000000000..f8de3e92f --- /dev/null +++ b/cli/internal/terraform/terraform/gcp/modules/jump_host/main.tf @@ -0,0 +1,73 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "4.83.0" + } + + google-beta = { + source = "hashicorp/google-beta" + version = "4.83.0" + } + } +} + + +data "google_compute_image" "image_ubuntu" { + family = "ubuntu-2204-lts" + project = "ubuntu-os-cloud" +} + +resource "google_compute_instance" "vm_instance" { + name = "${var.base_name}-jumphost" + machine_type = "n2d-standard-4" + zone = var.zone + + boot_disk { + initialize_params { + image = data.google_compute_image.image_ubuntu.self_link + } + } + + network_interface { + subnetwork = var.subnetwork + access_config { + } + } + + service_account { + scopes = ["compute-ro"] + } + + labels = var.labels + + metadata = { + serial-port-enable = "TRUE" + } + + metadata_startup_script = <, LB IP, 6443, TCP). +// Now the load balancer does not re-write the source IP address only the destination (DNAT). +// Therefore the 5-tuple is (VM IP, , VM IP, 6443, TCP). +// Now the VM responds to the SYN packet with a SYN-ACK packet, but the outgoing +// connection waits on a response from the load balancer and not the VM therefore +// dropping the packet. +// +// OpenShift also uses the same mechanism to redirect traffic to the API server: +// https://github.com/openshift/machine-config-operator/blob/e453bd20bac0e48afa74e9a27665abaf454d93cd/templates/master/00-master/azure/files/opt-libexec-openshift-azure-routes-sh.yaml +func (c *Cloud) PrepareControlPlaneNode(ctx context.Context, log *logger.Logger) error { + selfMetadata, err := c.Self(ctx) + if err != nil { + return fmt.Errorf("failed to get self metadata: %w", err) + } + + // skipping iptables setup for worker nodes + if selfMetadata.Role != role.ControlPlane { + log.Infof("not a control plane node, skipping iptables setup") + return nil + } + + // skipping iptables setup if no internal LB exists e.g. + // for public LB architectures + loadbalancerIP, err := c.getLoadBalancerPrivateIP(ctx) + if err != nil { + log.With(zap.Error(err)).Warnf("skipping iptables setup, failed to get load balancer private IP") + return nil + } + + log.Infof("Setting up iptables for control plane node with load balancer IP %s", loadbalancerIP) + + iptablesExec := iptables.New(exec.New(), iptables.ProtocolIPv4) + if err != nil { + return fmt.Errorf("failed to create iptables client: %w", err) + } + + const chainName = "azure-lb-nat" + if _, err := iptablesExec.EnsureChain(iptables.TableNAT, chainName); err != nil { + return fmt.Errorf("failed to create iptables chain: %w", err) + } + + if _, err := iptablesExec.EnsureRule(iptables.Append, iptables.TableNAT, "PREROUTING", "-j", chainName); err != nil { + return fmt.Errorf("failed to add rule to iptables chain: %w", err) + } + + if _, err := iptablesExec.EnsureRule(iptables.Append, iptables.TableNAT, "OUTPUT", "-j", chainName); err != nil { + return fmt.Errorf("failed to add rule to iptables chain: %w", err) + } + + if _, err := iptablesExec.EnsureRule(iptables.Append, iptables.TableNAT, chainName, "--dst", loadbalancerIP, "-p", "tcp", "--dport", "6443", "-j", "REDIRECT"); err != nil { + return fmt.Errorf("failed to add rule to iptables chain: %w", err) + } + + return nil +} + // convertToInstanceMetadata converts a armcomputev2.VirtualMachineScaleSetVM to a metadata.InstanceMetadata. func convertToInstanceMetadata(vm armcompute.VirtualMachineScaleSetVM, networkInterfaces []armnetwork.Interface, ) (metadata.InstanceMetadata, error) { diff --git a/internal/config/config.go b/internal/config/config.go index 020101b31..1e5711dbe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -82,6 +82,9 @@ type Config struct { // A fallback to DNS name is always available. CustomEndpoint string `yaml:"customEndpoint" validate:"omitempty,hostname_rfc1123"` // description: | + // Flag to enable/disable the internal load balancer. If enabled, the Constellation is only accessible from within the VPC. + InternalLoadBalancer bool `yaml:"internalLoadBalancer" validate:"omitempty"` + // description: | // Supported cloud providers and their specific configurations. Provider ProviderConfig `yaml:"provider" validate:"dive"` // description: | @@ -830,6 +833,12 @@ func (c *Config) Validate(force bool) error { } } + if c.InternalLoadBalancer { + if c.GetProvider() != cloudprovider.AWS && c.GetProvider() != cloudprovider.GCP { + return &ValidationError{validationErrMsgs: []string{"internalLoadBalancer is only supported for AWS and GCP"}} + } + } + err := validate.Struct(c) if err == nil { return nil diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index 443fd9657..31ca913a3 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -35,7 +35,7 @@ func init() { ConfigDoc.Type = "Config" ConfigDoc.Comments[encoder.LineComment] = "Config defines configuration used by CLI." ConfigDoc.Description = "Config defines configuration used by CLI." - ConfigDoc.Fields = make([]encoder.Doc, 10) + ConfigDoc.Fields = make([]encoder.Doc, 11) ConfigDoc.Fields[0].Name = "version" ConfigDoc.Fields[0].Type = "string" ConfigDoc.Fields[0].Note = "" @@ -71,21 +71,26 @@ func init() { ConfigDoc.Fields[6].Note = "" ConfigDoc.Fields[6].Description = "Optional custom endpoint (DNS name) for the Constellation API server.\nThis can be used to point a custom dns name at the Constellation API server\nand is added to the Subject Alternative Name (SAN) field of the TLS certificate used by the API server.\nA fallback to DNS name is always available." ConfigDoc.Fields[6].Comments[encoder.LineComment] = "Optional custom endpoint (DNS name) for the Constellation API server." - ConfigDoc.Fields[7].Name = "provider" - ConfigDoc.Fields[7].Type = "ProviderConfig" + ConfigDoc.Fields[7].Name = "internalLoadBalancer" + ConfigDoc.Fields[7].Type = "bool" ConfigDoc.Fields[7].Note = "" - ConfigDoc.Fields[7].Description = "Supported cloud providers and their specific configurations." - ConfigDoc.Fields[7].Comments[encoder.LineComment] = "Supported cloud providers and their specific configurations." - ConfigDoc.Fields[8].Name = "nodeGroups" - ConfigDoc.Fields[8].Type = "map[string]NodeGroup" + ConfigDoc.Fields[7].Description = "Flag to enable/disable the internal load balancer. If enabled, the Constellation is only accessible from within the VPC." + ConfigDoc.Fields[7].Comments[encoder.LineComment] = "Flag to enable/disable the internal load balancer. If enabled, the Constellation is only accessible from within the VPC." + ConfigDoc.Fields[8].Name = "provider" + ConfigDoc.Fields[8].Type = "ProviderConfig" ConfigDoc.Fields[8].Note = "" - ConfigDoc.Fields[8].Description = "Node groups to be created in the cluster." - ConfigDoc.Fields[8].Comments[encoder.LineComment] = "Node groups to be created in the cluster." - ConfigDoc.Fields[9].Name = "attestation" - ConfigDoc.Fields[9].Type = "AttestationConfig" + ConfigDoc.Fields[8].Description = "Supported cloud providers and their specific configurations." + ConfigDoc.Fields[8].Comments[encoder.LineComment] = "Supported cloud providers and their specific configurations." + ConfigDoc.Fields[9].Name = "nodeGroups" + ConfigDoc.Fields[9].Type = "map[string]NodeGroup" ConfigDoc.Fields[9].Note = "" - ConfigDoc.Fields[9].Description = "Configuration for attestation validation. This configuration provides sensible defaults for the Constellation version it was created for.\nSee the docs for an overview on attestation: https://docs.edgeless.systems/constellation/architecture/attestation" - ConfigDoc.Fields[9].Comments[encoder.LineComment] = "Configuration for attestation validation. This configuration provides sensible defaults for the Constellation version it was created for.\nSee the docs for an overview on attestation: https://docs.edgeless.systems/constellation/architecture/attestation" + ConfigDoc.Fields[9].Description = "Node groups to be created in the cluster." + ConfigDoc.Fields[9].Comments[encoder.LineComment] = "Node groups to be created in the cluster." + ConfigDoc.Fields[10].Name = "attestation" + ConfigDoc.Fields[10].Type = "AttestationConfig" + ConfigDoc.Fields[10].Note = "" + ConfigDoc.Fields[10].Description = "Configuration for attestation validation. This configuration provides sensible defaults for the Constellation version it was created for.\nSee the docs for an overview on attestation: https://docs.edgeless.systems/constellation/architecture/attestation" + ConfigDoc.Fields[10].Comments[encoder.LineComment] = "Configuration for attestation validation. This configuration provides sensible defaults for the Constellation version it was created for.\nSee the docs for an overview on attestation: https://docs.edgeless.systems/constellation/architecture/attestation" ProviderConfigDoc.Type = "ProviderConfig" ProviderConfigDoc.Comments[encoder.LineComment] = "ProviderConfig are cloud-provider specific configuration values used by the CLI."