├── .gitignore ├── CONTRIBUTING.md ├── Jenkinsfile ├── LICENSE ├── Makefile ├── OWNERS ├── README-QWIKLABS.md ├── README.md ├── VERSION ├── container ├── Dockerfile ├── README.md ├── cloudbuild.yaml ├── install.sh ├── prime-flask-server.py ├── primeserver.service └── requirements.txt ├── images ├── Debian-deployment.png ├── ab_load-test-1.png ├── ab_load-test-2.png ├── cos-deployment.png ├── gke-deployment.png ├── load_test-1.png ├── load_test-2.png ├── setup-2.png ├── setup-success.png ├── setup.png ├── tear-down.png └── validate.png ├── renovate.json ├── scripts ├── common.sh ├── create.sh ├── generate-tfvars.sh ├── teardown.sh └── validate.sh ├── terraform ├── main.tf ├── manifests │ ├── prime-server-ingress.yaml │ └── prime-server-svc.yaml ├── outputs.tf ├── provider.tf ├── variables.tf ├── versions.tf └── web-init.sh.tmpl └── test ├── boilerplate ├── boilerplate.Dockerfile.txt ├── boilerplate.Makefile.txt ├── boilerplate.go.txt ├── boilerplate.py.txt ├── boilerplate.sh.txt ├── boilerplate.tf.txt ├── boilerplate.xml.txt └── boilerplate.yaml.txt ├── make.sh ├── test_verify_boilerplate.py └── verify_boilerplate.py /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX leaves these everywhere on SMB shares 2 | ._* 3 | 4 | # OSX trash 5 | .DS_Store 6 | 7 | # Emacs save files 8 | *~ 9 | \#*\# 10 | .\#* 11 | 12 | # Vim-related files 13 | [._]*.s[a-w][a-z] 14 | [._]s[a-w][a-z] 15 | *.un~ 16 | Session.vim 17 | .netrwhist 18 | 19 | # Build artifacts 20 | build/* 21 | ### https://raw.github.com/github/gitignore/90f149de451a5433aebd94d02d11b0e28843a1af/Terraform.gitignore 22 | 23 | # Local .terraform directories 24 | **/.terraform/* 25 | 26 | # .tfstate files 27 | *.tfstate 28 | *.tfstate.* 29 | 30 | # Crash log files 31 | crash.log 32 | 33 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 34 | # .tfvars files are managed as part of configuration and so should be included in 35 | # version control. 36 | # 37 | # example.tfvars 38 | *.tfvars 39 | prime-server-deployment.yaml 40 | flask-prime.tgz 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | Contributions to this project must be accompanied by a Contributor License 8 | Agreement. You (or your employer) retain the copyright to your contribution; 9 | this simply gives us permission to use and redistribute your contributions as 10 | part of the project. Head over to https://cla.developers.google.com/ to see your 11 | current agreements on file or to sign a new one. 12 | 13 | You generally only need to submit a CLA once, so if you've already submitted one 14 | (even if it was for a different project), you probably don't need to do it again. 15 | 16 | ## Code reviews 17 | All submissions, including submissions by project members, require review. We 18 | use GitHub pull requests for this purpose. Consult GitHub Help for more 19 | information on using pull requests. 20 | 21 | ## Community Guidelines 22 | This project follows 23 | [Google's Open Source Community Guidelines](CODE-OF-CONDUCT.md). 24 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | /* 3 | Copyright 2018 Google LLC 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | // The declarative agent is defined in yaml. It was previously possible to 20 | // define containerTemplate but that has been deprecated in favor of the yaml 21 | // format 22 | // Reference: https://github.com/jenkinsci/kubernetes-plugin 23 | 24 | // set up pod label and GOOGLE_APPLICATION_CREDENTIALS (for Terraform) 25 | def label = "k8s-infra" 26 | def containerName = "k8s-node" 27 | def GOOGLE_APPLICATION_CREDENTIALS = '/home/jenkins/dev/jenkins-deploy-dev-infra.json' 28 | 29 | podTemplate(label: label, yaml: """ 30 | apiVersion: v1 31 | kind: Pod 32 | metadata: 33 | labels: 34 | jenkins: build-node 35 | spec: 36 | containers: 37 | - name: ${containerName} 38 | image: gcr.io/pso-helmsman-cicd/jenkins-k8s-node:${env.JENKINS_CONTAINER_VERSION} 39 | command: ['cat'] 40 | tty: true 41 | volumeMounts: 42 | # Mount the dev service account key 43 | - name: dev-key 44 | mountPath: /home/jenkins/dev 45 | volumes: 46 | # Create a volume that contains the dev json key that was saved as a secret 47 | - name: dev-key 48 | secret: 49 | secretName: jenkins-deploy-dev-infra 50 | """ 51 | ) { 52 | node(label) { 53 | try { 54 | // Options covers all other job properties or wrapper functions that apply to entire Pipeline. 55 | properties([disableConcurrentBuilds()]) 56 | // set env variable GOOGLE_APPLICATION_CREDENTIALS for Terraform 57 | env.GOOGLE_APPLICATION_CREDENTIALS=GOOGLE_APPLICATION_CREDENTIALS 58 | 59 | stage('Setup') { 60 | container(containerName) { 61 | // checkout code from scm i.e. commits related to the PR 62 | checkout scm 63 | 64 | // Setup gcloud service account access 65 | sh "gcloud auth activate-service-account --key-file=${GOOGLE_APPLICATION_CREDENTIALS}" 66 | sh "gcloud config set compute/zone ${env.ZONE}" 67 | sh "gcloud config set core/project ${env.PROJECT_ID}" 68 | sh "gcloud config set compute/region ${env.REGION}" 69 | } 70 | } 71 | stage('Lint') { 72 | container(containerName) { 73 | sh "make lint" 74 | } 75 | } 76 | 77 | stage('Create') { 78 | container(containerName) { 79 | sh "make create" 80 | } 81 | } 82 | 83 | stage('Validate') { 84 | container(containerName) { 85 | sh "make validate" 86 | } 87 | } 88 | 89 | } 90 | catch (err) { 91 | // if any exception occurs, mark the build as failed 92 | // and display a detailed message on the Jenkins console output 93 | currentBuild.result = 'FAILURE' 94 | echo "FAILURE caught echo ${err}" 95 | throw err 96 | } 97 | finally { 98 | stage('Teardown') { 99 | container(containerName) { 100 | sh "make teardown" 101 | sh "gcloud auth revoke" 102 | } 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Make will use bash instead of sh 16 | SHELL := /usr/bin/env bash 17 | 18 | # lint is the first target in the file so it will get picked up when you just 19 | # run 'make' on its own 20 | lint: check_shell check_shebangs check_python check_golang check_terraform \ 21 | check_docker check_base_files check_headers check_trailing_whitespace 22 | 23 | # create/delete/validate is for CICD 24 | .PHONY: create 25 | create: 26 | @source scripts/create.sh 27 | 28 | .PHONY: validate 29 | validate: 30 | @source scripts/validate.sh 31 | 32 | .PHONY: teardown 33 | teardown: 34 | @source scripts/teardown.sh 35 | 36 | .PHONY: check_shell 37 | check_shell: 38 | @source test/make.sh && check_shell 39 | 40 | .PHONY: check_python 41 | check_python: 42 | @source test/make.sh && check_python 43 | 44 | .PHONY: check_golang 45 | check_golang: 46 | @source test/make.sh && golang 47 | 48 | .PHONY: check_terraform 49 | check_terraform: 50 | @source test/make.sh && check_terraform 51 | 52 | .PHONY: check_docker 53 | check_docker: 54 | @source test/make.sh && docker 55 | 56 | .PHONY: check_base_files 57 | check_base_files: 58 | @source test/make.sh && basefiles 59 | 60 | .PHONY: check_shebangs 61 | check_shebangs: 62 | @source test/make.sh && check_bash 63 | 64 | .PHONY: check_trailing_whitespace 65 | check_trailing_whitespace: 66 | @source test/make.sh && check_trailing_whitespace 67 | 68 | .PHONY: check_headers 69 | check_headers: 70 | @echo "Checking file headers" 71 | @python test/verify_boilerplate.py 72 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - chrislovecnm 3 | - robinpercy 4 | - geojaz 5 | - techgnosis 6 | - erkolson 7 | labels: 8 | - gke-helmsman 9 | -------------------------------------------------------------------------------- /README-QWIKLABS.md: -------------------------------------------------------------------------------- 1 | # Migrating to Containers 2 | 3 | Containers are quickly becoming an industry standard for deployment of software applications. The business and technological advantages of containerizing workloads are driving many teams towards moving their applications to containers. This demo provides a basic walkthrough of migrating a stateless application from running on a VM to running on [Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/). It demonstrates the lifecycle of an application transitioning from a typical VM/OS-based deployment to a specialized os for containers to a platform for containers better known as [GKE](https://cloud.google.com/kubernetes-engine/). 4 | 5 | ## Table of Contents 6 | 7 | * [Table of Contents](#table-of-contents) 8 | * [Introduction](#introduction) 9 | * [Architecture](#architecture) 10 | * [Initial Setup](#initial-setup) 11 | * [Configure gcloud](#configure-gcloud) 12 | * [Get The Code](#get-the-code) 13 | * [Tools](#tools) 14 | * [Install Cloud SDK](#install-cloud-sdk) 15 | * [Install kubectl CLI](#install-kubectl-cli) 16 | * [Install Terraform](#install-terraform) 17 | * [Authenticate gcloud](#authenticate-gcloud) 18 | * [Deployment](#deployment) 19 | * [Exploring Prime Flask Environments ](#exploring-prime-flask-environments) 20 | * [Validation](#validation) 21 | * [Load Testing](#load-testing) 22 | * [Tear Down](#tear-down) 23 | * [More Info](#more-info) 24 | * [Troubleshooting](#troubleshooting) 25 | 26 | 27 | ## Introduction 28 | 29 | There are numerous advantages to using containers to deploy applications. Among these are: 30 | 31 | 1. _Isolated_ - Applications have their own libraries; no conflicts will arise from different libraries in other applications. 32 | 33 | 1. _Limited (limits on CPU/memory)_ - Applications may not hog resources from other applications. 34 | 35 | 1. _Portable_ - The container contains everything it needs and is not tied to an OS or Cloud provider. 36 | 37 | 1. _Lightweight_ - The kernel is shared, making it much smaller and faster than a full OS image. 38 | 39 | ***What you'll learn*** 40 | This project demonstrates migrating a simple Python application named [Prime-flask](container/prime-flask-server.py) to: 41 | 42 | 1. A [virtual machine (Debian VM)](https://cloud.google.com/compute/docs/instances/) where [Prime-flask](container/prime-flask-server.py) is deployed as the only application, much like a traditional application is run in an on-premises datacenter 43 | 44 | 1. A containerized version of [Prime-flask](container/prime-flask-server.py) is deployed on [Container-Optimized OS (COS)](https://cloud.google.com/container-optimized-os/) 45 | 46 | 1. A [Kubernetes](https://kubernetes.io/) deployment where `Prime-flask` is exposed via a load balancer and deployed in [Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) 47 | 48 | After the deployment you'll run a load test against the final deployment and scale it to accommodate the load. 49 | 50 | The python app [Prime-flask](container/prime-flask-server.py) has instructions for creating container in [this folder](container). 51 | 52 | ## Architecture 53 | 54 | **Configuration 1:** [Virtual machine](https://cloud.google.com/compute/docs/instances/) running Debian, app deployed directly to the host OS, no containers 55 | 56 | ![screenshot](./images/Debian-deployment.png) 57 | 58 | **Configuration 2:** Virtual machine running [Container-Optimized OS](https://cloud.google.com/container-optimized-os/), app deployed into a container 59 | 60 | ![screenshot](./images/cos-deployment.png) 61 | 62 | **Configuration 3:** [Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/) platform, many machines running many containers 63 | 64 | ![screenshot](./images/gke-deployment.png) 65 | 66 | A simple Python [Flask](http://flask.pocoo.org/) web application (`Prime-flask`) was created for this demonstration which contains two endpoints: 67 | 68 | `http://:8080/factorial/` and 69 | 70 | `http://:8080/prime/` 71 | 72 | Examples of use would look like: 73 | 74 | ```console 75 | curl http://35.227.149.80:8080/prime/10 76 | The sum of all primes less than 10 is 17 77 | 78 | curl http://35.227.149.80:8080/factorial/10 79 | The factorial of 10 is 3628800 80 | ``` 81 | 82 | Also included is a utility to validate a successful deployment. 83 | 84 | ## Initial Setup 85 | 86 | ### Configure gcloud 87 | 88 | When using Cloud Shell execute the following command in order to setup gcloud cli. 89 | 90 | ```console 91 | gcloud init 92 | ``` 93 | 94 | 95 | ### Get The Code 96 | 97 | * [Fork the repo](https://help.github.com/articles/fork-a-repo/) 98 | * [Clone your fork](https://help.github.com/articles/cloning-a-repository/) 99 | 100 | ## Deployment 101 | 102 | The infrastructure required by this project can be deployed by executing: 103 | ```console 104 | make create 105 | ``` 106 | 107 | This will call script [create.sh](scripts/create.sh) which will perform following tasks: 108 | 1. Package the deployable `Prime-flask` application, making it ready to be copied to [Google Cloud Storage](https://cloud.google.com/storage/). 109 | 1. Create the container image via [Google Cloud Build](https://cloud.google.com/cloud-build/) and push it to the private [Container Registry (GCR)](https://cloud.google.com/container-registry/) for your project. 110 | 1. Generate an appropriate configuration for [Terraform](https://www.terraform.io). 111 | 1. Execute Terraform which creates the scenarios we outlined above. 112 | 113 | Terraform creating single VM, COS VM, and GKE cluster: 114 | 115 | ![screenshot](./images/setup.png) 116 | 117 | Terraform outputs showing prime and factorial endpoints for Debian VM and COS system: 118 | 119 | ![screenshot](./images/setup-2.png) 120 | 121 | Kubernetes Cluster and [Prime-flask](container/prime-flask-server.py) service are up: 122 | 123 | ![screenshot](./images/setup-success.png) 124 | 125 | ## Exploring Prime Flask Environments 126 | 127 | We have now setup three different environments that our [Prime-flask](container/prime-flask-server.py) app could traverse as it is making its way to becoming a container app living on a single virtual machine to a [pod](https://kubernetes.io/docs/concepts/workloads/pods/pod/) running on a container orchestration platform like [Kubernetes](https://kubernetes.io/). 128 | 129 | At this point it would benefit you to explore the systems. 130 | 131 | Jump onto the Debian [virtual machine](https://cloud.google.com/compute/docs/instances/), `vm-webserver`, that has application running on host OS. In this environment there is no isolation, and portability is less efficient. In a sense the app running on the system has access to all the system and depending on other factors may not have automatic recovery of application if it fails. Scaling up this application may require to spin up more virtual machines and most likely will not be best use of resources. 132 | ``` 133 | gcloud compute ssh vm-webserver --zone us-west1-c 134 | ``` 135 | 136 | List all processes: 137 | ```bash 138 | ps aux 139 | ``` 140 | ```bash 141 | root 882 0.0 1.1 92824 6716 ? Ss 18:41 0:00 sshd: user [priv] 142 | user 888 0.0 0.6 92824 4052 ? S 18:41 0:00 sshd: user@pts/0 143 | user 889 0.0 0.6 19916 3880 pts/0 Ss 18:41 0:00 -bash 144 | user 895 0.0 0.5 38304 3176 pts/0 R+ 18:41 0:00 ps aux 145 | apprunn+ 7938 0.0 3.3 48840 20328 ? Ss Mar19 1:06 /usr/bin/python /usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 146 | apprunn+ 21662 0.0 3.9 69868 24032 ? S Mar20 0:05 /usr/bin/python /usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 147 | ``` 148 | 149 | Jump onto the [Container-Optimized OS (COS)](https://cloud.google.com/container-optimized-os/) machine, `cos-vm`, where we have docker running the container. COS is an optimized operating system with small OS footprint, which is part of what makes it secure to run container workloads. It has cloud-init and has Docker runt time preinstalled. This system on its own could be great to run several containers that did not need to be run on a platform that provided higher levels of reliability. 150 | 151 | ```bash 152 | gcloud compute ssh cos-vm --zone us-west1-c 153 | ``` 154 | 155 | We can also run `ps aux` on the host and see the prime-flask running, but notice docker and container references: 156 | ```bash 157 | root 626 0.0 5.7 496812 34824 ? Ssl Mar19 0:14 /usr/bin/docker run --rm --name=flaskservice -p 8080:8080 gcr.io/migration-to-containers/prime-flask:1.0.2 158 | root 719 0.0 0.5 305016 3276 ? Sl Mar19 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 8080 159 | root 724 0.0 0.8 614804 5104 ? Sl Mar19 0:09 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/mo 160 | chronos 741 0.0 0.0 204 4 ? Ss Mar19 0:00 /usr/bin/dumb-init /usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 161 | chronos 774 0.0 3.2 21324 19480 ? Ss Mar19 1:25 /usr/local/bin/python /usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 162 | chronos 14376 0.0 4.0 29700 24452 ? S Mar20 0:05 /usr/local/bin/python /usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 163 | ``` 164 | 165 | Also notice if we try to list the python path it does not exist: 166 | ```bash 167 | ls /usr/local/bin/python 168 | ls: cannot access '/usr/local/bin/python': No such file or directory 169 | ``` 170 | 171 | Docker list containers 172 | ```bash 173 | docker ps 174 | ``` 175 | ```bash 176 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 177 | d147963ec3ca gcr.io/migration-to-containers/prime-flask:1.0.2 "/usr/bin/dumb-init …" 39 hours ago Up 39 hours 0.0.0.0:8080->8080/tcp flaskservice 178 | ``` 179 | 180 | Now we can exec a command to see running process on the container: 181 | ```bash 182 | docker exec -it $(docker ps |awk '/prime-flask/ {print $1}') ps aux 183 | ``` 184 | ```bash 185 | PID USER TIME COMMAND 186 | 1 apprunne 0:00 /usr/bin/dumb-init /usr/local/bin/gunicorn --bind 0.0.0.0: 187 | 6 apprunne 1:25 {gunicorn} /usr/local/bin/python /usr/local/bin/gunicorn - 188 | 17 apprunne 0:05 {gunicorn} /usr/local/bin/python /usr/local/bin/gunicorn - 189 | 29 apprunne 0:00 ps aux 190 | ``` 191 | 192 | Jump on to [Kubernetes](https://kubernetes.io/). In this environment we can run hundreds or thousands of [pods](https://kubernetes.io/docs/concepts/workloads/pods/pod/) that are groupings of containers. Kubernetes is the defacto standard for deploying containers these days. It offers high productivity, reliability, and scalability. Kubernetes makes sure your containers have a home, and if container happens to fail, it will respawn it again. You can have many machines making up the cluster and in so doing you can spread it across different zones ensuring availability, and resilience to potential issues. 193 | 194 | Get cluster configuration: 195 | ```bash 196 | gcloud container clusters get-credentials prime-server-cluster 197 | ``` 198 | 199 | Get pods running in the default namespace: 200 | ```bash 201 | kubectl get pods 202 | ``` 203 | ```bash 204 | NAME READY STATUS RESTARTS AGE 205 | prime-server-6b94cdfc8b-dfckf 1/1 Running 0 2d5h 206 | ``` 207 | 208 | See what is running on the pod: 209 | ```bash 210 | kubectl exec $(kubectl get pods -lapp=prime-server -ojsonpath='{.items[].metadata.name}') -- ps aux 211 | ``` 212 | ```bash 213 | PID USER TIME COMMAND 214 | 1 apprunne 0:00 /usr/bin/dumb-init /usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 215 | 6 apprunne 1:16 {gunicorn} /usr/local/bin/python /usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 216 | 8 apprunne 2:52 {gunicorn} /usr/local/bin/python /usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 217 | 19 apprunne 0:00 ps aux 218 | ``` 219 | 220 | As you can see from the last example, python application is now running in a container. The application can't access anything on the host. The container is isolated. It runs in a linux namespace and can't (by default) access files, the network, or other resources running on the VM, in containers or otherwise. 221 | 222 | ## Validation 223 | 224 | Now that the application is deployed, we can validate these three deployments by executing: 225 | 226 | ```console 227 | make validate 228 | ``` 229 | 230 | A successful output will look like this: 231 | ```console 232 | Validating Debian VM Webapp... 233 | Testing endpoint http://35.227.149.80:8080 234 | Endpoint http://35.227.149.80:8080 is responding. 235 | **** http://35.227.149.80:8080/prime/10 236 | The sum of all primes less than 10 is 17 237 | The factorial of 10 is 3628800 238 | 239 | Validating Container OS Webapp... 240 | Testing endpoint http://35.230.123.231:8080 241 | Endpoint http://35.230.123.231:8080 is responding. 242 | **** http://35.230.123.231:8080/prime/10 243 | The sum of all primes less than 10 is 17 244 | The factorial of 10 is 3628800 245 | 246 | Validating Kubernetes Webapp... 247 | Testing endpoint http://35.190.89.136 248 | Endpoint http://35.190.89.136 is responding. 249 | **** http://35.190.89.136/prime/10 250 | The sum of all primes less than 10 is 17 251 | The factorial of 10 is 3628800 252 | ``` 253 | 254 | Of course, the IP addresses will likely differ for your deployment. 255 | 256 | ## Load Testing 257 | 258 | In a new console window, execute the following, replacing `[IP_ADDRESS]` with the IP address and port from your validation output from the previous step. Note that the Kubernetes deployment runs on port `80`, while the other two deployments run on port `8080`: 259 | ```console 260 | ab -c 120 -t 60 http:///prime/10000 261 | ``` 262 | ApacheBench (`ab`) will execute 120 concurrent requests against the provided endpoint for 1 minute. The demo application's replica is insufficiently sized to handle this volume of requests. 263 | 264 | This can be confirmed by reviewing the output from the `ab` command. A `Failed requests` value of greater than 0 means that the server couldn't respond successfully to this load: 265 | 266 | ![screenshot](./images/ab_load-test-1.png) 267 | 268 | One way to ensure that your system has capacity to handle this type of traffic is by scaling up. In this case, we would want to scale our service horizontally. 269 | 270 | In our Debian and COS architectures, horizontal scaling would include: 271 | 272 | 1. Creating a load balancer. 273 | 1. Spinning up additional instances. 274 | 1. Registering them with the load balancer. 275 | 276 | This is an involved process and is out of scope for this demonstration. 277 | 278 | For the third (Kubernetes) deployment the process is far easier: 279 | 280 | ```console 281 | kubectl scale --replicas 3 deployment/prime-server 282 | ``` 283 | 284 | After allowing 30 seconds for the replicas to initialize, re-run the load test: 285 | 286 | ```console 287 | ab -c 120 -t 60 http:///prime/10000 288 | ``` 289 | Notice how the `Failed requests` is now 0. This means that all of the 10,000+ requests were successfully answered by the server: 290 | 291 | ![screenshot](./images/ab_load-test-2.png) 292 | 293 | ## Tear Down 294 | 295 | When you are finished with this example you will want to clean up the resources that were created so that you avoid accruing charges: 296 | 297 | ``` 298 | $ make teardown 299 | ``` 300 | 301 | It will run `terraform destroy` which will destroy all of the resources created for this demonstration. 302 | 303 | ![screenshot](./images/tear-down.png) 304 | 305 | ## More Info 306 | For additional information see: [Embarking on a Journey Towards Containerizing Your Workloads](https://www.youtube.com/watch?v=_aFA-p87Eec&index=22&list=PLBgogxgQVM9uSqzLOc66kNIZMUOvnGbU4) 307 | 308 | ## Troubleshooting 309 | 310 | Occasionally the APIs take a few moments to complete. Running `make validate` immediately could potentially appear to fail, but in fact the instances haven't finished initializing. Waiting for a minute or two should resolve the issue. 311 | 312 | The setup of this demo **does** take up to **15** minutes. If there is no error the best thing to do is keep waiting. The execution of `make create` should **not** be interrupted. 313 | 314 | If you do get an error, it probably makes sense to re-execute the failing script. Occasionally there are network connectivity issues, and retrying will likely work the subsequent time. 315 | 316 | **This is not an officially supported Google product** 317 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migrating to Containers 2 | 3 | Containers are quickly becoming an industry standard for deployment of software applications. The business and technological advantages of containerizing workloads are driving many teams towards moving their applications to containers. This demo provides a basic walkthrough of migrating a stateless application from running on a VM to running on [Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/). It demonstrates the lifecycle of an application transitioning from a typical VM/OS-based deployment to three different containerized cloud infrastructure platforms. 4 | 5 | ## Table of Contents 6 | 7 | * [Table of Contents](#table-of-contents) 8 | * [Introduction](#introduction) 9 | * [Architecture](#architecture) 10 | * [Prerequisites](#prerequisites) 11 | * [Run Demo in a Google Cloud Shell](#run-demo-in-a-google-cloud-shell) 12 | * [Get The Code](#get-the-code) 13 | * [Tools](#tools) 14 | * [Install Cloud SDK](#install-cloud-sdk) 15 | * [Install kubectl CLI](#install-kubectl-cli) 16 | * [Install Terraform](#install-terraform) 17 | * [Authenticate gcloud](#authenticate-gcloud) 18 | * [Deployment](#deployment) 19 | * [Validation](#validation) 20 | * [Load Testing](#load-testing) 21 | * [Tear Down](#tear-down) 22 | * [More Info](#more-info) 23 | * [Troubleshooting](#troubleshooting) 24 | 25 | 26 | ## Introduction 27 | 28 | There are numerous advantages to using containers to deploy applications. Among these are: 29 | 30 | 1. _Isolated_ - Applications have their own libraries; no conflicts will arise from different libraries in other applications. 31 | 32 | 1. _Limited (limits on CPU/memory)_ - Applications may not hog resources from other applications. 33 | 34 | 1. _Portable_ - The container contains everything it needs and is not tied to an OS or Cloud provider. 35 | 36 | 1. _Lightweight_ - The kernel is shared, making it much smaller and faster than a full OS image. 37 | 38 | This project demonstrates migrating a simple Python application named `Prime-flask` to: 39 | 40 | 1. A legacy deployment (Debian VM) where `Prime-flask` is deployed as the only application, much like a traditional application is run in an on-premises datacenter 41 | 42 | 1. A containerized version deployed on [Container-Optimized OS (COS)](https://cloud.google.com/container-optimized-os/) 43 | 44 | 1. A [Kubernetes](https://kubernetes.io/) deployment where `Prime-flask` is exposed via a load balancer and deployed in [Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) 45 | 46 | After the deployment you'll run a load test against the final deployment and scale it to accommodate the load. 47 | 48 | ## Architecture 49 | 50 | **Configuration 1:** 51 | ![screenshot](./images/Debian-deployment.png) 52 | 53 | **Configuration 2:** 54 | ![screenshot](./images/cos-deployment.png) 55 | 56 | **Configuration 3:** 57 | ![screenshot](./images/gke-deployment.png) 58 | 59 | A simple Python [Flask](http://flask.pocoo.org/) web application (`Prime-flask`) was created for this demonstration which contains two endpoints: 60 | 61 | `http://:8080/factorial/` and 62 | 63 | `http://:8080/prime/` 64 | 65 | Examples of use would look like: 66 | 67 | ```console 68 | curl http://35.227.149.80:8080/prime/10 69 | The sum of all primes less than 10 is 17 70 | 71 | curl http://35.227.149.80:8080/factorial/10 72 | The factorial of 10 is 3628800 73 | ``` 74 | 75 | Also included is a utility to validate a successful deployment. 76 | 77 | ## Prerequisites 78 | 79 | ### Run Demo in a Google Cloud Shell 80 | 81 | Click the button below to run the demo in a [Google Cloud Shell](https://cloud.google.com/shell/docs/). 82 | 83 | [![Open in Cloud Shell](http://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/open?cloudshell_git_repo=https://github.com/GoogleCloudPlatform/gke-migration-to-containers.git&cloudshell_image=gcr.io/graphite-cloud-shell-images/terraform:latest&cloudshell_tutorial=README.md) 84 | 85 | 86 | All the tools for the demo are installed. When using Cloud Shell execute the following 87 | command in order to setup gcloud cli. When executing this command please setup your region 88 | and zone. 89 | 90 | ```console 91 | gcloud init 92 | ``` 93 | 94 | ### Get The Code 95 | 96 | * [Fork the repo](https://help.github.com/articles/fork-a-repo/) 97 | * [Clone your fork](https://help.github.com/articles/cloning-a-repository/) 98 | 99 | ### Tools 100 | In order to use the code in this demo you will need access to the following tools: 101 | 102 | * A bash, or bash-compatible, shell 103 | * Access to an existing Google Cloud project with the 104 | [Kubernetes Engine v1.10.0 or later](https://cloud.google.com/kubernetes-engine/docs/quickstart#before-you-begin) service enabled 105 | * If you do not have a Google Cloud Platform account you can sign up [here](https://cloud.google.com) and get 300 dollars of free credit on your new account. 106 | * [Google Cloud SDK version >= 253.0.0](https://cloud.google.com/sdk/docs/downloads-versioned-archives) 107 | * [ApacheBench](https://httpd.apache.org/docs/2.4/programs/ab.html) 108 | * [Hashicorp Terraform >= 0.12.3](https://www.terraform.io/downloads.html) 109 | * [gcloud](https://cloud.google.com/sdk/gcloud/) 110 | * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 111 | 112 | #### Install Cloud SDK 113 | The Google Cloud SDK is used to interact with your GCP resources. 114 | [Installation instructions](https://cloud.google.com/sdk/downloads) for multiple platforms are available online. 115 | 116 | #### Install kubectl CLI 117 | 118 | The kubectl CLI is used to interteract with both Kubernetes Engine and kubernetes in general. 119 | [Installation instructions](https://cloud.google.com/kubernetes-engine/docs/quickstart) 120 | for multiple platforms are available online. 121 | 122 | #### Install Terraform 123 | 124 | Terraform is used to automate the manipulation of cloud infrastructure. Its 125 | [installation instructions](https://www.terraform.io/intro/getting-started/install.html) are also available online. 126 | 127 | ### Authenticate gcloud 128 | 129 | Prior to running this demo, ensure you have authenticated your gcloud client by running the following command: 130 | 131 | ```console 132 | gcloud auth application-default login 133 | ``` 134 | 135 | ## Deployment 136 | 137 | The infrastructure required by this project can be deployed by executing: 138 | ```console 139 | make create 140 | ``` 141 | 142 | This will: 143 | 1. Package the deployable `Prime-flask` application. 144 | 1. Create the container image and push it to the private [Container Registry (GCR)](https://cloud.google.com/container-registry/) for your project. 145 | 1. Generate an appropriate configuration for [Terraform](https://www.terraform.io). 146 | 1. Execute Terraform which creates the three deployments. 147 | 148 | ![screenshot](./images/setup.png) 149 | 150 | ![screenshot](./images/setup-2.png) 151 | 152 | ![screenshot](./images/setup-success.png) 153 | 154 | ## Validation 155 | 156 | Now that the application is deployed, we can validate these three deployments by executing: 157 | 158 | ```console 159 | make validate 160 | ``` 161 | 162 | A successful output will look like this: 163 | ```console 164 | Validating Debian VM Webapp... 165 | Testing endpoint http://35.227.149.80:8080 166 | Endpoint http://35.227.149.80:8080 is responding. 167 | **** http://35.227.149.80:8080/prime/10 168 | The sum of all primes less than 10 is 17 169 | The factorial of 10 is 3628800 170 | 171 | Validating Container OS Webapp... 172 | Testing endpoint http://35.230.123.231:8080 173 | Endpoint http://35.230.123.231:8080 is responding. 174 | **** http://35.230.123.231:8080/prime/10 175 | The sum of all primes less than 10 is 17 176 | The factorial of 10 is 3628800 177 | 178 | Validating Kubernetes Webapp... 179 | Testing endpoint http://35.190.89.136 180 | Endpoint http://35.190.89.136 is responding. 181 | **** http://35.190.89.136/prime/10 182 | The sum of all primes less than 10 is 17 183 | The factorial of 10 is 3628800 184 | ``` 185 | 186 | Of course, the IP addresses will likely differ for your deployment. 187 | 188 | ## Load Testing 189 | 190 | In a new console window, execute the following, replacing `[IP_ADDRESS]` with the IP address and port from your validation output from the previous step. Note that the Kubernetes deployment runs on port `80`, while the other two deployments run on port `8080`: 191 | ```console 192 | ab -c 120 -t 60 http:///prime/10000 193 | ``` 194 | ApacheBench (`ab`) will execute 120 concurrent requests against the provided endpoint for 1 minute. The demo application's replica is insufficiently sized to handle this volume of requests. 195 | 196 | This can be confirmed by reviewing the output from the `ab` command. A `Failed requests` value of greater than 0 means that the server couldn't respond successfully to this load: 197 | 198 | ![screenshot](./images/ab_load-test-1.png) 199 | 200 | One way to ensure that your system has capacity to handle this type of traffic is by scaling up. In this case, we would want to scale our service horizontally. 201 | 202 | In our Debian and COS architectures, horizontal scaling would include: 203 | 204 | 1. Creating a load balancer. 205 | 1. Spinning up additional instances. 206 | 1. Registering them with the load balancer. 207 | 208 | This is an involved process and is out of scope for this demonstration. 209 | 210 | For the third (Kubernetes) deployment the process is far easier: 211 | 212 | ```console 213 | kubectl scale --replicas 3 deployment/prime-server 214 | ``` 215 | 216 | After allowing 30 seconds for the replicas to initialize, re-run the load test: 217 | 218 | ```console 219 | ab -c 120 -t 60 http:///prime/10000 220 | ``` 221 | Notice how the `Failed requests` is now 0. This means that all of the 10,000+ requests were successfully answered by the server: 222 | 223 | ![screenshot](./images/ab_load-test-2.png) 224 | 225 | ## Tear Down 226 | 227 | When you are finished with this example you will want to clean up the resources that were created so that you avoid accruing charges: 228 | 229 | ``` 230 | $ make teardown 231 | ``` 232 | 233 | It will run `terraform destroy` which will destroy all of the resources created for this demonstration. 234 | 235 | ![screenshot](./images/tear-down.png) 236 | 237 | ## More Info 238 | For additional information see: [Embarking on a Journey Towards Containerizing Your Workloads](https://www.youtube.com/watch?v=_aFA-p87Eec&index=22&list=PLBgogxgQVM9uSqzLOc66kNIZMUOvnGbU4) 239 | 240 | ## Troubleshooting 241 | 242 | Occasionally the APIs take a few moments to complete. Running `make validate` immediately could potentially appear to fail, but in fact the instances haven't finished initializing. Waiting for a minute or two should resolve the issue. 243 | 244 | The setup of this demo **does** take up to **15** minutes. If there is no error the best thing to do is keep waiting. The execution of `make create` should **not** be interrupted. 245 | 246 | If you do get an error, it probably makes sense to re-execute the failing script. Occasionally there are network connectivity issues, and retrying will likely work the subsequent time. 247 | 248 | **This is not an officially supported Google product** 249 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.2 2 | -------------------------------------------------------------------------------- /container/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM python:3.7-alpine 16 | 17 | ARG BUILD_DATE 18 | ARG VCS_REF 19 | 20 | LABEL \ 21 | org.label-schema.build-date=$BUILD_DATE \ 22 | org.label-schema.docker.dockerfile="/Dockerfile" \ 23 | org.label-schema.license="Apache License 2.0" \ 24 | org.label-schema.name="Prime python-flask container" \ 25 | org.label-schema.url="https://github.com/GoogleCloudPlatform/" \ 26 | org.label-schema.vcs-ref=$VCS_REF \ 27 | org.label-schema.vcs-type="Git" \ 28 | org.label-schema.vcs-url="https://github.com/GoogleCloudPlatform/gke-migration-to-containers" 29 | 30 | COPY prime-flask-server.py requirements.txt /opt/ 31 | 32 | RUN adduser -D apprunner && \ 33 | apk add --no-cache \ 34 | bash=5.1.16-r2 \ 35 | dumb-init=1.2.5-r1 && \ 36 | pip3 install -r /opt/requirements.txt && \ 37 | chown -R apprunner:apprunner /opt/ 38 | 39 | WORKDIR /opt 40 | USER apprunner 41 | EXPOSE 8080 42 | 43 | ENTRYPOINT ["/usr/bin/dumb-init", "/usr/local/bin/gunicorn", "--bind", "0.0.0.0:8080", "prime-flask-server"] 44 | -------------------------------------------------------------------------------- /container/README.md: -------------------------------------------------------------------------------- 1 | How to build and test the container. 2 | 3 | ### Environment Variables 4 | 5 | Environment variable defined in `gke-migration-to-containers/scripts/create.sh` 6 | 7 | | ENV VARS | Value | 8 | |----------|-------| 9 | | ROOT | . | 10 | | VERSION | 0.0.1 | 11 | | BUILD_DATE | $(date) | 12 | | VCS_REF | NO-VCS or $(git rev-parse HEAD) | 13 | 14 | ### Cloud Build 15 | If building from the container directory change `ROOT` to `..` 16 | 17 | ```bash 18 | gcloud builds submit "$ROOT/container" \ 19 | --config="${ROOT}/container/cloudbuild.yaml" \ 20 | --substitutions _VERSION="${VERSION}",_BUILD_DATE="${BUILD_DATE}",_VCS_REF="${VCS_REF}" 21 | ``` 22 | 23 | ### Run Locally 24 | 25 | In the below command ensure the environment variable `$GOOGLE_CLOUD_PROJECT` is set 26 | 27 | ```bash 28 | docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/prime-flask:0.0.1 29 | ``` 30 | -------------------------------------------------------------------------------- /container/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: 'gcr.io/cloud-builders/docker' 17 | args: ['build', '--build-arg', 'VCS_REF=${_VCS_REF}', '--build-arg', 'BUILD_DATE=${_BUILD_DATE}', '-t', 'gcr.io/$PROJECT_ID/prime-flask:${_VERSION}', '.'] 18 | timeout: 500s 19 | images: ['gcr.io/$PROJECT_ID/prime-flask:${_VERSION}'] 20 | -------------------------------------------------------------------------------- /container/install.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Check if required binaries exist 18 | # Globals: 19 | # None 20 | # Arguments: 21 | # DEPENDENCY - The command to verify is installed. 22 | # Returns: 23 | # None 24 | check_dependency_installed () { 25 | command -v "${1}" >/dev/null 2>&1 || { \ 26 | echo >&2 "${1} is required but is not installed. Aborting."; exit 1; } 27 | } 28 | 29 | ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 30 | 31 | check_dependency_installed "python3" 32 | check_dependency_installed "pip3" 33 | 34 | pip3 install -r "${ROOT}/requirements.txt" 35 | -------------------------------------------------------------------------------- /container/prime-flask-server.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """ 15 | A simple service that responds with whether an input (post) is 16 | prime or not. 17 | 18 | Usage:: 19 | ./web-server.py [port] 20 | 21 | Send a GET request:: 22 | curl http://localhost 23 | 24 | Send a HEAD request:: 25 | curl -I http://localhost 26 | 27 | Send a POST request:: 28 | curl -d "num=4" http://localhost 29 | """ 30 | 31 | from math import factorial 32 | from flask import Flask 33 | application = Flask(__name__) 34 | 35 | 36 | def is_prime(number): 37 | if number == 2 or number == 3: 38 | return True 39 | if number < 2 or number % 2 == 0: 40 | return False 41 | if number < 9: 42 | return True 43 | if number % 3 == 0: 44 | return False 45 | r = int(number**0.5) 46 | f = 5 47 | while f <= r: 48 | if number % f == 0: 49 | return False 50 | if number % (f+2) == 0: 51 | return False 52 | f += 6 53 | return True 54 | 55 | 56 | def sum_primes(num): 57 | sum = 0 58 | while (num > 1): 59 | if is_prime(num): 60 | sum = sum+num 61 | num = num - 1 62 | return sum 63 | 64 | 65 | @application.route("/factorial/") 66 | def get_factorial(num): 67 | return 'The factorial of %d is %d' % (num, factorial(num)) 68 | 69 | 70 | @application.route("/") 71 | def hello(): 72 | return "Server successfully started!" 73 | 74 | 75 | @application.route("/prime/") 76 | def detect_prime(num): 77 | return 'The sum of all primes less than %d is %d' % (num, sum_primes(num)) 78 | -------------------------------------------------------------------------------- /container/primeserver.service: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This service file is used by the vm example to start and manage the 16 | # prime server process 17 | 18 | [Unit] 19 | Description=Prime Server Application 20 | After=network.target 21 | 22 | [Service] 23 | Type=simple 24 | Restart=always 25 | User=apprunner 26 | WorkingDirectory=/prime 27 | ExecStart=/usr/local/bin/gunicorn --bind 0.0.0.0:8080 prime-flask-server 28 | 29 | [Install] 30 | WantedBy=multi-user.target 31 | -------------------------------------------------------------------------------- /container/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==2.0.3 2 | gunicorn==20.1.0 -------------------------------------------------------------------------------- /images/Debian-deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/Debian-deployment.png -------------------------------------------------------------------------------- /images/ab_load-test-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/ab_load-test-1.png -------------------------------------------------------------------------------- /images/ab_load-test-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/ab_load-test-2.png -------------------------------------------------------------------------------- /images/cos-deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/cos-deployment.png -------------------------------------------------------------------------------- /images/gke-deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/gke-deployment.png -------------------------------------------------------------------------------- /images/load_test-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/load_test-1.png -------------------------------------------------------------------------------- /images/load_test-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/load_test-2.png -------------------------------------------------------------------------------- /images/setup-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/setup-2.png -------------------------------------------------------------------------------- /images/setup-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/setup-success.png -------------------------------------------------------------------------------- /images/setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/setup.png -------------------------------------------------------------------------------- /images/tear-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/tear-down.png -------------------------------------------------------------------------------- /images/validate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/gke-migration-to-containers/4fcf652689093cfd9cc9473140b6c55ac4c85965/images/validate.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/common.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # "---------------------------------------------------------" 18 | # "- -" 19 | # "- Common commands for all scripts -" 20 | # "- -" 21 | # "---------------------------------------------------------" 22 | 23 | # Check if required binaries exist 24 | # Globals: 25 | # None 26 | # Arguments: 27 | # DEPENDENCY - The command to verify is installed. 28 | # Returns: 29 | # None 30 | check_dependency_installed () { 31 | command -v "${1}" >/dev/null 2>&1 || { \ 32 | echo >&2 "${1} is required but is not installed. Aborting."; exit 1; } 33 | } 34 | 35 | # Helper function to enable a given service for a given project 36 | # Globals: 37 | # None 38 | # Arguments: 39 | # PROJECT - ID of the project in which to enable the API 40 | # API - Name of the API to enable, e.g. compute.googleapis.com 41 | # Returns: 42 | # None 43 | enable_project_api() { 44 | gcloud services enable "${2}" --project "${1}" 45 | } 46 | 47 | # Helper function to return configured GCP project 48 | # Globals: 49 | # None 50 | # Arguments: 51 | # None 52 | # Returns: 53 | # PROJECT - name of the currently configured project 54 | get_project() { 55 | # gcloud config holds values related to your environment. If you already 56 | # defined a default project we will retrieve it and use it 57 | PROJECT=$(gcloud config get-value core/project) 58 | if [[ -z "${PROJECT}" ]]; then 59 | echo "gcloud cli must be configured with a default project." 1>&2 60 | echo "run 'gcloud config set core/project PROJECT'." 1>&2 61 | echo "replace 'PROJECT' with the project name." 1>&2 62 | exit 1; 63 | fi 64 | return 0 65 | } 66 | -------------------------------------------------------------------------------- /scripts/create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # Stop immediately if something goes wrong 17 | set -euo pipefail 18 | 19 | run_terraform() { 20 | # # Terraform steps: 21 | # TFVARS_FILE="${ROOT}/terraform/terraform.tfvars" 22 | # 23 | # # Remove a pre-existing tfvars file, if it exists 24 | # if [[ -f "${TFVARS_FILE}" ]] 25 | # then 26 | # rm ${TFVARS_FILE} 27 | # fi 28 | 29 | # shellcheck source=scripts/generate-tfvars.sh 30 | source "${ROOT}/scripts/generate-tfvars.sh" 31 | 32 | VERSION=$(cat "${ROOT}/VERSION") 33 | 34 | (cd "$ROOT/terraform"; terraform init -input=false) 35 | (cd "$ROOT/terraform"; terraform apply -input=false -auto-approve \ 36 | -var ver="$VERSION") 37 | } 38 | 39 | generate_static_ip() { 40 | # Check to make sure static IP hasn't already been created, then register it. 41 | if [[ $(gcloud compute addresses list | grep '^NAME:\sprime-server') = '' ]]; then 42 | gcloud compute addresses create prime-server --global 43 | fi 44 | } 45 | 46 | wait_for_cluster() { 47 | # Provisioning a Kubernetes Engine ingress can take some time as a Layer 7 48 | # http load balancer must be configured. This script will check and loop-retry 49 | # getting the external ip address associated with the load balancer then check 50 | # and loop-retry waiting for the endpoint to become healthy. 51 | 52 | echo "====================================================" 53 | echo "Initializing the test application. Please be patient, it takes a few\ 54 | minutes to complete." 55 | echo "====================================================" 56 | 57 | # The outer loop checks for the external IP address associated with the 58 | # HTTP load balancer created by the Kubernetes Engine prime-server ingress. It 59 | # should be created relatively quickly so the loop sleeps for 10 seconds and 60 | # error times out after 100 seconds. 61 | ELAPSED=0 62 | SLEEP=10 63 | MAX_TIME=100 64 | while true; do 65 | 66 | IP_ADDRESS=$(kubectl get ingress prime-server --namespace \ 67 | default -o jsonpath='{.status.loadBalancer.ingress..ip}') 68 | if [[ -n "$IP_ADDRESS" ]]; then 69 | SERVER_ADDRESS="http://${IP_ADDRESS}" 70 | echo "$SERVER_ADDRESS provisioned!" 71 | 72 | # This inner loop is to wait for the server to respond to a health check. 73 | # This can take much longer so we animate the cursor to ensure that the 74 | # user can see that the script has not timed out. We do not have an error 75 | # timeout for acquiring a health check because sometimes this step takes 76 | # unusually long. 77 | while true; do 78 | echo -ne 'waiting for endpoint to become healthy: |\r' 79 | sleep 1 80 | echo -ne 'waiting for endpoint to become healthy: /\r' 81 | sleep 1 82 | echo -ne 'waiting for endpoint to become healthy: -\r' 83 | sleep 1 84 | echo -ne 'waiting for endpoint to become healthy: \\\r' 85 | sleep 1 86 | if [[ "$(curl -s -o /dev/null -w "%{http_code}" "$SERVER_ADDRESS"/)" == \ 87 | "200" ]]; then 88 | break 89 | fi 90 | done 91 | echo "" 92 | echo "SUCCESS! $SERVER_ADDRESS is now healthy" 93 | break 94 | fi 95 | if [[ "${ELAPSED}" -gt "${MAX_TIME}" ]]; then 96 | echo "ERROR: ${MAX_TIME} seconds exceeded, no response from kubernetes api." 97 | exit 1 98 | fi 99 | echo "After ${ELAPSED} seconds, endpoint not yet provisioned, waiting..." 100 | sleep "$SLEEP" 101 | ELAPSED=$(( ELAPSED + SLEEP )) 102 | done 103 | } 104 | 105 | wait_for_service() { 106 | echo "** Checking for Kubernetes service **" 107 | RESPONSE="" 108 | EXPECTED="Server successfully started!" 109 | COUNTER=0 110 | for _ in {1..60} 111 | do 112 | # An ingress is created that has two backends, they do not become healthy 113 | # at once, and some subsequent calls lead to 404. We want to check service 114 | # endpoint to respond and stop giving 404 at least 20 times. 115 | echo -ne 'waiting for endpoint to become healthy: |\r' 116 | sleep 1 117 | echo -ne 'waiting for endpoint to become healthy: /\r' 118 | sleep 1 119 | echo -ne 'waiting for endpoint to become healthy: -\r' 120 | sleep 1 121 | echo -ne 'waiting for endpoint to become healthy: \\\r' 122 | sleep 1 123 | if [[ "$(curl -s -o /dev/null -w "%{http_code}" "$IP_ADDRESS"/)" == \ 124 | "200" ]]; then 125 | COUNTER=$((COUNTER+1)) 126 | if [ "$COUNTER" == 10 ]; then 127 | break 128 | fi 129 | else 130 | COUNTER=0 # Our counter needs to be reset until we get only 200's for 20 131 | # consecutive times 132 | fi 133 | done 134 | 135 | RESPONSE=$(curl -s "$IP_ADDRESS/") 136 | if [ "$RESPONSE" != "$EXPECTED" ] 137 | then 138 | echo "ERROR - Service failed to start correctly within allotted time" 139 | echo "=> $RESPONSE" 140 | exit 1 141 | fi 142 | 143 | echo "** Kubernetes service is up! ** " 144 | } 145 | 146 | run_build() { 147 | [ -d "${ROOT}/build" ] || mkdir "${ROOT}/build" 148 | tar -cvzf "$ROOT/build/flask-prime.tgz" -C "$ROOT/container" . 149 | 150 | # Cloud Build! 151 | BUILD_DATE=$(date) 152 | if command -v git >/dev/null; then 153 | VCS_REF=$(git rev-parse HEAD) 154 | else 155 | VCS_REF="NO-VCS" 156 | fi 157 | 158 | echo "Building container for prime-server version ${VERSION}" 159 | gcloud builds submit "$ROOT/container" \ 160 | --config="${ROOT}/container/cloudbuild.yaml" \ 161 | --substitutions _VERSION="${VERSION}",_BUILD_DATE="${BUILD_DATE}",_VCS_REF="${VCS_REF}" 162 | } 163 | 164 | ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" 165 | # shellcheck source=scripts/common.sh 166 | source "$ROOT/scripts/common.sh" 167 | 168 | VERSION=$(cat "${ROOT}/VERSION") 169 | 170 | check_dependency_installed gcloud 171 | check_dependency_installed terraform 172 | 173 | # Enable required GCP services 174 | get_project 175 | enable_project_api "${PROJECT}" compute.googleapis.com 176 | enable_project_api "${PROJECT}" container.googleapis.com 177 | enable_project_api "${PROJECT}" cloudbuild.googleapis.com 178 | 179 | run_build 180 | 181 | run_terraform 182 | 183 | generate_static_ip 184 | 185 | kubectl apply -f "${ROOT}/terraform/manifests/" --namespace default 186 | 187 | wait_for_cluster 188 | 189 | wait_for_service 190 | -------------------------------------------------------------------------------- /scripts/generate-tfvars.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # "---------------------------------------------------------" 18 | # "- -" 19 | # "- Helper script to generate terraform variables -" 20 | # "- file based on glcoud defaults. -" 21 | # "- -" 22 | # "---------------------------------------------------------" 23 | 24 | # "---------------------------------------------------------" 25 | # "- -" 26 | # "- Helper script to generate terraform variables -" 27 | # "- file based on glcoud defaults. -" 28 | # "- -" 29 | # "---------------------------------------------------------" 30 | 31 | # Stop immediately if something goes wrong 32 | set -euo pipefail 33 | 34 | # The purpose of this script is to populate variables for subsequent terraform 35 | # commands. 36 | 37 | ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" 38 | # shellcheck source=scripts/common.sh 39 | source "$ROOT/scripts/common.sh" 40 | 41 | check_dependency_installed gcloud 42 | 43 | # gcloud config holds values related to your environment. If you already 44 | # defined a default zone we will retrieve it and use it 45 | ZONE="$(gcloud config get-value compute/zone)" 46 | if [[ -z "${ZONE}" ]]; then 47 | echo "https://cloud.google.com/compute/docs/regions-zones/changing-default-zone-region" 1>&2 48 | echo "gcloud cli must be configured with a default zone." 1>&2 49 | echo "run 'gcloud config set compute/zone ZONE'." 1>&2 50 | echo "replace 'ZONE' with the zone name like us-west1-a." 1>&2 51 | exit 1; 52 | fi 53 | 54 | # Write out the file now in the terraform directory. 55 | cat < "${ROOT}/terraform/terraform.tfvars" 56 | project="${PROJECT}" 57 | zone="${ZONE}" 58 | EOF 59 | -------------------------------------------------------------------------------- /scripts/teardown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # Stop immediately if something goes wrong 17 | set -euo pipefail 18 | 19 | ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" 20 | # shellcheck source=scripts/common.sh 21 | source "$ROOT/scripts/common.sh" 22 | 23 | VERSION=$(cat "${ROOT}/VERSION") 24 | 25 | rm -f "${ROOT}/terraform/manifests/prime-server-deployment.yaml" 26 | 27 | check_dependency_installed terraform 28 | 29 | (cd "${ROOT}/terraform" && terraform destroy \ 30 | -var ver="$VERSION" -auto-approve) 31 | -------------------------------------------------------------------------------- /scripts/validate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2018 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # bash "strict-mode", fail immediately if there is a problem 17 | set -euo pipefail 18 | 19 | ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" 20 | # shellcheck source=scripts/common.sh 21 | source "$ROOT/scripts/common.sh" 22 | 23 | # health_check() - Wait for a server to respond with a 200 status code at a 24 | # given endpoint. It will periodically retry until the MAX_TIME is exceeded. 25 | # Usage: 26 | # health_check 27 | # Where: 28 | # is of the form 'http://:' 29 | # Returns: 30 | # 0 - when the server responds with a 200 status code 31 | # 1 - when the MAX_TIME is exceeded before a 200 status code is returned 32 | health_check() { 33 | SERVER_ADDRESS=$1 34 | ELAPSED=0 35 | SLEEP=10 36 | MAX_TIME=${MAX_TIME:-100} 37 | echo "Testing endpoint ${SERVER_ADDRESS}" 38 | while true; do 39 | HTTP_CODE=$(curl -s -o /dev/null -I --max-time 1 -w "%{http_code}" \ 40 | "${SERVER_ADDRESS}" || true) 41 | if [[ "${HTTP_CODE}" == "200" ]]; then 42 | echo "Endpoint ${SERVER_ADDRESS} is responding." 43 | return 0 44 | fi 45 | if [[ "${ELAPSED}" -gt "${MAX_TIME}" ]]; then 46 | echo "ERROR: ${MAX_TIME} seconds exceeded, no response from ${SERVER_ADDRESS}" 47 | exit 1 48 | fi 49 | echo "After ${ELAPSED} seconds, endpoint not yet healthy, waiting..." 50 | sleep "$SLEEP" 51 | ELAPSED=$(( ELAPSED + SLEEP )) 52 | done 53 | } 54 | 55 | # validate_prime() - validate that the prime server returns the correct result 56 | # Usage: 57 | # validate_prime 58 | # Where: 59 | # is of the form 'http://:' 60 | # Returns: 61 | # 0 - when the server responds with the correct answer to sum of primes less 62 | # than 10 63 | # 1 - when the server responsds with an incorrect answer 64 | validate_prime() { 65 | SERVER_ADDRESS=$1 66 | # Construct the test URL 67 | PRIME_TEST="${SERVER_ADDRESS}/prime/10" 68 | 69 | # Test the URL 70 | PRIME_RESPONSE=$(curl "${PRIME_TEST}" 2>/dev/null) 71 | 72 | # Extract the answer from the server response 73 | SUM=$(echo "${PRIME_RESPONSE}" | tr " " "\\n" | tail -n1) 74 | 75 | # Make sure that 'Sum of primes less than 10' == 17 76 | if [[ "${SUM}" != "17" ]]; then 77 | echo "ERROR: Sum of Primes less than 10 is 17, got ${SUM}" 78 | return 1 79 | fi 80 | 81 | # If we have made it this far, all is good, output the server responses. 82 | echo "${PRIME_RESPONSE}" 83 | return 0 84 | } 85 | 86 | # validate_factorial() - validate that the factorial server returns the correct 87 | # result 88 | # Usage: 89 | # validate_factorial 90 | # Where: 91 | # is of the form 'http://:' 92 | # Returns: 93 | # 0 - when the server responds with the correct answer for 10! 94 | # 1 - when the server responsds with an incorrect answer for 10! 95 | validate_factorial() { 96 | SERVER_ADDRESS=$1 97 | # Construct the test URL 98 | FACTORIAL_TEST="${SERVER_ADDRESS}/factorial/10" 99 | 100 | # Test the URL 101 | FACTORIAL_RESPONSE=$(curl "${FACTORIAL_TEST}" 2>/dev/null) 102 | 103 | # Extract the answer from the server response 104 | FACTORIAL=$(echo "${FACTORIAL_RESPONSE}" | tr " " "\\n" | tail -n1) 105 | 106 | # Make sure that 10! == 3628800 107 | if [[ "${FACTORIAL}" != "3628800" ]]; then 108 | echo "ERROR: Factorial of 10 is 3628800, got ${FACTORIAL}" 109 | return 1 110 | fi 111 | 112 | # If we have made it this far, all is good, output the server responses. 113 | echo "${FACTORIAL_RESPONSE}" 114 | } 115 | 116 | # validate_deployment() - given a server address, it runs health_check(), 117 | # validate_prime(), and validate_factorial() 118 | validate_deployment() { 119 | ADDRESS=$1 120 | health_check "${ADDRESS}" 121 | validate_prime "${ADDRESS}" 122 | validate_factorial "${ADDRESS}" 123 | } 124 | 125 | check_dependency_installed terraform 126 | 127 | # Each of the three deployments are validated 128 | echo "" 129 | echo "Validating Debian VM Webapp..." 130 | SERVER_ADDRESS=$(cd "$ROOT/terraform" && terraform output web_server_address) 131 | # Remove surrounding quotes from URL 132 | SERVER_ADDRESS="${SERVER_ADDRESS%\"}" 133 | SERVER_ADDRESS="${SERVER_ADDRESS#\"}" 134 | validate_deployment "$SERVER_ADDRESS" 135 | 136 | echo "" 137 | echo "Validating Container OS Webapp..." 138 | SERVER_ADDRESS=$(cd "$ROOT/terraform" && terraform output cos_server_address) 139 | # Remove surrounding quotes from URL 140 | SERVER_ADDRESS="${SERVER_ADDRESS%\"}" 141 | SERVER_ADDRESS="${SERVER_ADDRESS#\"}" 142 | validate_deployment "$SERVER_ADDRESS" 143 | 144 | echo "" 145 | echo "Validating Kubernetes Webapp..." 146 | SERVER_IP=$(kubectl get ingress prime-server \ 147 | --namespace default \ 148 | -o jsonpath='{.status.loadBalancer.ingress..ip}') 149 | SERVER_ADDRESS="http://${SERVER_IP}" 150 | validate_deployment "$SERVER_ADDRESS" 151 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Provides access to available Google Container Engine versions in a zone for a given project. 18 | // https://www.terraform.io/docs/providers/google/d/google_container_engine_versions.html 19 | data "google_container_engine_versions" "on-prem" { 20 | location = var.zone 21 | project = var.project 22 | } 23 | 24 | // https://www.terraform.io/docs/providers/template/index.html 25 | // This block defines the startup script used to pull a container 26 | // image from a private repo and install it as a systemd service. 27 | data "template_file" "startup_script" { 28 | template = < ${path.module}/manifests/prime-server-deployment.yaml" 206 | } 207 | 208 | 209 | provisioner "local-exec" { 210 | command = "echo \"${data.template_file.deployment_manifest.rendered}\" > ${path.module}/manifests/prime-server-deployment.yaml" 211 | } 212 | } 213 | 214 | resource "null_resource" "local_config" { 215 | provisioner "local-exec" { 216 | command = "gcloud container clusters get-credentials prime-server-cluster --project ${var.project}" 217 | } 218 | depends_on = [google_container_cluster.prime_cluster] 219 | } 220 | 221 | // This bucket will hold the deployment artifact, the tar file containing the 222 | // prime-server 223 | // 224 | resource "google_storage_bucket" "artifact_store" { 225 | name = "${var.project}-vm-artifacts" 226 | project = var.project 227 | # force_destroy = true 228 | } 229 | 230 | // https://www.terraform.io/docs/providers/google/r/storage_bucket_object.html 231 | resource "google_storage_bucket_object" "artifact" { 232 | name = "${var.ver}/flask-prime.tgz" 233 | source = "../build/flask-prime.tgz" 234 | bucket = google_storage_bucket.artifact_store.name 235 | // TODO: ignore lifecycle something so old versions don't get deleted 236 | } 237 | 238 | data "template_file" "web_init" { 239 | template = file("${path.module}/web-init.sh.tmpl") 240 | vars = { 241 | bucket = "${var.project}-vm-artifacts" 242 | version = var.ver 243 | } 244 | } 245 | 246 | // https://www.terraform.io/docs/providers/google/r/compute_instance.html 247 | resource "google_compute_instance" "web_server" { 248 | project = var.project 249 | name = "vm-webserver" 250 | machine_type = var.machine_type 251 | zone = var.zone 252 | 253 | tags = ["flask-web"] 254 | 255 | boot_disk { 256 | initialize_params { 257 | image = "debian-cloud/debian-10" 258 | } 259 | } 260 | 261 | network_interface { 262 | network = "default" 263 | access_config { 264 | // leave this block empty to get an automatically generated ephemeral 265 | // external IP 266 | } 267 | } 268 | 269 | 270 | // install pip and flask 271 | metadata_startup_script = data.template_file.web_init.rendered 272 | 273 | 274 | service_account { 275 | scopes = ["storage-ro", "compute-rw"] 276 | } 277 | 278 | depends_on = [ 279 | google_storage_bucket.artifact_store, 280 | google_storage_bucket_object.artifact, 281 | ] 282 | } 283 | 284 | // https://www.terraform.io/docs/providers/google/r/compute_firewall.html 285 | resource "google_compute_firewall" "flask_web" { 286 | name = "flask-web" 287 | network = "default" 288 | project = var.project 289 | allow { 290 | protocol = "tcp" 291 | ports = ["8080"] 292 | } 293 | 294 | source_ranges = ["0.0.0.0/0"] 295 | source_tags = ["flask-web"] 296 | } 297 | -------------------------------------------------------------------------------- /terraform/manifests/prime-server-ingress.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Create an ingress to access the prime-server over the internet. 16 | # https://kubernetes.io/docs/concepts/services-networking/ingress/ 17 | # https://cloud.google.com/kubernetes-engine/docs/tutorials/http-balancer 18 | apiVersion: networking.k8s.io/v1 19 | kind: Ingress 20 | metadata: 21 | name: prime-server 22 | annotations: 23 | kubernetes.io/ingress.global-static-ip-name: "prime-server" 24 | spec: 25 | defaultBackend: 26 | service: 27 | name: prime-server 28 | port: 29 | number: 8080 30 | -------------------------------------------------------------------------------- /terraform/manifests/prime-server-svc.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Service to expose the prime server http listen port 16 | # https://kubernetes.io/docs/concepts/services-networking/service/ 17 | apiVersion: v1 18 | kind: Service 19 | metadata: 20 | name: prime-server 21 | labels: 22 | app: prime-server 23 | spec: 24 | selector: 25 | app: prime-server 26 | ports: 27 | - port: 8080 28 | targetPort: 8080 29 | protocol: TCP 30 | name: http 31 | type: NodePort 32 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | output "prime_web_server" { 17 | value = "http://${google_compute_instance.web_server.network_interface[0].access_config[0].nat_ip}:8080/prime" 18 | } 19 | 20 | output "factorial_web_server" { 21 | value = "http://${google_compute_instance.web_server.network_interface[0].access_config[0].nat_ip}:8080/factorial" 22 | } 23 | 24 | output "web_server_address" { 25 | value = "http://${google_compute_instance.web_server.network_interface[0].access_config[0].nat_ip}:8080" 26 | } 27 | 28 | output "prime_cos_server" { 29 | value = "http://${google_compute_instance.container_server.network_interface[0].access_config[0].nat_ip}:8080/prime" 30 | } 31 | 32 | output "factorial_cos_server" { 33 | value = "http://${google_compute_instance.container_server.network_interface[0].access_config[0].nat_ip}:8080/factorial" 34 | } 35 | 36 | output "cos_server_address" { 37 | value = "http://${google_compute_instance.container_server.network_interface[0].access_config[0].nat_ip}:8080" 38 | } 39 | -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // provider.tf - by convention, this is the file where providers are defined. 18 | // All data sources and resources are dependent on a provider. Here, we 19 | // specify the version of the Google Cloud Provider that was used to 20 | // develop this example. 21 | 22 | provider "google" { 23 | version = "~> 2.11.0" 24 | } 25 | 26 | provider "null" { 27 | version = "~> 2.1.2" 28 | } 29 | 30 | provider "template" { 31 | version = "~> 2.1.2" 32 | } 33 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // variables.tf - this is where all variables are defined. The user must 18 | // provide these for any invocation of `terraform plan`, `apply`, or `destroy`. 19 | 20 | variable "cluster_name" { 21 | description = "The Kubernetes Engine cluster name" 22 | default = "prime-server-cluster" 23 | } 24 | 25 | variable "machine_type" { 26 | default = "e2-micro" 27 | } 28 | 29 | variable "gke_machine_type" { 30 | default = "e2-standard-2" 31 | } 32 | 33 | variable "project" { 34 | type = string 35 | } 36 | 37 | variable "replicas" { 38 | description = "Number of prime server replicas to create" 39 | default = "1" 40 | } 41 | 42 | variable "ver" { 43 | type = string 44 | } 45 | 46 | variable "zone" { 47 | type = string 48 | } 49 | -------------------------------------------------------------------------------- /terraform/versions.tf: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | terraform { 18 | required_version = ">= 0.12" 19 | } 20 | -------------------------------------------------------------------------------- /terraform/web-init.sh.tmpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2018 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # A newly booted VM will need to update package lists 17 | sudo apt-get update 18 | 19 | # Debian10 comes wth python3.7 by default, this installs the appropriate pip 20 | sudo apt-get install python3-pip -y 21 | 22 | # Create a user to run the application 23 | sudo adduser apprunner 24 | 25 | # Download the artifact 26 | sudo mkdir /prime 27 | gsutil cp gs://${bucket}/${version}/flask-prime.tgz /prime/flask-prime.tgz 28 | cd /prime 29 | sudo tar xzvf flask-prime.tgz 30 | sudo /prime/install.sh 31 | sudo chown -R apprunner:apprunner /prime 32 | 33 | # Create a service to run and manage the process 34 | sudo cp primeserver.service /lib/systemd/system 35 | sudo systemctl start primeserver 36 | # Enable the service so it will start on reboot 37 | sudo systemctl enable primeserver 38 | -------------------------------------------------------------------------------- /test/boilerplate/boilerplate.Dockerfile.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/boilerplate/boilerplate.Makefile.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/boilerplate/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /test/boilerplate/boilerplate.py.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/boilerplate/boilerplate.sh.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/boilerplate/boilerplate.tf.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /test/boilerplate/boilerplate.xml.txt: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /test/boilerplate/boilerplate.yaml.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/make.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # This function checks to make sure that every 18 | # shebang has a '- e' flag, which causes it 19 | # to exit on error 20 | function check_bash() { 21 | find . -name "*.sh" | while IFS= read -d '' -r file; 22 | do 23 | if [[ "$file" != *"bash -e"* ]]; 24 | then 25 | echo "$file is missing shebang with -e"; 26 | exit 1; 27 | fi; 28 | done; 29 | } 30 | 31 | # This function makes sure that the required files for 32 | # releasing to OSS are present 33 | function basefiles() { 34 | echo "Checking for required files" 35 | test -f CONTRIBUTING.md || echo "Missing CONTRIBUTING.md" 36 | test -f LICENSE || echo "Missing LICENSE" 37 | test -f README.md || echo "Missing README.md" 38 | } 39 | 40 | # This function runs the hadolint linter on 41 | # every file named 'Dockerfile' 42 | function docker() { 43 | echo "Running hadolint on Dockerfiles" 44 | find . -name "Dockerfile" -exec hadolint {} \; 45 | } 46 | 47 | # This function runs 'terraform validate' against all 48 | # files ending in '.tf' 49 | function check_terraform() { 50 | echo "Running terraform validate" 51 | #shellcheck disable=SC2156 52 | find . -name "*.tf" -exec bash -c 'terraform validate --check-variables=false $(dirname "{}")' \; 53 | } 54 | 55 | # This function runs 'go fmt' and 'go vet' on eery file 56 | # that ends in '.go' 57 | function golang() { 58 | echo "Running go fmt and go vet" 59 | find . -name "*.go" -exec go fmt {} \; 60 | find . -name "*.go" -exec go vet {} \; 61 | } 62 | 63 | # This function runs the flake8 linter on every file 64 | # ending in '.py' 65 | function check_python() { 66 | echo "Running flake8" 67 | find . -name "*.py" -exec flake8 {} \; 68 | } 69 | 70 | # This function runs the shellcheck linter on every 71 | # file ending in '.sh' 72 | function check_shell() { 73 | echo "Running shellcheck" 74 | find . -name "*.sh" -exec shellcheck -x {} \; 75 | } 76 | 77 | # This function makes sure that there is no trailing whitespace 78 | # in any files in the project. 79 | # There are some exclusions 80 | function check_trailing_whitespace() { 81 | echo "The following lines have trailing whitespace" 82 | grep -r '[[:blank:]]$' --exclude-dir=".terraform" --exclude="*.png" --exclude-dir=".git" . 83 | rc=$? 84 | if [ $rc = 0 ]; then 85 | exit 1 86 | fi 87 | } 88 | -------------------------------------------------------------------------------- /test/test_verify_boilerplate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | ''' A simple test for the verify_boilerplate python script. 18 | This will create a set of test files, both valid and invalid, 19 | and confirm that the has_valid_header call returns the correct 20 | value. 21 | 22 | It also checks the number of files that are found by the 23 | get_files call. 24 | ''' 25 | from copy import deepcopy 26 | from tempfile import mkdtemp 27 | from shutil import rmtree 28 | import unittest 29 | from verify_boilerplate import has_valid_header, get_refs, get_regexs, \ 30 | get_args, get_files 31 | 32 | 33 | class AllTestCase(unittest.TestCase): 34 | """ 35 | All of the setup, teardown, and tests are contained in this 36 | class. 37 | """ 38 | 39 | def write_file(self, filename, content, expected): 40 | """ 41 | A utility method that creates test files, and adds them to 42 | the cases that will be tested. 43 | 44 | Args: 45 | filename: (string) the file name (path) to be created. 46 | content: (list of strings) the contents of the file. 47 | expected: (boolean) True if the header is expected to be valid, 48 | false if not. 49 | """ 50 | 51 | file = open(filename, 'w+') 52 | for line in content: 53 | file.write(line + "\n") 54 | file.close() 55 | self.cases[filename] = expected 56 | 57 | def create_test_files(self, tmp_path, extension, header): 58 | """ 59 | Creates 2 test files for .tf, .xml, .go, etc and one for 60 | Dockerfile, and Makefile. 61 | 62 | The reason for the difference is that Makefile and Dockerfile 63 | don't have an extension. These would be substantially more 64 | difficult to create negative test cases, unless the files 65 | were written, deleted, and re-written. 66 | 67 | Args: 68 | tmp_path: (string) the path in which to create the files 69 | extension: (string) the file extension 70 | header: (list of strings) the header/boilerplate content 71 | """ 72 | 73 | content = "\n...blah \ncould be code or could be garbage\n" 74 | special_cases = ["Dockerfile", "Makefile"] 75 | header_template = deepcopy(header) 76 | valid_filename = tmp_path + extension 77 | valid_content = header_template.append(content) 78 | if extension not in special_cases: 79 | # Invalid test cases for non-*file files (.tf|.py|.sh|.yaml|.xml..) 80 | invalid_header = [] 81 | for line in header_template: 82 | if "2018" in line: 83 | invalid_header.append(line.replace('2018', 'YEAR')) 84 | else: 85 | invalid_header.append(line) 86 | invalid_header.append(content) 87 | invalid_content = invalid_header 88 | invalid_filename = tmp_path + "invalid." + extension 89 | self.write_file(invalid_filename, invalid_content, False) 90 | valid_filename = tmp_path + "testfile." + extension 91 | 92 | valid_content = header_template 93 | self.write_file(valid_filename, valid_content, True) 94 | 95 | def setUp(self): 96 | """ 97 | Set initial counts and values, and initializes the setup of the 98 | test files. 99 | """ 100 | self.cases = {} 101 | self.tmp_path = mkdtemp() + "/" 102 | self.my_args = get_args() 103 | self.my_refs = get_refs(self.my_args) 104 | self.my_regex = get_regexs() 105 | self.prexisting_file_count = len( 106 | get_files(self.my_refs.keys(), self.my_args)) 107 | for key in self.my_refs: 108 | self.create_test_files(self.tmp_path, key, 109 | self.my_refs.get(key)) 110 | 111 | def tearDown(self): 112 | """ Delete the test directory. """ 113 | rmtree(self.tmp_path) 114 | 115 | def test_files_headers(self): 116 | """ 117 | Confirms that the expected output of has_valid_header is correct. 118 | """ 119 | for case in self.cases: 120 | if self.cases[case]: 121 | self.assertTrue(has_valid_header(case, self.my_refs, 122 | self.my_regex)) 123 | else: 124 | self.assertFalse(has_valid_header(case, self.my_refs, 125 | self.my_regex)) 126 | 127 | def test_invalid_count(self): 128 | """ 129 | Test that the initial files found isn't zero, indicating 130 | a problem with the code. 131 | """ 132 | self.assertFalse(self.prexisting_file_count == 0) 133 | 134 | 135 | if __name__ == "__main__": 136 | unittest.main() 137 | -------------------------------------------------------------------------------- /test/verify_boilerplate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # Verifies that all source files contain the necessary copyright boilerplate 17 | # snippet. 18 | # This is based on existing work 19 | # https://github.com/kubernetes/test-infra/blob/master/hack 20 | # /verify_boilerplate.py 21 | from __future__ import print_function 22 | import argparse 23 | import glob 24 | import os 25 | import re 26 | import sys 27 | 28 | 29 | def get_args(): 30 | """Parses command line arguments. 31 | 32 | Configures and runs argparse.ArgumentParser to extract command line 33 | arguments. 34 | 35 | Returns: 36 | An argparse.Namespace containing the arguments parsed from the 37 | command line 38 | """ 39 | parser = argparse.ArgumentParser() 40 | parser.add_argument("filenames", 41 | help="list of files to check, " 42 | "all files if unspecified", 43 | nargs='*') 44 | rootdir = os.path.dirname(__file__) + "/../" 45 | rootdir = os.path.abspath(rootdir) 46 | parser.add_argument( 47 | "--rootdir", 48 | default=rootdir, 49 | help="root directory to examine") 50 | 51 | default_boilerplate_dir = os.path.join(rootdir, "test/boilerplate") 52 | parser.add_argument("--boilerplate-dir", default=default_boilerplate_dir) 53 | return parser.parse_args() 54 | 55 | 56 | def get_refs(ARGS): 57 | """Converts the directory of boilerplate files into a map keyed by file 58 | extension. 59 | 60 | Reads each boilerplate file's contents into an array, then adds that array 61 | to a map keyed by the file extension. 62 | 63 | Returns: 64 | A map of boilerplate lines, keyed by file extension. For example, 65 | boilerplate.py.txt would result in the k,v pair {".py": py_lines} where 66 | py_lines is an array containing each line of the file. 67 | """ 68 | refs = {} 69 | 70 | # Find and iterate over the absolute path for each boilerplate template 71 | for path in glob.glob(os.path.join( 72 | ARGS.boilerplate_dir, 73 | "boilerplate.*.txt")): 74 | extension = os.path.basename(path).split(".")[1] 75 | ref_file = open(path, 'r') 76 | ref = ref_file.read().splitlines() 77 | ref_file.close() 78 | refs[extension] = ref 79 | return refs 80 | 81 | 82 | # pylint: disable=too-many-locals 83 | def has_valid_header(filename, refs, regexs): 84 | """Test whether a file has the correct boilerplate header. 85 | 86 | Tests each file against the boilerplate stored in refs for that file type 87 | (based on extension), or by the entire filename (eg Dockerfile, Makefile). 88 | Some heuristics are applied to remove build tags and shebangs, but little 89 | variance in header formatting is tolerated. 90 | 91 | Args: 92 | filename: A string containing the name of the file to test 93 | refs: A map of boilerplate headers, keyed by file extension 94 | regexs: a map of compiled regex objects used in verifying boilerplate 95 | 96 | Returns: 97 | True if the file has the correct boilerplate header, otherwise returns 98 | False. 99 | """ 100 | try: 101 | with open(filename, 'r') as fp: # pylint: disable=invalid-name 102 | data = fp.read() 103 | except IOError: 104 | return False 105 | basename = os.path.basename(filename) 106 | extension = get_file_extension(filename) 107 | if extension: 108 | ref = refs[extension] 109 | else: 110 | ref = refs[basename] 111 | # remove build tags from the top of Go files 112 | if extension == "go": 113 | con = regexs["go_build_constraints"] 114 | (data, found) = con.subn("", data, 1) 115 | # remove shebang 116 | elif extension == "sh" or extension == "py": 117 | she = regexs["shebang"] 118 | (data, found) = she.subn("", data, 1) 119 | data = data.splitlines() 120 | # if our test file is smaller than the reference it surely fails! 121 | if len(ref) > len(data): 122 | return False 123 | # trim our file to the same number of lines as the reference file 124 | data = data[:len(ref)] 125 | year = regexs["year"] 126 | for datum in data: 127 | if year.search(datum): 128 | return False 129 | 130 | # if we don't match the reference at this point, fail 131 | if ref != data: 132 | return False 133 | return True 134 | 135 | 136 | def get_file_extension(filename): 137 | """Extracts the extension part of a filename. 138 | 139 | Identifies the extension as everything after the last period in filename. 140 | 141 | Args: 142 | filename: string containing the filename 143 | 144 | Returns: 145 | A string containing the extension in lowercase 146 | """ 147 | return os.path.splitext(filename)[1].split(".")[-1].lower() 148 | 149 | 150 | # These directories will be omitted from header checks 151 | SKIPPED_DIRS = [ 152 | 'Godeps', 'third_party', '_gopath', '_output', 153 | '.git', 'vendor', '__init__.py', 'node_modules' 154 | ] 155 | 156 | 157 | def normalize_files(files): 158 | """Extracts the files that require boilerplate checking from the files 159 | argument. 160 | 161 | A new list will be built. Each path from the original files argument will 162 | be added unless it is within one of SKIPPED_DIRS. All relative paths will 163 | be converted to absolute paths by prepending the root_dir path parsed from 164 | the command line, or its default value. 165 | 166 | Args: 167 | files: a list of file path strings 168 | 169 | Returns: 170 | A modified copy of the files list where any any path in a skipped 171 | directory is removed, and all paths have been made absolute. 172 | """ 173 | newfiles = [] 174 | for pathname in files: 175 | if any(x in pathname for x in SKIPPED_DIRS): 176 | continue 177 | newfiles.append(pathname) 178 | for idx, pathname in enumerate(newfiles): 179 | if not os.path.isabs(pathname): 180 | newfiles[idx] = os.path.join(ARGS.rootdir, pathname) 181 | return newfiles 182 | 183 | 184 | def get_files(extensions, ARGS): 185 | """Generates a list of paths whose boilerplate should be verified. 186 | 187 | If a list of file names has been provided on the command line, it will be 188 | treated as the initial set to search. Otherwise, all paths within rootdir 189 | will be discovered and used as the initial set. 190 | 191 | Once the initial set of files is identified, it is normalized via 192 | normalize_files() and further stripped of any file name whose extension is 193 | not in extensions. 194 | 195 | Args: 196 | extensions: a list of file extensions indicating which file types 197 | should have their boilerplate verified 198 | 199 | Returns: 200 | A list of absolute file paths 201 | """ 202 | files = [] 203 | if ARGS.filenames: 204 | files = ARGS.filenames 205 | else: 206 | for root, dirs, walkfiles in os.walk(ARGS.rootdir): 207 | # don't visit certain dirs. This is just a performance improvement 208 | # as we would prune these later in normalize_files(). But doing it 209 | # cuts down the amount of filesystem walking we do and cuts down 210 | # the size of the file list 211 | for dpath in SKIPPED_DIRS: 212 | if dpath in dirs: 213 | dirs.remove(dpath) 214 | for name in walkfiles: 215 | pathname = os.path.join(root, name) 216 | files.append(pathname) 217 | files = normalize_files(files) 218 | outfiles = [] 219 | for pathname in files: 220 | basename = os.path.basename(pathname) 221 | extension = get_file_extension(pathname) 222 | if extension in extensions or basename in extensions: 223 | outfiles.append(pathname) 224 | return outfiles 225 | 226 | 227 | def get_regexs(): 228 | """Builds a map of regular expressions used in boilerplate validation. 229 | 230 | There are two scenarios where these regexes are used. The first is in 231 | validating the date referenced is the boilerplate, by ensuring it is an 232 | acceptable year. The second is in identifying non-boilerplate elements, 233 | like shebangs and compiler hints that should be ignored when validating 234 | headers. 235 | 236 | Returns: 237 | A map of compiled regular expression objects, keyed by mnemonic. 238 | """ 239 | regexs = {} 240 | # Search for "YEAR" which exists in the boilerplate, but shouldn't in the 241 | # real thing 242 | regexs["year"] = re.compile('YEAR') 243 | # dates can be 2014, 2015, 2016 or 2017, company holder names can be 244 | # anything 245 | regexs["date"] = re.compile('(2014|2015|2016|2017|2018)') 246 | # strip // +build \n\n build constraints 247 | regexs["go_build_constraints"] = re.compile(r"^(// \+build.*\n)+\n", 248 | re.MULTILINE) 249 | # strip #!.* from shell/python scripts 250 | regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) 251 | return regexs 252 | 253 | 254 | def main(args): 255 | """Identifies and verifies files that should have the desired boilerplate. 256 | 257 | Retrieves the lists of files to be validated and tests each one in turn. 258 | If all files contain correct boilerplate, this function terminates 259 | normally. Otherwise it prints the name of each non-conforming file and 260 | exists with a non-zero status code. 261 | """ 262 | regexs = get_regexs() 263 | refs = get_refs(args) 264 | filenames = get_files(refs.keys(), args) 265 | nonconforming_files = [] 266 | for filename in filenames: 267 | if not has_valid_header(filename, refs, regexs): 268 | nonconforming_files.append(filename) 269 | if nonconforming_files: 270 | print('%d files have incorrect boilerplate headers:' % len( 271 | nonconforming_files)) 272 | for filename in sorted(nonconforming_files): 273 | print(os.path.relpath(filename, args.rootdir)) 274 | sys.exit(1) 275 | 276 | 277 | if __name__ == "__main__": 278 | ARGS = get_args() 279 | main(ARGS) 280 | --------------------------------------------------------------------------------