diff --git a/blueprints/apigee/hybrid-gke/README.md b/blueprints/apigee/hybrid-gke/README.md index cee4aec1..30589716 100644 --- a/blueprints/apigee/hybrid-gke/README.md +++ b/blueprints/apigee/hybrid-gke/README.md @@ -25,20 +25,20 @@ The diagram below depicts the architecture. terraform apply ``` +Create an A record in your DNS registrar to point the environment group hostname to the public IP address returned after the terraform configuration was applied. You might need to wait some time until the certificate is provisioned. + ## Testing the blueprint 2. Deploy an api proxy ``` - ./deploy-apiproxy.sh + ./deploy-apiproxy.sh apis-test ``` -3. In the console check the IP address that has been allocated to the Apigee ingress gateway and send some traffic to the deployed API proxy. +3. Send a request ``` - curl -k -v -H "Host:HOSTNAME" \ - --resolve HOSTNAME:443:IP_ADDRESS \ - https://HOSTNAME/httpbin/headers + curl -v https://HOSTNAME/httpbin/headers ``` @@ -56,4 +56,10 @@ The diagram below depicts the architecture. | [region](variables.tf#L84) | Region. | string | | "europe-west1" | | [zone](variables.tf#L90) | Zone. | string | | "europe-west1-c" | +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [ip_address](outputs.tf#L17) | GLB IP address. | | + diff --git a/blueprints/apigee/hybrid-gke/ansible.tf b/blueprints/apigee/hybrid-gke/ansible.tf index e5a491a3..b7694ab1 100644 --- a/blueprints/apigee/hybrid-gke/ansible.tf +++ b/blueprints/apigee/hybrid-gke/ansible.tf @@ -18,12 +18,13 @@ resource "local_file" "vars_file" { content = yamlencode({ - cluster = module.cluster.name - region = var.region - project_id = module.project.project_id - envgroup = local.envgroup - env = local.environment - hostname = var.hostname + cluster = module.cluster.name + region = var.region + project_id = module.project.project_id + envgroups = local.envgroups + environments = local.environments + service_accounts = local.google_sas + ingress_ip_name = local.ingress_ip_name }) filename = "${path.module}/ansible/vars/vars.yaml" file_permission = "0666" diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml new file mode 100644 index 00000000..e74ca159 --- /dev/null +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/k8s_service_accounts.yaml @@ -0,0 +1,28 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Create and annotate k8s service account + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: "{{ k8s_service_account }}" + namespace: apigee + annotations: + iam.gke.io/gcp-service-account: "{{ google_service_account }}@{{ project_id }}.iam.gserviceaccount.com" + with_items: "{{ k8s_service_accounts }}" + loop_control: + loop_var: k8s_service_account \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml index 4b72039b..0907846f 100644 --- a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/tasks/main.yaml @@ -1,11 +1,11 @@ # Copyright 2023 Google LLC -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,18 +19,27 @@ --project {{ project_id }} \ --internal-ip -- name: Install cert-manager - shell: > - kubectl apply \ - --validate=false \ - -f https://github.com/jetstack/cert-manager/releases/download/v1.7.2/cert-manager.yaml +- name: Download cert-manager + uri: + url: https://github.com/jetstack/cert-manager/releases/download/v1.7.2/cert-manager.yaml + dest: ~/cert-manager.yaml -- name: Wait until pods are ready in cert-manager namespace - shell: > - kubectl wait --for=condition=ready pods \ - -l app.kubernetes.io/instance=cert-manager \ - -n cert-manager \ - --timeout=90s +- name: Apply metrics-server manifest to the cluster. + kubernetes.core.k8s: + state: present + src: ~/cert-manager.yaml + +- name: + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app.kubernetes.io/instance=cert-manager" + namespace: cert-manager + wait_timeout: 90 + wait_condition: + type: Ready + status: True - name: Fetch apigeectl version uri: @@ -48,7 +57,7 @@ unarchive: src: "~/apigeectl.tar.gz" dest: "~" - remote_src: yes + remote_src: yes - name: Move apigeectl folder shell: > @@ -66,25 +75,69 @@ file: src: ~/apigeectl/{{ item }} dest: "~/hybrid-files/{{ item }}" - state: link + state: link with_items: - tools - config - templates - - plugins + - plugins -- name: Create service accounts - shell: > - ~/hybrid-files/tools/create-service-account -i {{ project_id }} -e non-prod -d ~/hybrid-files/service-accounts +- name: Create apigee namespace + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: apigee -- name: Create certificates +- name: Create k8s service accounts + include_tasks: k8s_service_accounts.yaml + vars: + google_service_account: "{{ item.key }}" + k8s_service_accounts: "{{ item.value }}" + with_dict: "{{ service_accounts }}" + +- name: Set hostnames + set_fact: + hostnames: "{{ hostnames | default([]) + item.value }}" + with_dict: "{{ envgroups }}" + +- name: Create certificate and private key shell: > openssl req \ -nodes \ -new \ -x509 \ - -keyout ~/hybrid-files/certs/{{ envgroup }}.key \ - -out ~/hybrid-files/certs/{{ envgroup }}.cert -subj '/CN='{{ hostname }}'' -days 3650 + -keyout ~/hybrid-files/certs/server.key \ + -out ~/hybrid-files/certs/server.crt \ + -subj "/CN=apigee.com' \ + -addext "subjectAltName={{ hostnames | map('regex_replace', '^', 'DNS:') | join(',') }}"" + -days 3650 + +- name: Read certificate + slurp: + src: ~/hybrid-files/certs/server.crt + register: certificate_output + +- name: Read private ket + slurp: + src: ~/hybrid-files/certs/server.key + register: privatekey_output + +- name: Create secret + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: tls-hybrid-ingress + namespace: apigee + type: kubernetes.io/tls + data: + tls.crt: "{{ certificate_output.content }}" + tls.key: "{{ privatekey_output.content }}" - name: Create overrides.yaml template: @@ -96,48 +149,185 @@ curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type:application/json" \ "https://apigee.googleapis.com/v1/organizations/{{ project_id }}:setSyncAuthorization" \ - -d '{"identities":["'"serviceAccount:apigee-non-prod@{{ project_id }}.iam.gserviceaccount.com"'"]}' + -d '{"identities":["'"serviceAccount:apigee-synchronizer@{{ project_id }}.iam.gserviceaccount.com"'"]}' - name: Dry-run (init) shell: > - ~/apigeectl/apigeectl init -f overrides/overrides.yaml --dry-run=client + ~/apigeectl/apigeectl init -f overrides/overrides.yaml --dry-run=client args: chdir: ~/hybrid-files - name: Install the Apigee deployment services Apigee Deployment Controller and Apigee Admission Webhook. shell: > - ~/apigeectl/apigeectl init -f overrides/overrides.yaml + ~/apigeectl/apigeectl init -f overrides/overrides.yaml args: - chdir: ~/hybrid-files + chdir: ~/hybrid-files -- name: Wait until pods are ready in apigee-system namespace - shell: > - kubectl wait --for=condition=ready pods \ - -l app=apigee-controller \ - -n apigee-system \ - --timeout=300s +- name: Wait for apigee-controller pod to be ready + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app=apigee-controller" + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True -- name: Wait until pods are ready in apigee namespace - shell: > - kubectl wait --for=condition=ready pods \ - -l app=apigee-ingressgateway-manager \ - -n apigee \ - --timeout=300s +- name: Wait for apigee-selfsigned-issuer issuer to be ready + kubernetes.core.k8s_info: + kind: Issuer + wait: yes + name: apigee-selfsigned-issuer + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True + +- name: Wait for apigee-serving-cert certificate to be ready + kubernetes.core.k8s_info: + kind: Certificate + wait: yes + name: apigee-serving-cert + namespace: apigee-system + wait_timeout: 600 + wait_condition: + type: Ready + status: True + +- name: Wait for apigee-resources-install job to be complete + kubernetes.core.k8s_info: + kind: Job + wait: yes + name: apigee-resources-install + namespace: apigee-system + wait_timeout: 360 + wait_condition: + type: Complete + status: True - name: Dry-run (apply) shell: > - ~/apigeectl/apigeectl apply -f overrides/overrides.yaml --dry-run=client + ~/apigeectl/apigeectl apply -f overrides/overrides.yaml --dry-run=client args: chdir: ~/hybrid-files - name: Install the Apigee runtime components shell: > - ~/apigeectl/apigeectl apply -f overrides/overrides.yaml + ~/apigeectl/apigeectl apply -f overrides/overrides.yaml args: - chdir: ~/hybrid-files + chdir: ~/hybrid-files -- name: Check status of the deployment - shell: > - while [ -n "$(kubectl get pods -n apigee | tail -n +2 | grep -v Running | grep -v Completed)" ]; do sleep 1; done - args: - chdir: ~/hybrid-files \ No newline at end of file +- name: Wait for apigee-runtime pod to be ready + kubernetes.core.k8s_info: + kind: Pod + wait: yes + label_selectors: + - "app=apigee-runtime" + namespace: apigee + wait_timeout: 360 + wait_condition: + type: Ready + status: True + +- name: + kubernetes.core.k8s: + state: present + definition: + apiVersion: apigee.cloud.google.com/v1alpha1 + kind: ApigeeRoute + metadata: + name: apigee-wildcard + namespace: apigee + spec: + hostnames: + - '*' + ports: + - number: 443 + protocol: HTTPS + tls: + credentialName: tls-hybrid-ingress + mode: SIMPLE + selector: + app: apigee-ingressgateway + enableNonSniClient: true + +- name: Create google-managed certificate + kubernetes.core.k8s: + state: present + definition: + apiVersion: networking.gke.io/v1 + kind: ManagedCertificate + metadata: + name: "apigee-cert-hybrid" + namespace: apigee + spec: + domains: "{{ hostnames }}" + +- name: Create backend config + kubernetes.core.k8s: + state: present + definition: + apiVersion: cloud.google.com/v1 + kind: BackendConfig + metadata: + name: apigee-ingress-backendconfig + namespace: apigee + spec: + healthCheck: + requestPath: /healthz/ready + port: 15021 + type: HTTP + logging: + enable: true + sampleRate: 0.5 + +- name: Create service + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Service + metadata: + name: apigee-ingressgateway-hybrid + namespace: apigee + annotations: + cloud.google.com/backend-config: '{"default": "apigee-ingress-backendconfig"}' + cloud.google.com/neg: '{"ingress": true}' + cloud.google.com/app-protocols: '{"https":"HTTPS", "status-port": "HTTP"}' + labels: + app: apigee-ingressgateway-hybrid + spec: + ports: + - name: status-port + port: 15021 + targetPort: 15021 + - name: https + port: 443 + targetPort: 8443 + selector: + app: apigee-ingressgateway + ingress_name: ingress + type: ClusterIP + +- name: Create ingress + kubernetes.core.k8s: + state: present + definition: + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + annotations: + networking.gke.io/managed-certificates: "apigee-cert-hybrid" + kubernetes.io/ingress.global-static-ip-name: "{{ ingress_ip_name }}" + kubernetes.io/ingress.allow-http: "false" + name: xlb-apigee + namespace: apigee + spec: + defaultBackend: + service: + name: apigee-ingressgateway-hybrid + port: + number: 443 \ No newline at end of file diff --git a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 index 1c2c09ed..691cc6d5 100644 --- a/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 +++ b/blueprints/apigee/hybrid-gke/ansible/roles/apigee-hybrid/templates/overrides.yaml.j2 @@ -1,29 +1,26 @@ gcp: region: {{ region }} projectID: {{ project_id }} + workloadIdentityEnabled: true k8sCluster: name: {{ cluster }} - region: CLUSTER_LOCATION # Must be the closest Google Cloud region to your cluster. + region: {{ region }} # Must be the closest Google Cloud region to your cluster. org: {{ project_id }} -instanceID: "instance-1" +instanceID: "{{ cluster }}-{{ region }}" cassandra: hostNetwork: false - # Set to false for single region installations and multi-region installations - # with connectivity between pods in different clusters, for example GKE installations. - # Set to true for multi-region installations with no communication between - # pods in different clusters, for example GKE On-prem, GKE on AWS, Anthos on bare metal, - # AKS, EKS, and OpenShift installations. - # See Multi-region deployment: Prerequisites virtualhosts: - - name: {{ envgroup }} +{% for k in envgroups %} + - name: {{ k }} + sslSecret: tls-hybrid-ingress + additionalGateways: ["apigee-wildcard"] selector: app: apigee-ingressgateway - sslCertPath: ./certs/{{ envgroup }}.cert - sslKeyPath: ./certs/{{ envgroup }}.key +{% endfor %} ao: args: @@ -37,27 +34,9 @@ ingressGateways: replicaCountMax: 10 envs: - - name: {{ env }} - serviceAccountPaths: - synchronizer: ./service-accounts/{{ project_id }}-apigee-non-prod.json - udca: ./service-accounts/{{ project_id }}-apigee-non-prod.json - runtime: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -mart: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -connectAgent: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -metrics: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -udca: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json - -watcher: - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json +{% for k in environments %} + - name: {{ k }} +{% endfor %} logger: - enabled: true - serviceAccountPath: ./service-accounts/{{ project_id }}-apigee-non-prod.json + enabled: false diff --git a/blueprints/apigee/hybrid-gke/apigee.tf b/blueprints/apigee/hybrid-gke/apigee.tf index e3dc6b2e..b92592aa 100644 --- a/blueprints/apigee/hybrid-gke/apigee.tf +++ b/blueprints/apigee/hybrid-gke/apigee.tf @@ -15,8 +15,51 @@ */ locals { - envgroup = "test" - environment = "apis-test" + envgroups = { + test = [var.hostname] + } + environments = { + apis-test = { + envgroups = ["test"] + } + } + org_short_name = (length(module.project.project_id) < 16 ? + module.project.project_id : + substr(module.project.project_id, 0, 15)) + org_hash = format("%s-%s", local.org_short_name, substr(sha256(module.project.project_id), 0, 7)) + org_env_hashes = { + for k, v in local.environments : + k => format("%s-%s-%s", local.org_short_name, length(k) < 16 ? k : substr(k, 0, 15), substr(sha256("${module.project.project_id}:${k}"), 0, 7)) + } + google_sas = { + apigee-metrics = [ + "apigee-metrics-sa" + ] + apigee-cassandra = [ + "apigee-cassandra-schema-setup-${local.org_hash}-sa", + "apigee-cassandra-user-setup-${local.org_hash}-sa" + ] + apigee-mart = [ + "apigee-mart-${local.org_hash}-sa", + "apigee-connect-agent-${local.org_hash}-sa" + ] + apigee-watcher = [ + "apigee-watcher-${local.org_hash}-sa" + ] + apigee-udca = concat([ + "apigee-udca-${local.org_hash}-sa" + ], + [for k, v in local.org_env_hashes : + "apigee-udca-${local.org_env_hashes[k]}-sa" + ]) + apigee-synchronizer = [ + for k, v in local.org_env_hashes : + "apigee-synchronizer-${local.org_env_hashes[k]}-sa" + ] + apigee-runtime = [for k, v in local.org_env_hashes : + "apigee-runtime-${local.org_env_hashes[k]}-sa" + ] + } } module "apigee" { @@ -26,20 +69,24 @@ module "apigee" { analytics_region = var.region runtime_type = "HYBRID" } - envgroups = { - (local.envgroup) = [var.hostname] - } - environments = { - (local.environment) = { - envgroups = [local.envgroup] - } + envgroups = local.envgroups + environments = local.environments +} + +module "sas" { + for_each = local.google_sas + source = "../../../modules/iam-service-account" + project_id = module.project.project_id + name = each.key + # authoritative roles granted *on* the service accounts to other identities + iam = { + "roles/iam.workloadIdentityUser" = [for v in each.value : "serviceAccount:${module.project.project_id}.svc.id.goog[apigee/${v}]"] } } resource "local_file" "deploy_apiproxy_file" { content = templatefile("${path.module}/templates/deploy-apiproxy.sh.tpl", { org = module.project.project_id - env = local.environment }) filename = "${path.module}/deploy-apiproxy.sh" file_permission = "0777" diff --git a/blueprints/apigee/hybrid-gke/diagram.png b/blueprints/apigee/hybrid-gke/diagram.png index 6d5c2d6b..57e07ca3 100644 Binary files a/blueprints/apigee/hybrid-gke/diagram.png and b/blueprints/apigee/hybrid-gke/diagram.png differ diff --git a/blueprints/apigee/hybrid-gke/glb.tf b/blueprints/apigee/hybrid-gke/glb.tf new file mode 100644 index 00000000..80ff2269 --- /dev/null +++ b/blueprints/apigee/hybrid-gke/glb.tf @@ -0,0 +1,25 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + ingress_ip_name = "apigee" +} + +module "addresses" { + source = "../../../modules/net-address" + project_id = module.project.project_id + global_addresses = [local.ingress_ip_name] +} diff --git a/blueprints/apigee/hybrid-gke/main.tf b/blueprints/apigee/hybrid-gke/main.tf index 5f1a676b..5be174ef 100644 --- a/blueprints/apigee/hybrid-gke/main.tf +++ b/blueprints/apigee/hybrid-gke/main.tf @@ -40,5 +40,12 @@ module "project" { "roles/resourcemanager.projectIamAdmin" = [module.mgmt_server.service_account_iam_email] "roles/iam.serviceAccountAdmin" = [module.mgmt_server.service_account_iam_email] "roles/iam.serviceAccountKeyAdmin" = [module.mgmt_server.service_account_iam_email] + "roles/monitoring.metricWriter" = [module.sas["apigee-metrics"].iam_email] + "roles/storage.objectAdmin" = [module.sas["apigee-cassandra"].iam_email] + "roles/apigeeconnect.Agent" = [module.sas["apigee-mart"].iam_email] + "roles/apigee.runtimeAgent" = [module.sas["apigee-watcher"].iam_email] + "roles/apigee.analyticsAgent" = [module.sas["apigee-udca"].iam_email] + "roles/apigee.synchronizerManager" = [module.sas["apigee-synchronizer"].iam_email] + "roles/cloudtrace.agent" = [module.sas["apigee-runtime"].iam_email] } -} \ No newline at end of file +} diff --git a/blueprints/apigee/hybrid-gke/mgmt.tf b/blueprints/apigee/hybrid-gke/mgmt.tf index f51975f5..538940e7 100644 --- a/blueprints/apigee/hybrid-gke/mgmt.tf +++ b/blueprints/apigee/hybrid-gke/mgmt.tf @@ -34,4 +34,12 @@ module "mgmt_server" { type = var.mgmt_server_config.disk_type size = var.mgmt_server_config.disk_size } -} \ No newline at end of file + metadata = { + startup-script = <