├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── deploy ├── .terraform.lock.hcl ├── compute.tf ├── main.tf ├── terraform.tfvars └── variables.tf ├── k8s ├── deployment.yml ├── ingress.yml ├── namespace.yml └── service.yml ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | ### Terraform ### 26 | # Local .terraform directories 27 | **/.terraform/* 28 | 29 | # .tfstate files 30 | *.tfstate 31 | *.tfstate.* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN yarn install && yarn build 5 | 6 | FROM nginx:alpine 7 | WORKDIR /usr/share/nginx/html 8 | RUN rm -rf ./* 9 | COPY --from=builder /app/build . 10 | ENTRYPOINT ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React App on Google Kubernetes Engine (GKE) with Terraform 2 | 3 | A reference project to deploy a React app onto Google Kubernetes Engine (GKE) with Terraform 4 | 5 | ## Pre-requisites 6 | 7 | - Make sure you have installed [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli), and [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) 8 | - Also install the `kubectl` CLI tool 9 | 10 | ```bash 11 | gcloud components install kubectl # installs the kubectl CLI tool 12 | terraform -help # prints Terraform options 13 | ``` 14 | 15 | ## Configuration 16 | 17 | - Populate `terraform.tfvars`: 18 | 19 | ```bash 20 | region = "europe-west2" 21 | zone = "europe-west2-b" 22 | project = 23 | creds = 24 | ``` 25 | 26 | ## Build/Push the Docker image 27 | 28 | ```bash 29 | docker build -t gcr.io//react-gke-app:v1 . # builds Docker image 30 | gcloud auth configure-docker # configure Docker CLI tool to authenticate with Container Registry 31 | docker push gcr.io//react-gke-app:v1 # pushes image to Container Registry! 32 | ``` 33 | 34 | ## Create GCP resources 35 | 36 | ```bash 37 | cd deploy # change to deploy directory 38 | terraform init # initialises Terraform 39 | terraform apply # deploys GCP stack 40 | terraform destroy # destroys GCP stack 41 | ``` 42 | 43 | ## Deployment 44 | 45 | ```bash 46 | gcloud container clusters get-credentials react-gke-cluster --region=europe-west2 # updates a kubeconfig file with appropriate credentials 47 | cd k8s 48 | kubectl apply -f namespace.yml # create namespace 49 | kubectl apply -f . # create deployment, ingress, and service 50 | kubectl delete -f . # remove resources 51 | ``` 52 | 53 | - Visit app at `app_ip` e.g. `http://34.117.202.234/` 54 | 55 | ## Contributing 56 | 57 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 58 | 59 | Please make sure to update tests as appropriate. 60 | 61 | If you find this project helpful, please give a :star: or even better buy me a coffee :coffee: :point_down: because I'm a caffeine addict :sweat_smile: 62 | 63 | Buy Me A Coffee 64 | 65 | ## License 66 | 67 | [MIT](https://choosealicense.com/licenses/mit/) 68 | -------------------------------------------------------------------------------- /deploy/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/google" { 5 | version = "3.77.0" 6 | constraints = "3.77.0" 7 | hashes = [ 8 | "h1:JbmJFrgLbrgtAk3/Y+PQkJ7g51VgFeB1lCbA9pwzRNQ=", 9 | "zh:0dfa53acdc6cd81973424e5b4497e37c4538db1e6ed5818ed0e96f837a31b286", 10 | "zh:1e54cffecddf069d682f7f45d99c18a49d86afd590af6be02d50397b04e468ec", 11 | "zh:21be65dd260ebf5f4130e4b9a719e3b260fc6f2e80c16a50f73a47fdbfe69c97", 12 | "zh:2955f3af0db620eb63f8c631448d2fd4566c4a270e655ce7e6bf8fa13806d7c6", 13 | "zh:2d3e9b876557c7d2406a438114b2ddf24a805418c3601ef7c550980508965650", 14 | "zh:2f6cf592606e7a198fa275e93ce39dbf8a76f916f4a0842543f45ebd5a3d281c", 15 | "zh:59a7d05f3309078735b82640582dd4683605c7c10eaa41136c348bfa2d1e54a6", 16 | "zh:6fc3d947db6bbd222bbfc658bf7a27ac9f144570bebe0ce41ce6df95bee63635", 17 | "zh:83b1eca52c25971d2fd2ad0a733156236383680832ef54d3c59d3f385a05f510", 18 | "zh:86e4c542c4ddebca82668dd8bfe3f86808b60bbd9c4edf0c08d37c758f6d57d3", 19 | "zh:8bd36a0df91862c003ca6a204ad5715a36d72b9a26a63e1378c18139f34b39c1", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /deploy/compute.tf: -------------------------------------------------------------------------------- 1 | resource "google_container_cluster" "app_cluster" { 2 | name = "react-gke-cluster" 3 | location = var.region 4 | enable_autopilot = true 5 | vertical_pod_autoscaling { 6 | enabled = true 7 | } 8 | } 9 | 10 | resource "google_compute_global_address" "external_static_ip" { 11 | name = "react-gke-static-ip" 12 | address_type = "EXTERNAL" 13 | ip_version = "IPV4" 14 | project = var.project 15 | description = "External static IP address for React GKE app" 16 | } 17 | 18 | output "app_ip" { 19 | value = google_compute_global_address.external_static_ip.address 20 | description = "External static IP address for React GKE app" 21 | } 22 | -------------------------------------------------------------------------------- /deploy/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | google = { 4 | source = "hashicorp/google" 5 | version = "3.77.0" 6 | } 7 | } 8 | } 9 | 10 | terraform { 11 | backend "gcs" { 12 | bucket = "react-gke-tf-state" 13 | prefix = "terraform/state" 14 | } 15 | } 16 | 17 | provider "google" { 18 | region = var.region 19 | zone = var.zone 20 | project = var.project 21 | credentials = var.creds 22 | } -------------------------------------------------------------------------------- /deploy/terraform.tfvars: -------------------------------------------------------------------------------- 1 | region = "europe-west2" 2 | zone = "europe-west2-b" 3 | project = "react-gke-terraform" 4 | creds = "sa-key.json" -------------------------------------------------------------------------------- /deploy/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | variable "zone" {} 3 | variable "project" {} 4 | variable "creds" {} -------------------------------------------------------------------------------- /k8s/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: react-gke-deployment 5 | namespace: react-gke 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: react-gke 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: react-gke 15 | spec: 16 | containers: 17 | - name: react-gke-backend-container 18 | image: "gcr.io/react-gke-terraform/react-gke-app:v1" 19 | imagePullPolicy: Always 20 | resources: 21 | limits: 22 | cpu: "1" 23 | requests: 24 | cpu: "0.2" 25 | ports: 26 | - containerPort: 80 27 | protocol: TCP 28 | -------------------------------------------------------------------------------- /k8s/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: react-gke-ingress 5 | namespace: react-gke 6 | annotations: 7 | kubernetes.io/ingress.global-static-ip-name: "react-gke-static-ip" 8 | kubernetes.io/ingress.allow-http: "true" 9 | kubernetes.io/ingress.class: "gce" 10 | spec: 11 | defaultBackend: 12 | service: 13 | name: react-gke-service 14 | port: 15 | number: 80 16 | -------------------------------------------------------------------------------- /k8s/namespace.yml: -------------------------------------------------------------------------------- 1 | 2 | kind: Namespace 3 | apiVersion: v1 4 | metadata: 5 | name: react-gke 6 | labels: 7 | name: react-gke -------------------------------------------------------------------------------- /k8s/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: react-gke-service 5 | namespace: react-gke 6 | spec: 7 | ports: 8 | - port: 80 9 | protocol: TCP 10 | targetPort: 80 11 | selector: 12 | app: react-gke 13 | type: NodePort 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gcp-react-gke", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "react": "^17.0.2", 10 | "react-dom": "^17.0.2", 11 | "react-scripts": "4.0.3", 12 | "web-vitals": "^1.0.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/gcp-react-gke-terraform/b06db527901795829a89e703053d5d8835a98378/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/gcp-react-gke-terraform/b06db527901795829a89e703053d5d8835a98378/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/gcp-react-gke-terraform/b06db527901795829a89e703053d5d8835a98378/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import logo from './logo.svg'; 2 | import './App.css'; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 | logo 9 |

10 | Edit src/App.js and save to reload. 11 |

12 | 18 | Learn React 19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | --------------------------------------------------------------------------------