From 55087b5ba8d699f82a8962772e45682ba5982675 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Wed, 3 Mar 2021 09:09:31 -0600 Subject: [PATCH 01/50] folder restructure --- Dockerfile => labs/gke-cloudbuild-progressions/Dockerfile | 0 .../gke-cloudbuild-progressions/builder}/cloudbuild-canary.yaml | 0 .../gke-cloudbuild-progressions/builder}/cloudbuild-dev.yaml | 0 .../gke-cloudbuild-progressions/builder}/cloudbuild-local.yaml | 0 .../gke-cloudbuild-progressions/builder}/cloudbuild-prod.yaml | 0 .../gke-cloudbuild-progressions/builder}/cloudbuild.yaml | 0 html.go => labs/gke-cloudbuild-progressions/html.go | 0 .../kubernetes}/deployments/canary/backend-canary.yaml | 0 .../kubernetes}/deployments/canary/frontend-canary.yaml | 0 .../kubernetes}/deployments/dev/backend-dev.yaml | 0 .../kubernetes}/deployments/dev/default.yml | 0 .../kubernetes}/deployments/dev/frontend-dev.yaml | 0 .../kubernetes}/deployments/prod/backend-production.yaml | 0 .../kubernetes}/deployments/prod/frontend-production.yaml | 0 .../gke-cloudbuild-progressions/kubernetes}/services/backend.yaml | 0 .../kubernetes}/services/frontend.yaml | 0 main.go => labs/gke-cloudbuild-progressions/main.go | 0 main_test.go => labs/gke-cloudbuild-progressions/main_test.go | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename Dockerfile => labs/gke-cloudbuild-progressions/Dockerfile (100%) rename {builder => labs/gke-cloudbuild-progressions/builder}/cloudbuild-canary.yaml (100%) rename {builder => labs/gke-cloudbuild-progressions/builder}/cloudbuild-dev.yaml (100%) rename {builder => labs/gke-cloudbuild-progressions/builder}/cloudbuild-local.yaml (100%) rename {builder => labs/gke-cloudbuild-progressions/builder}/cloudbuild-prod.yaml (100%) rename {builder => labs/gke-cloudbuild-progressions/builder}/cloudbuild.yaml (100%) rename html.go => labs/gke-cloudbuild-progressions/html.go (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/deployments/canary/backend-canary.yaml (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/deployments/canary/frontend-canary.yaml (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/deployments/dev/backend-dev.yaml (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/deployments/dev/default.yml (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/deployments/dev/frontend-dev.yaml (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/deployments/prod/backend-production.yaml (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/deployments/prod/frontend-production.yaml (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/services/backend.yaml (100%) rename {kubernetes => labs/gke-cloudbuild-progressions/kubernetes}/services/frontend.yaml (100%) rename main.go => labs/gke-cloudbuild-progressions/main.go (100%) rename main_test.go => labs/gke-cloudbuild-progressions/main_test.go (100%) diff --git a/Dockerfile b/labs/gke-cloudbuild-progressions/Dockerfile similarity index 100% rename from Dockerfile rename to labs/gke-cloudbuild-progressions/Dockerfile diff --git a/builder/cloudbuild-canary.yaml b/labs/gke-cloudbuild-progressions/builder/cloudbuild-canary.yaml similarity index 100% rename from builder/cloudbuild-canary.yaml rename to labs/gke-cloudbuild-progressions/builder/cloudbuild-canary.yaml diff --git a/builder/cloudbuild-dev.yaml b/labs/gke-cloudbuild-progressions/builder/cloudbuild-dev.yaml similarity index 100% rename from builder/cloudbuild-dev.yaml rename to labs/gke-cloudbuild-progressions/builder/cloudbuild-dev.yaml diff --git a/builder/cloudbuild-local.yaml b/labs/gke-cloudbuild-progressions/builder/cloudbuild-local.yaml similarity index 100% rename from builder/cloudbuild-local.yaml rename to labs/gke-cloudbuild-progressions/builder/cloudbuild-local.yaml diff --git a/builder/cloudbuild-prod.yaml b/labs/gke-cloudbuild-progressions/builder/cloudbuild-prod.yaml similarity index 100% rename from builder/cloudbuild-prod.yaml rename to labs/gke-cloudbuild-progressions/builder/cloudbuild-prod.yaml diff --git a/builder/cloudbuild.yaml b/labs/gke-cloudbuild-progressions/builder/cloudbuild.yaml similarity index 100% rename from builder/cloudbuild.yaml rename to labs/gke-cloudbuild-progressions/builder/cloudbuild.yaml diff --git a/html.go b/labs/gke-cloudbuild-progressions/html.go similarity index 100% rename from html.go rename to labs/gke-cloudbuild-progressions/html.go diff --git a/kubernetes/deployments/canary/backend-canary.yaml b/labs/gke-cloudbuild-progressions/kubernetes/deployments/canary/backend-canary.yaml similarity index 100% rename from kubernetes/deployments/canary/backend-canary.yaml rename to labs/gke-cloudbuild-progressions/kubernetes/deployments/canary/backend-canary.yaml diff --git a/kubernetes/deployments/canary/frontend-canary.yaml b/labs/gke-cloudbuild-progressions/kubernetes/deployments/canary/frontend-canary.yaml similarity index 100% rename from kubernetes/deployments/canary/frontend-canary.yaml rename to labs/gke-cloudbuild-progressions/kubernetes/deployments/canary/frontend-canary.yaml diff --git a/kubernetes/deployments/dev/backend-dev.yaml b/labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/backend-dev.yaml similarity index 100% rename from kubernetes/deployments/dev/backend-dev.yaml rename to labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/backend-dev.yaml diff --git a/kubernetes/deployments/dev/default.yml b/labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/default.yml similarity index 100% rename from kubernetes/deployments/dev/default.yml rename to labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/default.yml diff --git a/kubernetes/deployments/dev/frontend-dev.yaml b/labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/frontend-dev.yaml similarity index 100% rename from kubernetes/deployments/dev/frontend-dev.yaml rename to labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/frontend-dev.yaml diff --git a/kubernetes/deployments/prod/backend-production.yaml b/labs/gke-cloudbuild-progressions/kubernetes/deployments/prod/backend-production.yaml similarity index 100% rename from kubernetes/deployments/prod/backend-production.yaml rename to labs/gke-cloudbuild-progressions/kubernetes/deployments/prod/backend-production.yaml diff --git a/kubernetes/deployments/prod/frontend-production.yaml b/labs/gke-cloudbuild-progressions/kubernetes/deployments/prod/frontend-production.yaml similarity index 100% rename from kubernetes/deployments/prod/frontend-production.yaml rename to labs/gke-cloudbuild-progressions/kubernetes/deployments/prod/frontend-production.yaml diff --git a/kubernetes/services/backend.yaml b/labs/gke-cloudbuild-progressions/kubernetes/services/backend.yaml similarity index 100% rename from kubernetes/services/backend.yaml rename to labs/gke-cloudbuild-progressions/kubernetes/services/backend.yaml diff --git a/kubernetes/services/frontend.yaml b/labs/gke-cloudbuild-progressions/kubernetes/services/frontend.yaml similarity index 100% rename from kubernetes/services/frontend.yaml rename to labs/gke-cloudbuild-progressions/kubernetes/services/frontend.yaml diff --git a/main.go b/labs/gke-cloudbuild-progressions/main.go similarity index 100% rename from main.go rename to labs/gke-cloudbuild-progressions/main.go diff --git a/main_test.go b/labs/gke-cloudbuild-progressions/main_test.go similarity index 100% rename from main_test.go rename to labs/gke-cloudbuild-progressions/main_test.go From 0f41f9b0053cd61255214342bd7eb4a8217b08a3 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Wed, 3 Mar 2021 09:19:49 -0600 Subject: [PATCH 02/50] folder rename --- .../Dockerfile | 0 .../builder/cloudbuild-canary.yaml | 0 .../builder/cloudbuild-dev.yaml | 0 .../builder/cloudbuild-local.yaml | 0 .../builder/cloudbuild-prod.yaml | 0 .../builder/cloudbuild.yaml | 0 .../html.go | 0 .../kubernetes/deployments/canary/backend-canary.yaml | 0 .../kubernetes/deployments/canary/frontend-canary.yaml | 0 .../kubernetes/deployments/dev/backend-dev.yaml | 0 .../kubernetes/deployments/dev/default.yml | 0 .../kubernetes/deployments/dev/frontend-dev.yaml | 0 .../kubernetes/deployments/prod/backend-production.yaml | 0 .../kubernetes/deployments/prod/frontend-production.yaml | 0 .../kubernetes/services/backend.yaml | 0 .../kubernetes/services/frontend.yaml | 0 .../main.go | 0 .../main_test.go | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/Dockerfile (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/builder/cloudbuild-canary.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/builder/cloudbuild-dev.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/builder/cloudbuild-local.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/builder/cloudbuild-prod.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/builder/cloudbuild.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/html.go (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/deployments/canary/backend-canary.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/deployments/canary/frontend-canary.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/deployments/dev/backend-dev.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/deployments/dev/default.yml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/deployments/dev/frontend-dev.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/deployments/prod/backend-production.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/deployments/prod/frontend-production.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/services/backend.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/kubernetes/services/frontend.yaml (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/main.go (100%) rename labs/{gke-cloudbuild-progressions => gke-cloudbuild-progression}/main_test.go (100%) diff --git a/labs/gke-cloudbuild-progressions/Dockerfile b/labs/gke-cloudbuild-progression/Dockerfile similarity index 100% rename from labs/gke-cloudbuild-progressions/Dockerfile rename to labs/gke-cloudbuild-progression/Dockerfile diff --git a/labs/gke-cloudbuild-progressions/builder/cloudbuild-canary.yaml b/labs/gke-cloudbuild-progression/builder/cloudbuild-canary.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/builder/cloudbuild-canary.yaml rename to labs/gke-cloudbuild-progression/builder/cloudbuild-canary.yaml diff --git a/labs/gke-cloudbuild-progressions/builder/cloudbuild-dev.yaml b/labs/gke-cloudbuild-progression/builder/cloudbuild-dev.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/builder/cloudbuild-dev.yaml rename to labs/gke-cloudbuild-progression/builder/cloudbuild-dev.yaml diff --git a/labs/gke-cloudbuild-progressions/builder/cloudbuild-local.yaml b/labs/gke-cloudbuild-progression/builder/cloudbuild-local.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/builder/cloudbuild-local.yaml rename to labs/gke-cloudbuild-progression/builder/cloudbuild-local.yaml diff --git a/labs/gke-cloudbuild-progressions/builder/cloudbuild-prod.yaml b/labs/gke-cloudbuild-progression/builder/cloudbuild-prod.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/builder/cloudbuild-prod.yaml rename to labs/gke-cloudbuild-progression/builder/cloudbuild-prod.yaml diff --git a/labs/gke-cloudbuild-progressions/builder/cloudbuild.yaml b/labs/gke-cloudbuild-progression/builder/cloudbuild.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/builder/cloudbuild.yaml rename to labs/gke-cloudbuild-progression/builder/cloudbuild.yaml diff --git a/labs/gke-cloudbuild-progressions/html.go b/labs/gke-cloudbuild-progression/html.go similarity index 100% rename from labs/gke-cloudbuild-progressions/html.go rename to labs/gke-cloudbuild-progression/html.go diff --git a/labs/gke-cloudbuild-progressions/kubernetes/deployments/canary/backend-canary.yaml b/labs/gke-cloudbuild-progression/kubernetes/deployments/canary/backend-canary.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/deployments/canary/backend-canary.yaml rename to labs/gke-cloudbuild-progression/kubernetes/deployments/canary/backend-canary.yaml diff --git a/labs/gke-cloudbuild-progressions/kubernetes/deployments/canary/frontend-canary.yaml b/labs/gke-cloudbuild-progression/kubernetes/deployments/canary/frontend-canary.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/deployments/canary/frontend-canary.yaml rename to labs/gke-cloudbuild-progression/kubernetes/deployments/canary/frontend-canary.yaml diff --git a/labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/backend-dev.yaml b/labs/gke-cloudbuild-progression/kubernetes/deployments/dev/backend-dev.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/backend-dev.yaml rename to labs/gke-cloudbuild-progression/kubernetes/deployments/dev/backend-dev.yaml diff --git a/labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/default.yml b/labs/gke-cloudbuild-progression/kubernetes/deployments/dev/default.yml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/default.yml rename to labs/gke-cloudbuild-progression/kubernetes/deployments/dev/default.yml diff --git a/labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/frontend-dev.yaml b/labs/gke-cloudbuild-progression/kubernetes/deployments/dev/frontend-dev.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/deployments/dev/frontend-dev.yaml rename to labs/gke-cloudbuild-progression/kubernetes/deployments/dev/frontend-dev.yaml diff --git a/labs/gke-cloudbuild-progressions/kubernetes/deployments/prod/backend-production.yaml b/labs/gke-cloudbuild-progression/kubernetes/deployments/prod/backend-production.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/deployments/prod/backend-production.yaml rename to labs/gke-cloudbuild-progression/kubernetes/deployments/prod/backend-production.yaml diff --git a/labs/gke-cloudbuild-progressions/kubernetes/deployments/prod/frontend-production.yaml b/labs/gke-cloudbuild-progression/kubernetes/deployments/prod/frontend-production.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/deployments/prod/frontend-production.yaml rename to labs/gke-cloudbuild-progression/kubernetes/deployments/prod/frontend-production.yaml diff --git a/labs/gke-cloudbuild-progressions/kubernetes/services/backend.yaml b/labs/gke-cloudbuild-progression/kubernetes/services/backend.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/services/backend.yaml rename to labs/gke-cloudbuild-progression/kubernetes/services/backend.yaml diff --git a/labs/gke-cloudbuild-progressions/kubernetes/services/frontend.yaml b/labs/gke-cloudbuild-progression/kubernetes/services/frontend.yaml similarity index 100% rename from labs/gke-cloudbuild-progressions/kubernetes/services/frontend.yaml rename to labs/gke-cloudbuild-progression/kubernetes/services/frontend.yaml diff --git a/labs/gke-cloudbuild-progressions/main.go b/labs/gke-cloudbuild-progression/main.go similarity index 100% rename from labs/gke-cloudbuild-progressions/main.go rename to labs/gke-cloudbuild-progression/main.go diff --git a/labs/gke-cloudbuild-progressions/main_test.go b/labs/gke-cloudbuild-progression/main_test.go similarity index 100% rename from labs/gke-cloudbuild-progressions/main_test.go rename to labs/gke-cloudbuild-progression/main_test.go From f7d17912cdc74e569cdc599a91632bce4f263dc4 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Wed, 3 Mar 2021 16:31:53 -0600 Subject: [PATCH 03/50] folder restructure --- labs/{gke-cloudbuild-progression => gke-progression}/Dockerfile | 0 .../builder/cloudbuild-canary.yaml | 0 .../builder/cloudbuild-dev.yaml | 0 .../builder/cloudbuild-local.yaml | 0 .../builder/cloudbuild-prod.yaml | 0 .../builder/cloudbuild.yaml | 0 labs/{gke-cloudbuild-progression => gke-progression}/html.go | 0 .../kubernetes/deployments/canary/backend-canary.yaml | 0 .../kubernetes/deployments/canary/frontend-canary.yaml | 0 .../kubernetes/deployments/dev/backend-dev.yaml | 0 .../kubernetes/deployments/dev/default.yml | 0 .../kubernetes/deployments/dev/frontend-dev.yaml | 0 .../kubernetes/deployments/prod/backend-production.yaml | 0 .../kubernetes/deployments/prod/frontend-production.yaml | 0 .../kubernetes/services/backend.yaml | 0 .../kubernetes/services/frontend.yaml | 0 labs/{gke-cloudbuild-progression => gke-progression}/main.go | 0 labs/{gke-cloudbuild-progression => gke-progression}/main_test.go | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename labs/{gke-cloudbuild-progression => gke-progression}/Dockerfile (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/builder/cloudbuild-canary.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/builder/cloudbuild-dev.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/builder/cloudbuild-local.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/builder/cloudbuild-prod.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/builder/cloudbuild.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/html.go (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/deployments/canary/backend-canary.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/deployments/canary/frontend-canary.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/deployments/dev/backend-dev.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/deployments/dev/default.yml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/deployments/dev/frontend-dev.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/deployments/prod/backend-production.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/deployments/prod/frontend-production.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/services/backend.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/kubernetes/services/frontend.yaml (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/main.go (100%) rename labs/{gke-cloudbuild-progression => gke-progression}/main_test.go (100%) diff --git a/labs/gke-cloudbuild-progression/Dockerfile b/labs/gke-progression/Dockerfile similarity index 100% rename from labs/gke-cloudbuild-progression/Dockerfile rename to labs/gke-progression/Dockerfile diff --git a/labs/gke-cloudbuild-progression/builder/cloudbuild-canary.yaml b/labs/gke-progression/builder/cloudbuild-canary.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/builder/cloudbuild-canary.yaml rename to labs/gke-progression/builder/cloudbuild-canary.yaml diff --git a/labs/gke-cloudbuild-progression/builder/cloudbuild-dev.yaml b/labs/gke-progression/builder/cloudbuild-dev.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/builder/cloudbuild-dev.yaml rename to labs/gke-progression/builder/cloudbuild-dev.yaml diff --git a/labs/gke-cloudbuild-progression/builder/cloudbuild-local.yaml b/labs/gke-progression/builder/cloudbuild-local.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/builder/cloudbuild-local.yaml rename to labs/gke-progression/builder/cloudbuild-local.yaml diff --git a/labs/gke-cloudbuild-progression/builder/cloudbuild-prod.yaml b/labs/gke-progression/builder/cloudbuild-prod.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/builder/cloudbuild-prod.yaml rename to labs/gke-progression/builder/cloudbuild-prod.yaml diff --git a/labs/gke-cloudbuild-progression/builder/cloudbuild.yaml b/labs/gke-progression/builder/cloudbuild.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/builder/cloudbuild.yaml rename to labs/gke-progression/builder/cloudbuild.yaml diff --git a/labs/gke-cloudbuild-progression/html.go b/labs/gke-progression/html.go similarity index 100% rename from labs/gke-cloudbuild-progression/html.go rename to labs/gke-progression/html.go diff --git a/labs/gke-cloudbuild-progression/kubernetes/deployments/canary/backend-canary.yaml b/labs/gke-progression/kubernetes/deployments/canary/backend-canary.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/deployments/canary/backend-canary.yaml rename to labs/gke-progression/kubernetes/deployments/canary/backend-canary.yaml diff --git a/labs/gke-cloudbuild-progression/kubernetes/deployments/canary/frontend-canary.yaml b/labs/gke-progression/kubernetes/deployments/canary/frontend-canary.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/deployments/canary/frontend-canary.yaml rename to labs/gke-progression/kubernetes/deployments/canary/frontend-canary.yaml diff --git a/labs/gke-cloudbuild-progression/kubernetes/deployments/dev/backend-dev.yaml b/labs/gke-progression/kubernetes/deployments/dev/backend-dev.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/deployments/dev/backend-dev.yaml rename to labs/gke-progression/kubernetes/deployments/dev/backend-dev.yaml diff --git a/labs/gke-cloudbuild-progression/kubernetes/deployments/dev/default.yml b/labs/gke-progression/kubernetes/deployments/dev/default.yml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/deployments/dev/default.yml rename to labs/gke-progression/kubernetes/deployments/dev/default.yml diff --git a/labs/gke-cloudbuild-progression/kubernetes/deployments/dev/frontend-dev.yaml b/labs/gke-progression/kubernetes/deployments/dev/frontend-dev.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/deployments/dev/frontend-dev.yaml rename to labs/gke-progression/kubernetes/deployments/dev/frontend-dev.yaml diff --git a/labs/gke-cloudbuild-progression/kubernetes/deployments/prod/backend-production.yaml b/labs/gke-progression/kubernetes/deployments/prod/backend-production.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/deployments/prod/backend-production.yaml rename to labs/gke-progression/kubernetes/deployments/prod/backend-production.yaml diff --git a/labs/gke-cloudbuild-progression/kubernetes/deployments/prod/frontend-production.yaml b/labs/gke-progression/kubernetes/deployments/prod/frontend-production.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/deployments/prod/frontend-production.yaml rename to labs/gke-progression/kubernetes/deployments/prod/frontend-production.yaml diff --git a/labs/gke-cloudbuild-progression/kubernetes/services/backend.yaml b/labs/gke-progression/kubernetes/services/backend.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/services/backend.yaml rename to labs/gke-progression/kubernetes/services/backend.yaml diff --git a/labs/gke-cloudbuild-progression/kubernetes/services/frontend.yaml b/labs/gke-progression/kubernetes/services/frontend.yaml similarity index 100% rename from labs/gke-cloudbuild-progression/kubernetes/services/frontend.yaml rename to labs/gke-progression/kubernetes/services/frontend.yaml diff --git a/labs/gke-cloudbuild-progression/main.go b/labs/gke-progression/main.go similarity index 100% rename from labs/gke-cloudbuild-progression/main.go rename to labs/gke-progression/main.go diff --git a/labs/gke-cloudbuild-progression/main_test.go b/labs/gke-progression/main_test.go similarity index 100% rename from labs/gke-cloudbuild-progression/main_test.go rename to labs/gke-progression/main_test.go From cf0f55eb64b1901630514ecc7cb9f72727a99a53 Mon Sep 17 00:00:00 2001 From: Duncan Elliot <42836473+dmelliot@users.noreply.github.com> Date: Wed, 31 Mar 2021 05:27:12 +1100 Subject: [PATCH 04/50] Fix typos (gcme -> gceme) (#2) --- labs/gke-progression/builder/cloudbuild-dev.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/labs/gke-progression/builder/cloudbuild-dev.yaml b/labs/gke-progression/builder/cloudbuild-dev.yaml index 27d4b3f..e57fd0a 100644 --- a/labs/gke-progression/builder/cloudbuild-dev.yaml +++ b/labs/gke-progression/builder/cloudbuild-dev.yaml @@ -22,7 +22,7 @@ steps: args: - '-c' - | - docker build -t gcr.io/$PROJECT_ID/gcme:${BRANCH_NAME}-${SHORT_SHA} . + docker build -t gcr.io/$PROJECT_ID/gceme:${BRANCH_NAME}-${SHORT_SHA} . @@ -36,7 +36,7 @@ steps: args: - '-c' - | - docker push gcr.io/$PROJECT_ID/gcme:${BRANCH_NAME}-${SHORT_SHA} + docker push gcr.io/$PROJECT_ID/gceme:${BRANCH_NAME}-${SHORT_SHA} @@ -63,7 +63,7 @@ steps: - sed -i 's|gcr.io/cloud-solutions-images/gceme:.*|gcr.io/$PROJECT_ID/gcme:${BRANCH_NAME}-${SHORT_SHA}|' ./kubernetes/deployments/dev/*.yaml + sed -i 's|gcr.io/cloud-solutions-images/gceme:.*|gcr.io/$PROJECT_ID/gceme:${BRANCH_NAME}-${SHORT_SHA}|' ./kubernetes/deployments/dev/*.yaml kubectl get ns ${BRANCH_NAME} || kubectl create ns ${BRANCH_NAME} kubectl apply --namespace ${BRANCH_NAME} --recursive -f kubernetes/deployments/dev From 813d066b423f69aeda80b5e9d357b9b0edcc3162 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Tue, 30 Mar 2021 16:11:43 -0500 Subject: [PATCH 05/50] Cloudrun progression (#3) Cloud Run Progression --- labs/cloudrun-progression/Dockerfile | 32 +++ labs/cloudrun-progression/NOTES.md | 241 ++++++++++++++++++ labs/cloudrun-progression/README.md | 19 ++ labs/cloudrun-progression/app.py | 27 ++ .../branch-cloudbuild.yaml | 47 ++++ .../branch-trigger.json-tmpl | 11 + .../master-cloudbuild.yaml | 62 +++++ .../master-trigger.json-tmpl | 11 + labs/cloudrun-progression/tag-cloudbuild.yaml | 41 +++ .../tag-trigger.json-tmpl | 11 + 10 files changed, 502 insertions(+) create mode 100644 labs/cloudrun-progression/Dockerfile create mode 100644 labs/cloudrun-progression/NOTES.md create mode 100644 labs/cloudrun-progression/README.md create mode 100644 labs/cloudrun-progression/app.py create mode 100644 labs/cloudrun-progression/branch-cloudbuild.yaml create mode 100644 labs/cloudrun-progression/branch-trigger.json-tmpl create mode 100644 labs/cloudrun-progression/master-cloudbuild.yaml create mode 100644 labs/cloudrun-progression/master-trigger.json-tmpl create mode 100644 labs/cloudrun-progression/tag-cloudbuild.yaml create mode 100644 labs/cloudrun-progression/tag-trigger.json-tmpl diff --git a/labs/cloudrun-progression/Dockerfile b/labs/cloudrun-progression/Dockerfile new file mode 100644 index 0000000..5344650 --- /dev/null +++ b/labs/cloudrun-progression/Dockerfile @@ -0,0 +1,32 @@ +# Copyright 2021 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. + + +# Use the official lightweight Python image. +# https://hub.docker.com/_/python +FROM python:3.7-slim + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . ./ + +# Install production dependencies. +RUN pip install Flask gunicorn + +# Run the web service on container startup. Here we use the gunicorn +# webserver, with one worker process and 8 threads. +# For environments with multiple CPU cores, increase the number of workers +# to be equal to the cores available. +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app \ No newline at end of file diff --git a/labs/cloudrun-progression/NOTES.md b/labs/cloudrun-progression/NOTES.md new file mode 100644 index 0000000..eb0f64a --- /dev/null +++ b/labs/cloudrun-progression/NOTES.md @@ -0,0 +1,241 @@ +================= +TODO: +- [x] Commands for branches instead of master +- [x] Write deploy yaml & trigger json for branch + - [X] utilize beta run command below + - [X] need to pass in branch name variable for tag + - [ ] output Branch URL in build output +- [X] User: Switch to branch, make a change, push to branch +- User: Wait for build to complete +- [X] Update user command below to get branch name dynamically +- [x] Implement the same for canary and prod +- [x] Add TAG prod to iniitial depoyment +- [x] Updated triggers to use dynamic project ID...currently hard coded +================= + +# Tutorial Flow + +- [X]Preparing your environment +- [X]Creating your CloudRun Service +- [X]Enabling Dynamic Developer Deployments +- [X]Automating Canary Testing +- [X]Releasing to Production + +Working Doc: https://docs.google.com/document/d/1jSqtX7uLpAQD7ZqVdaU62v1Q_yUo3boJvYEalaaNf_8/edit + +CloudRun Proxy: https://github.com/sethvargo/cloud-run-proxy + + +## Preparing your environment + +```shell + +gcloud services enable \ + cloudresourcemanager.googleapis.com \ + container.googleapis.com \ + sourcerepo.googleapis.com \ + cloudbuild.googleapis.com \ + containerregistry.googleapis.com \ + run.googleapis.com + +cd ../ +mkdir workdir && cd workdir + + +export PROJECT_ID=$(gcloud config get-value project) +git config --global user.email "[EMAIL_ADDRESS]" +git config --global user.name "[USERNAME]" + + + +# Clone & remove Git +#git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop +#git clone git@github.com:cgrant/software-delivery-workshop.git -b cloudrun-progression +git clone git@github.com:cgrant/sdw-private.git -b cloudrun-progression cloudrun-progression + +cd cloudrun-progression/labs/cloudrun-progression +rm -rf ../../.git + +sed "s/PROJECT/${PROJECT_ID}/g" branch-trigger.json-tmpl > branch-trigger.json +sed "s/PROJECT/${PROJECT_ID}/g" master-trigger.json-tmpl > master-trigger.json +sed "s/PROJECT/${PROJECT_ID}/g" tag-trigger.json-tmpl > tag-trigger.json + + +git config credential.helper gcloud.sh +gcloud source repos create cloudrun-progression +git remote add gcp https://source.developers.google.com/p/$PROJECT_ID/r/cloudrun-progression +git branch -m master +git init && git add . && git commit -m "initial commit" +git push gcp master + + +gcloud builds submit --tag gcr.io/$PROJECT_ID/hello-cloudrun +gcloud beta run deploy hello-cloudrun \ + --image gcr.io/$PROJECT_ID/hello-cloudrun \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated \ + --tag=prod + +open https://pantheon.corp.google.com/run/detail/us-central1/hello-cloudrun/revisions + + + +PROD_URL=$(gcloud run services describe hello-cloudrun --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"prod\")|.url") +echo $PROD_URL +curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $PROD_URL + +``` + + +## Enable Dynamic Developer Deployments +Trigger on any branch name + +```shell +gcloud beta builds triggers create cloud-source-repositories --trigger-config branch-trigger.json + +open https://pantheon.corp.google.com/cloud-build/triggers + +git checkout -b foo +touch FOOBAR.md +git add . && git commit -m "updated" && git push gcp foo + +open https://pantheon.corp.google.com/cloud-build/builds +open https://pantheon.corp.google.com/run/detail/us-central1/hello-cloudrun/revisions + +#Get the URL of the service +BRANCH_URL=$(gcloud run services describe hello-cloudrun --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"foo\")|.url") +echo $BRANCH_URL +curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $BRANCH_URL + +``` + +## Automate Canary Testing +``` +gcloud beta builds triggers create cloud-source-repositories --trigger-config master-trigger.json + +open https://pantheon.corp.google.com/cloud-build/triggers + +git checkout master +git merge foo +git add . && git commit -m "merge foo" +git push gcp master + +open https://pantheon.corp.google.com/cloud-build/builds +open https://pantheon.corp.google.com/run/detail/us-central1/hello-cloudrun/revisions +``` + + +## Release to Production +``` +gcloud beta builds triggers create cloud-source-repositories --trigger-config tag-trigger.json + +open https://pantheon.corp.google.com/cloud-build/triggers + +git tag 1.0 && git push gcp 1.0 + +open https://pantheon.corp.google.com/cloud-build/builds +open https://pantheon.corp.google.com/run/detail/us-central1/hello-cloudrun/revisions +``` + + + + + + + + + + + + + + + +--- +Git Triggers + +- GITHUB REPO +- Deploying your service with a build trigger (master) +- Creating tokens and configurations + - Create a GitHub token to allow writing back to a pull request +- new file cloudbuild-preview.yaml +- Create Preview Trigger + +My Flow + +- Create a Git repository for your source code +- Deploying your service with a build trigger (tag) +- Canary Deploy from master +- Implement feature branch based dev deployments +- Utilize canary deployments to test production traffic +- Finalize production deployments from git tags + + + +# On FR branch or PR +Build & deploy no traffic +commit service name back to repo + +- Delete Trigger?? + +# On Master +Build & Deploy Preview +tag with git hash + +...todo + + +# On TAG Build Deploy & Test Canary @ 10% +- Deploy Canary + +gcloud beta run deploy hello --image us-docker.pkg.dev/cloudrun/container/hello --platform managed --tag=canary + +gcloud run deploy ${_SERVICE_NAME} \ + --platform managed \ + --region ${_REGION} \ + --allow-unauthenticated \ + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ + --tag=canary,sha$SHORT_SHA \ + --no-traffic + --update-tags=[$SHORT_SHA=$$CANARY] + +- Update Tags +```shell + gcloud beta run services update-traffic hello --update-tags prod=hello-00001-jub + gcloud beta run services update-traffic hello --update-tags canary=hello-00001-jub +``` + + +# Health check + +```shell +# Simple URL +export URL=https://googlffe.com +# Revision URL +export URL=$(gcloud run services describe hello --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"canary\")|.url ") +# Revision URL with path +export URL=$(gcloud run services describe hello --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"canary\")|.url ")/healthz + +# Let the canary take traffic for 5 min +sleep 300 + +# Check Health Status +SECONDS=15 +timeout -s TERM ${SECONDS} bash -c \ + 'while [[ "$(curl -s -o /dev/null -L -w ''%{http_code}'' ${URL})" != "200" ]];\ + do sleep 2;\ + done' ${1} || false +``` + + + +# Canary to live + + + + + +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com" \ + --role="roles/run.admin" \ No newline at end of file diff --git a/labs/cloudrun-progression/README.md b/labs/cloudrun-progression/README.md new file mode 100644 index 0000000..3bc835b --- /dev/null +++ b/labs/cloudrun-progression/README.md @@ -0,0 +1,19 @@ +# Serverless Software Delivery with CloudRun and CloudBuild + +CloudRun provides an easy way to deploy and run your applications with little overhead or effort. Many organizations utilize robust release pipelines for moving code into production. CloudRun provides unique traffic management capabilities allowing you to implement advanced release management techniques with little effort. + + +In this tutorial you'll implement a deployment pipeline for CloudRun that implements progression of code from developer branches to production with automated canary testing and percentage based traffic management. + +## Objectives + +- Create your CloudRun Service +- Enable Dynamic Developer Deployments +- Automate Canary Testing +- Release to Production + +## Preparing your environment +## Creating your CloudRun Service +## Enabling Dynamic Developer Deployments +## Automating Canary Testing +## Releasing to Production \ No newline at end of file diff --git a/labs/cloudrun-progression/app.py b/labs/cloudrun-progression/app.py new file mode 100644 index 0000000..c3cf55d --- /dev/null +++ b/labs/cloudrun-progression/app.py @@ -0,0 +1,27 @@ +#!/usr/bin/python +# +# Copyright 2021 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. +import os + +from flask import Flask + +app = Flask(__name__) + +@app.route('/') +def hello_world(): + return 'Hello World v1.0' + +if __name__ == "__main__": + app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080))) \ No newline at end of file diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml new file mode 100644 index 0000000..0d2046f --- /dev/null +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -0,0 +1,47 @@ +# Copyright 2021 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. + +# Default Values +substitutions: + _SERVICE_NAME: hello-cloudrun + _REGION: us-central1 + +steps: + +### Build + - id: "build image" + name: "gcr.io/cloud-builders/docker" + args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] + +### Push + - id: "push image" + name: "gcr.io/cloud-builders/docker" + args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] + +### Deploy + - id: "deploy prod service" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - '-c' + - | + gcloud run deploy ${_SERVICE_NAME} \ + --platform managed \ + --region ${_REGION} \ + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ + --tag=${BRANCH_NAME} \ + --no-traffic + + + diff --git a/labs/cloudrun-progression/branch-trigger.json-tmpl b/labs/cloudrun-progression/branch-trigger.json-tmpl new file mode 100644 index 0000000..01edb91 --- /dev/null +++ b/labs/cloudrun-progression/branch-trigger.json-tmpl @@ -0,0 +1,11 @@ +{ + "triggerTemplate": { + "projectId": "PROJECT", + "repoName": "cloudrun-progression", + "branchName": "[^(?!.*master)].*" + }, + "name": "branch", + "description": "Trigger dev build/deploy for any branch other than master", + + "filename": "branch-cloudbuild.yaml" +} \ No newline at end of file diff --git a/labs/cloudrun-progression/master-cloudbuild.yaml b/labs/cloudrun-progression/master-cloudbuild.yaml new file mode 100644 index 0000000..7a281cf --- /dev/null +++ b/labs/cloudrun-progression/master-cloudbuild.yaml @@ -0,0 +1,62 @@ +# Copyright 2020 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 +# +# https://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. + +# Default Values +substitutions: + _SERVICE_NAME: hello-cloudrun + _REGION: us-central1 + +steps: + +### Build + - id: "build image" + name: "gcr.io/cloud-builders/docker" + args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] + +### Push + - id: "push image" + name: "gcr.io/cloud-builders/docker" + args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] + +### Deploy + - id: "deploy canary service" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - '-c' + - | + gcloud run deploy ${_SERVICE_NAME} \ + --platform managed \ + --region ${_REGION} \ + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ + --tag=canary \ + --no-traffic + + + # Route Traffic + - id: "route prod traffic" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - '-c' + - | + apt-get install -y jq + export CANARY=$$(gcloud run services describe hello-cloudrun --platform managed --region ${_REGION} --format=json | jq --raw-output ".spec.traffic[] | select (.tag==\"canary\")|.revisionName") + export PROD=$$(gcloud run services describe hello-cloudrun --platform managed --region ${_REGION} --format=json | jq --raw-output ".spec.traffic[] | select (.tag==\"prod\")|.revisionName") + + echo SHORT_SHA is $SHORT_SHA + echo Canary is $${CANARY} + echo gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=sha-$SHORT_SHA=$${CANARY} --platform managed --region ${_REGION} + gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=sha-$SHORT_SHA=$${CANARY} --platform managed --region ${_REGION} + gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=$${PROD}=90,$${CANARY}=10 --platform managed --region ${_REGION} \ No newline at end of file diff --git a/labs/cloudrun-progression/master-trigger.json-tmpl b/labs/cloudrun-progression/master-trigger.json-tmpl new file mode 100644 index 0000000..40580b6 --- /dev/null +++ b/labs/cloudrun-progression/master-trigger.json-tmpl @@ -0,0 +1,11 @@ +{ + "triggerTemplate": { + "projectId": "PROJECT", + "repoName": "cloudrun-progression", + "branchName": "master" + }, + "name": "master", + "description": "Trigger canary build/deploy for any commit to the master branch", + + "filename": "master-cloudbuild.yaml" +} \ No newline at end of file diff --git a/labs/cloudrun-progression/tag-cloudbuild.yaml b/labs/cloudrun-progression/tag-cloudbuild.yaml new file mode 100644 index 0000000..009ca02 --- /dev/null +++ b/labs/cloudrun-progression/tag-cloudbuild.yaml @@ -0,0 +1,41 @@ +# Copyright 2021 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 +# +# https://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. + +# Default Values +substitutions: + _SERVICE_NAME: hello-cloudrun + _REGION: us-central1 + +steps: + + # Route Traffic + - id: "route prod traffic" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - '-c' + - | + apt-get install -y jq + export CANARY=$$(gcloud run services describe hello-cloudrun --platform managed --region ${_REGION} --format=json | jq --raw-output ".spec.traffic[] | select (.tag==\"canary\")|.revisionName") + export PROD=$$(gcloud run services describe hello-cloudrun --platform managed --region ${_REGION} --format=json | jq --raw-output ".spec.traffic[] | select (.tag==\"prod\")|.revisionName") + + + echo gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=prod=$${CANARY} --platform managed --region ${_REGION} + echo gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=$${PROD}=100 --platform managed --region ${_REGION} + + gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=prod=$${CANARY} --platform managed --region ${_REGION} + export NEW_PROD=$$(gcloud run services describe hello-cloudrun --platform managed --region ${_REGION} --format=json | jq --raw-output ".spec.traffic[] | select (.tag==\"prod\")|.revisionName") + gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=$${NEW_PROD}=100 --platform managed --region ${_REGION} + + diff --git a/labs/cloudrun-progression/tag-trigger.json-tmpl b/labs/cloudrun-progression/tag-trigger.json-tmpl new file mode 100644 index 0000000..b791530 --- /dev/null +++ b/labs/cloudrun-progression/tag-trigger.json-tmpl @@ -0,0 +1,11 @@ +{ + "triggerTemplate": { + "projectId": "PROJECT", + "repoName": "cloudrun-progression", + "tagName": ".*" + }, + "name": "tag", + "description": "Migrate from canary to prod triggered by creation of any tag", + + "filename": "tag-cloudbuild.yaml" +} \ No newline at end of file From 87ff3e6844c02781b2ce592fbad2400284d5f3f1 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Tue, 30 Mar 2021 16:38:34 -0500 Subject: [PATCH 06/50] Cloudrun progression --- labs/cloudrun-progression/NOTES.md | 241 ----------------------------- 1 file changed, 241 deletions(-) delete mode 100644 labs/cloudrun-progression/NOTES.md diff --git a/labs/cloudrun-progression/NOTES.md b/labs/cloudrun-progression/NOTES.md deleted file mode 100644 index eb0f64a..0000000 --- a/labs/cloudrun-progression/NOTES.md +++ /dev/null @@ -1,241 +0,0 @@ -================= -TODO: -- [x] Commands for branches instead of master -- [x] Write deploy yaml & trigger json for branch - - [X] utilize beta run command below - - [X] need to pass in branch name variable for tag - - [ ] output Branch URL in build output -- [X] User: Switch to branch, make a change, push to branch -- User: Wait for build to complete -- [X] Update user command below to get branch name dynamically -- [x] Implement the same for canary and prod -- [x] Add TAG prod to iniitial depoyment -- [x] Updated triggers to use dynamic project ID...currently hard coded -================= - -# Tutorial Flow - -- [X]Preparing your environment -- [X]Creating your CloudRun Service -- [X]Enabling Dynamic Developer Deployments -- [X]Automating Canary Testing -- [X]Releasing to Production - -Working Doc: https://docs.google.com/document/d/1jSqtX7uLpAQD7ZqVdaU62v1Q_yUo3boJvYEalaaNf_8/edit - -CloudRun Proxy: https://github.com/sethvargo/cloud-run-proxy - - -## Preparing your environment - -```shell - -gcloud services enable \ - cloudresourcemanager.googleapis.com \ - container.googleapis.com \ - sourcerepo.googleapis.com \ - cloudbuild.googleapis.com \ - containerregistry.googleapis.com \ - run.googleapis.com - -cd ../ -mkdir workdir && cd workdir - - -export PROJECT_ID=$(gcloud config get-value project) -git config --global user.email "[EMAIL_ADDRESS]" -git config --global user.name "[USERNAME]" - - - -# Clone & remove Git -#git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop -#git clone git@github.com:cgrant/software-delivery-workshop.git -b cloudrun-progression -git clone git@github.com:cgrant/sdw-private.git -b cloudrun-progression cloudrun-progression - -cd cloudrun-progression/labs/cloudrun-progression -rm -rf ../../.git - -sed "s/PROJECT/${PROJECT_ID}/g" branch-trigger.json-tmpl > branch-trigger.json -sed "s/PROJECT/${PROJECT_ID}/g" master-trigger.json-tmpl > master-trigger.json -sed "s/PROJECT/${PROJECT_ID}/g" tag-trigger.json-tmpl > tag-trigger.json - - -git config credential.helper gcloud.sh -gcloud source repos create cloudrun-progression -git remote add gcp https://source.developers.google.com/p/$PROJECT_ID/r/cloudrun-progression -git branch -m master -git init && git add . && git commit -m "initial commit" -git push gcp master - - -gcloud builds submit --tag gcr.io/$PROJECT_ID/hello-cloudrun -gcloud beta run deploy hello-cloudrun \ - --image gcr.io/$PROJECT_ID/hello-cloudrun \ - --platform managed \ - --region us-central1 \ - --allow-unauthenticated \ - --tag=prod - -open https://pantheon.corp.google.com/run/detail/us-central1/hello-cloudrun/revisions - - - -PROD_URL=$(gcloud run services describe hello-cloudrun --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"prod\")|.url") -echo $PROD_URL -curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $PROD_URL - -``` - - -## Enable Dynamic Developer Deployments -Trigger on any branch name - -```shell -gcloud beta builds triggers create cloud-source-repositories --trigger-config branch-trigger.json - -open https://pantheon.corp.google.com/cloud-build/triggers - -git checkout -b foo -touch FOOBAR.md -git add . && git commit -m "updated" && git push gcp foo - -open https://pantheon.corp.google.com/cloud-build/builds -open https://pantheon.corp.google.com/run/detail/us-central1/hello-cloudrun/revisions - -#Get the URL of the service -BRANCH_URL=$(gcloud run services describe hello-cloudrun --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"foo\")|.url") -echo $BRANCH_URL -curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $BRANCH_URL - -``` - -## Automate Canary Testing -``` -gcloud beta builds triggers create cloud-source-repositories --trigger-config master-trigger.json - -open https://pantheon.corp.google.com/cloud-build/triggers - -git checkout master -git merge foo -git add . && git commit -m "merge foo" -git push gcp master - -open https://pantheon.corp.google.com/cloud-build/builds -open https://pantheon.corp.google.com/run/detail/us-central1/hello-cloudrun/revisions -``` - - -## Release to Production -``` -gcloud beta builds triggers create cloud-source-repositories --trigger-config tag-trigger.json - -open https://pantheon.corp.google.com/cloud-build/triggers - -git tag 1.0 && git push gcp 1.0 - -open https://pantheon.corp.google.com/cloud-build/builds -open https://pantheon.corp.google.com/run/detail/us-central1/hello-cloudrun/revisions -``` - - - - - - - - - - - - - - - ---- -Git Triggers - -- GITHUB REPO -- Deploying your service with a build trigger (master) -- Creating tokens and configurations - - Create a GitHub token to allow writing back to a pull request -- new file cloudbuild-preview.yaml -- Create Preview Trigger - -My Flow - -- Create a Git repository for your source code -- Deploying your service with a build trigger (tag) -- Canary Deploy from master -- Implement feature branch based dev deployments -- Utilize canary deployments to test production traffic -- Finalize production deployments from git tags - - - -# On FR branch or PR -Build & deploy no traffic -commit service name back to repo - -- Delete Trigger?? - -# On Master -Build & Deploy Preview -tag with git hash - -...todo - - -# On TAG Build Deploy & Test Canary @ 10% -- Deploy Canary - -gcloud beta run deploy hello --image us-docker.pkg.dev/cloudrun/container/hello --platform managed --tag=canary - -gcloud run deploy ${_SERVICE_NAME} \ - --platform managed \ - --region ${_REGION} \ - --allow-unauthenticated \ - --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ - --tag=canary,sha$SHORT_SHA \ - --no-traffic - --update-tags=[$SHORT_SHA=$$CANARY] - -- Update Tags -```shell - gcloud beta run services update-traffic hello --update-tags prod=hello-00001-jub - gcloud beta run services update-traffic hello --update-tags canary=hello-00001-jub -``` - - -# Health check - -```shell -# Simple URL -export URL=https://googlffe.com -# Revision URL -export URL=$(gcloud run services describe hello --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"canary\")|.url ") -# Revision URL with path -export URL=$(gcloud run services describe hello --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"canary\")|.url ")/healthz - -# Let the canary take traffic for 5 min -sleep 300 - -# Check Health Status -SECONDS=15 -timeout -s TERM ${SECONDS} bash -c \ - 'while [[ "$(curl -s -o /dev/null -L -w ''%{http_code}'' ${URL})" != "200" ]];\ - do sleep 2;\ - done' ${1} || false -``` - - - -# Canary to live - - - - - -gcloud projects add-iam-policy-binding $PROJECT_ID \ - --member="serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com" \ - --role="roles/run.admin" \ No newline at end of file From 31622796c706f21adab1743411ceb276d70b18eb Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Wed, 30 Jun 2021 15:07:12 -0500 Subject: [PATCH 07/50] Software Delivery Workshop --- README.md | 371 +--------------- delivery-platform/docs/demo/1-Provision.md | 16 + delivery-platform/docs/demo/2-Demo.md | 155 +++++++ delivery-platform/docs/images/platform.png | Bin 0 -> 225336 bytes .../docs/workshop/1.2-provision.md | 187 ++++++++ .../docs/workshop/1.3-kustomize.md | 403 ++++++++++++++++++ .../docs/workshop/2.1-app-onboarding.md | 121 ++++++ .../docs/workshop/2.2-develop.md | 180 ++++++++ .../docs/workshop/3.2-release-progression.md | 170 ++++++++ delivery-platform/env.sh | 73 ++++ .../resources/provision/base_image/Dockerfile | 59 +++ .../resources/provision/base_image/README.md | 8 + .../resources/provision/clusters/tf/README.md | 14 + .../provision/clusters/tf/backend.tmpl | 22 + .../clusters/tf/cloudbuild-destroy.yaml | 33 ++ .../provision/clusters/tf/cloudbuild.yaml | 34 ++ .../provision/clusters/tf/clusters.tf | 83 ++++ .../tf/modules/platform-cluster/main.tf | 88 ++++ .../tf/modules/platform-cluster/outputs.tf | 39 ++ .../tf/modules/platform-cluster/variables.tf | 78 ++++ .../provision/clusters/tf/outputs.tf | 38 ++ .../provision/clusters/tf/terraform.tmpl | 17 + .../provision/clusters/tf/variables.tf | 21 + .../provision/clusters/tf/versions.tf | 19 + .../provision/foundation/tf/README.md | 14 + .../provision/foundation/tf/backend.tmpl | 22 + .../foundation/tf/cloudbuild-destroy.yaml | 31 ++ .../provision/foundation/tf/cloudbuild.yaml | 41 ++ .../provision/foundation/tf/networks.tf | 74 ++++ .../provision/foundation/tf/services.tf | 36 ++ .../provision/foundation/tf/terraform.tmpl | 17 + .../provision/foundation/tf/variables.tf | 19 + .../provision/foundation/tf/versions.tf | 19 + .../management-tools/acm/acm-install.sh | 19 + .../provision/management-tools/acm/acm.tf | 58 +++ .../management-tools/acm/backend.tmpl | 22 + .../acm/cloudbuild-destroy.yaml | 32 ++ .../management-tools/acm/cloudbuild.yaml | 47 ++ .../provision/management-tools/acm/outputs.tf | 19 + .../management-tools/acm/remote_state.tmpl | 24 ++ .../management-tools/acm/terraform.tmpl | 23 + .../management-tools/acm/variables.tf | 39 ++ .../management-tools/argo-install.sh | 58 +++ .../management-tools/gitea/gt-setup.sh | 52 +++ .../management-tools/tekton-install.sh | 19 + .../resources/provision/provision-all.sh | 98 +++++ .../provision/repos/create-config-repo.sh | 29 ++ .../provision/repos/create-template-repos.sh | 40 ++ .../resources/provision/repos/teardown.sh | 20 + .../resources/provision/teardown-all.sh | 39 ++ delivery-platform/resources/repos/README.md | 7 + .../resources/repos/app-templates/.gitignore | 1 + .../repos/app-templates/golang/Dockerfile | 19 + .../repos/app-templates/golang/README.md | 1 + .../app-templates/golang/cloudbuild.yaml | 154 +++++++ .../repos/app-templates/golang/go.mod | 3 + .../golang/k8s/dev/deployment.yaml | 34 ++ .../golang/k8s/dev/kustomization.yaml | 22 + .../golang/k8s/prod/deployment.yaml | 34 ++ .../golang/k8s/prod/kustomization.yaml | 22 + .../golang/k8s/stage/deployment.yaml | 35 ++ .../golang/k8s/stage/kustomization.yaml | 21 + .../repos/app-templates/golang/main.go | 40 ++ .../repos/app-templates/golang/skaffold.yaml | 41 ++ .../repos/app-templates/java/.gitignore | 17 + .../repos/app-templates/java/.gitlab-ci.yml | 33 ++ .../repos/app-templates/java/Dockerfile | 18 + .../repos/app-templates/java/README.md | 20 + .../java/k8s/dev/deployment.yaml | 27 ++ .../java/k8s/dev/kustomization.yaml | 22 + .../java/k8s/prod/deployment.yaml | 27 ++ .../java/k8s/prod/kustomization.yaml | 22 + .../java/k8s/stg/deployment.yaml | 28 ++ .../java/k8s/stg/kustomization.yaml | 21 + .../repos/app-templates/java/pom.xml | 85 ++++ .../repos/app-templates/java/skaffold.yaml | 43 ++ .../simple/SuperSimpleAppApplication.java | 27 ++ .../example/simple/web/HelloController.java | 35 ++ .../src/main/resources/application.properties | 3 + .../SuperSimpleAppApplicationTests.java | 27 ++ .../simple/web/HelloControllerTest.java | 39 ++ .../resources/repos/cluster-config/.gitignore | 1 + .../repos/cluster-config/dev/system/repo.yaml | 20 + .../cluster-config/prod/system/repo.yaml | 20 + .../cluster-config/stage/system/repo.yaml | 20 + .../repos/shared-kustomize/.gitignore | 1 + .../shared-kustomize/golang/deployment.yaml | 48 +++ .../golang/kustomization.yaml | 17 + .../shared-kustomize/golang/service.yaml | 25 ++ .../shared-kustomize/java/deployment.yaml | 51 +++ .../shared-kustomize/java/kustomization.yaml | 17 + .../repos/shared-kustomize/java/service.yaml | 25 ++ delivery-platform/scripts/app.sh | 234 ++++++++++ delivery-platform/scripts/common/clearvars.sh | 22 + .../scripts/common/manage-state.sh | 35 ++ .../scripts/common/set-apikey-var.sh | 13 + delivery-platform/scripts/deliver.sh | 41 ++ delivery-platform/scripts/git/gcsr.sh | 35 ++ delivery-platform/scripts/git/gh.sh | 79 ++++ delivery-platform/scripts/git/git-ask-pass.sh | 18 + delivery-platform/scripts/git/gl.sh | 52 +++ delivery-platform/scripts/git/set-git-env.sh | 95 +++++ delivery-platform/scripts/hydrate.sh | 58 +++ labs/cloudrun-progression/README.md | 19 - labs/gke-progression/README.md | 30 ++ 105 files changed, 4669 insertions(+), 375 deletions(-) create mode 100644 delivery-platform/docs/demo/1-Provision.md create mode 100644 delivery-platform/docs/demo/2-Demo.md create mode 100644 delivery-platform/docs/images/platform.png create mode 100644 delivery-platform/docs/workshop/1.2-provision.md create mode 100644 delivery-platform/docs/workshop/1.3-kustomize.md create mode 100644 delivery-platform/docs/workshop/2.1-app-onboarding.md create mode 100644 delivery-platform/docs/workshop/2.2-develop.md create mode 100644 delivery-platform/docs/workshop/3.2-release-progression.md create mode 100755 delivery-platform/env.sh create mode 100644 delivery-platform/resources/provision/base_image/Dockerfile create mode 100644 delivery-platform/resources/provision/base_image/README.md create mode 100644 delivery-platform/resources/provision/clusters/tf/README.md create mode 100644 delivery-platform/resources/provision/clusters/tf/backend.tmpl create mode 100644 delivery-platform/resources/provision/clusters/tf/cloudbuild-destroy.yaml create mode 100644 delivery-platform/resources/provision/clusters/tf/cloudbuild.yaml create mode 100644 delivery-platform/resources/provision/clusters/tf/clusters.tf create mode 100644 delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/main.tf create mode 100644 delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/outputs.tf create mode 100644 delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/variables.tf create mode 100644 delivery-platform/resources/provision/clusters/tf/outputs.tf create mode 100644 delivery-platform/resources/provision/clusters/tf/terraform.tmpl create mode 100644 delivery-platform/resources/provision/clusters/tf/variables.tf create mode 100644 delivery-platform/resources/provision/clusters/tf/versions.tf create mode 100644 delivery-platform/resources/provision/foundation/tf/README.md create mode 100644 delivery-platform/resources/provision/foundation/tf/backend.tmpl create mode 100644 delivery-platform/resources/provision/foundation/tf/cloudbuild-destroy.yaml create mode 100644 delivery-platform/resources/provision/foundation/tf/cloudbuild.yaml create mode 100644 delivery-platform/resources/provision/foundation/tf/networks.tf create mode 100644 delivery-platform/resources/provision/foundation/tf/services.tf create mode 100644 delivery-platform/resources/provision/foundation/tf/terraform.tmpl create mode 100644 delivery-platform/resources/provision/foundation/tf/variables.tf create mode 100644 delivery-platform/resources/provision/foundation/tf/versions.tf create mode 100755 delivery-platform/resources/provision/management-tools/acm/acm-install.sh create mode 100644 delivery-platform/resources/provision/management-tools/acm/acm.tf create mode 100644 delivery-platform/resources/provision/management-tools/acm/backend.tmpl create mode 100644 delivery-platform/resources/provision/management-tools/acm/cloudbuild-destroy.yaml create mode 100644 delivery-platform/resources/provision/management-tools/acm/cloudbuild.yaml create mode 100644 delivery-platform/resources/provision/management-tools/acm/outputs.tf create mode 100644 delivery-platform/resources/provision/management-tools/acm/remote_state.tmpl create mode 100644 delivery-platform/resources/provision/management-tools/acm/terraform.tmpl create mode 100644 delivery-platform/resources/provision/management-tools/acm/variables.tf create mode 100755 delivery-platform/resources/provision/management-tools/argo-install.sh create mode 100644 delivery-platform/resources/provision/management-tools/gitea/gt-setup.sh create mode 100755 delivery-platform/resources/provision/management-tools/tekton-install.sh create mode 100755 delivery-platform/resources/provision/provision-all.sh create mode 100755 delivery-platform/resources/provision/repos/create-config-repo.sh create mode 100755 delivery-platform/resources/provision/repos/create-template-repos.sh create mode 100755 delivery-platform/resources/provision/repos/teardown.sh create mode 100755 delivery-platform/resources/provision/teardown-all.sh create mode 100644 delivery-platform/resources/repos/README.md create mode 100644 delivery-platform/resources/repos/app-templates/.gitignore create mode 100644 delivery-platform/resources/repos/app-templates/golang/Dockerfile create mode 100644 delivery-platform/resources/repos/app-templates/golang/README.md create mode 100644 delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/go.mod create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/dev/deployment.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/prod/deployment.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/stage/deployment.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/main.go create mode 100644 delivery-platform/resources/repos/app-templates/golang/skaffold.yaml create mode 100644 delivery-platform/resources/repos/app-templates/java/.gitignore create mode 100644 delivery-platform/resources/repos/app-templates/java/.gitlab-ci.yml create mode 100644 delivery-platform/resources/repos/app-templates/java/Dockerfile create mode 100644 delivery-platform/resources/repos/app-templates/java/README.md create mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/dev/deployment.yaml create mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/dev/kustomization.yaml create mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/prod/deployment.yaml create mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/prod/kustomization.yaml create mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/stg/deployment.yaml create mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/stg/kustomization.yaml create mode 100644 delivery-platform/resources/repos/app-templates/java/pom.xml create mode 100644 delivery-platform/resources/repos/app-templates/java/skaffold.yaml create mode 100644 delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/SuperSimpleAppApplication.java create mode 100644 delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/web/HelloController.java create mode 100644 delivery-platform/resources/repos/app-templates/java/src/main/resources/application.properties create mode 100644 delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/SuperSimpleAppApplicationTests.java create mode 100644 delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/web/HelloControllerTest.java create mode 100644 delivery-platform/resources/repos/cluster-config/.gitignore create mode 100644 delivery-platform/resources/repos/cluster-config/dev/system/repo.yaml create mode 100644 delivery-platform/resources/repos/cluster-config/prod/system/repo.yaml create mode 100644 delivery-platform/resources/repos/cluster-config/stage/system/repo.yaml create mode 100644 delivery-platform/resources/repos/shared-kustomize/.gitignore create mode 100644 delivery-platform/resources/repos/shared-kustomize/golang/deployment.yaml create mode 100644 delivery-platform/resources/repos/shared-kustomize/golang/kustomization.yaml create mode 100644 delivery-platform/resources/repos/shared-kustomize/golang/service.yaml create mode 100644 delivery-platform/resources/repos/shared-kustomize/java/deployment.yaml create mode 100644 delivery-platform/resources/repos/shared-kustomize/java/kustomization.yaml create mode 100644 delivery-platform/resources/repos/shared-kustomize/java/service.yaml create mode 100755 delivery-platform/scripts/app.sh create mode 100755 delivery-platform/scripts/common/clearvars.sh create mode 100644 delivery-platform/scripts/common/manage-state.sh create mode 100755 delivery-platform/scripts/common/set-apikey-var.sh create mode 100755 delivery-platform/scripts/deliver.sh create mode 100755 delivery-platform/scripts/git/gcsr.sh create mode 100755 delivery-platform/scripts/git/gh.sh create mode 100755 delivery-platform/scripts/git/git-ask-pass.sh create mode 100755 delivery-platform/scripts/git/gl.sh create mode 100755 delivery-platform/scripts/git/set-git-env.sh create mode 100755 delivery-platform/scripts/hydrate.sh delete mode 100644 labs/cloudrun-progression/README.md create mode 100644 labs/gke-progression/README.md diff --git a/README.md b/README.md index 69670b8..15cc067 100644 --- a/README.md +++ b/README.md @@ -1,371 +1,30 @@ -# GKE Deployments with Cloud Build +# Software Delivery Workshop -The included scripts are intended to demonstrate how to use Google Cloud Build as a continuous integration system deploying code to GKE. This is not an official Google product. -The example here follows a pattern where: -- developers use cloud servers during local development -- all lower lifecycle testing occurs on branches other than master -- merges to master indicate a readiness for a canary (beta) deployment in production -- a tagged release indicates the canary deploy is fully signed off and rolled out to all remaining servers +This repository contains resources and materials targeted toward Software Delivery on Google Cloud. In addition to separate stand alone guides, an opinionated yet modular platform is provided to demonstrate software delivery practices. In contains scripts to standup a base platform infrastructure as well as other resources designed to facilitate hands on workshop and standard demo use cases. The platform provisioning resources are structured to be modular in nature supporting various runtime and tooling configurations. Ideally users can utilize their own choice of tooling for: Provisioning, Source Code Management, Templating, Build Engine, Image Storage and Deploy tooling. -There are 5 scripts included as part of the demo: -- cloudbuild-local.yaml - used by developers to compile and push local code to cloud servers -- cloudbuild-dev.yaml - used to deploy any branch to branch namespaces -- cloudbuild-canary.yaml - used to deploy the master branch to canary servers -- cloudbuild-prod.yaml - used to push repo tags to remaining production servers -- cloudbuild.yaml - an all in one script that can be used for branches, master and tags with one configuration +## Usage +This set of resources contains materials to provision the platform, deliver short demonstrations and facilitate hands on workshops. -This lab shows you how to setup a continuous delivery pipeline for GKE using Google Cloud Build. We’ll run through the following steps +### Workshop +The Software Delivery Workshop contains materials for a self led exploration or accompanying instructor led sessions. To get started with either click the button below to open the resources in Google Cloud Shell. -- Create a GKE Cluster -- Review the application structure -- Manually deploy the application -- Create a repository for our source -- Setup automated triggers in Cloud Build -- Automatically deploy Branches to custom namespaces -- Automatically deploy Master as a canary -- Automatically deploy Tags to production +[![Software Delivery Workshop](http://www.gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/GoogleCloudPlatform/software-delivery-workshop.git&cloudshell_workspace=.&cloudshell_tutorial=delivery-platform/docs/workshop/1.2-provision.md) +### Demo +For a mostly automated experience follow the instructions in the `docs\demo` folder. You will run an automated script to fully provision the platform before your demonstration. A separate guide describes the steps to perform during the demonstration and concludes with instructions how to reset the demo or tear down the infrastructure. +### Provision +If you just want to install the base platform and run your own exercises and workloads, run the following commands from within the `delivery-platform` directory. -## Set Variables - -``` - - export PROJECT=[[YOUR PROJECT NAME]] - # On Cloudshell - # export PROJECT=$(gcloud info --format='value(config.project)') - export CLUSTER=gke-deploy-cluster - export ZONE=us-central1-a - - gcloud config set compute/zone $ZONE - -``` -## Enable Services -``` -gcloud services enable container.googleapis.com --async -gcloud services enable containerregistry.googleapis.com --async -gcloud services enable cloudbuild.googleapis.com --async -gcloud services enable sourcerepo.googleapis.com --async -``` -## Create Cluster - -``` - - gcloud container clusters create ${CLUSTER} \ - --project=${PROJECT} \ - --zone=${ZONE} \ - --quiet - -``` - - -## Get Credentials - -``` - gcloud container clusters get-credentials ${CLUSTER} \ - --project=${PROJECT} \ - --zone=${ZONE} -``` - -## Give Cloud Build Rights - -For `kubectl` commands against GKE youll need to give Cloud Build Service Account container.developer role access on your clusters [details](https://github.com/GoogleCloudPlatform/cloud-builders/tree/master/kubectl). - -``` -PROJECT_NUMBER="$(gcloud projects describe \ - $(gcloud config get-value core/project -q) --format='get(projectNumber)')" - -gcloud projects add-iam-policy-binding ${PROJECT} \ - --member=serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com \ - --role=roles/container.developer - -``` - -## Deploy the application manually - +```shell +gcloud config set project +source ./env.sh +${BASE_DIR}/resources/provision/provision-all.sh ``` -kubectl create ns production -kubectl apply -f kubernetes/deployments/prod -n production -kubectl apply -f kubernetes/deployments/canary -n production -kubectl apply -f kubernetes/services -n production - -kubectl scale deployment gceme-frontend-production -n production --replicas 4 - -kubectl get pods -n production -l app=gceme -l role=frontend -kubectl get pods -n production -l app=gceme -l role=backend - -kubectl get service gceme-frontend -n production - -export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend) - -curl http://$FRONTEND_SERVICE_IP/version - -``` - - -## Create a repo for the code - -``` -gcloud alpha source repos create default -git init -git config credential.helper gcloud.sh -git remote add gcp https://source.developers.google.com/p/[PROJECT]/r/default -git add . -git commit -m "Initial Commit" -git push gcp master - -``` - -## Setup triggers -Ensure you have credentials available -``` -gcloud auth application-default login - -``` -**Branches** -``` - -cat < branch-build-trigger.json -{ - "triggerTemplate": { - "projectId": "${PROJECT}", - "repoName": "default", - "branchName": "[^(?!.*master)].*" - }, - "description": "branch", - "substitutions": { - "_CLOUDSDK_COMPUTE_ZONE": "${ZONE}", - "_CLOUDSDK_CONTAINER_CLUSTER": "${CLUSTER}" - }, - "filename": "builder/cloudbuild-dev.yaml" -} -EOF - - -curl -X POST \ - https://cloudbuild.googleapis.com/v1/projects/${PROJECT}/triggers \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \ - --data-binary @branch-build-trigger.json -``` - -**Master** -``` - -cat < master-build-trigger.json -{ - "triggerTemplate": { - "projectId": "${PROJECT}", - "repoName": "default", - "branchName": "master" - }, - "description": "master", - "substitutions": { - "_CLOUDSDK_COMPUTE_ZONE": "${ZONE}", - "_CLOUDSDK_CONTAINER_CLUSTER": "${CLUSTER}" - }, - "filename": "builder/cloudbuild-canary.yaml" -} -EOF - - -curl -X POST \ - https://cloudbuild.googleapis.com/v1/projects/${PROJECT}/triggers \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \ - --data-binary @master-build-trigger.json -``` - -**Tag** -``` - -cat < tag-build-trigger.json -{ - "triggerTemplate": { - "projectId": "${PROJECT}", - "repoName": "default", - "tagName": ".*" - }, - "description": "tag", - "substitutions": { - "_CLOUDSDK_COMPUTE_ZONE": "${ZONE}", - "_CLOUDSDK_CONTAINER_CLUSTER": "${CLUSTER}" - }, - "filename": "builder/cloudbuild-prod.yaml" -} -EOF - - -curl -X POST \ - https://cloudbuild.googleapis.com/v1/projects/${PROJECT}/triggers \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \ - --data-binary @tag-build-trigger.json -``` - -Review triggers are setup on the [Build Triggers Page](https://console.cloud.google.com/gcr/triggers) - - - - -### Build & Deploy of local content (optional) - -The following submits a build to Cloud Build and deploys the results to a user's namespace. - -``` -gcloud container builds submit \ - --config builder/cloudbuild-local.yaml \ - --substitutions=_VERSION=someversion,_USER=$(whoami),_CLOUDSDK_COMPUTE_ZONE=${ZONE},_CLOUDSDK_CONTAINER_CLUSTER=${CLUSTER} . - - -``` - - -## Deploy Branches to Namespaces - -Development branches are a set of environments your developers use to test their code changes before submitting them for integration into the live site. These environments are scaled-down versions of your application, but need to be deployed using the same mechanisms as the live environment. - -### Create a development branch - -To create a development environment from a feature branch, you can push the branch to the Git server and let Cloud Build deploy your environment. - -Create a development branch and push it to the Git server. - -``` -git checkout -b new-feature -``` - - -### Modify the site - -In order to demonstrate changing the application, you will be change the gceme cards from blue to orange. - -**Step 1** -Open html.go and replace the two instances of blue with orange. - -**Step 2** -Open main.go and change the version number from 1.0.0 to 2.0.0. The version is defined in this line: - -const version string = "2.0.0" - -### Kick off deployment - -**Step 1** - -Commit and push your changes. This will kick off a build of your development environment. - -``` -git add html.go main.go - -git commit -m "Version 2.0.0" - -git push gcp new-feature -``` - -**Step 2** - -After the change is pushed to the Git repository, navigate to the [Build History Page](https://console.cloud.google.com/cloud-build/builds) user interface where you can see that your build started for the new-feature branch - -Click into the build to review the details of the job - -**Step 3** - -Once that completes, verify that your application is accessible. You should see it respond with 2.0.0, which is the version that is now running. - -Retrieve the external IP for the production services. - -It can take several minutes before you see the load balancer external IP address. - -``` -kubectl get service gceme-frontend -n new-feature -``` - -Once an External-IP is provided store it for later use - -``` -export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=new-feature services gceme-frontend) - -curl http://$FRONTEND_SERVICE_IP/version - -``` - - ->Congratulations! You've setup a pipeline and deployed code to GKE with Cloud Build. - - -The rest of this example follows the same pattern but demonstrates the triggers for Master and Tags. - -## Deploy Master to canary - -Now that you have verified that your app is running your latest code in the development environment, deploy that code to the canary environment. - -**Step 1** -Create a canary branch and push it to the Git server. - -``` -git checkout master - -git merge new-feature - -git push gcp master -``` - -Again after you’ve pushed to the Git repository, navigate to the [Build History Page](https://console.cloud.google.com/gcr/builds) user interface where you can see that your build started for the master branch - -Click into the build to review the details of the job - -**Step 2** - -Once complete, you can check the service URL to ensure that some of the traffic is being served by your new version. You should see about 1 in 5 requests returning version 2.0.0. - -``` -export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend) - -while true; do curl http://$FRONTEND_SERVICE_IP/version; sleep 1; done -``` - -You can stop this command by pressing `Ctrl-C`. - ->Congratulations! -> ->You have deployed a canary release. Next you will deploy the new version to production by creating a tag. - - -## Deploy Tags to production - -Now that your canary release was successful and you haven't heard any customer complaints, you can deploy to the rest of your production fleet. - -**Step 1** -Merge the canary branch and push it to the Git server. - -``` -git tag v2.0.0 - -git push gcp v2.0.0 -``` - -Review the job on the the [Build History Page](https://console.cloud.google.com/gcr/builds) user interface where you can see that your build started for the v2.0.0 tag - -Click into the build to review the details of the job - -**Step 2** -Once complete, you can check the service URL to ensure that all of the traffic is being served by your new version, 2.0.0. You can also navigate to the site using your browser to see your orange cards. - -``` -export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend) - -while true; do curl http://$FRONTEND_SERVICE_IP/version; sleep 1; done -``` - -You can stop this command by pressing `Ctrl-C`. - ->Congratulations! -> ->You have successfully deployed your application to production! diff --git a/delivery-platform/docs/demo/1-Provision.md b/delivery-platform/docs/demo/1-Provision.md new file mode 100644 index 0000000..b6e0653 --- /dev/null +++ b/delivery-platform/docs/demo/1-Provision.md @@ -0,0 +1,16 @@ + +# Provision Platform Infrastructure + +```shell +gcloud config set project +source ./env.sh +${BASE_DIR}/resources/provision/provision-all.sh +``` + +# Cleanup + +```shell +gcloud config set project +source ./env.sh +${BASE_DIR}/resources/provision/teardown-all.sh +``` \ No newline at end of file diff --git a/delivery-platform/docs/demo/2-Demo.md b/delivery-platform/docs/demo/2-Demo.md new file mode 100644 index 0000000..b6a795f --- /dev/null +++ b/delivery-platform/docs/demo/2-Demo.md @@ -0,0 +1,155 @@ + +# Delivery Platform Demo + + +## Prerequisites + +From the `delivery-platform` directory run the following commands + +```shell +gcloud config set project +source ./env.sh +export APP_NAME="hello-web" +export TARGET_ENV=dev + +``` + + +### Create a new app + +This process covers the app onboarding process. In this case it includes copying a template repo to a new app repo, enables policy management for the app in ACM. + +This step: +- Copies code from a sample repo +- Updates the name and unique references +- Creates a new repo for the app team +- Adds entries into ACM for dev, stage, prod +- Performs initial code push and deploys to environments + +```shell +./scripts/app.sh create ${APP_NAME} golang +``` + + +### Checkout new app to make changes. + + +Clone Repos & open editor +``` + +git clone -b main $GIT_BASE_URL/${APP_NAME} $WORK_DIR/${APP_NAME} +git clone -b main $GIT_BASE_URL/${SHARED_KUSTOMIZE_REPO} $WORK_DIR/kustomize-base + +cd $WORK_DIR/${APP_NAME} +cloudshell workspace $WORK_DIR/${APP_NAME} +``` + +## Debug on GKE in Cloud Code + +In the editor use ctrl/cmd+shift+p to open the command palet and type `Cloud Code: Run on GKE' +Choose the default profile +Choose/type the `dev` cluster for deployment (any will do) +Accept the default image registry `gcr.io/{project}` +Switch to the output tab to watch progress +Click the URL when complete to see the web page + +Make a change to line 31 of main.go +Save and warch the build / deploy complete +Check the web page again + +This process can be done locally with minikube instead of GKE as well simply by switching the cluster used + + +## Deploy to stage + +### Review the initial state of stage + +open a tunnel +``` +kubectx stage \ + && kubectl port-forward --namespace hello-web $(kubectl get pod --namespace hello-web --selector="app=hello-web,role=backend" --output jsonpath='{.items[0].metadata.name}') 8080:8080 +``` + +Review the web page +use the web page preview in the top right of the browser just to the left of your profile. select preview on port 8080 + +Exit the tunnel +use `ctrl+c` to exit the tunnel + + +### Commit the code to `main` to trigger the deploy to `stage` + +``` +git add . +git commit -m "Updating to V2" +git push origin main +``` + +review the job progress in the [the build history page](https://console.cloud.google.com/cloud-build/builds) + + +### Review the changes +open a tunnel +``` +kubectx stage \ + && kubectl port-forward --namespace hello-web $(kubectl get pod --namespace hello-web --selector="app=hello-web,role=backend" --output jsonpath='{.items[0].metadata.name}') 8080:8080 +``` + +Review the web page +use the web page preview in the top right of the browser just to the left of your profile. select preview on port 8080 + +Exit the tunnel +use `ctrl+c` to exit the tunnel + + +## Release code to prod +Create a release by executing the following command + +```bash +git tag v2 +git push origin v2 +``` +Again review the latest job progress in the [the build history page](https://console.cloud.google.com/cloud-build/builds) + +When complete review the page live by creating your tunnel + + +```bash +kubectx prod \ + && kubectl port-forward --namespace hello-web $(kubectl get pod --namespace hello-web --selector="app=hello-web,role=backend" --output jsonpath='{.items[0].metadata.name}') 8080:8080 + ``` + +And again utilizing the web preview in the top right + +When you're done use `ctrl+c` in the terminal to exit out of the tunnel + + + +## Reset + +To reset the demo and start over delete the application with the following commands + +``` +cd $BASE_DIR +cloudshell workspace . +rm -rf $WORK_DIR/hello-web +./scripts/app.sh delete ${APP_NAME} + +``` + +## Example repo reset +The provision process creates new repos in your git provider for the workshop. In order to use the lastest versions you will need to pull the latest to this local directory then recreate the remote repos using the following commands + + +``` +./resources/provision/repos/teardown.sh +./resources/provision/repos/create-config-repo.sh +./resources/provision/repos/create-template-repos.sh +``` + +## Complete Teardown +Run the following comand to delete all the infrastructure + +``` +./resources/provision/teardown-all.sh +``` diff --git a/delivery-platform/docs/images/platform.png b/delivery-platform/docs/images/platform.png new file mode 100644 index 0000000000000000000000000000000000000000..8d72c90a665b0577cf141173c46e3d319cb6406c GIT binary patch literal 225336 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ȶ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Π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 aEpOrs=|RS46D@aupDXJ6YWT}HL;eqCGg2b} literal 0 HcmV?d00001 diff --git a/delivery-platform/docs/workshop/1.2-provision.md b/delivery-platform/docs/workshop/1.2-provision.md new file mode 100644 index 0000000..016cf03 --- /dev/null +++ b/delivery-platform/docs/workshop/1.2-provision.md @@ -0,0 +1,187 @@ +# +![](https://crg-sdw-imgs.web.app/images/sdw-title.svg?) + +In this lab you will execute initial setup steps to configure your workspace for use with this workshop. You will also review key Terraform files for use in your own customizations in the future. Finally, you will initiate the provision process to create the software delivery platform used in this workshop. + +## Objectives + +- Setup Workspace +- Review overall folder structure +- Review Terraform components (Remote state, Networking, Clusters) +- Review the Cloud Build job for platform provisioning +- Submit the job to provision platform + +Click **Start** to proceed. + +## Select a project + +Choose a project for this tutorial. The project will contain the services and tools to build and deploy applications and the GKE clusters that will act as your development, staging, and production environments. + +**_We recommend you create a new project for this tutorial. You may experience undesired side effects if you use an existing project with conflicting settings._** + + + +Change into the `delivery-platform` directory + +```bash +cd delivery-platform +``` + +Click **Next** to proceed. + +## Submit the job to provision platform + +In this step you will run a command to begin provisioning the platform used in this workshop. + +All of the steps have been tied together for you in a single script. When customizing your own platform you can utilize only the pieces and sequences that apply in the context of your platform. + +While the script is running you will review the details of the scripts and resources. + +First, take a moment to review the complete provision script. Note how each section is broken out for easy execution and customization. + +Review the provision-all.sh file + +### Provision +Execute the provision script + + ```bash +gcloud config set project {{project-id}} +source ./env.sh +${BASE_DIR}/resources/provision/provision-all.sh + ``` + +Click **Next** to proceed. + +## Review overall folder structure + +This repository provides a variety of resources for use both in a workshop setting as well as during your own exploration and customization. The majority of key resources are located in the `delivery-platform` folder. + +![](https://crg-sdw-imgs.web.app/images/folder-structure.png) + +### Docs + +The `delivery-platform/docs` folder contains instructions for demos and workshops. The file you're reading right now is located here as well. You can view it by opening delivery-platform/docs/workshop/1.2-provision.md. + +### Scripts + +The `delivery-platform/scripts` folder contains a few key scripts you'll review in later sections. In practice, you'll incorporate scripts like delivery-platform/scripts/hydrate.sh into your own software delivery pipelines. + +### Workdir + +The `delivery-platform/workdir` folder is used during the workshop as a temporary workspace to house various folders and files either managed by the tools or modified by you in the lessons. One example is your environment state file delivery-platform/workdir/state.env that contains various variables, including your clear text GitHub token. This entire folder is ignored by git and can be deleted safely at the completion of your workshop. + +### Resources + +The final directory of note is the `delivery-platform/resources` folder. This directory contains sample repositories used throughout the lessons as well as a series of Terraform scripts used to provision the platform. + +Click **Next** to proceed. + +## +![](https://crg-sdw-imgs.web.app/images/provisioning.png) + +In this lab you'll provision the underlying infrastructure needed to run the workshop. A software delivery platform consists of four main components: Foundation, Clusters, Tools and Applications. + +### Foundation + +The foundation includes all of the base GCP resources needed for the software delivery platform in general. In practice, this may include enabling APIs, setting up IAM resources and VPC networks, among others things. In this example, you're creating the underlying networking used throughout the workshop. + + +Click here to review the network configuration + + +This file can be adjusted to meet your organizational needs. + +### Remote State +The Terraform scripts in this workshop utilize remote state management to ensure the state is persisted between users and workspaces. If you were to lose your Cloudshell instance or chose to access the scripts from another device, remote state management will allow you to interact with your Terraform environment correctly. + +This state is provided by defining a backend of type Google Cloud Storage (GCS) as seen in `/foundation/tf/backend.tmpl` + + +Click here to view the code. + + +The values in this file are replaced with your actual project name and the file is renamed to `backend.tf` automatically in a later step. + +Click **Next** to proceed. + +## Clusters + +Four clusters are created as part of this workshop: `dev`, `stage`, `prod` and `mgmt`. The `prod` cluster will act as a production resource and the `stage` cluster will as a pre-production resource. Applications are deployed to production and staging through your delivery pipeline. The `dev` cluster is used by the application teams for application development and testing. The `mgmt` cluster is used to run the various management, build and deployment tools used by your delivery process. + +![](https://crg-sdw-imgs.web.app/images/clusters.png) + +To configure the clusters, you can modify the `clusters/tf/clusters.tf` file. +Click here to review the current configuration + + +Click **Next** to proceed. + +## Sample Repositories + +A series of sample repositories are provided for use in the workshop. These provide a consistent base configuration for the clusters and the applications that will be deployed on the platform. These repositories and concepts will be covered in more detail in later labs. + +In this step of the provisioning process, the scripts will create and push new repos in your git provider for each of these sample repos. From then on, the workshop will use the repos located in your git provider. + +These sample repositories can also be used to speed up the customization of your own platform. + + +Review the folders in the `resources/repos` directory + + +Click **Next** to proceed. + +## Platform Tools + +The final step of the platform provisioning process installs a series of tools you may use throughout the workshop or in your own customization. The scripts install Anthos Config Manager, ArgoCD, Tekton, and Gitea. This particular workshop only utilizes Anthos Config Manager. + +- Review acm-install.sh + +- Review tekton-install.sh + +- Review argo-install.sh + +- Review gt-setup.sh + +Click **Next** to proceed. + +## Review the Cloud Build job for platform provisioning + +This workshop utilizes Cloud Build to manage the execution of the Terraform used to provision the platform. Executing these steps with Cloud Build allows you to include the exact set of tools and utilities needed for your provisioning process, not having to depend on the user to install tools locally. Additionally, running in this fully managed runtime ensures that any issues with local connectivity or session state won't impact the execution of the job. + +### cloudbuild.yaml +To submit workloads to Cloud Build, a `cloudbuild.yaml` file is submitted through the gcloud CLI + + +Review the key lines of the foundation/tf/cloudbuild.yaml + + +## Congratulations !! + +You've reached the end of this lab! + +At this point you'll probably still have tabs in your editor open from reviewing the various files. Go ahead and close out of those files. + +Also your provisioning process may not be complete yet. Go ahead and open a new cloud shell terminal to conitune with the next few labs. + + + +Once the shell opens runthe following commands to return to the workshop state. + +```bash +gcloud config set project {{project-id}} +cd cloudshell_open/software-delivery-workshop/delivery-platform/ +source ./env.sh + +``` + +Next your instructor will discuss the concepts related to the next section + +After the lecture, run the following command to launch the next lab. + +```bash +teachme "${BASE_DIR}/docs/workshop/1.3-kustomize.md" +``` diff --git a/delivery-platform/docs/workshop/1.3-kustomize.md b/delivery-platform/docs/workshop/1.3-kustomize.md new file mode 100644 index 0000000..2ec7ea5 --- /dev/null +++ b/delivery-platform/docs/workshop/1.3-kustomize.md @@ -0,0 +1,403 @@ + + +# Kustomize + +In this lab you will work through some of the core concepts of Kustomize and learn how it can be used to help manage variations in applications and environments in your software delivery platform. + + +## Objectives + +- Combine a base yaml with an overlay +- Use common types: Images, Namespaces, Labels +- Apply multiple overlays to create an application + + +## Prerequisites + +This lab assumes you have already cloned the main repository and are starting in the `delivery-platform/` sub folder. + +Execute the following commands to set your project and local environment variables + + +```bash +gcloud config set project {{project-id}} +source ./env.sh +``` + + +To demonstrate the kustomize features, you will work out of a temporary directory specifically for this lab. + +Begin by changing to the work directory and creating the lab folder + + +```bash +mkdir $WORK_DIR/kustomize-lab +cd $WORK_DIR/kustomize-lab +cloudshell workspace . +``` + + +Click **Next** to proceed. + + +## Kustomize Basics + +One of the core features of Kustomize is its ability to overlay multiple file configurations. This allows you to manage a base set of resources and add overlays to configure the specific variations you may see between apps and environments. + +Rather than calling `kubectl apply` directly on your k8s manifests you would first run `kustomize build` to render or “hydrate” your manifests with the variations you’ve included from kustomize. + +The `kustomize` command looks for a file called `kustomization.yaml` as the primary configuration. + +To start, you will create a folder to hold your base configuration files + + +```bash +mkdir -p chat-app/base +``` + + +Create a simple `deployment.yaml` in the base folder + + +```yaml +cat < chat-app/base/deployment.yaml +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + metadata: + name: chat-app + spec: + containers: + - name: chat-app + image: chat-app-image +EOF +``` + + +Now create a `kustomization.yaml` file that references the `deployment.yaml` as the base resources where the variations will be applied. + +Create the base `kustomization.yaml` + + +```yaml +cat < chat-app/base/kustomization.yaml +bases: + - deployment.yaml +EOF +``` + + +Running kustomize command on the base folder outputs the deployment yaml with no changes, which is expected since you haven’t included any variations yet. + + +```bash +kustomize build chat-app/base +``` + + +Click **Next** to proceed. + + +## Common Overlays: Images, Namespaces, Labels + +Images, namespaces and labels are very commonly customized for each application and environment. Since they are commonly changed, Kustomize lets you declare them directly in the `kustomize.yaml`, eliminating the need to create many patches for these common scenarios. + +This technique is often used to create a specific instance of a template. One base set of resources can now be used for multiple implementations by simply changing the name and it’s namespace. + +In this example, you will add a namespace, name prefix and add some labels to your `kustomization.yaml`. + +Update the `kustomization.yaml` file to include common labels and namespaces. + + +```yaml +cat < chat-app/base/kustomization.yaml +bases: + - deployment.yaml + +namespace: my-namespace +nameprefix: my- +commonLabels: + app: my-app + +EOF +``` + + +Executing the build at this point shows that the resulting yaml now contains the namespace, labels and prefixed names in both the service and deployment definitions. + + +```bash +kustomize build chat-app/base +``` + + +Note how the output contains labels and namespaces that are not in the deployment yaml. Note also how the name was changed from `chat-app` to `my-chat-app` + +(Output do not copy) + + +

+kind: Deployment
+metadata:
+  labels:
+    app: my-app
+  name: my-chat-app
+  namespace: my-namespace
+
+ + +Click **Next** to proceed. + + +## Patches and Overlays + +Kustomize also provides the ability to apply patches that overlay the base resources. This technique is often used to provide variability between applications, and environments. + +In this step you will create environment variations for a single application that use the same base resources. + +Start by creating folders for the different environments + + +```bash +mkdir -p chat-app/dev +mkdir -p chat-app/prod +``` + + +Notice that the patches below do not contain the container image name. That value is provided in the base/deployment.yaml you created in the previous step. These patches do however contain unique environment variables for dev and prod. + +Write the patches with the following command + + +```yaml +cat < chat-app/dev/deployment.yaml +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: chat-app + env: + - name: ENVIRONMENT + value: dev +EOF +``` + + +Now do the same for prod + + +```yaml +cat < chat-app/prod/deployment.yaml +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: chat-app + env: + - name: ENVIRONMENT + value: prod +EOF +``` + + +Now implement the kustomize yamls for each directory. + +First rewrite the base customization.yaml removing the namespace and name prefix as this is just the base config with no variation. Those fields will be moved to the environment files in just a moment. + +Rewrite the `base/kustomization.yaml `file + + +```yaml +cat < chat-app/base/kustomization.yaml +bases: + - deployment.yaml + +commonLabels: + app: chat-app + +EOF +``` + + +Now implement the variations for dev and prod. Note the addition now of the `patches:` section of the file. This indicates that kustomize should overlay those files on top of the base resources. + +Create the `dev/kustomization.yaml `file + + +```yaml +cat < chat-app/dev/kustomization.yaml +bases: +- ../base + +namespace: dev +nameprefix: dev- +commonLabels: + env: dev + + +patches: +- deployment.yaml +EOF +``` + + +Now create the `prod/kustomization.yaml `file + + +```yaml +cat < chat-app/prod/kustomization.yaml +bases: +- ../base + +namespace: prod +nameprefix: prod- +commonLabels: + env: prod + +patches: +- deployment.yaml +EOF +``` + + +With the base and environment files created, you can execute the kustomize process to patch the base files. + +Run the following command to see the merged result. + + +```bash +kustomize build chat-app/dev +``` + + +Note the output contains merged results such as labels from base and dev configurations as well as the container image name from the base and the environment variable from the dev folders. + +Click **Next** to proceed. + + +## Multiple layers of overlays + +Many organizations have a team that helps support the app teams and manage the platform. Frequently these teams will want to include specific details that are to be included in all apps across all environments such as a logging agent. + +In this example you will create a `shared-kustomize` folder and resources which will be included in all applications regardless of which environment they’re deployed. + +Start by creating the folder + + +```bash +mkdir shared-kustomize +``` + + +Create a simple `deployment.yaml` in the base folder + + +```yaml +cat < shared-kustomize/deployment.yaml +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: logging-agent + image: logging-agent-image +EOF +``` + + +Now create a `kustomization.yaml` file that references the `deployment.yaml` as the base resources where the variations will be applied. + +Create the base `kustomization.yaml` + + +```yaml +cat < shared-kustomize/kustomization.yaml +bases: + - deployment.yaml +EOF +``` + + +Since you want the` shared-kustomize `folder to be the base for all your applications you will need to update your `chat-app/base/kustomization.yaml` to use `shared-kustomize` as the base then patch its own deployment.yaml on top. The environment folders will then patch again on top of that. + + +```yaml +cat < chat-app/base/kustomization.yaml +bases: + - ../../shared-kustomize + +commonLabels: + app: chat-app + +patches: +- deployment.yaml + +EOF +``` + + +Run the following command to see the merged result. + + +```bash +kustomize build chat-app/dev +``` + + +Note the output contains merged results from the app base, the app environment as well as the shared-kustomize folders. Specifically you can see in the containers section values from all three locations. + +(output do not copy) + + +
+containers:
+      - env:
+        - name: ENVIRONMENT
+          value: dev
+        name: chat-app
+      - image: image
+        name: app
+      - image: logging-agent-image
+        name: logging-agent
+
+ + +Click **Next** to proceed. + + +## Congratulations !! + +You've reached the end of this lab. + +Close any editor tabs you still have open and change back to the main lab directory with the following commands. + + +```bash +cd $BASE_DIR +cloudshell workspace . +``` + + +Next your instructor will discuss the concepts related to the next section + +After the lecture, run the following command to launch the next lab. + + +```bash +teachme "${BASE_DIR}/docs/workshop/2.1-app-onboarding.md" +``` + diff --git a/delivery-platform/docs/workshop/2.1-app-onboarding.md b/delivery-platform/docs/workshop/2.1-app-onboarding.md new file mode 100644 index 0000000..28bfe76 --- /dev/null +++ b/delivery-platform/docs/workshop/2.1-app-onboarding.md @@ -0,0 +1,121 @@ +# App Onboarding + +Creating a new application often includes more than loading a lanugage's example hello world app and start coding. + +In reality the process invariably includes at least a minimal set of core tasks such as creating a new app repository for the developers, pulling in a template from an approved list of base applications, setting up foundational build and deploy mechanics, and various other ancilary elements like registering with enterprise change management systems. + +Mature organizations implement automation processes that enable developer self serve, and minimize platform administrator engagement. + +This lab examines patterns for automating the creation of a new application and reviews an automation script that can be modified and extended to meet your custom needs. + +## Objectives + +- Review Base Repos +- Review app-create script +- Create and review new application + + +Click **Start** to begin the lab. + +## Prerequisites + +This lab assumes you have already cloned the main repository and are starting in the `delivery-platform/` folder. + +This lab does not require infrastructure to be fully provisioned. + + + + + +Execute the following command to set your project and local environment variables + +```bash +gcloud config set project {{project-id}} +source ./env.sh + +``` + +Click **Next** to continue. + + +## Base Repositories + +The app onboarding process included in this workshop performs a few key functions, one of which is to create a new repository from a template. + +As part of the provisioning process starter repos from this workshop were copied to your remote git provider for use throughout the lessons. This lab will utilize these repos in the onboarding process. +Review the local version of the app templates repo in the `resources/repos/app-templates` directory + + +Click here locate the folder + +Take note of the folder names used for each of the template types. These names are used to indicate which template should be used for the new application. + +Next to the `app-templates` folder is a folder called `shared-kustomize`. This directory contains base kustomize overlays that will be merged with the app configs as part of the hydration step in the pipeline. Review the contents of that folder and note that the sub folder names match those of the app-template directory. + +To customize or add additional templates simply add the appropriate folders to both of these directories. + +The third folder in this directory named `cluster-config` is a sample implementation of a repo used by Anthos Config Manager which you'll use later in this lab. This repo will contain the full rendered manifests to be applied to the various clusters. + +## App Creation Script + +The script utilized in this workshop was created to not only facilitate app onboarding within this workshop but also to make the patterns visible and customizable. + +While you can utilize the example script as is, it's assumed your organization will have different onboarding needs and thus it will need to be modified. + +Rather than provide an locked down opinionated binary this script is written as an accessible bash script to inform and inspire your own implementaion. + + +Review the `scripts/app.sh` file + +The usage of this script is simply `app.sh create new_app_name template_to_use`. The create function performs the following steps: + +- Clones down templates repo +- Modify template place holders with actual values (ie the actual app name) +- Creates new remote repo in your git provider to hold the app instance +- Configures deployment targets for the various environments +- Configures base software delivery pipeline + +Variations and customizations you may choose to implement within your organization might include registering the app in a CMDB, updating ingress or load balancer entries, incorporating a user interface for developer self service etc. + + + +## Create & Review new application + +In this step you will create an new instance of an app using the provided app creation script that will be used throughout the remainder of the workshop. + +First set some variables including the name of the app to be created. + +```bash +export APP_NAME=hello-web +export TARGET_ENV=dev +``` + + +Next execute the app create function to instantiate the app + +```bash +./scripts/app.sh create ${APP_NAME} golang +``` + +Review the app source repo in your remote git provider + +Review the namespace addition in the cluster-config repo on your remote git provider + +## Review your new app + +- Review your new app in you git provider +- Review the entry in the Config-repo +- Review the webhook in the app repo +- Review the trigger in cloud build + +## Congratulations !! + +You've reached the end of this lab + + +After the next lecture, run the following command to launch the next lab. + +```bash +teachme "${BASE_DIR}/docs/workshop/2.2-develop.md" +``` + diff --git a/delivery-platform/docs/workshop/2.2-develop.md b/delivery-platform/docs/workshop/2.2-develop.md new file mode 100644 index 0000000..95f6f5d --- /dev/null +++ b/delivery-platform/docs/workshop/2.2-develop.md @@ -0,0 +1,180 @@ + +# App Development + +In this lab you will walk through a typical developer workflow use-case that highlights local development integrations that are available within Cloud Code. + + +## Objectives + + +* Introduce Cloud Code Plugins +* Introduce skaffold for pipeline abstraction +* Utilize local and remote clusters +* Utilize hot reloading +* Debug apps on remote gke realtime + + +## Prerequisites + +This lab assumes you have already cloned the main repository and are starting in the `delivery-platform/` folder. + +Next select the project you are using for the lab. + +<walkthrough-project-setup></walkthrough-project-setup> + +Execute the following command to set your project and local environment variables. + + + + +```bash +gcloud config set project {{project-id}} +source ./env.sh +export APP_NAME=hello-web +``` + + + +## Clone the repositories + +In a previous lab you created a new application for use by the development team. In practice a member of the dev team would trigger the script through a UI or other process. Once the app is created the developer and the rest of their team would need to clone the remote repo first before being able to make changes. + +In practice some organizations may choose to include kustomize overlays through HTTP references rather than local references to the files. In this workshop the kustomize overlays are included as local files for clarity and discussion so you'll be cloning that repo as well. + +Clone the application and kustomize repositories. + + +```bash +git clone -b main $GIT_BASE_URL/${APP_NAME} $WORK_DIR/${APP_NAME} +git clone -b main $GIT_BASE_URL/${SHARED_KUSTOMIZE_REPO} $WORK_DIR/kustomize-base +``` + + +Switch your editor's workspace to the application directory. + + +```bash +cd $WORK_DIR/${APP_NAME} +cloudshell workspace $WORK_DIR/${APP_NAME} +``` + + +## Develop with Cloud Code + +Cloud Code reduces many of the tedious repetitive steps a developer typically needs to execute to develop applications for containers based runtimes. + +Cloud Code features can be accessed through the command palette by typing `Cmd+Shift+P` on Mac or `Ctrl+Shift+P` on Windows or ChromeOS, then typing `Cloud Code` to filter down to Cloud Code commands. + +Alternatively many of the most commonly used commands are available by clicking on the Cloud Code indicator in the status bar below the editor. + +Finally there are use-case specific icons on the left side of the navigator that take you directly to sections like the API or GKE Explorer. + + + +Click here to highlight the GKE explorer + + + + +## Local Development Loop + +During development it’s useful to work with your application against a local kubernetes cluster like minikube. In this section you’ll use the Cloud Code plugin to deploy your application to a local instance of minikube, then hot reload changes made in the code. + +Start minikube + +In Cloud Shell IDE, click the word `minikube` in the status bar. In the prompt at the top of the screen click on the `minikube` option then click. `start` + +**Wait for `minikube` to finish starting**. It takes 1-3 minutes. + +Be sure you’re using the minikube context by setting it on the config in your terminal. + +```bash +kubectl config use-context minikube +``` + +### Run on Minikube + +Once minikube is running, build and deploy the application with Cloud Code. Locate the Run on K8s command in your command pallet by: + +1. Using the hotkey combination `cmd/ctrl+shift+p` +1. Type “`Cloud Code: Run on kubernetes`” and select the option +1. Select `Kubernetes: Run/Debug - local` and confirm you want to use the current context (minikube) to run the app. + +Once the deploy is complete, review the app by clicking on the URL provided in the output window. + +Change this line in the main.go file to a different output. When you save the file, notice the build and deploy automatically begin to redeploy the application to the cluster. Once completed return to the tab with the application deployed and refresh the window to see the results updated. + +To stop the hot deploy process find the stop button in the debug configuration. + + + +Locate the debug configuration pane + + + +Stopping the process not only detaches the process but also cleans up all the resources used on the cluster. + + + + +## Remote Debugging + +Cloud Code also simplifies the process of developing for kubernetes by integrating live debugging of applications running in kubernetes clusters. For this section you will deploy your application to a remote dev cluster and perform some simple live debugging. + +Cloud Code can utilize any Kubernetes cluster in your local contexts. For this example you'll utilize the remote dev cluster. + +Cloud Code understands remote vs local deployment patterns. When you used minikube earlier, Cloud Code defaulted to using your local Docker build and store the image locally. Since this is a remote deployment the system will prompt you for a remote registry. + +Under the hood Cloud Code is using skaffold to build and deploy. For the remote clusters you’ll be prompted for which profile to use. The workshop is designed to use the [default] profile for local development. + +These prompts will occur only when there are no existing configurations found. To set or change configurations switch to the debug view then select the settings icon next to the launch configurations dropdown. + +First start by switching to the dev cluster context with the command below + + +```bash +kubectl config use-context dev +``` + + +This time you’ll choose `Cloud Code: Debug on Kubernetes` from the command pallet. + +1. Using the hotkey combination `cmd/ctrl+shift+p` +1. Type “`Cloud Code: Debug on Kubernetes`” and select the option +1. Select `Kubernetes: Run/Debug - dev` and confirm you want to use current context (dev) to run the app. +1. If asked which image repo to use, choose the default value of `gcr.io/{project}` +1. If asked which cluster to use, choose to use the current `dev` context. + +To watch the progress be sure you’ve selected the Output window. The editor may have switched to debug view. + +Once the build and deploy completes, the output will provide a URL for viewing the deployed application. + +Click the URL provided to see the application results. + +Stopping the process by clicking the stop button in the debug console + + +## Return to the main workspace + +When you're done modifying the application and reviewing the changes, return to the main workshop workspace by executing the following command. + +Switch your editor's workspace to the application directory to prepare for the next lab. + + +```bash +cd $BASE_DIR +cloudshell workspace $BASE_DIR/.. +``` + + +## Congratulations !! + +You've reached the end of this lab. + +After the next lecture, run the following command to launch the next lab. + + +```bash +teachme "${BASE_DIR}/docs/workshop/3.2-release-progression.md" +``` + diff --git a/delivery-platform/docs/workshop/3.2-release-progression.md b/delivery-platform/docs/workshop/3.2-release-progression.md new file mode 100644 index 0000000..b0b8ac5 --- /dev/null +++ b/delivery-platform/docs/workshop/3.2-release-progression.md @@ -0,0 +1,170 @@ +# Release Progression w/ Git Event + +In this lab you will implement a release progression process that utilizes git events as the primary workflow. Using git to manage the process allows organizations to move many of the stage gates and checks towards the development team. While this practice is a core concept in the popular gitops practices, git-based workflows can be used with traditional imperative as well as gitops oriented declarative processes. + +## Objectives +- Review lifecycle variations +- Create commit & tag triggers +- Initiate a release +- Approve a release + + + +Click **Start** to begin the lab. + +## Prerequisites + +This lab assumes you have already cloned the main repository and are starting in the `delivery-platform/` folder. + +This lab also assumes you are continuing from the previous lab and should still have the hello-web app in your workdir. + + +### Setup your environment + + + +Execute the following command to set your project and local environment variables + +```bash +gcloud config set project {{project-id}} +source ./env.sh +export APP_NAME=hello-web +``` + + +## CI/CD Models and Workflows + +### Source control models +There are many models for how exactly to structure your branches and releases. This lab follows a model similar to how Google manages code, but the techniques can be applied to any model. + +At Google, software engineers create a branch for small updates to the code base. These changes are then submitted for validation and review as a ChangeList (CL). The CL process runs automated tests as well as requiring approval by code owners. Once approved, the code is automatically integrated into the main code base, and is a candidate to be deployed in the next release. Change management for a production release is handled through a separate process, but once approved the code is pulled from the main line. + +To approximate this model in Git, this lab assumes the following model: + +* Tags represent production releases. +* The Main branch represents code ready for production, or "staged" for production. +* Any development efforts are handled on branches other than Main. + +The PR process in most Git providers allows you to implement your own review processes. Many providers allow you to restrict which users are allowed to perform actions on the various branches and tags, which can be used for separation of concerns. + +### Git events for workflow +A central part of any CI/CD system utilizes git events as the trigger for continuous processes. + +This lab is currently triggered by events from pushes to branches as well as the creation of new tags. + +Click **Next** to begin reviewing the implementation. + +## Review what Onboarding setup + +Earlier, you created an application using the scripts provided on this platform. In that process, a few key elements were set up for your application to enable the workflow in this lab. + +### Webhooks +Whenever events occur within your provider, the events can trigger a request to an external endpoint called a webhook. A webhook was created and configured for your `hello-web` application during the onboard process. You can review it in the settings section of your repository. + +Execute the following command to generate a link to that location in your project. + +```bash +echo $GIT_BASE_URL/$APP_NAME/settings/hooks +``` +Copy the value and paste into a new browser tab to review the webhook on your application repo. + +### Triggers + +The webhook is configured to call an endpoint with details about the event that occurred. The onboarding script created an endpoint in Google Cloud to accept these webhook requests. + +Review the trigger setup for your application in Cloud Build with the link below + +[https://console.cloud.google.com/cloud-build/triggers/edit/hello-web-webhook-trigger](https://console.cloud.google.com/cloud-build/triggers/edit/hello-web-webhook-trigger) + + + +### Workflow Configuration + +The webhook endpoint triggers a Cloud Build job that executes a workflow defined in a `cloudbuild.yaml` file. The cloud build implementation includes steps to clone the repo, build and push an image, hydrate resources, and finally deploy the assets to the appropriate environment. + +Review the `cloudbuild.yaml` in your `hello-web` project + +Click here to open cloudbuild.yaml + + +### View Initial Deployment +During the onboarding process, the workflow was exercised for the first time to create an initial deployment for your application. You can see your application running from the GKE Workloads page: + +[Cick here to view the deployment](https://console.cloud.google.com/kubernetes/workload) + +Now since these services are not exposed publicly, you'll need to create a tunnel to the cluster to view the web page. + +Execute the following command to create the stage tunnel + +```bash +kubectx stage \ + && kubectl port-forward --namespace hello-web $(kubectl get pod --namespace hello-web --selector="app=hello-web,role=backend" --output jsonpath='{.items[0].metadata.name}') 8080:8080 + ``` + +With multiple clusters configured, you could use `kubectl` to switch context. In this lab, we use `kubectx`, a command available in Cloud Shell to help you manage and switch context. + +When that's' running, you can select the web preview in the top right of the screen, just to the left of your profile icon. Selecting "Preview on port 8080" will open a view of the app in the cluster. + +Exit out of the tunnel by typing `ctrl+c` in the terminal on your keyboard + +## Deploy code to Stage clusters + +Change the output of your hello-web application to say "Hello World - V2" + + +Changed this line + + + +Commit the code to the main branch by executing the following command + +```bash +cd $WORK_DIR/hello-web +git add . +git commit -m "Updating to V2" +git push origin main +``` + +Review the Cloud Build trigger in progress by cicking into the latest job on [the build history page](https://console.cloud.google.com/cloud-build/builds) + +When that completes you can review the updated change by opening your tunnel + +```bash +kubectx stage \ + && kubectl port-forward --namespace hello-web $(kubectl get pod --namespace hello-web --selector="app=hello-web,role=backend" --output jsonpath='{.items[0].metadata.name}') 8080:8080 + ``` + +And again utilizing the web preview in the top right + +When you're done use `ctrl+c` in the terminal to exit out of the tunnel + +## Release code to prod + +Releases to production are also triggered through git events. In this case, the creation of a tag is the triggering event instead of code being pushed to a branch. + +Create a release by executing the following command + +```bash +git tag v2 +git push origin v2 +``` +Again review the latest job progress in the [the build history page](https://console.cloud.google.com/cloud-build/builds) + +When complete review the page live by creating your tunnel + + +```bash +kubectx prod \ + && kubectl port-forward --namespace hello-web $(kubectl get pod --namespace hello-web --selector="app=hello-web,role=backend" --output jsonpath='{.items[0].metadata.name}') 8080:8080 + ``` + +And again utilizing the web preview in the top right + +When you're done use `ctrl+c` in the terminal to exit out of the tunnel + + +## Congratulations!!! + +You've reached the end of the lab! \ No newline at end of file diff --git a/delivery-platform/env.sh b/delivery-platform/env.sh new file mode 100755 index 0000000..3e0bc4b --- /dev/null +++ b/delivery-platform/env.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +## Set Environment Variables +export PROJECT_ID=$(gcloud config get-value project) +export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') + + +# Set Base directory & Working directory variables +export BASE_DIR=$PWD +export WORK_DIR=$BASE_DIR/workdir +export SCRIPTS=$BASE_DIR/scripts +mkdir -p $WORK_DIR/bin + +export PATH=$PATH:$WORK_DIR/bin:$SCRIPTS: + +# Load any persisted variables +source $SCRIPTS/common/manage-state.sh +load_state + +# Git details +git config --global user.email $(gcloud config get-value account) +git config --global user.name ${USER} +source $SCRIPTS/git/set-git-env.sh + +source $SCRIPTS/common/set-apikey-var.sh + + +# Set the image repo to use +# if [[ ${IMAGE_REPO} == "" ]]; then +# printf "Enter your image repo location eg: gcr.io/" && read imagerepo +# export IMAGE_REPO=${imagerepo} +# fi + + +# Platform Config +export ACM_IN_USE=True +# TODO: PROVISION_TOOL=tf +# TODO: CONFIG_TOOL=acm +# TODO: BUILD_TOOL=cloudbuild +# TODO: IMAGE_REPO=gcr +# TODO: DEPLOY_TOOL=argo + +# Repo Names +export REPO_PREFIX=mcd +export APP_TEMPLATES_REPO=$REPO_PREFIX-app-templates +export SHARED_KUSTOMIZE_REPO=$REPO_PREFIX-shared_kustomize +export CLUSTER_CONFIG_REPO=$REPO_PREFIX-cluster-config +export HYDRATED_CONFIG_REPO=${CLUSTER_CONFIG_REPO} + +# Repository Name +export IMAGE_REPO=gcr.io/${PROJECT_ID} + +# variable pass through for access tokens +export GIT_ASKPASS=$SCRIPTS/git/git-ask-pass.sh + + +# Persist variables for later use +write_state \ No newline at end of file diff --git a/delivery-platform/resources/provision/base_image/Dockerfile b/delivery-platform/resources/provision/base_image/Dockerfile new file mode 100644 index 0000000..a482067 --- /dev/null +++ b/delivery-platform/resources/provision/base_image/Dockerfile @@ -0,0 +1,59 @@ +# Copyright 2021 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. + +FROM ubuntu:18.04 +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y apt-utils wget gnupg2 unzip git jq \ + apt-transport-https ca-certificates \ + dnsutils curl gettext + +ENV TERRAFORM_VERSION=0.13.5 +ENV HELM_VERSION=2.14.3 +ENV KUBECTL_VERSION=1.16.1 +ENV GO_VERSION=1.14.2 +ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DO_NOT_WARN + +# Install terraform +RUN wget -q https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \ + unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \ + chmod +x terraform && \ + mv terraform /usr/local/bin && \ + rm -rf terraform_${TERRAFORM_VERSION}_linux_amd64.zip + +# Install helm +RUN wget -q https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \ + tar zxfv helm-v${HELM_VERSION}-linux-amd64.tar.gz && \ + mv linux-amd64/helm /usr/local/bin && \ + rm -rf linux-amd64 helm-v${HELM_VERSION}-linux-amd64.tar.gz + +# Install kubectl +RUN wget -q https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \ + chmod +x kubectl && \ + mv kubectl /usr/local/bin/ + +# Install gcloud +RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \ + wget -q -O- https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - && \ + apt-get update && \ + apt-get install -y google-cloud-sdk + +# Install anthos-platform-cli +# COPY cli anthos-platform-cli +# RUN wget -q https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz && \ +# tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz && \ +# PATH=$PATH:/usr/local/go/bin && \ +# cd anthos-platform-cli && \ +# go build && \ +# cp anthos-platform-cli /usr/local/bin diff --git a/delivery-platform/resources/provision/base_image/README.md b/delivery-platform/resources/provision/base_image/README.md new file mode 100644 index 0000000..c7c0065 --- /dev/null +++ b/delivery-platform/resources/provision/base_image/README.md @@ -0,0 +1,8 @@ + +Create the base imaged used throughout the provisioning processes + +``` + +gcloud builds submit --tag gcr.io/$PROJECT_ID/delivery-platform-installer + +``` \ No newline at end of file diff --git a/delivery-platform/resources/provision/clusters/tf/README.md b/delivery-platform/resources/provision/clusters/tf/README.md new file mode 100644 index 0000000..33e83d7 --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/README.md @@ -0,0 +1,14 @@ + + +Create the foundation +- Requires `base_image` and `foundation` to be created first + +``` +gcloud builds submit +``` + +Destroy the foundation + +``` +gcloud builds submit --config cloudbuild-destroy.yaml +``` \ No newline at end of file diff --git a/delivery-platform/resources/provision/clusters/tf/backend.tmpl b/delivery-platform/resources/provision/clusters/tf/backend.tmpl new file mode 100644 index 0000000..e0f4837 --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/backend.tmpl @@ -0,0 +1,22 @@ +/** + * Copyright 2021 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. + */ + +terraform { + backend "gcs" { + bucket = "YOUR_PROJECT_ID-delivery-platform-tf-state" + prefix = "clusters" + } +} diff --git a/delivery-platform/resources/provision/clusters/tf/cloudbuild-destroy.yaml b/delivery-platform/resources/provision/clusters/tf/cloudbuild-destroy.yaml new file mode 100644 index 0000000..5816115 --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/cloudbuild-destroy.yaml @@ -0,0 +1,33 @@ +# Copyright 2021 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. + +timeout: 3600s # 1-hour +tags: + - delivery-platform + - delivery-platform-clusters + +steps: +- name: 'gcr.io/${PROJECT_ID}/delivery-platform-installer' + id: 'destroy-clusters' + entrypoint: 'bash' + args: + - '-xe' + - '-c' + - | + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" terraform.tmpl > terraform.tfvars + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" backend.tmpl > backend.tf + + terraform init + terraform destroy -auto-approve + diff --git a/delivery-platform/resources/provision/clusters/tf/cloudbuild.yaml b/delivery-platform/resources/provision/clusters/tf/cloudbuild.yaml new file mode 100644 index 0000000..e0fea85 --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/cloudbuild.yaml @@ -0,0 +1,34 @@ +# Copyright 2021 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. + +timeout: 3600s # 1-hr +tags: + - delivery-platform + - delivery-platform-clusters +steps: +- name: 'gcr.io/${PROJECT_ID}/delivery-platform-installer' + id: 'create-clusters' + entrypoint: 'bash' + args: + - '-xe' + - '-c' + - | + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" terraform.tmpl > terraform.tfvars + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" backend.tmpl > backend.tf + + #export TF_LOG="ERROR" + + terraform init + terraform plan -out=terraform.tfplan + terraform apply -auto-approve terraform.tfplan diff --git a/delivery-platform/resources/provision/clusters/tf/clusters.tf b/delivery-platform/resources/provision/clusters/tf/clusters.tf new file mode 100644 index 0000000..d7dc707 --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/clusters.tf @@ -0,0 +1,83 @@ +/** + * Copyright 2021 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 { + cluster_type = "regional" +} + +provider "google" { + project = var.project_id + version = "~> 3.44.0" +} + +data "google_compute_network" "delivery-platform" { + name = "delivery-platform" +} + +data "google_compute_subnetwork" "delivery-platform-west1" { + name = "delivery-platform-west1" + region = "us-west1" +} + +data "google_compute_subnetwork" "delivery-platform-west2" { + name = "delivery-platform-west2" + region = "us-west2" +} + +data "google_compute_subnetwork" "delivery-platform-central1" { + name = "delivery-platform-central1" + region = "us-central1" +} + +module "delivery-platform-dev" { + source = "./modules/platform-cluster" + project_id = var.project_id + name = "dev" + region = "us-west1" + network = data.google_compute_network.delivery-platform.name + subnetwork = data.google_compute_subnetwork.delivery-platform-west1.name + ip_range_pods = "delivery-platform-pods-dev" + ip_range_services = "delivery-platform-services-dev" + release_channel = "STABLE" + zones = ["us-west1-a"] +} + +module "delivery-platform-staging" { + source = "./modules/platform-cluster" + project_id = var.project_id + name = "stage" + region = "us-west2" + network = data.google_compute_network.delivery-platform.name + subnetwork = data.google_compute_subnetwork.delivery-platform-west2.name + ip_range_pods = "delivery-platform-pods-staging" + ip_range_services = "delivery-platform-services-staging" + release_channel = "STABLE" + zones = ["us-west2-a"] +} + +module "delivery-platform-prod" { + source = "./modules/platform-cluster" + project_id = var.project_id + name = "prod" + region = "us-central1" + network = data.google_compute_network.delivery-platform.name + subnetwork = data.google_compute_subnetwork.delivery-platform-central1.name + ip_range_pods = "delivery-platform-pods-prod" + ip_range_services = "delivery-platform-services-prod" + release_channel = "STABLE" + zones = ["us-central1-a"] +} + diff --git a/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/main.tf b/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/main.tf new file mode 100644 index 0000000..bc930b7 --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/main.tf @@ -0,0 +1,88 @@ +/** + * Copyright 2021 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. + */ + +resource "google_service_account" "service_account" { + project = var.project_id + account_id = "tf-sa-${var.name}" + display_name = "Cluster Service Account for ${var.name}" +} + +resource "google_project_iam_member" "cluster_iam_logginglogwriter" { + project = var.project_id + role = "roles/logging.logWriter" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_project_iam_member" "cluster_iam_monitoringmetricwriter" { + project = var.project_id + role = "roles/monitoring.metricWriter" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_project_iam_member" "cluster_iam_monitoringviewer" { + project = var.project_id + role = "roles/monitoring.viewer" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_project_iam_member" "cluster_iam_artifactregistryreader" { + project = var.project_id + role = "roles/artifactregistry.reader" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_project_iam_member" "cluster_iam_storageobjectviewer" { + project = var.project_id + role = "roles/storage.objectViewer" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +module "platform_cluster" { + source = "terraform-google-modules/kubernetes-engine/google//modules/beta-public-cluster" + version = "~> 15.0.0" + project_id = var.project_id + name = var.name + region = var.region + network = var.network + subnetwork = var.subnetwork + ip_range_pods = var.ip_range_pods + ip_range_services = var.ip_range_services + kubernetes_version = var.gke_kubernetes_version + release_channel = var.release_channel + regional = false + zones = var.zones + + enable_binary_authorization = true + + create_service_account = false + service_account = google_service_account.service_account.email + identity_namespace = "${var.project_id}.svc.id.goog" + node_metadata = "GKE_METADATA_SERVER" + + remove_default_node_pool = true + + node_pools = [ + { + name = "wi-pool" + machine_type = var.machine_type + min_count = var.minimum_node_pool_instances + max_count = var.maximum_node_pool_instances + auto_upgrade = true + initial_node_count = 1 + }, + ] +} + diff --git a/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/outputs.tf b/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/outputs.tf new file mode 100644 index 0000000..8309ada --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/outputs.tf @@ -0,0 +1,39 @@ +/** + * Copyright 2021 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. + */ + +output "service_account" { + value = module.platform_cluster.service_account + description = "Service account used to create the cluster and node pool(s)" +} + +output "region" { + value = var.region + description = "Region for development cluster" +} + +output "name" { + value = var.name + description = "Cluster Name" +} + +output "endpoint" { + value = module.platform_cluster.endpoint + description = "Cluster endpoint used to identify the cluster" +} +output "location" { + value = module.platform_cluster.location + description = "Cluster location" +} \ No newline at end of file diff --git a/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/variables.tf b/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/variables.tf new file mode 100644 index 0000000..d88278b --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/modules/platform-cluster/variables.tf @@ -0,0 +1,78 @@ +/** + * Copyright 2021 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. + */ + +variable "project_id" { + description = "Project ID where the cluster will run" +} + +variable "name" { + description = "A unique name for the resource" +} + +variable "region" { + description = "The name of the region to run the cluster" +} + +variable "network" { + description = "The name of the network to run the cluster" +} + +variable "subnetwork" { + description = "The name of the subnet to run the cluster" +} + +variable "ip_range_pods" { + description = "The secondary range for the pods" +} + +variable "ip_range_services" { + description = "The secondary range for the services" +} + +variable "machine_type" { + description = "Type of node to use to run the cluster" + default = "e2-standard-2" +} + +variable "gke_kubernetes_version" { + description = "Kubernetes version to deploy Masters and Nodes with" + default = "1.18" +} + +variable "minimum_node_pool_instances" { + type = number + description = "Number of node-pool instances to have active" + default = 1 +} + +variable "maximum_node_pool_instances" { + type = number + description = "Maximum number of node-pool instances to scale to" + default = 1 +} + +variable "release_channel" { + type = string + description = "(Beta) The release channel of this cluster. Accepted values are `UNSPECIFIED`, `RAPID`, `REGULAR` and `STABLE`. Defaults to `UNSPECIFIED`." + default = "STABLE" +} + +variable "zones" { + type = list(string) + description = "The zones to host the cluster in (optional if regional cluster / required if zonal)" + default = [] +} + diff --git a/delivery-platform/resources/provision/clusters/tf/outputs.tf b/delivery-platform/resources/provision/clusters/tf/outputs.tf new file mode 100644 index 0000000..5cbf309 --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/outputs.tf @@ -0,0 +1,38 @@ +/** + * Copyright 2021 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. + */ + +output "development__cluster-service-account" { + value = module.delivery-platform-dev.service_account + description = "Service account used to create the cluster and node pool(s)" +} + +output "staging__cluster-service-account" { + value = module.delivery-platform-staging.service_account + description = "Service account used to create the cluster and node pool(s)" +} + +output "dev_name" { value = module.delivery-platform-dev.name } +output "dev_location" { value = module.delivery-platform-dev.location } +output "dev_endpoint" { value = module.delivery-platform-dev.endpoint } + +output "stage_name" { value = module.delivery-platform-staging.name } +output "stage_location" { value = module.delivery-platform-staging.location } +output "stage_endpoint" { value = module.delivery-platform-staging.endpoint } + +output "prod_name" { value = module.delivery-platform-prod.name } +output "prod_location" { value = module.delivery-platform-prod.location } +output "prod_endpoint" { value = module.delivery-platform-prod.endpoint } + diff --git a/delivery-platform/resources/provision/clusters/tf/terraform.tmpl b/delivery-platform/resources/provision/clusters/tf/terraform.tmpl new file mode 100644 index 0000000..19010a7 --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/terraform.tmpl @@ -0,0 +1,17 @@ +/** + * Copyright 2021 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. + */ + +project_id = "YOUR_PROJECT_ID" diff --git a/delivery-platform/resources/provision/clusters/tf/variables.tf b/delivery-platform/resources/provision/clusters/tf/variables.tf new file mode 100644 index 0000000..9a97b7a --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/variables.tf @@ -0,0 +1,21 @@ +/** + * Copyright 2021 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. + */ + +variable "project_id" { + description = "The project ID to host the cluster in" +} + + diff --git a/delivery-platform/resources/provision/clusters/tf/versions.tf b/delivery-platform/resources/provision/clusters/tf/versions.tf new file mode 100644 index 0000000..c001c4e --- /dev/null +++ b/delivery-platform/resources/provision/clusters/tf/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2021 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. + */ + +terraform { + required_version = ">= 0.13" +} diff --git a/delivery-platform/resources/provision/foundation/tf/README.md b/delivery-platform/resources/provision/foundation/tf/README.md new file mode 100644 index 0000000..e73924f --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/README.md @@ -0,0 +1,14 @@ + + +Create the foundation +- Requires `base_image` to be created first + +``` +gcloud builds submit +``` + +Destroy the foundation + +``` +gcloud builds submit --config cloudbuild-destroy.yaml +``` \ No newline at end of file diff --git a/delivery-platform/resources/provision/foundation/tf/backend.tmpl b/delivery-platform/resources/provision/foundation/tf/backend.tmpl new file mode 100644 index 0000000..f2ee1b3 --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/backend.tmpl @@ -0,0 +1,22 @@ +/** + * Copyright 2021 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. + */ + +terraform { + backend "gcs" { + bucket = "YOUR_PROJECT_ID-delivery-platform-tf-state" + prefix = "foundation" + } +} diff --git a/delivery-platform/resources/provision/foundation/tf/cloudbuild-destroy.yaml b/delivery-platform/resources/provision/foundation/tf/cloudbuild-destroy.yaml new file mode 100644 index 0000000..352ef5e --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/cloudbuild-destroy.yaml @@ -0,0 +1,31 @@ +# Copyright 2021 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. + +timeout: 3600s # 1-hour +tags: + - delivery-platform + - delivery-platform-foundation +steps: +- name: 'gcr.io/${PROJECT_ID}/delivery-platform-installer' + id: 'destroy-foundation' + entrypoint: 'bash' + args: + - '-xe' + - '-c' + - | + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" terraform.tmpl > terraform.tfvars + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" backend.tmpl > backend.tf + + terraform init + terraform destroy -auto-approve diff --git a/delivery-platform/resources/provision/foundation/tf/cloudbuild.yaml b/delivery-platform/resources/provision/foundation/tf/cloudbuild.yaml new file mode 100644 index 0000000..453e50d --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/cloudbuild.yaml @@ -0,0 +1,41 @@ +# Copyright 2021 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. + +timeout: 3600s # 1-hr +tags: + - delivery-platform + - delivery-platform-foundation +steps: +- name: 'gcr.io/cloud-builders/gsutil' + id: 'create-tf-state-bucket' + entrypoint: 'bash' + args: + - '-xe' + - '-c' + - | + gsutil mb gs://${PROJECT_ID}-delivery-platform-tf-state || true + +- name: 'gcr.io/${PROJECT_ID}/delivery-platform-installer' + id: 'create-foundation' + entrypoint: 'bash' + args: + - '-xe' + - '-c' + - | + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" terraform.tmpl > terraform.tfvars + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" backend.tmpl > backend.tf + + terraform init + terraform plan -out=terraform.tfplan + terraform apply -auto-approve terraform.tfplan diff --git a/delivery-platform/resources/provision/foundation/tf/networks.tf b/delivery-platform/resources/provision/foundation/tf/networks.tf new file mode 100644 index 0000000..94503ed --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/networks.tf @@ -0,0 +1,74 @@ +/** + * Copyright 2021 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. + */ + +provider "google" { + version = "~> 3.44.0" + project = var.project_id +} + +resource "google_compute_network" "delivery-platform" { + name = "delivery-platform" + auto_create_subnetworks = false + depends_on = [module.project-services.project_id] +} + +resource "google_compute_subnetwork" "delivery-platform-central1" { + name = "delivery-platform-central1" + ip_cidr_range = "10.2.0.0/16" + region = "us-central1" + network = google_compute_network.delivery-platform.self_link + + secondary_ip_range { + range_name = "delivery-platform-pods-prod" + ip_cidr_range = "172.16.0.0/16" + } + secondary_ip_range { + range_name = "delivery-platform-services-prod" + ip_cidr_range = "192.168.2.0/24" + } +} + +resource "google_compute_subnetwork" "delivery-platform-west1" { + name = "delivery-platform-west1" + ip_cidr_range = "10.4.0.0/16" + region = "us-west1" + network = google_compute_network.delivery-platform.self_link + + secondary_ip_range { + range_name = "delivery-platform-pods-dev" + ip_cidr_range = "172.18.0.0/16" + } + secondary_ip_range { + range_name = "delivery-platform-services-dev" + ip_cidr_range = "192.168.4.0/24" + } +} + +resource "google_compute_subnetwork" "delivery-platform-west2" { + name = "delivery-platform-west2" + ip_cidr_range = "10.5.0.0/16" + region = "us-west2" + network = google_compute_network.delivery-platform.self_link + + secondary_ip_range { + range_name = "delivery-platform-pods-staging" + ip_cidr_range = "172.19.0.0/16" + } + secondary_ip_range { + range_name = "delivery-platform-services-staging" + ip_cidr_range = "192.168.5.0/24" + } +} diff --git a/delivery-platform/resources/provision/foundation/tf/services.tf b/delivery-platform/resources/provision/foundation/tf/services.tf new file mode 100644 index 0000000..7fd4dc4 --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/services.tf @@ -0,0 +1,36 @@ +/** + * Copyright 2021 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. + */ + +module "project-services" { + source = "terraform-google-modules/project-factory/google//modules/project_services" + version = "~> 9.0" + + project_id = var.project_id + + # Don't disable the services + disable_services_on_destroy = false + disable_dependent_services = false + + activate_apis = [ + "compute.googleapis.com", + "container.googleapis.com", + "cloudresourcemanager.googleapis.com", + "sqladmin.googleapis.com", + "artifactregistry.googleapis.com", + "gkehub.googleapis.com", + "multiclusteringress.googleapis.com" + ] +} diff --git a/delivery-platform/resources/provision/foundation/tf/terraform.tmpl b/delivery-platform/resources/provision/foundation/tf/terraform.tmpl new file mode 100644 index 0000000..19010a7 --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/terraform.tmpl @@ -0,0 +1,17 @@ +/** + * Copyright 2021 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. + */ + +project_id = "YOUR_PROJECT_ID" diff --git a/delivery-platform/resources/provision/foundation/tf/variables.tf b/delivery-platform/resources/provision/foundation/tf/variables.tf new file mode 100644 index 0000000..56b442b --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/variables.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2021 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. + */ + +variable "project_id" { + description = "The project ID to host the cluster in" +} diff --git a/delivery-platform/resources/provision/foundation/tf/versions.tf b/delivery-platform/resources/provision/foundation/tf/versions.tf new file mode 100644 index 0000000..c001c4e --- /dev/null +++ b/delivery-platform/resources/provision/foundation/tf/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2021 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. + */ + +terraform { + required_version = ">= 0.13" +} diff --git a/delivery-platform/resources/provision/management-tools/acm/acm-install.sh b/delivery-platform/resources/provision/management-tools/acm/acm-install.sh new file mode 100755 index 0000000..b45ce61 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/acm-install.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + +#gsutil cp gs://config-management-release/released/latest/config-management-operator.yaml config-management-operator.yaml +gcloud builds submit \ + --substitutions=_CLUSTER_CONFIG_REPO="$GIT_BASE_URL/$CLUSTER_CONFIG_REPO",_BRANCH="main",_DEV_PATH="dev",_STAGE_PATH="stage",_PROD_PATH="prod" diff --git a/delivery-platform/resources/provision/management-tools/acm/acm.tf b/delivery-platform/resources/provision/management-tools/acm/acm.tf new file mode 100644 index 0000000..8f44267 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/acm.tf @@ -0,0 +1,58 @@ +/** + * Copyright 2021 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. + */ + +module "acm-dev" { + source = "terraform-google-modules/kubernetes-engine/google//modules/acm" + version = "14.1.0" + + project_id = var.project_id + cluster_name = data.terraform_remote_state.clusters.outputs.dev_name + location = data.terraform_remote_state.clusters.outputs.dev_location + cluster_endpoint = data.terraform_remote_state.clusters.outputs.dev_endpoint + + secret_type = "ssh" + sync_repo = var.acm_repo_location + sync_branch = var.acm_branch + policy_dir = var.dev_dir +} +module "acm-stage" { + source = "terraform-google-modules/kubernetes-engine/google//modules/acm" + version = "14.1.0" + + project_id = var.project_id + cluster_name = data.terraform_remote_state.clusters.outputs.stage_name + location = data.terraform_remote_state.clusters.outputs.stage_location + cluster_endpoint = data.terraform_remote_state.clusters.outputs.stage_endpoint + + secret_type = "ssh" + sync_repo = var.acm_repo_location + sync_branch = var.acm_branch + policy_dir = var.stage_dir +} +module "acm-prod" { + source = "terraform-google-modules/kubernetes-engine/google//modules/acm" + version = "14.1.0" + + project_id = var.project_id + cluster_name = data.terraform_remote_state.clusters.outputs.prod_name + location = data.terraform_remote_state.clusters.outputs.prod_location + cluster_endpoint = data.terraform_remote_state.clusters.outputs.prod_endpoint + + secret_type = "ssh" + sync_repo = var.acm_repo_location + sync_branch = var.acm_branch + policy_dir = var.prod_dir +} \ No newline at end of file diff --git a/delivery-platform/resources/provision/management-tools/acm/backend.tmpl b/delivery-platform/resources/provision/management-tools/acm/backend.tmpl new file mode 100644 index 0000000..ddeb6c4 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/backend.tmpl @@ -0,0 +1,22 @@ +/** + * Copyright 2021 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. + */ + +terraform { + backend "gcs" { + bucket = "YOUR_PROJECT_ID-delivery-platform-tf-state" + prefix = "acm" + } +} diff --git a/delivery-platform/resources/provision/management-tools/acm/cloudbuild-destroy.yaml b/delivery-platform/resources/provision/management-tools/acm/cloudbuild-destroy.yaml new file mode 100644 index 0000000..2dc1e31 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/cloudbuild-destroy.yaml @@ -0,0 +1,32 @@ +# Copyright 2021 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. + +timeout: 3600s # 1-hour +tags: + - delivery-platform + - delivery-platform-acm +steps: +- name: 'gcr.io/${PROJECT_ID}/delivery-platform-installer' + id: 'remove-acm' + entrypoint: 'bash' + args: + - '-xe' + - '-c' + - | + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" terraform.tmpl > terraform.tfvars + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" backend.tmpl > backend.tf + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" remote_state.tmpl > remote_state.tf + + terraform init + terraform destroy -auto-approve diff --git a/delivery-platform/resources/provision/management-tools/acm/cloudbuild.yaml b/delivery-platform/resources/provision/management-tools/acm/cloudbuild.yaml new file mode 100644 index 0000000..9cc1390 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/cloudbuild.yaml @@ -0,0 +1,47 @@ +# Copyright 2021 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. + +timeout: 3600s # 1-hr +tags: + - delivery-platform + - delivery-platform-acm +steps: +- name: 'gcr.io/${PROJECT_ID}/delivery-platform-installer' + id: 'apply-acm' + entrypoint: 'bash' + args: + - '-xe' + - '-c' + - | + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" backend.tmpl > backend.tf + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" remote_state.tmpl > remote_state.tf + sed "s/YOUR_PROJECT_ID/${PROJECT_ID}/g" terraform.tmpl > terraform.tfvars + sed -i "s|CONFIG_REPO|${_CLUSTER_CONFIG_REPO}|g" terraform.tfvars + sed -i "s|BRANCH|${_BRANCH}|g" terraform.tfvars + sed -i "s|DEV_PATH|${_DEV_PATH}|g" terraform.tfvars + sed -i "s|STAGE_PATH|${_STAGE_PATH}|g" terraform.tfvars + sed -i "s|PROD_PATH|${_PROD_PATH}|g" terraform.tfvars + + #export TF_LOG="ERROR" + + terraform init + terraform plan -var-file="terraform.tfvars" -out=terraform.tfplan + terraform apply -auto-approve terraform.tfplan + +substitutions: + _CLUSTER_CONFIG_REPO: https://github.com/GoogleCloudPlatform/csp-config-management/ + _BRANCH: 1.0.0 + _DEV_PATH: foo-corp + _STAGE_PATH: foo-corp + _PROD_PATH: foo-corp diff --git a/delivery-platform/resources/provision/management-tools/acm/outputs.tf b/delivery-platform/resources/provision/management-tools/acm/outputs.tf new file mode 100644 index 0000000..67be211 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/outputs.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2021 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. + */ + +output "test" { + value = "test" +} \ No newline at end of file diff --git a/delivery-platform/resources/provision/management-tools/acm/remote_state.tmpl b/delivery-platform/resources/provision/management-tools/acm/remote_state.tmpl new file mode 100644 index 0000000..7d3b841 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/remote_state.tmpl @@ -0,0 +1,24 @@ +/** + * Copyright 2021 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. + */ + +data "terraform_remote_state" "clusters" { + backend = "gcs" + + config = { + bucket = "YOUR_PROJECT_ID-delivery-platform-tf-state" + prefix = "clusters" + } +} diff --git a/delivery-platform/resources/provision/management-tools/acm/terraform.tmpl b/delivery-platform/resources/provision/management-tools/acm/terraform.tmpl new file mode 100644 index 0000000..d153723 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/terraform.tmpl @@ -0,0 +1,23 @@ +/** + * Copyright 2021 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. + */ + +project_id = "YOUR_PROJECT_ID" +acm_repo_location = "CONFIG_REPO" +acm_branch = "BRANCH" +acm_dir = "DEV_PATH" +dev_dir = "DEV_PATH" +stage_dir = "STAGE_PATH" +prod_dir = "PROD_PATH" \ No newline at end of file diff --git a/delivery-platform/resources/provision/management-tools/acm/variables.tf b/delivery-platform/resources/provision/management-tools/acm/variables.tf new file mode 100644 index 0000000..914440a --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/acm/variables.tf @@ -0,0 +1,39 @@ +/** + * Copyright 2021 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. + */ + +variable "project_id" { + description = "The project ID to host the cluster in" +} + +variable "acm_repo_location" { + description = "The location of the git repo ACM will sync to" +} +variable "acm_branch" { + description = "The git branch ACM will sync to" +} +variable "acm_dir" { + description = "The directory in git ACM will sync to" +} + +variable "dev_dir" { + description = "The directory in git Dev ACM will sync to" +} +variable "stage_dir" { + description = "The directory in git Stage ACM will sync to" +} +variable "prod_dir" { + description = "The directory in git Prod ACM will sync to" +} \ No newline at end of file diff --git a/delivery-platform/resources/provision/management-tools/argo-install.sh b/delivery-platform/resources/provision/management-tools/argo-install.sh new file mode 100755 index 0000000..c7da28c --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/argo-install.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + +ARGOCD_VERSION="v1.8.7" + +gcloud container clusters get-credentials dev --region us-west1-a --project $PROJECT_ID +kubectl create namespace argocd +kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/${ARGOCD_VERSION}/manifests/install.yaml +echo Waiting for Argo install... +kubectl wait --for=condition=ready pod -n argocd -l app.kubernetes.io/name=argocd-server --timeout=60s + +curl -sSL -o ${WORK_DIR}/bin/argocd https://github.com/argoproj/argo-cd/releases/download/${ARGOCD_VERSION}/argocd-linux-amd64 +chmod 755 ${WORK_DIR}/bin/argocd + +USER=admin +PASSWORD=$(kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2) +kubectl port-forward svc/argocd-server -n argocd 8080:443 & +PORT_FWD_PID=$! + +# Argo UI http://localhost:8080 +argocd login localhost:8080 --insecure --username=$USER --password=$PASSWORD + +gcloud container clusters get-credentials dev --region us-west1-a --project $PROJECT_ID +kubectl config delete-context dev +kubectl config rename-context gke_${PROJECT_ID}_us-west1-a_dev dev + +gcloud container clusters get-credentials stage --region us-west2-a --project $PROJECT_ID +kubectl config delete-context stage +kubectl config rename-context gke_${PROJECT_ID}_us-west2-a_stage stage + +gcloud container clusters get-credentials prod --region us-central1-a --project $PROJECT_ID +kubectl config delete-context prod +kubectl config rename-context gke_${PROJECT_ID}_us-central1-a_prod prod + +argocd cluster add dev +argocd cluster add stage +argocd cluster add prod +kill $PORT_FWD_PID + +echo --- Note: some errors are seen in argo install output. +echo You can ignore the following +echo FATA[0000] dial tcp [::1]:8080: connect: connection refused +echo FATA[0001] rpc error: code = Unauthenticated desc = invalid session: token signature is invalid +echo +echo Argo install completed diff --git a/delivery-platform/resources/provision/management-tools/gitea/gt-setup.sh b/delivery-platform/resources/provision/management-tools/gitea/gt-setup.sh new file mode 100644 index 0000000..5085f01 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/gitea/gt-setup.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +# Gitea setup + +gcloud compute firewall-rules create "allow-http" --allow=tcp:3000 + --source-ranges="0.0.0.0/0" --description="Allow http" + +gcloud compute addresses create gitea-ip +export IP_ADDRESS=$(gcloud compute addresses describe gitea-ip --format="value(address)") + +export ADMIN_PASS=$(date +%s | sha256sum | base64 | head -c 8 ; echo) + +cat < vals.yaml +gitea: + config: + server: + DOMAIN: ${IP_ADDRESS}:3000 + admin: + username: gitea_admin + password: ${ADMIN_PASS} + email: "gitea@local.domain" +service: + http: + type: LoadBalancer + loadBalancerIP: ${IP_ADDRESS} +EOF + +helm repo add gitea-charts https://dl.gitea.io/charts/ +helm upgrade --install -f vals.yaml gitea gitea-charts/gitea + +echo "================================" +echo +echo User: gitea_admin +echo Password: ${ADMIN_PASS} +echo Site: http://${IP_ADDRESS}:3000 +echo +echo "================================" \ No newline at end of file diff --git a/delivery-platform/resources/provision/management-tools/tekton-install.sh b/delivery-platform/resources/provision/management-tools/tekton-install.sh new file mode 100755 index 0000000..36178a5 --- /dev/null +++ b/delivery-platform/resources/provision/management-tools/tekton-install.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +gcloud container clusters get-credentials dev --region us-west1-a --project $PROJECT_ID +kubectl apply --filename https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml diff --git a/delivery-platform/resources/provision/provision-all.sh b/delivery-platform/resources/provision/provision-all.sh new file mode 100755 index 0000000..4eca4c7 --- /dev/null +++ b/delivery-platform/resources/provision/provision-all.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +# Prerequisites + + source ${BASE_DIR}/scripts/git/set-git-env.sh + + ## Prepare CloudBuild + + ### Enable APIS + # TODO trim this down... one of these is needed to create the compute service account + gcloud services enable \ + cloudresourcemanager.googleapis.com \ + container.googleapis.com \ + sourcerepo.googleapis.com \ + cloudbuild.googleapis.com \ + containerregistry.googleapis.com \ + anthosconfigmanagement.googleapis.com \ + run.googleapis.com \ + apikeys.googleapis.com \ + secretmanager.googleapis.com + + ### Grant the Project Editor role to the Cloud Build service account in order to provision project resources + + gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ + --role=roles/owner + + ### Grant the IAM Service Account User role to the Cloud Build service account for the Cloud Run runtime service account: + + gcloud iam service-accounts add-iam-policy-binding \ + $PROJECT_NUMBER-compute@developer.gserviceaccount.com \ + --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ + --role=roles/iam.serviceAccountUser + + ### Grant the Storage Object Viewer role to the default Compute service account in order to read GCR.io images + + gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member=serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com \ + --role=roles/storage.objectViewer + + ### Create the base image used for provisioning + cd ${BASE_DIR}/resources/provision/base_image + gcloud builds submit --tag gcr.io/$PROJECT_ID/delivery-platform-installer + cd $BASE_DIR + + ### Create GIT secret + printf ${GIT_TOKEN} | gcloud secrets create gh_token --data-file=- + + +# Create Template Repos +cd ${BASE_DIR}/resources/provision/repos +./create-template-repos.sh +cd $BASE_DIR + +# Create Config Repos +cd ${BASE_DIR}/resources/provision/repos +./create-config-repo.sh +cd $BASE_DIR + +# Provision network and foundational elements +cd ${BASE_DIR}/resources/provision/foundation/tf +gcloud builds submit +cd $BASE_DIR + +# Provision the clusters +cd ${BASE_DIR}/resources/provision/clusters/tf +gcloud builds submit +cd $BASE_DIR + +# Install ACM +cd ${BASE_DIR}/resources/provision/management-tools/acm +./acm-install.sh +cd $BASE_DIR + +# Install Tekton +cd ${BASE_DIR}/resources/provision/management-tools +./tekton-install.sh +cd $BASE_DIR + +# Install Argo +cd ${BASE_DIR}/resources/provision/management-tools +./argo-install.sh +cd $BASE_DIR diff --git a/delivery-platform/resources/provision/repos/create-config-repo.sh b/delivery-platform/resources/provision/repos/create-config-repo.sh new file mode 100755 index 0000000..b3c9d8e --- /dev/null +++ b/delivery-platform/resources/provision/repos/create-config-repo.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +## Create sample repos +echo $GIT_BASE_URL + +# Create config repo +cp -R $BASE_DIR/resources/repos/cluster-config $WORK_DIR +cd $WORK_DIR/cluster-config +git init && git symbolic-ref HEAD refs/heads/main && git add . && git commit -m "initial commit" +$BASE_DIR/scripts/git/${GIT_CMD} create $CLUSTER_CONFIG_REPO +git remote add origin $GIT_BASE_URL/$CLUSTER_CONFIG_REPO +git push origin main +cd $BASE_DIR +rm -rf $WORK_DIR/cluster-config diff --git a/delivery-platform/resources/provision/repos/create-template-repos.sh b/delivery-platform/resources/provision/repos/create-template-repos.sh new file mode 100755 index 0000000..90274ea --- /dev/null +++ b/delivery-platform/resources/provision/repos/create-template-repos.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +## Create sample repos +echo $GIT_BASE_URL + +# Create templates repo +cp -R $BASE_DIR/resources/repos/app-templates $WORK_DIR +cd $WORK_DIR/app-templates +git init && git symbolic-ref HEAD refs/heads/main && git add . && git commit -m "initial commit" +$BASE_DIR/scripts/git/${GIT_CMD} create $APP_TEMPLATES_REPO +git remote add origin $GIT_BASE_URL/$APP_TEMPLATES_REPO +git push origin main +cd $BASE_DIR +rm -rf $WORK_DIR/app-templates + +# Create shared kustomize repo +cp -R $BASE_DIR/resources/repos/shared-kustomize $WORK_DIR +cd $WORK_DIR/shared-kustomize +git init && git symbolic-ref HEAD refs/heads/main && git add . && git commit -m "initial commit" +$BASE_DIR/scripts/git/${GIT_CMD} create $SHARED_KUSTOMIZE_REPO +git remote add origin $GIT_BASE_URL/$SHARED_KUSTOMIZE_REPO +git push origin main +cd $BASE_DIR +rm -rf $WORK_DIR/shared-kustomize + diff --git a/delivery-platform/resources/provision/repos/teardown.sh b/delivery-platform/resources/provision/repos/teardown.sh new file mode 100755 index 0000000..fa0aa22 --- /dev/null +++ b/delivery-platform/resources/provision/repos/teardown.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +$BASE_DIR/scripts/git/${GIT_CMD} delete $APP_TEMPLATES_REPO +$BASE_DIR/scripts/git/${GIT_CMD} delete $SHARED_KUSTOMIZE_REPO +$BASE_DIR/scripts/git/${GIT_CMD} delete $CLUSTER_CONFIG_REPO \ No newline at end of file diff --git a/delivery-platform/resources/provision/teardown-all.sh b/delivery-platform/resources/provision/teardown-all.sh new file mode 100755 index 0000000..28835b6 --- /dev/null +++ b/delivery-platform/resources/provision/teardown-all.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + +cd ${BASE_DIR}/resources/provision/clusters/tf +gcloud builds submit --config cloudbuild-destroy.yaml +cd ${BASE_DIR} + +cd ${BASE_DIR}/resources/provision/foundation/tf +gcloud builds submit --config cloudbuild-destroy.yaml +cd ${BASE_DIR} + +# Base Repos +cd ${BASE_DIR}/resources/provision/repos +./teardown.sh +cd ${BASE_DIR}/ + +# Delete git secret +gcloud secrets delete gh_token + +# Delete contexts +kubectl config delete-context dev +kubectl config delete-context stage +kubectl config delete-context prod + +# remove tfstate to avoid conflict with reprovision +gsutil rm gs://${PROJECT_ID}-delivery-platform-tf-state/**.tfstate diff --git a/delivery-platform/resources/repos/README.md b/delivery-platform/resources/repos/README.md new file mode 100644 index 0000000..85cda88 --- /dev/null +++ b/delivery-platform/resources/repos/README.md @@ -0,0 +1,7 @@ +# Sample Repos + +The sample repos provided in this directory are push to your git provider as part of the provisioning process. From then on the workshop and platform will utilize the repositories located in your git provider. + +This directory contains a simple `cluster-config` repo designed for use with Anthos Config Manager. + +Additional repositories are provided for application templates and the associated kustomize folders. To add aditional app templates, simply create the appropriate structures in the `app-templates` and `shared-kustomize` tempalates forlder prior to provisioning, or add them directly to your git provider's repo anytime after provisioning. \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/.gitignore b/delivery-platform/resources/repos/app-templates/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/golang/Dockerfile b/delivery-platform/resources/repos/app-templates/golang/Dockerfile new file mode 100644 index 0000000..782ca9e --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/Dockerfile @@ -0,0 +1,19 @@ +# Copyright 2021 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. + +FROM golang:1 +WORKDIR /app +COPY * ./ +RUN go build -o golang-template +CMD ["/app/golang-template"] diff --git a/delivery-platform/resources/repos/app-templates/golang/README.md b/delivery-platform/resources/repos/app-templates/golang/README.md new file mode 100644 index 0000000..5043735 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/README.md @@ -0,0 +1 @@ +# app-template diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml b/delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml new file mode 100644 index 0000000..216eca0 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml @@ -0,0 +1,154 @@ +# Copyright 2021 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. + +steps: + # TEMP STEP + # - id: gitref + # name: bash + # args: + # - '-c' + # - | + # echo "${_REF}" + # IFS='/' read -a array <<< "${_REF}" + # echo ${array[1]} + + # if [ ${array[1]} == "tags" ] ; then + # echo "FOUND TAG" + # else + # echo "FOUND BRANCH" + # fi + # echo ${array[1]} ${array[2]} + + # Create the hydrated directories + - id: create-dirs + name: bash + entrypoint: bash + args: + - '-c' + - | + mkdir -p app-hydrated/dev + mkdir -p app-hydrated/stage + mkdir -p app-hydrated/prod + + + # Clone the repos + - id: clone-app + name: gcr.io/cloud-builders/git + entrypoint: bash + args: + - '-c' + - | + IFS='/' read -a array <<< "${_REF}" + git clone -b ${array[2]} ${_APP_REPO} app-repo + + - id: clone-kustomize + name: gcr.io/cloud-builders/git + entrypoint: bash + # changed from GIT_URL to APP_REPO + args: + - '-c' + - | + git clone ${_KUSTOMIZE_REPO} kustomize-base + + - id: clone-config + name: gcr.io/cloud-builders/git + entrypoint: bash + + args: + - '-c' + - | + git clone ${_CONFIG_REPO} config-repo + + # Build and push the image + - id: skaffold-build + name: gcr.io/k8s-skaffold/skaffold + entrypoint: bash + # changed from GIT_URL to APP_REPO + args: + - '-c' + - | + cd app-repo + skaffold build --default-repo ${_DEFAULT_IMAGE_REPO} + + # Render the manifests + - id: skaffold-render + name: gcr.io/k8s-skaffold/skaffold + entrypoint: bash + # changed from GIT_URL to APP_REPO + args: + - '-c' + - | + cd app-repo + skaffold render \ + --default-repo ${_DEFAULT_IMAGE_REPO} \ + --output ../app-hydrated/stage/resources.yaml + + # Deliver the app + - id: deliver + name: gcr.io/cloud-builders/git + secretEnv: ['GH_TOKEN'] + entrypoint: bash + args: + - '-c' + - | + + # Setup auth for commit + echo 'exec echo "$$GH_TOKEN"' > ~/git-ask-pass.sh + chmod +x ~/git-ask-pass.sh + export GIT_ASKPASS=~/git-ask-pass.sh + git config --global user.email "CloudBuild@yourcompany.com" + git config --global user.name "Cloud Build" + + # Copy the hydrated file to the config repo & push + cd config-repo + git pull + + # Main, Tag or Branch + IFS='/' read -a array <<< "${_REF}" + + # Tag + if [[ ${array[1]} = 'tags' ]] ; then + cp ../app-hydrated/stage/resources.yaml \ + ./prod/namespaces/${_APP_NAME} + + # Main + elif [[ ${array[2]} = 'main' ]] ; then + cp ../app-hydrated/stage/resources.yaml \ + ./stage/namespaces/${_APP_NAME} + + # Feature Branch + else + echo "NOT_IMPLEMENTED: Not deploying feature branches" + fi + + git add . && git commit -m "Deploying new image" + git push origin main + + # Cleanup the workspace + - id: cleanup + name: bash + entrypoint: bash + args: + - '-c' + - | + rm -rf app-hydrated + rm -rf app-hydrated + rm -rf kustomize-base + + +availableSecrets: + secretManager: + - versionName: projects/$PROJECT_ID/secrets/gh_token/versions/latest + env: GH_TOKEN + diff --git a/delivery-platform/resources/repos/app-templates/golang/go.mod b/delivery-platform/resources/repos/app-templates/golang/go.mod new file mode 100644 index 0000000..343c10a --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/go.mod @@ -0,0 +1,3 @@ +module example.com/golang + +go 1.16 diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/dev/deployment.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/dev/deployment.yaml new file mode 100644 index 0000000..742fd06 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/dev/deployment.yaml @@ -0,0 +1,34 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: app # Must match base value + image: app # Overwrites values from base - Needs to match skaffold artifact + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: dev diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml new file mode 100644 index 0000000..18965c4 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +bases: +- ../../../kustomize-base/golang +patches: +- deployment.yaml +namePrefix: "golang-template-" # App name (dash) +commonLabels: + app: golang-template # App name for selectors + role: backend diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/prod/deployment.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/prod/deployment.yaml new file mode 100644 index 0000000..f32c239 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/prod/deployment.yaml @@ -0,0 +1,34 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: app + image: app + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: prod diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml new file mode 100644 index 0000000..1e08b92 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +bases: +- ../../../kustomize-base/golang +patches: +- deployment.yaml +namePrefix: "golang-template-" +commonLabels: + app: golang-template + role: backend diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/stage/deployment.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/deployment.yaml new file mode 100644 index 0000000..b449115 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/deployment.yaml @@ -0,0 +1,35 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + replicas: 1 + template: + spec: + containers: + - name: app + image: app + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: stg diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml new file mode 100644 index 0000000..8a65f83 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml @@ -0,0 +1,21 @@ +# Copyright 2021 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. + +bases: +- ../../../kustomize-base/golang +patches: +- deployment.yaml +commonLabels: + env: stg +namePrefix: golang-template- diff --git a/delivery-platform/resources/repos/app-templates/golang/main.go b/delivery-platform/resources/repos/app-templates/golang/main.go new file mode 100644 index 0000000..4b896c1 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/main.go @@ -0,0 +1,40 @@ +// Copyright 2021 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. + +package main + +import ( + "fmt" + "log" + "net/http" + "os" +) + +func main() { + env := os.Getenv("ENVIRONMENT") + port := 8080 + log.Printf("Running in environment: %s\n", env) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Printf("Received request from %s at %s", r.RemoteAddr, r.URL.EscapedPath()) + fmt.Fprint(w, "Hello World!") + }) + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + log.Printf("Received health check from %s", r.RemoteAddr) + w.WriteHeader(http.StatusOK) + }) + log.Printf("Starting server on port: %v", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) + +} diff --git a/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml b/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml new file mode 100644 index 0000000..9bc7103 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml @@ -0,0 +1,41 @@ +# Copyright 2021 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. + +apiVersion: skaffold/v1beta14 +kind: Config +build: + artifacts: + - image: app # Match name in deployment yaml +deploy: + kustomize: + path: k8s/dev + +profiles: +- name: dev + activation: + - command: dev + deploy: + kustomize: + path: k8s/dev + +- name: test + deploy: + kustomize: + path: k8s/stage + +- name: prod + deploy: + kustomize: + path: k8s/prod + diff --git a/delivery-platform/resources/repos/app-templates/java/.gitignore b/delivery-platform/resources/repos/app-templates/java/.gitignore new file mode 100644 index 0000000..5a79060 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/.gitignore @@ -0,0 +1,17 @@ +Thumbs.db +.DS_Store +.gradle +build/ +target/ +out/ +.idea +*.iml +*.ipr +*.iws +.project +.settings +.classpath +.factorypath +.mvn/ +dependency-reduced-pom.xml +.vscode/ \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/.gitlab-ci.yml b/delivery-platform/resources/repos/app-templates/java/.gitlab-ci.yml new file mode 100644 index 0000000..f073030 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/.gitlab-ci.yml @@ -0,0 +1,33 @@ +# Copyright 2021 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. + +include: +- project: 'platform-admins/shared-ci-cd' + file: 'ci/java-docker.yaml' +- project: 'platform-admins/shared-ci-cd' + file: 'skaffold/build.yaml' +- project: 'platform-admins/shared-ci-cd' + file: 'skaffold/render.yaml' +- project: 'platform-admins/shared-ci-cd' + file: 'cd/validate.yaml' +- project: 'platform-admins/shared-ci-cd' + file: 'cd/push-manifests.yaml' + +stages: + - test + - build + - render-manifests + - prepare-config + - validate-config + - push-manifests \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/Dockerfile b/delivery-platform/resources/repos/app-templates/java/Dockerfile new file mode 100644 index 0000000..a89b03e --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/Dockerfile @@ -0,0 +1,18 @@ +# Copyright 2021 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. + +FROM adoptopenjdk/openjdk13:jre-13.0.2_8-alpine +COPY target/app.jar app.jar +EXPOSE 8080 +CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar app.jar \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/README.md b/delivery-platform/resources/repos/app-templates/java/README.md new file mode 100644 index 0000000..71431fe --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/README.md @@ -0,0 +1,20 @@ +# Overview + +This is a sample application written in Java. In most cases, this should not be used +for development of a new application. + +## Useage + +Replace files (ex: pom.xml and src/ folders) to match the intended application's source. + +## Critical Files + +The following is a list of critical files utilized in the conventions for building +an Anthos application. + +| File/Folder | Description | Required | +|:-------------:|:----------------------|-----------:| +| Dockerfile :whale: | File used to create the Docker image (built with kaniko) | :white_check_mark: | +| skaffold.yaml | Used in local development to keep development environment in sync with changes. If not using skaffold, this file is optional (but recommended) | :white_large_square: | +| .gitlab-ci.yml | CICD Pipeline setup to build to inherit the conventions for the development organization/ecosystem | :white_check_mark: | +| k8s/ | Folder containing the Kubernetes resource manifests for "dev", "stage" and "prod". Resource files are configured to use Kustomize during the CICD build. | :white_check_mark: | \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/dev/deployment.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/dev/deployment.yaml new file mode 100644 index 0000000..3432a1b --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/k8s/dev/deployment.yaml @@ -0,0 +1,27 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + serviceAccountName: java-template-ksa + containers: + - name: app + env: + - name: ENVIRONMENT + value: dev \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/dev/kustomization.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/dev/kustomization.yaml new file mode 100644 index 0000000..867735b --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/k8s/dev/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +bases: +- ../../../kustomize-base/golang +patches: +- deployment.yaml +namePrefix: "java-template-" +commonLabels: + app: java-template + role: backend \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/prod/deployment.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/prod/deployment.yaml new file mode 100644 index 0000000..ebc7759 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/k8s/prod/deployment.yaml @@ -0,0 +1,27 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + serviceAccountName: java-template-ksa + containers: + - name: app + env: + - name: ENVIRONMENT + value: prod \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/prod/kustomization.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/prod/kustomization.yaml new file mode 100644 index 0000000..867735b --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/k8s/prod/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +bases: +- ../../../kustomize-base/golang +patches: +- deployment.yaml +namePrefix: "java-template-" +commonLabels: + app: java-template + role: backend \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/stg/deployment.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/stg/deployment.yaml new file mode 100644 index 0000000..96f198a --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/k8s/stg/deployment.yaml @@ -0,0 +1,28 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + replicas: 1 + template: + spec: + serviceAccountName: java-template-ksa + containers: + - name: app + env: + - name: ENVIRONMENT + value: stg \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/stg/kustomization.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/stg/kustomization.yaml new file mode 100644 index 0000000..04b4d11 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/k8s/stg/kustomization.yaml @@ -0,0 +1,21 @@ +# Copyright 2021 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. + +bases: +- ../../../kustomize-base/golang +patches: +- deployment.yaml +commonLabels: + env: stg +namePrefix: java-template- \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/pom.xml b/delivery-platform/resources/repos/app-templates/java/pom.xml new file mode 100644 index 0000000..1766ad8 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/pom.xml @@ -0,0 +1,85 @@ + + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.5.RELEASE + + + com.example + super-simple-app + 0.0.1-SNAPSHOT + super-simple-app + Sample executable Java application + + + 13 + app-${project.version} + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-web + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.1.2 + + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + ${jar.finalName} + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/skaffold.yaml b/delivery-platform/resources/repos/app-templates/java/skaffold.yaml new file mode 100644 index 0000000..0f50faa --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/skaffold.yaml @@ -0,0 +1,43 @@ +# Copyright 2021 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. + +apiVersion: skaffold/v2beta5 +kind: Config +# Defaults are configured for local dev +build: + artifacts: + - image: app +deploy: + kustomize: + paths: + - k8s/dev +profiles: + # Profile used when building images in CI + - name: ci + build: + cluster: + dockerConfig: + path: ~/.docker/config.json + # Profile used when rendering production manifests + - name: prod + deploy: + kustomize: + paths: + - k8s/prod + # Profile used when rendering staging manifests + - name: staging + deploy: + kustomize: + paths: + - k8s/stg diff --git a/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/SuperSimpleAppApplication.java b/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/SuperSimpleAppApplication.java new file mode 100644 index 0000000..0f98141 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/SuperSimpleAppApplication.java @@ -0,0 +1,27 @@ +// Copyright 2021 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. + +package com.example.simple; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SuperSimpleAppApplication { + + public static void main(String[] args) { + SpringApplication.run(SuperSimpleAppApplication.class, args); + } + +} \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/web/HelloController.java b/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/web/HelloController.java new file mode 100644 index 0000000..799e52f --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/web/HelloController.java @@ -0,0 +1,35 @@ +// Copyright 2021 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. + +package com.example.simple.web; + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String index() { + String environment = System.getenv("ENVIRONMENT"); + String response = "SimpleApp

Super Simple Java App

"; + if (!StringUtils.isEmpty(environment)) { + response += "\n\n

Environment: " + environment + "

"; + } + response += ""; + return response; + } + +} \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/src/main/resources/application.properties b/delivery-platform/resources/repos/app-templates/java/src/main/resources/application.properties new file mode 100644 index 0000000..ba3b5f6 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/src/main/resources/application.properties @@ -0,0 +1,3 @@ +# Remap the Acutator path to "/" & enable /health +management.endpoints.web.base-path=/ +management.endpoints.web.path-mapping.health=health \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/SuperSimpleAppApplicationTests.java b/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/SuperSimpleAppApplicationTests.java new file mode 100644 index 0000000..50c6c06 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/SuperSimpleAppApplicationTests.java @@ -0,0 +1,27 @@ +// Copyright 2020 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. + +package com.example.simple; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SuperSimpleAppApplicationTests { + + @Test + void contextLoads() { + } + +} \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/web/HelloControllerTest.java b/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/web/HelloControllerTest.java new file mode 100644 index 0000000..27b5eb1 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/web/HelloControllerTest.java @@ -0,0 +1,39 @@ +// Copyright 2021 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. + +package com.example.simple.web; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@SpringBootTest +@AutoConfigureMockMvc +public class HelloControllerTest { + + @Autowired + private MockMvc mvc; + + @Test + public void getHello() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/delivery-platform/resources/repos/cluster-config/.gitignore b/delivery-platform/resources/repos/cluster-config/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/delivery-platform/resources/repos/cluster-config/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/delivery-platform/resources/repos/cluster-config/dev/system/repo.yaml b/delivery-platform/resources/repos/cluster-config/dev/system/repo.yaml new file mode 100644 index 0000000..ff73b49 --- /dev/null +++ b/delivery-platform/resources/repos/cluster-config/dev/system/repo.yaml @@ -0,0 +1,20 @@ +# Copyright 2021 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. + +apiVersion: configmanagement.gke.io/v1 +kind: Repo +metadata: + name: repo +spec: + version: 1.0.0 \ No newline at end of file diff --git a/delivery-platform/resources/repos/cluster-config/prod/system/repo.yaml b/delivery-platform/resources/repos/cluster-config/prod/system/repo.yaml new file mode 100644 index 0000000..ff73b49 --- /dev/null +++ b/delivery-platform/resources/repos/cluster-config/prod/system/repo.yaml @@ -0,0 +1,20 @@ +# Copyright 2021 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. + +apiVersion: configmanagement.gke.io/v1 +kind: Repo +metadata: + name: repo +spec: + version: 1.0.0 \ No newline at end of file diff --git a/delivery-platform/resources/repos/cluster-config/stage/system/repo.yaml b/delivery-platform/resources/repos/cluster-config/stage/system/repo.yaml new file mode 100644 index 0000000..ff73b49 --- /dev/null +++ b/delivery-platform/resources/repos/cluster-config/stage/system/repo.yaml @@ -0,0 +1,20 @@ +# Copyright 2021 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. + +apiVersion: configmanagement.gke.io/v1 +kind: Repo +metadata: + name: repo +spec: + version: 1.0.0 \ No newline at end of file diff --git a/delivery-platform/resources/repos/shared-kustomize/.gitignore b/delivery-platform/resources/repos/shared-kustomize/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/delivery-platform/resources/repos/shared-kustomize/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/delivery-platform/resources/repos/shared-kustomize/golang/deployment.yaml b/delivery-platform/resources/repos/shared-kustomize/golang/deployment.yaml new file mode 100644 index 0000000..080de4c --- /dev/null +++ b/delivery-platform/resources/repos/shared-kustomize/golang/deployment.yaml @@ -0,0 +1,48 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + template: + metadata: + name: app + spec: + containers: + - name: app + image: app + resources: + limits: + memory: "512Mi" + cpu: "500m" + env: + - name: ENVIRONMENT + value: base + - name: LOG_LEVEL + value: info + readinessProbe: + initialDelaySeconds: 1 + periodSeconds: 1 + httpGet: + path: /healthz + port: 8080 + ports: + - name: http + containerPort: 8080 diff --git a/delivery-platform/resources/repos/shared-kustomize/golang/kustomization.yaml b/delivery-platform/resources/repos/shared-kustomize/golang/kustomization.yaml new file mode 100644 index 0000000..8c06dd4 --- /dev/null +++ b/delivery-platform/resources/repos/shared-kustomize/golang/kustomization.yaml @@ -0,0 +1,17 @@ +# Copyright 2021 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. + +resources: + - service.yaml + - deployment.yaml diff --git a/delivery-platform/resources/repos/shared-kustomize/golang/service.yaml b/delivery-platform/resources/repos/shared-kustomize/golang/service.yaml new file mode 100644 index 0000000..5807847 --- /dev/null +++ b/delivery-platform/resources/repos/shared-kustomize/golang/service.yaml @@ -0,0 +1,25 @@ +# Copyright 2021 Google Inc. All rights reserved. +# +# 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. + +kind: Service +apiVersion: v1 +metadata: + name: app +spec: + type: ClusterIP + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP diff --git a/delivery-platform/resources/repos/shared-kustomize/java/deployment.yaml b/delivery-platform/resources/repos/shared-kustomize/java/deployment.yaml new file mode 100644 index 0000000..9ee5d17 --- /dev/null +++ b/delivery-platform/resources/repos/shared-kustomize/java/deployment.yaml @@ -0,0 +1,51 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + template: + metadata: + name: app + spec: + containers: + - name: app + image: app + resources: + limits: + memory: "512Mi" + cpu: "500m" + env: + - name: ENVIRONMENT + value: base + - name: LOG_LEVEL + value: info + readinessProbe: + tcpSocket: + port: 8080 + periodSeconds: 15 + livenessProbe: + periodSeconds: 15 + httpGet: + path: /health + port: 8080 + ports: + - name: http + containerPort: 8080 diff --git a/delivery-platform/resources/repos/shared-kustomize/java/kustomization.yaml b/delivery-platform/resources/repos/shared-kustomize/java/kustomization.yaml new file mode 100644 index 0000000..78941d5 --- /dev/null +++ b/delivery-platform/resources/repos/shared-kustomize/java/kustomization.yaml @@ -0,0 +1,17 @@ +# Copyright 2021 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. + +resources: + - service.yaml + - deployment.yaml \ No newline at end of file diff --git a/delivery-platform/resources/repos/shared-kustomize/java/service.yaml b/delivery-platform/resources/repos/shared-kustomize/java/service.yaml new file mode 100644 index 0000000..89f0c04 --- /dev/null +++ b/delivery-platform/resources/repos/shared-kustomize/java/service.yaml @@ -0,0 +1,25 @@ +# Copyright 2021 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. + +kind: Service +apiVersion: v1 +metadata: + name: app +spec: + type: ClusterIP + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP \ No newline at end of file diff --git a/delivery-platform/scripts/app.sh b/delivery-platform/scripts/app.sh new file mode 100755 index 0000000..18696b8 --- /dev/null +++ b/delivery-platform/scripts/app.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +### +# +# Used for app oboarding and termination. +# App creation clones the templates repo then copies the +# desierd template folder to a temporary workspace. The script +# then substitutes place holder values for actual values creates a +# new remote remote and pushes the initial version. +# Additonally if ACM is in use this script adds appropriate namespace +# configurations to ensure the app is managed by ACM. +# +# USAGE: +# app.sh +# +### + + +create () { + APP_NAME=${1:-"my-app"} + APP_LANG=${2:-"golang"} + BASE_PATH=${3:-""} + + # Ensure the git vendor script is set + if [[ -z "$GIT_CMD" ]]; then + echo "GIT_CMD not set - exiting" 1>&2 + exit 1 + fi + + printf 'Creating application: %s \n' $APP_NAME + + # Create an instance of the template. + cd $WORK_DIR/ + git clone -b main $GIT_BASE_URL/$APP_TEMPLATES_REPO app-templates + rm -rf app-templates/.git + cd app-templates/${APP_LANG} + + ## Insert name of new app + + + find . -name kustomization.yaml -exec sed -i "s/namePrefix:.*/namePrefix: ${APP_NAME}-/g" {} \; + find . -name kustomization.yaml -exec sed -i "s/ app:.*/ app: ${APP_NAME}/g" {} \; + + + ## Insert image name of new app + find . -name deployment.yaml -exec sed -i "s/image: app/image: ${APP_NAME}/g" {} \; + find . -name skaffold.yaml -exec sed -i "s/image: app/image: ${APP_NAME}/g" {} \; + + + ## Create and push to new repo + git init + git checkout -b main + git symbolic-ref HEAD refs/heads/main + $BASE_DIR/scripts/git/${GIT_CMD} create ${APP_NAME} + git remote add origin $GIT_BASE_URL/${APP_NAME} + git add . && git commit -m "initial commit" + git push origin main + + + # Configure Build + create_cloudbuild_trigger ${APP_NAME} + + + # Configure Deployment + + ## Add App Namespace if using config manager + if [[ ${ACM_IN_USE} ]]; then + for dir in k8s/* ; do + #if [ -d "$dir" ]; then + echo ---adding ${dir##*/} + addAcmEntry ${dir##*/} + #fi + done + fi + + # Initial deploy + cd $WORK_DIR/app-templates/${APP_LANG} + echo "v1" > version.txt + git add . && git commit -m "v1" + git push origin main + git tag v1 + git push origin v1 + + # Cleanup + cd $BASE_DIR + rm -rf $WORK_DIR/app-templates +} + + + +delete () { + echo 'Destroy Application' + APP_NAME=${1:-"my-app"} + BASE_PATH=${2:-""} + $BASE_DIR/scripts/git/${GIT_CMD} delete $APP_NAME + + # Remove any orphaned hydrated directories from other processes + rm -rf $WORK_DIR/$APP_NAME-hydrated + + if [[ ${ACM_IN_USE} ]]; then + cd $WORK_DIR/ + git clone -b main $GIT_BASE_URL/$CLUSTER_CONFIG_REPO acm-repo + cd acm-repo + for dir in * ; do + #if [ -d "$dir" ]; then + echo ---deleting ${dir##*/} + + echo "Do ACM Stuff" + + rm -rf ${dir##*/}/namespaces/${APP_NAME} + + #fi + done + git add . && git commit -m "Removing app: ${APP_NAME}" && git push origin main + cd $BASE_DIR + rm -rf $WORK_DIR/acm-repo + fi + + # Delete secret + SECRET_NAME=${APP_NAME}-webhook-trigger-secret + gcloud secrets delete ${SECRET_NAME} -q + + # Delete trigger + TRIGGER_NAME=${APP_NAME}-webhook-trigger + gcloud alpha builds triggers delete ${TRIGGER_NAME} -q + +} + + +addAcmEntry(){ + + ENV=${1:-"dev"} + + + #for env in ${APP_NAME}/k8s + echo "Do ACM Stuff" + + cd $WORK_DIR/ + git clone -b main $GIT_BASE_URL/$CLUSTER_CONFIG_REPO acm-repo + cd acm-repo + mkdir -p ${ENV}/namespaces/${APP_NAME} + cat < ${ENV}/namespaces/${APP_NAME}/namespace.yaml +apiVersion: v1 +kind: Namespace +metadata: + name: ${APP_NAME} + labels: + istio-injection: enabled + +EOF + git add . && git commit -m "Adding app: ${APP_NAME}" && git push origin main + cd $BASE_DIR + rm -rf $WORK_DIR/acm-repo +} + + + +create_cloudbuild_trigger () { + APP_NAME=${1:-"my-app"} + ## Project variables + if [[ ${PROJECT_ID} == "" ]]; then + echo "PROJECT_ID env variable is not set" + exit -1 + fi + if [[ ${PROJECT_NUMBER} == "" ]]; then + echo "PROJECT_NUMBER env variable is not set" + exit -1 + fi + + ## API Key + if [[ ${APP_LANG} == "" ]]; then + echo "APP_LANG env variable is not set" + exit -1 + fi + + ## API Key + if [[ ${API_KEY_VALUE} == "" ]]; then + echo "API_KEY_VALUE env variable is not set" + exit -1 + fi + + + ## Create Secret + SECRET_NAME=${APP_NAME}-webhook-trigger-secret + SECRET_VALUE=$(sed "s/[^a-zA-Z0-9]//g" <<< $(openssl rand -base64 15)) + SECRET_PATH=projects/${PROJECT_NUMBER}/secrets/${SECRET_NAME}/versions/1 + printf ${SECRET_VALUE} | gcloud secrets create ${SECRET_NAME} --data-file=- + gcloud secrets add-iam-policy-binding ${SECRET_NAME} \ + --member=serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-cloudbuild.iam.gserviceaccount.com \ + --role='roles/secretmanager.secretAccessor' + + ## Create CloudBuild Webhook Endpoint + REPO_LOCATION=https://github.com/${GIT_USERNAME}/${GIT_REPO_NAME} + + TRIGGER_NAME=${APP_NAME}-webhook-trigger + BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/cloudbuild.yaml + + ## Setup Trigger & Webhook + gcloud alpha builds triggers create webhook \ + --name=${TRIGGER_NAME} \ + --repo=${REPO_LOCATION} \ + --substitutions='_APP_NAME='${APP_NAME}',_APP_REPO=$(body.repository.git_url),_CONFIG_REPO='${GIT_BASE_URL}'/'${CLUSTER_CONFIG_REPO}',_DEFAULT_IMAGE_REPO='${IMAGE_REPO}',_KUSTOMIZE_REPO='${GIT_BASE_URL}'/'${SHARED_KUSTOMIZE_REPO}',_REF=$(body.ref)' \ + --branch='*' \ + --inline-config=$BUILD_YAML_PATH \ + --secret=${SECRET_PATH} + + + + ## Retrieve the URL + WEBHOOK_URL="https://cloudbuild.googleapis.com/v1/projects/${PROJECT_ID}/triggers/${TRIGGER_NAME}:webhook?key=${API_KEY_VALUE}&secret=${SECRET_VALUE}" + + ## Create Github Webhook + $BASE_DIR/scripts/git/${GIT_CMD} create_webhook ${APP_NAME} $WEBHOOK_URL + +} + + +# execute function matching first arg and pass rest of args through +$1 $2 $3 $4 $5 diff --git a/delivery-platform/scripts/common/clearvars.sh b/delivery-platform/scripts/common/clearvars.sh new file mode 100755 index 0000000..8f00036 --- /dev/null +++ b/delivery-platform/scripts/common/clearvars.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +unset GIT_TOKEN +unset GIT_PROVIDER +unset GIT_USERNAME +rm -f workdir/state.env + diff --git a/delivery-platform/scripts/common/manage-state.sh b/delivery-platform/scripts/common/manage-state.sh new file mode 100644 index 0000000..ae9645d --- /dev/null +++ b/delivery-platform/scripts/common/manage-state.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +function load_state() { + mkdir -p $WORK_DIR + touch $WORK_DIR/state.env + source $WORK_DIR/state.env +} + +function write_state() { + mkdir -p $WORK_DIR + rm -f $WORK_DIR/state.env + echo "# Updated $(date)" > $WORK_DIR/state.env + echo "export GIT_PROVIDER=${GIT_PROVIDER}" >> $WORK_DIR/state.env + echo "export GIT_USERNAME=${GIT_USERNAME}" >> $WORK_DIR/state.env + echo "export GIT_TOKEN=${GIT_TOKEN}" >> $WORK_DIR/state.env + echo "export GIT_BASE_URL=${GIT_BASE_URL}" >> $WORK_DIR/state.env + echo "export GIT_CMD=${GIT_CMD}" >> $WORK_DIR/state.env + echo "export API_KEY_VALUE=${API_KEY_VALUE}" >> $WORK_DIR/state.env + +} diff --git a/delivery-platform/scripts/common/set-apikey-var.sh b/delivery-platform/scripts/common/set-apikey-var.sh new file mode 100755 index 0000000..0778987 --- /dev/null +++ b/delivery-platform/scripts/common/set-apikey-var.sh @@ -0,0 +1,13 @@ +source ${BASE_DIR}/scripts/common/manage-state.sh + +if [[ ${API_KEY_VALUE} == "" ]]; then + echo "" + echo "No API Key found. Please generate a key from the URL and paste it at the prompt." + echo "Goto https://console.cloud.google.com/apis/credentials > 'Create Credentials' > 'API Key'" + echo "As a best practice, you can restrict the key to Cloud Build API under 'API restrictions' > 'Restrict key'" + echo "" + printf "Paste your API Key here and press enter: " && read keyval + export API_KEY_VALUE=${keyval} +fi + +write_state \ No newline at end of file diff --git a/delivery-platform/scripts/deliver.sh b/delivery-platform/scripts/deliver.sh new file mode 100755 index 0000000..eb4105a --- /dev/null +++ b/delivery-platform/scripts/deliver.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +# inputs +# app-name +# target-env +# hydrated_repo_path + +APP_NAME=${1:-"my-app"} +TARGET_ENV=${2:-"dev"} +HYDRATED_REPO_PATH=${3} + + +cd $WORK_DIR +git clone -b main $GIT_BASE_URL/${HYDRATED_CONFIG_REPO} +cd ${HYDRATED_CONFIG_REPO} +mkdir -p ${HYDRATED_REPO_PATH} +cp ${WORK_DIR}/${APP_NAME}-hydrated/${TARGET_ENV}/resources.yaml \ + ${WORK_DIR}/${HYDRATED_CONFIG_REPO}/${HYDRATED_REPO_PATH} + +git add . && git commit -m "Deploying new image ${IMAGE_ID}" +git push origin main + + +cd ${BASE_DIR} +rm -rf ${WORK_DIR}/${HYDRATED_CONFIG_REPO} +#rm -rf ${WORK_DIR}/${APP_NAME}-hydrated diff --git a/delivery-platform/scripts/git/gcsr.sh b/delivery-platform/scripts/git/gcsr.sh new file mode 100755 index 0000000..6688a1b --- /dev/null +++ b/delivery-platform/scripts/git/gcsr.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +action=$1 +repo=$2 +base=https://source.developers.google.com/p/${PROJECT_ID}/r/ + +if [[ $action == 'create' ]]; then + # Create + gcloud source repos create ${repo} + echo "Created ${repo}" +fi + +if [[ $action == 'delete' ]]; then + # Delete + echo "Deleting https://source.developers.google.com/p/${PROJECT_ID}/r/${repo}" + gcloud source repos delete ${repo} --quiet + echo "Deleted ${repo}" +fi + + diff --git a/delivery-platform/scripts/git/gh.sh b/delivery-platform/scripts/git/gh.sh new file mode 100755 index 0000000..88678e7 --- /dev/null +++ b/delivery-platform/scripts/git/gh.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + +## Input Validation +if [[ ${GIT_TOKEN} == "" ]]; then + echo "GIT_TOKEN variable not set. Please rerun the env script" + exit -1 +fi +if [[ ${GIT_USERNAME} == "" ]]; then + echo "GIT_USERNAME variable not set. Please rerun the env script" + exit -1 +fi +if [[ ${API_KEY_VALUE} == "" ]]; then + echo "API_KEY_VALUE variable not set. Please rerun the env script" + exit -1 +fi +if [[ $1 == "create_webhook" ]]; then + if [[ $2 == "" || $3 == "" ]]; then + echo "Missing parameters" + echo "Usage: gh create_webhook " + exit -1 + fi +fi + +if [[ $2 == "" || $1 == "" ]]; then + echo "Usage: gh [webhook_url]" + exit -1 +fi + +## Local variables +action=$1 +repo=$2 +WEBHOOK_URL=$3 +GIT_API_BASE="https://api.github.com" + +export GIT_ASKPASS=$BASE_DIR/common/ghp.sh + +## Execution +create_webhook () { + curl -H "Authorization: token ${GIT_TOKEN}" \ + -d '{"config": {"url": "'${WEBHOOK_URL}'", "content_type": "json"}}' \ + -X POST ${GIT_API_BASE}/repos/${GIT_USERNAME}/${repo}/hooks +} + + +if [[ $action == 'create' ]]; then + # Create + curl -H "Authorization: token ${GIT_TOKEN}" ${GIT_API_BASE}/user/repos -d '{"name": "'"${repo}"'"}' > /dev/null + echo "Created ${repo}" + + #TODO: Check if repo exists first +fi + +if [[ $action == 'delete' ]]; then + # Delete + echo + echo "Deleting ${GIT_API_BASE}/repos/${GIT_USERNAME}/${repo}" + curl -H "Authorization: token ${GIT_TOKEN}" -X "DELETE" ${GIT_API_BASE}/repos/${GIT_USERNAME}/${repo} + echo "Deleted ${repo}" +fi + +if [[ $action == 'create_webhook' ]]; then + # Webhook + create_webhook +fi + diff --git a/delivery-platform/scripts/git/git-ask-pass.sh b/delivery-platform/scripts/git/git-ask-pass.sh new file mode 100755 index 0000000..6d5aa12 --- /dev/null +++ b/delivery-platform/scripts/git/git-ask-pass.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +exec echo "${GIT_TOKEN}" \ No newline at end of file diff --git a/delivery-platform/scripts/git/gl.sh b/delivery-platform/scripts/git/gl.sh new file mode 100755 index 0000000..42c49ef --- /dev/null +++ b/delivery-platform/scripts/git/gl.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + + +if [[ ${GL_TOKEN} == "" ]]; then + echo "GL_TOKEN variable not set. Please rerun the env script" + exit -1 +fi +if [[ ${GITLAB_USERNAME} == "" ]]; then + echo "GITLAB_USERNAME variable not set. Please rerun the env script" + exit -1 +fi +if [[ $2 == "" || $1 == "" ]]; then + echo "Usage: gl " + exit -1 +fi +action=$1 +repo=$2 +user=$GITLAB_USERNAME +token=${GL_TOKEN} +base=https://gitlab.com/api/v4 + + + +if [[ $action == 'create' ]]; then + # Create + curl -H "Private-Token: ${token}" -H "Content-Type:application/json" ${base}/projects/ -d '{"name":"'$repo'"}' > /dev/null + echo "\nCreated ${repo}" + + #TODO: Check if repo exists first +fi + +if [[ $action == 'delete' ]]; then + # Delete + echo "Deleting ${user}%2f${repo}" + curl -H "Private-Token: ${token}" -X "DELETE" ${base}/projects/${user}%2f${repo} + echo "\nDeleted ${repo}" +fi diff --git a/delivery-platform/scripts/git/set-git-env.sh b/delivery-platform/scripts/git/set-git-env.sh new file mode 100755 index 0000000..1199759 --- /dev/null +++ b/delivery-platform/scripts/git/set-git-env.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +source ${BASE_DIR}/scripts/common/manage-state.sh +# Set Git provider as either GitHub, GitLab, or Cloud Source Repository +if [[ ${GIT_PROVIDER} == "" ]]; then + PS3="Select a Git Provider: " + select provider in GitHub GitLab Cloud-Source-Repository; do + case $provider in + "GitHub") + echo "you chose GitHub"; + export GIT_PROVIDER=GitHub + export GIT_CMD=gh.sh + break + ;; + "GitLab") + echo "you chose GitLab"; + export GIT_PROVIDER=GitLab + export GIT_CMD=gl.sh + break + ;; + "Cloud-Source-Repository") + echo "you chose Cloud Source Repository"; + export GIT_PROVIDER=Cloud-Source-Repository + export GIT_CMD=gcsr.sh + break + ;; + esac + done +fi + + +# Set Username to use with git +if [[ ${GIT_USERNAME} == "" ]] && [[ ${GIT_PROVIDER} != "Cloud-Source-Repository" ]]; then + printf "Enter your ${GIT_PROVIDER} username: " && read ghusername + export GIT_USERNAME=${ghusername} +fi + + + +# Set Personal Access Tokens to use for operations +if [[ ${GIT_TOKEN} == "" ]] && [[ ${GIT_PROVIDER} != "Cloud-Source-Repository" ]]; then + + if [[ ${GIT_PROVIDER} == "GitHub" ]]; then + echo "" + echo "No Github token found. Please generate a token from the following URL and paste it below." + echo "https://github.com/settings/tokens/new?scopes=repo%2Cread%3Auser%2Cread%3Aorg%2Cuser%3Aemail%2Cwrite%3Arepo_hook%2Cdelete_repo" + printf "Paste your token here and press enter: " && read ghtoken + export GIT_TOKEN=${ghtoken} + fi + + if [[ ${GIT_PROVIDER} == "GitLab" ]]; then + echo "" + echo "No GitLab token found. Please generate a token from the GitLab UI" + echo 'Provide a name and choose the "API" scope from the following URL:' + echo "https://gitlab.com/profile/personal_access_tokens" + printf "Once completed paste your token here and press enter: " && read ghtoken + export GIT_TOKEN=${ghtoken} + fi +fi + + +# Set Git URL +if [[ ${GIT_PROVIDER} == "GitHub" ]]; then + export GIT_BASE_URL=https://${GIT_USERNAME}@github.com/${GIT_USERNAME} +fi + +if [[ ${GIT_PROVIDER} == "GitLab" ]]; then + export GIT_BASE_URL=https://${GIT_USERNAME}@gitlab.com/${GIT_USERNAME} +fi + +if [[ ${GIT_PROVIDER} == "Cloud-Source-Repository" ]]; then + if [[ ${PROJECT_ID} == "" ]]; then + echo "" + echo "PROJECT_ID is not set. Please make sure to source the env.sh script." + exit -1 + fi + export GIT_BASE_URL=https://source.developers.google.com/p/${PROJECT_ID}/r/ +fi + +write_state diff --git a/delivery-platform/scripts/hydrate.sh b/delivery-platform/scripts/hydrate.sh new file mode 100755 index 0000000..d6436e4 --- /dev/null +++ b/delivery-platform/scripts/hydrate.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +# inputs +# app-name +# target-env +# + +# outputs: +# ${WORK_DIR}/${APP_NAME}-hydrated/${TARGET_ENV}/resources.yaml + +APP_NAME=${1:-"my-app"} +TARGET_ENV=${2:-"dev"} + + +cd $WORK_DIR +git clone -b main $GIT_BASE_URL/${APP_NAME} +git clone -b main $GIT_BASE_URL/${SHARED_KUSTOMIZE_REPO} kustomize-base + +### Hydrate +cd ${WORK_DIR}/${APP_NAME}/k8s/${TARGET_ENV} +## use Git Commit sha +COMMIT_SHA=$(git rev-parse --short HEAD) +kustomize edit set image app=${IMAGE_REPO}/${APP_NAME}:${COMMIT_SHA} +IMAGE_ID=${COMMIT_SHA} + +## -OR- use image sha +##docker build --tag ${IMAGE_REPO}/${APP_NAME}:${COMMIT_SHA} . +##docker push ${IMAGE_REPO}/${APP_NAME}:${COMMIT_SHA} +##IMAGE_SHA=$(gcloud container images describe ${IMAGE_REPO}/${APP_NAME}:${COMMIT_SHA} --format='value(image_summary.digest)') +#kustomize edit set image app=${IMAGE_REPO}/${APP_NAME}@${IMAGE_SHA} +#IMAGE_ID=${IMAGE_SHA} + +mkdir -p ${WORK_DIR}/${APP_NAME}-hydrated/${TARGET_ENV} +kustomize build . -o ${WORK_DIR}/${APP_NAME}-hydrated/${TARGET_ENV}/resources.yaml + + +cd ${BASE_DIR} +rm -rf ${WORK_DIR}/${APP_NAME} +rm -rf ${WORK_DIR}/kustomize-base +rm -rf ${WORK_DIR}/${HYDRATED_CONFIG_REPO} + + + diff --git a/labs/cloudrun-progression/README.md b/labs/cloudrun-progression/README.md deleted file mode 100644 index 3bc835b..0000000 --- a/labs/cloudrun-progression/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Serverless Software Delivery with CloudRun and CloudBuild - -CloudRun provides an easy way to deploy and run your applications with little overhead or effort. Many organizations utilize robust release pipelines for moving code into production. CloudRun provides unique traffic management capabilities allowing you to implement advanced release management techniques with little effort. - - -In this tutorial you'll implement a deployment pipeline for CloudRun that implements progression of code from developer branches to production with automated canary testing and percentage based traffic management. - -## Objectives - -- Create your CloudRun Service -- Enable Dynamic Developer Deployments -- Automate Canary Testing -- Release to Production - -## Preparing your environment -## Creating your CloudRun Service -## Enabling Dynamic Developer Deployments -## Automating Canary Testing -## Releasing to Production \ No newline at end of file diff --git a/labs/gke-progression/README.md b/labs/gke-progression/README.md new file mode 100644 index 0000000..15cc067 --- /dev/null +++ b/labs/gke-progression/README.md @@ -0,0 +1,30 @@ +# Software Delivery Workshop + + +This repository contains resources and materials targeted toward Software Delivery on Google Cloud. In addition to separate stand alone guides, an opinionated yet modular platform is provided to demonstrate software delivery practices. In contains scripts to standup a base platform infrastructure as well as other resources designed to facilitate hands on workshop and standard demo use cases. The platform provisioning resources are structured to be modular in nature supporting various runtime and tooling configurations. Ideally users can utilize their own choice of tooling for: Provisioning, Source Code Management, Templating, Build Engine, Image Storage and Deploy tooling. + + +## Usage + +This set of resources contains materials to provision the platform, deliver short demonstrations and facilitate hands on workshops. + +### Workshop +The Software Delivery Workshop contains materials for a self led exploration or accompanying instructor led sessions. To get started with either click the button below to open the resources in Google Cloud Shell. + +[![Software Delivery Workshop](http://www.gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/GoogleCloudPlatform/software-delivery-workshop.git&cloudshell_workspace=.&cloudshell_tutorial=delivery-platform/docs/workshop/1.2-provision.md) + + +### Demo +For a mostly automated experience follow the instructions in the `docs\demo` folder. You will run an automated script to fully provision the platform before your demonstration. A separate guide describes the steps to perform during the demonstration and concludes with instructions how to reset the demo or tear down the infrastructure. + +### Provision + +If you just want to install the base platform and run your own exercises and workloads, run the following commands from within the `delivery-platform` directory. + +```shell +gcloud config set project +source ./env.sh +${BASE_DIR}/resources/provision/provision-all.sh +``` + + From 53a6cbf9733f90391d9bcf25a1cc8aa03fc734dc Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Thu, 1 Jul 2021 09:29:21 -0500 Subject: [PATCH 08/50] Software Delivery Workshop --- .gitignore | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ea299ce..3f8e001 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ build_assets/ -TODO.md \ No newline at end of file +TODO.md +workdir/ +_local/ + +terraform.tfvars +backend.tf +.terraform/ +.talismanrc From d65d54a2bc6fc6fb8d1790330ac03bb999f4bfa6 Mon Sep 17 00:00:00 2001 From: xiangshen-dk Date: Thu, 1 Jul 2021 16:02:40 -0400 Subject: [PATCH 09/50] update lab 1.2 --- delivery-platform/docs/images/clusters.png | Bin 0 -> 13819 bytes .../docs/workshop/1.2-provision.md | 46 ++++++++++++------ 2 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 delivery-platform/docs/images/clusters.png diff --git a/delivery-platform/docs/images/clusters.png b/delivery-platform/docs/images/clusters.png new file mode 100644 index 0000000000000000000000000000000000000000..0453f5a2f8ba373d7691ba5f673c98a253ad6a6a GIT binary patch literal 13819 zcmbt*byQbxx96czK*B;w6e&poQIHfQ1nH1&B&9nAB&DQ8y1PSZ5Tua?K@jQg4q-OG zcg?zY?z;1?nRUN^AnM_q=RD8e`xAQ;AR{G;ca`KSf*^R}V#0C=f+7gtFJPm>*A~=d zANT{!=DD~6Ha7OJY3V=kS1o6S*S2yx4wRPG7KX-V29&l=mIjo9GBS@41Uq*2rF4%3 zQ>Ua79bKn%SGV}Q9D=5Io%y=Nb%``#G&D3EG;CH5R#r~*k0JtIzaExD1a8keCc5yLvMfe9Iha#(R4FzMKfXm03 zq4MR;uv84$mF;GnsJg}S&}d&4)JyQzfeD8#tFrF}Kr2rlNXtTvt=>ky_Xv3CmR z>tbj7f2uF$b2v2$>}Aii_Q zY4JybY???)s_j-5ox)n3$NP_8VQjdNo0bDp-V_ zoxQ80k+K7ay4e3^XD{U>0vBP^Fo)G)FBH-#v zr?by~zt@X?>3yxrO-02+RxnZ}@VSU;Jf2n424f;JO4O1)`DKq4AFt=3BPtkY$`;fN z>nUP3zq?sj&~P5F8&_sC$_eV zI5->@7qf1YZ~m@Zou4;&`*vev!~J;2VEapFSC_-u&qpRhbXZt^$;ruqfmb6n6&2?< zHqKXz8qRmRSnTcXQ&Ljs4B%%Mdy^a-9G*OR;$UOraVkjU|{&G>`iD*&1sFpW_x5<7y&%%#yv#6+P0&or>CW5u8XDN?07MWi%g0k z9OwPFr6nV2X=w=w2`#OuH@%4!wgX6VO3FPpHtV&YB{VcNg@uLv{r#_Ay<#H2_QJ_o zdutHQd%nYI8D&w@q~d+bOZBh)Rlzi^Zjrhc^+h?RBAe$+1u@lKt`|G!pG~e_p;y%R z=C{svUSYP`36;V^j7(@`RzJt~7G)cwdd_P)(|NB8C?a3U9S&yKVPIcfS z3yF%li4YSLFRiSknwfFNQTor!=x)p0W>%+5)HO8p-9c-FU6C=4jg6)I{_Pv8x#VY# z080NqfBsm_wxS>r9q}B-1N~{DYDLvKIal7Bn3-}$p$ks zGt<4%)xFR4=MCJm^6%fjD1V0Yl;`TRuks~H6SPb>`}Nn)wg&65#Uk1k7AyqV`XbCk z0Si5GencHqb#AAX-4P-LKB1xbogV^W6S`u$cKaRF)=*2S&sC*w(;DG-F;kuz2=Y3| ze(p#h`s}dn-n0Bn(H>=NaYHF>58@lX{W8Gy;wBz^lZSsx{I=aLeH%07vpvfa4S|EE zAhYc+x7J-vZ-;4gnLcMxqo1QP_V%T^f4{HoVlm0%rGA)b^Xh1kD2u$DT$mXNDQSR5 zgXS~)HG-?}pQmhPCAp(|LfkxGTXo!-6OW9QRC^PIq^=$UTzmDGD*XNn; zxcQsci}ESik96?}ms=6)>u8>F$f7k{3|Mj&tDy=?T%1VQu38@ z6cX}w#H0wN?ai9vJtoErRSTajUQ#R*V8Gr}54ZE!Y5;@HcZrA`o!IxT56q3aWQ%Z-w-aCQU1&Zq9W8u8s@MAA z5j#5rGqd~tilU8;jk-EnM2C8*en9HwM^`|=Q~0KQrerLa4UKmbfpjdVISE2XN9XA5 zjEV$(NA+wtKgml?{np%!R^dlNLV|OP5kX=n|BU2q^rvAVkx@~@Lqka8P>wuodZon_ zvxbzM91+r;@GM3o4W+SIr}_Ew=kf9JzZ*QJGI!JR^KlW2-?jI!y_&QiKo-){s;R1W zcDpVQP$SqlI6_PZqgo#3hK2`o9oKIZw1^8`aFJb;vzV%POX78jj*mZc`e2>L%rO=e z?MNTNT`#8bUdm!x#MZ`adFy@Af*F1O3@1}uVOGI*?l8W!?rBoFsG8ZjY)Iyc2!$Pyn^;8K7385H&`0&Bj$_jSEFzz5DX#X#* zI5h&C~%oc5Obr`!$`3JMCUs+>+u)(5ljk*(vi&d!&JHd`zl>Rb(c2@G7n!zxN8 zcXDzvlcA!j8i?~DeLq)GQL#v?!F{1S*2l*O2MSoNV|a(YW{rK%*qBN!ffpqrAS${z z`-=mRezZs{iQ8^vt|Nl<2}HD2T0_G{lQ&idi_Y_uu&()3qwiFH9;7zLm1bjX2Az?E zS<-E7Z7dqqw#x$$h%>3z)MUm)^|(pcwd&BxzwwM2#KgPbldo zzNS8w(^76;ABy&e$Af<*m=J#+EvqFWm#P>|iTQY`0!+_p=l)sceI!{Ri<)=#D#?pZ zUhe(vHcl~&3gtAB;PGlZQ%lQYz$_@$?N!vwF~24z7#EeRl9LhSWPjDKB|j&}>15CN zL==A@WwWHLY_~+OjqlS~Ute)l?wA>8Qt#ox!TCnb=2!I%D6k%9+ieD2(Y)?wuX*Ai zKTArifal1{$~L(FmC?1bvQktW{?p|B{q*9&{ytl^+Vhm+@^S@bWrv>6k4;DNJKEc~ z4i7^^LJkfNM9>UpSc!2JmMhJFVO}Gn+b%r%`oybiUP)Q`*VtGv8JBfi2zf_GM+D6) z)pDc$4|mwpU2a`qoYA;jty`^!o(A!X9_Z5zNKNG^B@fb!nWop}Q|%r}_JvfdI?X6W zO|`JdynJibyiIyh!(aMHwjld0iD~nh-8BFDNyc%w1mR8we&I$J4c35$>afcZPU%Jq2169ui0Dk(595U7I@V?lOy_Pot87khEh zKw`4{-+kx!I<IV2TOUdT}K49|O5 zC#>XOL9*mcTe`x(u1C|Q3m(xMp3mPoy1y9R9(lMqMM_MJ2q-Ar5G3HgIAgy=|9rJC z*Ls{h^Q$u6*~tmd^^~#K&5N};JZLMIM9GC7PvSRK{9qjTqIw$ z;^oQQja#=;p?W`WCu#ont*?fXlQ|f3E>?1()Syc#|FxjhhZL*jfa|oVh{;3c0@e4J zd7AaE^z1tho4-?VZ5DsU1>V8P5Cf6%D6OiBrNka$isy6Vj+N)e;FTdHAqmvveEb-AFOHi* zxO;n!su$&tB>qrl-gocb6)3+V5C**B+BY#U*dLVUui&^75f$~~$Gz2+m2+T4M!Er% z*hq14F{KTZpm3fmgPE^LIn9U>Y;;sFN=KgGUKlv-?SSs#>S}>JEG(h%Yp|2QfBzmH z9%hp!6ePgFz|c3;(3t!lbURI)ab-uGRR&>BW@hHh%uK!8X=kl0 z6bnLZ^g5Tr^9N0k42MhoLMQIhhPVX^8p_IT^Va+O`~0t|Lfb&9+}{jTgYr=F_3LXb zEv*+XUYuxnQ3eJD?eFcS`;`JSBf$P?W}n38Hg_jLz+v+5$$pyK8JfFBaBYJVt4xU6 zZtTUvw7dzicatrjBOZe5JO1B_HM|j4Od<1P-$%qI-j6>t$&lvj{6Am%{fotvy&Wzw zZsy-Vc08Fj zutVD*bK)HXfy{$K`CaD%2gS?F>+R)5bp3i)XQvT|Di%gqL24H#TaImm7n?f_e@@C<=7I&6`N0 zVHubJygWR7>sU%EDtGSQwJaI{WfkS+VQ1K0MsMFL$;f2qCpY@5kf#n zxV^VL2xNrEdCzELq6$C^DR$ajfbtL-8M!^%hEK`~Jnao=v1?banmF*x&(DMW5O{&o z*d9&=^e>J|G^EyP7ZZV625he=HP!lfXMSi%0bylh+u7ZfS60^g@-`tcks|23wVho8 ztB#kW@vU38=u}-?PJw3*F57N*&$fj&0L1}Gi~?Ng%}BlqP%NhP_QhR5yW^96Q^eBR zdS`1Zx!*|N7<2}RLc|g@FE}YBuNlqF&4-+-Y!;M0KotY|{{1`b zFd#s}=g;F{2Qh0@0|Uw`EIfxmJ_G?cITgt4gQ}A_^JQyI86P^u|-oy#4!XJETVMn~W1>nGRwJfEKV9+Z%fK%Lt+ zJgiY>7z{-L_#sqhYPKoZdF#>FKKxz~e6#TnZ%e6mgv9sECKh1uQ>}f$$*{1IP+ZSA*j--vNr;pWJtR?o@F0aSx6 zP*zlYB`z+=1uzD_TABU&WdsG?X35RY_VM$psj7k~2Z*|Y?ghjr?$f7ym9o~Jp7n3< zallNOxtZ|P%ByXbNu!#BlG3xs5U4atMoe@x0QX|eX8rYxuW;YKrh4anNa`}+^Yrxe zn^O(=@VRmY!s3CnBSZ1){G+2IU~SGoO$`kV#l^+%PGqjT$+ubK1>ZY)CE& zM^{x<4G9TBwDtA%4Gio+dNap7vsvf{rk~b(4iuVhaRju7*%(Niq0B!v*4F1Ix8-;s zUO#{S3`_u#QBX)v26_(3!M##%XkZW<7spPFn<8UmVsf%F%uqfG#R$qxhRY6>$cMhZ zK1+LhIH}&Qu88*ds3-$-^UEmH&;W#0zUJrs`Pni#xMgK!WlW5W=&0{XORd3$>UhMZ z0M8>?nRp2LYk4n4$PZNjwBWaBh}3|E+WXs|x2Mox}%L+MmXEP_O3vB$((d`PVi#8}!<)mG|hg@qfC2Lqhe$ z16uR57$pO!cCyFCIYQY_8xsM6A2@|?Q>AWAI;oWMi__{V4o*~LWV;2CQ_y)+95cX> zT$27mYU+I~EUa=UNft-0dxw)lZzQnvyU)-3iG2?v3!4cV{ z4Gz&+cP#tPP-IJsEj({Sa+S;^g>D|HQN?floq$hO@IXK!Kav-@m7aESj}Hoo_-t zpV@YVuMb&Ro;-dW<>+v zk9eo;nfJoR93)U-a%yYI?8ve3NYIh@@882CcH;5u?CerxUTbPMvN=$ex;y zbA%1Ik#ZQx0G9gVk%Ir#_TdiT^X==`(GZ9TK0ZD=X$d^?(6(PCdZIw^{RyesnW8)d7 z(W_h1zWRI>mz0wcJoLkjNkrSgKv7fk$Fya7nK)Svpxs23&2=K8%D0n&A!4ZvkHA|3 z8>PKn3@L^*+S)SW#RdBZ-0twnV89q4|IW~6SXo&?!@BlB^pPwvgu#y=Kfw1!SS|ku z?2RQ&ZY4#G~-%V&nkTMebkkH%~~#iN2qy3F>m?@;+3Kd$(3rY(FwT!50& zFaL8~9C)fN-391Ifpi=k3PM6c4~Qy=3WSXdHh=#73FSzY{uLzw_z%Dxy(kYhrz~`I z-YqQ7rKhC4Ytk+sP^d&t>Q$DLGcgv8mG3lvsIHwe9eM7_h*X41Cmwz5l!}SVEN}aJ) z%0-&yZix>bKFp=2o#u5p02lVltBk)6?cLqNA)KRy8WDr37=z}^KkbRHe4@qDH#|0e z(TETL*}i#mKYZIm71;7ZZ_-8EU5_yOJf#AnCtk~BVV+>Y`ZXV~K7!-uF<8)D*vhvpwj4B3H5p)a;yF97Ke$8A?J9Cg=;(pU$sZ2I_G_dPY zVo3T9Rj`RHrWY4)$=?JCQDxXea+&G?(eB&*fl^{^3Jd={WFms5NqO1DUVlfQG`F`k0~RGyz(=P@_hyYi8sg1{kS9 za;|P}HR`QwP`AE^kUw)jU4OFUJUTJqx-yglu;-tIvK>J~6mYOMhK7XR<*&D%4+K)? z?TyCfF>PIyC6%BG?puz0R@h@FP0inlye@2PY+$P0A|++mpnt`cojVPh!1bG1LI)H9 zIeB>?y!R5nf-nHe4xfvyX8h|kl9bppz|ij9yXTFGHx5W?WCT!IP*k)DS4~b#%*@WB zP}0+P^!GpF=5_|m0LeFSUQOhWa|ET0ZX3Q#%CyF?XhF^Uec1#x{{?NF^Yc>+7#`uV24OG( zhV!V=KTsOfI`6MYyXCJx;Nh8QZKZkgqcpiKV)vhIAP zbCAx?)meF-)^EQt0c;BjR!0l4IL!w4$-X`UuCWM>XLASR4IUndKz$3KE-;e%`g7P* zu;g1?Tfyr^^#qkSvieF)3_2AN1gxkXRQK*p50Xu{HO*V^?(DP%la8i|;xjQ70Qj_i z|IX{Ug&=+5Krzbl^ChLFi!vf;1cAsQA4s2ELuf!S>DIC9YfZp2nPVh0@Emd-mT6nM zyCoAjN4mT7uX`CYz&fBck?woNYwO@(q`$w)ZdFNKyaRe9f18N<%zs0?xt}c4M(zk= zS-yJJ15zdWxA}a56y|ZLl7d2V%<=It0RCN8?Iat_Z3MZ=b2$ruTy||x1V9M_P6F4N z<1v<`xsLoa}%B=t1<$GhD1;=H_7+U}8KMlgxn!|oZ8Rv^62dr(CM zc6IL7Rw*hd#Dhr>@(g#1n#1dwZ< zo`_Q^lls@bQ9%LIZaUx(H7%{5k5BSPV~)=i6_1H7z!m_8-W1_-0ROD;$RPiEhiTLZAKV7Ow| z$AJy%8EhghrVvj_W~K z3v`h7-d=vktv|t}Px=UAfWpC{ftCYFF;21Rft^3yi)nJPfC66M+NTkymXLSU(m#Mq znva*~2zfVY^GR%tmxDK#vNA!0=`HwQt-#|g3yKzx`+iN@)ueYRz&K-_yz_7TGuwRM zQ1WM)d3h2MXb?p+X-T{tB;Za)*!!mTpItXdFby zIgX2T{QRkNz3NT9Y8s<<@Rg7=aI`q6cENlP}TM3>}kSLIOmfm5uz^%gf$XAgoaXLTk{@9 zSE3}tluJsApsr}CfEOhM6L=XQL}Y@Y-{S`>sHm`G=u(HD?8414P098BGR1d)^1XJsFC_={i*c+3QuX3w@ik_ckV>(G<_YotKjn%i~lU9UTqyF!e%$?%3|NT)(KZGep9O*$3r5k$AU$Y>@VTAbK;BN(*XehBgh>!TKfgC`-UMxa zjEsa%(EW1q>(_+}-!{O{L-hbmZ2JY4KXA{*?pT|>CD{kE0jQwE&(6*e?bVS2FrkrR zFl4}}0qz}Z1+UT8+6vDCo84ow+AgUtdwUjMYV&sILDUOWDM$C?tkoLiPt>KZ5ff z!-|0>z_h{c?k;e0N&9pluo`M=aWOG5@$s&pTR(ijWnn1-uC`}hZ@c^vM6tKGP)zG@ z+4eOp%c=<=w_wlBg4Ba@3EXyO%=a33=d@*KXD7B7U;_gkU3Px{1MK_k?AMP|Hh+By z?OlX|U<&mGo(D~!Fy_AQZboiy5(NCE`Po^tyQ&5!;N!(GYm9%jnuTgUG&~H+ePP_& z+$J!5I^EE@i!}anE-jm8-6{NZgEiI*Gq2{#W%_4l|18!K0=fXbW44hjsZsfb1@3<+UUPC(o(0#%3DF}#kGq6RW zMt~WCfhwq{mj*E{BH{}*#;>`gwA94NNK9J#aC;ki1~3N&KMB%Kz))faW59@`9C_^B zBw)HB5drZP5rKxl<^ZLD!-5-wRs$(;*Y?YbB%mUCu!vBaQWvw?$6J;TRJ^iBkhvt5=w+zJ+8dTkhylP6L!psjTzcn4v#i4wIiJv9bL_67$WILT<3v?c1^&S}qp9-p@cQ4I?~ zL)x*`q^0o@fdAUs+7@={1n%M9UKSu0;5klA)JmXkpojGYUYgC|GAhZ*0jJ{ylInJ{ z2Nr_$*^w25FPnEo#Xi&k92^{oL31M`$D=KP7Cvw?5do=d(66?&wKc12jQ*@eq^71e z5W5V8AB+aDBjJwVKRmNeCk-x>*?0drW=T9QP#s0Dy=^x64tNHLFkc@Z03RAo&RZ3Y zkR32?1K1AU2n+Ngv9SdxQsMo9Lqi0E{k6Q(_$&<46o588GZT8YTD6XZ)DT0!Y;RQ* zw#Q;ENQ9=Y;;ZeJfO%c`n627fQVpa4ZcLZWM8;tbdiOjCi=1V`n*_2*qT>auNh zVBkwDt4F0;rsQ~d!9Z&OF6S2)lb$(*^e#HwEt1czY-VWdmUgtd^NO6Br$Jpo{vmH? zMoNCYb@y4JzZ542$vWg2sM$x49)a2i$6|?V5UAs3!^LlyUb;gk;KBMnA^SNCZNizeA0-#n=EZ@K_>(FA%EapbxXp^ zu!2Q!u3iO7#&2y`0W)N5`u@;g=|;_xjD_>sh7yX;XUuLr2kNF9Xy&Eu{g9a%Z0wIV z>w6)--+GPRwniLA%O6i}-3f)MifevpfZyRiX18U!`lw~?n84~Z!hXpUPU#QL;Q--d zXe5AX-kx0*g$iUDlrP`UA`zHu0Ef3dwrUV?7X|6LC(hrV0vn<;Q2Lfm8+RdMm+Ndn z2a59{PEyJkjRdd!n`Rw`wm(xm+UE@a0>WElWQ?I7>}+i>h5Mw)qfeDyqZKXCd{SJD z28E3JU*;qKQ%`DldNBiLUw&P&(q{d1@NUuXvJLldnrLe-{!KD*y*hXZe0R(ubX?(> zz3TGwZLyB-n~zaZ6=3XvcO6d8cq>RGO+5el@Aoox8WPF%n>sa3z5#cESW5%nX}O*M z_B{kgZtg<_JZUhMp;M(>@)~-&>wF}cE1zz?aKrEC?f^FkJ~IL6g8sBu-+u!uW7*As zghno*@`U0eG?{`nC1?VFk*DH9#nDd{djPk%EKn9Off+6atsdb-*LMPxI-mSursX9# zGSDbe^{tO;Ue>3=&NcZISnmhL0~b>dNFh1kO5CTwtI}r&kf+cVOhGmFjmuS%GCbgUAG36#Myebyd|p z36%d>F02!LWv~qZjHjyYa5R?F*RRPMawpUS3{y))gGZAA2g1QI)YaA1-o6B!=aRL* zW*iCSKPSF%!xr9hXJ-eFcl~$WGmuiyv6zW+@2f1#t0l~F=9hdiCDZ~y3oD9YRIy&_ zOHupsmWzuEr7>4AFRZs2R4BBu!3{#RVa5;4O_!ay0LZs;otZb1tMz_B;(9tdRA3eF z-k~7q(53g3tPyf?aY43rcAoPlKqsW$X}1-Iqyz=uNo7TJwE+dZHGj!)_xE44W<#lf z)4WbAWyU+S@h4bJ5sxZ+-i5AdYr0wdjpYu>1q2D!fXx;MIE`@a$eg^FgV-Qja+))V$ z;JG`)55h~db4Z@?82N49v*rZZaX;vLXKycb_cWxWLL_UNqaP}Qvz?zmXZe)q%NTey z(E5jdsRYcdzI^$Sc<_W$V?J1H*#0sLusP$*jg3`gWlMmZ>Br*ePF4Znf*4KaZ2SIw z9#r^J<*RYllDwIV^77`xFp)GSl~R4^)WP@!re{(8i3yZTw)JZ;NZS3<+|2Ap!n4~1 z1eE7Z<00d!%!mG`P`K=HT#4R@&^>wgju{uSr~;3VPI_eAdTGuZE+ z^ChuZ@sE6j$PhSn#fQyt*;{(&Jo{k4-j5BW4IKE&3L8cH{I(;I-1YjG_VrKhEB?|s zI=sX9@PvYm7#GvKe*_mQO;Uy@iU0(RY90qkf!(ZJ?Cqv#8VhRh`PeXUk#&2C7q<1M zW^E>~MMeF347;4fg@kUH>-zRY7GVWv25zQ+XAc7yswXSjtm|l}6uK~MUSt{o&HdaT zOggN@=>KMT;a^wx;$IG^A7=bt#~Xeg5`ZQ6udg>WrE$*l>is@l1C}uZycsAJkggl5 zc46NRn~h%a#1=}l9}*PsP4>Vp0OLbiF9kH|fO&n*q4C6@%dDcqC#S-i)ufh!OZ4 zReh|&lf(2U!XQHL9~z2`j#g4sd_m9xWerdfW|2k}^OQgg2Y=TUC#?`vb|mRUgC{`+ zK%UyKy@q~P!_f>5uutfPPSrTzGkGs9EL55dKScb4gJGU?XmF7I(W8LSP*IlzZo5DZ zUKmsVRb@kqz<)_5@~FU6^{ZFr{i!b!%E*psv&ifHpu2b3$^8YiV0wDm$OS+PM)0sy zF#QKGV9N36_!y-T+IWzq@1IWtGYKcXrplx6JWpfZm=0fI_%f%!S>kutm;eb2F9V$M z3~VzntJ_<)K*>U_g3c!>gdl(aKh{-Hjrq^Egn$u?J-CXCySKaB)!RD-KB2oi-$Qe# zv4Bg^{e|f^F!P~Uiikeshh)FBlu92{Vc!Rj2Bz6dOFvTjgWtlyzz}BONvDw<8>?b# z%W*mV)Cq(II)&eSu4JE;mvbDAt_7y&@!-wB%UzX#%0h4_Bt0uDl=zV@I21r)|IzSw zE`1f4&wvRWU}eD~msJ|t7ciRh_HCgIU4C}<5m-^6$;AeRC_){;jD%q<%PMsyCMH_i zEEv|kcUau(IpT&7F5ds0^nWz=eEN3cX#grTU{sspnza3k7JulL0K6PFPRpD1&>i5B@RvypbX4K2fYEF0|cDkYQ`rx7_W+qhhzghO`_fa z#u!eE-)Fxp8d4%b8`-u#vb|bxN`dMLZE>J`c6JpYMM_E@SsSuFdUP3i#+Ta$Flbw6 z#_0m^3yme_ zyXG%{D*@KJ#CNCKFDr!y$oORq1u6{P-=}sfksm*Ptgq)oV2T6wkIRhoFW&I3_y!g9i_6*ZV6% z|5;UR?46`upz#&O#Y_L0icf1P3tvGKj>Q;Qw!L{X3dX!(GG(ayAp=A2r$^q+(~wTk zWeXhrshH@yBk-?ce>qfIZafH-1T-UzEdVi*Nh{I)9tc+C;=;nluP-bQ9_T~v!9;Rf z8`h=EHM_LrIM+@Hzv%Gk6ESEPnuse12MH51^9IZaI9OSsc*35C3$YEQLZ%{(+N@;c zQvXkCEeGQQwY5A5%wh;D8kw2~)!es_y4=kpE~d_7iu=O% z3H_Rv;awf?`!&l#@%g**PcfviS}cUQ8EA%EKP7u7&P?>wum4OU$2D-#$e)ZYZpbQ0Fz78T_BNn;KYjzzdBg~6Hrxuuq#6P1EFgKjp z7vcFSWWnaB2Tjox|JN%Ol`$}^VYuXUSXt{;EL+CSS>0|Af`3>rX>B1jG+VN{zxXt1 y6a2?-AN?nG{jY(xe_hSPe>vRtZ#=CtkA+8%IVTqGrNOUnA>uEjgbSZ*d;Kq=OGG6A literal 0 HcmV?d00001 diff --git a/delivery-platform/docs/workshop/1.2-provision.md b/delivery-platform/docs/workshop/1.2-provision.md index 016cf03..d5b140b 100644 --- a/delivery-platform/docs/workshop/1.2-provision.md +++ b/delivery-platform/docs/workshop/1.2-provision.md @@ -1,7 +1,7 @@ # ![](https://crg-sdw-imgs.web.app/images/sdw-title.svg?) -In this lab you will execute initial setup steps to configure your workspace for use with this workshop. You will also review key Terraform files for use in your own customizations in the future. Finally, you will initiate the provision process to create the software delivery platform used in this workshop. +In this lab, you will execute initial setup steps to configure your workspace for use with this workshop. You will also review key Terraform files for use in your own customizations in the future. Finally, you will initiate the provision process to create the software delivery platform used in this workshop. ## Objectives @@ -17,7 +17,7 @@ Click **Start** to proceed. Choose a project for this tutorial. The project will contain the services and tools to build and deploy applications and the GKE clusters that will act as your development, staging, and production environments. -**_We recommend you create a new project for this tutorial. You may experience undesired side effects if you use an existing project with conflicting settings._** +**We recommend you create a new project for this tutorial. You may experience undesired side effects if you use an existing project with conflicting settings. If you are using QwikLabs, choose the project starting with _qwiklabs-_** @@ -31,11 +31,11 @@ Click **Next** to proceed. ## Submit the job to provision platform -In this step you will run a command to begin provisioning the platform used in this workshop. +In this step, you will run a command to begin provisioning the platform used in this workshop. -All of the steps have been tied together for you in a single script. When customizing your own platform you can utilize only the pieces and sequences that apply in the context of your platform. +All of the steps have been tied together for you in a single script. When customizing your own platform, you can utilize only the pieces and sequences that apply in the context of your requirements. -While the script is running you will review the details of the scripts and resources. +While the script is running, you will review the details of the scripts and resources. First, take a moment to review the complete provision script. Note how each section is broken out for easy execution and customization. @@ -50,6 +50,8 @@ source ./env.sh ${BASE_DIR}/resources/provision/provision-all.sh ``` + **Note:** You don't need to wait here for the provisioning process. You will come back later. + Click **Next** to proceed. ## Review overall folder structure @@ -72,14 +74,14 @@ The `delivery-platform/workdir` folder is used during the workshop as a temporar ### Resources -The final directory of note is the `delivery-platform/resources` folder. This directory contains sample repositories used throughout the lessons as well as a series of Terraform scripts used to provision the platform. +The final directory of note is the `delivery-platform/resources` folder. This directory contains sample repositories used throughout the lessons and a series of Terraform scripts used to provision the platform. Click **Next** to proceed. ## ![](https://crg-sdw-imgs.web.app/images/provisioning.png) -In this lab you'll provision the underlying infrastructure needed to run the workshop. A software delivery platform consists of four main components: Foundation, Clusters, Tools and Applications. +In this lab, you'll provision the underlying infrastructure needed to run the workshop. A software delivery platform consists of four main components: Foundation, Clusters, Tools, and Applications. ### Foundation @@ -92,7 +94,7 @@ Click here to review the network configuration This file can be adjusted to meet your organizational needs. ### Remote State -The Terraform scripts in this workshop utilize remote state management to ensure the state is persisted between users and workspaces. If you were to lose your Cloudshell instance or chose to access the scripts from another device, remote state management will allow you to interact with your Terraform environment correctly. +The Terraform scripts in this workshop utilize remote state management to ensure the state is persisted between users and workspaces. If you were to lose your Cloud Shell instance or chose to access the scripts from another device, remote state management would allow you to interact with your Terraform environment correctly. This state is provided by defining a backend of type Google Cloud Storage (GCS) as seen in `/foundation/tf/backend.tmpl` @@ -108,11 +110,18 @@ Click **Next** to proceed. ## Clusters -Four clusters are created as part of this workshop: `dev`, `stage`, `prod` and `mgmt`. The `prod` cluster will act as a production resource and the `stage` cluster will as a pre-production resource. Applications are deployed to production and staging through your delivery pipeline. The `dev` cluster is used by the application teams for application development and testing. The `mgmt` cluster is used to run the various management, build and deployment tools used by your delivery process. +Three clusters are created as part of this workshop: `dev`, `stage`, and `prod`. + +* The `prod` cluster will act as a production resource. +* The `stage` cluster will act as a pre-production resource. +* The `dev` cluster is used by the application teams for application development and testing. The `dev` cluster is also used to run the various management, build and deployment tools needed by your delivery process. + +Applications are deployed to production and staging through your delivery pipeline. ![](https://crg-sdw-imgs.web.app/images/clusters.png) -To configure the clusters, you can modify the `clusters/tf/clusters.tf` file. +In the lab, you are using GKE zonal clusters for cost-saving. To use regional clusters or configure the clusters, +you can modify the `clusters/tf/clusters.tf` file. Click here to review the current configuration @@ -120,7 +129,7 @@ Click **Next** to proceed. ## Sample Repositories -A series of sample repositories are provided for use in the workshop. These provide a consistent base configuration for the clusters and the applications that will be deployed on the platform. These repositories and concepts will be covered in more detail in later labs. +A series of sample repositories are provided for use in the workshop. These provide a consistent base configuration for the clusters and the applications that will be deployed on the platform. More details of these repositories and concepts will be covered in later labs. In this step of the provisioning process, the scripts will create and push new repos in your git provider for each of these sample repos. From then on, the workshop will use the repos located in your git provider. @@ -148,7 +157,11 @@ Click **Next** to proceed. ## Review the Cloud Build job for platform provisioning -This workshop utilizes Cloud Build to manage the execution of the Terraform used to provision the platform. Executing these steps with Cloud Build allows you to include the exact set of tools and utilities needed for your provisioning process, not having to depend on the user to install tools locally. Additionally, running in this fully managed runtime ensures that any issues with local connectivity or session state won't impact the execution of the job. +This workshop utilizes Cloud Build to manage the execution of the Terraform scripts used to provision the platform. + +Executing these steps with Cloud Build allows you to include the exact set of tools and utilities needed for your provisioning process, not having to depend on the user to install tools locally. + +Additionally, running in this fully managed runtime ensures that any issues with local connectivity or session state won't impact the execution of the job. ### cloudbuild.yaml To submit workloads to Cloud Build, a `cloudbuild.yaml` file is submitted through the gcloud CLI @@ -161,11 +174,14 @@ Review the key lines of the foundation/tf/cloudbuild.yaml ## Congratulations !! + + + You've reached the end of this lab! -At this point you'll probably still have tabs in your editor open from reviewing the various files. Go ahead and close out of those files. +At this point, you'll probably still have tabs in your editor open from reviewing the various files. Go ahead and close out of those files. -Also your provisioning process may not be complete yet. Go ahead and open a new cloud shell terminal to conitune with the next few labs. +Also, your provisioning process may not be complete yet. Go ahead and open a new cloud shell terminal to conitune with the next few labs. @@ -178,7 +194,7 @@ source ./env.sh ``` -Next your instructor will discuss the concepts related to the next section +Next, your instructor will discuss the concepts related to the next section. After the lecture, run the following command to launch the next lab. From fad7fb6f5a966de253c29d90c7ca328c311d2afb Mon Sep 17 00:00:00 2001 From: xiangshen-dk Date: Thu, 1 Jul 2021 17:42:11 -0400 Subject: [PATCH 10/50] update lab docs --- .../docs/workshop/1.3-kustomize.md | 56 ++++++++--------- .../docs/workshop/2.1-app-onboarding.md | 41 ++++++++----- .../docs/workshop/2.2-develop.md | 61 +++++++++---------- .../docs/workshop/3.2-release-progression.md | 8 ++- 4 files changed, 88 insertions(+), 78 deletions(-) diff --git a/delivery-platform/docs/workshop/1.3-kustomize.md b/delivery-platform/docs/workshop/1.3-kustomize.md index 2ec7ea5..877c221 100644 --- a/delivery-platform/docs/workshop/1.3-kustomize.md +++ b/delivery-platform/docs/workshop/1.3-kustomize.md @@ -2,19 +2,19 @@ # Kustomize -In this lab you will work through some of the core concepts of Kustomize and learn how it can be used to help manage variations in applications and environments in your software delivery platform. +In this lab, you will work through some of the core concepts of Kustomize and learn how it can be used to help manage variations in applications and environments in your software delivery platform. ## Objectives -- Combine a base yaml with an overlay +- Combine a base YAML file with an overlay - Use common types: Images, Namespaces, Labels - Apply multiple overlays to create an application ## Prerequisites -This lab assumes you have already cloned the main repository and are starting in the `delivery-platform/` sub folder. +This lab assumes you have already cloned the main repository and starts in the `delivery-platform/` subfolder. Execute the following commands to set your project and local environment variables @@ -44,7 +44,7 @@ Click **Next** to proceed. One of the core features of Kustomize is its ability to overlay multiple file configurations. This allows you to manage a base set of resources and add overlays to configure the specific variations you may see between apps and environments. -Rather than calling `kubectl apply` directly on your k8s manifests you would first run `kustomize build` to render or “hydrate” your manifests with the variations you’ve included from kustomize. +Rather than calling `kubectl apply` directly on your k8s manifests, you would first run `kustomize build` to render or “hydrate” your manifests with the variations you’ve included from kustomize. The `kustomize` command looks for a file called `kustomization.yaml` as the primary configuration. @@ -90,7 +90,7 @@ EOF ``` -Running kustomize command on the base folder outputs the deployment yaml with no changes, which is expected since you haven’t included any variations yet. +Running the kustomize command on the base folder outputs the deployment YAML files with no changes, which is expected since you haven’t included any variations yet. ```bash @@ -105,7 +105,7 @@ Click **Next** to proceed. Images, namespaces and labels are very commonly customized for each application and environment. Since they are commonly changed, Kustomize lets you declare them directly in the `kustomize.yaml`, eliminating the need to create many patches for these common scenarios. -This technique is often used to create a specific instance of a template. One base set of resources can now be used for multiple implementations by simply changing the name and it’s namespace. +This technique is often used to create a specific instance of a template. One base set of resources can now be used for multiple implementations by simply changing the name and its namespace. In this example, you will add a namespace, name prefix and add some labels to your `kustomization.yaml`. @@ -126,7 +126,7 @@ EOF ``` -Executing the build at this point shows that the resulting yaml now contains the namespace, labels and prefixed names in both the service and deployment definitions. +Executing the build at this point shows that the resulting YAML file now contains the namespace, labels and prefixed names in both the service and deployment definitions. ```bash @@ -134,7 +134,7 @@ kustomize build chat-app/base ``` -Note how the output contains labels and namespaces that are not in the deployment yaml. Note also how the name was changed from `chat-app` to `my-chat-app` +Note how the output contains labels and namespaces that are not in the deployment YAML file. Note also how the name was changed from `chat-app` to `my-chat-app` (Output do not copy) @@ -154,9 +154,9 @@ Click **Next** to proceed. ## Patches and Overlays -Kustomize also provides the ability to apply patches that overlay the base resources. This technique is often used to provide variability between applications, and environments. +Kustomize also provides the ability to apply patches that overlay the base resources. This technique is often used to provide variability between applications and environments. -In this step you will create environment variations for a single application that use the same base resources. +In this step, you will create environment variations for a single application that use the same base resources. Start by creating folders for the different environments @@ -211,9 +211,9 @@ EOF ``` -Now implement the kustomize yamls for each directory. +Now implement the kustomize YAML files for each directory. -First rewrite the base customization.yaml removing the namespace and name prefix as this is just the base config with no variation. Those fields will be moved to the environment files in just a moment. +First, rewrite the base customization.yaml, remove the namespace and name prefix as this is just the base config with no variation. Those fields will be moved to the environment files in just a moment. Rewrite the `base/kustomization.yaml `file @@ -288,9 +288,9 @@ Click **Next** to proceed. ## Multiple layers of overlays -Many organizations have a team that helps support the app teams and manage the platform. Frequently these teams will want to include specific details that are to be included in all apps across all environments such as a logging agent. +Many organizations have a team that helps support the app teams and manage the platform. Frequently these teams will want to include specific details that are to be included in all apps across all environments, such as a logging agent. -In this example you will create a `shared-kustomize` folder and resources which will be included in all applications regardless of which environment they’re deployed. +In this example, you will create a `shared-kustomize` folder and resources which will be included in all applications regardless of which environment they’re deployed. Start by creating the folder @@ -332,7 +332,7 @@ EOF ``` -Since you want the` shared-kustomize `folder to be the base for all your applications you will need to update your `chat-app/base/kustomization.yaml` to use `shared-kustomize` as the base then patch its own deployment.yaml on top. The environment folders will then patch again on top of that. +Since you want the `shared-kustomize` folder to be the base for all your applications, you will need to update your `chat-app/base/kustomization.yaml` to use `shared-kustomize` as the base. Then patch its own deployment.yaml on top. The environment folders will then patch again on top of that. ```yaml @@ -358,22 +358,19 @@ kustomize build chat-app/dev ``` -Note the output contains merged results from the app base, the app environment as well as the shared-kustomize folders. Specifically you can see in the containers section values from all three locations. +Note the output contains merged results from the app base, the app environment, and the shared-kustomize folders. Specifically, you can see in the containers section values from all three locations. (output do not copy) - -
-containers:
-      - env:
-        - name: ENVIRONMENT
-          value: dev
-        name: chat-app
-      - image: image
-        name: app
-      - image: logging-agent-image
-        name: logging-agent
-
+ containers: + - env: + - name: ENVIRONMENT + value: dev + name: chat-app + - image: image + name: app + - image: logging-agent-image + name: logging-agent Click **Next** to proceed. @@ -381,6 +378,9 @@ Click **Next** to proceed. ## Congratulations !! + + + You've reached the end of this lab. Close any editor tabs you still have open and change back to the main lab directory with the following commands. diff --git a/delivery-platform/docs/workshop/2.1-app-onboarding.md b/delivery-platform/docs/workshop/2.1-app-onboarding.md index 28bfe76..3e0f861 100644 --- a/delivery-platform/docs/workshop/2.1-app-onboarding.md +++ b/delivery-platform/docs/workshop/2.1-app-onboarding.md @@ -1,10 +1,15 @@ # App Onboarding -Creating a new application often includes more than loading a lanugage's example hello world app and start coding. +Creating a new application often includes more than loading a language's example "hello world" app and start coding. -In reality the process invariably includes at least a minimal set of core tasks such as creating a new app repository for the developers, pulling in a template from an approved list of base applications, setting up foundational build and deploy mechanics, and various other ancilary elements like registering with enterprise change management systems. +In reality, the process invariably includes at least a minimal set of core tasks such as: -Mature organizations implement automation processes that enable developer self serve, and minimize platform administrator engagement. +* Creating a new app repository for the developers. +* Pulling in a template from an approved list of base applications. +* Setting up foundational build and deploy mechanics. +* Setting up various other ancillary elements like registering with enterprise change management systems. + +Mature organizations implement automation processes that enable developers to self-serve and minimize platform administrator engagement. This lab examines patterns for automating the creation of a new application and reviews an automation script that can be modified and extended to meet your custom needs. @@ -12,7 +17,7 @@ This lab examines patterns for automating the creation of a new application and - Review Base Repos - Review app-create script -- Create and review new application +- Create and review a new application Click **Start** to begin the lab. @@ -43,18 +48,19 @@ Click **Next** to continue. The app onboarding process included in this workshop performs a few key functions, one of which is to create a new repository from a template. As part of the provisioning process starter repos from this workshop were copied to your remote git provider for use throughout the lessons. This lab will utilize these repos in the onboarding process. + Review the local version of the app templates repo in the `resources/repos/app-templates` directory -Click here locate the folder +Click here to locate the folder
Take note of the folder names used for each of the template types. These names are used to indicate which template should be used for the new application. -Next to the `app-templates` folder is a folder called `shared-kustomize`. This directory contains base kustomize overlays that will be merged with the app configs as part of the hydration step in the pipeline. Review the contents of that folder and note that the sub folder names match those of the app-template directory. +Next to the `app-templates` folder is a folder called `shared-kustomize`. This directory contains base kustomize overlays that will be merged with the app configs as part of the hydration step in the pipeline. Review the contents of that folder and note that the subfolder names match those of the app-template directory. -To customize or add additional templates simply add the appropriate folders to both of these directories. +To customize or add additional templates, add the appropriate folders to both of these directories. -The third folder in this directory named `cluster-config` is a sample implementation of a repo used by Anthos Config Manager which you'll use later in this lab. This repo will contain the full rendered manifests to be applied to the various clusters. +The third folder in this directory, named `cluster-config` is a sample implementation of a repo used by `Anthos Config Manager`, which you'll use later in this lab. This repo will contain the full rendered manifests to be applied to the various clusters. ## App Creation Script @@ -62,7 +68,7 @@ The script utilized in this workshop was created to not only facilitate app onbo While you can utilize the example script as is, it's assumed your organization will have different onboarding needs and thus it will need to be modified. -Rather than provide an locked down opinionated binary this script is written as an accessible bash script to inform and inspire your own implementaion. +Rather than provide a locked-down opinionated binary, this script is written as an accessible bash script to inform and inspire your own implementation. Review the `scripts/app.sh` file @@ -70,20 +76,20 @@ Review the `scripts/app.sh` file The usage of this script is simply `app.sh create new_app_name template_to_use`. The create function performs the following steps: - Clones down templates repo -- Modify template place holders with actual values (ie the actual app name) +- Modify template placeholders with actual values (i.e. the actual app name) - Creates new remote repo in your git provider to hold the app instance - Configures deployment targets for the various environments - Configures base software delivery pipeline -Variations and customizations you may choose to implement within your organization might include registering the app in a CMDB, updating ingress or load balancer entries, incorporating a user interface for developer self service etc. +Variations and customizations you may choose to implement within your organization might include registering the app in a CMDB, updating ingress or load balancer entries, incorporating a user interface for developer self-service, etc. ## Create & Review new application -In this step you will create an new instance of an app using the provided app creation script that will be used throughout the remainder of the workshop. +In this step, you will create a new instance of an app using the provided app creation script that will be used throughout the remainder of the workshop. -First set some variables including the name of the app to be created. +First, set some variables, including the name of the app to be created. ```bash export APP_NAME=hello-web @@ -103,13 +109,16 @@ Review the namespace addition in the cluster-config repo on your remote git prov ## Review your new app -- Review your new app in you git provider -- Review the entry in the Config-repo +- Review your new app in your git provider +- Review the entry in the config repo - Review the webhook in the app repo -- Review the trigger in cloud build +- Review the trigger in Cloud Build ## Congratulations !! + + + You've reached the end of this lab diff --git a/delivery-platform/docs/workshop/2.2-develop.md b/delivery-platform/docs/workshop/2.2-develop.md index 95f6f5d..0440bb9 100644 --- a/delivery-platform/docs/workshop/2.2-develop.md +++ b/delivery-platform/docs/workshop/2.2-develop.md @@ -1,7 +1,7 @@ # App Development -In this lab you will walk through a typical developer workflow use-case that highlights local development integrations that are available within Cloud Code. +In this lab, you will walk through a typical developer workflow use case that highlights local development integrations available within Cloud Code. ## Objectives @@ -11,21 +11,19 @@ In this lab you will walk through a typical developer workflow use-case that hig * Introduce skaffold for pipeline abstraction * Utilize local and remote clusters * Utilize hot reloading -* Debug apps on remote gke realtime +* Debug apps on remote GKE realtime ## Prerequisites This lab assumes you have already cloned the main repository and are starting in the `delivery-platform/` folder. -Next select the project you are using for the lab. +Next, select the project you are using for the lab. -<walkthrough-project-setup></walkthrough-project-setup> + Execute the following command to set your project and local environment variables. - - ```bash gcloud config set project {{project-id}} @@ -37,9 +35,11 @@ export APP_NAME=hello-web ## Clone the repositories -In a previous lab you created a new application for use by the development team. In practice a member of the dev team would trigger the script through a UI or other process. Once the app is created the developer and the rest of their team would need to clone the remote repo first before being able to make changes. +In a previous lab, you created a new application for use by the development team. In practice, a dev team member would trigger the script through a UI or other process. Once the app is created, the developer and the rest of their team would need to clone the remote repo first before making changes. + +In practice, some organizations may choose to include kustomize overlays through HTTP references rather than local references to the files. -In practice some organizations may choose to include kustomize overlays through HTTP references rather than local references to the files. In this workshop the kustomize overlays are included as local files for clarity and discussion so you'll be cloning that repo as well. +In this workshop, the kustomize overlays are included as local files for clarity and discussion. So you'll be cloning that repo as well. Clone the application and kustomize repositories. @@ -61,28 +61,26 @@ cloudshell workspace $WORK_DIR/${APP_NAME} ## Develop with Cloud Code -Cloud Code reduces many of the tedious repetitive steps a developer typically needs to execute to develop applications for containers based runtimes. +Cloud Code reduces many tedious and repetitive steps a developer typically needs to execute for developing container based applications. Cloud Code features can be accessed through the command palette by typing `Cmd+Shift+P` on Mac or `Ctrl+Shift+P` on Windows or ChromeOS, then typing `Cloud Code` to filter down to Cloud Code commands. -Alternatively many of the most commonly used commands are available by clicking on the Cloud Code indicator in the status bar below the editor. +Alternatively, many of the most commonly used commands are available by clicking on the Cloud Code indicator in the status bar below the editor. -Finally there are use-case specific icons on the left side of the navigator that take you directly to sections like the API or GKE Explorer. +Finally, there are use-case specific icons on the left side of the navigator that take you directly to sections like the API or GKE Explorer. - Click here to highlight the GKE explorer - ## Local Development Loop -During development it’s useful to work with your application against a local kubernetes cluster like minikube. In this section you’ll use the Cloud Code plugin to deploy your application to a local instance of minikube, then hot reload changes made in the code. +During development, it’s useful to work with your application against a local Kubernetes cluster like `minikube`. In this section, you’ll use the Cloud Code plugin to deploy your application to a local instance of minikube, then hot reload changes made in the code. Start minikube -In Cloud Shell IDE, click the word `minikube` in the status bar. In the prompt at the top of the screen click on the `minikube` option then click. `start` +In Cloud Shell IDE, click the word `minikube` in the status bar. In the prompt at the top of the screen, click on the `minikube` option, then click. `start` **Wait for `minikube` to finish starting**. It takes 1-3 minutes. @@ -97,39 +95,37 @@ kubectl config use-context minikube Once minikube is running, build and deploy the application with Cloud Code. Locate the Run on K8s command in your command pallet by: 1. Using the hotkey combination `cmd/ctrl+shift+p` -1. Type “`Cloud Code: Run on kubernetes`” and select the option +1. Type “`Cloud Code: Run on Kubernetes`” and select the option 1. Select `Kubernetes: Run/Debug - local` and confirm you want to use the current context (minikube) to run the app. -Once the deploy is complete, review the app by clicking on the URL provided in the output window. +Once the deployment is complete, review the app by clicking on the URL provided in the output window. -Change this line in the main.go file to a different output. When you save the file, notice the build and deploy automatically begin to redeploy the application to the cluster. Once completed return to the tab with the application deployed and refresh the window to see the results updated. +Change this line in the main.go file to a different output. When you save the file, notice the build and deploy automatically begin to redeploy the application to the cluster. -To stop the hot deploy process find the stop button in the debug configuration. +Once completed, return to the tab with the application deployed and refresh the window to see the results updated. - +To stop the hot deploy process, find the stop button in the debug configuration. + Locate the debug configuration pane - Stopping the process not only detaches the process but also cleans up all the resources used on the cluster. - - ## Remote Debugging -Cloud Code also simplifies the process of developing for kubernetes by integrating live debugging of applications running in kubernetes clusters. For this section you will deploy your application to a remote dev cluster and perform some simple live debugging. +Cloud Code also simplifies developing for Kubernetes by integrating live debugging of applications running in Kubernetes clusters. For this section, you will deploy your application to a remote dev cluster and perform some simple live debugging. -Cloud Code can utilize any Kubernetes cluster in your local contexts. For this example you'll utilize the remote dev cluster. +Cloud Code can utilize any Kubernetes cluster in your local contexts. For this example, you'll use the remote dev cluster. -Cloud Code understands remote vs local deployment patterns. When you used minikube earlier, Cloud Code defaulted to using your local Docker build and store the image locally. Since this is a remote deployment the system will prompt you for a remote registry. +Cloud Code understands remote vs. local deployment patterns. When you used minikube earlier, Cloud Code defaulted to using your local Docker build and store the image locally. Since this is a remote deployment the system will prompt you for a remote registry. -Under the hood Cloud Code is using skaffold to build and deploy. For the remote clusters you’ll be prompted for which profile to use. The workshop is designed to use the [default] profile for local development. +Under the hood, Cloud Code is using skaffold to build and deploy. For the remote clusters, you’ll be prompted for which profile to use. The workshop is designed to use the [default] profile for local development. -These prompts will occur only when there are no existing configurations found. To set or change configurations switch to the debug view then select the settings icon next to the launch configurations dropdown. +These prompts will occur only when there are no existing configurations found. To set or change configurations, switch to the debug view then select the settings icon next to the launch configurations dropdown. -First start by switching to the dev cluster context with the command below +First, start by switching to the dev cluster context with the command below ```bash @@ -141,13 +137,13 @@ This time you’ll choose `Cloud Code: Debug on Kubernetes` from the command pal 1. Using the hotkey combination `cmd/ctrl+shift+p` 1. Type “`Cloud Code: Debug on Kubernetes`” and select the option -1. Select `Kubernetes: Run/Debug - dev` and confirm you want to use current context (dev) to run the app. +1. Select `Kubernetes: Run/Debug - dev` and confirm you want to use the current context (dev) to run the app. 1. If asked which image repo to use, choose the default value of `gcr.io/{project}` 1. If asked which cluster to use, choose to use the current `dev` context. To watch the progress be sure you’ve selected the Output window. The editor may have switched to debug view. -Once the build and deploy completes, the output will provide a URL for viewing the deployed application. +Once the build and deployment complete, the output will provide a URL for viewing the deployed application. Click the URL provided to see the application results. @@ -169,6 +165,9 @@ cloudshell workspace $BASE_DIR/.. ## Congratulations !! + + + You've reached the end of this lab. After the next lecture, run the following command to launch the next lab. diff --git a/delivery-platform/docs/workshop/3.2-release-progression.md b/delivery-platform/docs/workshop/3.2-release-progression.md index b0b8ac5..d9cad80 100644 --- a/delivery-platform/docs/workshop/3.2-release-progression.md +++ b/delivery-platform/docs/workshop/3.2-release-progression.md @@ -1,6 +1,6 @@ # Release Progression w/ Git Event -In this lab you will implement a release progression process that utilizes git events as the primary workflow. Using git to manage the process allows organizations to move many of the stage gates and checks towards the development team. While this practice is a core concept in the popular gitops practices, git-based workflows can be used with traditional imperative as well as gitops oriented declarative processes. +In this lab, you will implement a release progression process that utilizes git events as the primary workflow. Using git to manage the process allows organizations to move many of the stage gates and checks towards the development team. While this practice is a core concept in the popular gitops practices, git-based workflows can be used with traditional imperative as well as gitops oriented declarative processes. ## Objectives - Review lifecycle variations @@ -66,7 +66,7 @@ Execute the following command to generate a link to that location in your projec ```bash echo $GIT_BASE_URL/$APP_NAME/settings/hooks ``` -Copy the value and paste into a new browser tab to review the webhook on your application repo. +Copy the value and paste it into a new browser tab to review the webhook on your application repo. ### Triggers @@ -167,4 +167,6 @@ When you're done use `ctrl+c` in the terminal to exit out of the tunnel ## Congratulations!!! -You've reached the end of the lab! \ No newline at end of file + + +You've reached the end of the lab! From 7b6b91180e315ccabbd5f97ffb7c7942826906bd Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 2 Jul 2021 09:47:43 -0500 Subject: [PATCH 11/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15cc067..02a33f2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Software Delivery Workshop -This repository contains resources and materials targeted toward Software Delivery on Google Cloud. In addition to separate stand alone guides, an opinionated yet modular platform is provided to demonstrate software delivery practices. In contains scripts to standup a base platform infrastructure as well as other resources designed to facilitate hands on workshop and standard demo use cases. The platform provisioning resources are structured to be modular in nature supporting various runtime and tooling configurations. Ideally users can utilize their own choice of tooling for: Provisioning, Source Code Management, Templating, Build Engine, Image Storage and Deploy tooling. +This repository contains resources and materials targeted toward Software Delivery on Google Cloud. In addition to separate stand alone guides, an opinionated yet modular platform is provided to demonstrate software delivery practices. It contains scripts to standup a base platform infrastructure as well as other resources designed to facilitate hands on workshop and standard demo use cases. The platform provisioning resources are structured to be modular in nature supporting various runtime and tooling configurations. Ideally users can utilize their own choice of tooling for: Provisioning, Source Code Management, Templating, Build Engine, Image Storage and Deploy tooling. ## Usage From a0d4bb336505723bee16453ae70d72407210405b Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 2 Jul 2021 09:48:50 -0500 Subject: [PATCH 12/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02a33f2..354c733 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This repository contains resources and materials targeted toward Software Delive This set of resources contains materials to provision the platform, deliver short demonstrations and facilitate hands on workshops. ### Workshop -The Software Delivery Workshop contains materials for a self led exploration or accompanying instructor led sessions. To get started with either click the button below to open the resources in Google Cloud Shell. +The Software Delivery Workshop contains materials for a self led exploration or accompanying instructor led sessions. To get started click the button below to open the resources in Google Cloud Shell. [![Software Delivery Workshop](http://www.gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/GoogleCloudPlatform/software-delivery-workshop.git&cloudshell_workspace=.&cloudshell_tutorial=delivery-platform/docs/workshop/1.2-provision.md) From 9a997470f3316252a1e3dcee32d32fd421f77cf2 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 2 Jul 2021 09:59:56 -0500 Subject: [PATCH 13/50] Updates to lab 1.2 --- .gitignore | 1 + delivery-platform/docs/workshop/1.2-provision.md | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3f8e001..172a41c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ terraform.tfvars backend.tf .terraform/ .talismanrc +.DS_Store \ No newline at end of file diff --git a/delivery-platform/docs/workshop/1.2-provision.md b/delivery-platform/docs/workshop/1.2-provision.md index d5b140b..6443e04 100644 --- a/delivery-platform/docs/workshop/1.2-provision.md +++ b/delivery-platform/docs/workshop/1.2-provision.md @@ -42,7 +42,16 @@ First, take a moment to review the complete provision script. Note how each sect Review the provision-all.sh file ### Provision -Execute the provision script +The provision process begins by prompting you for three values. + +First you will be prompted to select your git provider, and then your git username. Choose Github for the provider and enter your github username. + +Next you will be prompted to provide a github personal access token. A link is provided in the terminal to help you generate one. Click on the link and provide a name such as DeliveryWorkshop for the token. Once generated copy the token and paste into the terminal. + +The final prompt will be asking for an API key. Again a link is provided that will take you to the credentials page where you will create the key. Instructions are provided in the terminal. Once completed copy the API key and paste into the terminal. + + +To initiate the process execute the following commands. ```bash gcloud config set project {{project-id}} From 52e60a582db7a47e7eec323eacf82021a23a0acc Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 2 Jul 2021 13:02:10 -0500 Subject: [PATCH 14/50] Update README.md --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 354c733..0e3776c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,25 @@ The Software Delivery Workshop contains materials for a self led exploration or [![Software Delivery Workshop](http://www.gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/GoogleCloudPlatform/software-delivery-workshop.git&cloudshell_workspace=.&cloudshell_tutorial=delivery-platform/docs/workshop/1.2-provision.md) +If at any time during the workshop you somehow exit out of the lab instructions, you can relaunch the lab using the related command below: + +``` +teachme "${BASE_DIR}/docs/workshop/1.2-provision.md" +teachme "${BASE_DIR}/docs/workshop/1.3-kustomize.md" +teachme "${BASE_DIR}/docs/workshop/2.1-app-onboarding.md" +teachme "${BASE_DIR}/docs/workshop/2.2-develop.md" +teachme "${BASE_DIR}/docs/workshop/3.2-release-progression.md" + +``` + +If at anytime you lose your terminal session, ensure you are in the `software-delivery-workshop/delivery-platform` directory then run the following commands to reset your environment variables. + + +```shell +gcloud config set project +source ./env.sh +``` + ### Demo For a mostly automated experience follow the instructions in the `docs\demo` folder. You will run an automated script to fully provision the platform before your demonstration. A separate guide describes the steps to perform during the demonstration and concludes with instructions how to reset the demo or tear down the infrastructure. From fe380ef7a3a89256b2075db47b23143dfdfbc657 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Wed, 7 Jul 2021 15:46:57 -0500 Subject: [PATCH 15/50] Multiple lab updates * multiple lab updates --- .../docs/workshop/1.2-provision.md | 6 ++-- .../docs/workshop/1.3-kustomize.md | 35 +++++++++++++------ .../docs/workshop/2.1-app-onboarding.md | 30 +++++++++++++--- .../app-templates/golang/cloudbuild.yaml | 19 ---------- delivery-platform/scripts/app.sh | 3 ++ 5 files changed, 57 insertions(+), 36 deletions(-) diff --git a/delivery-platform/docs/workshop/1.2-provision.md b/delivery-platform/docs/workshop/1.2-provision.md index 6443e04..ecebb01 100644 --- a/delivery-platform/docs/workshop/1.2-provision.md +++ b/delivery-platform/docs/workshop/1.2-provision.md @@ -59,7 +59,9 @@ source ./env.sh ${BASE_DIR}/resources/provision/provision-all.sh ``` - **Note:** You don't need to wait here for the provisioning process. You will come back later. +The provision process will take approximately 20 minutes to complete. Continue with this and future labs while the script is provisioning. + + **Note:** You do not need to wait for the provisioning process to complete before continuing. Click **Next** to proceed. @@ -190,7 +192,7 @@ You've reached the end of this lab! At this point, you'll probably still have tabs in your editor open from reviewing the various files. Go ahead and close out of those files. -Also, your provisioning process may not be complete yet. Go ahead and open a new cloud shell terminal to conitune with the next few labs. +Also, your provisioning process may not be complete yet. Go ahead and open a new cloud shell terminal to continue with the next few labs. diff --git a/delivery-platform/docs/workshop/1.3-kustomize.md b/delivery-platform/docs/workshop/1.3-kustomize.md index 877c221..d5f8745 100644 --- a/delivery-platform/docs/workshop/1.3-kustomize.md +++ b/delivery-platform/docs/workshop/1.3-kustomize.md @@ -2,6 +2,9 @@ # Kustomize +Kustomize is a tool that introduces a template-free way to customize application configuration that simplifies the use of off-the-shelf applications. It's available as a stand alone utility and is built into kubectl through `kubectl apply -k`. For additional details read more at [kustomize.io](https://kustomize.io/). + + In this lab, you will work through some of the core concepts of Kustomize and learn how it can be used to help manage variations in applications and environments in your software delivery platform. @@ -16,7 +19,7 @@ In this lab, you will work through some of the core concepts of Kustomize and le This lab assumes you have already cloned the main repository and starts in the `delivery-platform/` subfolder. -Execute the following commands to set your project and local environment variables +Copy and execute the following commands in your terminal to set your project and local environment variables ```bash @@ -30,7 +33,7 @@ To demonstrate the kustomize features, you will work out of a temporary director Begin by changing to the work directory and creating the lab folder -```bash +```shell mkdir $WORK_DIR/kustomize-lab cd $WORK_DIR/kustomize-lab cloudshell workspace . @@ -51,12 +54,14 @@ The `kustomize` command looks for a file called `kustomization.yaml` as the prim To start, you will create a folder to hold your base configuration files -```bash +```shell mkdir -p chat-app/base ``` -Create a simple `deployment.yaml` in the base folder +Create a simple `deployment.yaml` in the base folder + +Copy and execute the following commands in your terminal ```yaml @@ -81,6 +86,7 @@ Now create a `kustomization.yaml` file that references the `deployment.yaml` as Create the base `kustomization.yaml` +Copy and execute the following commands in your terminal ```yaml cat < chat-app/base/kustomization.yaml @@ -93,7 +99,7 @@ EOF Running the kustomize command on the base folder outputs the deployment YAML files with no changes, which is expected since you haven’t included any variations yet. -```bash +```shell kustomize build chat-app/base ``` @@ -111,6 +117,7 @@ In this example, you will add a namespace, name prefix and add some labels to yo Update the `kustomization.yaml` file to include common labels and namespaces. +Copy and execute the following commands in your terminal ```yaml cat < chat-app/base/kustomization.yaml @@ -129,7 +136,7 @@ EOF Executing the build at this point shows that the resulting YAML file now contains the namespace, labels and prefixed names in both the service and deployment definitions. -```bash +```shell kustomize build chat-app/base ``` @@ -161,7 +168,7 @@ In this step, you will create environment variations for a single application th Start by creating folders for the different environments -```bash +```shell mkdir -p chat-app/dev mkdir -p chat-app/prod ``` @@ -217,6 +224,7 @@ First, rewrite the base customization.yaml, remove the namespace and name prefix Rewrite the `base/kustomization.yaml `file +Copy and execute the following commands in your terminal ```yaml cat < chat-app/base/kustomization.yaml @@ -234,6 +242,7 @@ Now implement the variations for dev and prod. Note the addition now of the `pat Create the `dev/kustomization.yaml `file +Copy and execute the following commands in your terminal ```yaml cat < chat-app/dev/kustomization.yaml @@ -254,6 +263,7 @@ EOF Now create the `prod/kustomization.yaml `file +Copy and execute the following commands in your terminal ```yaml cat < chat-app/prod/kustomization.yaml @@ -295,13 +305,14 @@ In this example, you will create a `shared-kustomize` folder and resources which Start by creating the folder -```bash +```shell mkdir shared-kustomize ``` Create a simple `deployment.yaml` in the base folder +Copy and execute the following commands in your terminal ```yaml cat < shared-kustomize/deployment.yaml @@ -323,6 +334,7 @@ Now create a `kustomization.yaml` file that references the `deployment.yaml` as Create the base `kustomization.yaml` +Copy and execute the following commands in your terminal ```yaml cat < shared-kustomize/kustomization.yaml @@ -334,6 +346,7 @@ EOF Since you want the `shared-kustomize` folder to be the base for all your applications, you will need to update your `chat-app/base/kustomization.yaml` to use `shared-kustomize` as the base. Then patch its own deployment.yaml on top. The environment folders will then patch again on top of that. +Copy and execute the following commands in your terminal ```yaml cat < chat-app/base/kustomization.yaml @@ -353,7 +366,7 @@ EOF Run the following command to see the merged result. -```bash +```shell kustomize build chat-app/dev ``` @@ -361,7 +374,7 @@ kustomize build chat-app/dev Note the output contains merged results from the app base, the app environment, and the shared-kustomize folders. Specifically, you can see in the containers section values from all three locations. (output do not copy) - +
     containers:
           - env:
             - name: ENVIRONMENT
@@ -371,7 +384,7 @@ Note the output contains merged results from the app base, the app environment,
             name: app
           - image: logging-agent-image
             name: logging-agent
-
+
Click **Next** to proceed. diff --git a/delivery-platform/docs/workshop/2.1-app-onboarding.md b/delivery-platform/docs/workshop/2.1-app-onboarding.md index 3e0f861..e8d78ad 100644 --- a/delivery-platform/docs/workshop/2.1-app-onboarding.md +++ b/delivery-platform/docs/workshop/2.1-app-onboarding.md @@ -103,17 +103,39 @@ Next execute the app create function to instantiate the app ./scripts/app.sh create ${APP_NAME} golang ``` -Review the app source repo in your remote git provider - -Review the namespace addition in the cluster-config repo on your remote git provider - ## Review your new app +The onboarding process created a variety of resources and updated configurations related to your new application instance. Take a moment to review what the script did. + - Review your new app in your git provider - Review the entry in the config repo - Review the webhook in the app repo - Review the trigger in Cloud Build + +*Review your new app in your git provider* + +Head over to [github.com](https://github.com) for your username. Under your repositories locate the newly created `hello-web` repository. + +Review the configuration file such as `skaffold.yaml` or `k8s/dev/deployment.yaml` and note that the onboarding script inserted the unique app name throughout. + +*Review the entry in the config repo* +Again in [github.com](https://github.com) for your username. Under your repositories locate the `mcd-cluster-config` repo. This base repository was created during the provisioning process, but has just been updated to include a `hello-web` namespace in the dev stage and prod folders. + + +*Review the webhook in the app repo* +A webhook was added to the `hello-web` application repo in github for your new app. + +Back on [github.com](https://github.com) for your username, under your repositories locate the newly created `hello-web` repository. Then on settings page select webhooks from the left nav. + +Review the webhook that was added to the repository. + +*Review the trigger in Cloud Build* + +The webhook in the github repo makes a call out to Cloud Build to execute the build and deploy processes. + +Review the Cloud Build trigger by visiting the [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) in the Google Cloud console. + ## Congratulations !! diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml b/delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml index 216eca0..ed34b31 100644 --- a/delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml @@ -13,23 +13,6 @@ # limitations under the License. steps: - # TEMP STEP - # - id: gitref - # name: bash - # args: - # - '-c' - # - | - # echo "${_REF}" - # IFS='/' read -a array <<< "${_REF}" - # echo ${array[1]} - - # if [ ${array[1]} == "tags" ] ; then - # echo "FOUND TAG" - # else - # echo "FOUND BRANCH" - # fi - # echo ${array[1]} ${array[2]} - # Create the hydrated directories - id: create-dirs name: bash @@ -74,7 +57,6 @@ steps: - id: skaffold-build name: gcr.io/k8s-skaffold/skaffold entrypoint: bash - # changed from GIT_URL to APP_REPO args: - '-c' - | @@ -85,7 +67,6 @@ steps: - id: skaffold-render name: gcr.io/k8s-skaffold/skaffold entrypoint: bash - # changed from GIT_URL to APP_REPO args: - '-c' - | diff --git a/delivery-platform/scripts/app.sh b/delivery-platform/scripts/app.sh index 18696b8..022658b 100755 --- a/delivery-platform/scripts/app.sh +++ b/delivery-platform/scripts/app.sh @@ -90,9 +90,12 @@ create () { # Initial deploy cd $WORK_DIR/app-templates/${APP_LANG} + git pull echo "v1" > version.txt git add . && git commit -m "v1" git push origin main + sleep 10 + git pull git tag v1 git push origin v1 From 3e8c01e99c7437433c606e6ea1e072e602a28c64 Mon Sep 17 00:00:00 2001 From: Henry Bell Date: Tue, 10 Aug 2021 15:17:05 +0100 Subject: [PATCH 16/50] Bump Terraform version, fix dependencies (#13) --- .../resources/provision/base_image/Dockerfile | 2 +- .../resources/provision/clusters/tf/clusters.tf | 1 - .../resources/provision/clusters/tf/outputs.tf | 16 ++++++++++++---- .../resources/provision/clusters/tf/versions.tf | 6 ++++++ .../provision/foundation/tf/networks.tf | 1 - .../provision/foundation/tf/services.tf | 2 +- .../provision/foundation/tf/versions.tf | 6 ++++++ 7 files changed, 26 insertions(+), 8 deletions(-) diff --git a/delivery-platform/resources/provision/base_image/Dockerfile b/delivery-platform/resources/provision/base_image/Dockerfile index a482067..e52233c 100644 --- a/delivery-platform/resources/provision/base_image/Dockerfile +++ b/delivery-platform/resources/provision/base_image/Dockerfile @@ -19,7 +19,7 @@ RUN apt-get update && \ apt-transport-https ca-certificates \ dnsutils curl gettext -ENV TERRAFORM_VERSION=0.13.5 +ENV TERRAFORM_VERSION=1.0.3 ENV HELM_VERSION=2.14.3 ENV KUBECTL_VERSION=1.16.1 ENV GO_VERSION=1.14.2 diff --git a/delivery-platform/resources/provision/clusters/tf/clusters.tf b/delivery-platform/resources/provision/clusters/tf/clusters.tf index d7dc707..5f9cb53 100644 --- a/delivery-platform/resources/provision/clusters/tf/clusters.tf +++ b/delivery-platform/resources/provision/clusters/tf/clusters.tf @@ -20,7 +20,6 @@ locals { provider "google" { project = var.project_id - version = "~> 3.44.0" } data "google_compute_network" "delivery-platform" { diff --git a/delivery-platform/resources/provision/clusters/tf/outputs.tf b/delivery-platform/resources/provision/clusters/tf/outputs.tf index 5cbf309..2da1ce9 100644 --- a/delivery-platform/resources/provision/clusters/tf/outputs.tf +++ b/delivery-platform/resources/provision/clusters/tf/outputs.tf @@ -26,13 +26,21 @@ output "staging__cluster-service-account" { output "dev_name" { value = module.delivery-platform-dev.name } output "dev_location" { value = module.delivery-platform-dev.location } -output "dev_endpoint" { value = module.delivery-platform-dev.endpoint } +output "dev_endpoint" { + value = module.delivery-platform-dev.endpoint + sensitive = true +} output "stage_name" { value = module.delivery-platform-staging.name } output "stage_location" { value = module.delivery-platform-staging.location } -output "stage_endpoint" { value = module.delivery-platform-staging.endpoint } +output "stage_endpoint" { + value = module.delivery-platform-staging.endpoint + sensitive = true +} output "prod_name" { value = module.delivery-platform-prod.name } output "prod_location" { value = module.delivery-platform-prod.location } -output "prod_endpoint" { value = module.delivery-platform-prod.endpoint } - +output "prod_endpoint" { + value = module.delivery-platform-prod.endpoint + sensitive = true +} diff --git a/delivery-platform/resources/provision/clusters/tf/versions.tf b/delivery-platform/resources/provision/clusters/tf/versions.tf index c001c4e..0823918 100644 --- a/delivery-platform/resources/provision/clusters/tf/versions.tf +++ b/delivery-platform/resources/provision/clusters/tf/versions.tf @@ -16,4 +16,10 @@ terraform { required_version = ">= 0.13" + required_providers { + google = { + source = "hashicorp/google" + version = "~> 3.44.0" + } + } } diff --git a/delivery-platform/resources/provision/foundation/tf/networks.tf b/delivery-platform/resources/provision/foundation/tf/networks.tf index 94503ed..b50e07d 100644 --- a/delivery-platform/resources/provision/foundation/tf/networks.tf +++ b/delivery-platform/resources/provision/foundation/tf/networks.tf @@ -15,7 +15,6 @@ */ provider "google" { - version = "~> 3.44.0" project = var.project_id } diff --git a/delivery-platform/resources/provision/foundation/tf/services.tf b/delivery-platform/resources/provision/foundation/tf/services.tf index 7fd4dc4..dd47a08 100644 --- a/delivery-platform/resources/provision/foundation/tf/services.tf +++ b/delivery-platform/resources/provision/foundation/tf/services.tf @@ -16,7 +16,7 @@ module "project-services" { source = "terraform-google-modules/project-factory/google//modules/project_services" - version = "~> 9.0" + version = "~> 11.1" project_id = var.project_id diff --git a/delivery-platform/resources/provision/foundation/tf/versions.tf b/delivery-platform/resources/provision/foundation/tf/versions.tf index c001c4e..0823918 100644 --- a/delivery-platform/resources/provision/foundation/tf/versions.tf +++ b/delivery-platform/resources/provision/foundation/tf/versions.tf @@ -16,4 +16,10 @@ terraform { required_version = ">= 0.13" + required_providers { + google = { + source = "hashicorp/google" + version = "~> 3.44.0" + } + } } From 21c4c2032e891881f89628a74212b3e29c600012 Mon Sep 17 00:00:00 2001 From: crgrant Date: Mon, 30 Aug 2021 18:44:49 +0000 Subject: [PATCH 17/50] v1 --- delivery-platform/version.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 delivery-platform/version.txt diff --git a/delivery-platform/version.txt b/delivery-platform/version.txt new file mode 100644 index 0000000..626799f --- /dev/null +++ b/delivery-platform/version.txt @@ -0,0 +1 @@ +v1 From 2333e8bef321ffa3526e5b01ca5667927e51cf64 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Mon, 30 Aug 2021 16:12:27 -0500 Subject: [PATCH 18/50] Cleanup extra file --- delivery-platform/version.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 delivery-platform/version.txt diff --git a/delivery-platform/version.txt b/delivery-platform/version.txt deleted file mode 100644 index 626799f..0000000 --- a/delivery-platform/version.txt +++ /dev/null @@ -1 +0,0 @@ -v1 From 64c5048f448acab7929132a04585a7f8f717532b Mon Sep 17 00:00:00 2001 From: crgrant Date: Tue, 31 Aug 2021 16:30:20 +0000 Subject: [PATCH 19/50] v1 --- delivery-platform/Untitled.txt | 2 ++ .../resources/provision/repos/create-template-repos.sh | 6 ++++++ delivery-platform/scripts/app.sh | 2 +- delivery-platform/version.txt | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 delivery-platform/Untitled.txt create mode 100644 delivery-platform/version.txt diff --git a/delivery-platform/Untitled.txt b/delivery-platform/Untitled.txt new file mode 100644 index 0000000..aaea623 --- /dev/null +++ b/delivery-platform/Untitled.txt @@ -0,0 +1,2 @@ +https://cloudbuild.googleapis.com/v1/projects/crg-sdw-8-30-2021-2-324521/triggers/hello-web-webhook-trigger:webhook?key=AIzaSyCyXJSxs8HIiGh6AfoMv0ncSMDMrp8iGIM&secret=txeSngEcm8r3TGtrnOW +https://cloudbuild.googleapis.com/v1/projects/crg-sdw-8-30-2021-2-324521/triggers/hello-web-webhook-trigger:webhook?key=AIzaSyCyXJSxs8HIiGh6AfoMv0ncSMDMrp8iGIM&secret=txeSngEcm8r3TGtrnOW \ No newline at end of file diff --git a/delivery-platform/resources/provision/repos/create-template-repos.sh b/delivery-platform/resources/provision/repos/create-template-repos.sh index 90274ea..3c6e849 100755 --- a/delivery-platform/resources/provision/repos/create-template-repos.sh +++ b/delivery-platform/resources/provision/repos/create-template-repos.sh @@ -23,11 +23,17 @@ cp -R $BASE_DIR/resources/repos/app-templates $WORK_DIR cd $WORK_DIR/app-templates git init && git symbolic-ref HEAD refs/heads/main && git add . && git commit -m "initial commit" $BASE_DIR/scripts/git/${GIT_CMD} create $APP_TEMPLATES_REPO +sleep 5 git remote add origin $GIT_BASE_URL/$APP_TEMPLATES_REPO +echo "check --- Push 1" +git push origin main +echo "check --- Push 2" git push origin main cd $BASE_DIR rm -rf $WORK_DIR/app-templates +echo "check --- did it work?" +sleep 120 # Create shared kustomize repo cp -R $BASE_DIR/resources/repos/shared-kustomize $WORK_DIR cd $WORK_DIR/shared-kustomize diff --git a/delivery-platform/scripts/app.sh b/delivery-platform/scripts/app.sh index 022658b..258e5c5 100755 --- a/delivery-platform/scripts/app.sh +++ b/delivery-platform/scripts/app.sh @@ -208,7 +208,7 @@ create_cloudbuild_trigger () { --role='roles/secretmanager.secretAccessor' ## Create CloudBuild Webhook Endpoint - REPO_LOCATION=https://github.com/${GIT_USERNAME}/${GIT_REPO_NAME} + REPO_LOCATION=https://github.com/${GIT_USERNAME}/${APP_NAME} TRIGGER_NAME=${APP_NAME}-webhook-trigger BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/cloudbuild.yaml diff --git a/delivery-platform/version.txt b/delivery-platform/version.txt new file mode 100644 index 0000000..626799f --- /dev/null +++ b/delivery-platform/version.txt @@ -0,0 +1 @@ +v1 From 597a0eef4f1f845453d84e068b5071b4055a43c1 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Tue, 31 Aug 2021 13:23:39 -0500 Subject: [PATCH 20/50] Webhook update & GitHub Client retry --- delivery-platform/scripts/app.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/delivery-platform/scripts/app.sh b/delivery-platform/scripts/app.sh index 258e5c5..b1ca0ae 100755 --- a/delivery-platform/scripts/app.sh +++ b/delivery-platform/scripts/app.sh @@ -70,6 +70,9 @@ create () { git remote add origin $GIT_BASE_URL/${APP_NAME} git add . && git commit -m "initial commit" git push origin main + # Auth fails intermittetly on the very first client call for some reason + # Adding a retry to ensure the source is pushed. + git push origin main # Configure Build @@ -216,9 +219,7 @@ create_cloudbuild_trigger () { ## Setup Trigger & Webhook gcloud alpha builds triggers create webhook \ --name=${TRIGGER_NAME} \ - --repo=${REPO_LOCATION} \ --substitutions='_APP_NAME='${APP_NAME}',_APP_REPO=$(body.repository.git_url),_CONFIG_REPO='${GIT_BASE_URL}'/'${CLUSTER_CONFIG_REPO}',_DEFAULT_IMAGE_REPO='${IMAGE_REPO}',_KUSTOMIZE_REPO='${GIT_BASE_URL}'/'${SHARED_KUSTOMIZE_REPO}',_REF=$(body.ref)' \ - --branch='*' \ --inline-config=$BUILD_YAML_PATH \ --secret=${SECRET_PATH} From c8edc9c2ef519c7bc4b58a86c02be6cc122f0590 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Tue, 31 Aug 2021 13:45:51 -0500 Subject: [PATCH 21/50] Webhook update & GitHub Client retry --- .../resources/provision/repos/create-template-repos.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/delivery-platform/resources/provision/repos/create-template-repos.sh b/delivery-platform/resources/provision/repos/create-template-repos.sh index 3c6e849..5400d11 100755 --- a/delivery-platform/resources/provision/repos/create-template-repos.sh +++ b/delivery-platform/resources/provision/repos/create-template-repos.sh @@ -25,15 +25,14 @@ git init && git symbolic-ref HEAD refs/heads/main && git add . && git commit -m $BASE_DIR/scripts/git/${GIT_CMD} create $APP_TEMPLATES_REPO sleep 5 git remote add origin $GIT_BASE_URL/$APP_TEMPLATES_REPO -echo "check --- Push 1" git push origin main -echo "check --- Push 2" +# Auth fails intermittetly on the very first client call for some reason + # Adding a retry to ensure the source is pushed. git push origin main cd $BASE_DIR rm -rf $WORK_DIR/app-templates -echo "check --- did it work?" -sleep 120 + # Create shared kustomize repo cp -R $BASE_DIR/resources/repos/shared-kustomize $WORK_DIR cd $WORK_DIR/shared-kustomize From 2129d0b7d6c82f82adcb135bb1cba86bf05abe2b Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Tue, 31 Aug 2021 13:47:21 -0500 Subject: [PATCH 22/50] Webhook update & GitHub Client retry --- delivery-platform/Untitled.txt | 2 -- delivery-platform/version.txt | 1 - 2 files changed, 3 deletions(-) delete mode 100644 delivery-platform/Untitled.txt delete mode 100644 delivery-platform/version.txt diff --git a/delivery-platform/Untitled.txt b/delivery-platform/Untitled.txt deleted file mode 100644 index aaea623..0000000 --- a/delivery-platform/Untitled.txt +++ /dev/null @@ -1,2 +0,0 @@ -https://cloudbuild.googleapis.com/v1/projects/crg-sdw-8-30-2021-2-324521/triggers/hello-web-webhook-trigger:webhook?key=AIzaSyCyXJSxs8HIiGh6AfoMv0ncSMDMrp8iGIM&secret=txeSngEcm8r3TGtrnOW -https://cloudbuild.googleapis.com/v1/projects/crg-sdw-8-30-2021-2-324521/triggers/hello-web-webhook-trigger:webhook?key=AIzaSyCyXJSxs8HIiGh6AfoMv0ncSMDMrp8iGIM&secret=txeSngEcm8r3TGtrnOW \ No newline at end of file diff --git a/delivery-platform/version.txt b/delivery-platform/version.txt deleted file mode 100644 index 626799f..0000000 --- a/delivery-platform/version.txt +++ /dev/null @@ -1 +0,0 @@ -v1 From e1052df5ba7c50a8fd2576fefb812f3616f10c7a Mon Sep 17 00:00:00 2001 From: gushob21 <43795024+gushob21@users.noreply.github.com> Date: Tue, 7 Sep 2021 16:09:58 +0000 Subject: [PATCH 23/50] Incorporate Clouddeploy as CD platform and add an user option to run CD via ACM or Clouddeploy (#14) Incorporate Cloud Deploy --- delivery-platform/env.sh | 3 +- .../resources/provision/provision-all.sh | 13 +++ .../app-templates/golang/cloudbuild-cd.yaml | 79 +++++++++++++ .../app-templates/golang/deploy/pipeline.yaml | 29 +++++ .../app-templates/golang/deploy/prod.yaml | 26 +++++ .../app-templates/golang/deploy/stage.yaml | 25 +++++ .../golang/k8s/stage/kustomization.yaml | 3 +- .../repos/app-templates/golang/skaffold.yaml | 4 +- delivery-platform/scripts/app.sh | 104 ++++++++++++++++-- .../scripts/common/manage-state.sh | 1 + .../scripts/common/set-apikey-var.sh | 1 + .../scripts/continuous-delivery/set-cd-env.sh | 38 +++++++ 12 files changed, 310 insertions(+), 16 deletions(-) create mode 100644 delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/deploy/prod.yaml create mode 100644 delivery-platform/resources/repos/app-templates/golang/deploy/stage.yaml create mode 100755 delivery-platform/scripts/continuous-delivery/set-cd-env.sh diff --git a/delivery-platform/env.sh b/delivery-platform/env.sh index 3e0bc4b..500e4c2 100755 --- a/delivery-platform/env.sh +++ b/delivery-platform/env.sh @@ -39,6 +39,8 @@ source $SCRIPTS/git/set-git-env.sh source $SCRIPTS/common/set-apikey-var.sh +#set CD system +source $SCRIPTS/continuous-delivery/set-cd-env.sh # Set the image repo to use # if [[ ${IMAGE_REPO} == "" ]]; then @@ -48,7 +50,6 @@ source $SCRIPTS/common/set-apikey-var.sh # Platform Config -export ACM_IN_USE=True # TODO: PROVISION_TOOL=tf # TODO: CONFIG_TOOL=acm # TODO: BUILD_TOOL=cloudbuild diff --git a/delivery-platform/resources/provision/provision-all.sh b/delivery-platform/resources/provision/provision-all.sh index 4eca4c7..5fee2fd 100755 --- a/delivery-platform/resources/provision/provision-all.sh +++ b/delivery-platform/resources/provision/provision-all.sh @@ -34,6 +34,19 @@ apikeys.googleapis.com \ secretmanager.googleapis.com + #Enable CLoud Deploy APIs and grant the service account required roles. Since it is not GA yet, put enabling APIs behind an IF condition + + if [ ${CONTINUOUS_DELIVERY_SYSTEM}="Clouddeploy" ]; then + gcloud services enable clouddeploy.googleapis.com cloudresourcemanager.googleapis.com + # TODO trim the following down + gcloud projects add-iam-policy-binding --member="serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" --role roles/clouddeploy.admin ${PROJECT_ID} + gcloud projects add-iam-policy-binding --member="serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" --role roles/container.developer ${PROJECT_ID} + gcloud projects add-iam-policy-binding --member="serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" --role roles/iam.serviceAccountUser ${PROJECT_ID} + gcloud projects add-iam-policy-binding --member="serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" --role roles/clouddeploy.jobRunner ${PROJECT_ID} + gcloud projects add-iam-policy-binding --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" --role roles/container.admin ${PROJECT_ID} + fi + + ### Grant the Project Editor role to the Cloud Build service account in order to provision project resources gcloud projects add-iam-policy-binding $PROJECT_ID \ diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml new file mode 100644 index 0000000..98faaa4 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml @@ -0,0 +1,79 @@ +# Copyright 2021 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. + +steps: + + # Clone the repos + - id: clone-app + name: gcr.io/cloud-builders/git + entrypoint: bash + args: + - '-c' + - | + IFS='/' read -a array <<< "${_REF}" + git clone -b ${array[2]} ${_APP_REPO} app-repo + + - id: clone-kustomize + name: gcr.io/cloud-builders/git + entrypoint: bash + # changed from GIT_URL to APP_REPO + args: + - '-c' + - | + git clone ${_KUSTOMIZE_REPO} kustomize-base + sleep 5 + + - id: hack-for-clouddeploy + name: gcr.io/cloud-builders/git + entrypoint: bash + args: + - '-c' + - | + cd app-repo + find . -name kustomization.yaml -exec sed -i "s?- ../../../kustomize-base?- ../../kustomize-base?g" {} \; + cp -r ../kustomize-base . + + # Build and push the image + - id: skaffold-build + name: gcr.io/k8s-skaffold/skaffold + entrypoint: bash + args: + - '-c' + - | + cd app-repo + ls -lrt /workspace/kustomize-base/golang + skaffold build --file-output=/workspace/artifacts.json \ + --default-repo ${_DEFAULT_IMAGE_REPO} \ + --push=true + + - name: 'google/cloud-sdk:latest' + entrypoint: 'sh' + args: + - -xe + - -c + - | + gcloud config set deploy/region us-central1 + cd app-repo + sed -i s/PROJECT_ID/$PROJECT_ID/g deploy/* + gcloud alpha deploy apply --file deploy/pipeline.yaml + gcloud alpha deploy apply --file deploy/stage.yaml + gcloud alpha deploy apply --file deploy/prod.yaml + gcloud alpha deploy releases create $SHORT_SHA-$(date +%s) \ + --delivery-pipeline sample-app \ + --description "$(git log -1 --pretty='%s')" \ + --build-artifacts /workspace/artifacts.json \ + --annotations="commit_ui=${_APP_REPO}/+/$COMMIT_SHA" + + + diff --git a/delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml b/delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml new file mode 100644 index 0000000..d7deeba --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml @@ -0,0 +1,29 @@ +# Copyright 2021 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. + +apiVersion: deploy.cloud.google.com/v1beta1 +kind: DeliveryPipeline +metadata: + name: sample-app + labels: + app: sample-app +description: delivery pipeline +serialPipeline: + stages: + - targetId: stage + profiles: + - stage + - targetId: prod + profiles: + - prod \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/golang/deploy/prod.yaml b/delivery-platform/resources/repos/app-templates/golang/deploy/prod.yaml new file mode 100644 index 0000000..f82f543 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/deploy/prod.yaml @@ -0,0 +1,26 @@ +# Copyright 2021 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. + +apiVersion: deploy.cloud.google.com/v1beta1 +kind: Target +metadata: + name: prod + annotations: {} + labels: {} +description: prod +requireApproval: false +gkeCluster: + project: PROJECT_ID + cluster: prod + location: us-central1-a \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/golang/deploy/stage.yaml b/delivery-platform/resources/repos/app-templates/golang/deploy/stage.yaml new file mode 100644 index 0000000..fbfc660 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/deploy/stage.yaml @@ -0,0 +1,25 @@ +# Copyright 2021 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. + +apiVersion: deploy.cloud.google.com/v1beta1 +kind: Target +metadata: + name: stage + annotations: {} + labels: {} +description: stage +gkeCluster: + project: PROJECT_ID + cluster: stage + location: us-west2-a \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml index 8a65f83..d6451f7 100644 --- a/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml @@ -17,5 +17,6 @@ bases: patches: - deployment.yaml commonLabels: - env: stg + app: golang-template + role: backend namePrefix: golang-template- diff --git a/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml b/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml index 9bc7103..3c8e5d4 100644 --- a/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml @@ -17,6 +17,7 @@ kind: Config build: artifacts: - image: app # Match name in deployment yaml + context: ./ deploy: kustomize: path: k8s/dev @@ -29,7 +30,7 @@ profiles: kustomize: path: k8s/dev -- name: test +- name: stage deploy: kustomize: path: k8s/stage @@ -38,4 +39,3 @@ profiles: deploy: kustomize: path: k8s/prod - diff --git a/delivery-platform/scripts/app.sh b/delivery-platform/scripts/app.sh index b1ca0ae..1130ce2 100755 --- a/delivery-platform/scripts/app.sh +++ b/delivery-platform/scripts/app.sh @@ -55,7 +55,9 @@ create () { find . -name kustomization.yaml -exec sed -i "s/namePrefix:.*/namePrefix: ${APP_NAME}-/g" {} \; find . -name kustomization.yaml -exec sed -i "s/ app:.*/ app: ${APP_NAME}/g" {} \; - + find . -name pipeline.yaml -exec sed -i "s/ name:.*/ name: ${APP_NAME}/g" {} \; + find . -name pipeline.yaml -exec sed -i "s/ app:.*/ app: ${APP_NAME}/g" {} \; + find . -name cloudbuild-cd.yaml -exec sed -i "s/--delivery-pipeline sample-app/--delivery-pipeline ${APP_NAME}/g" {} \; ## Insert image name of new app find . -name deployment.yaml -exec sed -i "s/image: app/image: ${APP_NAME}/g" {} \; @@ -75,14 +77,19 @@ create () { git push origin main - # Configure Build - create_cloudbuild_trigger ${APP_NAME} - + # Configure Build based on CD system. + if [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "ACM" ]]; then + echo "calling regular webhook" + create_cloudbuild_trigger ${APP_NAME} + elif [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "Clouddeploy" ]]; then + echo "calling CD webhook" + create_cloudbuild_trigger_for_clouddeploy ${APP_NAME} + fi # Configure Deployment ## Add App Namespace if using config manager - if [[ ${ACM_IN_USE} ]]; then + if [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "ACM" ]]; then for dir in k8s/* ; do #if [ -d "$dir" ]; then echo ---adding ${dir##*/} @@ -118,7 +125,7 @@ delete () { # Remove any orphaned hydrated directories from other processes rm -rf $WORK_DIR/$APP_NAME-hydrated - if [[ ${ACM_IN_USE} ]]; then + if [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "ACM" ]]; then cd $WORK_DIR/ git clone -b main $GIT_BASE_URL/$CLUSTER_CONFIG_REPO acm-repo cd acm-repo @@ -135,15 +142,33 @@ delete () { git add . && git commit -m "Removing app: ${APP_NAME}" && git push origin main cd $BASE_DIR rm -rf $WORK_DIR/acm-repo - fi + elif [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "Clouddeploy" ]]; then + #Delete the deployments for dev, staging and prod. The deployments with CD are created with default namespace + kubectx dev && kubectl delete deploy $(kubectl get deploy --namespace default --selector="app=${APP_NAME}" --output jsonpath='{.items[0].metadata.name}') || true + kubectx stage && kubectl delete deploy $(kubectl get deploy --namespace default --selector="app=${APP_NAME}" --output jsonpath='{.items[0].metadata.name}') || true + kubectx prod && kubectl delete deploy $(kubectl get deploy --namespace default --selector="app=${APP_NAME}" --output jsonpath='{.items[0].metadata.name}') || true + + #Also delete CD pipelines. Pipelines are in us-central1 + gcloud alpha deploy delivery-pipelines delete ${APP_NAME} --region="us-central1" --force -q || true + fi # Delete secret - SECRET_NAME=${APP_NAME}-webhook-trigger-secret - gcloud secrets delete ${SECRET_NAME} -q + if [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "ACM" ]]; then + SECRET_NAME=${APP_NAME}-webhook-trigger-secret + gcloud secrets delete ${SECRET_NAME} -q + elif [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "Clouddeploy" ]]; then + SECRET_NAME=${APP_NAME}-webhook-trigger-cd-secret + gcloud secrets delete ${SECRET_NAME} -q + fi # Delete trigger - TRIGGER_NAME=${APP_NAME}-webhook-trigger - gcloud alpha builds triggers delete ${TRIGGER_NAME} -q + if [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "ACM" ]]; then + TRIGGER_NAME=${APP_NAME}-webhook-trigger + gcloud alpha builds triggers delete ${TRIGGER_NAME} -q + elif [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "Clouddeploy" ]]; then + TRIGGER_NAME=${APP_NAME}-clouddeploy-webhook-trigger + gcloud alpha builds triggers delete ${TRIGGER_NAME} -q + fi } @@ -234,5 +259,60 @@ create_cloudbuild_trigger () { } +create_cloudbuild_trigger_for_clouddeploy () { + APP_NAME=${1:-"my-app"} + ## Project variables + if [[ ${PROJECT_ID} == "" ]]; then + echo "PROJECT_ID env variable is not set" + exit -1 + fi + if [[ ${PROJECT_NUMBER} == "" ]]; then + echo "PROJECT_NUMBER env variable is not set" + exit -1 + fi + + ## API Key + if [[ ${APP_LANG} == "" ]]; then + echo "APP_LANG env variable is not set" + exit -1 + fi + + ## API Key + if [[ ${API_KEY_VALUE} == "" ]]; then + echo "API_KEY_VALUE env variable is not set" + exit -1 + fi + + + ## Create Secret + SECRET_NAME=${APP_NAME}-webhook-trigger-cd-secret + SECRET_VALUE=$(sed "s/[^a-zA-Z0-9]//g" <<< $(openssl rand -base64 15)) + SECRET_PATH=projects/${PROJECT_NUMBER}/secrets/${SECRET_NAME}/versions/1 + printf ${SECRET_VALUE} | gcloud secrets create ${SECRET_NAME} --data-file=- + gcloud secrets add-iam-policy-binding ${SECRET_NAME} \ + --member=serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-cloudbuild.iam.gserviceaccount.com \ + --role='roles/secretmanager.secretAccessor' + + ## Create CloudBuild Webhook Endpoint + REPO_LOCATION=https://github.com/${GIT_USERNAME}/${APP_NAME} + + TRIGGER_NAME=${APP_NAME}-clouddeploy-webhook-trigger + BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/cloudbuild-cd.yaml + + ## Setup Trigger & Webhook + gcloud alpha builds triggers create webhook \ + --name=${TRIGGER_NAME} \ + --substitutions='_APP_NAME='${APP_NAME}',_APP_REPO=$(body.repository.git_url),_CONFIG_REPO='${GIT_BASE_URL}'/'${CLUSTER_CONFIG_REPO}',_DEFAULT_IMAGE_REPO='${IMAGE_REPO}',_KUSTOMIZE_REPO='${GIT_BASE_URL}'/'${SHARED_KUSTOMIZE_REPO}',_REF=$(body.ref)' \ + --inline-config=$BUILD_YAML_PATH \ + --secret=${SECRET_PATH} + + ## Retrieve the URL + WEBHOOK_URL="https://cloudbuild.googleapis.com/v1/projects/${PROJECT_ID}/triggers/${TRIGGER_NAME}:webhook?key=${API_KEY_VALUE}&secret=${SECRET_VALUE}" + + ## Create Github Webhook + $BASE_DIR/scripts/git/${GIT_CMD} create_webhook ${APP_NAME} $WEBHOOK_URL + +} + # execute function matching first arg and pass rest of args through -$1 $2 $3 $4 $5 +$1 $2 $3 $4 $5 $6 diff --git a/delivery-platform/scripts/common/manage-state.sh b/delivery-platform/scripts/common/manage-state.sh index ae9645d..9797452 100644 --- a/delivery-platform/scripts/common/manage-state.sh +++ b/delivery-platform/scripts/common/manage-state.sh @@ -31,5 +31,6 @@ function write_state() { echo "export GIT_BASE_URL=${GIT_BASE_URL}" >> $WORK_DIR/state.env echo "export GIT_CMD=${GIT_CMD}" >> $WORK_DIR/state.env echo "export API_KEY_VALUE=${API_KEY_VALUE}" >> $WORK_DIR/state.env + echo "export CONTINUOUS_DELIVERY_SYSTEM=${CONTINUOUS_DELIVERY_SYSTEM}" >> $WORK_DIR/state.env } diff --git a/delivery-platform/scripts/common/set-apikey-var.sh b/delivery-platform/scripts/common/set-apikey-var.sh index 0778987..fc4da91 100755 --- a/delivery-platform/scripts/common/set-apikey-var.sh +++ b/delivery-platform/scripts/common/set-apikey-var.sh @@ -8,6 +8,7 @@ if [[ ${API_KEY_VALUE} == "" ]]; then echo "" printf "Paste your API Key here and press enter: " && read keyval export API_KEY_VALUE=${keyval} + echo "" fi write_state \ No newline at end of file diff --git a/delivery-platform/scripts/continuous-delivery/set-cd-env.sh b/delivery-platform/scripts/continuous-delivery/set-cd-env.sh new file mode 100755 index 0000000..a3dce9d --- /dev/null +++ b/delivery-platform/scripts/continuous-delivery/set-cd-env.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +source ${BASE_DIR}/scripts/common/manage-state.sh +# Set continuous delivery system as either ACM or Cloud deploy +if [[ ${CONTINUOUS_DELIVERY_SYSTEM} == "" ]]; then + PS3="Select a Continuous Delivery system: " + select provider in ACM Clouddeploy; do + case $provider in + "ACM") + echo "you chose ACM"; + export CONTINUOUS_DELIVERY_SYSTEM="ACM" + break + ;; + "Clouddeploy") + echo "you chose Clouddeploy"; + export CONTINUOUS_DELIVERY_SYSTEM="Clouddeploy" + break + ;; + esac + done +fi + +write_state From 80c95e63e6c6ebb92a75fccc8521694a49c93e54 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Thu, 16 Sep 2021 16:06:09 -0500 Subject: [PATCH 24/50] Including images (#15) * Migrating images into repo --- .firebaserc | 5 +++++ .gitignore | 4 +++- .../docs/workshop/1.2-provision.md | 6 +++--- firebase.json | 10 ++++++++++ images/README.md | 8 ++++++++ images/clouddeploy-prod.png | Bin 0 -> 45638 bytes images/clouddeploy-stage.png | Bin 0 -> 36526 bytes images/clusters.png | Bin 0 -> 13819 bytes images/folder-structure.png | Bin 0 -> 8407 bytes images/provisioning.png | Bin 0 -> 6452 bytes images/sdw-title.png | Bin 0 -> 3653 bytes images/sdw-title.svg | 1 + 12 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 .firebaserc create mode 100644 firebase.json create mode 100644 images/README.md create mode 100644 images/clouddeploy-prod.png create mode 100644 images/clouddeploy-stage.png create mode 100644 images/clusters.png create mode 100644 images/folder-structure.png create mode 100644 images/provisioning.png create mode 100644 images/sdw-title.png create mode 100644 images/sdw-title.svg diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..6fa37dc --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "crg-sdw-imgs" + } +} diff --git a/.gitignore b/.gitignore index 172a41c..2389ff8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ terraform.tfvars backend.tf .terraform/ .talismanrc -.DS_Store \ No newline at end of file +.DS_Store + +.firebase/ \ No newline at end of file diff --git a/delivery-platform/docs/workshop/1.2-provision.md b/delivery-platform/docs/workshop/1.2-provision.md index ecebb01..7716aea 100644 --- a/delivery-platform/docs/workshop/1.2-provision.md +++ b/delivery-platform/docs/workshop/1.2-provision.md @@ -1,5 +1,5 @@ # -![](https://crg-sdw-imgs.web.app/images/sdw-title.svg?) +![](https://crg-sdw-imgs.web.app/sdw-title.svg?) In this lab, you will execute initial setup steps to configure your workspace for use with this workshop. You will also review key Terraform files for use in your own customizations in the future. Finally, you will initiate the provision process to create the software delivery platform used in this workshop. @@ -69,7 +69,7 @@ Click **Next** to proceed. This repository provides a variety of resources for use both in a workshop setting as well as during your own exploration and customization. The majority of key resources are located in the `delivery-platform` folder. -![](https://crg-sdw-imgs.web.app/images/folder-structure.png) +![](https://crg-sdw-imgs.web.app/folder-structure.png) ### Docs @@ -90,7 +90,7 @@ The final directory of note is the `delivery-platform/resources` folder. This di Click **Next** to proceed. ## -![](https://crg-sdw-imgs.web.app/images/provisioning.png) +![](https://crg-sdw-imgs.web.app/provisioning.png) In this lab, you'll provision the underlying infrastructure needed to run the workshop. A software delivery platform consists of four main components: Foundation, Clusters, Tools, and Applications. diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..ea53ae7 --- /dev/null +++ b/firebase.json @@ -0,0 +1,10 @@ +{ + "hosting": { + "public": "images", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } +} diff --git a/images/README.md b/images/README.md new file mode 100644 index 0000000..9f7496a --- /dev/null +++ b/images/README.md @@ -0,0 +1,8 @@ +# Repo Images +The assets in this folder are used in the various instructions and readme files. They can be accessed via github or firebase hosting. + +To deploy images to firebase hosting perform the following actions. From the project's root directory run the following command to upload the contents of /images to firebase hosting + +``` +firebase deploy --only hosting +``` \ No newline at end of file diff --git a/images/clouddeploy-prod.png b/images/clouddeploy-prod.png new file mode 100644 index 0000000000000000000000000000000000000000..7c0d3a9bb2a5ae76fa73d38c66fb0078fc7588a3 GIT binary patch literal 45638 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≤)eui|78Flf3@E*ebAqmF}3* zchY}-+`XnXqur{*JdPtdg)K9{b~x|VnyoRV9O4(pgYOt^1oZ(BDxj*fV!Ec$5p8>( z=Z9ksldjNk>|@L6Nu{q3Z;EMCCN)2Dt>+K-NMZ97I7)9V$eP7;X$@Xav*NAZQS+Y) z;&Ws3TueFLR}}BBh0W|zO?^>bie;1Rk|Y&$&NPKyG$&gnHVO}HHg}loXMS3PY3k2W z>5M7|&oRfqmPo;bZH&rmm(_#r;rfIe`EHk`u)AD)8+RQRyeo$z{Uq{Vz|msTFm1%i=qAt(w9v60f0$I3|v@Y zqK41Q_YAhJSXMAD1E{&=EzNQEi!ZUL?hd93r3@61Kv=8z5y!7niey=P5vk~`9*M1i zoLBxUrQ!UXv(*Wi(OX-*ZQB#JZrjs~05s(y_Y#^dC(`4aT9Y`1KTPg zUqy{-DWbW5Y&tSL(;y}vrwrOj)uDeRdbWZ*ipG0n*;c7^eGZLk^whdWe>luE#uPZ1 zw}1UjMFp1P_&U?{2+oKA*|VZ{RIfe80}(vYwMQn6Wwer@n$cpC&~05xR>Tnpv#NBM z9d^Gl*lQVp9;8%ux8ozSeyuj+8z7|Z(j;Qh&0d<}-%*#&RyCI%wbt92!Y=2ynoO>2 zPgdm}&Les0HVJZbYqX7l3~<+l??_(@4dnCx;qzz=N;2*gQ#aeNUJq8z`!t5Zxi0R% zQ@hJukYuLm=72veGfo>gH^KF2JFcd1TSLT?%*LE;@!2wv;NNDqh-;F zO!fQGY0C=P)`;W%K70s^=rV^i^Amd`|6NaYAacAbt{Ihum6Tw|ICgtJ6Abo4+u*|P zD!k|f*JyU{&d1r6;BzWFBO`ZFkbpt9q^^5|&`SqL9Nk$I7&}YhPSLfp`rNfd0l`vM zEJp?g9}@y2O~(;6@ylKq^2X3*Ny&0^1ffx+UUNukRko<$km-lM^u&}HA`mZdavWb*s|Q(P_!ho^kpN z4ObUq?sA_9#vI?cTf2s2OPsU!k=a>W*)f_))0rms!NQU>kAvM-+}^jD!KwzNnjG>N zkuDK{_u@~WA*|X__xcK1T&EycGL0P8B>0rQZr=CIrS(Pm63SufI1yK0uAWa}2YTg7 zNBgNRk>Z$w#V^noBF6_d?)a$^gi48xc4=(XfWtD*fIRjCa%xo4?#jM!Z-?d9xdTvX zu64YG+W~fY{;QFCi-*1wtJg~YMonqm{=c%7|2uc@7k~3}j-uh^#h_JnW#N|5qN?UB z7Q?%`nHl51-(9{vF2Q-*Vq>a~XKE}>;Tn+Qn!hRe)KiZclNz2)cv6(Un6(0lz5B%`yCsIO=OF_`y7gB;&VwU&Kd1GTXO9YkaN0Ay=SFcI_~(9<`d_W6%vp( zv~d?YJR+gGX@hq;&LVEMJYQ$>Qq@ z{sH<$g6&HSF@2DUp_gx!rG9mY_ulK=!JdlP+@be2Wb$F5q#L2nSO7iq%Bk zbR6-mYO5|4H902=CCLDiOvhZ zhBlr`u7^!(2pFCijUo1){#ZmtNOZ1e%A*DwWZxBl!J3Ox@gFi>E!+e zLX?uAy|gKW6RyO`Fx9{+o%$TB5w8c zPon1UCqAV+Zgz?9#*@}*hAbP{P(*_EuJrc1k*eqaTigG^7xBBn{&_7qS4icaU8>5} z+4uMRH^O^&^a@`P9%n~KMbgizJahu`Zt1j-5W^gtvW&03AoK_(9;PqNK+Z58)fs0< zo3bPlUDKTI;25b*0HTTIYY!TpP*y5143$EW95LO5AAd_mS7-h zZ_Rkzjjf_@&8P-acM});zu+HeeP^qMh3L#gS{=LGICv#ika_2O0k+6c=LpDi$$$R+ zy&!WG`EYnnzql&oxg;NFp;FA7FVHox$l}>0tuxEFO6$E&^T|sk*&!A<>(}1UDakqW zgBt{ZxJ=K!PoTdq_zEcZypI9WXRw1GY_xnJXgm5?lTmtZgMqdm$^+I~W+H91@m9Xh z$?*dVE{|tqHdM`LMpSp!`yP`FHOr-j?&=5l9B#_GaWjj17SY^78~f+jGVNA-Y|pRh zWa1+lMF)j7r8I_mj2O)j0-SO`9`(b-pAJb8@Vi5yHOyk66+}pKR>5qAfuL3YgD0ZD z$a|4IHHVIFVovkz4&kQXfwz{Lsa69@KEqw9iCD5uA?(tIN904)-trE z`#GjRXD!Vt`Z-6sg}j(n(0LIT9cG!$Tirmv$0CY+x+purrd}Yg7^OO|q@$bn%*+_o z{42C2W|+^%^C7-nxh=ySk#=`z$E3jjjHTsEmXAL1nt@+q`Km0aO=bN)FmP6IRnHRj zDSvFq*888|+h;KF*w4;xpQm3mw_IMrOm{6a6 z%CLwAA~c-g*$y3S9RSrIA{D3K>lfCi4VN0@=)VgFazQr>;ElFEG6A> z>^*@F^WgW@qlM6%fla#UY;n=VdNLH>Mlx43QZ;uFSmBUNBHX(D8#IO0t&3EOb5L8~V}~ zl8IGOeKIlib;oPn=jE+lPCS^qy0@TgoP=(k>Cb=AjqZwwN|k%&J}1+&LdH;64Vf1H7Lm%4Sa3T}Vo)!%Rf(r6|<+$htVl!%h0AX^Z{<3S@+ zSKX_lFjo*j#d`hPzXGfW8Pga#ADMXK*tP9x06^%$6h4kBfGJlz4eFO-MGvIriTFkbJnn@tER9>w75_-144oGgh;Sc7V#UQ6vPQ~~LAIED* z`ovgtweqd?{RnM)2GjOI>}GLjJwOAAQ?4qVhepf03j7RrH1gmhY2XshjLc8!Ft3fa zyQRlqjjW~k{d9`l^dy%-ZJ#hNBIz>Mqa2Le#EtK?NP&cFIOhxf^riz^TUd^-+4HBe zd_0Z2-lKAgyf>cQ-qjZRaKh$tK||QG=)b+LuoF>AuoJ+A8sP7pW>Ht;E~}ET6N!c} zXWQr}vBdWu^vq`}CwNT^OZ^pE=lHxNjr*Cx*5me8p5@1PfhP-GqQz?*f|jisB*M@D zhxS{9@ttJMW3qc;S_S)v zx%XT0lp`TGaw0)1PdzOx6_s;9T#cK00#nmT62(RQLQj=9J|EO-I=GQF>&z9`YQm|Y zm6nS#+XZ+uiRsYAz%wXif6A@LE)DX39xnZi0BxBO6DLLp>D|LM=0DsEpMSVv2H79q z55%igU7$%fl!`HiNN4sIGxlgisY@o~n}`6X@4F?PevXy3u<(N{lfFm2m!xXPuUCPe zLfq4g61%=?jOZaAW@%LMaSBA`%3n2f9YMS=STegBT#W9mvhwkUAqErOCOvpQXIT1( z8FJ6G%|I5>Q;7w<0~-Fq1srmj1c9U6Ryu21EzlPru3o?~g-On8tc0;^OWWeq`a%t5 zyMBGV<|b{Zoe}152R?1BXM$LVSco~!%Bd3_jiHpr0tjkZ8Ut$sz*H2piwL#88z}G5 zs$5p@%CvM;X<4odI9u7<&2-xXNcki65RnoW1X(Ie$^pXNhAHYrL`s$Wrt!5R&5cQ+ z;ME(ePIC*S1;AvMxd;g6b}oqTj++1^8K<-~f_9&|?1Jtb=7fn?;DhooKQRVGeG=qi zQpydz7bC5$*9XmENtan_kVrYM*DQKMTg8p`a2m4)SsuV-{~43yQu8ELOwy8W*=K>1 zNVTJ*Ey=X?2a$ZyH(F9LM77S4iAvhfEV%QLxCJ%)=WUQijyA&I<@N_EMC%T$RxMWm zoxd7`7Nehp>0B1B0F3!4A#02=&G9v}-AEu(l-LC@P#k6|0GeL%fGM&vDff z0y6>a6r};T*>Eqa^3A0vIoL#`jFsB0`w!~eE3iy#bRjn7*sPwTyH~c=tOH*#?MsIu ziyI^~zo3@KGvmtuk8$z(LJG$h1rQiykMyA>)bGE(ucPn-K1@>@8aut!fC zgME<&UD&TIt+x(HXJIZL4%^$=fb0Hz74<6@Z8MZJ0Wb6%$k>i~M?s~nYXn$ZIFQT0 zREMwVhg?>vg)x2xmpm6lH%Ba8weZo1!*%RcO^-p zc4Il(U<(G6Jq@vZ`PPFNOW^ZTUZ6MNVvF(p~ zhK2irFUgV=t+feg6fZ1GV>XM!GZ&QuLr1keQ-Yk5c%G1d>CZupskYzOvncYHqr5}& zIC6*HSfROHn6zPySQns`;vnh+v##w%gWI1YYU72S#;-TpRTH8(x%JW&W4>SQa@>xP zMw_T`ELuoUa+TIImdUhw4}I-#RSe+@O7M$Iv;K>nVd_WA+*j&3FoQvj!CkEq!uj?; z58wFL0Kdx`0U07>ymqj1+yoU?1Q}^8F&Ck(I9KW91dJ|}#s^&`K#3bad^t7zXU5wx zKq?S%Fn>lYW5l1& z>yL47Kl*c*cA%H(Xm0-3-cMeytqzP}P>gql;qQlVi_#yiuD9UrPz;OJot$ zfXLooY5U{A{(MQaH2{%a6@RpH_3uS?lO-!Byx1nB=`;&oS=KPu9nwv&5LA&rZ8 zkWc&fOZEKZyy>BcwKbNQRF!$d};4Yvn>fajp{(h;h zGBkH5uLJmldrz*_s3TV3WrxxbK$&hFK}74D6`FTl*e&$fHX8#dTze?E`xge{Qc4i3 zBOE9!I2VoqGB7~Qoy{kSk#{=3qx}g_iQj)0q!b3k$F=AMBzANst32q$PI%`4A@+nJ zH)7~ZW{`-wDHAxH-^6O2TeT(cuES->Wr!W0^gTxDR=Q^6U59e?flM9;5MQ0qQxS<8 zleae1MkJYa#Li)gB(1;^(Y%I}Kw2SwP)5N~92CC0Iu>md^J&t|KX~(R_L!dy&u7V0 z;xSN9l!FJrmDY)iif7=2m%W}Y#rasD7>tf2N!$l|4r2kU{yc!!H=pOjs5=Xkt>8DlR{9?8|L{6)=kFRmp$lsln(;7~ssB(qA+lDu z`1@Yv)-gqqh%RZVU83t^UVFcTYyrGb_xyqf3Md?irRv4Htd0&KE0*r5U)=(VZtx{; zC+t53-s;(F=Oh7c-6Rkme$;n(LWpDO3OvvJbNjX-Ex-Y0=>kFnbnTu131ec1Lp{B0 z=?m%md%Amyc?vw`4Io42#}-BaswkauuLMAv6G}1mF^4oRRW{VM&qWOTj}jYzU>W|U z04>nD33w!H=CUZbrAHkO`RAw7zcYO9anefR34vRuCG4(k<0r;7 zAf7AxhW`sgk;Ww*O1WT}D)TN@5-=yd@@2qpsSNA+t#s^>!q+Pjl@5&qQdJW_3uQlD z|4oE3c3A-{EM*PUN{m+lrA=k48T4B~>}8RNR!Mn!QjAf7Ww?H!WWDBi61wYW8#yR3 z^NC-v&N8x>NA=qq&f@I+5MX|Nr$>hCd?I49QMnqKVR2P1csL25toeU@LIb7toCIpI zh_$CK4g=KsHL4P_$hYnOkh!!!9Z%TC&6{pvgHaS`G7EF7*`<4P^I0+z>kA{fF;hkUtgPC zUX?Z@d~qT!Sv1ZN3LZL;uX>+>_vZ7`(10`N3joTqNS~&G$1j%@o8<^WgswH}xZ8(A zE+@U+rWj!H1)aj8;n|L9MUe>aafhZDzHmT7R+@&WVw%<@r%quc3n(h+BHNmQ?2)Sd z-cK4{=kS_?WlnW?p}f+G=fR>56n)*+bkMGUnb0HyH=_C*n>M1sULSx3sDOnh2~%+% zAlD2KAHUKxLlg$1J(SvWpmBopr|LbnX7k68l^xt!SL&_6TM#E$+y2z6&FNqDBl)dF zsI9t`u-#;hc7JXx-|GRuaM+!=btCB3Vc=k`@frup;gCM2?-4?ec4VbhgLMHv%D=&? zbr7b$KhvNCRAWwXfvN=Nv-!T`Jn`Juo@4{LL}T>x1fWj9O`QV1@KY$#4X%D{Y1!Hj zzofNbG*#<22pqdZ!JYXfX~|`L9|pizxhz;job7WXUE*WYDGEzcwsU(su^w&+PV^o3 zyt^)}0d1R1{~ZeWt?+#QJ(qxbZ+0=Q_h;*9zdMErCnNy9o4~*|I9vvlB@_brsImcp zd&mS4m!J9^{2Co+t?T*KHf|YWy?>|4NAs*2a1Nu1^%o_}%n{6I145C;i^@ftfn>4B z99DoH%^8VtFhAk+3n|P@-y2-3Irb4+%aIr2RDh4<>lm19rI)mL0(<+UUNHJ@+|{^d z+Y5|*4WQge?FqDI?uHGh9yX{AP`bJ~)kmToF7QTtxOOB4)DOj+8x&c|tH%J|bEn4L zpXB}Cr-1xVC;VN->WOXqEsgu$_!0Usp)m^9t?8$a@Xd+@2xDUD=ymm&Jn+M&g3M?6 zu~e}>wA#3uNzUOIt;)r3AIG^OKVX-$(28k4P7Jz`tGzM)L7x_-dN2Xq+kNT`t^GyB z5J0H!9$D*67=4BzN&(K0 zarDS|-YXgP&$=_*q0B;^Ain_e_t|23S2REy*!}#=Ma)W~+qaqOuWOC|37Of0XzsA4 zwfmpVar`L}2d7kEb3Zvgp(cZ7&L^cUV{hcIa;gtS`zf`!ex&D^ZdH;59{+V-cUo7X3!(Ij6^4AUk8u&>{&JPQ>f~;eRlV0m@o1~-6b^Nh4 z$48ZIQgGkaS%ATfF$j#~b7G)}x7bDw!!3~lktM{m@_L#dQ*ZgI2JR^*u7u!nzma!i zX7ev>kZdFZ zD*%9`Khh8A`^n$8IsMfnWkfLi!nwAv6*w3Z-x z5r85YZLF>dr;iQ+Bej-L`ETJDcGYcXAPkY}TNcH~zkN8+6DshpNh6pKpo(5W0**lbE zJ2nSLgA%1EvQv@0_iS0&+2aTq+4JBS-{-r!_e%F(_rKrok9+^Ro#VXUukn06=i~W! znN@ms6*#3!OqOgJ7w5PhrwSlAH3RihSu)vtyODwOL=S#D@u6sCUh64kXOfOsw%O5D zj&!xYPdD!CkcAWGMvP_m*agUi3fKF##2^2_F6#$KjcrW)R>ApH+FWn5z}Vg8K|mwf z+pcB{nl!u%G*UgL(xGPhDc0X5Ni8{?nvJVZrR-}JM|>zc<#lJ8IQYjVP^HX-mSL}( zDNG}38tF$WwA}`RjkQf~%t625>I#8esILaQcS8N-7eKV

pSIAFz)`WIp#ZT$&xW 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 literal 0 HcmV?d00001 diff --git a/images/clusters.png b/images/clusters.png new file mode 100644 index 0000000000000000000000000000000000000000..0453f5a2f8ba373d7691ba5f673c98a253ad6a6a GIT binary patch literal 13819 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 literal 0 HcmV?d00001 diff --git a/images/folder-structure.png b/images/folder-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..96e6e1c97a74974cb4dc7510e60137446cd2c8f6 GIT binary patch literal 8407 zcmZ{qbyQU0yYEFBhL9XU7`l;Aq(M3*B^8j4A*EYFx*KT_X^?J4x|DLLLAtw!uDkuM zd(K+ttb7016MN0PYtMe4_&%TSM5wFEt3V&1z-2Fkv<9(;;HqMk^U%&4dr6oQV1HpFVegk^dmp;jX{ivO~%Ze+B! z4(>Ag-2JAtrIi+K=*`9^zJLV3Bv$;+co5$*44xi6s86?iwDYsxF#vfnI5 z$ZDNdq+ndh9J)FzFn;K?LZt!!Mi|6D*?zu}X>Ki&K&9T;zeVNZV4nQAngYtE^*uR; zN`SdY8k-W^wt|#HC;B9Zc&pFFe7^Od@%=_M?BSyIVEVmd z3hXhqrBXd#s@8RfoX_XN&M#Nk%~mmq*}gq)qjBCL3HER^vgLj$?ztByVS3vMJ7AED zP7Zib%<^L*2}IHn-o;=C%y6(}1&92!sKBFo9gp1kGx93~a&33!eR|=B`sdj^=bZcms*{aCbJw=XFXl*X~u<={QrB{jOX`8}DSb zFN)8s->2532O~q&EAehi;xR!khGt^PayXgo=GMN*dIFQtW%JkRTAai%@H5>c%=)8K zamil{E_X>I??<>{j8>yr2@%9BifJ#h`PWqv=)b74=lWf={K|MKzdKXSJyouoP7u(?6kwH+Z;DB#`y%#;SA70=p4Z1c8MStE zd}3n^^0DKC9}oX}*{TpTtHkTqS&jgE>1R~k?sYc2-1W$E1bYmux1Bouy1ljNb2Mcp z7ow-*Oh*uhagV`7)vvfXEA-5e0ns}B5%-Kp{sd5B-d z?NY5p)GK?03`+vMR7-I*OQ;CQzuJ#oQs)jK_Z+%ai8Dq%hVDm;Tpd1_saZlUo?;>` zE?d(N{+A0V6vD10zcK}kamZh+BNppzv`V$hQz@r19r1Mwsa|dOK@mXI$9z*tQNrW4 zN7SE=OQ#gzu~m|A8-1W>U^w^mJ^y_sNXBhcfV!{~<>FvHki?J;AZpzYgfk=p0y_s@s$>aDVp1Db`Q2U!6N^BFUXWl6dS72IJ*t~6KeZgo zd0S9W;M&M#*qY4T=V>wvLX>JPr2v&h0a(xN`}erfZ{3$z(w9d|+-edFvZL7|)x;3?w})+N_|FAdz0MYkG4%4@Nb+0WY#w>r{{`pg`kf9H^94Nk>fWN`lEpK> zr7Slgpt=a9iG%T?$WVC7M8y75N@lI1fmC}x+<8%mT-d()GGRKKWyQ=E9v&VKgpQo! zQsq5^NrQ*u^4?}IWB=Ew%r4UsY7z0Gd~GSpG6AfFLFSnXeQ8^GNmZ~T)>#VllRB_ZZ0Nojj}K@le$K7U@=34~4!+Qsl-P*X5Q) z532(N|0!HcZKs9pePgs8qF%8_EWpo9%0&V{A_q+Qs-zmb1t=9GW!lC&ohMRXaLL{M z9N{HR@Nh^#dxJLmRDp%qmn&5678J96Eiy+)965p9(O4 zR-f+TwTmX}S=U<-`m`P%YlV4v@cd}%VI$H_bb+->k*9^q?r8B+A@C_WQJ=aj9&M8) zD#a6E@hN`&4sSQN=`|_yK_`#sBdeiA*+z$Z=DbGiGhP{tt^vqcVy=JWWP{Bv+{PRz z`Zw9rm5?-1uhYRiN$#C5=BWo$dqTQYAQDVbto-8L*X13Odj?r83wq)SrQmb3}Ok+A5g{P+`t*GTRV?@6r zR@7_`@7jW}59(6Krw#J!o#I?%n1&0Q^ zxk(F86<-+;xTFVYG3#gEH4n+#4J34HXd~-Llf$G#{RvfC^regvi$GQ#Fz9_ zg3hHAP2OeGepw~JNlke5tn!d;R})tyvbmh1pVU?g;(jO;?yY~j78tANh?+e@u;G&oQ6x@-X@ z)U*_$#2M+9Y>Qr2LjL}Jpj{juJ-(v_wB`VljT8i5>AXFha*5>0($b||G7CTGNVr0? zt5B4@Cmh!R+D;}}j2T>&w-yyAETj)BYQ>~KF4rlBj4Kk$)4V={6+=FJB;hJgp;aM) z-auYaVaQ#nt|UPAyxK%hY`nY?t~Leqk1w#gAzVRWD3?dVz}m>W*itXv9?kpFqe#8v{) z{1Yx?@eZz^a`y%?$!4BvJ+URxe^v`PfvW@j_3**a$QbLo&l z1;!Za+eW4ECyCFoJW&v$o*Ko-2yfYThEwXrpST6nx-FkbGsJvi0Uf3i1KuG>#=*Kh z?kuR~oZrX~qnX4uxjZ!7onU_ZwIXMGw{x#lUdn?E^`l5Y-39WUuq#-=A*-A<^S+?C z_oTVb|9*yYvS2UdWZX*hwGq)d_RSiS&SIFvAamGUnvS1%4p5bbt?k8Xxe3o0Lie1* z!wd>hC-Y-};pL=$_obopteR6|;SiV4YLkhqC?FnNKV`577$ z@E~f;ifM>VI0_7M{clSB{V(<@w3qe}=f;vY?XOzfLIy*U53zaL$-l~1%qgdGu4q!q zY>aE|5FQvzA0Zcp5SF99+BR`+`KTIb`F1kB%s_Ajvi}a!^Pp{N&vK9FbgsgKI$lIc z@&)!W3tKXBt-9r&%auZiYWe0}aKi(`zE>9hc+pyARNSu!j+AV2STN}8to9~a`a#1K z!c5{OCD(0)_!u(^Q}C)ax;X##vYo_)dx$H#YsPzuTBd!1g?>)fG}>wAJ{4EvsWOVW z)=79plt&zoe z4Ao_fi-EBgTkPeUUpE?aaK$5+(?UNHv{Z4u(Bu-kqtz3NcizYNnO$5t4g-&Jdnrpx z$}Q)Q%MG#=e7pQ1L2+}A|BYN`NNVBC-hq2U+b8<@1sKub0TY9#XNsE%nH-EiUy=1( zI-;}a8R>bFE|Yk-yu9T2pXD`FoIo&s@B#$OngGo6<_5V_&hFbu3);-r0A`dadF#9D zEo0v4l^h-l=h5fr0o_gr0=Q>5=(xY+Ic2_b!9y_?C|*};S(Lb+D&$*EoN6=8(}an2 z*p>4mso48~&|Pc_#O$#OYm0tK+tFhuW+MIY_X8LdRy6(=U}rjw zo{v2|t8poV6#vy2{?iFg_;j=+UPbo~i2l#M@SolxmB=?uRqAgIR?N20OCGG!Jb10q-?0oS^i5(MReo zzFq09(d=@Aw?u|G^6!>eu}|llmi*;Mb0w-xZTM~B<4%-)p&2i&hHEWG)|_-eYd6hY z01K%0Z2bAgJ@QA#vw8Q37bL~t&*Sco4@Y;?P~m#(6YHOt!ILyW3x|`8tWqFqN@f(ujuZiE&S zoo<1dH*KNqa{B_8d{lY5V(wkNO2G_lDIh?hNM67pTKxzLj*uO%>jHDq;H#0?nQBSM zB!f`hFm2S6$!pD9*!4DZeo6IP0OLG+WDGkUg{^CPf81lP=+#FdWj%@rrSTZjE?$rc zqTWVQ;)Tb#H0I?x9GF1iBCS0)$o)Civ=cQL1ndwa9aVNh75%-mmiw5Uo0BpuM z1;veqzOU7NeK5fYeICRBM(fjC+tVOczK?$|Qvyjuxp0>tuh{2?$jOxE|;Qx~m{_m5C$?K3uMR9;G zC9-R?`vm}Hw8d#<74Im`gb_Wlqcz`zkvyoNOQr$G)CQA)2BhFKl&@R8WvbAiP90oD zL?hsF__s>8(QdAOVK$l4I_lHwF_@Sn_QcHV09X{>*wT&dNUkVoi?;JtAdy_M+^XoK zNY!pA#P8M1<)pp03p|dC?R=ick3+Xd0eJ>p069zn;9m)XuG>l>tw7=GWB^kB47oia zy`j%TFEhccmO~HB+|!o_h=wHFsWRIZ8g`n~EA9dPsgNw*AzsAo83z~$jWJ^wqhz3EB@Tr$r1 zA3yTT=%(td2-wt@{YlNuAcF&f05_9!fQeOh7OKHF3ep9AE}rkeOAC`Z^)>&o)k3+B zqGyVa52(ntdH{4;k|q^G{^6J^!(5#WIhn;0Bk!G}w0AMju(cz%V=~jsiyXF(hgfol z@2<~^05NJSy8V>l%I|pjvh|@7z(&5nEx9?6`hsJV{JBOj38$7DI#ZQ&eMY|RgA6`N zA8f5KIM=SYM>IgwLc-!EVJBHCL>xL5@Jk?KQu!=~t(`?F2LQUPdv9v=>{;eFxtQIu z&hX+55dbBn3b}}DsO@jdPzbO{j$xSnV2n{0crNDcT99Q%ZOS&ZQxr!#wN2FC1Gy>x~9rrKKA((xe4z- zer9~g(-lo6pbBI;6M)Jl`8kC9eJ=J@+46{G&`!qE4(nQ*;UkE5!+H{GL>!A9_PwHnjDB4b!5LP9k6EFBLN(?HKxNXq_B zo%?n@>EQhGeUn4VORG`qOgs5g64Zg&jxIFGD@2~8wBb?W+-4RDo2JZ6*FY@wpL1TC zoL*9>9y4Ba?)ghaW{}5Nj$=b5?C~{!UuH(0*Yp*bU4Oi}gvy(pN?33)Ze9dn#f_H1 z9NMin;~Wkw3ECFNb?&{M*%w$b*aH7R7zLlX`n5*4b^A-$T%&!ae_9B^o&n83mwg#3 zCjN&%0$~7R+~F$KHTKh%Xz@I;GK8UKd}_pl`19}PaZR_@%TM3b3>dWJFu!^FOaGEm zfMrN$VLYiqzito!_UX-W24}Q z4A}sp^gCDWFHifM{W-_A%^o=3Kl8&)8mMsZcnc z#ns52qjtLcHzpHx{cs+KSKl4B(cuiNckapbcY&(J4d1hc-BjV?lmZY*UL6pBm8p$7 zuvOrrQg0i0Ny4;8Yw)w9zZ70?Q=&y!$>=v$@!(Whe8Ot8jo#C1@7 zjOP6%o>l2uO#x)F-e6-ab*|>vZ^k#WAV4S9FJ1(xE)d@ihW6NIy0+0^(l1+pVU&DZ3wIlsHw-Wyd%+w$s_Yv?Tz?!eac_4s#EzjvNQ(Fs~HQCsPZesgVX*al(74YHs9l}Fa!Bv<$?~8oNUu`fu-kF^CPKT z9Mr<1rG48Z6 zl)Wp}oU=IIzV5t0KyR9go%2an2E$j`co%QZ_l9~SlHN_BICcP>Ll-ihMXwQxN%MLi zkK?%jQAnOuFs~jmR9yh{+EXY|3h;2WYmO!gmo$4)tc#>QQZS;u0AHZW$sa(A;y&_% z2M=gLK#8{5-rJdDGAKhAqCf@>0K#Bb0G(ai4E%uAS|hw0`se#w+?y{>B)GZ$t>u7F z(R~d_4#}6@I@8e}i`Uh|w4b;JeK&`aEAgYWPv`4t!XZrlCm!17h>3+ICw5tdVr4{qfNFYLd zqIqU~pa4c?H|c=LFP_>&GOT16r>?JEeNP4#+9Vj}>a1qYIk*FyGQ(LG5+w28-H-E_ zGO*^HZ%-7~T2HJBhIN8ztM|}l{f)=Iba}Se6GJ>_-_gd4#U6=+ddR;!fN}qRRm>LF zPCP7hb|FMsErbYHcz99t$Mh?$gZ@8W0w9tArLa|5;eJSt zy@tx?&zS=HX|oqla1Nk#c-4U<#89@$U?GV*JMGPEHK{flQ%vR5Hy_VSLyGZ#2NZK^ zj#w@bpOtw=g64x9Wia^vwS}e&Ul?~vS&n9vTDSnHDjpEK3K@Jw4dK2U!`i$z3V|Xf zhaA=IWht^UXjp|NpWECIK2c*z*{!Us7>q@hq1{W$;LG*`#^o?zTBg%(%{L1v=)v|aNCsmlb_Jm~=%js}ePQ^I)X19e!b|yludozT*c-}e$O|`)fJ`f!I1^we z+MB8Nh!~=f%VXql>%pVy)QM1c4K6Gwc=b!v<1h=522!vn@`ZMz zFPE|<0{nw=FxNI|^-HM$Ka)wwX*tik)A#zcJeIrc3px%-GGJAett9Su@vy~vgpFyW zJN^8;AZbDmbW_{fq#INNsF&pZ-6&IBweCDSW6Wx{UM zp1zMDWedZno&+wQdxbqx!fm(S{P$P>{v*Bl|3K&*##E6C=|5k*~?#MGrU=8WpyW!2pLVS)RsA25@`Ig6tSG+nI(zYu<{$ z9)JG%+V<9S6kvCIR)3Eech>(__vr!S8^_zPuO`UtPeikX3WrBYfIkTM{rfL~i+IFD z=hnk#b?^RYipd73ZmCg6D_~m60>f@$1j`$RzkfK|nk<^2(N~F_Q+TLf&Mp5#J}+MV zXqGySjOLEu>cPSQI!^cS;Gkgy*$rj?@6V-F|M;c0!2d*<|IIFO!UrEwmA9cdli1A_ Rz=b?flvS0flKL3*e*pEpp#1;< literal 0 HcmV?d00001 diff --git a/images/provisioning.png b/images/provisioning.png new file mode 100644 index 0000000000000000000000000000000000000000..4dab9eb3b10f5f29e454133f948f400b07d1c9be GIT binary patch literal 6452 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 literal 0 HcmV?d00001 diff --git a/images/sdw-title.png b/images/sdw-title.png new file mode 100644 index 0000000000000000000000000000000000000000..dd18c665b79c4a5dbe36266c22e78ac53ac4f177 GIT binary patch literal 3653 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 X#ghsdSBG?@00000NkvXXu0mjfBZ3}j literal 0 HcmV?d00001 diff --git a/images/sdw-title.svg b/images/sdw-title.svg new file mode 100644 index 0000000..08d2953 --- /dev/null +++ b/images/sdw-title.svg @@ -0,0 +1 @@ +Software Delivery Workshop \ No newline at end of file From d8c63cd171d6b59be579d90028d94bd3cb422932 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Thu, 16 Sep 2021 16:19:03 -0500 Subject: [PATCH 25/50] remove java sample (#16) --- .../repos/app-templates/java/.gitignore | 17 ---- .../repos/app-templates/java/.gitlab-ci.yml | 33 ------- .../repos/app-templates/java/Dockerfile | 18 ---- .../repos/app-templates/java/README.md | 20 ----- .../java/k8s/dev/deployment.yaml | 27 ------ .../java/k8s/dev/kustomization.yaml | 22 ----- .../java/k8s/prod/deployment.yaml | 27 ------ .../java/k8s/prod/kustomization.yaml | 22 ----- .../java/k8s/stg/deployment.yaml | 28 ------ .../java/k8s/stg/kustomization.yaml | 21 ----- .../repos/app-templates/java/pom.xml | 85 ------------------- .../repos/app-templates/java/skaffold.yaml | 43 ---------- .../simple/SuperSimpleAppApplication.java | 27 ------ .../example/simple/web/HelloController.java | 35 -------- .../src/main/resources/application.properties | 3 - .../SuperSimpleAppApplicationTests.java | 27 ------ .../simple/web/HelloControllerTest.java | 39 --------- .../shared-kustomize/java/deployment.yaml | 51 ----------- .../shared-kustomize/java/kustomization.yaml | 17 ---- .../repos/shared-kustomize/java/service.yaml | 25 ------ 20 files changed, 587 deletions(-) delete mode 100644 delivery-platform/resources/repos/app-templates/java/.gitignore delete mode 100644 delivery-platform/resources/repos/app-templates/java/.gitlab-ci.yml delete mode 100644 delivery-platform/resources/repos/app-templates/java/Dockerfile delete mode 100644 delivery-platform/resources/repos/app-templates/java/README.md delete mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/dev/deployment.yaml delete mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/dev/kustomization.yaml delete mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/prod/deployment.yaml delete mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/prod/kustomization.yaml delete mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/stg/deployment.yaml delete mode 100644 delivery-platform/resources/repos/app-templates/java/k8s/stg/kustomization.yaml delete mode 100644 delivery-platform/resources/repos/app-templates/java/pom.xml delete mode 100644 delivery-platform/resources/repos/app-templates/java/skaffold.yaml delete mode 100644 delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/SuperSimpleAppApplication.java delete mode 100644 delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/web/HelloController.java delete mode 100644 delivery-platform/resources/repos/app-templates/java/src/main/resources/application.properties delete mode 100644 delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/SuperSimpleAppApplicationTests.java delete mode 100644 delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/web/HelloControllerTest.java delete mode 100644 delivery-platform/resources/repos/shared-kustomize/java/deployment.yaml delete mode 100644 delivery-platform/resources/repos/shared-kustomize/java/kustomization.yaml delete mode 100644 delivery-platform/resources/repos/shared-kustomize/java/service.yaml diff --git a/delivery-platform/resources/repos/app-templates/java/.gitignore b/delivery-platform/resources/repos/app-templates/java/.gitignore deleted file mode 100644 index 5a79060..0000000 --- a/delivery-platform/resources/repos/app-templates/java/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -Thumbs.db -.DS_Store -.gradle -build/ -target/ -out/ -.idea -*.iml -*.ipr -*.iws -.project -.settings -.classpath -.factorypath -.mvn/ -dependency-reduced-pom.xml -.vscode/ \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/.gitlab-ci.yml b/delivery-platform/resources/repos/app-templates/java/.gitlab-ci.yml deleted file mode 100644 index f073030..0000000 --- a/delivery-platform/resources/repos/app-templates/java/.gitlab-ci.yml +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2021 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. - -include: -- project: 'platform-admins/shared-ci-cd' - file: 'ci/java-docker.yaml' -- project: 'platform-admins/shared-ci-cd' - file: 'skaffold/build.yaml' -- project: 'platform-admins/shared-ci-cd' - file: 'skaffold/render.yaml' -- project: 'platform-admins/shared-ci-cd' - file: 'cd/validate.yaml' -- project: 'platform-admins/shared-ci-cd' - file: 'cd/push-manifests.yaml' - -stages: - - test - - build - - render-manifests - - prepare-config - - validate-config - - push-manifests \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/Dockerfile b/delivery-platform/resources/repos/app-templates/java/Dockerfile deleted file mode 100644 index a89b03e..0000000 --- a/delivery-platform/resources/repos/app-templates/java/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2021 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. - -FROM adoptopenjdk/openjdk13:jre-13.0.2_8-alpine -COPY target/app.jar app.jar -EXPOSE 8080 -CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar app.jar \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/README.md b/delivery-platform/resources/repos/app-templates/java/README.md deleted file mode 100644 index 71431fe..0000000 --- a/delivery-platform/resources/repos/app-templates/java/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Overview - -This is a sample application written in Java. In most cases, this should not be used -for development of a new application. - -## Useage - -Replace files (ex: pom.xml and src/ folders) to match the intended application's source. - -## Critical Files - -The following is a list of critical files utilized in the conventions for building -an Anthos application. - -| File/Folder | Description | Required | -|:-------------:|:----------------------|-----------:| -| Dockerfile :whale: | File used to create the Docker image (built with kaniko) | :white_check_mark: | -| skaffold.yaml | Used in local development to keep development environment in sync with changes. If not using skaffold, this file is optional (but recommended) | :white_large_square: | -| .gitlab-ci.yml | CICD Pipeline setup to build to inherit the conventions for the development organization/ecosystem | :white_check_mark: | -| k8s/ | Folder containing the Kubernetes resource manifests for "dev", "stage" and "prod". Resource files are configured to use Kustomize during the CICD build. | :white_check_mark: | \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/dev/deployment.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/dev/deployment.yaml deleted file mode 100644 index 3432a1b..0000000 --- a/delivery-platform/resources/repos/app-templates/java/k8s/dev/deployment.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2021 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. - -kind: Deployment -apiVersion: apps/v1 -metadata: - name: app -spec: - template: - spec: - serviceAccountName: java-template-ksa - containers: - - name: app - env: - - name: ENVIRONMENT - value: dev \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/dev/kustomization.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/dev/kustomization.yaml deleted file mode 100644 index 867735b..0000000 --- a/delivery-platform/resources/repos/app-templates/java/k8s/dev/kustomization.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2021 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. - -bases: -- ../../../kustomize-base/golang -patches: -- deployment.yaml -namePrefix: "java-template-" -commonLabels: - app: java-template - role: backend \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/prod/deployment.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/prod/deployment.yaml deleted file mode 100644 index ebc7759..0000000 --- a/delivery-platform/resources/repos/app-templates/java/k8s/prod/deployment.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2021 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. - -kind: Deployment -apiVersion: apps/v1 -metadata: - name: app -spec: - template: - spec: - serviceAccountName: java-template-ksa - containers: - - name: app - env: - - name: ENVIRONMENT - value: prod \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/prod/kustomization.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/prod/kustomization.yaml deleted file mode 100644 index 867735b..0000000 --- a/delivery-platform/resources/repos/app-templates/java/k8s/prod/kustomization.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2021 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. - -bases: -- ../../../kustomize-base/golang -patches: -- deployment.yaml -namePrefix: "java-template-" -commonLabels: - app: java-template - role: backend \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/stg/deployment.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/stg/deployment.yaml deleted file mode 100644 index 96f198a..0000000 --- a/delivery-platform/resources/repos/app-templates/java/k8s/stg/deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2021 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. - -kind: Deployment -apiVersion: apps/v1 -metadata: - name: app -spec: - replicas: 1 - template: - spec: - serviceAccountName: java-template-ksa - containers: - - name: app - env: - - name: ENVIRONMENT - value: stg \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/k8s/stg/kustomization.yaml b/delivery-platform/resources/repos/app-templates/java/k8s/stg/kustomization.yaml deleted file mode 100644 index 04b4d11..0000000 --- a/delivery-platform/resources/repos/app-templates/java/k8s/stg/kustomization.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2021 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. - -bases: -- ../../../kustomize-base/golang -patches: -- deployment.yaml -commonLabels: - env: stg -namePrefix: java-template- \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/pom.xml b/delivery-platform/resources/repos/app-templates/java/pom.xml deleted file mode 100644 index 1766ad8..0000000 --- a/delivery-platform/resources/repos/app-templates/java/pom.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.2.5.RELEASE - - - com.example - super-simple-app - 0.0.1-SNAPSHOT - super-simple-app - Sample executable Java application - - - 13 - app-${project.version} - - - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-jdbc - - - org.springframework.boot - spring-boot-starter-web - - - org.mybatis.spring.boot - mybatis-spring-boot-starter - 2.1.2 - - - - com.h2database - h2 - runtime - - - org.springframework.boot - spring-boot-starter-test - test - - - org.junit.vintage - junit-vintage-engine - - - - - - - ${jar.finalName} - - - org.springframework.boot - spring-boot-maven-plugin - - - - - \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/skaffold.yaml b/delivery-platform/resources/repos/app-templates/java/skaffold.yaml deleted file mode 100644 index 0f50faa..0000000 --- a/delivery-platform/resources/repos/app-templates/java/skaffold.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2021 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. - -apiVersion: skaffold/v2beta5 -kind: Config -# Defaults are configured for local dev -build: - artifacts: - - image: app -deploy: - kustomize: - paths: - - k8s/dev -profiles: - # Profile used when building images in CI - - name: ci - build: - cluster: - dockerConfig: - path: ~/.docker/config.json - # Profile used when rendering production manifests - - name: prod - deploy: - kustomize: - paths: - - k8s/prod - # Profile used when rendering staging manifests - - name: staging - deploy: - kustomize: - paths: - - k8s/stg diff --git a/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/SuperSimpleAppApplication.java b/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/SuperSimpleAppApplication.java deleted file mode 100644 index 0f98141..0000000 --- a/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/SuperSimpleAppApplication.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 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. - -package com.example.simple; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SuperSimpleAppApplication { - - public static void main(String[] args) { - SpringApplication.run(SuperSimpleAppApplication.class, args); - } - -} \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/web/HelloController.java b/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/web/HelloController.java deleted file mode 100644 index 799e52f..0000000 --- a/delivery-platform/resources/repos/app-templates/java/src/main/java/com/example/simple/web/HelloController.java +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2021 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. - -package com.example.simple.web; - -import org.springframework.web.bind.annotation.RestController; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMapping; - -@RestController -public class HelloController { - - @RequestMapping("/") - public String index() { - String environment = System.getenv("ENVIRONMENT"); - String response = "SimpleApp

Super Simple Java App

"; - if (!StringUtils.isEmpty(environment)) { - response += "\n\n

Environment: " + environment + "

"; - } - response += ""; - return response; - } - -} \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/src/main/resources/application.properties b/delivery-platform/resources/repos/app-templates/java/src/main/resources/application.properties deleted file mode 100644 index ba3b5f6..0000000 --- a/delivery-platform/resources/repos/app-templates/java/src/main/resources/application.properties +++ /dev/null @@ -1,3 +0,0 @@ -# Remap the Acutator path to "/" & enable /health -management.endpoints.web.base-path=/ -management.endpoints.web.path-mapping.health=health \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/SuperSimpleAppApplicationTests.java b/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/SuperSimpleAppApplicationTests.java deleted file mode 100644 index 50c6c06..0000000 --- a/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/SuperSimpleAppApplicationTests.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 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. - -package com.example.simple; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SuperSimpleAppApplicationTests { - - @Test - void contextLoads() { - } - -} \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/web/HelloControllerTest.java b/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/web/HelloControllerTest.java deleted file mode 100644 index 27b5eb1..0000000 --- a/delivery-platform/resources/repos/app-templates/java/src/test/java/com/example/simple/web/HelloControllerTest.java +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2021 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. - -package com.example.simple.web; - -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -@SpringBootTest -@AutoConfigureMockMvc -public class HelloControllerTest { - - @Autowired - private MockMvc mvc; - - @Test - public void getHello() throws Exception { - mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } -} \ No newline at end of file diff --git a/delivery-platform/resources/repos/shared-kustomize/java/deployment.yaml b/delivery-platform/resources/repos/shared-kustomize/java/deployment.yaml deleted file mode 100644 index 9ee5d17..0000000 --- a/delivery-platform/resources/repos/shared-kustomize/java/deployment.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2021 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. - -kind: Deployment -apiVersion: apps/v1 -metadata: - name: app -spec: - strategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 0 - template: - metadata: - name: app - spec: - containers: - - name: app - image: app - resources: - limits: - memory: "512Mi" - cpu: "500m" - env: - - name: ENVIRONMENT - value: base - - name: LOG_LEVEL - value: info - readinessProbe: - tcpSocket: - port: 8080 - periodSeconds: 15 - livenessProbe: - periodSeconds: 15 - httpGet: - path: /health - port: 8080 - ports: - - name: http - containerPort: 8080 diff --git a/delivery-platform/resources/repos/shared-kustomize/java/kustomization.yaml b/delivery-platform/resources/repos/shared-kustomize/java/kustomization.yaml deleted file mode 100644 index 78941d5..0000000 --- a/delivery-platform/resources/repos/shared-kustomize/java/kustomization.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2021 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. - -resources: - - service.yaml - - deployment.yaml \ No newline at end of file diff --git a/delivery-platform/resources/repos/shared-kustomize/java/service.yaml b/delivery-platform/resources/repos/shared-kustomize/java/service.yaml deleted file mode 100644 index 89f0c04..0000000 --- a/delivery-platform/resources/repos/shared-kustomize/java/service.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2021 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. - -kind: Service -apiVersion: v1 -metadata: - name: app -spec: - type: ClusterIP - ports: - - name: http - port: 8080 - targetPort: 8080 - protocol: TCP \ No newline at end of file From 177f51f727762e295a8933f7b3a7a072e9639da5 Mon Sep 17 00:00:00 2001 From: gushob21 <43795024+gushob21@users.noreply.github.com> Date: Mon, 20 Sep 2021 21:28:36 +0000 Subject: [PATCH 26/50] Add steps for explaining clouddeploy flow in 3.2release-progression.md (#17) * Incorporating cloud deploy --- .../docs/images/clouddeploy-prod.png | Bin 0 -> 45638 bytes .../docs/images/clouddeploy-stage.png | Bin 0 -> 36526 bytes .../docs/workshop/3.2-release-progression.md | 47 +++++++++++++++--- .../app-templates/golang/cloudbuild-cd.yaml | 12 ++--- .../app-templates/golang/deploy/prod.yaml | 6 +-- .../app-templates/golang/deploy/stage.yaml | 6 +-- 6 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 delivery-platform/docs/images/clouddeploy-prod.png create mode 100644 delivery-platform/docs/images/clouddeploy-stage.png diff --git a/delivery-platform/docs/images/clouddeploy-prod.png b/delivery-platform/docs/images/clouddeploy-prod.png new file mode 100644 index 0000000000000000000000000000000000000000..7c0d3a9bb2a5ae76fa73d38c66fb0078fc7588a3 GIT binary patch literal 45638 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≤)eui|78Flf3@E*ebAqmF}3* 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

pSIAFz)`WIp#ZT$&xW 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 literal 0 HcmV?d00001 diff --git a/delivery-platform/docs/workshop/3.2-release-progression.md b/delivery-platform/docs/workshop/3.2-release-progression.md index d9cad80..584d840 100644 --- a/delivery-platform/docs/workshop/3.2-release-progression.md +++ b/delivery-platform/docs/workshop/3.2-release-progression.md @@ -73,13 +73,13 @@ Copy the value and paste it into a new browser tab to review the webhook on your The webhook is configured to call an endpoint with details about the event that occurred. The onboarding script created an endpoint in Google Cloud to accept these webhook requests. Review the trigger setup for your application in Cloud Build with the link below - +#### If you chose ACM as continuous delivery platform: [https://console.cloud.google.com/cloud-build/triggers/edit/hello-web-webhook-trigger](https://console.cloud.google.com/cloud-build/triggers/edit/hello-web-webhook-trigger) - - +#### If you chose Clouddeploy as continuous delivery platform: +[https://console.cloud.google.com/cloud-build/triggers/edit/hello-web-webhook-trigger](https://console.cloud.google.com/cloud-build/triggers/edit/hello-web-clouddeploy-webhook-trigger) ### Workflow Configuration - +#### If you chose ACM as continuous delivery platform The webhook endpoint triggers a Cloud Build job that executes a workflow defined in a `cloudbuild.yaml` file. The cloud build implementation includes steps to clone the repo, build and push an image, hydrate resources, and finally deploy the assets to the appropriate environment. Review the `cloudbuild.yaml` in your `hello-web` project @@ -87,9 +87,18 @@ Review the `cloudbuild.yaml` in your `hello-web` project Click here to open cloudbuild.yaml +#### If you chose Clouddeploy as continuous delivery platform +The webhook endpoint triggers a Cloud Build job that executes a workflow defined in a `cloudbuild-cd.yaml` file. The cloud build implementation includes steps to clone the repo, build and push an image, and finally create pipelines for deployment in stage and prod. + +Review the `cloudbuild-cd.yaml` in your `hello-web` project + +Click here to open cloudbuild-cd.yaml + + ### View Initial Deployment During the onboarding process, the workflow was exercised for the first time to create an initial deployment for your application. You can see your application running from the GKE Workloads page: +Note: If you chose Clouddeploy as delivery system, the pipelines created in previous steps automatically runs in its first target which is stage and completes the deployment. [Cick here to view the deployment](https://console.cloud.google.com/kubernetes/workload) Now since these services are not exposed publicly, you'll need to create a tunnel to the cluster to view the web page. @@ -126,10 +135,10 @@ git add . git commit -m "Updating to V2" git push origin main ``` - Review the Cloud Build trigger in progress by cicking into the latest job on [the build history page](https://console.cloud.google.com/cloud-build/builds) -When that completes you can review the updated change by opening your tunnel +#### If you chose ACM as continuous delivery platform +When the Cloud Build trigger completes, you can review the updated change by opening your tunnel ```bash kubectx stage \ @@ -140,8 +149,22 @@ And again utilizing the web preview in the top right When you're done use `ctrl+c` in the terminal to exit out of the tunnel +#### If you chose Clouddeploy as continuous delivery platform +When the Cloud Build trigger completes, it will create a Clouddeploy pipeline in your project. Review it by clicking [Clouddeploy pipelines](https://pantheon.corp.google.com/deploy/delivery-pipelines). +You will see that the pipeline contains two stages : stage and prod. When the pipeline is created, it automatically deploys to stage environment. +![](https://crg-sdw-imgs.web.app/clouddeploy-stage.png) +Click the three dots on stage pipeline box and choose "View release" to see more details. +Now, you can review the updated change by opening your tunnel + +```bash +kubectx stage \ + && kubectl port-forward --namespace hello-web $(kubectl get pod --namespace hello-web --selector="app=hello-web,role=backend" --output jsonpath='{.items[0].metadata.name}') 8080:8080 + ``` + ## Release code to prod +#### If you chose ACM as continuous delivery platform + Releases to production are also triggered through git events. In this case, the creation of a tag is the triggering event instead of code being pushed to a branch. Create a release by executing the following command @@ -164,7 +187,19 @@ And again utilizing the web preview in the top right When you're done use `ctrl+c` in the terminal to exit out of the tunnel +#### If you chose Clouddeploy as continuous delivery platform +Go to the pipipeline created earlier by clicking [Clouddeploy pipelines](https://pantheon.corp.google.com/deploy/delivery-pipelines). + +A "promote" link will appear on stage box, click it. It will open a dialog box, select Prod as target and click Promote button at the bottom left. +It will trigger the release in Prod. Once the pipeline finishes, you will see Prod pipelines in green status as shown in the pic below. + +![](https://crg-sdw-imgs.web.app/clouddeploy-prod.png) +Now, review the page live by creating your tunnel +```bash +kubectx prod \ + && kubectl port-forward --namespace hello-web $(kubectl get pod --namespace hello-web --selector="app=hello-web,role=backend" --output jsonpath='{.items[0].metadata.name}') 8080:8080 + ``` ## Congratulations!!! diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml index 98faaa4..f128bc8 100644 --- a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml @@ -34,7 +34,7 @@ steps: git clone ${_KUSTOMIZE_REPO} kustomize-base sleep 5 - - id: hack-for-clouddeploy + - id: setup-for-clouddeploy name: gcr.io/cloud-builders/git entrypoint: bash args: @@ -52,7 +52,6 @@ steps: - '-c' - | cd app-repo - ls -lrt /workspace/kustomize-base/golang skaffold build --file-output=/workspace/artifacts.json \ --default-repo ${_DEFAULT_IMAGE_REPO} \ --push=true @@ -65,11 +64,12 @@ steps: - | gcloud config set deploy/region us-central1 cd app-repo + SHORT_SHA=$(git rev-parse --short HEAD) sed -i s/PROJECT_ID/$PROJECT_ID/g deploy/* - gcloud alpha deploy apply --file deploy/pipeline.yaml - gcloud alpha deploy apply --file deploy/stage.yaml - gcloud alpha deploy apply --file deploy/prod.yaml - gcloud alpha deploy releases create $SHORT_SHA-$(date +%s) \ + gcloud beta deploy apply --file deploy/pipeline.yaml + gcloud beta deploy apply --file deploy/stage.yaml + gcloud beta deploy apply --file deploy/prod.yaml + gcloud beta deploy releases create rel-${SHORT_SHA}-$(date +%s) \ --delivery-pipeline sample-app \ --description "$(git log -1 --pretty='%s')" \ --build-artifacts /workspace/artifacts.json \ diff --git a/delivery-platform/resources/repos/app-templates/golang/deploy/prod.yaml b/delivery-platform/resources/repos/app-templates/golang/deploy/prod.yaml index f82f543..e8f9d1a 100644 --- a/delivery-platform/resources/repos/app-templates/golang/deploy/prod.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/deploy/prod.yaml @@ -20,7 +20,5 @@ metadata: labels: {} description: prod requireApproval: false -gkeCluster: - project: PROJECT_ID - cluster: prod - location: us-central1-a \ No newline at end of file +gke: + cluster: projects/PROJECT_ID/locations/us-central1-a/clusters/prod diff --git a/delivery-platform/resources/repos/app-templates/golang/deploy/stage.yaml b/delivery-platform/resources/repos/app-templates/golang/deploy/stage.yaml index fbfc660..6995e53 100644 --- a/delivery-platform/resources/repos/app-templates/golang/deploy/stage.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/deploy/stage.yaml @@ -19,7 +19,5 @@ metadata: annotations: {} labels: {} description: stage -gkeCluster: - project: PROJECT_ID - cluster: stage - location: us-west2-a \ No newline at end of file +gke: + cluster: projects/PROJECT_ID/locations/us-west2-a/clusters/stage \ No newline at end of file From df641764daa87824a73bddbc5318482a7bae65fb Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 24 Sep 2021 16:18:17 -0500 Subject: [PATCH 27/50] Simplifying value replacement (#18) --- ...dbuild-cd.yaml => cloudbuild-cd.yaml.tmpl} | 2 +- .../{pipeline.yaml => pipeline.yaml.tmpl} | 4 +-- .../golang/k8s/dev/deployment.yaml.tmpl | 34 ++++++++++++++++++ ...omization.yaml => kustomization.yaml.tmpl} | 4 +-- .../golang/k8s/prod/deployment.yaml.tmpl | 34 ++++++++++++++++++ .../kustomization.yaml.tmpl} | 4 +-- .../golang/k8s/stage/deployment.yaml.tmpl | 35 +++++++++++++++++++ .../kustomization.yaml.tmpl} | 4 +-- .../{skaffold.yaml => skaffold.yaml.tmpl} | 2 +- delivery-platform/scripts/app.sh | 12 +------ 10 files changed, 114 insertions(+), 21 deletions(-) rename delivery-platform/resources/repos/app-templates/golang/{cloudbuild-cd.yaml => cloudbuild-cd.yaml.tmpl} (97%) rename delivery-platform/resources/repos/app-templates/golang/deploy/{pipeline.yaml => pipeline.yaml.tmpl} (95%) create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/dev/deployment.yaml.tmpl rename delivery-platform/resources/repos/app-templates/golang/k8s/dev/{kustomization.yaml => kustomization.yaml.tmpl} (87%) create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/prod/deployment.yaml.tmpl rename delivery-platform/resources/repos/app-templates/golang/k8s/{stage/kustomization.yaml => prod/kustomization.yaml.tmpl} (92%) create mode 100644 delivery-platform/resources/repos/app-templates/golang/k8s/stage/deployment.yaml.tmpl rename delivery-platform/resources/repos/app-templates/golang/k8s/{prod/kustomization.yaml => stage/kustomization.yaml.tmpl} (92%) rename delivery-platform/resources/repos/app-templates/golang/{skaffold.yaml => skaffold.yaml.tmpl} (94%) diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml.tmpl similarity index 97% rename from delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml rename to delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml.tmpl index f128bc8..9519783 100644 --- a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml.tmpl @@ -70,7 +70,7 @@ steps: gcloud beta deploy apply --file deploy/stage.yaml gcloud beta deploy apply --file deploy/prod.yaml gcloud beta deploy releases create rel-${SHORT_SHA}-$(date +%s) \ - --delivery-pipeline sample-app \ + --delivery-pipeline ${APP_NAME} \ --description "$(git log -1 --pretty='%s')" \ --build-artifacts /workspace/artifacts.json \ --annotations="commit_ui=${_APP_REPO}/+/$COMMIT_SHA" diff --git a/delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml b/delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml.tmpl similarity index 95% rename from delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml rename to delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml.tmpl index d7deeba..a93d8a6 100644 --- a/delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/deploy/pipeline.yaml.tmpl @@ -15,9 +15,9 @@ apiVersion: deploy.cloud.google.com/v1beta1 kind: DeliveryPipeline metadata: - name: sample-app + name: ${APP_NAME} labels: - app: sample-app + app: ${APP_NAME} description: delivery pipeline serialPipeline: stages: diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/dev/deployment.yaml.tmpl b/delivery-platform/resources/repos/app-templates/golang/k8s/dev/deployment.yaml.tmpl new file mode 100644 index 0000000..d139a24 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/dev/deployment.yaml.tmpl @@ -0,0 +1,34 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: app # Must match base value + image: ${APP_NAME} # Overwrites values from base - Needs to match skaffold artifact + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: dev diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml.tmpl similarity index 87% rename from delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml rename to delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml.tmpl index 18965c4..9951235 100644 --- a/delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/dev/kustomization.yaml.tmpl @@ -16,7 +16,7 @@ bases: - ../../../kustomize-base/golang patches: - deployment.yaml -namePrefix: "golang-template-" # App name (dash) +namePrefix: "${APP_NAME}-" # App name (dash) commonLabels: - app: golang-template # App name for selectors + app: ${APP_NAME} # App name for selectors role: backend diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/prod/deployment.yaml.tmpl b/delivery-platform/resources/repos/app-templates/golang/k8s/prod/deployment.yaml.tmpl new file mode 100644 index 0000000..f554824 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/prod/deployment.yaml.tmpl @@ -0,0 +1,34 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: app + image: ${APP_NAME} + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: prod diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml.tmpl similarity index 92% rename from delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml rename to delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml.tmpl index d6451f7..a49b3ab 100644 --- a/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml.tmpl @@ -16,7 +16,7 @@ bases: - ../../../kustomize-base/golang patches: - deployment.yaml +namePrefix: "${APP_NAME}-" commonLabels: - app: golang-template + app: ${APP_NAME} role: backend -namePrefix: golang-template- diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/stage/deployment.yaml.tmpl b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/deployment.yaml.tmpl new file mode 100644 index 0000000..aab1747 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/deployment.yaml.tmpl @@ -0,0 +1,35 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + replicas: 1 + template: + spec: + containers: + - name: app + image: ${APP_NAME} + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: stg diff --git a/delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml.tmpl similarity index 92% rename from delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml rename to delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml.tmpl index 1e08b92..cea5b6a 100644 --- a/delivery-platform/resources/repos/app-templates/golang/k8s/prod/kustomization.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/k8s/stage/kustomization.yaml.tmpl @@ -16,7 +16,7 @@ bases: - ../../../kustomize-base/golang patches: - deployment.yaml -namePrefix: "golang-template-" commonLabels: - app: golang-template + app: ${APP_NAME} role: backend +namePrefix: ${APP_NAME}- diff --git a/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml b/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml.tmpl similarity index 94% rename from delivery-platform/resources/repos/app-templates/golang/skaffold.yaml rename to delivery-platform/resources/repos/app-templates/golang/skaffold.yaml.tmpl index 3c8e5d4..4cf7d94 100644 --- a/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml +++ b/delivery-platform/resources/repos/app-templates/golang/skaffold.yaml.tmpl @@ -16,7 +16,7 @@ apiVersion: skaffold/v1beta14 kind: Config build: artifacts: - - image: app # Match name in deployment yaml + - image: ${APP_NAME} # Match name in deployment yaml context: ./ deploy: kustomize: diff --git a/delivery-platform/scripts/app.sh b/delivery-platform/scripts/app.sh index 1130ce2..3f36c77 100755 --- a/delivery-platform/scripts/app.sh +++ b/delivery-platform/scripts/app.sh @@ -51,17 +51,7 @@ create () { cd app-templates/${APP_LANG} ## Insert name of new app - - - find . -name kustomization.yaml -exec sed -i "s/namePrefix:.*/namePrefix: ${APP_NAME}-/g" {} \; - find . -name kustomization.yaml -exec sed -i "s/ app:.*/ app: ${APP_NAME}/g" {} \; - find . -name pipeline.yaml -exec sed -i "s/ name:.*/ name: ${APP_NAME}/g" {} \; - find . -name pipeline.yaml -exec sed -i "s/ app:.*/ app: ${APP_NAME}/g" {} \; - find . -name cloudbuild-cd.yaml -exec sed -i "s/--delivery-pipeline sample-app/--delivery-pipeline ${APP_NAME}/g" {} \; - - ## Insert image name of new app - find . -name deployment.yaml -exec sed -i "s/image: app/image: ${APP_NAME}/g" {} \; - find . -name skaffold.yaml -exec sed -i "s/image: app/image: ${APP_NAME}/g" {} \; + for template in $(find . -name '*.tmpl'); do envsubst < ${template} > ${template%.*}; done ## Create and push to new repo From 459463d2a6a20a20231ca198fa509b4db71d5754 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 24 Sep 2021 16:22:21 -0500 Subject: [PATCH 28/50] remove check for API_KEY in gh.sh (#19) --- delivery-platform/scripts/git/gh.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/delivery-platform/scripts/git/gh.sh b/delivery-platform/scripts/git/gh.sh index 88678e7..825c22c 100755 --- a/delivery-platform/scripts/git/gh.sh +++ b/delivery-platform/scripts/git/gh.sh @@ -23,10 +23,7 @@ if [[ ${GIT_USERNAME} == "" ]]; then echo "GIT_USERNAME variable not set. Please rerun the env script" exit -1 fi -if [[ ${API_KEY_VALUE} == "" ]]; then - echo "API_KEY_VALUE variable not set. Please rerun the env script" - exit -1 -fi + if [[ $1 == "create_webhook" ]]; then if [[ $2 == "" || $3 == "" ]]; then echo "Missing parameters" From 4ccbf0ac0ee6e26039b5fed28534103a999f4b29 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Mon, 27 Sep 2021 16:30:45 -0500 Subject: [PATCH 29/50] Kustomize lab (#20) * Kustomize lab --- labs/hydrating-with-kustomize/README.md | 353 ++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 labs/hydrating-with-kustomize/README.md diff --git a/labs/hydrating-with-kustomize/README.md b/labs/hydrating-with-kustomize/README.md new file mode 100644 index 0000000..2b785bd --- /dev/null +++ b/labs/hydrating-with-kustomize/README.md @@ -0,0 +1,353 @@ +# Hydrating with Kustomize + +Kustomize is a tool that introduces a template-free way to customize application configuration, simplifying the use of off-the-shelf applications. It's available as a stand alone utility and is built into kubectl through `kubectl apply -k` of can be used as a stand alone CLI. For additional details read more at [kustomize.io](https://kustomize.io/). +## Objectives + +In this tutorial you work through some of the core concepts of Kustomize and use it to manage variations in the applications and environments. + +You will: + +- Utilize kustomize command line client +- Override common elements +- Patch larger yaml structures +- Utilize multiple layers of overlays + +## Preparing your workspace + +1. Open Cloud Shell editor by visiting the following url + +``` +https://ide.cloud.google.com +``` + +2. In the terminal window create a working directory for this tutorial + +``` +mkdir kustomize-lab +``` + +3. Change into the directory and set the IDE workspace + +``` +cd kustomize-lab && cloudshell workspace . +``` + +## Utilizing kustomize command line client + +The power of kustomize comes from the ability to overlay and modify base Kubernetes yamls with custom values. In order to do this kustomize requires a base file with instructions on where the files are and what to override. Kustomize is included in the Kubernetes ecosystem and can be executed through various methods. + +In this section you will create a base kustomize configuration and process the files with the stand alone kustomize command line client. + +1. To start, you will create a folder to hold your base configuration files + +``` +mkdir -p chat-app/base +``` + +2. Create a simple kubernetes ``deployment.yaml`` in the base folder + +``` +cat < chat-app/base/deployment.yaml +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + metadata: + name: chat-app + spec: + containers: + - name: chat-app + image: chat-app-image +EOF +``` + +3. Create the base `kustomization.yaml` + + Kustomize looks for a file called kustomization.yaml as an entry point. This file contains references to the various base and override files as well as specific override values. + + Create a `kustomization.yaml` file that references the `deployment.yaml` as the base resources. +``` +cat < chat-app/base/kustomization.yaml +bases: + - deployment.yaml +EOF +``` + +4. Run the kustomize command on the base folder. Doing so outputs the deployment YAML files with no changes, which is expected since you haven't included any variations yet. + +``` +kustomize build chat-app/base +``` + +This standalone client can be combined with the kubectl client to apply the output directly as in the following example. Doing so streams the output of the build command directly into the kubectl apply command. + +(Do Not Execute - Included for reference only) + +

+kustomize build chat-app/base | kubectl apply -f -
+
+ +This technique is useful if a specific version of the kustomize client is needed. + +Alternatively kustomize can be executed with the tooling integrated within kubectl itself. As in the following example. + +(Do Not Execute - Included for reference only) + +
+kubectl apply -k chat-app/base
+
+ +## Overriding common elements + +Now that your workspace is configured and you verified kustomize is working, it's time to override some of the base values. + +Images, namespaces and labels are very commonly customized for each application and environment. Since they are commonly changed, Kustomize lets you declare them directly in the `kustomize.yaml`, eliminating the need to create many patches for these common scenarios. + +This technique is often used to create a specific instance of a template. One base set of resources can now be used for multiple implementations by simply changing the name and its namespace. + +In this example, you will add a namespace, name prefix and add some labels to your `kustomization.yaml`. + +1. Update the `kustomization.yaml` file to include common labels and namespaces. + + Copy and execute the following commands in your terminal + +``` +cat < chat-app/base/kustomization.yaml +bases: + - deployment.yaml + +namespace: my-namespace +nameprefix: my- +commonLabels: + app: my-app + +EOF +``` + +2. Execute the build command + + Executing the build at this point shows that the resulting YAML file now contains the namespace, labels and prefixed names in both the service and deployment definitions. + +``` +kustomize build chat-app/base +``` + +Note how the output contains labels and namespaces that are not in the deployment YAML file. Note also how the name was changed from `chat-app` to `my-chat-app` + +(Output do not copy) +
+kind: Deployment
+metadata:
+  labels:
+    app: my-app
+  name: my-chat-app
+  namespace: my-namespace
+
+ +## Patching larger yaml structures + +Kustomize also provides the ability to apply patches that overlay the base resources. This technique is often used to provide variability between applications and environments. + +In this step, you will create environment variations for a single application that use the same base resources. + +1. Start by creating folders for the different environments + +``` +mkdir -p chat-app/dev +mkdir -p chat-app/prod +``` + + + +2. Write the stage patch with the following command + +``` +cat < chat-app/dev/deployment.yaml +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: chat-app + env: + - name: ENVIRONMENT + value: dev +EOF +``` + +3. Now Write the prod patch with the following command + +``` +cat < chat-app/prod/deployment.yaml +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: chat-app + env: + - name: ENVIRONMENT + value: prod +EOF +``` + +Notice that the patches above do not contain the container image name. That value is provided in the base/deployment.yaml you created in the previous step. These patches do however contain unique environment variables for dev and prod. + +4. Implement the kustomize YAML files for the base directory + +Rewrite the base kustomization.yaml, remove the namespace and name prefix as this is just the base config with no variation. Those fields will be moved to the environment files in just a moment. + +``` +cat < chat-app/base/kustomization.yaml +bases: + - deployment.yaml + +commonLabels: + app: chat-app + +EOF +``` +5. Implement the kustomize YAML files for the dev directory + +Now implement the variations for dev and prod by executing the following commands in your terminal. + +``` +cat < chat-app/dev/kustomization.yaml +bases: +- ../base + +namespace: dev +nameprefix: dev- +commonLabels: + env: dev + +patches: +- deployment.yaml +EOF +``` + +Note the addition of the `patches`: section of the file. This indicates that kustomize should overlay those files on top of the base resources. + +6. Implement the kustomize YAML files for the prod directory + +``` +cat < chat-app/prod/kustomization.yaml +bases: +- ../base + +namespace: prod +nameprefix: prod- +commonLabels: + env: prod + +patches: +- deployment.yaml +EOF +``` + +7. Run kustomize to merge the files +With the base and environment files created, you can execute the kustomize process to patch the base files. + + Run the following command for dev to see the merged result. + +``` +kustomize build chat-app/dev +``` + +Note the output contains merged results such as labels from base and dev configurations as well as the container image name from the base and the environment variable from the dev folders. + + +## Utilizing multiple layers of overlays + +Many organizations have a team that helps support the app teams and manage the platform. Frequently these teams will want to include specific details that are to be included in all apps across all environments, such as a logging agent. + +In this example, you will create a `shared-kustomize` folder and resources which will be included by all applications and regardless of which environment they're deployed. + +1. Create the shared-kustomize folder + +``` +mkdir shared-kustomize +``` + +2. Create a simple `deployment.yaml` in the shared folder + + +``` +cat < shared-kustomize/deployment.yaml +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: logging-agent + image: logging-agent-image +EOF +``` + +3. Create a kustomization.yaml in the shared folder + +``` +cat < shared-kustomize/kustomization.yaml +bases: + - deployment.yaml +EOF +``` + +4. Reference the shared-kustomize folder from your application + +Since you want the `shared-kustomize` folder to be the base for all your applications, you will need to update your `chat-app/base/kustomization.yaml` to use `shared-kustomize` as the base. Then patch its own deployment.yaml on top. The environment folders will then patch again on top of that. + +Copy and execute the following commands in your terminal + +``` +cat < chat-app/base/kustomization.yaml +bases: + - ../../shared-kustomize + +commonLabels: + app: chat-app + +patches: +- deployment.yaml + +EOF +``` + +5. Run kustomize and view the merged results for dev + + +``` +kustomize build chat-app/dev +``` + +Note the output contains merged results from the app base, the app environment, and the `shared-kustomize` folders. Specifically, you can see in the containers section values from all three locations. + +(output do not copy) +
+
+```
+    containers:
+          - env:
+            - name: ENVIRONMENT
+              value: dev
+            name: chat-app
+          - image: image
+            name: app
+          - image: logging-agent-image
+            name: logging-agent
+```
+
+
+ + From 038cdc31b56909f0b6b00f6bae2c93eab2d0f4b8 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Mon, 27 Sep 2021 16:31:19 -0500 Subject: [PATCH 30/50] Cloud code lab (#21) * Cloud Code lab --- .../4dwlgh6avps.png | Bin 0 -> 1747 bytes .../61jmwjqwlhk.png | Bin 0 -> 1803 bytes labs/developing-with-cloud-code/README.md | 161 ++++++++++++++++++ .../fyg1yamuy0d.png | Bin 0 -> 1868 bytes .../imias3q32ah.png | Bin 0 -> 1660 bytes .../mz5gbbsj9lq.png | Bin 0 -> 1818 bytes .../developing-with-cloud-code/oepub6vk28.png | Bin 0 -> 1701 bytes .../sq5ck29lduc.png | Bin 0 -> 2359 bytes .../developing-with-cloud-code/tw2ka8wmpl.png | Bin 0 -> 16796 bytes .../wvofl4zlg6a.png | Bin 0 -> 3729 bytes .../developing-with-cloud-code/x30hwes9pu.png | Bin 0 -> 5899 bytes .../y1nu68cr85h.png | Bin 0 -> 2359 bytes .../zygset0j2tn.png | Bin 0 -> 2359 bytes 13 files changed, 161 insertions(+) create mode 100644 labs/developing-with-cloud-code/4dwlgh6avps.png create mode 100644 labs/developing-with-cloud-code/61jmwjqwlhk.png create mode 100644 labs/developing-with-cloud-code/README.md create mode 100644 labs/developing-with-cloud-code/fyg1yamuy0d.png create mode 100644 labs/developing-with-cloud-code/imias3q32ah.png create mode 100644 labs/developing-with-cloud-code/mz5gbbsj9lq.png create mode 100644 labs/developing-with-cloud-code/oepub6vk28.png create mode 100644 labs/developing-with-cloud-code/sq5ck29lduc.png create mode 100644 labs/developing-with-cloud-code/tw2ka8wmpl.png create mode 100644 labs/developing-with-cloud-code/wvofl4zlg6a.png create mode 100644 labs/developing-with-cloud-code/x30hwes9pu.png create mode 100644 labs/developing-with-cloud-code/y1nu68cr85h.png create mode 100644 labs/developing-with-cloud-code/zygset0j2tn.png diff --git a/labs/developing-with-cloud-code/4dwlgh6avps.png b/labs/developing-with-cloud-code/4dwlgh6avps.png new file mode 100644 index 0000000000000000000000000000000000000000..f2275cb3852bcf7956ce219500512928e388a9cd GIT binary patch literal 1747 zcmZ`)Ycv}O7BVIviFmiEGSg~wJ8O_4rQRv3olr3> zA~I7%yaCQlylD@i#)6WGns2`T(21AyIt=>Y6A+I0*T6 zxZs398}NlOtl_XV7?1RK0Jj~l`WcSL8Od!=97G-zbX82%u5uaG?9NBGe&WEB4Wuhf z1;+1_5Y}>WsXuG9suJ*zJ4ik6dfU51KNB9~6bW`|;A(&Fbf!L6+nQkbr-T)B5E{lm zKfB$lpGbnoFN~f`-5Y3bZq7{Q-H^Y>Z?7+TUh0e*pwIf1JM*fmEwgoXb=yjS9E78z zV}IJ$xu!f*I5|41H%(6)X_2Vh`_#{yID7M*4^gkSy*=bC7XEijP(W|WXOC2(L?(+Z zD=X{&$|*V^*$k0n@z=%&eS_W)1X~-+Xg*o&vJ~!`bI81ot-W!9Tt$M%&7NuutUP3X zuBr8!PzTmm?V0m_Nq@QYb>F~$-o0yj_6$i`#7_!^cK>!uTDNeETX@||jcPNSK4|6a z>RO(<|7+PoBh&pC2JBkB3%Pdlb!Vb~8$NvCcnANBTnZ5Z4^R~yluux>u|PWiG>bu=0E5gUMrjjHaQM)+ zbp9|O+qWFk-PeQd`d5Z>;QsQq1BoYB<|WI53;AUP&B*0G&9cht;`&u=dl@J3c3 z;n>>EpYL%|4kAl-Kyuq_$1pzeu}P>?C>$gh8JT0Ei|VOER`elm_zLN?lhdqXMjv)G^@I&RWFbRdJzccxU}@00)oCKmUVic(_`NC$R#=@rX&O(jm;^rN7)-Hm1W z*o*Q>wm%SdSyjUL!UDE2_^1zRB&(++mO^0|8yRWADsF7N9mG{UjbCi0LxP_l7jgru zf*cywbssv;&D%qT4U3q?s7e_BID z!EdJ#N;n*QexfY0y6!IUFm!ypt2APuQH;{=s}G=u21v44;>c|8b$-8!2}fS(2czOapv*bP?5R(OEibeHEh7xq~`%5A|m8z%;!-0vNt(3wHt?q zrRM|2D&3enj(VZa6lm-`|9V_>bY`X1i@BEAtgVDE+h$QT5KkrVq~Z`T6h<)vd;~I? zO!-vPMQq)w#Oem`qN+3cD>W$Q5ci(}7U*g6Ea4wLib~CCgN#9oe@ew!jIez@& z%QWSlF+npG>ZV$5Ymh@ob7ptS=U1(PFAI;kdfeRb_TEvX$MT5szQYO0$;p@I;}^Ol zAGmauZ)xab6R{cO9<5cqLwpiJbGz}z{k?Tg@bm>XHso3rkz#6U+OqFIhS5OK0L#nE zmhD;usb#6AY?bUS`|Yj)ZmQtK6O_=~-_!C^O`c#)U&whf;S2{3*^N8+jbwpS6 zd~vi%w_xCvL~XO_ScbJtF}l%-*IeXgMtW#aV}t)M+J@fF=`D2m9CW^>Ao2<+y&L!@ z-9i2Yd*6o{%puM-V%KAAOo)_hid?DR&fOA5o3MUB z(CB{ZyFnJRhzH;ODB5r+@mDvgNJ?E{8jNHOJAWfRp$}1r!TibV!jj#T6=l|~m c_#=@=#Ze24JonIzs%NE!L1DcbJT7MZ4z?&>_5c6? literal 0 HcmV?d00001 diff --git a/labs/developing-with-cloud-code/61jmwjqwlhk.png b/labs/developing-with-cloud-code/61jmwjqwlhk.png new file mode 100644 index 0000000000000000000000000000000000000000..5e811a61a90fe0571e391f8f6d0f62c3652b65a4 GIT binary patch literal 1803 zcmb7FdsNbA82)KiW;%8A98l3PS1Yru8D^4FX*#WG<^@fYshe^{a9+ujVa?1^!L2o% zjwn$lSuj?LisG3U*jmEC3y8WlNtqL*Nn-NzGCF75nfqsF|9s~=@8vts^S|Zqoru1eq@9eQI-G)vvVXD`!ox~8 z?b~L*D}EOo8h0l44fvb$pIX-1#8;JWy5EmF)L)mh*o^Kr<`la;kW>qm7$Q0XkMfR+ z(fb#_{&v*lIJJ6N>F@tgMA<=ZGy?!H*w9u0h)XZE2TFis3jlr`Y6e)^1p|Qjf0&}x z{UH%8P5NF)vc>1ms=U={FS1__RSyTNuX^>C-)iqz8gerh<2}peIx;E{2H}jB4Ev!8 zmhQ1B^j<|Um)_d%8?X-kY)2C0UsC1ePLG0v;v>AeYbdfIr+amIO&?L`%txo=@{n1r zTN*|?asr{~XCrTLj;MWjwWoOo+iq;qxPOg*nxwnnw=?Pd*xBU0t1s1!k)8-jYVspR zymkJh-S+97o8ocDGwAz{lxBbb(?t7^i4#g~*V)&`Cf2%+RCk6mBo}5yDC*rkZDBW< zW&4W>MOiKSm9ET$)p*won|B`MPNz*L4?`*x6%Fi)2ZPe9I3iMnvMn<6Coii4K^emM zFb8e?o$6ST@{*Wv5{AHPRY=Kv-tE&Lq}RR`LD#H2+1q8lAfaM9WUF5-)Nao%%?>?4 zn4E(ZdGPy{^AU_828}PHqJo(hSHHET7xsz5%9+9^uFf~6m`@G0&P!~ctnMPiw85aZ zC15gK*A~`6id)foBzeXm$xQn7yNzJPGy*bZsd_!J18mQo5W1p3@rbxl2B9VB76o^d zAH*dvsdJJiuZT8y-7wUMkqPLtFvR;LT0{>xm;JC%pOT)iXqeUy-b4vBnPgDzsIi7nXF^Ucr;qTwTO%Kdz?l)> zGPFew(>|);^O~Ha!=p-{PR4W3Tq{lqR?+k~|I414#(4998E&*c&@>|!x*v7<`&EQqo@rE^i+-{q=@5ByK^@?xsnd2W~3V8R}$bY|i7#rg6 z{x#%@Ez#ERA?^9oyWNkPX;#-LWGpFEqdQF{VUtpExs-Y-lp@t$Q#7I{M^&k?jTOA2 z7^`<}5yT09ZA;Aq6H8Cnrpk#cc!zOOX2K+fWt`c6U5KkB>|oNOwL+H*Hy`hVJGHU$ z)>&OMCt=Bnc6`?EW8y)rmme-APkgA8bl?T)kq_n4iqolXhj>3anjxW=Wts9*-K=@7 zNOb)JcUjM22MPoqQ`HY(26N}PeD)6gczQ$jodiM`_jCswcDFK zmIrbz^`nP$AFGk{=CLnS$7nIyCc0qHr7q0aM$%}{V>izlzGQp3yo&#kQ}xXfOt%7Q z7hran{&+zeYaGfs-MoKeD+gW-oSQYuW{bn*v~YD)r{K-NZ0oV2a$`V!*y;s}{`o)Z z%3CM?q_W@fr@`Ec}K5;l<0J?v9jIQONTP%D+V z3l--%&UoXnQ}ucEW1Hke?)fY+w6`l?g469M=Z0r;u9$$4d^H19tFA0I;xh sH!Z2&|7Y@T<8q|{Py%_Ce3@)^nI~n&lzq}+T5>?>sqi57$@i}O0Y*fFNdN!< literal 0 HcmV?d00001 diff --git a/labs/developing-with-cloud-code/README.md b/labs/developing-with-cloud-code/README.md new file mode 100644 index 0000000..a7c5aff --- /dev/null +++ b/labs/developing-with-cloud-code/README.md @@ -0,0 +1,161 @@ +# Developing with Cloud Code + +# Objectives + +In this lab you will: + +- Explore Cloud Code Plugins +- Deploy to Kubernetes cluster +- Stream kubernetes Logs +- Utilize hot reloading of changes +- Debug live Kubernetes apps + +# Preparing your workspace + +## Clone the app + +To clone the repository and open it in your development environment: + +1. Open Cloud Shell editor by visiting the following url + +``` +https://ide.cloud.google.com +``` + +1. In the terminal window clone the application source with the following command: + +``` +git clone https://github.com/viglesiasce/sample-app.git -b golden-path +``` + +1. Change into the directory and set the IDE workspace to the repo root + +``` +cd sample-app && cloudshell workspace . +``` + +## Start Minikube + +In this section, you build, test, deploy, and access your application using a local version of kubernetes called Minikube. + +1. In the terminal, start minikube by running: + +``` +minikube start +``` + +> Minikube sets up a local Kubernetes cluster in your Cloud Shell, this setup will take a few minutes. While it is starting, take a moment to review the various interfaces provided by Cloud Code in the next step. + +# Exploring Cloud Code Plugin + +Cloud Code provides IDE support for the full development cycle of Kubernetes and Cloud Run applications, from creating an application from an existing template to monitoring your deployed app's resources. You'll be using different commands and views provided by Cloud Code. In this step you will get acquainted with the basic interfaces. + +1. Review the various explorer views from the activity bar + + Multiple user interface panels are accessible from the Activity bar. To briefly get acquainted with the various view click on the icons for each view + + API Explorer: + + - Click the Cloud Code - Cloud APIs icon Cloud Code from the Activity bar. Additional details for working in this view are available in the [documentation](https://cloud.google.com/code/docs/vscode/client-libraries). + + + ![image](oepub6vk28.png) + + Secret Manager Explorer: + + - Click on the Secret Manager view  in the Activity bar. Additional details for for working in this view are available in the [documentation](https://cloud.google.com/code/docs/vscode/secret-manager) + + ![image](imias3q32ah.png) + + Cloud Run Explorer: + + - Navigate to the Cloud Run Explorer using the Cloud Run icon in the Activity bar on the left . Additional details for for working in this view are available in the [documentation](https://cloud.google.com/code/docs/vscode/cloud-run-overview) + + ![image](4dwlgh6avps.png) + + Kubernetes explorer: + + - Navigate to the Kubernetes Explorer using the icon in the Activity bar on the left . Additional details for for working in this view are available in the [documentation](https://cloud.google.com/code/docs/vscode/k8s-overview) + + ![image](mz5gbbsj9lq.png) + +2. Review common commands available from the status bar + + Frequently used commands can be accessed quickly through the indicator in the status bar. + + - Locate the Cloud Code extension indicator in the status bar and click on it. + ![image](zygset0j2tn.png) + - Review the various commands available to run and debug on CloudRun and Kubernetes + - Click on Open Welcome Page for additional details and sample activities + +3. Review Commands available in the command pallet + + Additional commands are available from the command pallet. Review the list of commands you can access. + + - Open the Command Palette (press Ctrl/Cmd+Shift+P) and then type Cloud Code to filter the available commands. + - Use your arrow keys to cycle through the list of commands. + +# Deploying to Kubernetes cluster + +In this section, you build, test, deploy, and access your application. + +1. In the pane at the bottom of Cloud Shell Editor, select Cloud Code  +![image](sq5ck29lduc.png) +2. In the panel that appears at the top, select Run on Kubernetes. If prompted, select Yes to use the minikube Kubernetes context. + +This command starts a build of the source code and then runs the tests. The build and tests will take a few minutes to run. These tests include unit tests and a validation step that checks the rules that are set for the deployment environment. This validation step is already configured, and it ensures that you get warning of deployment issues even while you're still working in your development environment. + +3. Select the Output tab in the lower pane to view progress and notifications + ![image](wvofl4zlg6a.png) +4. Select "Kubernetes: Run/Debug - Detailed" in the channel drop down to the right to view additional details and logs streaming live from the containers +![image](x30hwes9pu.png) + +When the build and tests are done, the Output tab says: `Resource deployment/gceme-backend-dev status completed successfully`, and two URLs are listed. + +5. In the Cloud Code terminal, hover over the first URL in the output (http://localhost:8080), and then in the tool tip that appears select Open Web Preview. + +The local version of the application opens in your browser. This version of the app is running in minikube's Kubernetes cluster. + +6. In your browser, refresh the page. The number next to Counter increases, showing that the app is responding to your refresh. + +In your browser, keep this page open so that you can view the application as you make any changes in your local environment. + +# Utilize hot reloading of changes + +In this section, you make a change to the application, and view the change as the app runs in the local Kubernetes cluster. In the output tab for the Kubernetes: Run/Debug channel, in addition to the application urls, the output also says `Watching for changes.` This means that watch mode is enabled. While Cloud Code is in watch mode, Cloud Code will detect any saved changes in your repo and will automatically rebuild and redeploy the app with the latest changes. + +1. In Cloud Shell Editor, go to the main.go file. +2. In this main.go file, in line 23, change the color from green to blue. +3. Save the file. + +Cloud Code detects that the change to the app is saved, and it redeploys the change automatically. The Output tab shows Update initiated. This redeployment will take a few minutes to run. + +This automatic rebuild is similar to hot code reloading, which is a feature available for some application types and frameworks. + +4. When the build is done, go to your browser where you have the app open and refresh the page. + +When you refresh, the color at the top of the table changes from blue to green. + +This setup gives you this automatic reloading for any architecture, with any components. When using Cloud Code and minikube, anything that is running in Kubernetes has this hot code reloading functionality. + +# Debugging live Kubernetes apps + +You have run the application, made a change, and viewed the running app. In this section, you debug the application to be confident that it's ready to commit back into the main repo. + +For this debug example, we'll focus on the section of the code for the page counter. + +1. In Cloud Shell Editor, open the file main.go +2. Set a breakpoint in the application by clicking to the left number of line 82 (if err != nil {) +3. In the blue pane at the bottom of Cloud Shell Editor, select Cloud Code ![image](y1nu68cr85h.png) +4. In the panel that appears at the top, select Debug on Kubernetes. + +Cloud Code runs and attaches debuggers so that you'll be able to access the in-memory state of the application, not just the user-facing behavior of the application. + +5. At the end of the deploy process a prompt will appear at the top of your window asking to confirm the directory in the container where the application is deployed. ![image](tw2ka8wmpl.png) + + Verify the value is set to /go/src/app and hit enter to accept the value +6. Wait for the debugger to finish deploying. You'll know it's complete when the status bar turns orange and the output reports `"Attached debugger to container "sample-app-dev-..." successfully`." +7. In the Cloud Code terminal, hover over the first URL in the output (http://localhost:8081), and then in the tool tip that appears select Open Web Preview. The page won't finish loading which is expected. +8. Switch back to the IDE where the debugger now appears. Code comes up in the tab, and you'll see the call stack, what variables are available at that part of the code. You can expand the Variables - Local to see here the current counter variable value. +9. To allow the page to continue to load, select the "Continue" icon in the debugging window ![image](fyg1yamuy0d.png) +10. When you're finished debugging click the stop button to terminate each of the running threads. ![image](61jmwjqwlhk.png) \ No newline at end of file diff --git a/labs/developing-with-cloud-code/fyg1yamuy0d.png b/labs/developing-with-cloud-code/fyg1yamuy0d.png new file mode 100644 index 0000000000000000000000000000000000000000..a69b795a1635ab4700eeaf5056e808a4c9b340a2 GIT binary patch literal 1868 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 literal 0 HcmV?d00001 diff --git a/labs/developing-with-cloud-code/oepub6vk28.png b/labs/developing-with-cloud-code/oepub6vk28.png new file mode 100644 index 0000000000000000000000000000000000000000..504c2000d5a135ed20fc578540c93d9dbd297cad GIT binary patch literal 1701 zcmV;W23q-vP);H0)Wkai8dr)EMO+y#fLEe|8-s{Z5mtBs6-e-gJ1?jp z>P`g-HxkxRLC>y~m{I`Cca(xpp{U|pcE zMeDV9@7|L{|N=+}&E*N5P^+ifqS()v-I0~+Mx9{eh-e}Ofxhq$$@S#we zo12xIiK107{-m*PI%RftR+N^OhTVst#B)DFz7Ij6`w^EfU*P^XJdjws_(~5vUFFZ8KVtXp-D2Uwg-YFcxpe6g|1qx9)YPQZixtlW zgN}|4eypy7z8J0B#*G`5n(?q?$r5q+@L~RAT<7@l<4Vm~(IOb!yLXQtuVc`vK`#2^ z-@kv0UAuNEHDjT^zFxd~^-ARA&2OP1wQUcP)O z7B5~bs;a8^kdl)pPbxJNLAzk^;K2ia9#+uzqW?wrfPUC6Q&Ur7$BrFJO*<}JxWIQ0 z<;9B^A}cG)QSzx%r<9tB;)`JL@ZrO-lR>{ra?zqWWpZ*-Y~Q|JsVQ^*{CU2clIPE# zi_FYSarEd>J|s?`KCRUB0^bCK&dyHjWWId)V)eUd$(+^qk`k+1h$Z9W<6`U9tzq@fo;}N_ zMa|QvPpw}2*s)_;MS=KX1p#a*22(*VWZ2b#xJ^VDRM06Mp`- zL9562C5nrS#rN;u`7X9ObLNb)zfJ-dJa_I~(cRt6J%j}<9^a3H`xifc{NTHW1#33n zPd9-J1{oO{5hbIw(^Vi6#+hU;JkU@exH3&`j5qY0Lu&@@+Y`hUvY z0=b>CYsCopT^Zd4GlE?p|M~MSK}LQPLY;vTY)BX*STjbjW{hCX7{Qt`f;D3VYsLuH vj1jCEBUm&4wBWR~w6K3Ud^`U>QBdzM9P8ww7!Q8^00000NkvXXu0mjf%{^3! literal 0 HcmV?d00001 diff --git a/labs/developing-with-cloud-code/sq5ck29lduc.png b/labs/developing-with-cloud-code/sq5ck29lduc.png new file mode 100644 index 0000000000000000000000000000000000000000..28d59eb97e3a475120b70eb560d7c227c70b4fa0 GIT binary patch literal 2359 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 literal 0 HcmV?d00001 diff --git a/labs/developing-with-cloud-code/tw2ka8wmpl.png b/labs/developing-with-cloud-code/tw2ka8wmpl.png new file mode 100644 index 0000000000000000000000000000000000000000..6733a6364f90592cfc80bfb70b203633f4ccd11c GIT binary patch literal 16796 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 literal 0 HcmV?d00001 diff --git a/labs/developing-with-cloud-code/wvofl4zlg6a.png b/labs/developing-with-cloud-code/wvofl4zlg6a.png new file mode 100644 index 0000000000000000000000000000000000000000..53ccc68c3276707fd18ec04b90bca2baaf32dec2 GIT binary patch literal 3729 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 N&h{SE0=s~ie*uhCKI{Mh literal 0 HcmV?d00001 diff --git a/labs/developing-with-cloud-code/x30hwes9pu.png b/labs/developing-with-cloud-code/x30hwes9pu.png new file mode 100644 index 0000000000000000000000000000000000000000..56df6ddec0c69b36ce24cac0058636c3de5d2015 GIT binary patch literal 5899 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 literal 0 HcmV?d00001 diff --git a/labs/developing-with-cloud-code/zygset0j2tn.png b/labs/developing-with-cloud-code/zygset0j2tn.png new file mode 100644 index 0000000000000000000000000000000000000000..28d59eb97e3a475120b70eb560d7c227c70b4fa0 GIT binary patch literal 2359 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 literal 0 HcmV?d00001 From 127d8cbfcfdaaddb8b597b790b4f0f680ecd4df9 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 1 Oct 2021 13:01:49 -0500 Subject: [PATCH 31/50] App onboarding (#22) * app onboarding lab --- delivery-platform/app.sh | 179 +++++++++ delivery-platform/onboard-env.sh | 60 ++++ .../golang/cloudbuild-build-only.yaml | 51 +++ ...dbuild-cd.yaml.tmpl => cloudbuild-cd.yaml} | 2 +- labs/app-onboarding/README.md | 339 ++++++++++++++++++ 5 files changed, 630 insertions(+), 1 deletion(-) create mode 100755 delivery-platform/app.sh create mode 100755 delivery-platform/onboard-env.sh create mode 100644 delivery-platform/resources/repos/app-templates/golang/cloudbuild-build-only.yaml rename delivery-platform/resources/repos/app-templates/golang/{cloudbuild-cd.yaml.tmpl => cloudbuild-cd.yaml} (97%) create mode 100644 labs/app-onboarding/README.md diff --git a/delivery-platform/app.sh b/delivery-platform/app.sh new file mode 100755 index 0000000..eb77966 --- /dev/null +++ b/delivery-platform/app.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + + +### +# +# Used for app oboarding and termination. +# App creation clones the templates repo then copies the +# desierd template folder to a temporary workspace. The script +# then substitutes place holder values for actual values creates a +# new remote remote and pushes the initial version. +# Additonally if ACM is in use this script adds appropriate namespace +# configurations to ensure the app is managed by ACM. +# +# USAGE: +# app.sh +# +### + + +create () { + APP_NAME=${1:-"my-app"} + APP_LANG=${2:-"golang"} + + + # Ensure the git vendor script is set + if [[ -z "$GIT_CMD" ]]; then + echo "GIT_CMD not set - exiting" 1>&2 + exit 1 + fi + # Ensure the git token is set + if [[ -z "$GIT_TOKEN" ]]; then + echo "GIT_TOKEN not set - exiting" 1>&2 + exit 1 + fi + + + printf 'Creating application: %s \n' $APP_NAME + + # Create an instance of the template. + cd $WORK_DIR/ + git clone -b main $GIT_BASE_URL/$APP_TEMPLATES_REPO app-templates + rm -rf app-templates/.git + cd app-templates/${APP_LANG} + + ## Insert name of new app + for template in $(find . -name '*.tmpl'); do envsubst < ${template} > ${template%.*}; done + + + ## Create and push to new repo + git init + git checkout -b main + git symbolic-ref HEAD refs/heads/main + $BASE_DIR/scripts/git/${GIT_CMD} create ${APP_NAME} + git remote add origin $GIT_BASE_URL/${APP_NAME} + git add . && git commit -m "initial commit" + git push origin main + # Auth fails intermittetly on the very first client call for some reason + # Adding a retry to ensure the source is pushed. + git push origin main + + + # Configure Pipeline + create_cloudbuild_trigger ${APP_NAME} + + # Initial deploy + cd $WORK_DIR/app-templates/${APP_LANG} + git pull + echo "v1" > version.txt + git add . && git commit -m "v1" + git push origin main + sleep 10 + git pull + git tag v1 + git push origin v1 + + # Cleanup + cd $BASE_DIR + rm -rf $WORK_DIR/app-templates +} + + + +delete () { + echo 'Destroy Application' + APP_NAME=${1:-"my-app"} + + $BASE_DIR/scripts/git/${GIT_CMD} delete $APP_NAME + + # Remove any orphaned hydrated directories from other processes + rm -rf $WORK_DIR/$APP_NAME-hydrated + + + #Also delete CD pipelines. Pipelines are in us-central1 + gcloud alpha deploy delivery-pipelines delete ${APP_NAME} --region="us-central1" --force -q || true + + + # Delete secret + SECRET_NAME=${APP_NAME}-webhook-trigger-cd-secret + gcloud secrets delete ${SECRET_NAME} -q + + + # Delete trigger + TRIGGER_NAME=${APP_NAME}-clouddeploy-webhook-trigger + gcloud alpha builds triggers delete ${TRIGGER_NAME} -q + +} + + +create_cloudbuild_trigger () { + APP_NAME=${1:-"my-app"} + ## Project variables + if [[ ${PROJECT_ID} == "" ]]; then + echo "PROJECT_ID env variable is not set" + exit -1 + fi + if [[ ${PROJECT_NUMBER} == "" ]]; then + echo "PROJECT_NUMBER env variable is not set" + exit -1 + fi + + ## API Key + if [[ ${APP_LANG} == "" ]]; then + echo "APP_LANG env variable is not set" + exit -1 + fi + + ## API Key + if [[ ${API_KEY_VALUE} == "" ]]; then + echo "API_KEY_VALUE env variable is not set" + exit -1 + fi + + + ## Create Secret + SECRET_NAME=${APP_NAME}-webhook-trigger-cd-secret + SECRET_VALUE=$(sed "s/[^a-zA-Z0-9]//g" <<< $(openssl rand -base64 15)) + SECRET_PATH=projects/${PROJECT_NUMBER}/secrets/${SECRET_NAME}/versions/1 + printf ${SECRET_VALUE} | gcloud secrets create ${SECRET_NAME} --data-file=- + gcloud secrets add-iam-policy-binding ${SECRET_NAME} \ + --member=serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-cloudbuild.iam.gserviceaccount.com \ + --role='roles/secretmanager.secretAccessor' + + ## Create CloudBuild Webhook Endpoint + REPO_LOCATION=https://github.com/${GIT_USERNAME}/${APP_NAME} + + TRIGGER_NAME=${APP_NAME}-clouddeploy-webhook-trigger + BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/cloudbuild-build-only.yaml + + ## Setup Trigger & Webhook + gcloud alpha builds triggers create webhook \ + --name=${TRIGGER_NAME} \ + --substitutions='_APP_NAME='${APP_NAME}',_APP_REPO=$(body.repository.git_url),_CONFIG_REPO='${GIT_BASE_URL}'/'${CLUSTER_CONFIG_REPO}',_DEFAULT_IMAGE_REPO='${IMAGE_REPO}',_KUSTOMIZE_REPO='${GIT_BASE_URL}'/'${SHARED_KUSTOMIZE_REPO}',_REF=$(body.ref)' \ + --inline-config=$BUILD_YAML_PATH \ + --secret=${SECRET_PATH} + + ## Retrieve the URL + WEBHOOK_URL="https://cloudbuild.googleapis.com/v1/projects/${PROJECT_ID}/triggers/${TRIGGER_NAME}:webhook?key=${API_KEY_VALUE}&secret=${SECRET_VALUE}" + + ## Create Github Webhook + $BASE_DIR/scripts/git/${GIT_CMD} create_webhook ${APP_NAME} $WEBHOOK_URL + +} + +# execute function matching first arg and pass rest of args through +$1 $2 $3 $4 $5 $6 diff --git a/delivery-platform/onboard-env.sh b/delivery-platform/onboard-env.sh new file mode 100755 index 0000000..8ba14f8 --- /dev/null +++ b/delivery-platform/onboard-env.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# Copyright 2021 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. + +## Set Environment Variables +export PROJECT_ID=$(gcloud config get-value project) +export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') + + +# Set Base directory & Working directory variables +export BASE_DIR=$PWD +export WORK_DIR=$BASE_DIR/workdir +export SCRIPTS=$BASE_DIR/scripts +mkdir -p $WORK_DIR/bin + +export PATH=$PATH:$WORK_DIR/bin:$SCRIPTS: + +export GIT_PROVIDER=GitHub +export GIT_CMD=gh.sh +export CONTINUOUS_DELIVERY_SYSTEM=Clouddeploy + +# Load any persisted variables +source $SCRIPTS/common/manage-state.sh +load_state + +# Git details +git config --global user.email $(gcloud config get-value account) +git config --global user.name ${USER} +source $SCRIPTS/git/set-git-env.sh + +source $SCRIPTS/common/set-apikey-var.sh + +# Repo Names +export REPO_PREFIX=mcd +export APP_TEMPLATES_REPO=$REPO_PREFIX-app-templates +export SHARED_KUSTOMIZE_REPO=$REPO_PREFIX-shared_kustomize +export CLUSTER_CONFIG_REPO=$REPO_PREFIX-cluster-config +export HYDRATED_CONFIG_REPO=${CLUSTER_CONFIG_REPO} + +# Repository Name +export IMAGE_REPO=gcr.io/${PROJECT_ID} + +# variable pass through for access tokens +export GIT_ASKPASS=$SCRIPTS/git/git-ask-pass.sh + + +# Persist variables for later use +write_state \ No newline at end of file diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-build-only.yaml b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-build-only.yaml new file mode 100644 index 0000000..e8d1262 --- /dev/null +++ b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-build-only.yaml @@ -0,0 +1,51 @@ +# Copyright 2021 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. + +steps: + + # Clone the repos + - id: clone-app + name: gcr.io/cloud-builders/git + entrypoint: bash + args: + - '-c' + - | + IFS='/' read -a array <<< "${_REF}" + git clone -b ${array[2]} ${_APP_REPO} app-repo + + - id: clone-kustomize + name: gcr.io/cloud-builders/git + entrypoint: bash + # changed from GIT_URL to APP_REPO + args: + - '-c' + - | + git clone ${_KUSTOMIZE_REPO} kustomize-base + sleep 5 + + # Build and push the image + - id: skaffold-build + name: gcr.io/k8s-skaffold/skaffold + entrypoint: bash + args: + - '-c' + - | + cd app-repo + skaffold build --file-output=/workspace/artifacts.json \ + --default-repo ${_DEFAULT_IMAGE_REPO} \ + --push=true + + + + diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml.tmpl b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml similarity index 97% rename from delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml.tmpl rename to delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml index 9519783..77a3b4c 100644 --- a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml.tmpl +++ b/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml @@ -70,7 +70,7 @@ steps: gcloud beta deploy apply --file deploy/stage.yaml gcloud beta deploy apply --file deploy/prod.yaml gcloud beta deploy releases create rel-${SHORT_SHA}-$(date +%s) \ - --delivery-pipeline ${APP_NAME} \ + --delivery-pipeline ${_APP_NAME} \ --description "$(git log -1 --pretty='%s')" \ --build-artifacts /workspace/artifacts.json \ --annotations="commit_ui=${_APP_REPO}/+/$COMMIT_SHA" diff --git a/labs/app-onboarding/README.md b/labs/app-onboarding/README.md new file mode 100644 index 0000000..78c47e8 --- /dev/null +++ b/labs/app-onboarding/README.md @@ -0,0 +1,339 @@ +# App Onboarding + +In this lab you will + +* Utilize predefined & custom app templates +* Create a new instance of an application +* Implement Build, Package and Deploy logic +* Configure automated pipeline execution +* Bundle onboarding logic for simple app creation + +## Preparing your workspace + +1. Open Cloud Shell editor by visiting the following url + +``` +https://ide.cloud.google.com +``` + +2. Ensure your project name is set in CLI + +``` +gcloud config set project {{project-id}} +``` + +3. Enable APIs + +``` +gcloud services enable \ + cloudbuild.googleapis.com \ + secretmanager.googleapis.com + +``` + +4. In the terminal window clone the application source with the following command: + +``` +git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop.git +``` + +5. Change into the directory and set the IDE workspace to the repo root + +``` +cd software-delivery-workshop && rm -rf .git +cd delivery-platform && cloudshell workspace . +``` + +## Utilizing predefined & custom app templates + +Developers should be able to choose from a set of templates commonly used within the organization. The onboarding process will create a centralized set of template repositories stored in your GitHub account. In later steps these template repositories will be copied and modified for use as the base for new applications. For this lab you will seed your template repository with a sample structure provided here. You can add your own templates by adding additional folders modeled after the sample. + +In this step you will create your own repository to hold app templates, from the example files provided. A helper script is provided to simplify the interactions with GitHub. + +These are one time steps used to populate your template repositories. Future steps will reused these repositories. + +### Configure GitHub Access + +The steps in this tutorial call the GitHub API to create and configure repositories. Your GitHub username and a personal access token are required at various points that follow. The script below will help you acquire the values and store them as local variables for later use. + +``` +source ./onboard-env.sh +``` + +### Create App Template Repository + +Sample application templates are provided along with this lab as an example of how you might integrate your own base templates. In this step you create your own copy of these files in a repo called `mcd-app-templates` in your GitHub account. + +1. Copy the template to the working directory + +``` +cp -R $BASE_DIR/resources/repos/app-templates $WORK_DIR +cd $WORK_DIR/app-templates +``` + +2. Create an empty remote repository in your GitHub account + +``` +$BASE_DIR/scripts/git/gh.sh create mcd-app-templates +``` + +3. Push the template repository to your remote repository + +``` +git init && git symbolic-ref HEAD refs/heads/main && git add . && git commit -m "initial commit" +git remote add origin $GIT_BASE_URL/mcd-app-templates +git push origin main +``` + +4. Clean up the working directory + +``` +cd $BASE_DIR +rm -rf $WORK_DIR/app-templates +``` + +### Create Shared Base Configs Repository + +This tutorial utilizes a tool called Kustomize that uses base configuration files shared by multiple teams then overlays application specific configurations over top. This enables platform teams to scale across many teams and environments. + +In this step you create the shared configuration repository called `mcd-shared_kustomize `from the samples provided + +1. Copy the template to the working directory + +``` +cp -R $BASE_DIR/resources/repos/shared-kustomize $WORK_DIR +cd $WORK_DIR/shared-kustomize +``` + +2. Create an empty remote repository in your GitHub account + +``` +$BASE_DIR/scripts/git/gh.sh create mcd-shared_kustomize +``` + +3. Push the template repository to your remote repository + +``` +git init && git symbolic-ref HEAD refs/heads/main && git add . && git commit -m "initial commit" +git remote add origin $GIT_BASE_URL/mcd-shared_kustomize +git push origin main +``` + +4. Clean up the working directory + +``` +cd $BASE_DIR +rm -rf $WORK_DIR/shared-kustomize +``` + +With your template repositories created you're ready to use them to create an app instance + +## Creating a new instance of an application + +Creating a new application from a template often requires that placeholder variables be swapped out with real values across multiple files in the template structure. Once the substitution is completed a new repository is created for the new app instance. It is this app instance repository that the developers will clone and work with in their day to day development. + +In this step you will substitute values in an app template and post the resulting files to a new repository. + +### Define a name for the new application + +``` +export APP_NAME=my-app +``` + +### Retrieve the Golang template repository + +``` +cd $WORK_DIR/ +git clone -b main $GIT_BASE_URL/mcd-app-templates app-templates +rm -rf app-templates/.git +cd app-templates/golang +``` + +### Substitute placeholder values + +One of the most common needs for onboarding is swapping out variables in templates for actual instances used in the application. For example providing the application name. The following command creates instances of all .tmpl files with the values stored in environment variables. + +``` +for template in $(find . -name '*.tmpl'); do envsubst < ${template} > ${template%.*}; done +``` + +### Create a new repo and store the updated files + +1. Create an empty remote repository in your GitHub account + +``` +$BASE_DIR/scripts/git/gh.sh create ${APP_NAME} +``` + +2. Push the template repository to your remote repository + +``` +git init && git symbolic-ref HEAD refs/heads/main && git add . && git commit -m "initial commit" +git remote add origin $GIT_BASE_URL/${APP_NAME} +git push origin main +``` + +Now that the app instance has been created it's time to implement continuous builds. + + +## Configuring automated pipeline execution + +The central part of a Continuous Integration system is the ability to execute the pipeline logic based on the events originating in the source control system. When a developer commits code in their repository events are fired that can be configured to trigger processes in other systems. + +In this step you will configure GitHub to call Google Cloud Build and execute your pipeline, whenever users commit or tag code in their repository. + +### Enable Secure Access + +You will need 2 elements to configure secure access to your application pipeline. An API key and a secret unique to the pipeline. + +#### API Key + +The API key is used to identify the client that is calling into a given API. In this case the client will be GitHub. A best practice not covered here is to lock down the scope of the API key to only the specific APIs that client will be accessing. You created the key in a previous step. + +1. You can review the key by clicking on [this link]( https://console.cloud.google.com/apis/credentials) +2. You can ensure the value is set by running the following command + +``` +echo $API_KEY_VALUE +``` + +#### Pipeline Secret + +The secrets are used to authorize a caller and ensure they have rights to the specific cloud build target job. You may have 2 different repositories in GitHub that should only have access to their own pipelines. While the API_KEY limits which APIs can be utilized by github (in this case the Cloud Build API is being called), the secret limits which Job in the Cloud Build API can be executed by the client. + +1. Define the secret name, location and value + +``` +SECRET_NAME=${APP_NAME}-webhook-trigger-cd-secret +SECRET_PATH=projects/${PROJECT_NUMBER}/secrets/${SECRET_NAME}/versions/1 +SECRET_VALUE=$(sed "s/[^a-zA-Z0-9]//g" <<< $(openssl rand -base64 15)) +``` + +2. Create the secret + +``` +printf ${SECRET_VALUE} | gcloud secrets create ${SECRET_NAME} --data-file=- +``` + +3. Allow Cloud Build to read the secret + +``` +gcloud secrets add-iam-policy-binding ${SECRET_NAME} \ + --member=serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-cloudbuild.iam.gserviceaccount.com \ + --role='roles/secretmanager.secretAccessor' +``` + +### Create Cloud Build Trigger + +The Cloud Build Trigger is the configuration that will actually be executing the CICD processes. +The job requires a few key values to be provided on creation in order to properly configure the trigger. + +1. Define the name of the trigger and where the configuration file can be found + +``` +export TRIGGER_NAME=${APP_NAME}-clouddeploy-webhook-trigger +export BUILD_YAML_PATH=$WORK_DIR/app-templates/golang/cloudbuild-build-only.yaml +``` + +2. Define the location of the shared base configuration repo. + +``` +export KUSTOMIZE_REPO=${GIT_BASE_URL}/mcd-shared_kustomize +``` + +3. A variable was set in the onboard-env.sh script defining the project's container registry. Review the value with the command below. + +``` +echo $IMAGE_REPO +``` + + +5. Create CloudBuild Webhook Trigger using the variables created previously. The application repo location is pulled from the body of the request from GitHub. A value below references the path in the request body where it's located + +``` + gcloud alpha builds triggers create webhook \ + --name=${TRIGGER_NAME} \ + --substitutions='_APP_NAME='${APP_NAME}',_APP_REPO=$(body.repository.git_url),_CONFIG_REPO='${GIT_BASE_URL}'/'${CLUSTER_CONFIG_REPO}',_DEFAULT_IMAGE_REPO='${IMAGE_REPO}',_KUSTOMIZE_REPO='${GIT_BASE_URL}'/'${SHARED_KUSTOMIZE_REPO}',_REF=$(body.ref)' \ + --inline-config=$BUILD_YAML_PATH \ + --secret=${SECRET_PATH} +``` + +6. Review the newly created Cloud Build trigger in the Console by [visiting this link](https://console.cloud.google.com/cloud-build/triggers) + + + +### Configure GitHub Webhook + +1. Define a variable for the webhook URL + +``` +WEBHOOK_URL="https://cloudbuild.googleapis.com/v1/projects/${PROJECT_ID}/triggers/${TRIGGER_NAME}:webhook?key=${API_KEY_VALUE}&secret=${SECRET_VALUE}" +``` + +2. Configure the webhook in GitHub + +``` +$BASE_DIR/scripts/git/gh.sh create_webhook ${APP_NAME} $WEBHOOK_URL + +``` + +3. Go to the application repo and review the newly configured webhook + +``` +REPO_URL=${GIT_BASE_URL}/${APP_NAME}/settings/hooks +echo $REPO_URL +``` + +Now that you've manually performed all the steps needed to create a new application it's time to automate it in a script. + +## Automating all the onboarding steps + +In practice it's not feasible to execute each of the above steps for every new application. Instead the logic should be incorporated into a script for easy execution. The steps above have already been included in a script for your use. + +In this step you will use the script provided to create a new application + +### Create a new application + +1. Ensure you're in the right directory + +``` +cd $BASE_DIR +``` + +2. Create a new application + +``` +./app.sh create demo-app +``` + +All of the steps are executed automatically. + +### Review the GitHub Repo + +At this point you will be able to review the new repository in Github + +1. Retrieve the GitHub repository URL by executing the following command + +``` +echo ${GIT_BASE_URL}/${APP_NAME} +``` + +2. Open the URL with your web browser to review the new application +3. Note examples where the template variables have been replace with instance values as shown in the url below + +``` +echo ${GIT_BASE_URL}/${APP_NAME}/blob/main/k8s/prod/deployment.yaml#L24 +``` + +4. Review the web hook configured at the url below + +``` +echo ${GIT_BASE_URL}/${APP_NAME}/settings/hooks +``` + +### Review the CloudBuild Trigger + +The trigger was automatically set up by the script + +1. Review the Cloud Build trigger in the Console by [visiting this link](https://console.cloud.google.com/cloud-build/triggers) +2. Review the build history [on this page](https://console.cloud.google.com/cloud-build/builds) \ No newline at end of file From 1fb9d7b7fcf3a98a621ae26e8459e0748601d009 Mon Sep 17 00:00:00 2001 From: Shobhit Gupta <43795024+gushob21@users.noreply.github.com> Date: Mon, 11 Oct 2021 22:02:07 +0000 Subject: [PATCH 32/50] Adding standalone files for cloud-deploy (#24) Co-authored-by: gushob --- labs/cloud-deploy/Dockerfile | 19 ++++++++ labs/cloud-deploy/README.md | 1 + labs/cloud-deploy/go.mod | 3 ++ labs/cloud-deploy/k8s/canary/deployment.yaml | 35 ++++++++++++++ .../k8s/canary/kustomization.yaml | 22 +++++++++ labs/cloud-deploy/k8s/preview/deployment.yaml | 34 +++++++++++++ .../k8s/preview/kustomization.yaml | 22 +++++++++ labs/cloud-deploy/k8s/prod/deployment.yaml | 34 +++++++++++++ labs/cloud-deploy/k8s/prod/kustomization.yaml | 22 +++++++++ .../kustomize-base/golang/deployment.yaml | 48 +++++++++++++++++++ .../kustomize-base/golang/kustomization.yaml | 17 +++++++ .../kustomize-base/golang/service.yaml | 25 ++++++++++ labs/cloud-deploy/main.go | 40 ++++++++++++++++ labs/cloud-deploy/skaffold.yaml | 41 ++++++++++++++++ 14 files changed, 363 insertions(+) create mode 100644 labs/cloud-deploy/Dockerfile create mode 100644 labs/cloud-deploy/README.md create mode 100644 labs/cloud-deploy/go.mod create mode 100644 labs/cloud-deploy/k8s/canary/deployment.yaml create mode 100644 labs/cloud-deploy/k8s/canary/kustomization.yaml create mode 100644 labs/cloud-deploy/k8s/preview/deployment.yaml create mode 100644 labs/cloud-deploy/k8s/preview/kustomization.yaml create mode 100644 labs/cloud-deploy/k8s/prod/deployment.yaml create mode 100644 labs/cloud-deploy/k8s/prod/kustomization.yaml create mode 100644 labs/cloud-deploy/kustomize-base/golang/deployment.yaml create mode 100644 labs/cloud-deploy/kustomize-base/golang/kustomization.yaml create mode 100644 labs/cloud-deploy/kustomize-base/golang/service.yaml create mode 100644 labs/cloud-deploy/main.go create mode 100644 labs/cloud-deploy/skaffold.yaml diff --git a/labs/cloud-deploy/Dockerfile b/labs/cloud-deploy/Dockerfile new file mode 100644 index 0000000..28d9b14 --- /dev/null +++ b/labs/cloud-deploy/Dockerfile @@ -0,0 +1,19 @@ +# Copyright 2021 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. + +FROM golang:1 +WORKDIR /app +COPY * ./ +RUN go build -o cloud-deploy-tutorial +CMD ["/app/cloud-deploy-tutorial"] diff --git a/labs/cloud-deploy/README.md b/labs/cloud-deploy/README.md new file mode 100644 index 0000000..5043735 --- /dev/null +++ b/labs/cloud-deploy/README.md @@ -0,0 +1 @@ +# app-template diff --git a/labs/cloud-deploy/go.mod b/labs/cloud-deploy/go.mod new file mode 100644 index 0000000..343c10a --- /dev/null +++ b/labs/cloud-deploy/go.mod @@ -0,0 +1,3 @@ +module example.com/golang + +go 1.16 diff --git a/labs/cloud-deploy/k8s/canary/deployment.yaml b/labs/cloud-deploy/k8s/canary/deployment.yaml new file mode 100644 index 0000000..dec9407 --- /dev/null +++ b/labs/cloud-deploy/k8s/canary/deployment.yaml @@ -0,0 +1,35 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + replicas: 1 + template: + spec: + containers: + - name: app + image: app + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: canary diff --git a/labs/cloud-deploy/k8s/canary/kustomization.yaml b/labs/cloud-deploy/k8s/canary/kustomization.yaml new file mode 100644 index 0000000..8c422d6 --- /dev/null +++ b/labs/cloud-deploy/k8s/canary/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +bases: +- ../../kustomize-base/golang +patches: +- deployment.yaml +commonLabels: + app: cloud-deploy-tutorial + role: backend +namePrefix: cloud-deploy-sample- diff --git a/labs/cloud-deploy/k8s/preview/deployment.yaml b/labs/cloud-deploy/k8s/preview/deployment.yaml new file mode 100644 index 0000000..29b7eaa --- /dev/null +++ b/labs/cloud-deploy/k8s/preview/deployment.yaml @@ -0,0 +1,34 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: app # Must match base value + image: app # Overwrites values from base - Needs to match skaffold artifact + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: preview diff --git a/labs/cloud-deploy/k8s/preview/kustomization.yaml b/labs/cloud-deploy/k8s/preview/kustomization.yaml new file mode 100644 index 0000000..898b96a --- /dev/null +++ b/labs/cloud-deploy/k8s/preview/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +bases: +- ../../kustomize-base/golang +patches: +- deployment.yaml +namePrefix: cloud-deploy-sample- # App name (dash) +commonLabels: + app: cloud-deploy-tutorial # App name for selectors + role: backend diff --git a/labs/cloud-deploy/k8s/prod/deployment.yaml b/labs/cloud-deploy/k8s/prod/deployment.yaml new file mode 100644 index 0000000..f32c239 --- /dev/null +++ b/labs/cloud-deploy/k8s/prod/deployment.yaml @@ -0,0 +1,34 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + template: + spec: + containers: + - name: app + image: app + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 256Mi + env: + - name: ENVIRONMENT + value: prod diff --git a/labs/cloud-deploy/k8s/prod/kustomization.yaml b/labs/cloud-deploy/k8s/prod/kustomization.yaml new file mode 100644 index 0000000..9143d26 --- /dev/null +++ b/labs/cloud-deploy/k8s/prod/kustomization.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +bases: +- ../../kustomize-base/golang +patches: +- deployment.yaml +namePrefix: cloud-deploy-sample- +commonLabels: + app: cloud-deploy-tutorial + role: backend diff --git a/labs/cloud-deploy/kustomize-base/golang/deployment.yaml b/labs/cloud-deploy/kustomize-base/golang/deployment.yaml new file mode 100644 index 0000000..080de4c --- /dev/null +++ b/labs/cloud-deploy/kustomize-base/golang/deployment.yaml @@ -0,0 +1,48 @@ +# Copyright 2021 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. + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: app +spec: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + template: + metadata: + name: app + spec: + containers: + - name: app + image: app + resources: + limits: + memory: "512Mi" + cpu: "500m" + env: + - name: ENVIRONMENT + value: base + - name: LOG_LEVEL + value: info + readinessProbe: + initialDelaySeconds: 1 + periodSeconds: 1 + httpGet: + path: /healthz + port: 8080 + ports: + - name: http + containerPort: 8080 diff --git a/labs/cloud-deploy/kustomize-base/golang/kustomization.yaml b/labs/cloud-deploy/kustomize-base/golang/kustomization.yaml new file mode 100644 index 0000000..8c06dd4 --- /dev/null +++ b/labs/cloud-deploy/kustomize-base/golang/kustomization.yaml @@ -0,0 +1,17 @@ +# Copyright 2021 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. + +resources: + - service.yaml + - deployment.yaml diff --git a/labs/cloud-deploy/kustomize-base/golang/service.yaml b/labs/cloud-deploy/kustomize-base/golang/service.yaml new file mode 100644 index 0000000..5807847 --- /dev/null +++ b/labs/cloud-deploy/kustomize-base/golang/service.yaml @@ -0,0 +1,25 @@ +# Copyright 2021 Google Inc. All rights reserved. +# +# 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. + +kind: Service +apiVersion: v1 +metadata: + name: app +spec: + type: ClusterIP + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP diff --git a/labs/cloud-deploy/main.go b/labs/cloud-deploy/main.go new file mode 100644 index 0000000..4b896c1 --- /dev/null +++ b/labs/cloud-deploy/main.go @@ -0,0 +1,40 @@ +// Copyright 2021 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. + +package main + +import ( + "fmt" + "log" + "net/http" + "os" +) + +func main() { + env := os.Getenv("ENVIRONMENT") + port := 8080 + log.Printf("Running in environment: %s\n", env) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Printf("Received request from %s at %s", r.RemoteAddr, r.URL.EscapedPath()) + fmt.Fprint(w, "Hello World!") + }) + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + log.Printf("Received health check from %s", r.RemoteAddr) + w.WriteHeader(http.StatusOK) + }) + log.Printf("Starting server on port: %v", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) + +} diff --git a/labs/cloud-deploy/skaffold.yaml b/labs/cloud-deploy/skaffold.yaml new file mode 100644 index 0000000..97354e8 --- /dev/null +++ b/labs/cloud-deploy/skaffold.yaml @@ -0,0 +1,41 @@ +# Copyright 2021 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. + +apiVersion: skaffold/v1beta14 +kind: Config +build: + artifacts: + - image: app # Match name in deployment yaml + context: ./ +#deploy: +# kustomize: +# path: k8s/dev + +profiles: +- name: preview + activation: + - command: preview + deploy: + kustomize: + path: k8s/preview + +- name: canary + deploy: + kustomize: + path: k8s/canary + +- name: prod + deploy: + kustomize: + path: k8s/prod From 79f0a561936a4c4568be28a7b7652ea2d911a0ac Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Tue, 12 Oct 2021 17:40:59 -0500 Subject: [PATCH 33/50] build file cleanup (#26) --- delivery-platform/app.sh | 2 +- delivery-platform/docs/workshop/1.2-provision.md | 13 ------------- .../docs/workshop/2.1-app-onboarding.md | 2 +- .../docs/workshop/3.2-release-progression.md | 4 ++-- .../resources/provision/provision-all.sh | 9 --------- .../golang/{ => build}/cloudbuild-build-only.yaml | 0 .../golang/{ => build}/cloudbuild-cd.yaml | 0 .../golang/{ => build}/cloudbuild.yaml | 0 delivery-platform/scripts/app.sh | 4 ++-- labs/app-onboarding/README.md | 4 ++-- 10 files changed, 8 insertions(+), 30 deletions(-) rename delivery-platform/resources/repos/app-templates/golang/{ => build}/cloudbuild-build-only.yaml (100%) rename delivery-platform/resources/repos/app-templates/golang/{ => build}/cloudbuild-cd.yaml (100%) rename delivery-platform/resources/repos/app-templates/golang/{ => build}/cloudbuild.yaml (100%) diff --git a/delivery-platform/app.sh b/delivery-platform/app.sh index eb77966..80315da 100755 --- a/delivery-platform/app.sh +++ b/delivery-platform/app.sh @@ -158,7 +158,7 @@ create_cloudbuild_trigger () { REPO_LOCATION=https://github.com/${GIT_USERNAME}/${APP_NAME} TRIGGER_NAME=${APP_NAME}-clouddeploy-webhook-trigger - BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/cloudbuild-build-only.yaml + BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/build/cloudbuild-build-only.yaml ## Setup Trigger & Webhook gcloud alpha builds triggers create webhook \ diff --git a/delivery-platform/docs/workshop/1.2-provision.md b/delivery-platform/docs/workshop/1.2-provision.md index 7716aea..9f8b313 100644 --- a/delivery-platform/docs/workshop/1.2-provision.md +++ b/delivery-platform/docs/workshop/1.2-provision.md @@ -152,19 +152,6 @@ Review the folders in the `resources/repos` directory Click **Next** to proceed. -## Platform Tools - -The final step of the platform provisioning process installs a series of tools you may use throughout the workshop or in your own customization. The scripts install Anthos Config Manager, ArgoCD, Tekton, and Gitea. This particular workshop only utilizes Anthos Config Manager. - -- Review acm-install.sh - -- Review tekton-install.sh - -- Review argo-install.sh - -- Review gt-setup.sh - -Click **Next** to proceed. ## Review the Cloud Build job for platform provisioning diff --git a/delivery-platform/docs/workshop/2.1-app-onboarding.md b/delivery-platform/docs/workshop/2.1-app-onboarding.md index e8d78ad..ebaece1 100644 --- a/delivery-platform/docs/workshop/2.1-app-onboarding.md +++ b/delivery-platform/docs/workshop/2.1-app-onboarding.md @@ -93,7 +93,7 @@ First, set some variables, including the name of the app to be created. ```bash export APP_NAME=hello-web -export TARGET_ENV=dev + ``` diff --git a/delivery-platform/docs/workshop/3.2-release-progression.md b/delivery-platform/docs/workshop/3.2-release-progression.md index 584d840..0909e66 100644 --- a/delivery-platform/docs/workshop/3.2-release-progression.md +++ b/delivery-platform/docs/workshop/3.2-release-progression.md @@ -83,7 +83,7 @@ Review the trigger setup for your application in Cloud Build with the link below The webhook endpoint triggers a Cloud Build job that executes a workflow defined in a `cloudbuild.yaml` file. The cloud build implementation includes steps to clone the repo, build and push an image, hydrate resources, and finally deploy the assets to the appropriate environment. Review the `cloudbuild.yaml` in your `hello-web` project - + Click here to open cloudbuild.yaml @@ -91,7 +91,7 @@ Click here to open cloudbuild.yaml The webhook endpoint triggers a Cloud Build job that executes a workflow defined in a `cloudbuild-cd.yaml` file. The cloud build implementation includes steps to clone the repo, build and push an image, and finally create pipelines for deployment in stage and prod. Review the `cloudbuild-cd.yaml` in your `hello-web` project - + Click here to open cloudbuild-cd.yaml diff --git a/delivery-platform/resources/provision/provision-all.sh b/delivery-platform/resources/provision/provision-all.sh index 5fee2fd..208590b 100755 --- a/delivery-platform/resources/provision/provision-all.sh +++ b/delivery-platform/resources/provision/provision-all.sh @@ -100,12 +100,3 @@ cd ${BASE_DIR}/resources/provision/management-tools/acm ./acm-install.sh cd $BASE_DIR -# Install Tekton -cd ${BASE_DIR}/resources/provision/management-tools -./tekton-install.sh -cd $BASE_DIR - -# Install Argo -cd ${BASE_DIR}/resources/provision/management-tools -./argo-install.sh -cd $BASE_DIR diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-build-only.yaml b/delivery-platform/resources/repos/app-templates/golang/build/cloudbuild-build-only.yaml similarity index 100% rename from delivery-platform/resources/repos/app-templates/golang/cloudbuild-build-only.yaml rename to delivery-platform/resources/repos/app-templates/golang/build/cloudbuild-build-only.yaml diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml b/delivery-platform/resources/repos/app-templates/golang/build/cloudbuild-cd.yaml similarity index 100% rename from delivery-platform/resources/repos/app-templates/golang/cloudbuild-cd.yaml rename to delivery-platform/resources/repos/app-templates/golang/build/cloudbuild-cd.yaml diff --git a/delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml b/delivery-platform/resources/repos/app-templates/golang/build/cloudbuild.yaml similarity index 100% rename from delivery-platform/resources/repos/app-templates/golang/cloudbuild.yaml rename to delivery-platform/resources/repos/app-templates/golang/build/cloudbuild.yaml diff --git a/delivery-platform/scripts/app.sh b/delivery-platform/scripts/app.sh index 3f36c77..44e3e65 100755 --- a/delivery-platform/scripts/app.sh +++ b/delivery-platform/scripts/app.sh @@ -229,7 +229,7 @@ create_cloudbuild_trigger () { REPO_LOCATION=https://github.com/${GIT_USERNAME}/${APP_NAME} TRIGGER_NAME=${APP_NAME}-webhook-trigger - BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/cloudbuild.yaml + BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/build/cloudbuild.yaml ## Setup Trigger & Webhook gcloud alpha builds triggers create webhook \ @@ -287,7 +287,7 @@ create_cloudbuild_trigger_for_clouddeploy () { REPO_LOCATION=https://github.com/${GIT_USERNAME}/${APP_NAME} TRIGGER_NAME=${APP_NAME}-clouddeploy-webhook-trigger - BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/cloudbuild-cd.yaml + BUILD_YAML_PATH=$WORK_DIR/app-templates/${APP_LANG}/build/cloudbuild-cd.yaml ## Setup Trigger & Webhook gcloud alpha builds triggers create webhook \ diff --git a/labs/app-onboarding/README.md b/labs/app-onboarding/README.md index 78c47e8..df476f5 100644 --- a/labs/app-onboarding/README.md +++ b/labs/app-onboarding/README.md @@ -232,7 +232,7 @@ The job requires a few key values to be provided on creation in order to properl ``` export TRIGGER_NAME=${APP_NAME}-clouddeploy-webhook-trigger -export BUILD_YAML_PATH=$WORK_DIR/app-templates/golang/cloudbuild-build-only.yaml +export BUILD_YAML_PATH=$WORK_DIR/app-templates/golang/build/cloudbuild-build-only.yaml ``` 2. Define the location of the shared base configuration repo. @@ -303,7 +303,7 @@ cd $BASE_DIR 2. Create a new application ``` -./app.sh create demo-app +./app.sh create demo-app golang ``` All of the steps are executed automatically. From 1fadb2f4d3abdfe90614ae5a4d8769017b5bddc3 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Wed, 13 Oct 2021 08:10:36 -0500 Subject: [PATCH 34/50] Cloud Deploy Tutorial (#27) --- labs/cloud-deploy/README.md | 326 +++++++++++++++++++++++++++++++++++- 1 file changed, 325 insertions(+), 1 deletion(-) diff --git a/labs/cloud-deploy/README.md b/labs/cloud-deploy/README.md index 5043735..fc25de6 100644 --- a/labs/cloud-deploy/README.md +++ b/labs/cloud-deploy/README.md @@ -1 +1,325 @@ -# app-template + +# Releasing with Cloud Deploy + +## Objectives + +In this tutorial you will create three GKE clusters named preview, canary and prod. Then, create a Cloud Deploy target corresponding to each cluster and a Cloud Deploy pipeline that will define the sequence of steps to perform deployment in those targets. + +The deployment flow will be triggered by a cloudbuild pipeline that will create Cloud Deploy release and perform the deployment in the preview cluster. After you have verified that the deployment in preview was successful and working as expected, you will manually promote the release in the canary cluster. Promotion of the release in the prod cluster will require approval, you will approve the prod pipeline in Cloud Deploy UI and finally promote it. + +The objectives of this tutorial can be broken down into the following steps: + +- Prepare your workspace +- Define Cloud Deploy targets +- Define Cloud Deploy pipeline +- Create a Release +- Promote a deployment +- Approve a production release + +## Before you begin + +For this reference guide, you need a Google Cloud [project](https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#projects). You can create a new one, or select a project you already created: + +1. Select or create a Google Cloud project. + +[GO TO THE PROJECT SELECTOR PAGE](https://console.corp.google.com/projectselector2/home/dashboard) + +1. Enable billing for your project. + +[ENABLE BILLING](https://support.google.com/cloud/answer/6293499#enable-billing) + +## + +## Platform Setup + +### Preparing your workspace + +We will set up our environment here required to run this tutorial. When this step is completed, we will have a GKE cluster created where we can run the deployments. + +1. **Set gcloud config defaults** + +``` +gcloud config set project + +gcloud config set deploy/region us-central1 +``` + +2. **Clone Repo** + +``` +git clone https://github.com/gushob21/software-delivery-workshop +cd software-delivery-workshop/labs/cloud-deploy/ +cloudshell workspace . +rm -rf deploy && mkdir deploy +``` + +3. **Set environment variables** + +``` +export PROJECT_ID=$(gcloud config get-value project) +export PROJECT_NUMBER=$(gcloud projects list --filter="$PROJECT_ID" --format="value(PROJECT_NUMBER)") +``` + +4. **Enable APIs** + +``` +gcloud services enable \ +cloudresourcemanager.googleapis.com \ + container.googleapis.com \ + cloudbuild.googleapis.com \ + containerregistry.googleapis.com \ + secretmanager.googleapis.com \ + clouddeploy.googleapis.com +``` + +5. **Create GKE clusters** + +``` + gcloud container clusters create preview \ +--zone=us-central1-a --async + gcloud container clusters create canary \ +--zone=us-central1-b --async + gcloud container clusters create prod \ +--zone=us-central1-c +``` + +### Defining Cloud Deploy Targets + +1. **Create a file in the deploy directory named preview.yaml with the following command in cloudshell:** + +``` +cat <deploy/preview.yaml +apiVersion: deploy.cloud.google.com/v1beta1 +kind: Target +metadata: + name: preview + annotations: {} + labels: {} +description: Target for preview environment +gke: + cluster: projects/$PROJECT_ID/locations/us-central1-a/clusters/preview +EOF +``` + + As you noticed, the "kind" tag is "Target". It allows us to add some metadata to the target, a description and finally the GKE cluster where the deployment is supposed to happen for this target. + +2. **Create a file in the deploy directory named canary.yaml with the following command in cloudshell:** + +``` +cat <deploy/canary.yaml +apiVersion: deploy.cloud.google.com/v1beta1 +kind: Target +metadata: + name: canary + annotations: {} + labels: {} +description: Target for canary environment +gke: + cluster: projects/$PROJECT_ID/locations/us-central1-b/clusters/canary +EOF +``` + +3. **Create a file in the deploy directory named prod.yaml with the following command in cloudshell:** + +``` +cat <deploy/prod.yaml +apiVersion: deploy.cloud.google.com/v1beta1 +kind: Target +metadata: + name: prod + annotations: {} + labels: {} +description: Target for prod environment +requireApproval: true +gke: + cluster: projects/$PROJECT_ID/locations/us-central1-c/clusters/prod +EOF +``` + +Notice the tag requireApproval which is set to true. This will not allow promotion into prod target until the approval is granted. You require `roles/clouddeploy.approver `role to approve a release. + +4. **Create the Deploy Targets** + +``` + gcloud config set deploy/region us-central1 +gcloud beta deploy apply --file deploy/preview.yaml +gcloud beta deploy apply --file deploy/canary.yaml +gcloud beta deploy apply --file deploy/prod.yaml +``` + +## App Creation + +As part of the creation of a new application the CICD pipeline is typically setup to perform automatic builds, integration testing and deployments. The following steps are considered part of the setup process for a new app. Each new application will have a deployment pipeline configured. + +### Defining Cloud Deploy pipeline + +1. **Create a file in the deploy directory named pipeline.yaml with the following command in cloudshell:** + +``` +cat <>deploy/pipeline.yaml +apiVersion: deploy.cloud.google.com/v1beta1 +kind: DeliveryPipeline +metadata: + name: sample-app + labels: + app: sample-app +description: delivery pipeline +serialPipeline: + stages: + - targetId: preview + profiles: + - preview + - targetId: canary + profiles: + - canary + - targetId: prod + profiles: + - prod +EOF +``` + +** ** +** **As you noticed, the "kind" tag is "DeliveryPipeline". It lets you define the metadata for the pipeline, a description and an order of deployment into various targets via serialPipeline tag. + +`serialPipeline` tag contains a tag named stages which is a list of all targets to which this delivery pipeline is configured to deploy. + +`targetId` identifies the specific target to use for this stage of the delivery pipeline. The value is the metadata.name property from the target definition. + +`profiles` is a list of zero or more Skaffold profile names, from skaffold.yaml. Cloud Deploy uses the profile with skaffold render when creating the release. + +2. **Apply Pipeline** + +``` +gcloud beta deploy apply --file deploy/pipeline.yaml +``` + +## Development Phase + +As the applications are developed automated CICD toolchains will build and store assets. The following commands are executed to build the application using skaffold and store assets for deployment with Cloud Deploy. This step would be performed by your CICD process for every application build. + +1. **Build and store the application with skaffold** + +``` +skaffold build \ +--file-output=artifacts.json \ +--default-repo gcr.io/$PROJECT_ID \ +--push=true +``` + +## Release Phase + +At the end of your CICD process, typically when the code is Tagged for production, you will initiate the release process by calling the `cloud deploy release` command. Later once the deployment has been validated and approved you'll move the release through the various target environments by promoting and approving the action through automated processes or manual approvals. + + + +### Creating a release + +We created Cloud Deploy files in this tutorial earlier to get an understanding of how Cloud Deploy works. For the purpose of demo, we have created the same Cloud Deploy files and pushed them to a github repo with a sample go application and we will use Cloud Deploy to do the release of that application. + +``` +export REL_TIMESTAMP=$(date '+%Y%m%d-%H%M%S') + +gcloud beta deploy releases create \ +sample-app-release-${REL_TIMESTAMP} \ +--delivery-pipeline=sample-app \ +--description="Release demo" \ +--build-artifacts=artifacts.json \ +--annotations="release-id=rel-${REL_TIMESTAMP}" +``` + + +### Review the release + +When a Cloud Deploy release is created, it automatically rolls it out in the first target which is preview. + +1. Go to [ in Google cloud console](https://console.cloud.google.com/deploy) +2. Click on "sample-app" + + On this screen you will see a graphic representation of your pipeline. + +3. Confirm a green outline on the left side of the preview box which means that the release has been deployed to that environment. +4. Optionally review additional details about the release by clicking on the release name under Release Details in the lower section of the screen + +5. Verify that the release successfully deployed the application, run the following command it cloushell + +``` +gcloud container clusters get-credentials preview --zone us-central1-a && kubectl port-forward --namespace default $(kubectl get pod --namespace default --selector="app=cloud-deploy-tutorial" --output jsonpath='{.items[0].metadata.name}') 8080:8080 +``` + +6. Click on the web preview icon in the upper right of the screen. +7. Select Preview on port 8080 + +This will take you to a new page which shows the message "Hello World!" + +8. Use `ctrl+c` in the terminal to end the port-forward. + +### Promoting a release + +Now that your release is deployed to the first target (preview) in the pipeline, you can promote it to the next target (canary). Run the following command to begin the process. + +``` +gcloud beta deploy releases promote \ +--release=sample-app-release-${REL_TIMESTAMP} \ +--delivery-pipeline=sample-app \ +--quiet +``` + +### Review the release promotion + +1. Go to the [sample-app pipeline in the Google Cloud console](https://console.cloud.google.com/deploy/delivery-pipelines/us-central1/sample-app) +2. Confirm a green outline on the left side of the Canary box which means that the release has been deployed to that environment. + +3. Verify the application is deployed correctly by creating a tunnel to it + +``` +gcloud container clusters get-credentials canary --zone us-central1-b && kubectl port-forward --namespace default $(kubectl get pod --namespace default --selector="app=cloud-deploy-tutorial" --output jsonpath='{.items[0].metadata.name}') 8080:8080 +``` + +4. Click on the web preview icon in the upper right of the screen. +5. Select Preview on port 8080 + +This will take you to a new page which shows the message "Hello World!" + +6. Use `ctrl+c` in the terminal to end the port-forward. + +### Approving a production release + +Remember when we created prod target via prod.yaml, we specified the tag requireApproval as true. This will force a requirement of approval for promotion in prod. + +1. Promote the canary release to production with the following command + +``` +gcloud beta deploy releases promote \ +--release=sample-app-release-${REL_TIMESTAMP} \ +--delivery-pipeline=sample-app \ +--quiet +``` + +2. Go to the [sample-app pipeline in the Google Cloud console](https://console.cloud.google.com/deploy/delivery-pipelines/us-central1/sample-app) + +3. Notice the yellow indicator noting "1 pending". + + This message indicates there is a release queued for deployment to production but requires review and approval. + +4. Click on the "Review" button just below the yellow notice. +5. In the next screen click "Review" again to access the approval screen for production +6. Optionally review the Manifest Diff to review the changes. In this case a whole new file. +7. Click on the "Approve" button +8. Return to the [sample-app pipeline page](https://console.cloud.google.com/deploy/delivery-pipelines/us-central1/sample-app) where you will see the release to prod in progress. + +### Review the production release + +As with the other environments you can review the deployment when it completes using the steps below. + +1. Run the following command it cloudshell to create the port-forward + +``` +gcloud container clusters get-credentials prod --zone us-central1-c && kubectl port-forward --namespace default $(kubectl get pod --namespace default --selector="app=cloud-deploy-tutorial" --output jsonpath='{.items[0].metadata.name}') 8080:8080 +``` + +2. Click on the web preview icon in the upper right of the screen. +3. Select Preview on port 8080 + +This will take you to a new page which shows the message "Hello World!" + +4. Use `ctrl+c` in the terminal to end the port-forward. \ No newline at end of file From 775bd67a68fe0b980c16741254cad9a0516ab9f9 Mon Sep 17 00:00:00 2001 From: Henry Bell Date: Wed, 13 Oct 2021 14:11:59 +0100 Subject: [PATCH 35/50] Initial commit (#25) * Initial commit * Add Apache license header --- labs/understanding-skaffold/README.md | 370 ++++++++++++++++++ .../getting-started/.gitignore | 2 + .../getting-started/app/Dockerfile | 26 ++ .../getting-started/app/main.go | 28 ++ .../getting-started/base/deployment.yaml | 38 ++ .../getting-started/base/kustomization.yaml | 19 + .../getting-started/namespaces.yaml | 29 ++ .../overlays/dev/deployment.yaml | 20 + .../overlays/dev/kustomization.yaml | 25 ++ .../overlays/prod/deployment.yaml | 22 ++ .../overlays/prod/kustomization.yaml | 25 ++ .../overlays/staging/deployment.yaml | 22 ++ .../overlays/staging/kustomization.yaml | 25 ++ 13 files changed, 651 insertions(+) create mode 100644 labs/understanding-skaffold/README.md create mode 100644 labs/understanding-skaffold/getting-started/.gitignore create mode 100644 labs/understanding-skaffold/getting-started/app/Dockerfile create mode 100644 labs/understanding-skaffold/getting-started/app/main.go create mode 100644 labs/understanding-skaffold/getting-started/base/deployment.yaml create mode 100644 labs/understanding-skaffold/getting-started/base/kustomization.yaml create mode 100644 labs/understanding-skaffold/getting-started/namespaces.yaml create mode 100644 labs/understanding-skaffold/getting-started/overlays/dev/deployment.yaml create mode 100644 labs/understanding-skaffold/getting-started/overlays/dev/kustomization.yaml create mode 100644 labs/understanding-skaffold/getting-started/overlays/prod/deployment.yaml create mode 100644 labs/understanding-skaffold/getting-started/overlays/prod/kustomization.yaml create mode 100644 labs/understanding-skaffold/getting-started/overlays/staging/deployment.yaml create mode 100644 labs/understanding-skaffold/getting-started/overlays/staging/kustomization.yaml diff --git a/labs/understanding-skaffold/README.md b/labs/understanding-skaffold/README.md new file mode 100644 index 0000000..994e21e --- /dev/null +++ b/labs/understanding-skaffold/README.md @@ -0,0 +1,370 @@ +# Understanding Skaffold + +[Skaffold](https://skaffold.dev/) is a tool that handles the workflow for +building, pushing and deploying your application. You can use Skaffold to +easily configure a local development workspace, streamline your inner +development loop, and integrate with other tools such as +[Kustomize](kustomize.dev) and [Helm](https://helm.sh/) to help manage your +Kubernetes manifests. + +## Objectives + +In this tutorial you work through some of the core concepts of Skaffold, use it +to automate your inner development loop, then deploy an application. + +You will: + +- Configure and enable Skaffold for local development +- Build and run a simple golang application +- Manage local application deployment with Skaffold +- Render manifests and deploy your application + +## Preparing your workspace + +1. Open the Cloud Shell editor by visiting the following url: + +``` +https://shell.cloud.google.com +``` + +2. If you have not done so already, in the terminal window clone the application source with the following command: + +``` +git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop.git +``` + +3. Change into the cloned repository directory: + +``` +cd software-delivery-workshop/labs/understanding-skaffold/getting-started +``` + +4. Set your Cloud Shell workspace to the current directory by running the following command: + +``` +cloudshell workspace . +``` + +## Preparing your project + +1. Ensure your Google Cloud project is set correctly by running the following command: + +``` +gcloud config set project {{project-id}} +``` + +## Getting started with Skaffold + +1. Run the following command to create the top-level Skaffold configuration file, `skaffold.yaml`: + +``` +cat < skaffold.yaml +apiVersion: skaffold/v2beta21 +kind: Config +metadata: + name: getting-started-kustomize +build: + tagPolicy: + gitCommit: + ignoreChanges: true + artifacts: + - image: skaffold-kustomize + context: app + docker: + dockerfile: Dockerfile +deploy: + kustomize: + paths: + - overlays/dev +profiles: +- name: staging + deploy: + kustomize: + paths: + - overlays/staging +- name: prod + deploy: + kustomize: + paths: + - overlays/prod +EOF +``` + +2. Open the file `skaffold.yaml` in the IDE pane. This is the top-level configuration file that defines the tSkaffold pipeline. + +Notice the Kubernetes-like YAML format and the following sections in the YAML: + + - `build` + - `deploy` + - `profiles` + +These sections define how the application should be built and deployed, as well as profiles for each deployment target. + +You can read more about the full list of Skaffold stages in the Skaffold Pipeline Stages [documentation](https://skaffold.dev/docs/pipeline-stages). + +# Build + +The `build` section contains configuration that defines how the application +should be built. In this case you can see configuration for how `git` tags +should be handled, as well as an `artifacts` section that defines the container +images, that comprise the application. + +As well as this, in this section you can see the reference to the `Dockerfile` +to be used to build the images. Skaffold additionally supports other build +tools such as `Jib`, `Maven`, `Gradle`, Cloud-native `Buildpacks`, `Bazel` and +custom scripts. You can read more about this configuration in the [Skaffold +Build documentation](). + +# Deploy + +The `deploy` section contains configuration that defines how the application +should be deployed. In this case you can see an example for a default +deployment that configures Skaffold to use the the +[`Kustomize`](https://kustomize.io/) tool. + +The `Kustomize` tool provides functionality for generating Kubernetes manifests +by combining a set of common component YAML files (under the `base` directory) +with a one or more "overlays" that typically correspond to one or more +deployment targets -- typically *dev*, *test*, *staging* and *production* or +similar. + +In this example you can see two overlays for three targets, *dev*, *staging* +and *prod*. The *dev* overlay will be used during local development and the +*staging* and *prod* overlays for when deploying using Skaffold. + +# Profiles + +The `profiles` section contains configuration that defines build, test and +deployment configurations for different contexts. Different contexts are +typically different environments in your application deployment pipeline, like +`staging` or `prod` in this example. This means that you can easily manage +manifests whose contents need to differ for different target environments, +without repeating boilerplate configuration. + +Configuration in the `profiles` sectionm can replace or patch any items from +the main configuration (i.e. the `build`, `test` or `deploy` sections, for +example). + +As an example of this, open the file `overlays > prod > deployment.yaml`. +Notice that the number of replicas for the application is configured here to be +three, overriding the base configuration. + +# Navigating the application source code. + +1. Open the following file `app > main.go` in the IDE pane. This is a simple + golang application that writes a string to `stdout` every second. + +2. Notice that the application also outputs the name of the Kubernetes pod in which it it running. + +# Viewing the Dockerfile + +1. Open the file `app > Dockerfile` in the IDE pane. This file contains a + sequence of directives to build the application container image for the + `main.go` file, and is referenced in the top-level `skaffold.yaml` file. + +## Configuring your Kubernetes environment + +1. Run the following command to ensure your local Kubernetes cluster is running and configured: + +``` +minikube start +``` + +The may take several minutes. You should see the following output if the cluster has started successfully: +``` +Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default +``` + +2. Run the following command to create Kubernetes namespaces for `dev`, `staging` and `prod`: + +``` +kubectl apply -f namespaces.yaml +``` + +You should see the following output: + +``` +namespace/dev created +namespace/staging created +namespace/prod created +``` + +## Using Skaffold for local development + +1. Run the following command to build the application and deploy it to a local Kubernetes cluster running in Cloud Shell: + +``` +skaffold dev +``` + +You should see the application container build process run, which may take a +minute, and then the application output repeating every second: + +``` +[skaffold-kustomize] Hello world from pod skaffold-kustomize-dev-xxxxxxxxx-xxxxx +``` + +Note that the exact pod name will vary from the generic output given above. + +## Making changes to the application + +Now that the application is running in your local Kubernetes cluster, you can +make changes to the code, and Skaffold will automatically rebuild and redeploy +the application to the cluster. + +1. Open the file `app > main.go` in the IDE pane, and change the output string: + +``` +"Hello world from pod %s!\n" +``` + + to: + +``` +"Hello Skaffold world from pod %s!\n" +``` + +When you have made the change you should see Skaffold rebuild the image and redeploy it to the cluster, with the change in output visibile in the terminal window. + +2. Now, also in the file "app > main.go" in the IDE pane, change the line: + +``` +time.Sleep(time.Second * 1) +``` + + to + +``` +time.Sleep(time.Second * 10) +``` + +Again you should see the application rebuilt and redeployed, with the output line appearing once every 10 seconds. + +## Making changes to the Kubernetes config + +Next you will make a change to the Kubernetes config, and once more Skaffold will automatically redeploy. + +1. Open the file `base > deployment.yaml` in the IDE and change the line: + +``` +replicas: 1 +``` + +to + +``` +replicas: 2 +``` + +Once the application has been redeployed, you should see two pods running -- each will have a different name. + +2. Now, change the same line in the file `base > deployment.yaml` back to: + +``` +replicas: 1 +``` + +You should see one of the pods removed from service so that only one is remaining. + +3. Finally, press `Ctrl-C` in the terminal window to stop Skaffold local development. + +## Cutting a release + +Next, you will create a release by building a release image, and deploying it to a cluster. + +1. Run the following command to build the release: + +``` +skaffold build --file-output artifacts.json +``` + +This command will build the final image (if necessary) and output the release details to the `artifacts.json` file. + +If you wanted to use a tool like Cloud Deploy to deploy to your clusters, this +file contains the release information. This means that the artifact(s) are +immutable on the route to live. + +2. Run the following command to view the contents of the `artifacts.json` file: + +``` +cat artifacts.json | jq +``` + +Notice that the file contains the reference to the image that will be used in the final deployment. + +## Deploying to staging + +1. Run the following command to deploy the release using the `staging` profile: + +``` +skaffold deploy --profile staging --build-artifacts artifacts.json --tail +``` + +Once deployment is complete you should see output from three pods similar to the following: + +``` +[skaffold-kustomize] Hello world from pod skaffold-kustomize-staging-xxxxxxxxxx-xxxxx! +``` + +2. Press Ctrl-C in the terminal window to stop Skaffold output. + +3. Run the following command to observe your application up and running in the cluster: + +``` +kubectl get all --namespace staging +``` + +You should see two distinct pod names, because the `staging` profile for the application specifies there should be two replicas in the deployment. + +## Deploying to production + +1. Now run the following command to deploy the release using the `prod` profile: + +``` +skaffold deploy --profile prod --build-artifacts artifacts.json --tail +``` + +Once deployment is complete you should see output from three pods similar to the following: + +``` +[skaffold-kustomize] Hello world from pod skaffold-kustomize-prod-xxxxxxxxxx-xxxxx! +``` + +2. Press Ctrl-C in the terminal window to stop Skaffold output. + +You should see three distinct pod names, because the `prod` profile for the application specifies there should be three replicas in the deployment. + +3. Run the following command to observe your application up and running in the cluster: + +``` +kubectl get all --namespace prod +``` + +You should see output that contains lines similar to the following that show the prod deployment: + +``` +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/skaffold-kustomize-prod 3/3 3 3 16m +``` + +You should also see three application pods running. + +``` +NAME READY STATUS RESTARTS AGE +pod/skaffold-kustomize-prod-xxxxxxxxxx-xxxxx 1/1 Running 0 10m +pod/skaffold-kustomize-prod-xxxxxxxxxx-xxxxx 1/1 Running 0 10m +pod/skaffold-kustomize-prod-xxxxxxxxxx-xxxxx 1/1 Running 0 10m +``` + +## Cleaning up + +1. Run the following command to shut down the local cluster: + +``` +minikube delete +``` + +## Finishing up + +Congratulations! You have completed the `Understanding Skaffold` lab and have learned how to configure and use Skaffold for local development and application deployment. + diff --git a/labs/understanding-skaffold/getting-started/.gitignore b/labs/understanding-skaffold/getting-started/.gitignore new file mode 100644 index 0000000..e92a2c6 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/.gitignore @@ -0,0 +1,2 @@ +skaffold.yaml +artifacts.json diff --git a/labs/understanding-skaffold/getting-started/app/Dockerfile b/labs/understanding-skaffold/getting-started/app/Dockerfile new file mode 100644 index 0000000..fd31c95 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/app/Dockerfile @@ -0,0 +1,26 @@ +# Copyright 2021 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. + +FROM golang:1.15 as builder +COPY main.go . +# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations +ARG SKAFFOLD_GO_GCFLAGS +RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /app main.go + +FROM alpine:3.10 +# Define GOTRACEBACK to mark this container as using the Go language runtime +# for `skaffold debug` (https://skaffold.dev/docs/workflows/debug/). +ENV GOTRACEBACK=single +CMD ["./app"] +COPY --from=builder /app . diff --git a/labs/understanding-skaffold/getting-started/app/main.go b/labs/understanding-skaffold/getting-started/app/main.go new file mode 100644 index 0000000..205d305 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/app/main.go @@ -0,0 +1,28 @@ +// Copyright 2021 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. + +package main + +import ( + "fmt" + "os" + "time" +) + +func main() { + for { + fmt.Printf("Hello world from pod %s!\n", os.Getenv("POD_NAME")) + time.Sleep(time.Second * 1) + } +} diff --git a/labs/understanding-skaffold/getting-started/base/deployment.yaml b/labs/understanding-skaffold/getting-started/base/deployment.yaml new file mode 100644 index 0000000..3f69808 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/base/deployment.yaml @@ -0,0 +1,38 @@ +# Copyright 2021 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skaffold-kustomize + labels: + app: skaffold-kustomize +spec: + replicas: 1 + selector: + matchLabels: + app: skaffold-kustomize + template: + metadata: + labels: + app: skaffold-kustomize + spec: + containers: + - name: skaffold-kustomize + image: skaffold-kustomize + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name diff --git a/labs/understanding-skaffold/getting-started/base/kustomization.yaml b/labs/understanding-skaffold/getting-started/base/kustomization.yaml new file mode 100644 index 0000000..13240d8 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/base/kustomization.yaml @@ -0,0 +1,19 @@ +# Copyright 2021 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. + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml diff --git a/labs/understanding-skaffold/getting-started/namespaces.yaml b/labs/understanding-skaffold/getting-started/namespaces.yaml new file mode 100644 index 0000000..0fec0c4 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/namespaces.yaml @@ -0,0 +1,29 @@ +# Copyright 2021 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. + +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Namespace + metadata: + name: dev +- apiVersion: v1 + kind: Namespace + metadata: + name: staging +- apiVersion: v1 + kind: Namespace + metadata: + name: prod diff --git a/labs/understanding-skaffold/getting-started/overlays/dev/deployment.yaml b/labs/understanding-skaffold/getting-started/overlays/dev/deployment.yaml new file mode 100644 index 0000000..476da37 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/overlays/dev/deployment.yaml @@ -0,0 +1,20 @@ +# Copyright 2021 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skaffold-kustomize + labels: + env: dev diff --git a/labs/understanding-skaffold/getting-started/overlays/dev/kustomization.yaml b/labs/understanding-skaffold/getting-started/overlays/dev/kustomization.yaml new file mode 100644 index 0000000..1cd4a92 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/overlays/dev/kustomization.yaml @@ -0,0 +1,25 @@ +# Copyright 2021 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. + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: dev +nameSuffix: -dev + +patchesStrategicMerge: +- deployment.yaml + +resources: +- ../../base diff --git a/labs/understanding-skaffold/getting-started/overlays/prod/deployment.yaml b/labs/understanding-skaffold/getting-started/overlays/prod/deployment.yaml new file mode 100644 index 0000000..2711ad8 --- /dev/null +++ b/labs/understanding-skaffold/getting-started/overlays/prod/deployment.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skaffold-kustomize + labels: + env: prod +spec: + replicas: 3 diff --git a/labs/understanding-skaffold/getting-started/overlays/prod/kustomization.yaml b/labs/understanding-skaffold/getting-started/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..8b22d3e --- /dev/null +++ b/labs/understanding-skaffold/getting-started/overlays/prod/kustomization.yaml @@ -0,0 +1,25 @@ +# Copyright 2021 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. + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: prod +nameSuffix: -prod + +patchesStrategicMerge: +- deployment.yaml + +resources: +- ../../base diff --git a/labs/understanding-skaffold/getting-started/overlays/staging/deployment.yaml b/labs/understanding-skaffold/getting-started/overlays/staging/deployment.yaml new file mode 100644 index 0000000..8b4420d --- /dev/null +++ b/labs/understanding-skaffold/getting-started/overlays/staging/deployment.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skaffold-kustomize + labels: + env: staging +spec: + replicas: 2 diff --git a/labs/understanding-skaffold/getting-started/overlays/staging/kustomization.yaml b/labs/understanding-skaffold/getting-started/overlays/staging/kustomization.yaml new file mode 100644 index 0000000..28001ac --- /dev/null +++ b/labs/understanding-skaffold/getting-started/overlays/staging/kustomization.yaml @@ -0,0 +1,25 @@ +# Copyright 2021 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. + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: staging +nameSuffix: -staging + +patchesStrategicMerge: +- deployment.yaml + +resources: +- ../../base From b354136a27f578d3d7bfe9b1a20870ef73198e2a Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Thu, 14 Oct 2021 08:51:26 -0500 Subject: [PATCH 36/50] Cloudrun lab (#29) * Cloud Run tutorial --- labs/cloudrun-progression/README.md | 405 ++++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 labs/cloudrun-progression/README.md diff --git a/labs/cloudrun-progression/README.md b/labs/cloudrun-progression/README.md new file mode 100644 index 0000000..e7fab68 --- /dev/null +++ b/labs/cloudrun-progression/README.md @@ -0,0 +1,405 @@ +# Canary Deployments with Cloud Run and CLoud Build + +This document shows you how to implement a deployment pipeline for +Cloud Run that implements progression of code from developer +branches to production with automated canary testing and percentage-based +traffic management. It is intended for platform administrators who are +responsible for creating and managing CI/CD pipelines to +GKE. This document assumes that you have a basic +understanding of Git, Cloud Run, and CI/CD pipeline concepts. + +Cloud Run lets you deploy and run your applications with little +overhead or effort. Many organizations use robust release pipelines to move code +into production. Cloud Run provides unique traffic management +capabilities that let you implement advanced release management techniques with +little effort. + +- Create your Cloud Run service +- Enable a developer branch +- Implement canary testing +- Roll out safely to production + + +## Preparing your environment + +1. In Cloud Shell, create environment variables to use throughout + this tutorial: + + ```sh + export PROJECT_ID=$(gcloud config get-value project) + export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') + ``` +2. Enable the following APIs: + + - Resource Manager + - GKE + - Cloud Source Repositories + - Cloud Build + - Container Registry + - Cloud Run + + ```sh + gcloud services enable \ + cloudresourcemanager.googleapis.com \ + container.googleapis.com \ + sourcerepo.googleapis.com \ + cloudbuild.googleapis.com \ + containerregistry.googleapis.com \ + run.googleapis.com + ``` +3. Grant the Cloud Run Admin role (`roles/run.admin`) to + the Cloud Build service account: + + ```sh + gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ + --role=roles/run.admin + ``` +4. Grant the IAM Service Account User role + (`roles/iam.serviceAccountUser`) to the Cloud Build service + account for the Cloud Run runtime service account: + + ```sh + gcloud iam service-accounts add-iam-policy-binding \ + $PROJECT_NUMBER-compute@developer.gserviceaccount.com \ + --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ + --role=roles/iam.serviceAccountUser + ``` +5. If you haven't used Git in Cloud Shell previously, set the + `user.name` and `user.email` values that you want to use: + + ```sh + git config --global user.email "YOUR_EMAIL_ADDRESS" + git config --global user.name "YOUR_USERNAME" + ``` +6. Clone and prepare the sample repository: + + ```sh + git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop cloudrun-progression + + cd cloudrun-progression/labs/cloudrun-progression + rm -rf ../../.git + ``` +7. Replace placeholder values in the sample repository with your `PROJECT_ID`: + + ```sh + sed "s/PROJECT/${PROJECT_ID}/g" branch-trigger.json-tmpl > branch-trigger.json + sed "s/PROJECT/${PROJECT_ID}/g" master-trigger.json-tmpl > master-trigger.json + sed "s/PROJECT/${PROJECT_ID}/g" tag-trigger.json-tmpl > tag-trigger.json + ``` +8. Store the code from the sample repository in CSR: + + ```sh + gcloud source repos create cloudrun-progression + git init + git config credential.helper gcloud.sh + git remote add gcp https://source.developers.google.com/p/$PROJECT_ID/r/cloudrun-progression + git branch -m master + git add . && git commit -m "initial commit" + git push gcp master + ``` + +## Creating your Cloud Run service + +In this section, you build and deploy the initial production application that +you use throughout this tutorial. + +1. In Cloud Shell, build and deploy the application, including a + service that requires authentication. To make a public service, use the + `--allow-unauthenticated` flag as described in + [Allowing public (unauthenticated) access](/run/docs/authenticating/public). + + ```sh + gcloud builds submit --tag gcr.io/$PROJECT_ID/hello-cloudrun + gcloud run deploy hello-cloudrun \ + --image gcr.io/$PROJECT_ID/hello-cloudrun \ + --platform managed \ + --region us-central1 \ + --tag=prod -q + ``` + + The output looks like the following: + + ```none + Deploying container to Cloud Run service [hello-cloudrun] in project [sdw-mvp6] region [us-central1] + ✓ Deploying new service... Done. + ✓ Creating Revision... + ✓ Routing traffic... + Done. + Service [hello-cloudrun] revision [hello-cloudrun-00001-tar] has been deployed and is serving 100 percent of traffic. + Service URL: https://hello-cloudrun-apwaaxltma-uc.a.run.app + The revision can be reached directly at https://prod---hello-cloudrun-apwaaxltma-uc.a.run.app + ``` + + The output includes the service URL and a unique URL for the revision. Your + values will differ slightly from what's indicated here. +2. After the deployment is complete, view the newly deployed service on + the + [Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) + in the Console. + + [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) + +3. In Cloud Shell, view the authenticated service response: + + ```sh + PROD_URL=$(gcloud run services describe hello-cloudrun \ + --platform managed \ + --region us-central1 \ + --format=json | jq \ + --raw-output ".status.url") + echo $PROD_URL + curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $PROD_URL + ``` + +## Enabling dynamic developer deployments + +In this section, you enable a unique URL for development branches in Git. Each +branch is represented by a URL that's identified by the branch name. Commits to +the branch trigger a deployment, and the updates are accessible at that same +URL. + +1. In Cloud Shell, set up the trigger: + + ```sh + gcloud beta builds triggers create cloud-source-repositories \ + --trigger-config branch-trigger.json + ``` +2. To review the trigger, go to the + [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) + in the Console. + + [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) + +3. In Cloud Shell, create a new branch: + + ```sh + git checkout -b new-feature-1 + ``` +4. Open the sample application in the Cloud Shell: + + ```sh + edit app.py + ``` +5. In the sample application, modify the code to indicate v1.1 instead of v1.0: + + ```py + @app.route('/') + def hello_world(): + return 'Hello World v1.1' + ``` +6. To return to your terminal, click **Open Terminal**. +7. In Cloud Shell, commit the change and push to the remote + repository: + + ```sh + git add . && git commit -m "updated" && git push gcp new-feature-1 + ``` +8. To review the build in progress, go to the + [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) + in the Console. + + [Go to Builds](https://console.cloud.google.com/cloud-build/builds) + +9. After the build completes, to review the new revision, go to the + [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) + in the Console. + + [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) + +10. In Cloud Shell, get the unique URL for this branch: + + ```sh + BRANCH_URL=$(gcloud run services describe hello-cloudrun \ + --platform managed \ + --region us-central1 \ + --format=json | jq \ + --raw-output ".status.traffic[] | select (.tag==\"new-feature-1\")|.url") + echo $BRANCH_URL + ``` + +11. Access the authenticated URL: + + ```sh + curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $BRANCH_URL + ``` + The updated response output looks like the following: + + ```none + Hello World v1.1 + ``` + +## Automating canary testing + +When code is released to production, it's common to release code to a small +subset of live traffic before migrating all traffic to the new code base. + +In this section, you implement a trigger that is activated when code is +committed to the main branch. The trigger deploys the code to a unique canary +URL and it routes 10% of all live traffic to the new revision. + +1. In Cloud Shell, set up the branch trigger: + + ```sh + gcloud beta builds triggers create cloud-source-repositories \ + --trigger-config master-trigger.json + ``` +2. To review the new trigger, go to the + [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) + in the Console. + + [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) + +3. In Cloud Shell, merge the branch to the main line and push to + the remote repository: + + ```sh + git checkout master + git merge new-feature-1 + git push gcp master + ``` +4. To review the build in progress, go to the + [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) + in the Console. + + [Go to Builds](https://console.cloud.google.com/cloud-build/builds) + +5. After the build is complete, to review the new revision, go to the + [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) + in the Console. + + [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) + + As the following screenshot shows, 90% of the traffic is routed to `prod`, + 10% to `canary`, and 0% to the branch revisions. + + ![Traffic for the canary deployment in the Revisions page.](images/implementing-cloud-run-canary-deployments-git-branches-cloud-build-canary-revisions.png) +6. Review the lines of `master-cloudbuild.yaml` that implement the logic for + the canary deployment. + + The following lines deploy the new revision and use the `tag` flag to route + traffic from the unique canary URL: + + ```sh + gcloud run deploy ${_SERVICE_NAME} \ + --platform managed \ + --region ${_REGION} \ + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ + --tag=canary \ + --no-traffic + ``` + The following line adds a static tag to the revision that notes the Git + short SHA of the deployment: + + ```sh + gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=sha-$SHORT_SHA=$${CANARY} --platform managed --region ${_REGION} + ``` + The following line updates the traffic to route 90% to production and 10% to + canary: + + ```sh + gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=$${PROD}=90,$${CANARY}=10 --platform managed --region ${_REGION} + ``` +7. In Cloud Shell, get the unique URL for the canary revision: + + ```sh + CANARY_URL=$(gcloud run services describe hello-cloudrun \ + --platform managed \ + --region us-central1 \ + --format=json | jq \ + --raw-output ".status.traffic[] | select (.tag==\"canary\")|.url") + echo $CANARY_URL + ``` +8. Review the canary endpoint directly: + + ```sh + curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $CANARY_URL + ``` +9. To see percentage-based responses, make a series of requests: + + ```sh + LIVE_URL=$(gcloud run services describe hello-cloudrun \ + --platform managed \ + --region us-central1 \ + --format=json | jq \ + --raw-output ".status.url") + + for i in {0..20};do + curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $LIVE_URL; echo \n + done + ``` + +## Releasing to production + +After the canary deployment is validated with a small subset of traffic, you +release the deployment to the remainder of the live traffic. + +In this section, you set up a trigger that is activated when you create a tag +in the repository. The trigger migrates 100% of traffic to the already deployed +revision based on the commit SHA of the tag. Using the commit SHA ensures the +revision validated with canary traffic is the revision used for the remainder of +production traffic. + +1. In Cloud Shell, set up the tag trigger: + + ```sh + gcloud beta builds triggers create cloud-source-repositories \ + --trigger-config tag-trigger.json + ``` +2. To review the new trigger, go to the + [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) + in the Console. + + [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) + +3. In Cloud Shell, create a new tag and push to the remote repository: + + ```sh + git tag 1.1 + git push gcp 1.1 + ``` +4. To review the build in progress, go to the + [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) + in the Console. + + [Go to Builds](https://console.cloud.google.com/cloud-build/builds) + +5. After the build is complete, to review the new revision, go to the + [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) + in the Console. + + [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) + + As the following screenshot shows, the revision is updated to indicate the + `prod` tag and it is serving 100% of live traffic. + + ![Traffic for the production deployment in the Revisions page.](images/implementing-cloud-run-canary-deployments-git-branches-cloud-build-production-revisions.png) +6. In Cloud Shell, to see percentage-based responses, make a + series of requests: + + ```sh + LIVE_URL=$(gcloud run services describe hello-cloudrun \ + --platform managed \ + --region us-central1 \ + --format=json | jq \ + --raw-output ".status.url") + + for i in {0..20};do + curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $LIVE_URL; echo \n + Done + ``` +7. Review the lines of `tag-cloudbuild.yaml` that implement the production + deployment logic. + + The following line updates the canary revision adding the `prod` tag. The + deployed revision is now tagged for both `prod` and `canary`: + + ```sh + gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=prod=$${CANARY} --platform managed --region ${_REGION} + ``` + The following line updates the traffic for the base service URL to route + 100% of traffic to the revision tagged as `prod`: + + ```sh + gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=$${NEW_PROD}=100 --platform managed --region ${_REGION} + ``` From 7c821e0716564b50e22aafa3fbd4d9c078e1ae91 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Thu, 14 Oct 2021 08:58:35 -0500 Subject: [PATCH 37/50] Update README.md --- labs/cloudrun-progression/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labs/cloudrun-progression/README.md b/labs/cloudrun-progression/README.md index e7fab68..de38cd1 100644 --- a/labs/cloudrun-progression/README.md +++ b/labs/cloudrun-progression/README.md @@ -1,4 +1,4 @@ -# Canary Deployments with Cloud Run and CLoud Build +# Canary Deployments with Cloud Run and Cloud Build This document shows you how to implement a deployment pipeline for Cloud Run that implements progression of code from developer From 2e70d5a20c1c0c8736ad079488d51532f42e16d1 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Thu, 14 Oct 2021 09:00:34 -0500 Subject: [PATCH 38/50] Update README.md --- labs/cloudrun-progression/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/labs/cloudrun-progression/README.md b/labs/cloudrun-progression/README.md index de38cd1..3f71cea 100644 --- a/labs/cloudrun-progression/README.md +++ b/labs/cloudrun-progression/README.md @@ -270,10 +270,10 @@ URL and it routes 10% of all live traffic to the new revision. [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - As the following screenshot shows, 90% of the traffic is routed to `prod`, + Now 90% of the traffic is routed to `prod`, 10% to `canary`, and 0% to the branch revisions. - ![Traffic for the canary deployment in the Revisions page.](images/implementing-cloud-run-canary-deployments-git-branches-cloud-build-canary-revisions.png) + 6. Review the lines of `master-cloudbuild.yaml` that implement the logic for the canary deployment. @@ -370,10 +370,10 @@ production traffic. [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - As the following screenshot shows, the revision is updated to indicate the + The revision is updated to indicate the `prod` tag and it is serving 100% of live traffic. - ![Traffic for the production deployment in the Revisions page.](images/implementing-cloud-run-canary-deployments-git-branches-cloud-build-production-revisions.png) + 6. In Cloud Shell, to see percentage-based responses, make a series of requests: From aa18b936b113cae420419770e46c29b932dc4de1 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Thu, 14 Oct 2021 10:31:54 -0500 Subject: [PATCH 39/50] moving context rename up to provision script (#30) --- .../provision/management-tools/argo-install.sh | 12 ------------ .../resources/provision/provision-all.sh | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/delivery-platform/resources/provision/management-tools/argo-install.sh b/delivery-platform/resources/provision/management-tools/argo-install.sh index c7da28c..c1476bc 100755 --- a/delivery-platform/resources/provision/management-tools/argo-install.sh +++ b/delivery-platform/resources/provision/management-tools/argo-install.sh @@ -33,18 +33,6 @@ PORT_FWD_PID=$! # Argo UI http://localhost:8080 argocd login localhost:8080 --insecure --username=$USER --password=$PASSWORD -gcloud container clusters get-credentials dev --region us-west1-a --project $PROJECT_ID -kubectl config delete-context dev -kubectl config rename-context gke_${PROJECT_ID}_us-west1-a_dev dev - -gcloud container clusters get-credentials stage --region us-west2-a --project $PROJECT_ID -kubectl config delete-context stage -kubectl config rename-context gke_${PROJECT_ID}_us-west2-a_stage stage - -gcloud container clusters get-credentials prod --region us-central1-a --project $PROJECT_ID -kubectl config delete-context prod -kubectl config rename-context gke_${PROJECT_ID}_us-central1-a_prod prod - argocd cluster add dev argocd cluster add stage argocd cluster add prod diff --git a/delivery-platform/resources/provision/provision-all.sh b/delivery-platform/resources/provision/provision-all.sh index 208590b..9f8197e 100755 --- a/delivery-platform/resources/provision/provision-all.sh +++ b/delivery-platform/resources/provision/provision-all.sh @@ -95,6 +95,20 @@ cd ${BASE_DIR}/resources/provision/clusters/tf gcloud builds submit cd $BASE_DIR +# Rename contexts +gcloud container clusters get-credentials dev --region us-west1-a --project $PROJECT_ID +kubectl config delete-context dev +kubectl config rename-context gke_${PROJECT_ID}_us-west1-a_dev dev + +gcloud container clusters get-credentials stage --region us-west2-a --project $PROJECT_ID +kubectl config delete-context stage +kubectl config rename-context gke_${PROJECT_ID}_us-west2-a_stage stage + +gcloud container clusters get-credentials prod --region us-central1-a --project $PROJECT_ID +kubectl config delete-context prod +kubectl config rename-context gke_${PROJECT_ID}_us-central1-a_prod prod + + # Install ACM cd ${BASE_DIR}/resources/provision/management-tools/acm ./acm-install.sh From 1d568214bad7cfb05e397474ffbb9134752940dd Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Wed, 10 Nov 2021 15:42:05 -0600 Subject: [PATCH 40/50] Update README.md Env var for app name --- labs/app-onboarding/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/labs/app-onboarding/README.md b/labs/app-onboarding/README.md index df476f5..dbbd91e 100644 --- a/labs/app-onboarding/README.md +++ b/labs/app-onboarding/README.md @@ -303,7 +303,8 @@ cd $BASE_DIR 2. Create a new application ``` -./app.sh create demo-app golang +export APP_NAME=demo-app +./app.sh create ${APP_NAME} ``` All of the steps are executed automatically. @@ -336,4 +337,4 @@ echo ${GIT_BASE_URL}/${APP_NAME}/settings/hooks The trigger was automatically set up by the script 1. Review the Cloud Build trigger in the Console by [visiting this link](https://console.cloud.google.com/cloud-build/triggers) -2. Review the build history [on this page](https://console.cloud.google.com/cloud-build/builds) \ No newline at end of file +2. Review the build history [on this page](https://console.cloud.google.com/cloud-build/builds) From 6f6d3ab212bbdc7bb8746e7201d3d35c4094cf3b Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 12 Nov 2021 13:55:54 -0600 Subject: [PATCH 41/50] Cleanup directory before checkout (#31) --- delivery-platform/app.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/delivery-platform/app.sh b/delivery-platform/app.sh index 80315da..4f7fc7f 100755 --- a/delivery-platform/app.sh +++ b/delivery-platform/app.sh @@ -51,6 +51,7 @@ create () { printf 'Creating application: %s \n' $APP_NAME # Create an instance of the template. + rm -rf $WORK_DIR/app-templates cd $WORK_DIR/ git clone -b main $GIT_BASE_URL/$APP_TEMPLATES_REPO app-templates rm -rf app-templates/.git @@ -78,12 +79,12 @@ create () { # Initial deploy cd $WORK_DIR/app-templates/${APP_LANG} - git pull + git pull origin main echo "v1" > version.txt git add . && git commit -m "v1" git push origin main sleep 10 - git pull + git pull origin main git tag v1 git push origin v1 From 0532c262ed2d84512a0867d314f8319e91788b18 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 12 Nov 2021 14:42:13 -0600 Subject: [PATCH 42/50] Clarify app.sh call --- labs/app-onboarding/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/labs/app-onboarding/README.md b/labs/app-onboarding/README.md index dbbd91e..c942d79 100644 --- a/labs/app-onboarding/README.md +++ b/labs/app-onboarding/README.md @@ -302,9 +302,10 @@ cd $BASE_DIR 2. Create a new application +The format is app.sh create + ``` -export APP_NAME=demo-app -./app.sh create ${APP_NAME} +./app.sh create demo-app golang ``` All of the steps are executed automatically. From 6dbd04e993b4b0698a25036f61cfa6b8810e7aaa Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 12 Nov 2021 14:42:59 -0600 Subject: [PATCH 43/50] Clarify app.sh call --- labs/app-onboarding/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labs/app-onboarding/README.md b/labs/app-onboarding/README.md index c942d79..89db507 100644 --- a/labs/app-onboarding/README.md +++ b/labs/app-onboarding/README.md @@ -302,7 +302,7 @@ cd $BASE_DIR 2. Create a new application -The format is app.sh create +The format is `app.sh create ` ``` ./app.sh create demo-app golang From 3c10b2e045a50f50c839e7cd2090c25fdde22294 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Fri, 25 Feb 2022 17:24:21 -0600 Subject: [PATCH 44/50] GKE progression lab refresh --- labs/gke-progression/Dockerfile | 15 - labs/gke-progression/README.md | 327 +++++++++++++++++- .../branch-cloudbuild.yaml.tmpl} | 24 +- .../build/branch-trigger.json.tmpl | 16 + .../main-cloudbuild.yaml.tmpl} | 23 +- .../build/main-trigger.json.tmpl | 16 + .../build/tag-cloudbuild.yaml.tmpl | 52 +++ .../build/tag-trigger.json.tmpl | 16 + .../builder/cloudbuild-canary.yaml | 68 ---- .../builder/cloudbuild-local.yaml | 76 ---- labs/gke-progression/builder/cloudbuild.yaml | 91 ----- labs/gke-progression/html.go | 110 ------ .../canary/frontend-canary.yaml.tmpl} | 23 +- .../deployments/dev/frontend-dev.yaml.tmpl} | 23 +- .../prod/frontend-production.yaml.tmpl} | 24 +- .../services/frontend.yaml.tmpl} | 6 +- .../deployments/canary/backend-canary.yaml | 49 --- .../deployments/dev/backend-dev.yaml | 49 --- .../deployments/prod/backend-production.yaml | 49 --- .../kubernetes/services/backend.yaml | 27 -- labs/gke-progression/main.go | 175 ---------- labs/gke-progression/main_test.go | 29 -- labs/gke-progression/src/Dockerfile | 32 ++ .../dev/default.yml => src/app.py} | 30 +- 24 files changed, 510 insertions(+), 840 deletions(-) delete mode 100644 labs/gke-progression/Dockerfile rename labs/gke-progression/{builder/cloudbuild-dev.yaml => build/branch-cloudbuild.yaml.tmpl} (65%) create mode 100644 labs/gke-progression/build/branch-trigger.json.tmpl rename labs/gke-progression/{builder/cloudbuild-prod.yaml => build/main-cloudbuild.yaml.tmpl} (57%) create mode 100644 labs/gke-progression/build/main-trigger.json.tmpl create mode 100644 labs/gke-progression/build/tag-cloudbuild.yaml.tmpl create mode 100644 labs/gke-progression/build/tag-trigger.json.tmpl delete mode 100644 labs/gke-progression/builder/cloudbuild-canary.yaml delete mode 100644 labs/gke-progression/builder/cloudbuild-local.yaml delete mode 100644 labs/gke-progression/builder/cloudbuild.yaml delete mode 100644 labs/gke-progression/html.go rename labs/gke-progression/{kubernetes/deployments/canary/frontend-canary.yaml => k8s/deployments/canary/frontend-canary.yaml.tmpl} (70%) rename labs/gke-progression/{kubernetes/deployments/dev/frontend-dev.yaml => k8s/deployments/dev/frontend-dev.yaml.tmpl} (68%) rename labs/gke-progression/{kubernetes/deployments/prod/frontend-production.yaml => k8s/deployments/prod/frontend-production.yaml.tmpl} (70%) rename labs/gke-progression/{kubernetes/services/frontend.yaml => k8s/services/frontend.yaml.tmpl} (92%) delete mode 100644 labs/gke-progression/kubernetes/deployments/canary/backend-canary.yaml delete mode 100644 labs/gke-progression/kubernetes/deployments/dev/backend-dev.yaml delete mode 100644 labs/gke-progression/kubernetes/deployments/prod/backend-production.yaml delete mode 100644 labs/gke-progression/kubernetes/services/backend.yaml delete mode 100644 labs/gke-progression/main.go delete mode 100644 labs/gke-progression/main_test.go create mode 100644 labs/gke-progression/src/Dockerfile rename labs/gke-progression/{kubernetes/deployments/dev/default.yml => src/app.py} (62%) diff --git a/labs/gke-progression/Dockerfile b/labs/gke-progression/Dockerfile deleted file mode 100644 index 957415a..0000000 --- a/labs/gke-progression/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -FROM golang:onbuild diff --git a/labs/gke-progression/README.md b/labs/gke-progression/README.md index 15cc067..1acf57d 100644 --- a/labs/gke-progression/README.md +++ b/labs/gke-progression/README.md @@ -1,30 +1,325 @@ -# Software Delivery Workshop +# Continuous deployment to Google Kubernetes Engine (GKE) with Cloud Build -This repository contains resources and materials targeted toward Software Delivery on Google Cloud. In addition to separate stand alone guides, an opinionated yet modular platform is provided to demonstrate software delivery practices. In contains scripts to standup a base platform infrastructure as well as other resources designed to facilitate hands on workshop and standard demo use cases. The platform provisioning resources are structured to be modular in nature supporting various runtime and tooling configurations. Ideally users can utilize their own choice of tooling for: Provisioning, Source Code Management, Templating, Build Engine, Image Storage and Deploy tooling. -## Usage +## Overview -This set of resources contains materials to provision the platform, deliver short demonstrations and facilitate hands on workshops. -### Workshop -The Software Delivery Workshop contains materials for a self led exploration or accompanying instructor led sessions. To get started with either click the button below to open the resources in Google Cloud Shell. +In this lab, you'll learn to set up a continuous delivery pipeline for GKE with Cloud Build. You'll complete the following steps: -[![Software Delivery Workshop](http://www.gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/GoogleCloudPlatform/software-delivery-workshop.git&cloudshell_workspace=.&cloudshell_tutorial=delivery-platform/docs/workshop/1.2-provision.md) +* Create the GKE Application +* Automate deployments for git branches +* Automate deployments for git main branch +* Automating deployments for git tags -### Demo -For a mostly automated experience follow the instructions in the `docs\demo` folder. You will run an automated script to fully provision the platform before your demonstration. A separate guide describes the steps to perform during the demonstration and concludes with instructions how to reset the demo or tear down the infrastructure. +## Preparing your environment -### Provision +1. In Cloud Shell, create environment variables to use throughout this tutorial: -If you just want to install the base platform and run your own exercises and workloads, run the following commands from within the `delivery-platform` directory. + ```sh + export PROJECT_ID=$(gcloud config get-value project) + export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') -```shell -gcloud config set project -source ./env.sh -${BASE_DIR}/resources/provision/provision-all.sh -``` + export ZONE=us-central1-b + export CLUSTER=gke-progression-cluster + export APP_NAME=myapp + ``` +2. Enable the following APIs: + + - Resource Manager + - GKE + - Cloud Source Repositories + - Cloud Build + - Container Registry + + + + ```sh + gcloud services enable \ + cloudresourcemanager.googleapis.com \ + container.googleapis.com \ + sourcerepo.googleapis.com \ + cloudbuild.googleapis.com \ + containerregistry.googleapis.com \ + --async + ``` + + +3. Clone the sample source: + + ```sh + git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop.git gke-progression + + cd gke-progression/labs/gke-progression + rm -rf ../../.git + ``` + +4. Replace placeholder values in the sample repository with your `PROJECT_ID`: + + ```sh + for template in $(find . -name '*.tmpl'); do envsubst '${PROJECT_ID} ${ZONE} ${CLUSTER} ${APP_NAME}' < ${template} > ${template%.*}; done + ``` + +5. Store the code from the sample repository in CSR: + + ```sh + gcloud source repos create gke-progression + git init + git config credential.helper gcloud.sh + git remote add gcp https://source.developers.google.com/p/$PROJECT_ID/r/gke-progression + git branch -m main + git add . && git commit -m "initial commit" + git push gcp main + ``` + +6. Create your GKE cluster. + + ```sh + gcloud container clusters create ${CLUSTER} \ + --project=${PROJECT_ID} \ + --zone=${ZONE} + ``` + + +7. Give Cloud Build rights to your cluster. + ```sh + gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member=serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com \ + --role=roles/container.developer + ``` + +Your environment is ready! + + +## Creating your GKE Application + +In this section, you build and deploy the initial production application that you use throughout this tutorial. + +1. Build the application with Cloud Build: + + ```sh + gcloud builds submit --tag gcr.io/$PROJECT_ID/$APP_NAME:1.0.0 src/ + ``` + +2. Manually deploy to Canary and Production environments: + + + Create the production and canary deployments and services using the `kubectl apply` commands. + + ```console + kubectl create ns production + kubectl apply -f k8s/deployments/prod -n production + kubectl apply -f k8s/deployments/canary -n production + kubectl apply -f k8s/services -n production + ``` + + +3. Review number of running pods + + Confirm that you have four Pods running for the frontend, including three for production traffic and one for canary releases. That means that changes to your canary release will only affect 1 out of 4 (25%) of users. + + + ```console + kubectl get pods -n production -l app=$APP_NAME -l role=frontend + ``` + + +4. Retrieve the external IP address for the production services. + + > **Note:** It can take several minutes before you see the load balancer external IP address. + + ```sh + kubectl get service $APP_NAME -n production + ``` + + Once the load balancer returns the IP address continue to the next step + +5. Store the external IP for later use. + + ```sh + export PRODUCTION_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services $APP_NAME) + ``` + +6. Review the application + + Check the version output of the service. It should read Hello World v1.0 + + ```sh + curl http://$PRODUCTION_IP + ``` + +Congratulations! You deployed the sample app! Next, you'll set up a pipeline for continuously and deploying your changes. + + + + + +## Automating deployments for git branches + +1. Set up the trigger: + + ```sh + gcloud beta builds triggers create cloud-source-repositories \ + --trigger-config build/branch-trigger.json + ``` + +2. To review the trigger, go to the + [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) + in the Console. + + [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) + +3. Create a new branch: + + ```sh + git checkout -b new-feature-1 + ``` + +4. Modify the code to indicate v1.1 + + Edit `src/app.py` and change the response from 1.0 to 1.1 + + ```py + @app.route('/') + def hello_world(): + return 'Hello World v1.1' + ``` + +5. Commit the change and push to the remote repository: + + ```sh + git add . && git commit -m "updated" && git push gcp new-feature-1 + ``` + + +6. To review the build in progress, go to the + [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) + in the Console. + + [Go to Builds](https://console.cloud.google.com/cloud-build/builds) + + Once the build completes continue to the next step + +7. Retrieve the external IP address for the newly deployed branch service. + + > **Note:** It can take several minutes before you see the load balancer external IP address. + + ```sh + kubectl get service $APP_NAME -n new-feature-1 + ``` + + Once the load balancer returns the IP address continue to the next step + +5. Store the external IP for later use. + + ```sh + export BRANCH_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=new-feature-1 services $APP_NAME) + ``` + +6. Review the application + + Check the version output of the service. It should read Hello World v1.0 + + ```sh + curl http://$BRANCH_IP + ``` + +## Automate deployments for git main branch + + +When code is released to production, it's common to release code to a small subset of live traffic before migrating all traffic to the new code base. + +In this section, you implement a trigger that is activated when code is committed to the main branch. The trigger deploys the code to a unique canary URL and it routes 10% of all live traffic to the new revision. + +1. Set up the trigger for the main branch: + + ```sh + gcloud beta builds triggers create cloud-source-repositories \ + --trigger-config build/main-trigger.json + ``` + +2. To review the new trigger, go to the + [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) + in the Console. + + [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) + + +3. Merge the branch to the main line and push to + the remote repository: + + ```sh + git checkout main + git merge new-feature-1 + git push gcp main + ``` + +4. To review the build in progress, go to the + [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) + in the Console. + + [Go to Builds](https://console.cloud.google.com/cloud-build/builds) + + Once the build has completed continue to the next step + +5. Review mulitple responses from the server + + Run the following command and note that approxomatly 25% of the responses are showing the new response of Hello World v1.1 + + ```sh + while true; do curl -w "\n" http://$PRODUCTION_IP; sleep 1; done + ``` + + When you're ready to continue pres `Ctrl+c` to exit out of the loop. + + +## Automating deployments for git tags + +After the canary deployment is validated with a small subset of traffic, you release the deployment to the remainder of the live traffic. + +In this section, you set up a trigger that is activated when you create a tag in the repository. The trigger labels the image with the appropriate tag then deploys the updates to prod ensuring 100% of traffic is accessing the tagged image. + +1. Set up the tag trigger: + + ```sh + gcloud beta builds triggers create cloud-source-repositories \ + --trigger-config build/tag-trigger.json + ``` + +2. To review the new trigger, go to the + [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) + in the Console. + + [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) + +3. Create a new tag and push to the remote repository: + + ```sh + git tag 1.1 + git push gcp 1.1 + ``` +4. To review the build in progress, go to the + [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) + in the Console. + + [Go to Builds](https://console.cloud.google.com/cloud-build/builds) + + +5. Review mulitple responses from the server + + Run the following command and note that 100% of the responses are showing the new response of Hello World v1.1 + + This may take a moment as the new pods are deployed and health checked within GKE + + ```sh + while true; do curl -w "\n" http://$PRODUCTION_IP; sleep 1; done + ``` + + When you're ready to continue pres `Ctrl+c` to exit out of the loop. + + + Congratulations! You created CI/CD triggers in Cloud Build for branches and tags to deploy your apps to GKE. \ No newline at end of file diff --git a/labs/gke-progression/builder/cloudbuild-dev.yaml b/labs/gke-progression/build/branch-cloudbuild.yaml.tmpl similarity index 65% rename from labs/gke-progression/builder/cloudbuild-dev.yaml rename to labs/gke-progression/build/branch-cloudbuild.yaml.tmpl index e57fd0a..5c84a22 100644 --- a/labs/gke-progression/builder/cloudbuild-dev.yaml +++ b/labs/gke-progression/build/branch-cloudbuild.yaml.tmpl @@ -22,7 +22,7 @@ steps: args: - '-c' - | - docker build -t gcr.io/$PROJECT_ID/gceme:${BRANCH_NAME}-${SHORT_SHA} . + docker build -t gcr.io/$PROJECT_ID/$APP_NAME:${BRANCH_NAME}-${SHORT_SHA} ./src @@ -36,7 +36,7 @@ steps: args: - '-c' - | - docker push gcr.io/$PROJECT_ID/gceme:${BRANCH_NAME}-${SHORT_SHA} + docker push gcr.io/$PROJECT_ID/$APP_NAME:${BRANCH_NAME}-${SHORT_SHA} @@ -44,28 +44,22 @@ steps: - id: 'deploy' name: 'gcr.io/cloud-builders/gcloud' env: - - 'CLOUDSDK_COMPUTE_ZONE=${_CLOUDSDK_COMPUTE_ZONE}' - - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLOUDSDK_CONTAINER_CLUSTER}' - 'KUBECONFIG=/kube/config' entrypoint: 'bash' args: - '-c' - | - CLUSTER=$$(gcloud config get-value container/cluster) + PROJECT=$$(gcloud config get-value core/project) - ZONE=$$(gcloud config get-value compute/zone) + - gcloud container clusters get-credentials "$${CLUSTER}" \ + gcloud container clusters get-credentials "${_CLUSTER}" \ --project "$${PROJECT}" \ - --zone "$${ZONE}" + --zone "${_ZONE}" - - - - - sed -i 's|gcr.io/cloud-solutions-images/gceme:.*|gcr.io/$PROJECT_ID/gceme:${BRANCH_NAME}-${SHORT_SHA}|' ./kubernetes/deployments/dev/*.yaml + sed -i 's|gcr.io/$PROJECT_ID/$APP_NAME:.*|gcr.io/$PROJECT_ID/$APP_NAME:${BRANCH_NAME}-${SHORT_SHA}|' ./k8s/deployments/dev/*.yaml kubectl get ns ${BRANCH_NAME} || kubectl create ns ${BRANCH_NAME} - kubectl apply --namespace ${BRANCH_NAME} --recursive -f kubernetes/deployments/dev - kubectl apply --namespace ${BRANCH_NAME} --recursive -f kubernetes/services + kubectl apply --namespace ${BRANCH_NAME} --recursive -f k8s/deployments/dev + kubectl apply --namespace ${BRANCH_NAME} --recursive -f k8s/services diff --git a/labs/gke-progression/build/branch-trigger.json.tmpl b/labs/gke-progression/build/branch-trigger.json.tmpl new file mode 100644 index 0000000..6e496e0 --- /dev/null +++ b/labs/gke-progression/build/branch-trigger.json.tmpl @@ -0,0 +1,16 @@ +{ + "name": "branch", + "description": "Trigger dev build/deploy for any branch other than master", + "filename": "build/branch-cloudbuild.yaml", + + "triggerTemplate": { + "projectId": "$PROJECT_ID", + "repoName": "gke-progression", + "branchName": "main", + "invertRegex": true + }, + "substitutions": { + "_ZONE": "${ZONE}", + "_CLUSTER": "${CLUSTER}" + } +} \ No newline at end of file diff --git a/labs/gke-progression/builder/cloudbuild-prod.yaml b/labs/gke-progression/build/main-cloudbuild.yaml.tmpl similarity index 57% rename from labs/gke-progression/builder/cloudbuild-prod.yaml rename to labs/gke-progression/build/main-cloudbuild.yaml.tmpl index d806995..666ceb1 100644 --- a/labs/gke-progression/builder/cloudbuild-prod.yaml +++ b/labs/gke-progression/build/main-cloudbuild.yaml.tmpl @@ -22,8 +22,7 @@ steps: args: - '-c' - | - docker build -t gcr.io/$PROJECT_ID/gceme:$TAG_NAME . - + docker build -t gcr.io/$PROJECT_ID/$APP_NAME:${SHORT_SHA} ./src ### Test @@ -36,7 +35,7 @@ steps: args: - '-c' - | - docker push gcr.io/$PROJECT_ID/gceme:$TAG_NAME + docker push gcr.io/$PROJECT_ID/$APP_NAME:${SHORT_SHA} @@ -44,27 +43,21 @@ steps: - id: 'deploy' name: 'gcr.io/cloud-builders/gcloud' env: - - 'CLOUDSDK_COMPUTE_ZONE=${_CLOUDSDK_COMPUTE_ZONE}' - - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLOUDSDK_CONTAINER_CLUSTER}' - 'KUBECONFIG=/kube/config' entrypoint: 'bash' args: - '-c' - | - CLUSTER=$$(gcloud config get-value container/cluster) - PROJECT=$$(gcloud config get-value core/project) - ZONE=$$(gcloud config get-value compute/zone) - + PROJECT=$$(gcloud config get-value core/project) - gcloud container clusters get-credentials "$${CLUSTER}" \ + gcloud container clusters get-credentials "${_CLUSTER}" \ --project "$${PROJECT}" \ - --zone "$${ZONE}" - + --zone "${_ZONE}" - sed -i 's|gcr.io/cloud-solutions-images/gceme:.*|gcr.io/$PROJECT_ID/gceme:$TAG_NAME|' ./kubernetes/deployments/prod/*.yaml + sed -i 's|gcr.io/$PROJECT_ID/$APP_NAME:.*|gcr.io/$PROJECT_ID/$APP_NAME:${SHORT_SHA}|' ./k8s/deployments/canary/*.yaml kubectl get ns production || kubectl create ns production - kubectl apply --namespace production --recursive -f kubernetes/deployments/prod - kubectl apply --namespace production --recursive -f kubernetes/services + kubectl apply --namespace production --recursive -f k8s/deployments/canary + kubectl apply --namespace production --recursive -f k8s/services diff --git a/labs/gke-progression/build/main-trigger.json.tmpl b/labs/gke-progression/build/main-trigger.json.tmpl new file mode 100644 index 0000000..fbfde6a --- /dev/null +++ b/labs/gke-progression/build/main-trigger.json.tmpl @@ -0,0 +1,16 @@ +{ + "name": "main", + "description": "Trigger canary build/deploy for any commit to the main branch", + + "filename": "build/main-cloudbuild.yaml", + "triggerTemplate": { + "projectId": "$PROJECT_ID", + "repoName": "gke-progression", + "branchName": "main" + }, + "substitutions": { + "_ZONE": "${ZONE}", + "_CLUSTER": "${CLUSTER}" + } + +} \ No newline at end of file diff --git a/labs/gke-progression/build/tag-cloudbuild.yaml.tmpl b/labs/gke-progression/build/tag-cloudbuild.yaml.tmpl new file mode 100644 index 0000000..b4e19f0 --- /dev/null +++ b/labs/gke-progression/build/tag-cloudbuild.yaml.tmpl @@ -0,0 +1,52 @@ +# Copyright 2018 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 +# +# https://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. + + +steps: + +### Add Tag + + - id: 'add-tag' + name: 'gcr.io/cloud-builders/gcloud' + entrypoint: 'bash' + args: + - '-c' + - | + gcloud container images add-tag gcr.io/$PROJECT_ID/$APP_NAME:${SHORT_SHA} \ + gcr.io/$PROJECT_ID/$APP_NAME:$TAG_NAME \ + --quiet + + + +### Deploy + - id: 'deploy' + name: 'gcr.io/cloud-builders/gcloud' + env: + - 'KUBECONFIG=/kube/config' + entrypoint: 'bash' + args: + - '-c' + - | + PROJECT=$$(gcloud config get-value core/project) + + gcloud container clusters get-credentials "${_CLUSTER}" \ + --project "$${PROJECT}" \ + --zone "${_ZONE}" + + + sed -i 's|gcr.io/$PROJECT_ID/$APP_NAME:.*|gcr.io/$PROJECT_ID/$APP_NAME:$TAG_NAME|' ./k8s/deployments/prod/*.yaml + + kubectl apply --namespace production --recursive -f k8s/deployments/prod + kubectl apply --namespace production --recursive -f k8s/services + diff --git a/labs/gke-progression/build/tag-trigger.json.tmpl b/labs/gke-progression/build/tag-trigger.json.tmpl new file mode 100644 index 0000000..b1bdb7f --- /dev/null +++ b/labs/gke-progression/build/tag-trigger.json.tmpl @@ -0,0 +1,16 @@ +{ + "name": "tag", + "description": "Migrate from canary to prod triggered by creation of any tag", + "filename": "build/tag-cloudbuild.yaml", + + "triggerTemplate": { + "projectId": "$PROJECT_ID", + "repoName": "gke-progression", + "tagName": ".*" + }, + "substitutions": { + "_ZONE": "${ZONE}", + "_CLUSTER": "${CLUSTER}" + } + +} \ No newline at end of file diff --git a/labs/gke-progression/builder/cloudbuild-canary.yaml b/labs/gke-progression/builder/cloudbuild-canary.yaml deleted file mode 100644 index 03b54b4..0000000 --- a/labs/gke-progression/builder/cloudbuild-canary.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2018 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 -# -# https://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. - - -steps: - -### Build - - - id: 'build' - name: 'gcr.io/cloud-builders/docker' - entrypoint: 'bash' - args: - - '-c' - - | - docker build -t gcr.io/$PROJECT_ID/gceme:${SHORT_SHA} . - - - -### Test - - -### Publish - - id: 'publish' - name: 'gcr.io/cloud-builders/docker' - entrypoint: 'bash' - args: - - '-c' - - | - docker push gcr.io/$PROJECT_ID/gceme:${SHORT_SHA} - - - -### Deploy - - id: 'deploy' - name: 'gcr.io/cloud-builders/gcloud' - env: - - 'CLOUDSDK_COMPUTE_ZONE=${_CLOUDSDK_COMPUTE_ZONE}' - - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLOUDSDK_CONTAINER_CLUSTER}' - - 'KUBECONFIG=/kube/config' - entrypoint: 'bash' - args: - - '-c' - - | - CLUSTER=$$(gcloud config get-value container/cluster) - PROJECT=$$(gcloud config get-value core/project) - ZONE=$$(gcloud config get-value compute/zone) - - gcloud container clusters get-credentials "$${CLUSTER}" \ - --project "$${PROJECT}" \ - --zone "$${ZONE}" - - - sed -i 's|gcr.io/cloud-solutions-images/gceme:.*|gcr.io/$PROJECT_ID/gceme:${SHORT_SHA}|' ./kubernetes/deployments/canary/*.yaml - - kubectl apply --namespace production --recursive -f kubernetes/deployments/canary - kubectl apply --namespace production --recursive -f kubernetes/services - diff --git a/labs/gke-progression/builder/cloudbuild-local.yaml b/labs/gke-progression/builder/cloudbuild-local.yaml deleted file mode 100644 index 9c29607..0000000 --- a/labs/gke-progression/builder/cloudbuild-local.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2018 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 -# -# https://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. - -# Usage: -# gcloud container builds submit \ -# --config cloudbuild-local.yaml \ -# --substitutions=_VERSION=someversion,_USER=$(whoami),_CLOUDSDK_COMPUTE_ZONE=${ZONE},_CLOUDSDK_CONTAINER_CLUSTER=${CLUSTER} . - - -steps: - -### Build - - - id: 'build' - name: 'gcr.io/cloud-builders/docker' - entrypoint: 'bash' - args: - - '-c' - - | - echo $PROJECT_ID - docker build -t gcr.io/$PROJECT_ID/gceme:${_USER}-${_VERSION} . - - - -### Test - - -### Publish - - id: 'publish' - name: 'gcr.io/cloud-builders/docker' - entrypoint: 'bash' - args: - - '-c' - - | - docker push gcr.io/$PROJECT_ID/gceme:${_USER}-${_VERSION} - - -### Deploy - - id: 'deploy' - name: 'gcr.io/cloud-builders/gcloud' - env: - - 'CLOUDSDK_COMPUTE_ZONE=${_CLOUDSDK_COMPUTE_ZONE}' - - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLOUDSDK_CONTAINER_CLUSTER}' - - 'KUBECONFIG=/kube/config' - entrypoint: 'bash' - args: - - '-c' - - | - CLUSTER=$$(gcloud config get-value container/cluster) - PROJECT=$$(gcloud config get-value core/project) - ZONE=$$(gcloud config get-value compute/zone) - - gcloud container clusters get-credentials "$${CLUSTER}" \ - --project "$${PROJECT}" \ - --zone "$${ZONE}" - - sed -i 's|gcr.io/cloud-solutions-images/gceme:.*|gcr.io/$PROJECT_ID/gceme:${_USER}-${_VERSION}|' ./kubernetes/deployments/dev/*.yaml - - kubectl get ns ${_USER} || kubectl create ns ${_USER} - kubectl apply --namespace ${_USER} --recursive -f kubernetes/deployments/dev - kubectl apply --namespace ${_USER} --recursive -f kubernetes/services - - echo service available at http://`kubectl --namespace=${_USER} get service/gceme-frontend -o jsonpath="{.status.loadBalancer.ingress[0].ip}"` - - \ No newline at end of file diff --git a/labs/gke-progression/builder/cloudbuild.yaml b/labs/gke-progression/builder/cloudbuild.yaml deleted file mode 100644 index 9548cae..0000000 --- a/labs/gke-progression/builder/cloudbuild.yaml +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2018 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 -# -# https://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. - -steps: - -### Build - - - id: 'build' - name: 'gcr.io/cloud-builders/docker' - entrypoint: 'bash' - args: - - '-c' - - | - [[ "$BRANCH_NAME" ]] && VERSION=${BRANCH_NAME}-${SHORT_SHA} - [[ "$TAG_NAME" ]] && VERSION=$TAG_NAME - docker build -t gcr.io/$PROJECT_ID/helloworld:$${VERSION} . - - - -### Test - - -### Publish - - id: 'publish' - name: 'gcr.io/cloud-builders/docker' - entrypoint: 'bash' - args: - - '-c' - - | - [[ "$BRANCH_NAME" ]] && VERSION=${BRANCH_NAME}-${SHORT_SHA} - [[ "$TAG_NAME" ]] && VERSION=$TAG_NAME - docker push gcr.io/$PROJECT_ID/helloworld:$${VERSION} - - - -### Deploy - - id: 'deploy' - name: 'gcr.io/cloud-builders/gcloud' - env: - - 'CLOUDSDK_COMPUTE_ZONE=${_CLOUDSDK_COMPUTE_ZONE}' - - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLOUDSDK_CONTAINER_CLUSTER}' - - 'KUBECONFIG=/kube/config' - entrypoint: 'bash' - args: - - '-c' - - | - CLUSTER=$$(gcloud config get-value container/cluster) - PROJECT=$$(gcloud config get-value core/project) - ZONE=$$(gcloud config get-value compute/zone) - - [[ "$BRANCH_NAME" ]] && VERSION=${BRANCH_NAME}-${SHORT_SHA} - [[ "$TAG_NAME" ]] && VERSION=$TAG_NAME - - gcloud container clusters get-credentials "$${CLUSTER}" \ - --project "$${PROJECT}" \ - --zone "$${ZONE}" - - - if [[ "$TAG_NAME" ]] ; then - # Production Deploy - TARGET_ENV="prod" - NS="default" - - elif [[ ${BRANCH_NAME} == "master" ]] ; then - # Canary Deploy - TARGET_ENV="canary" - NS="default" - else - # Dev Deploy - TARGET_ENV="dev" - NS=$${BRANCH_NAME} - VERSION=$${BRANCH_NAME}-$${VERSION} - fi - - sed -i 's|gcr.io/$PROJECT_ID/helloworld:.*|gcr.io/$PROJECT_ID/helloworld:$${VERSION}|' ./kubernetes/deployments/$${TARGET_ENV}/*.yaml - - kubectl get ns $${NS} || kubectl create ns $${NS} - kubectl apply --namespace $${NS} --recursive -f kubernetes/deployments/$${TARGET_ENV} - kubectl apply --namespace $${NS} --recursive -f kubernetes/services - diff --git a/labs/gke-progression/html.go b/labs/gke-progression/html.go deleted file mode 100644 index d274513..0000000 --- a/labs/gke-progression/html.go +++ /dev/null @@ -1,110 +0,0 @@ -/** -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. -**/ - -package main - -const ( - html = ` - - - - - - - -Frontend Web Server - - -
-
-
 
-
- - -
-
-
Backend that serviced this request
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name{{.Name}}
Version{{.Version}}
ID{{.Id}}
Hostname{{.Hostname}}
Zone{{.Zone}}
Project{{.Project}}
Internal IP{{.InternalIP}}
External IP{{.ExternalIP}}
-
-
- -
-
-
Proxy that handled this request
-
-
- - - - - - - - - - - - - - - -
Address{{.ClientIP}}
Request{{.LBRequest}}
Error{{.Error}}
-
- -
-
-
 
-
-
-` -) diff --git a/labs/gke-progression/kubernetes/deployments/canary/frontend-canary.yaml b/labs/gke-progression/k8s/deployments/canary/frontend-canary.yaml.tmpl similarity index 70% rename from labs/gke-progression/kubernetes/deployments/canary/frontend-canary.yaml rename to labs/gke-progression/k8s/deployments/canary/frontend-canary.yaml.tmpl index 7b3b432..dcdae5a 100644 --- a/labs/gke-progression/kubernetes/deployments/canary/frontend-canary.yaml +++ b/labs/gke-progression/k8s/deployments/canary/frontend-canary.yaml.tmpl @@ -1,4 +1,4 @@ -# Copyright 2015 Google Inc. All rights reserved. +# Copyright 2021 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,35 +15,32 @@ kind: Deployment apiVersion: apps/v1 metadata: - name: gceme-frontend-canary + name: $APP_NAME-canary spec: selector: matchLabels: - app: gceme + app: $APP_NAME role: frontend env: canary - replicas: template: metadata: name: frontend labels: - app: gceme + app: $APP_NAME role: frontend env: canary spec: containers: - name: frontend - image: gcr.io/cloud-solutions-images/gceme:1.0.0 + image: gcr.io/$PROJECT_ID/$APP_NAME:1.0.0 + imagePullPolicy: Always resources: limits: memory: "500Mi" cpu: "100m" - imagePullPolicy: Always - readinessProbe: - httpGet: - path: /healthz - port: 80 - command: ["sh", "-c", "app -frontend=true -backend-service=http://gceme-backend:8080 -port=80"] ports: - name: frontend - containerPort: 80 + containerPort: 8080 + env: + - name: PORT + value: "8080" \ No newline at end of file diff --git a/labs/gke-progression/kubernetes/deployments/dev/frontend-dev.yaml b/labs/gke-progression/k8s/deployments/dev/frontend-dev.yaml.tmpl similarity index 68% rename from labs/gke-progression/kubernetes/deployments/dev/frontend-dev.yaml rename to labs/gke-progression/k8s/deployments/dev/frontend-dev.yaml.tmpl index bc78f40..294ceac 100644 --- a/labs/gke-progression/kubernetes/deployments/dev/frontend-dev.yaml +++ b/labs/gke-progression/k8s/deployments/dev/frontend-dev.yaml.tmpl @@ -1,4 +1,4 @@ -# Copyright 2015 Google Inc. All rights reserved. +# Copyright 2021 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,35 +15,32 @@ kind: Deployment apiVersion: apps/v1 metadata: - name: gceme-frontend-dev + name: $APP_NAME-dev spec: selector: matchLabels: - app: gceme + app: $APP_NAME role: frontend env: dev - replicas: template: metadata: name: frontend labels: - app: gceme + app: $APP_NAME role: frontend env: dev spec: containers: - name: frontend - image: gcr.io/cloud-solutions-images/gceme:1.0.0 + image: gcr.io/$PROJECT_ID/$APP_NAME:1.0.0 + imagePullPolicy: Always resources: limits: memory: "500Mi" cpu: "100m" - imagePullPolicy: Always - readinessProbe: - httpGet: - path: /healthz - port: 80 - command: ["sh", "-c", "app -frontend=true -backend-service=http://${GCEME_BACKEND_SERVICE_HOST}:${GCEME_BACKEND_SERVICE_PORT} -port=80"] ports: - name: frontend - containerPort: 80 + containerPort: 8080 + env: + - name: PORT + value: "8080" \ No newline at end of file diff --git a/labs/gke-progression/kubernetes/deployments/prod/frontend-production.yaml b/labs/gke-progression/k8s/deployments/prod/frontend-production.yaml.tmpl similarity index 70% rename from labs/gke-progression/kubernetes/deployments/prod/frontend-production.yaml rename to labs/gke-progression/k8s/deployments/prod/frontend-production.yaml.tmpl index d76106d..0ea0d17 100644 --- a/labs/gke-progression/kubernetes/deployments/prod/frontend-production.yaml +++ b/labs/gke-progression/k8s/deployments/prod/frontend-production.yaml.tmpl @@ -1,4 +1,4 @@ -# Copyright 2015 Google Inc. All rights reserved. +# Copyright 2021 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,35 +15,33 @@ kind: Deployment apiVersion: apps/v1 metadata: - name: gceme-frontend-production + name: $APP_NAME-production spec: - replicas: + replicas: 3 selector: matchLabels: - app: gceme + app: $APP_NAME role: frontend env: production template: metadata: name: frontend labels: - app: gceme + app: $APP_NAME role: frontend env: production spec: containers: - name: frontend - image: gcr.io/cloud-solutions-images/gceme:1.0.0 + image: gcr.io/$PROJECT_ID/$APP_NAME:1.0.0 + imagePullPolicy: Always resources: limits: memory: "500Mi" cpu: "100m" - imagePullPolicy: Always - readinessProbe: - httpGet: - path: /healthz - port: 80 - command: ["sh", "-c", "app -frontend=true -backend-service=http://gceme-backend:8080 -port=80"] ports: - name: frontend - containerPort: 80 + containerPort: 8080 + env: + - name: PORT + value: "8080" \ No newline at end of file diff --git a/labs/gke-progression/kubernetes/services/frontend.yaml b/labs/gke-progression/k8s/services/frontend.yaml.tmpl similarity index 92% rename from labs/gke-progression/kubernetes/services/frontend.yaml rename to labs/gke-progression/k8s/services/frontend.yaml.tmpl index dbf9360..67db9c2 100644 --- a/labs/gke-progression/kubernetes/services/frontend.yaml +++ b/labs/gke-progression/k8s/services/frontend.yaml.tmpl @@ -15,14 +15,14 @@ kind: Service apiVersion: v1 metadata: - name: gceme-frontend + name: $APP_NAME spec: type: LoadBalancer ports: - name: http port: 80 - targetPort: 80 + targetPort: 8080 protocol: TCP selector: - app: gceme + app: $APP_NAME role: frontend diff --git a/labs/gke-progression/kubernetes/deployments/canary/backend-canary.yaml b/labs/gke-progression/kubernetes/deployments/canary/backend-canary.yaml deleted file mode 100644 index 8c52f27..0000000 --- a/labs/gke-progression/kubernetes/deployments/canary/backend-canary.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -kind: Deployment -apiVersion: apps/v1 -metadata: - name: gceme-backend-canary -spec: - selector: - matchLabels: - app: gceme - role: backend - env: canary - replicas: 1 - template: - metadata: - name: backend - labels: - app: gceme - role: backend - env: canary - spec: - containers: - - name: backend - image: gcr.io/cloud-solutions-images/gceme:1.0.0 - resources: - limits: - memory: "500Mi" - cpu: "100m" - imagePullPolicy: Always - readinessProbe: - httpGet: - path: /healthz - port: 8080 - command: ["sh", "-c", "app -port=8080"] - ports: - - name: backend - containerPort: 8080 diff --git a/labs/gke-progression/kubernetes/deployments/dev/backend-dev.yaml b/labs/gke-progression/kubernetes/deployments/dev/backend-dev.yaml deleted file mode 100644 index c028866..0000000 --- a/labs/gke-progression/kubernetes/deployments/dev/backend-dev.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -kind: Deployment -apiVersion: apps/v1 -metadata: - name: gceme-backend-dev -spec: - selector: - matchLabels: - app: gceme - role: backend - env: dev - replicas: 1 - template: - metadata: - name: backend - labels: - app: gceme - role: backend - env: dev - spec: - containers: - - name: backend - image: gcr.io/cloud-solutions-images/gceme:1.0.0 - resources: - limits: - memory: "500Mi" - cpu: "100m" - imagePullPolicy: Always - readinessProbe: - httpGet: - path: /healthz - port: 8080 - command: ["sh", "-c", "app -port=8080"] - ports: - - name: backend - containerPort: 8080 diff --git a/labs/gke-progression/kubernetes/deployments/prod/backend-production.yaml b/labs/gke-progression/kubernetes/deployments/prod/backend-production.yaml deleted file mode 100644 index 3ae4af9..0000000 --- a/labs/gke-progression/kubernetes/deployments/prod/backend-production.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -kind: Deployment -apiVersion: apps/v1 -metadata: - name: gceme-backend-production -spec: - selector: - matchLabels: - app: gceme - role: backend - env: production - replicas: 1 - template: - metadata: - name: backend - labels: - app: gceme - role: backend - env: production - spec: - containers: - - name: backend - image: gcr.io/cloud-solutions-images/gceme:1.0.0 - resources: - limits: - memory: "500Mi" - cpu: "100m" - imagePullPolicy: Always - readinessProbe: - httpGet: - path: /healthz - port: 8080 - command: ["sh", "-c", "app -port=8080"] - ports: - - name: backend - containerPort: 8080 diff --git a/labs/gke-progression/kubernetes/services/backend.yaml b/labs/gke-progression/kubernetes/services/backend.yaml deleted file mode 100644 index 9fb093c..0000000 --- a/labs/gke-progression/kubernetes/services/backend.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -kind: Service -apiVersion: v1 -metadata: - name: gceme-backend -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - protocol: TCP - selector: - role: backend - app: gceme diff --git a/labs/gke-progression/main.go b/labs/gke-progression/main.go deleted file mode 100644 index 06284a9..0000000 --- a/labs/gke-progression/main.go +++ /dev/null @@ -1,175 +0,0 @@ -/** -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. -**/ - -package main - -import ( - "encoding/json" - "flag" - "fmt" - "html/template" - "io/ioutil" - "log" - "net/http" - "net/http/httputil" - - "cloud.google.com/go/compute/metadata" -) - -type Instance struct { - Id string - Name string - Version string - Hostname string - Zone string - Project string - InternalIP string - ExternalIP string - LBRequest string - ClientIP string - Error string -} - -const version string = "1.0.0" - -func main() { - showversion := flag.Bool("version", false, "display version") - frontend := flag.Bool("frontend", false, "run in frontend mode") - port := flag.Int("port", 8080, "port to bind") - backend := flag.String("backend-service", "http://127.0.0.1:8081", "hostname of backend server") - flag.Parse() - - if *showversion { - fmt.Printf("Version %s\n", version) - return - } - - http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "%s\n", version) - }) - - if *frontend { - frontendMode(*port, *backend) - } else { - backendMode(*port) - } - -} - -func backendMode(port int) { - log.Println("Operating in backend mode...") - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - i := newInstance() - raw, _ := httputil.DumpRequest(r, true) - i.LBRequest = string(raw) - resp, _ := json.Marshal(i) - fmt.Fprintf(w, "%s", resp) - }) - http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) - -} - -func frontendMode(port int, backendURL string) { - log.Println("Operating in frontend mode...") - tpl := template.Must(template.New("out").Parse(html)) - - transport := http.Transport{DisableKeepAlives: false} - client := &http.Client{Transport: &transport} - req, _ := http.NewRequest( - "GET", - backendURL, - nil, - ) - req.Close = false - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - i := &Instance{} - resp, err := client.Do(req) - if err != nil { - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintf(w, "Error: %s\n", err.Error()) - return - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "Error: %s\n", err.Error()) - return - } - err = json.Unmarshal([]byte(body), i) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "Error: %s\n", err.Error()) - return - } - tpl.Execute(w, i) - }) - - http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { - resp, err := client.Do(req) - if err != nil { - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintf(w, "Backend could not be connected to: %s", err.Error()) - return - } - defer resp.Body.Close() - ioutil.ReadAll(resp.Body) - w.WriteHeader(http.StatusOK) - }) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) -} - -type assigner struct { - err error -} - -func (a *assigner) assign(getVal func() (string, error)) string { - if a.err != nil { - return "" - } - s, err := getVal() - if err != nil { - a.err = err - } - return s -} - -func newInstance() *Instance { - var i = new(Instance) - if !metadata.OnGCE() { - i.Error = "Not running on GCE" - return i - } - - a := &assigner{} - i.Id = a.assign(metadata.InstanceID) - i.Zone = a.assign(metadata.Zone) - i.Name = a.assign(metadata.InstanceName) - i.Hostname = a.assign(metadata.Hostname) - i.Project = a.assign(metadata.ProjectID) - i.InternalIP = a.assign(metadata.InternalIP) - i.ExternalIP = a.assign(metadata.ExternalIP) - i.Version = version - - if a.err != nil { - i.Error = a.err.Error() - } - return i -} diff --git a/labs/gke-progression/main_test.go b/labs/gke-progression/main_test.go deleted file mode 100644 index 3d74151..0000000 --- a/labs/gke-progression/main_test.go +++ /dev/null @@ -1,29 +0,0 @@ -/** -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. -**/ - -package main - -import ( - "cloud.google.com/go/compute/metadata" - "testing" -) - -func TestGCE(t *testing.T) { - i := newInstance() - if !metadata.OnGCE() && i.Error != "Not running on GCE" { - t.Error("Test not running on GCE, but error does not indicate that fact.") - } -} diff --git a/labs/gke-progression/src/Dockerfile b/labs/gke-progression/src/Dockerfile new file mode 100644 index 0000000..5344650 --- /dev/null +++ b/labs/gke-progression/src/Dockerfile @@ -0,0 +1,32 @@ +# Copyright 2021 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. + + +# Use the official lightweight Python image. +# https://hub.docker.com/_/python +FROM python:3.7-slim + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . ./ + +# Install production dependencies. +RUN pip install Flask gunicorn + +# Run the web service on container startup. Here we use the gunicorn +# webserver, with one worker process and 8 threads. +# For environments with multiple CPU cores, increase the number of workers +# to be equal to the cores available. +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app \ No newline at end of file diff --git a/labs/gke-progression/kubernetes/deployments/dev/default.yml b/labs/gke-progression/src/app.py similarity index 62% rename from labs/gke-progression/kubernetes/deployments/dev/default.yml rename to labs/gke-progression/src/app.py index ddd44d6..7c2aae3 100644 --- a/labs/gke-progression/kubernetes/deployments/dev/default.yml +++ b/labs/gke-progression/src/app.py @@ -1,25 +1,27 @@ -# Copyright 2015 Google Inc. All rights reserved. +#!/usr/bin/python +# +# Copyright 2021 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 +# 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. -# -kind: ResourceQuota -apiVersion: v1 -metadata: - name: default -spec: - hard: - memory: 2Gi - cpu: 2 - pods: 4 - services: 2 - resourcequotas: 1 +import os + +from flask import Flask + +app = Flask(__name__) + +@app.route('/') +def hello_world(): + return 'Hello World v1.0' + +if __name__ == "__main__": + app.run(debug=True,host='0.0.0.0',port=8080) \ No newline at end of file From 039b97b4ab50a38a49121f3c4e0b0af58dd79b30 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Mon, 28 Feb 2022 13:33:40 -0600 Subject: [PATCH 45/50] Gke progression refresh (#34) * formatting updates --- labs/gke-progression/README.md | 72 ++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/labs/gke-progression/README.md b/labs/gke-progression/README.md index 1acf57d..f5c121c 100644 --- a/labs/gke-progression/README.md +++ b/labs/gke-progression/README.md @@ -1,12 +1,12 @@ # Continuous deployment to Google Kubernetes Engine (GKE) with Cloud Build - - ## Overview -In this lab, you'll learn to set up a continuous delivery pipeline for GKE with Cloud Build. You'll complete the following steps: +In this lab, you'll learn to set up a continuous delivery pipeline for GKE with Cloud Build. This lab highlights how to trigger Cloud Build jobs for different git events as well as a simple pattern for automated canary releases in GKE. + +You'll complete the following steps: * Create the GKE Application * Automate deployments for git branches @@ -16,13 +16,12 @@ In this lab, you'll learn to set up a continuous delivery pipeline for GKE with ## Preparing your environment -1. In Cloud Shell, create environment variables to use throughout this tutorial: +1. Create environment variables to use throughout this tutorial: ```sh export PROJECT_ID=$(gcloud config get-value project) export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') - export ZONE=us-central1-b export CLUSTER=gke-progression-cluster export APP_NAME=myapp @@ -36,8 +35,6 @@ In this lab, you'll learn to set up a continuous delivery pipeline for GKE with - Cloud Build - Container Registry - - ```sh gcloud services enable \ cloudresourcemanager.googleapis.com \ @@ -48,8 +45,7 @@ In this lab, you'll learn to set up a continuous delivery pipeline for GKE with --async ``` - -3. Clone the sample source: +3. Clone the sample source and switch to the lab directory: ```sh git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop.git gke-progression @@ -60,6 +56,8 @@ In this lab, you'll learn to set up a continuous delivery pipeline for GKE with 4. Replace placeholder values in the sample repository with your `PROJECT_ID`: + This command creates instances of the various config files unique to your current environment. + ```sh for template in $(find . -name '*.tmpl'); do envsubst '${PROJECT_ID} ${ZONE} ${CLUSTER} ${APP_NAME}' < ${template} > ${template%.*}; done ``` @@ -84,8 +82,10 @@ In this lab, you'll learn to set up a continuous delivery pipeline for GKE with --zone=${ZONE} ``` - 7. Give Cloud Build rights to your cluster. + + Cloud Build will be deploying the application to your GKE Cluster and will need rights to do so. + ```sh gcloud projects add-iam-policy-binding ${PROJECT_ID} \ --member=serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com \ @@ -107,23 +107,24 @@ In this section, you build and deploy the initial production application that yo 2. Manually deploy to Canary and Production environments: - Create the production and canary deployments and services using the `kubectl apply` commands. - ```console + ```sh kubectl create ns production kubectl apply -f k8s/deployments/prod -n production kubectl apply -f k8s/deployments/canary -n production kubectl apply -f k8s/services -n production ``` + The service deployed here will route traffic to both the canary and prod deployments. + 3. Review number of running pods Confirm that you have four Pods running for the frontend, including three for production traffic and one for canary releases. That means that changes to your canary release will only affect 1 out of 4 (25%) of users. - ```console + ```sh kubectl get pods -n production -l app=$APP_NAME -l role=frontend ``` @@ -155,13 +156,36 @@ In this section, you build and deploy the initial production application that yo Congratulations! You deployed the sample app! Next, you'll set up a pipeline for continuously and deploying your changes. +## Automating deployments for git branches + +In this section you will set up a trigger that will execute a Cloudbuild job on commit of any branch other than `main`. The Cloud Build file used here will automatically create a namespace and deployment for any existing or new branches, allowing developers to preview their code before integration with the main branch. + +1. Set up the trigger: + The key component of this trigger is the use of the `branchName` parameter to match `main` and the `invertRegex` parameter which is set to true and alters the `branchName` pattern to match anything that is not `main`. For your reference you can find the following lines in `build/branch-trigger.json`. + -## Automating deployments for git branches + + ```console + "branchName": "main", + "invertRegex": true + ``` -1. Set up the trigger: + Additionally the last few lines of the Cloud Build file used with this trigger create a namespace named after the branch that triggered the job, then deploys the application and service within the new namespace. For your reference you can find the following lines in `build/branch-cloudbuild.yaml` + + + + + ```console + kubectl get ns ${BRANCH_NAME} || kubectl create ns ${BRANCH_NAME} + kubectl apply --namespace ${BRANCH_NAME} --recursive -f k8s/deployments/dev + kubectl apply --namespace ${BRANCH_NAME} --recursive -f k8s/services + ``` + + + Now that you understand the mechanisms in use, create the trigger with the gcloud command below. ```sh gcloud beta builds triggers create cloud-source-repositories \ --trigger-config build/branch-trigger.json @@ -230,10 +254,9 @@ Congratulations! You deployed the sample app! Next, you'll set up a pipeline for ## Automate deployments for git main branch +Before code is released to production, it's common to release code to a small subset of live traffic before migrating all traffic to the new code base. -When code is released to production, it's common to release code to a small subset of live traffic before migrating all traffic to the new code base. - -In this section, you implement a trigger that is activated when code is committed to the main branch. The trigger deploys the code to a unique canary URL and it routes 10% of all live traffic to the new revision. +In this section, you implement a trigger that is activated when code is committed to the main branch. The trigger deploys the canary deployment which receives 25% of all live traffic to the new revision. 1. Set up the trigger for the main branch: @@ -266,16 +289,15 @@ In this section, you implement a trigger that is activated when code is committe Once the build has completed continue to the next step -5. Review mulitple responses from the server +5. Review multiple responses from the server - Run the following command and note that approxomatly 25% of the responses are showing the new response of Hello World v1.1 + Run the following command and note that approximately 25% of the responses are showing the new response of Hello World v1.1 ```sh while true; do curl -w "\n" http://$PRODUCTION_IP; sleep 1; done ``` - When you're ready to continue pres `Ctrl+c` to exit out of the loop. - + When you're ready to continue press `Ctrl+c` to exit out of the loop. ## Automating deployments for git tags @@ -308,8 +330,7 @@ In this section, you set up a trigger that is activated when you create a tag in [Go to Builds](https://console.cloud.google.com/cloud-build/builds) - -5. Review mulitple responses from the server +5. Review multiple responses from the server Run the following command and note that 100% of the responses are showing the new response of Hello World v1.1 @@ -319,7 +340,6 @@ In this section, you set up a trigger that is activated when you create a tag in while true; do curl -w "\n" http://$PRODUCTION_IP; sleep 1; done ``` - When you're ready to continue pres `Ctrl+c` to exit out of the loop. + When you're ready to continue press `Ctrl+c` to exit out of the loop. - Congratulations! You created CI/CD triggers in Cloud Build for branches and tags to deploy your apps to GKE. \ No newline at end of file From 71a2396818e376a238ee2935318d7df466dea47d Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Mon, 5 Dec 2022 13:15:49 -0600 Subject: [PATCH 46/50] Scheduled builds (#36) --- labs/cloudbuild-scheduled-jobs/README.md | 137 ++++++++++++++++++ .../code-oss-java/Dockerfile | 22 +++ .../code-oss-java/cloudbuild.yaml | 7 + 3 files changed, 166 insertions(+) create mode 100644 labs/cloudbuild-scheduled-jobs/README.md create mode 100644 labs/cloudbuild-scheduled-jobs/code-oss-java/Dockerfile create mode 100644 labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml diff --git a/labs/cloudbuild-scheduled-jobs/README.md b/labs/cloudbuild-scheduled-jobs/README.md new file mode 100644 index 0000000..c0577ab --- /dev/null +++ b/labs/cloudbuild-scheduled-jobs/README.md @@ -0,0 +1,137 @@ +# Cloud Build Scheduled Jobs + +This lab demonstrates how to automate the execution of Cloud Build jobs based on a scheduled frequency. + +This technique is often used to ensure the latest upstream changes are always included in your containers. + +In this example a custom image has been built on top of a base image. To ensure security updates and patches from the base image are always applied to the custom image as well, Cloud Scheduler is used to trigger the build on a regular basis. + + +What you will accomplish +- Create Artifact Registry to Store and Scan your custom image +- Configure Github with Google cloud to store your image configurations +- Create a Cloud Build trigger to automate creation of custom image +- Configure Cloud Scheduler to initiate builds on a regular basis +- Review the results of the processes + +## Setup and Requirements + +### Enable APIs + +```sh +gcloud services enable \ + container.googleapis.com \ + cloudbuild.googleapis.com \ + containerregistry.googleapis.com \ + containerscanning.googleapis.com \ + artifactregistry.googleapis.com \ + cloudscheduler.googleapis.com +``` + + +### Set Environment Variables +The following variables will be used multiple times throughout the tutorial. + + + +```sh +PROJECT_ID=[your project id] +``` + +```sh +GITHUB_USER=[your github name] +``` + + +```sh +PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') +REGION=us-central1 + +``` + +### Artifact Registry Repository +In this lab you will be using Artifact Registry to store and scan your images. Create the repository with the following command. + +```sh +gcloud artifacts repositories create custom-images \ + --repository-format=docker \ + --location=$REGION \ + --description="Docker repository" +``` + +Configure docker to utilize your gcloud credentials when accessing Artifact Registry. + +```sh +gcloud auth configure-docker $REGION-docker.pkg.dev +``` + + +## Git Repository +In practice you will keep the Dockerfile for your custom images in a git repo. The automated process will access that repo during the build process to pull the relevant configs and Dockerfile. + +### Fork the sample repository + +For this tutorial you will fork a sample repo that provides the container definitions used in this lab. + +- [Click this link to fork the repo](https://github.com/GoogleCloudPlatform/software-delivery-workshop/fork) + + +### Connect Cloud Build to GitHub + +Next you will connect that repository to Cloud Build using the built in Github connection capability. Follow the link below to the instructions describing how to complete the process. + +- [Connect the github repo](https://cloud.google.com/build/docs/automating-builds/github/connect-repo-github) + + + +## Cloud Build + +```sh +TRIGGER_NAME=custom-image-trigger + +gcloud beta builds triggers create manual \ + --region=us-central1 \ + --name=${TRIGGER_NAME} \ + --repo=${GITHUB_USER}/software-delivery-workshop \ + --repo-type=GITHUB \ + --branch=main \ + --build-config=labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml \ + --substitutions=_REGION=us-central1,_AR_REPO_NAME=custom-images,_AR_IMAGE_NAME=code-oss-java,_IMAGE_DIR=labs/cloudbuild-scheduled-jobs/code-oss-java + +TRIGGER_ID=$(gcloud beta builds triggers list --region=us-central1 \ + --filter=name="${TRIGGER_NAME}" --format="value(id)") + +``` + +## Cloud Scheduler + + +```sh +gcloud scheduler jobs create http run-build \ + --schedule='3 * * * *' \ + --uri=https://cloudbuild.googleapis.com/v1/projects/${PROJECT_ID}/locations/us-central1/triggers/${TRIGGER_ID}:run \ + --location=us-central1 \ + --oauth-service-account-email=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com \ + --oauth-token-scope=https://www.googleapis.com/auth/cloud-platform +``` + + +Test the initial functionality by running the job [manually from the console](https://console.cloud.google.com/cloudscheduler) +- On the Cloud Scheduler page find the entry you just created called run-build +- Click the three dots for that row under the Actions column +- Click Force a job run to test the system manually + + +## Review The Results + +### Vulnerabilities +To see the vulnerabilities in an image: +- Open the [Repositories page](https://console.cloud.google.com/artifacts) +- In the repositories list, click a repository. +- Click an image name. +- Vulnerability totals for each image digest are displayed in the Vulnerabilities column. + +To view the list of vulnerabilities for an image: +- click the link in the Vulnerabilities column +- The vulnerability list shows the severity, availability of a fix, and the name of the package that contains the vulnerability. + diff --git a/labs/cloudbuild-scheduled-jobs/code-oss-java/Dockerfile b/labs/cloudbuild-scheduled-jobs/code-oss-java/Dockerfile new file mode 100644 index 0000000..87f0fce --- /dev/null +++ b/labs/cloudbuild-scheduled-jobs/code-oss-java/Dockerfile @@ -0,0 +1,22 @@ + +FROM us-central1-docker.pkg.dev/cloud-workstations-images/predefined/code-oss:latest + +RUN wget https://open-vsx.org/api/vscjava/vscode-java-debug/0.40.1/file/vscjava.vscode-java-debug-0.40.1.vsix && \ +unzip vscjava.vscode-java-debug-0.40.1.vsix "extension/*" &&\ +mv extension /opt/code-oss/extensions/java-debug + +RUN wget https://open-vsx.org/api/vscjava/vscode-java-dependency/0.19.1/file/vscjava.vscode-java-dependency-0.19.1.vsix && \ +unzip vscjava.vscode-java-dependency-0.19.1.vsix "extension/*" &&\ +mv extension /opt/code-oss/extensions/java-dependency + +RUN wget https://open-vsx.org/api/redhat/java/1.6.0/file/redhat.java-1.6.0.vsix && \ +unzip redhat.java-1.6.0.vsix "extension/*" &&\ +mv extension /opt/code-oss/extensions/redhat-java + +RUN wget https://open-vsx.org/api/vscjava/vscode-maven/0.35.2/file/vscjava.vscode-maven-0.35.2.vsix && \ +unzip vscjava.vscode-maven-0.35.2.vsix "extension/*" &&\ +mv extension /opt/code-oss/extensions/java-maven + +RUN wget https://open-vsx.org/api/vscjava/vscode-java-test/0.35.0/file/vscjava.vscode-java-test-0.35.0.vsix && \ +unzip vscjava.vscode-java-test-0.35.0.vsix "extension/*" &&\ +mv extension /opt/code-oss/extensions/java-test \ No newline at end of file diff --git a/labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml b/labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml new file mode 100644 index 0000000..ef9d1b1 --- /dev/null +++ b/labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml @@ -0,0 +1,7 @@ +steps: + +# build +- id: "build" + name: 'gcr.io/cloud-builders/docker' + args: ['build', '-t', '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_AR_REPO_NAME}/${_AR_IMAGE_NAME}', './${_IMAGE_DIR}'] + waitFor: ['-'] From 287f9d29d2878348ba77bfee86a1f0cf5904bc55 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 8 Dec 2022 10:05:16 -0500 Subject: [PATCH 47/50] Pushing image to Artifact Registry (#37) --- labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml b/labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml index ef9d1b1..503ff0c 100644 --- a/labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml +++ b/labs/cloudbuild-scheduled-jobs/code-oss-java/cloudbuild.yaml @@ -5,3 +5,5 @@ steps: name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_AR_REPO_NAME}/${_AR_IMAGE_NAME}', './${_IMAGE_DIR}'] waitFor: ['-'] +images: +- '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_AR_REPO_NAME}/${_AR_IMAGE_NAME}' From f837a3d189597073665ac986add6946ad1f384b6 Mon Sep 17 00:00:00 2001 From: Shobhit Gupta <43795024+gushob21@users.noreply.github.com> Date: Thu, 15 Dec 2022 13:27:27 -0500 Subject: [PATCH 48/50] Cloud deploy lab - cluster name change and fixing github repo link (#41) * change cluster name from canary to staging and fixing repo link --- labs/cloud-deploy/README.md | 38 +++++++++---------- .../k8s/{canary => staging}/deployment.yaml | 2 +- .../{canary => staging}/kustomization.yaml | 0 labs/cloud-deploy/skaffold.yaml | 4 +- 4 files changed, 22 insertions(+), 22 deletions(-) rename labs/cloud-deploy/k8s/{canary => staging}/deployment.yaml (97%) rename labs/cloud-deploy/k8s/{canary => staging}/kustomization.yaml (100%) diff --git a/labs/cloud-deploy/README.md b/labs/cloud-deploy/README.md index fc25de6..a1b175b 100644 --- a/labs/cloud-deploy/README.md +++ b/labs/cloud-deploy/README.md @@ -3,9 +3,9 @@ ## Objectives -In this tutorial you will create three GKE clusters named preview, canary and prod. Then, create a Cloud Deploy target corresponding to each cluster and a Cloud Deploy pipeline that will define the sequence of steps to perform deployment in those targets. +In this tutorial you will create three GKE clusters named preview, staging and prod. Then, create a Cloud Deploy target corresponding to each cluster and a Cloud Deploy pipeline that will define the sequence of steps to perform deployment in those targets. -The deployment flow will be triggered by a cloudbuild pipeline that will create Cloud Deploy release and perform the deployment in the preview cluster. After you have verified that the deployment in preview was successful and working as expected, you will manually promote the release in the canary cluster. Promotion of the release in the prod cluster will require approval, you will approve the prod pipeline in Cloud Deploy UI and finally promote it. +The deployment flow will be triggered by a cloudbuild pipeline that will create Cloud Deploy release and perform the deployment in the preview cluster. After you have verified that the deployment in preview was successful and working as expected, you will manually promote the release in the staging cluster. Promotion of the release in the prod cluster will require approval, you will approve the prod pipeline in Cloud Deploy UI and finally promote it. The objectives of this tutorial can be broken down into the following steps: @@ -47,7 +47,7 @@ gcloud config set deploy/region us-central1 2. **Clone Repo** ``` -git clone https://github.com/gushob21/software-delivery-workshop +git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop cd software-delivery-workshop/labs/cloud-deploy/ cloudshell workspace . rm -rf deploy && mkdir deploy @@ -77,7 +77,7 @@ cloudresourcemanager.googleapis.com \ ``` gcloud container clusters create preview \ --zone=us-central1-a --async - gcloud container clusters create canary \ + gcloud container clusters create staging \ --zone=us-central1-b --async gcloud container clusters create prod \ --zone=us-central1-c @@ -103,19 +103,19 @@ EOF As you noticed, the "kind" tag is "Target". It allows us to add some metadata to the target, a description and finally the GKE cluster where the deployment is supposed to happen for this target. -2. **Create a file in the deploy directory named canary.yaml with the following command in cloudshell:** +2. **Create a file in the deploy directory named staging.yaml with the following command in cloudshell:** ``` -cat <deploy/canary.yaml +cat <deploy/staging.yaml apiVersion: deploy.cloud.google.com/v1beta1 kind: Target metadata: - name: canary + name: staging annotations: {} labels: {} -description: Target for canary environment +description: Target for staging environment gke: - cluster: projects/$PROJECT_ID/locations/us-central1-b/clusters/canary + cluster: projects/$PROJECT_ID/locations/us-central1-b/clusters/staging EOF ``` @@ -141,9 +141,9 @@ Notice the tag requireApproval which is set to true. This will not allow promoti 4. **Create the Deploy Targets** ``` - gcloud config set deploy/region us-central1 +gcloud config set deploy/region us-central1 gcloud beta deploy apply --file deploy/preview.yaml -gcloud beta deploy apply --file deploy/canary.yaml +gcloud beta deploy apply --file deploy/staging.yaml gcloud beta deploy apply --file deploy/prod.yaml ``` @@ -169,9 +169,9 @@ serialPipeline: - targetId: preview profiles: - preview - - targetId: canary + - targetId: staging profiles: - - canary + - staging - targetId: prod profiles: - prod @@ -179,7 +179,7 @@ EOF ``` ** ** -** **As you noticed, the "kind" tag is "DeliveryPipeline". It lets you define the metadata for the pipeline, a description and an order of deployment into various targets via serialPipeline tag. +As you noticed, the "kind" tag is "DeliveryPipeline". It lets you define the metadata for the pipeline, a description and an order of deployment into various targets via serialPipeline tag. `serialPipeline` tag contains a tag named stages which is a list of all targets to which this delivery pipeline is configured to deploy. @@ -208,7 +208,7 @@ skaffold build \ ## Release Phase -At the end of your CICD process, typically when the code is Tagged for production, you will initiate the release process by calling the `cloud deploy release` command. Later once the deployment has been validated and approved you'll move the release through the various target environments by promoting and approving the action through automated processes or manual approvals. +At the end of your CICD process, typically when the code is Tagged for production, you will initiate the release process by calling the `cloud deploy release` command. Later, once the deployment has been validated and approved you'll move the release through the various target environments by promoting and approving the action through automated processes or manual approvals. @@ -255,7 +255,7 @@ This will take you to a new page which shows the message "Hello World!" ### Promoting a release -Now that your release is deployed to the first target (preview) in the pipeline, you can promote it to the next target (canary). Run the following command to begin the process. +Now that your release is deployed to the first target (preview) in the pipeline, you can promote it to the next target (staging). Run the following command to begin the process. ``` gcloud beta deploy releases promote \ @@ -267,12 +267,12 @@ gcloud beta deploy releases promote \ ### Review the release promotion 1. Go to the [sample-app pipeline in the Google Cloud console](https://console.cloud.google.com/deploy/delivery-pipelines/us-central1/sample-app) -2. Confirm a green outline on the left side of the Canary box which means that the release has been deployed to that environment. +2. Confirm a green outline on the left side of the staging box which means that the release has been deployed to that environment. 3. Verify the application is deployed correctly by creating a tunnel to it ``` -gcloud container clusters get-credentials canary --zone us-central1-b && kubectl port-forward --namespace default $(kubectl get pod --namespace default --selector="app=cloud-deploy-tutorial" --output jsonpath='{.items[0].metadata.name}') 8080:8080 +gcloud container clusters get-credentials staging --zone us-central1-b && kubectl port-forward --namespace default $(kubectl get pod --namespace default --selector="app=cloud-deploy-tutorial" --output jsonpath='{.items[0].metadata.name}') 8080:8080 ``` 4. Click on the web preview icon in the upper right of the screen. @@ -286,7 +286,7 @@ This will take you to a new page which shows the message "Hello World!" Remember when we created prod target via prod.yaml, we specified the tag requireApproval as true. This will force a requirement of approval for promotion in prod. -1. Promote the canary release to production with the following command +1. Promote the staging release to production with the following command ``` gcloud beta deploy releases promote \ diff --git a/labs/cloud-deploy/k8s/canary/deployment.yaml b/labs/cloud-deploy/k8s/staging/deployment.yaml similarity index 97% rename from labs/cloud-deploy/k8s/canary/deployment.yaml rename to labs/cloud-deploy/k8s/staging/deployment.yaml index dec9407..7aa6ad7 100644 --- a/labs/cloud-deploy/k8s/canary/deployment.yaml +++ b/labs/cloud-deploy/k8s/staging/deployment.yaml @@ -32,4 +32,4 @@ spec: memory: 256Mi env: - name: ENVIRONMENT - value: canary + value: staging diff --git a/labs/cloud-deploy/k8s/canary/kustomization.yaml b/labs/cloud-deploy/k8s/staging/kustomization.yaml similarity index 100% rename from labs/cloud-deploy/k8s/canary/kustomization.yaml rename to labs/cloud-deploy/k8s/staging/kustomization.yaml diff --git a/labs/cloud-deploy/skaffold.yaml b/labs/cloud-deploy/skaffold.yaml index 97354e8..43314ba 100644 --- a/labs/cloud-deploy/skaffold.yaml +++ b/labs/cloud-deploy/skaffold.yaml @@ -30,10 +30,10 @@ profiles: kustomize: path: k8s/preview -- name: canary +- name: staging deploy: kustomize: - path: k8s/canary + path: k8s/staging - name: prod deploy: From 74b22c4389d72eba4265595311a9a32e219c4381 Mon Sep 17 00:00:00 2001 From: Christopher Grant Date: Wed, 19 Apr 2023 12:29:02 -0400 Subject: [PATCH 49/50] Updating lab to use GitHub (#43) * Updating lab for Github --- labs/cloudrun-progression/README.md | 807 ++++++++++-------- .../branch-cloudbuild.yaml | 2 +- .../branch-trigger.json-tmpl | 11 - ...r-cloudbuild.yaml => main-cloudbuild.yaml} | 2 +- .../master-trigger.json-tmpl | 11 - .../tag-trigger.json-tmpl | 11 - 6 files changed, 434 insertions(+), 410 deletions(-) delete mode 100644 labs/cloudrun-progression/branch-trigger.json-tmpl rename labs/cloudrun-progression/{master-cloudbuild.yaml => main-cloudbuild.yaml} (98%) delete mode 100644 labs/cloudrun-progression/master-trigger.json-tmpl delete mode 100644 labs/cloudrun-progression/tag-trigger.json-tmpl diff --git a/labs/cloudrun-progression/README.md b/labs/cloudrun-progression/README.md index 3f71cea..40cbf57 100644 --- a/labs/cloudrun-progression/README.md +++ b/labs/cloudrun-progression/README.md @@ -1,405 +1,462 @@ # Canary Deployments with Cloud Run and Cloud Build -This document shows you how to implement a deployment pipeline for -Cloud Run that implements progression of code from developer -branches to production with automated canary testing and percentage-based -traffic management. It is intended for platform administrators who are -responsible for creating and managing CI/CD pipelines to -GKE. This document assumes that you have a basic -understanding of Git, Cloud Run, and CI/CD pipeline concepts. - -Cloud Run lets you deploy and run your applications with little -overhead or effort. Many organizations use robust release pipelines to move code -into production. Cloud Run provides unique traffic management -capabilities that let you implement advanced release management techniques with -little effort. +## Overview + +This document shows you how to implement a deployment pipeline for Cloud Run that implements progression of code from developer branches to production with automated canary testing and percentage based traffic management. It is intended for platform administrators who are responsible for creating and managing CI/CD pipelines. This document assumes that you have a basic understanding of Git, Cloud Run, and CI/CD pipeline concepts.  + +Cloud Run lets you deploy and run your applications with little overhead or effort. Many organizations use robust release pipelines to move code into production. Cloud Run provides unique traffic management capabilities that let you implement advanced release management techniques with little effort. + +### Objectives  - Create your Cloud Run service -- Enable a developer branch +- Enable developer branch - Implement canary testing -- Roll out safely to production - - -## Preparing your environment - -1. In Cloud Shell, create environment variables to use throughout - this tutorial: - - ```sh - export PROJECT_ID=$(gcloud config get-value project) - export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') - ``` -2. Enable the following APIs: - - - Resource Manager - - GKE - - Cloud Source Repositories - - Cloud Build - - Container Registry - - Cloud Run - - ```sh - gcloud services enable \ - cloudresourcemanager.googleapis.com \ - container.googleapis.com \ - sourcerepo.googleapis.com \ - cloudbuild.googleapis.com \ - containerregistry.googleapis.com \ - run.googleapis.com - ``` -3. Grant the Cloud Run Admin role (`roles/run.admin`) to - the Cloud Build service account: - - ```sh - gcloud projects add-iam-policy-binding $PROJECT_ID \ - --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ - --role=roles/run.admin - ``` -4. Grant the IAM Service Account User role - (`roles/iam.serviceAccountUser`) to the Cloud Build service - account for the Cloud Run runtime service account: - - ```sh - gcloud iam service-accounts add-iam-policy-binding \ - $PROJECT_NUMBER-compute@developer.gserviceaccount.com \ - --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ - --role=roles/iam.serviceAccountUser - ``` -5. If you haven't used Git in Cloud Shell previously, set the - `user.name` and `user.email` values that you want to use: - - ```sh - git config --global user.email "YOUR_EMAIL_ADDRESS" - git config --global user.name "YOUR_USERNAME" - ``` -6. Clone and prepare the sample repository: - - ```sh - git clone https://github.com/GoogleCloudPlatform/software-delivery-workshop cloudrun-progression - - cd cloudrun-progression/labs/cloudrun-progression - rm -rf ../../.git - ``` -7. Replace placeholder values in the sample repository with your `PROJECT_ID`: - - ```sh - sed "s/PROJECT/${PROJECT_ID}/g" branch-trigger.json-tmpl > branch-trigger.json - sed "s/PROJECT/${PROJECT_ID}/g" master-trigger.json-tmpl > master-trigger.json - sed "s/PROJECT/${PROJECT_ID}/g" tag-trigger.json-tmpl > tag-trigger.json - ``` -8. Store the code from the sample repository in CSR: - - ```sh - gcloud source repos create cloudrun-progression - git init - git config credential.helper gcloud.sh - git remote add gcp https://source.developers.google.com/p/$PROJECT_ID/r/cloudrun-progression - git branch -m master - git add . && git commit -m "initial commit" - git push gcp master - ``` - -## Creating your Cloud Run service - -In this section, you build and deploy the initial production application that -you use throughout this tutorial. - -1. In Cloud Shell, build and deploy the application, including a - service that requires authentication. To make a public service, use the - `--allow-unauthenticated` flag as described in - [Allowing public (unauthenticated) access](/run/docs/authenticating/public). - - ```sh - gcloud builds submit --tag gcr.io/$PROJECT_ID/hello-cloudrun - gcloud run deploy hello-cloudrun \ - --image gcr.io/$PROJECT_ID/hello-cloudrun \ - --platform managed \ - --region us-central1 \ - --tag=prod -q - ``` - - The output looks like the following: - - ```none - Deploying container to Cloud Run service [hello-cloudrun] in project [sdw-mvp6] region [us-central1] - ✓ Deploying new service... Done. - ✓ Creating Revision... - ✓ Routing traffic... - Done. - Service [hello-cloudrun] revision [hello-cloudrun-00001-tar] has been deployed and is serving 100 percent of traffic. - Service URL: https://hello-cloudrun-apwaaxltma-uc.a.run.app - The revision can be reached directly at https://prod---hello-cloudrun-apwaaxltma-uc.a.run.app - ``` - - The output includes the service URL and a unique URL for the revision. Your - values will differ slightly from what's indicated here. -2. After the deployment is complete, view the newly deployed service on - the - [Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - in the Console. - - [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - -3. In Cloud Shell, view the authenticated service response: - - ```sh - PROD_URL=$(gcloud run services describe hello-cloudrun \ - --platform managed \ - --region us-central1 \ - --format=json | jq \ - --raw-output ".status.url") - echo $PROD_URL - curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $PROD_URL - ``` - -## Enabling dynamic developer deployments - -In this section, you enable a unique URL for development branches in Git. Each -branch is represented by a URL that's identified by the branch name. Commits to -the branch trigger a deployment, and the updates are accessible at that same -URL. +- Rollout safely to production -1. In Cloud Shell, set up the trigger: +### Costs + +This tutorial uses billable components of Google Cloud, including: + +- [Cloud Run](https://cloud.google.com/run/pricing) +- [Cloud Build](https://cloud.google.com/build/pricing) + +Use the [Pricing Calculator](https://cloud.google.com/products/calculator) to generate a cost estimate based on your projected usage. + +### Before you begin + +1. Create a Google Cloud project. + +[GO TO THE MANAGE RESOURCES PAGE](https://console.cloud.google.com/cloud-resource-manager) + +2. Enable billing for your project. + +[ENABLE BILLING](https://support.google.com/cloud/answer/6293499#enable-billing) + +3. In Cloud Console, go to [Cloud Shell](https://cloud.google.com/shell/docs/how-cloud-shell-works) to execute the commands listed in this tutorial. + +[GO TO CLOUD](https://console.cloud.google.com/?cloudshell=true&_ga=2.130021388.-1258454239.1533315939&_gac=1.201996835.1554234100.CKPetZmVsuECFUfNDQodJ-UGUQ) SHELL + +At the bottom of the Cloud Console, a [Cloud Shell](https://cloud.google.com/shell/docs/how-cloud-shell-works) session opens and displays a command-line prompt. Cloud Shell is a shell environment with the Cloud SDK already installed, including the [gcloud](https://cloud.google.com/sdk/gcloud) command-line tool, and with values already set for your current project. It can take a few seconds for the session to initialize. + +When you finish this tutorial, you can avoid continued billing by deleting the resources you created. See [Cleaning up](https://docs.google.com/document/d/1yCbt_zPaWJ7u59xWgd1KuxMlNr1juBjjYo7WASIQeL0/edit?resourcekey=0-a--Q0cYLuCdecVW6PKWmJg#heading=h.mlrdlgcohh7k) for more detail. + + +## Preparing Your Environment + +### Set Project Variables +1. In Cloud Shell, create environment variables to use throughout this tutorial: + +```sh +export PROJECT_ID=$(gcloud config get-value project) +export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') +``` + + +### Enable APIs + +Enable the following APIs on your project + +- Cloud Resource Manager +- Cloud Build +- Container Registry +- Cloud Run + +```sh +gcloud services enable \ +cloudresourcemanager.googleapis.com \ +container.googleapis.com \ +secretmanager.googleapis.com \ +cloudbuild.googleapis.com \ +containerregistry.googleapis.com \ +run.googleapis.com +``` + + +### Grant Rights +Grant the Cloud Run Admin role (roles/run.admin) to the Cloud Build service account: + +```sh +gcloud projects add-iam-policy-binding $PROJECT_ID \ +--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ +--role=roles/run.admin +``` + +Grant the IAM Service Account User role (`roles/iam.serviceAccountUser`) to the Cloud Build service account for the Cloud Run runtime service account: + +```sh +gcloud iam service-accounts add-iam-policy-binding \ +$PROJECT_NUMBER-compute@developer.gserviceaccount.com \ +--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \ +--role=roles/iam.serviceAccountUser +``` + + + + +### Set Git values + +If you haven't used Git in Cloud Shell previously, set the `user.name` and `user.email` values that you want to use: + +```sh +git config --global user.email "[YOUR_EMAIL_ADDRESS]" +git config --global user.name "[YOUR_USERNAME]" +git config --global credential.helper store +``` + +If you're using MFA with GitHub, create a personal access token and use it as your password when interacting with GitHub through the command-line +- Follow [this link to create an access token](https://github.com/settings/tokens/new?scopes=repo%2Cread%3Auser%2Cread%3Aorg%2Cuser%3Aemail%2Cwrite&description=Cloud%20Run%20Tutorial) +- Leave the tab open + +Store your GitHub ID in an environment variable for easier access + +```sh +export GH_USER= +``` + +### Fork the project repo + +First fork the sample repo into your GitHub account [through the GitHub UI](https://github.com/GoogleCloudPlatform/software-delivery-workshop/fork). + +### Clone The Project Repo + +Clone and prepare the sample repository: + +```sh +git clone https://github.com/$GH_USER/software-delivery-workshop.git cloudrun-progression + +cd cloudrun-progression/labs/cloudrun-progression +``` + + + + +## Connecting Your Git Repo + +Cloud Build enables you to create and manage connections to source code repositories using the Google Cloud console. You can [create and manage connections](https://cloud.google.com/build/docs/repositories) using Cloud Build repositories (1st gen) or Cloud Build repositories (2nd gen). For this tutorial you will utilize Cloud Build repositories (2nd gen) to connect your GitHub repo and access a the sample source repo. + +### Grant Required Permissions + +To connect your GitHub host, grant the Cloud Build Connection Admin (`roles/cloudbuild.connectionAdmin`) role to your user account +```sh +PN=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)") +CLOUD_BUILD_SERVICE_AGENT="service-${PN}@gcp-sa-cloudbuild.iam.gserviceaccount.com" +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:${CLOUD_BUILD_SERVICE_AGENT}" \ + --role="roles/secretmanager.admin" +``` + +### Create the Host connection + +Configure the Cloud Build Repository connection by running the command below. + +```sh +gcloud alpha builds connections create github $GH_USER --region=us-central1 +``` + +Click the link provided in the output and follow the onscreen instructions to complete the connection. + + +Verify the installation of your GitHub connection by running the following command: + +``` +gcloud alpha builds connections describe $GH_USER --region=us-central1 +``` + +### Link the specific repository + +Using the host connection you just configured, link in the sample repo you forked + +```sh + gcloud alpha builds repositories create cloudrun-progression \ + --remote-uri=https://github.com/$GH_USER/software-delivery-workshop.git \ + --connection=$GH_USER \ + --region=us-central1 +``` - ```sh - gcloud beta builds triggers create cloud-source-repositories \ - --trigger-config branch-trigger.json - ``` -2. To review the trigger, go to the - [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) - in the Console. - - [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) - -3. In Cloud Shell, create a new branch: - - ```sh - git checkout -b new-feature-1 - ``` -4. Open the sample application in the Cloud Shell: - - ```sh - edit app.py - ``` -5. In the sample application, modify the code to indicate v1.1 instead of v1.0: - - ```py - @app.route('/') - def hello_world(): - return 'Hello World v1.1' - ``` -6. To return to your terminal, click **Open Terminal**. -7. In Cloud Shell, commit the change and push to the remote - repository: - - ```sh - git add . && git commit -m "updated" && git push gcp new-feature-1 - ``` -8. To review the build in progress, go to the - [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) - in the Console. - - [Go to Builds](https://console.cloud.google.com/cloud-build/builds) - -9. After the build completes, to review the new revision, go to the - [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - in the Console. - - [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - -10. In Cloud Shell, get the unique URL for this branch: - - ```sh - BRANCH_URL=$(gcloud run services describe hello-cloudrun \ - --platform managed \ - --region us-central1 \ - --format=json | jq \ - --raw-output ".status.traffic[] | select (.tag==\"new-feature-1\")|.url") - echo $BRANCH_URL - ``` +### Set Repository Name variable + +Store the Repository name for later use + +```sh +export REPO_NAME=projects/$PROJECT_ID/locations/us-central1/connections/$GH_USER/repositories/cloudrun-progression +``` + + +## Deploying Your Cloud Run Service + +In this section, you build and deploy the initial production application that you use throughout this tutorial. + +### Deploy the service +1. In Cloud Shell, build and deploy the application, including a service that requires authentication. To make a public service use the --allow-unauthenticated flag as [noted in the documentation](https://cloud.google.com/run/docs/authenticating/public). + +```sh +gcloud builds submit --tag gcr.io/$PROJECT_ID/hello-cloudrun + +gcloud run deploy hello-cloudrun \ +--image gcr.io/$PROJECT_ID/hello-cloudrun \ +--platform managed \ +--region us-central1 \ +--tag=prod -q +``` + + + +The output looks like the following: + + ``` + Deploying container to Cloud Run service [hello-cloudrun] in project [sdw-mvp6] region [us-central1] +✓ Deploying new service... Done.                                                            +  ✓ Creating Revision... +  ✓ Routing traffic... +Done. +Service [hello-cloudrun] revision [hello-cloudrun-00001-tar] has been deployed and is serving 100 percent of traffic. +Service URL: https://hello-cloudrun-apwaaxltma-uc.a.run.app +The revision can be reached directly at https://prod---hello-cloudrun-apwaaxltma-uc.a.run.app +``` + +The output includes the service URL and a unique URL for the revision. Your values will differ slightly from what's indicated here.  + +### Validate the deploy +2. After the deployment is complete, view the newly deployed service on the [Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) in the Cloud Console. + +4. In Cloud Shell, view the authenticated service response: + + ```sh +PROD_URL=$(gcloud run services describe hello-cloudrun --platform managed --region us-central1 --format=json | jq --raw-output ".status.url") + +echo $PROD_URL + +curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $PROD_URL +``` + + +## Enabling Branch Deployments + +### Setup Branch Trigger + +In this section, you enable developers with a unique URL for development branches in Git. Each branch is represented by a URL identified by the branch name. Commits to the branch trigger a deployment, and the updates are accessible at that same URL. + +1. In Cloud Shell, set up the trigger: +```sh +gcloud alpha builds triggers create github \ +--name=branchtrigger \ +--repository=$REPO_NAME \ +--branch-pattern='[^(?!.*main)].*' \ +--build-config=labs/cloudrun-progression/branch-cloudbuild.yaml \ +--region=us-central1 +``` + +2. To review the trigger, go to the [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers;region=us-central1) in the Cloud Console: + +### Create changes on a branch +1. In Cloud Shell, create a new branch: +```sh +git checkout -b new-feature-1 +``` + +4. Open the sample application using your favorite editor or using the Cloud Shell IDE: +```sh +edit app.py +``` + +5. In the sample application, modify line 24 to indicate v1.1 instead of v1.0: +```python +@app.route('/') + +def hello_world(): +    return 'Hello World v1.1' + +``` + +6. To return to your terminal, click Open Terminal. + +### Execute the branch trigger +1. In Cloud Shell, commit the change and push to the remote repository: + +```sh +git add . && git commit -m "updated" && git push origin new-feature-1 +``` + +> NOTE: Use the personal access token you created earlier for the password if you have 2FA enabled on GitHub + +1. To review the build in progress, go to the [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) in the Cloud Console. +2. After the build completes, to review the new revision, go to the [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) in the Cloud Console: +3. In Cloud Shell, get the unique URL for this branch: +```sh +BRANCH_URL=$(gcloud run services describe hello-cloudrun --platform managed --region us-central1 --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"new-feature-1\")|.url") + +echo $BRANCH_URL +``` + 11. Access the authenticated URL: +```sh +curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $BRANCH_URL +``` + +The updated response output looks like the following: - ```sh - curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $BRANCH_URL - ``` - The updated response output looks like the following: +``` +Hello World v1.1 +``` + - ```none - Hello World v1.1 - ``` -## Automating canary testing +# Automating Canary Testing -When code is released to production, it's common to release code to a small -subset of live traffic before migrating all traffic to the new code base. +When code is released to production, it's common to release code to a small subset of live traffic before migrating all traffic to the new code base.  -In this section, you implement a trigger that is activated when code is -committed to the main branch. The trigger deploys the code to a unique canary -URL and it routes 10% of all live traffic to the new revision. +In this section, you implement a trigger that is activated when code is committed to the main branch. The trigger deploys the code to a unique canary URL and it routes 10% of all live traffic to the new revision.  1. In Cloud Shell, set up the branch trigger: +```sh +gcloud alpha builds triggers create github \ + --name=maintrigger \ + --repository=$REPO_NAME \ + --branch-pattern=main \ + --build-config=labs/cloudrun-progression/main-cloudbuild.yaml \ + --region=us-central1 +``` + +2. To review the new trigger, go to the [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) in the Cloud Console. +3. In Cloud Shell, merge the branch to the main line and push to the remote repository: +```sh +git checkout main +git merge new-feature-1 +git push origin main +``` + + +4. To review the build in progress, go to the [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) in the Cloud Console. +5. After the build is complete, to review the new revision, go to the [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) in the Cloud Console. Note that 90% of the traffic is routed to prod, 10% to canary, and 0% to the branch revisions. + + + +Review the key lines of `main-cloudbuild.yaml` that implement the logic for the canary deploy. + +Lines 39-45 deploy the new revision and use the tag flag to route traffic from the unique canary URL: +``` +gcloud run deploy ${_SERVICE_NAME} \ +--platform managed \ +--region ${_REGION} \ +--image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ +--tag=canary \ +--no-traffic +``` + + +Line 61 adds a static tag to the revision that notes the Git short SHA of the deployment: +``` +gcloud beta run services update-traffic  ${_SERVICE_NAME} --update-tags=sha-$SHORT_SHA=$${CANARY}  --platform managed  --region ${_REGION} +``` + + Line 62 updates the traffic to route 90% to production and 10% to canary: +``` +gcloud run services update-traffic  ${_SERVICE_NAME} --to-revisions=$${PROD}=90,$${CANARY}=10  --platform managed  --region ${_REGION} +``` + + +1. In Cloud Shell, get the unique URL for the canary revision: + +```sh +CANARY_URL=$(gcloud run services describe hello-cloudrun --platform managed --region us-central1 --format=json | jq --raw-output ".status.traffic[] | select (.tag==\"canary\")|.url") + +echo $CANARY_URL +``` - ```sh - gcloud beta builds triggers create cloud-source-repositories \ - --trigger-config master-trigger.json - ``` -2. To review the new trigger, go to the - [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) - in the Console. - - [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) - -3. In Cloud Shell, merge the branch to the main line and push to - the remote repository: - - ```sh - git checkout master - git merge new-feature-1 - git push gcp master - ``` -4. To review the build in progress, go to the - [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) - in the Console. - - [Go to Builds](https://console.cloud.google.com/cloud-build/builds) - -5. After the build is complete, to review the new revision, go to the - [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - in the Console. - - [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - - Now 90% of the traffic is routed to `prod`, - 10% to `canary`, and 0% to the branch revisions. - - -6. Review the lines of `master-cloudbuild.yaml` that implement the logic for - the canary deployment. - - The following lines deploy the new revision and use the `tag` flag to route - traffic from the unique canary URL: - - ```sh - gcloud run deploy ${_SERVICE_NAME} \ - --platform managed \ - --region ${_REGION} \ - --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ - --tag=canary \ - --no-traffic - ``` - The following line adds a static tag to the revision that notes the Git - short SHA of the deployment: - - ```sh - gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=sha-$SHORT_SHA=$${CANARY} --platform managed --region ${_REGION} - ``` - The following line updates the traffic to route 90% to production and 10% to - canary: - - ```sh - gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=$${PROD}=90,$${CANARY}=10 --platform managed --region ${_REGION} - ``` -7. In Cloud Shell, get the unique URL for the canary revision: - - ```sh - CANARY_URL=$(gcloud run services describe hello-cloudrun \ - --platform managed \ - --region us-central1 \ - --format=json | jq \ - --raw-output ".status.traffic[] | select (.tag==\"canary\")|.url") - echo $CANARY_URL - ``` 8. Review the canary endpoint directly: - ```sh - curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $CANARY_URL - ``` -9. To see percentage-based responses, make a series of requests: +```sh +curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $CANARY_URL +``` - ```sh - LIVE_URL=$(gcloud run services describe hello-cloudrun \ - --platform managed \ - --region us-central1 \ - --format=json | jq \ - --raw-output ".status.url") +9. To see percentage-based responses, make a series of requests: +```sh +LIVE_URL=$(gcloud run services describe hello-cloudrun --platform managed --region us-central1 --format=json | jq --raw-output ".status.url") +for i in {0..20};do + curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $LIVE_URL; echo \n +done +``` - for i in {0..20};do - curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $LIVE_URL; echo \n - done - ``` -## Releasing to production +# Releasing to Production -After the canary deployment is validated with a small subset of traffic, you -release the deployment to the remainder of the live traffic. +After the canary deployment is validated with a small subset of traffic, you release the deployment to the remainder of the live traffic.  -In this section, you set up a trigger that is activated when you create a tag -in the repository. The trigger migrates 100% of traffic to the already deployed -revision based on the commit SHA of the tag. Using the commit SHA ensures the -revision validated with canary traffic is the revision used for the remainder of -production traffic. +In this section, you set up a trigger that is activated when you create a tag in the repository. The trigger migrates 100% of traffic to the already deployed revision based on the commit SHA of the tag. Using the commit sha ensures the revision validated with canary traffic is the revision utilized for the remainder of production traffic.  1. In Cloud Shell, set up the tag trigger: +```sh +gcloud alpha builds triggers create github \ + --name=tagtrigger \ + --repository=$REPO_NAME \ + --tag-pattern=. \ + --build-config=labs/cloudrun-progression/tag-cloudbuild.yaml \ + --region=us-central1 +``` + +2. To review the new trigger, go to the [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) in the Cloud Console. +3. In Cloud Shell, create a new tag and push to the remote repository: +```sh +git tag 1.1 +git push origin 1.1 +``` + +4. To review the build in progress, go to the [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) in the Cloud Console. - ```sh - gcloud beta builds triggers create cloud-source-repositories \ - --trigger-config tag-trigger.json - ``` -2. To review the new trigger, go to the - [Cloud Build Triggers page](https://console.cloud.google.com/cloud-build/triggers) - in the Console. +5. After the build is complete, to review the new revision, go to the [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) in the Cloud Console. Note that the revision is updated to indicate the prod tag and it is serving 100% of live traffic. - [Go to Triggers](https://console.cloud.google.com/cloud-build/triggers) -3. In Cloud Shell, create a new tag and push to the remote repository: - ```sh - git tag 1.1 - git push gcp 1.1 - ``` -4. To review the build in progress, go to the - [Cloud Build Builds page](https://console.cloud.google.com/cloud-build/builds) - in the Console. - - [Go to Builds](https://console.cloud.google.com/cloud-build/builds) - -5. After the build is complete, to review the new revision, go to the - [Cloud Run Revisions page](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - in the Console. - - [Go to Revisions](https://console.cloud.google.com/run/detail/us-central1/hello-cloudrun/revisions) - - The revision is updated to indicate the - `prod` tag and it is serving 100% of live traffic. - - -6. In Cloud Shell, to see percentage-based responses, make a - series of requests: - - ```sh - LIVE_URL=$(gcloud run services describe hello-cloudrun \ - --platform managed \ - --region us-central1 \ - --format=json | jq \ - --raw-output ".status.url") - - for i in {0..20};do - curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $LIVE_URL; echo \n - Done - ``` -7. Review the lines of `tag-cloudbuild.yaml` that implement the production - deployment logic. - - The following line updates the canary revision adding the `prod` tag. The - deployed revision is now tagged for both `prod` and `canary`: - - ```sh - gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=prod=$${CANARY} --platform managed --region ${_REGION} - ``` - The following line updates the traffic for the base service URL to route - 100% of traffic to the revision tagged as `prod`: - - ```sh - gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=$${NEW_PROD}=100 --platform managed --region ${_REGION} - ``` +6. In Cloud Shell, to see percentage-based responses, make a series of requests: + +```sh +LIVE_URL=$(gcloud run services describe hello-cloudrun --platform managed --region us-central1 --format=json | jq --raw-output ".status.url") + +for i in {0..20};do + curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $LIVE_URL; echo \n +done +``` + + +7. Review the key lines of `tag-cloudbuild.yaml` that implement the production deployment logic.  + +Line 37 updates the canary revision adding the prod tag. The deployed revision is now tagged for both prod and canary: + +``` +gcloud beta run services update-traffic  ${_SERVICE_NAME} --update-tags=prod=$${CANARY}  --platform managed  --region ${_REGION} +``` + +Line 39 updates the traffic for the base service URL to route 100% of traffic to the revision tagged as prod: + +``` +gcloud run services update-traffic  ${_SERVICE_NAME} --to-revisions=$${NEW_PROD}=100  --platform managed  --region ${_REGION} +``` + + +# Cleaning up + +To avoid incurring charges to your Google Cloud Platform account for the resources used in this tutorial: + +### Delete the project + +The easiest way to eliminate billing is to delete the project you created for the tutorial. + +Caution: Deleting a project has the following effects: + +- Everything in the project is deleted. If you used an existing project for this tutorial, when you delete it, you also delete any other work you've done in the project. +- Custom project IDs are lost. When you created this project, you might have created a custom project ID that you want to use in the future. To preserve the URLs that use the project ID, such as an appspot.com URL, delete selected resources inside the project instead of deleting the whole project. + +If you plan to explore multiple tutorials and quickstarts, reusing projects can help you avoid exceeding project quota limits. + +1. In the Cloud Console, go to the Manage resources page. + [Go to the Manage resources page](https://console.cloud.google.com/iam-admin/projects) +2. In the project list, select the project that you want to delete and then click Delete ![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAABGCAYAAACDkrchAAAAAXNSR0IArs4c6QAAAe9JREFUaEPtWctKQlEUXdcH+BnVT1SD1LRxeiUoatCwbvgbEQVmFASJEUFEX1GNKkPKhg6tT3DiAzSuEGgonu095yKyznjt11p7b6/nWN1ut4sZOhYLmnI1qdCUCwQqRIV8ZoAt5zPh4nBUSEyZzwZUyGfCxeGokJgynw2okM+Ei8MZUajZbMJxsnh8eh6bUCQSQeHqEtHoylisCmCigtx/7a1Wa6j/RqOB/YMsXl/eVOL3MOFwGOcXZ0jEY0NtgsEgQqGQkr+JCqpUvpC2N5QC6ADF4lHc3lwruWJBLk1USKlZRoOMt5zH/IyaTzRDfxl1Oh20221jCQYCgd4GlBxPBZVK79jc2pHEE2GXlhfxcH8nsmFB/XRRIVHzAGw5wNvNKVuOLce1zRkamAIuBS4FLgUuBS4F4R4YgPPjlB+n//qHP6zCgeIMcYZmfYZqtRpi8TXhZKjD7fQ68vmcuoHXlnPfiWKrSXzXfkRBVcGnuRNkMrYqvIfzdNHoOiiXy9je2R35ACbKpg+cTMRRLBZgWZbIheeC3GjVahWHR8f4/KigXq+LEugHu3fZ8wtzsFMpOM4e3Jc76dFSkDSoSTwLMsmuDt9USAeLJn1QIZPs6vBNhXSwaNIHFTLJrg7fVEgHiyZ9UCGT7OrwPXMK/QL+cgFNd5b6egAAAABJRU5ErkJggg==). +3. In the dialog, type the project ID and then click Shut down to delete the project. + +# What's next + +- Review [Managing Revisions with Cloud Run](https://cloud.google.com/run/docs/managing/revisions). +- Review Cloud Run [Rollbacks, gradual rollouts, and traffic migration](https://cloud.google.com/run/docs/rollouts-rollbacks-traffic-migration) +- Review [Using tags for accessing revisions](https://cloud.google.com/run/docs/rollouts-rollbacks-traffic-migration#tags). +- Review [Creating and managing build triggers](https://cloud.google.com/build/docs/automating-builds/create-manage-triggers) in Cloud Build. \ No newline at end of file diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml index 0d2046f..d79ec9c 100644 --- a/labs/cloudrun-progression/branch-cloudbuild.yaml +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -22,7 +22,7 @@ steps: ### Build - id: "build image" name: "gcr.io/cloud-builders/docker" - args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] + args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "labs/cloudrun-progression"] ### Push - id: "push image" diff --git a/labs/cloudrun-progression/branch-trigger.json-tmpl b/labs/cloudrun-progression/branch-trigger.json-tmpl deleted file mode 100644 index 01edb91..0000000 --- a/labs/cloudrun-progression/branch-trigger.json-tmpl +++ /dev/null @@ -1,11 +0,0 @@ -{ - "triggerTemplate": { - "projectId": "PROJECT", - "repoName": "cloudrun-progression", - "branchName": "[^(?!.*master)].*" - }, - "name": "branch", - "description": "Trigger dev build/deploy for any branch other than master", - - "filename": "branch-cloudbuild.yaml" -} \ No newline at end of file diff --git a/labs/cloudrun-progression/master-cloudbuild.yaml b/labs/cloudrun-progression/main-cloudbuild.yaml similarity index 98% rename from labs/cloudrun-progression/master-cloudbuild.yaml rename to labs/cloudrun-progression/main-cloudbuild.yaml index 7a281cf..4135506 100644 --- a/labs/cloudrun-progression/master-cloudbuild.yaml +++ b/labs/cloudrun-progression/main-cloudbuild.yaml @@ -22,7 +22,7 @@ steps: ### Build - id: "build image" name: "gcr.io/cloud-builders/docker" - args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] + args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "labs/cloudrun-progression"] ### Push - id: "push image" diff --git a/labs/cloudrun-progression/master-trigger.json-tmpl b/labs/cloudrun-progression/master-trigger.json-tmpl deleted file mode 100644 index 40580b6..0000000 --- a/labs/cloudrun-progression/master-trigger.json-tmpl +++ /dev/null @@ -1,11 +0,0 @@ -{ - "triggerTemplate": { - "projectId": "PROJECT", - "repoName": "cloudrun-progression", - "branchName": "master" - }, - "name": "master", - "description": "Trigger canary build/deploy for any commit to the master branch", - - "filename": "master-cloudbuild.yaml" -} \ No newline at end of file diff --git a/labs/cloudrun-progression/tag-trigger.json-tmpl b/labs/cloudrun-progression/tag-trigger.json-tmpl deleted file mode 100644 index b791530..0000000 --- a/labs/cloudrun-progression/tag-trigger.json-tmpl +++ /dev/null @@ -1,11 +0,0 @@ -{ - "triggerTemplate": { - "projectId": "PROJECT", - "repoName": "cloudrun-progression", - "tagName": ".*" - }, - "name": "tag", - "description": "Migrate from canary to prod triggered by creation of any tag", - - "filename": "tag-cloudbuild.yaml" -} \ No newline at end of file From ef2fb0c396ef37da406de503ff26ecda57d3613c Mon Sep 17 00:00:00 2001 From: Daniil Morzhevskiy Date: Tue, 30 May 2023 20:09:28 +0300 Subject: [PATCH 50/50] Fix for Lab GSP1078 (Cloud Run Canary Deployments) --- .../branch-trigger.json-tmpl | 11 ++++ .../cloudrun-progression/main-cloudbuild.yaml | 2 +- .../master-cloudbuild.yaml | 61 +++++++++++++++++++ .../master-trigger.json-tmpl | 11 ++++ .../tag-trigger.json-tmpl | 11 ++++ 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 labs/cloudrun-progression/branch-trigger.json-tmpl create mode 100644 labs/cloudrun-progression/master-cloudbuild.yaml create mode 100644 labs/cloudrun-progression/master-trigger.json-tmpl create mode 100644 labs/cloudrun-progression/tag-trigger.json-tmpl diff --git a/labs/cloudrun-progression/branch-trigger.json-tmpl b/labs/cloudrun-progression/branch-trigger.json-tmpl new file mode 100644 index 0000000..2a6ee51 --- /dev/null +++ b/labs/cloudrun-progression/branch-trigger.json-tmpl @@ -0,0 +1,11 @@ +{ + "triggerTemplate": { + "projectId": "PROJECT", + "repoName": "cloudrun-progression", + "branchName": "[^(?!.*master)].*" + }, + "name": "branch", + "description": "Trigger dev build/deploy for any branch other than master", + + "filename": "branch-cloudbuild.yaml" + } \ No newline at end of file diff --git a/labs/cloudrun-progression/main-cloudbuild.yaml b/labs/cloudrun-progression/main-cloudbuild.yaml index 4135506..7a281cf 100644 --- a/labs/cloudrun-progression/main-cloudbuild.yaml +++ b/labs/cloudrun-progression/main-cloudbuild.yaml @@ -22,7 +22,7 @@ steps: ### Build - id: "build image" name: "gcr.io/cloud-builders/docker" - args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "labs/cloudrun-progression"] + args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] ### Push - id: "push image" diff --git a/labs/cloudrun-progression/master-cloudbuild.yaml b/labs/cloudrun-progression/master-cloudbuild.yaml new file mode 100644 index 0000000..edee628 --- /dev/null +++ b/labs/cloudrun-progression/master-cloudbuild.yaml @@ -0,0 +1,61 @@ +# Copyright 2020 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 + # + # https://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. + + # Default Values + substitutions: + _SERVICE_NAME: hello-cloudrun + _REGION: us-central1 + + steps: + + ### Build + - id: "build image" + name: "gcr.io/cloud-builders/docker" + args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] + + ### Push + - id: "push image" + name: "gcr.io/cloud-builders/docker" + args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] + + ### Deploy + - id: "deploy canary service" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - '-c' + - | + gcloud run deploy ${_SERVICE_NAME} \ + --platform managed \ + --region ${_REGION} \ + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ + --tag=canary \ + --no-traffic + + + # Route Traffic + - id: "route prod traffic" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - '-c' + - | + apt-get install -y jq + export CANARY=$$(gcloud run services describe hello-cloudrun --platform managed --region ${_REGION} --format=json | jq --raw-output ".spec.traffic[] | select (.tag==\"canary\")|.revisionName") + export PROD=$$(gcloud run services describe hello-cloudrun --platform managed --region ${_REGION} --format=json | jq --raw-output ".spec.traffic[] | select (.tag==\"prod\")|.revisionName") + echo SHORT_SHA is $SHORT_SHA + echo Canary is $${CANARY} + echo gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=sha-$SHORT_SHA=$${CANARY} --platform managed --region ${_REGION} + gcloud beta run services update-traffic ${_SERVICE_NAME} --update-tags=sha-$SHORT_SHA=$${CANARY} --platform managed --region ${_REGION} + gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=$${PROD}=90,$${CANARY}=10 --platform managed --region ${_REGION} \ No newline at end of file diff --git a/labs/cloudrun-progression/master-trigger.json-tmpl b/labs/cloudrun-progression/master-trigger.json-tmpl new file mode 100644 index 0000000..84ad5ba --- /dev/null +++ b/labs/cloudrun-progression/master-trigger.json-tmpl @@ -0,0 +1,11 @@ +{ + "triggerTemplate": { + "projectId": "PROJECT", + "repoName": "cloudrun-progression", + "branchName": "master" + }, + "name": "master", + "description": "Trigger canary build/deploy for any commit to the master branch", + + "filename": "master-cloudbuild.yaml" + } \ No newline at end of file diff --git a/labs/cloudrun-progression/tag-trigger.json-tmpl b/labs/cloudrun-progression/tag-trigger.json-tmpl new file mode 100644 index 0000000..d8daac2 --- /dev/null +++ b/labs/cloudrun-progression/tag-trigger.json-tmpl @@ -0,0 +1,11 @@ +{ + "triggerTemplate": { + "projectId": "PROJECT", + "repoName": "cloudrun-progression", + "tagName": ".*" + }, + "name": "tag", + "description": "Migrate from canary to prod triggered by creation of any tag", + + "filename": "tag-cloudbuild.yaml" + } \ No newline at end of file