├── .gitignore ├── .prettierrc.js ├── 01 ├── index.js ├── package-lock.json ├── package.json ├── public │ └── tachyons.min.css └── views │ └── index.pug ├── 01_writing_a_note_taking_app.md ├── 02 ├── .dockerignore ├── Dockerfile ├── index.js ├── package-lock.json ├── package.json ├── public │ └── tachyons.min.css └── views │ └── index.pug ├── 02_deploying_to_kubernetes.md ├── 03 ├── .dockerignore ├── Dockerfile ├── index.js ├── kube │ ├── knote.yaml │ └── mongo.yaml ├── package-lock.json ├── package.json ├── public │ └── tachyons.min.css └── views │ └── index.pug ├── 03_scaling.md ├── 04-05 ├── .dockerignore ├── Dockerfile ├── index.js ├── kube │ ├── knote.yaml │ ├── minio.yaml │ └── mongo.yaml ├── package-lock.json ├── package.json ├── public │ └── tachyons.min.css └── views │ └── index.pug ├── 04_deploying_to_the_cloud.md ├── LICENSE ├── README.md └── assets ├── architecture.svg ├── aws-eks-console.png ├── build-1.svg ├── build-2.svg ├── build-3.svg ├── build-4.svg ├── certificate.jpg ├── chart-fallback.svg ├── chart.keyshape ├── chart.svg ├── console-dropdown-select.jpg ├── console-dropdown.jpg ├── console-welcome-select.jpg ├── console-welcome.jpg ├── cover-zero-to-k8s-nodejs-a4.svg ├── cover.svg ├── create-key-select.jpg ├── create-key.jpg ├── dialog-show-keys-select.jpg ├── dialog-show-keys.jpg ├── dockerfile-image-containers-1.svg ├── eks-1.svg ├── eks-2.svg ├── eks-3.svg ├── eks-4.svg ├── eks-5.svg ├── ingress.keyshape ├── ingress.svg ├── knote-add-image.gif ├── knote-add-notes.gif ├── minikube-service.keyshape ├── minikube-service.svg ├── modal-create-click.jpg ├── modal-create.jpg ├── multicontainer.svg ├── orchestrators-popularity.svg ├── security-credentials-welcome-select.jpg ├── security-credentials-welcome.jpg ├── service-with-ports.svg ├── service.keyshape ├── service.svg ├── sign-in.png ├── stateful-fallback.svg ├── stateful.keyshape ├── stateful.svg ├── tetris.keyshape └── tetris.svg /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | */public/uploads 3 | 4 | *.tar 5 | data/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | jsxSingleQuote: true, 8 | }; 9 | -------------------------------------------------------------------------------- /01/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const MongoClient = require('mongodb').MongoClient 4 | const multer = require('multer') 5 | const { marked } = require('marked') 6 | 7 | const app = express() 8 | const port = process.env.PORT || 3000 9 | const mongoURL = process.env.MONGO_URL || 'mongodb://localhost:27017/dev' 10 | 11 | async function initMongo() { 12 | console.log('Initialising MongoDB...') 13 | let success = false 14 | while (!success) { 15 | try { 16 | client = await MongoClient.connect(mongoURL, { 17 | useNewUrlParser: true, 18 | useUnifiedTopology: true, 19 | }) 20 | success = true 21 | } catch { 22 | console.log('Error connecting to MongoDB, retrying in 1 second') 23 | await new Promise(resolve => setTimeout(resolve, 1000)) 24 | } 25 | } 26 | console.log('MongoDB initialised') 27 | return client.db(client.s.options.dbName).collection('notes') 28 | } 29 | 30 | async function start() { 31 | const db = await initMongo() 32 | 33 | app.set('view engine', 'pug') 34 | app.set('views', path.join(__dirname, 'views')) 35 | app.use(express.static(path.join(__dirname, 'public'))) 36 | 37 | app.get('/', async (req, res) => { 38 | res.render('index', { notes: await retrieveNotes(db) }) 39 | }) 40 | 41 | app.post( 42 | '/note', 43 | multer({ dest: path.join(__dirname, 'public/uploads/') }).single('image'), 44 | async (req, res) => { 45 | if (!req.body.upload && req.body.description) { 46 | await saveNote(db, { description: req.body.description }) 47 | res.redirect('/') 48 | } else if (req.body.upload && req.file) { 49 | const link = `/uploads/${encodeURIComponent(req.file.filename)}` 50 | res.render('index', { 51 | content: `${req.body.description} ![](${link})`, 52 | notes: await retrieveNotes(db), 53 | }) 54 | } 55 | }, 56 | ) 57 | 58 | app.listen(port, () => { 59 | console.log(`App listening on http://localhost:${port}`) 60 | }) 61 | } 62 | 63 | async function saveNote(db, note) { 64 | await db.insertOne(note) 65 | } 66 | 67 | async function retrieveNotes(db) { 68 | const notes = await db.find().toArray() 69 | const sortedNotes = notes.reverse() 70 | return sortedNotes.map(it => ({ ...it, description: marked(it.description) })) 71 | } 72 | 73 | start() 74 | -------------------------------------------------------------------------------- /01/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knote", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "mongodb": "mongod" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.17.1", 14 | "marked": "^4.0.10", 15 | "mongodb": "^3.5.2", 16 | "mongodb-prebuilt": "^6.5.0", 17 | "multer": "^1.4.1", 18 | "pug": "^3.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /01/views/index.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title= title 4 | link(rel="stylesheet", href="tachyons.min.css") 5 | body.ph3.pt0.pb4.mw7.center.sans-serif 6 | h1.f2.mb0 #[span.gold k]note 7 | p.f5.mt1.mb4.lh-copy A simple note-taking app. 8 | form(action="/note" method="POST" enctype="multipart/form-data") 9 | ol.list.pl0 10 | li.mv3 11 | label.f6.b.db.mb2(for="image") Upload an image 12 | input.f6.link.dim.br1.ba.b--black-20.ph3.pv2.mb2.dib.black.bg-white.pointer(type="file" name="image") 13 | input.f6.link.dim.br1.ba.bw1.ph3.pv2.mb2.dib.black.bg-white.pointer.ml2(type="submit" value="Upload" name="upload") 14 | li.mv3 15 | label.f6.b.db.mb2(for="description") Write your content here 16 | textarea.f4.db.border-box.hover-black.w-100.measure.ba.b--black-20.pa2.br2.mb2(rows="5" name="description") #{content || ''} 17 | input.f6.link.dim.br1.ba.bw1.ph3.pv2.mb2.dib.black.bg-white.pointer(type="submit" value="Publish" name="publish") 18 | if notes.length > 0 19 | ul.list.pl0 20 | p.f6.b.db.mb2 Notes 21 | each note in notes 22 | li.mv3.bb.bw2.b--light-yellow.bg-washed-yellow.ph4.pv2 23 | p.measure!= note.description 24 | else 25 | p.lh-copy.f6 You don't have any notes yet. 26 | -------------------------------------------------------------------------------- /02/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /02/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.0-slim 2 | COPY . . 3 | RUN npm install 4 | CMD [ "node", "index.js" ] 5 | -------------------------------------------------------------------------------- /02/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const MongoClient = require('mongodb').MongoClient 4 | const multer = require('multer') 5 | const { marked } = require('marked') 6 | 7 | const app = express() 8 | const port = process.env.PORT || 3000 9 | const mongoURL = process.env.MONGO_URL || 'mongodb://localhost:27017/dev' 10 | 11 | async function initMongo() { 12 | console.log('Initialising MongoDB...') 13 | let success = false 14 | while (!success) { 15 | try { 16 | client = await MongoClient.connect(mongoURL, { 17 | useNewUrlParser: true, 18 | useUnifiedTopology: true, 19 | }) 20 | success = true 21 | } catch { 22 | console.log('Error connecting to MongoDB, retrying in 1 second') 23 | await new Promise(resolve => setTimeout(resolve, 1000)) 24 | } 25 | } 26 | console.log('MongoDB initialised') 27 | return client.db(client.s.options.dbName).collection('notes') 28 | } 29 | 30 | async function start() { 31 | const db = await initMongo() 32 | 33 | app.set('view engine', 'pug') 34 | app.set('views', path.join(__dirname, 'views')) 35 | app.use(express.static(path.join(__dirname, 'public'))) 36 | 37 | app.get('/', async (req, res) => { 38 | res.render('index', { notes: await retrieveNotes(db) }) 39 | }) 40 | 41 | app.post( 42 | '/note', 43 | multer({ dest: path.join(__dirname, 'public/uploads/') }).single('image'), 44 | async (req, res) => { 45 | if (!req.body.upload && req.body.description) { 46 | await saveNote(db, { description: req.body.description }) 47 | res.redirect('/') 48 | } else if (req.body.upload && req.file) { 49 | const link = `/uploads/${encodeURIComponent(req.file.filename)}` 50 | res.render('index', { 51 | content: `${req.body.description} ![](${link})`, 52 | notes: await retrieveNotes(db), 53 | }) 54 | } 55 | }, 56 | ) 57 | 58 | app.listen(port, () => { 59 | console.log(`App listening on http://localhost:${port}`) 60 | }) 61 | } 62 | 63 | async function saveNote(db, note) { 64 | await db.insertOne(note) 65 | } 66 | 67 | async function retrieveNotes(db) { 68 | const notes = await db.find().toArray() 69 | const sortedNotes = notes.reverse() 70 | return sortedNotes.map(it => ({ ...it, description: marked(it.description) })) 71 | } 72 | 73 | start() 74 | -------------------------------------------------------------------------------- /02/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knote", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.17.1", 13 | "marked": "^4.0.10", 14 | "mongodb": "^3.5.2", 15 | "multer": "^1.4.1", 16 | "pug": "^3.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /02/views/index.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title= title 4 | link(rel="stylesheet", href="tachyons.min.css") 5 | body.ph3.pt0.pb4.mw7.center.sans-serif 6 | h1.f2.mb0 #[span.gold k]note 7 | p.f5.mt1.mb4.lh-copy A simple note-taking app. 8 | form(action="/note" method="POST" enctype="multipart/form-data") 9 | ol.list.pl0 10 | li.mv3 11 | label.f6.b.db.mb2(for="image") Upload an image 12 | input.f6.link.dim.br1.ba.b--black-20.ph3.pv2.mb2.dib.black.bg-white.pointer(type="file" name="image") 13 | input.f6.link.dim.br1.ba.bw1.ph3.pv2.mb2.dib.black.bg-white.pointer.ml2(type="submit" value="Upload" name="upload") 14 | li.mv3 15 | label.f6.b.db.mb2(for="description") Write your content here 16 | textarea.f4.db.border-box.hover-black.w-100.measure.ba.b--black-20.pa2.br2.mb2(rows="5" name="description") #{content || ''} 17 | input.f6.link.dim.br1.ba.bw1.ph3.pv2.mb2.dib.black.bg-white.pointer(type="submit" value="Publish" name="publish") 18 | if notes.length > 0 19 | ul.list.pl0 20 | p.f6.b.db.mb2 Notes 21 | each note in notes 22 | li.mv3.bb.bw2.b--light-yellow.bg-washed-yellow.ph4.pv2 23 | p.measure!= note.description 24 | else 25 | p.lh-copy.f6 You don't have any notes yet. 26 | -------------------------------------------------------------------------------- /03/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | kube 3 | -------------------------------------------------------------------------------- /03/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.0-slim 2 | COPY . . 3 | RUN npm install 4 | CMD [ "node", "index.js" ] 5 | -------------------------------------------------------------------------------- /03/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const MongoClient = require('mongodb').MongoClient 4 | const multer = require('multer') 5 | const { marked } = require('marked') 6 | 7 | const app = express() 8 | const port = process.env.PORT || 3000 9 | const mongoURL = process.env.MONGO_URL || 'mongodb://localhost:27017/dev' 10 | 11 | async function initMongo() { 12 | console.log('Initialising MongoDB...') 13 | let success = false 14 | while (!success) { 15 | try { 16 | client = await MongoClient.connect(mongoURL, { 17 | useNewUrlParser: true, 18 | useUnifiedTopology: true, 19 | }) 20 | success = true 21 | } catch { 22 | console.log('Error connecting to MongoDB, retrying in 1 second') 23 | await new Promise(resolve => setTimeout(resolve, 1000)) 24 | } 25 | } 26 | console.log('MongoDB initialised') 27 | return client.db(client.s.options.dbName).collection('notes') 28 | } 29 | 30 | async function start() { 31 | const db = await initMongo() 32 | 33 | app.set('view engine', 'pug') 34 | app.set('views', path.join(__dirname, 'views')) 35 | app.use(express.static(path.join(__dirname, 'public'))) 36 | 37 | app.get('/', async (req, res) => { 38 | res.render('index', { notes: await retrieveNotes(db) }) 39 | }) 40 | 41 | app.post( 42 | '/note', 43 | multer({ dest: path.join(__dirname, 'public/uploads/') }).single('image'), 44 | async (req, res) => { 45 | if (!req.body.upload && req.body.description) { 46 | await saveNote(db, { description: req.body.description }) 47 | res.redirect('/') 48 | } else if (req.body.upload && req.file) { 49 | const link = `/uploads/${encodeURIComponent(req.file.filename)}` 50 | res.render('index', { 51 | content: `${req.body.description} ![](${link})`, 52 | notes: await retrieveNotes(db), 53 | }) 54 | } 55 | }, 56 | ) 57 | 58 | app.listen(port, () => { 59 | console.log(`App listening on http://localhost:${port}`) 60 | }) 61 | } 62 | 63 | async function saveNote(db, note) { 64 | await db.insertOne(note) 65 | } 66 | 67 | async function retrieveNotes(db) { 68 | const notes = await db.find().toArray() 69 | const sortedNotes = notes.reverse() 70 | return sortedNotes.map(it => ({ ...it, description: marked(it.description) })) 71 | } 72 | 73 | start() 74 | -------------------------------------------------------------------------------- /03/kube/knote.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: knote 5 | spec: 6 | selector: 7 | app: knote 8 | ports: 9 | - port: 80 10 | targetPort: 3000 11 | type: LoadBalancer 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: knote 17 | spec: 18 | replicas: 1 19 | selector: 20 | matchLabels: 21 | app: knote 22 | template: 23 | metadata: 24 | labels: 25 | app: knote 26 | spec: 27 | containers: 28 | - name: knote 29 | image: learnk8s/knote-js:1.0.0 30 | ports: 31 | - containerPort: 3000 32 | env: 33 | - name: MONGO_URL 34 | value: mongodb://mongo:27017/dev 35 | imagePullPolicy: Always 36 | -------------------------------------------------------------------------------- /03/kube/mongo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: mongo-pvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 256Mi 11 | --- 12 | apiVersion: v1 13 | kind: Service 14 | metadata: 15 | name: mongo 16 | spec: 17 | selector: 18 | app: mongo 19 | ports: 20 | - port: 27017 21 | targetPort: 27017 22 | --- 23 | apiVersion: apps/v1 24 | kind: Deployment 25 | metadata: 26 | name: mongo 27 | spec: 28 | selector: 29 | matchLabels: 30 | app: mongo 31 | template: 32 | metadata: 33 | labels: 34 | app: mongo 35 | spec: 36 | containers: 37 | - name: mongo 38 | image: mongo:6.0.2-focal 39 | ports: 40 | - containerPort: 27017 41 | volumeMounts: 42 | - name: storage 43 | mountPath: /data/db 44 | volumes: 45 | - name: storage 46 | persistentVolumeClaim: 47 | claimName: mongo-pvc 48 | -------------------------------------------------------------------------------- /03/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knote", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.17.1", 13 | "marked": "^4.0.10", 14 | "mongodb": "^3.5.2", 15 | "multer": "^1.4.1", 16 | "pug": "^3.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /03/views/index.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title= title 4 | link(rel="stylesheet", href="tachyons.min.css") 5 | body.ph3.pt0.pb4.mw7.center.sans-serif 6 | h1.f2.mb0 #[span.gold k]note 7 | p.f5.mt1.mb4.lh-copy A simple note-taking app. 8 | form(action="/note" method="POST" enctype="multipart/form-data") 9 | ol.list.pl0 10 | li.mv3 11 | label.f6.b.db.mb2(for="image") Upload an image 12 | input.f6.link.dim.br1.ba.b--black-20.ph3.pv2.mb2.dib.black.bg-white.pointer(type="file" name="image") 13 | input.f6.link.dim.br1.ba.bw1.ph3.pv2.mb2.dib.black.bg-white.pointer.ml2(type="submit" value="Upload" name="upload") 14 | li.mv3 15 | label.f6.b.db.mb2(for="description") Write your content here 16 | textarea.f4.db.border-box.hover-black.w-100.measure.ba.b--black-20.pa2.br2.mb2(rows="5" name="description") #{content || ''} 17 | input.f6.link.dim.br1.ba.bw1.ph3.pv2.mb2.dib.black.bg-white.pointer(type="submit" value="Publish" name="publish") 18 | if notes.length > 0 19 | ul.list.pl0 20 | p.f6.b.db.mb2 Notes 21 | each note in notes 22 | li.mv3.bb.bw2.b--light-yellow.bg-washed-yellow.ph4.pv2 23 | p.measure!= note.description 24 | else 25 | p.lh-copy.f6 You don't have any notes yet. 26 | -------------------------------------------------------------------------------- /03_scaling.md: -------------------------------------------------------------------------------- 1 | **TL;DR:** In this chapter, you'll learn how to scale your Node.js application with Kubernetes by making it stateless. 2 | 3 | Kubernetes is all about making your applications robust when traffic is high. 4 | 5 | But, how do you actually do that? 6 | 7 | Scaling. 8 | 9 | If your application is popular and you receive more traffic than usual, you might need to increment the number of instances running to cope with the load. 10 | 11 | And if the sudden increase in traffic vanishes after a few days, you might want to save resources in your infrastructure and decrease the unused instances. 12 | 13 | Kubernetes is designed to help you scale up and down your applications so that you can handle different traffic patterns to your applications. 14 | 15 | However, there's a gotcha. 16 | 17 | Not all applications can be scaled out of the box. 18 | 19 | If your application is holding any data in memory or on disk, you might not be able to scale it unless it's refactored to be stateless. 20 | 21 | But don't worry, you will learn how to do it in this article. 22 | 23 | The application which you'll learn to scale today is called Knote. 24 | 25 | If you have been following along the tutorial series, you would have learnt to deploy this application in a cluster during [the previous chapter](https://learnk8s.io/deploying-nodejs-kubernetes). 26 | 27 | This is what you'll be looking at today. 28 | 29 | ## Table of contents 30 | 31 | 1. [Scaling, high availability and resiliency](#scaling-high-availability-and-resiliency) 32 | 1. [The application](#the-application) 33 | 1. [Scaling and statefulness](#scaling-and-statefulness) 34 | 1. [Making the app stateless](#making-the-app-stateless) 35 | 1. [Defining the MinIO component](#defining-the-minio-component) 36 | 1. [Deploying the app](#deploying-the-app) 37 | 1. [Scaling the app](#scaling-the-app) 38 | 39 | ## Scaling, high availability and resiliency 40 | 41 | Scaling means increasing or decreasing the number of instances of an application component. 42 | 43 | You could scale your app to 2, 3, or 10 copies. 44 | 45 | The benefits of this are twofold: 46 | 47 | - **High availability:** if one of the instances crashes, there are still other replicas running that can handle incoming requests. 48 | - **Resiliency:** the total traffic to your application is distributed between all the replicas. Thus, the risk of overload of a single Pod is reduced. 49 | 50 | But not all apps can be scaled horizontally. 51 | 52 | If your application has any local state, you might need to refactor the code to remove it. 53 | 54 | In this section, you will learn how to scale your app to any number of replicas in Kubernetes. 55 | 56 | And you will learn how to refactor your apps to externalise any local state. 57 | 58 | ## The application 59 | 60 | The application that you will scale on Kubernetes is the following: 61 | 62 | ![Adding images and notes in Knote](assets/knote-add-image.gif) 63 | 64 | The application is made of two parts: 65 | 66 | 1. A front-end written in Node.js and Express. 67 | 1. A MongoDB to persist the data. 68 | 69 | All the code to build and deploy the app is available [in this repository](https://github.com/learnk8s/knote-js/tree/master/04). 70 | 71 | **If you deployed the application already, you should delete the previous deployment and start fresh.** 72 | 73 | ## Scaling and statefulness 74 | 75 | First of all, deploy your application to your Minikube cluster and wait until all the Pods are ready: 76 | 77 | ```terminal|command=1|title=bash 78 | kubectl apply -f kube 79 | ``` 80 | 81 | At the moment, there is one Pod running for the app and one for the database. 82 | 83 | Kubernetes makes it very easy to increase the number of replicas to 2: 84 | 85 | ```terminal|command=1|title=bash 86 | kubectl scale --replicas=2 deployment/knote 87 | ``` 88 | 89 | You can watch how a new Pod is created with: 90 | 91 | ```terminal|command=1|title=bash 92 | kubectl get pods -l app=knote --watch 93 | ``` 94 | 95 | > The `-l` flag is an alias for the `--selector` flag. With this flag, we only select the Pods with the `app=knote` label. 96 | 97 | There are now two replicas of the Knote Pod running. 98 | 99 | _So, are you already done?_ 100 | 101 | Reaccess your app: 102 | 103 | ```terminal|command=1|title=bash 104 | minikube service knote --url 105 | ``` 106 | 107 | And create a note with a picture. 108 | 109 | Now try to reload your app a couple of times (i.e. hit your browser's reload button). 110 | 111 | _Did you notice any glitch?_ 112 | 113 | **The picture that you added to your note is not displayed on every reload.** 114 | 115 | If you pay attention, the picture is only displayed on every second reload, on average. 116 | 117 | _Why is that?_ 118 | 119 | Remember that your application saves uploaded pictures in the local file system. 120 | 121 | If your app runs in a container, the pictures are saved only within that container's file system. 122 | 123 | When you had only a single Pod, this was fine. 124 | 125 | But since you have two replicas, there's an issue. 126 | 127 | The picture that you previously uploaded is saved in only one of the two Pods. 128 | 129 | When you access your app, the `knote` Service selects one of the available Pods. 130 | 131 | When it selects the Pod that has the picture in its file system, the image is displayed. 132 | 133 | But when it selects the other Pod, the picture isn't displayed, because the container doesn't have it. 134 | 135 | ```animation 136 | { 137 | "description": "Uploading files inside the app makes it stateful", 138 | "animation": "assets/stateful.svg", 139 | "fallback": "assets/stateful-fallback.svg" 140 | } 141 | ``` 142 | 143 | **Your application is stateful.** 144 | 145 | The pictures in the local filesystem constitute a state that is local to each container. 146 | 147 | **To be scalable, applications must be stateless.** 148 | 149 | Stateless means that an instance can be killed restarted or duplicated at any time without any data loss or inconsistent behaviour. 150 | 151 | _You must make your app stateless before you can scale it._ 152 | 153 | In this section, you will refactor your app to make it stateless. 154 | 155 | **But before you proceed, remove your current application from the cluster:** 156 | 157 | ```terminal|command=1|title=bash 158 | kubectl delete -f kube 159 | ``` 160 | 161 | The command ensures that the old data in the database's persistent volume does not stick around. 162 | 163 | ## Making the app stateless 164 | 165 | _How can you make your application stateless?_ 166 | 167 | The challenge is that uploaded pictures are saved in the container's file system where they can be accessed only by the current Pod. 168 | 169 | However, you could save the pictures in a central place where all Pods can access them. 170 | 171 | **An object storage is an ideal system to store files centrally.** 172 | 173 | You could use a cloud solution, such as [Amazon S3](https://aws.amazon.com/s3/). 174 | 175 | But to hone your Kubernetes skills, you could deploy an object storage service yourself. 176 | 177 | [MinIO](https://github.com/minio/minio) is an open-source object storage service that can be installed on your infrastructure. 178 | 179 | And you can install MinIO in your Kubernetes cluster. 180 | 181 | In the next step you should refactor your app to include MinIO. 182 | 183 | The architecture of your app looks like this: 184 | 185 | ![Knote application architecture](assets/architecture.svg) 186 | 187 | It consists of three components: 188 | 189 | 1. Knote as the primary application for creating notes 190 | 1. MongoDB for storing the text of the notes, and 191 | 1. MinIO for storing the pictures of the notes. 192 | 193 | Only the Knote component is accessible from outside the cluster — the MongoDB and MinIO components are hidden inside. 194 | 195 | For convenience, the following image is already packaged with the code changes `learnk8s/knote-js:2.0.0`. 196 | 197 | You don't have to change the code, the new container image already supports MinIO. 198 | 199 | > If you're interested, you can find the final source code file [in this repository](https://github.com/learnk8s/knote-js/tree/master/04-05). 200 | 201 | The new application expect three more variables: 202 | 203 | 1. The `MINIO_HOST` — this corresponds to the name fo the MinIO container. 204 | 1. `MINIO_ACCESS_KEY` — the key to access the MinIO bucket. 205 | 1. `MINIO_SECRET_KEY` — the secret that you need to present alongside the key to authenticate with MinIO. 206 | 207 | ## Updating the Knote configuration 208 | 209 | If you want to deploy the new version of your app to Kubernetes, you have to do a few changes to the YAML resources. 210 | 211 | In particular, you have to update the Docker image name and add the additional environment variables in the Deployment resource. 212 | 213 | You should change the Deployment resource in your `knote.yaml` file as follows (changed lines are highlighted): 214 | 215 | ```yaml|highlight=17,23-28|title=kube/knote.yaml 216 | apiVersion: apps/v1 217 | kind: Deployment 218 | metadata: 219 | name: knote 220 | spec: 221 | replicas: 1 222 | selector: 223 | matchLabels: 224 | app: knote 225 | template: 226 | metadata: 227 | labels: 228 | app: knote 229 | spec: 230 | containers: 231 | - name: knote 232 | image: learnk8s/knote-js:2.0.0 233 | ports: 234 | - containerPort: 3000 235 | env: 236 | - name: MONGO_URL 237 | value: mongodb://mongo:27017/dev 238 | - name: MINIO_ACCESS_KEY 239 | value: mykey 240 | - name: MINIO_SECRET_KEY 241 | value: mysecret 242 | - name: MINIO_HOST 243 | value: minio 244 | imagePullPolicy: Always 245 | ``` 246 | 247 | > Please note that the command runs the `learnk8s/knote-js:2.0.0` image. 248 | 249 | However, there is still something missing. 250 | 251 | You should create a Kubernetes resource definition for the new MinIO component. 252 | 253 | _So, let's do it._ 254 | 255 | ## Defining the MinIO component 256 | 257 | The task at hand is to deploy MinIO to a Kubernetes cluster. 258 | 259 | You should be able to guess what the Kubernetes description for MinIO looks like. 260 | 261 | **It should look like the MongoDB description that you defined in the ["Deploying to Kubernetes" section](https://learnk8s.io/deploying-nodejs-kubernetes#defining-the-database-tier).** 262 | 263 | Just like MongoDB, MinIO requires the following: 264 | 265 | - Persistent storage 266 | - Be accessible to other Pods inside the cluster by using a Service 267 | 268 | Here's the complete MinIO configuration to be saved in `kube/minio.yaml`: 269 | 270 | ```yaml|title=kube/minio.yaml 271 | apiVersion: v1 272 | kind: PersistentVolumeClaim 273 | metadata: 274 | name: minio-pvc 275 | spec: 276 | accessModes: 277 | - ReadWriteOnce 278 | resources: 279 | requests: 280 | storage: 256Mi 281 | --- 282 | apiVersion: v1 283 | kind: Service 284 | metadata: 285 | name: minio 286 | spec: 287 | selector: 288 | app: minio 289 | ports: 290 | - port: 9000 291 | targetPort: 9000 292 | --- 293 | apiVersion: apps/v1 294 | kind: Deployment 295 | metadata: 296 | name: minio 297 | spec: 298 | strategy: 299 | type: Recreate 300 | selector: 301 | matchLabels: 302 | app: minio 303 | template: 304 | metadata: 305 | labels: 306 | app: minio 307 | spec: 308 | containers: 309 | - name: minio 310 | image: minio/minio:RELEASE.2022-10-29T06-21-33Z 311 | args: 312 | - server 313 | - /storage 314 | env: 315 | - name: MINIO_ACCESS_KEY 316 | value: mykey 317 | - name: MINIO_SECRET_KEY 318 | value: mysecret 319 | ports: 320 | - containerPort: 9000 321 | volumeMounts: 322 | - name: storage 323 | mountPath: /storage 324 | volumes: 325 | - name: storage 326 | persistentVolumeClaim: 327 | claimName: minio-pvc 328 | ``` 329 | 330 | Just like the YAML file `kube/mongo.yaml`, this file also has a Deployment, Service and PersistenVolumeClaim resource definition. 331 | 332 | The same concepts and understanding can be applied here. 333 | 334 | With that, you just defined the last of the three components of your application. 335 | 336 | Your Kubernetes configuration is now complete. 337 | 338 | _It's time to deploy the application!_ 339 | 340 | ## Deploying the app 341 | 342 | **Now comes the big moment!** 343 | 344 | You will deploy the new version of your app to Kubernetes. 345 | 346 | First of all, make sure that you have the following three YAML files in the `kube` directory: 347 | 348 | ```terminal|command=1|title=bash 349 | tree . 350 | kube/ 351 | ├── knote.yaml 352 | ├── minio.yaml 353 | └── mongo.yaml 354 | ``` 355 | 356 | Also, make sure that you deleted the previous version of the app from the cluster: 357 | 358 | ```terminal|command=1|title=bash 359 | kubectl get pods 360 | ``` 361 | 362 | The command shouldn't output any resources. 363 | 364 | > Deleting the previous version of the app makes sure that the old data in the MongoDB database is removed. 365 | 366 | Now deploy your app to Kubernetes: 367 | 368 | ```terminal|command=1|title=bash 369 | kubectl apply -f kube 370 | ``` 371 | 372 | Watch your Pods being created: 373 | 374 | ```terminal|command=1|title=bash 375 | kubectl get pods --watch 376 | ``` 377 | 378 | You should see three Pods. 379 | 380 | Once all three Pods are _Running_, your application is ready. 381 | 382 | Access your app with: 383 | 384 | ```terminal|command=1|title=bash 385 | minikube service knote --url 386 | ``` 387 | 388 | The command should open your app in a web browser. 389 | 390 | Verify that it works correctly by creating some notes with pictures. 391 | 392 | **It should work as expected!** 393 | 394 | As a summary, here is what your application looks like now: 395 | 396 | ![Knote application architecture](assets/architecture.svg) 397 | 398 | It consists of three components: 399 | 400 | 1. Knote as the primary application for creating notes 401 | 1. MongoDB for storing the text of the notes, and 402 | 1. MinIO for storing the pictures of the notes. 403 | 404 | Only the Knote component is accessible from outside the cluster — the MongoDB and MinIO components are hidden inside. 405 | 406 | _At the moment, you're running a single Knote container._ 407 | 408 | But the topic of this section is "scaling". 409 | 410 | _So, let's scale it!_ 411 | 412 | ## Scaling the app 413 | 414 | Your app is now stateless because it saves the uploaded pictures on a MinIO server instead of the Pod's file system. 415 | 416 | _In other words, when you scale your app, all pictures should appear on every request._ 417 | 418 | **It's the moment of truth.** 419 | 420 | Scale the Knote container to 10 replicas: 421 | 422 | ```terminal|command=1|title=bash 423 | kubectl scale --replicas=10 deployment/knote 424 | ``` 425 | 426 | There should be nine additional Knote Pods being created. 427 | 428 | You can watch them come online with: 429 | 430 | ```terminal|command=1|title=bash 431 | kubectl get pods -l app=knote --watch 432 | ``` 433 | 434 | After a short moment, the new Pods should all be _Running_. 435 | 436 | Go back to your app in the web browser and reload the page a couple of times. 437 | 438 | _Are the pictures always displayed?_ 439 | 440 | **Yes they are!** 441 | 442 | Thanks to statelessness, your Pods can now be scaled to any number of replicas without any data loss or inconsistent behaviour. 443 | 444 | _You just created a scalable app!_ 445 | 446 | When you're done playing with your app, delete it from the cluster with: 447 | 448 | ```terminal|command=1|title=bash 449 | kubectl delete -f kube 450 | ``` 451 | 452 | And stop Minikube with: 453 | 454 | ```terminal|command=1|title=bash 455 | minikube stop 456 | ``` 457 | 458 | You don't need Minikube anymore. 459 | 460 | ## Recap and next steps 461 | 462 | In this section, you learned how to refactor an app and make it scalable with Kubernetes. 463 | 464 | Here's a racap of what you learned: 465 | 466 | 1. You scaled the application to two instances and noticed that it was stateful. 467 | 1. You refactored the app and externalised the state using an object store — MinIO. 468 | 1. You deployed MinIO in the cluster with persistent storage. 469 | 1. You redeploy the application with the changes. 470 | 1. You scaled the application again and verified that it's stateless. 471 | 472 | [In the next section, you will create a new Kubernetes cluster in the cloud and deploy your app there!](https://learnk8s.io/deploying-nodejs-kubernetes-eks) 473 | -------------------------------------------------------------------------------- /04-05/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | kube 3 | -------------------------------------------------------------------------------- /04-05/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.0-slim 2 | COPY . . 3 | RUN npm install 4 | CMD [ "node", "index.js" ] 5 | -------------------------------------------------------------------------------- /04-05/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const MongoClient = require('mongodb').MongoClient 4 | const multer = require('multer') 5 | const { marked } = require('marked') 6 | const minio = require('minio') 7 | 8 | const app = express() 9 | const port = process.env.PORT || 3000 10 | const mongoURL = process.env.MONGO_URL || 'mongodb://localhost:27017/dev' 11 | const minioHost = process.env.MINIO_HOST || 'localhost' 12 | const minioBucket = 'image-storage' 13 | 14 | async function initMongo() { 15 | console.log('Initialising MongoDB...') 16 | let success = false 17 | while (!success) { 18 | try { 19 | client = await MongoClient.connect(mongoURL, { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true, 22 | }) 23 | success = true 24 | } catch { 25 | console.log('Error connecting to MongoDB, retrying in 1 second') 26 | await new Promise(resolve => setTimeout(resolve, 1000)) 27 | } 28 | } 29 | console.log('MongoDB initialised') 30 | return client.db(client.s.options.dbName).collection('notes') 31 | } 32 | 33 | async function initMinIO() { 34 | console.log('Initialising MinIO...') 35 | const client = new minio.Client({ 36 | endPoint: minioHost, 37 | port: 9000, 38 | useSSL: false, 39 | accessKey: process.env.MINIO_ACCESS_KEY, 40 | secretKey: process.env.MINIO_SECRET_KEY, 41 | }) 42 | let success = false 43 | while (!success) { 44 | try { 45 | if (!(await client.bucketExists(minioBucket))) { 46 | await client.makeBucket(minioBucket) 47 | } 48 | success = true 49 | } catch { 50 | await new Promise(resolve => setTimeout(resolve, 1000)) 51 | } 52 | } 53 | console.log('MinIO initialised') 54 | return client 55 | } 56 | 57 | async function start() { 58 | const db = await initMongo() 59 | const minio = await initMinIO() 60 | 61 | app.set('view engine', 'pug') 62 | app.set('views', path.join(__dirname, 'views')) 63 | app.use(express.static(path.join(__dirname, 'public'))) 64 | 65 | app.get('/', async (req, res) => { 66 | res.render('index', { notes: await retrieveNotes(db) }) 67 | }) 68 | 69 | app.post( 70 | '/note', 71 | multer({ storage: multer.memoryStorage() }).single('image'), 72 | async (req, res) => { 73 | if (!req.body.upload && req.body.description) { 74 | await saveNote(db, { description: req.body.description }) 75 | res.redirect('/') 76 | } else if (req.body.upload && req.file) { 77 | await minio.putObject( 78 | minioBucket, 79 | req.file.originalname, 80 | req.file.buffer, 81 | ) 82 | const link = `/img/${encodeURIComponent(req.file.originalname)}` 83 | res.render('index', { 84 | content: `${req.body.description} ![](${link})`, 85 | notes: await retrieveNotes(db), 86 | }) 87 | } 88 | }, 89 | ) 90 | 91 | app.get('/img/:name', async (req, res) => { 92 | const stream = await minio.getObject( 93 | minioBucket, 94 | decodeURIComponent(req.params.name), 95 | ) 96 | stream.pipe(res) 97 | }) 98 | 99 | app.listen(port, () => { 100 | console.log(`App listening on http://localhost:${port}`) 101 | }) 102 | } 103 | 104 | async function saveNote(db, note) { 105 | await db.insertOne(note) 106 | } 107 | 108 | async function retrieveNotes(db) { 109 | const notes = await db.find().toArray() 110 | const sortedNotes = notes.reverse() 111 | return sortedNotes.map(it => ({ ...it, description: marked(it.description) })) 112 | } 113 | 114 | start() 115 | -------------------------------------------------------------------------------- /04-05/kube/knote.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: knote 5 | spec: 6 | selector: 7 | app: knote 8 | ports: 9 | - port: 80 10 | targetPort: 3000 11 | type: LoadBalancer 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: knote 17 | spec: 18 | replicas: 1 19 | selector: 20 | matchLabels: 21 | app: knote 22 | template: 23 | metadata: 24 | labels: 25 | app: knote 26 | spec: 27 | containers: 28 | - name: knote 29 | image: learnk8s/knote-js:2.0.0 30 | ports: 31 | - containerPort: 3000 32 | env: 33 | - name: MONGO_URL 34 | value: mongodb://mongo:27017/dev 35 | - name: MINIO_ACCESS_KEY 36 | value: mykey 37 | - name: MINIO_SECRET_KEY 38 | value: mysecret 39 | - name: MINIO_HOST 40 | value: minio 41 | imagePullPolicy: Always 42 | -------------------------------------------------------------------------------- /04-05/kube/minio.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: minio-pvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 256Mi 11 | --- 12 | apiVersion: v1 13 | kind: Service 14 | metadata: 15 | name: minio 16 | spec: 17 | selector: 18 | app: minio 19 | ports: 20 | - port: 9000 21 | targetPort: 9000 22 | --- 23 | apiVersion: apps/v1 24 | kind: Deployment 25 | metadata: 26 | name: minio 27 | spec: 28 | selector: 29 | matchLabels: 30 | app: minio 31 | template: 32 | metadata: 33 | labels: 34 | app: minio 35 | spec: 36 | containers: 37 | - name: minio 38 | image: minio/minio:RELEASE.2022-10-29T06-21-33Z 39 | args: 40 | - server 41 | - /storage 42 | env: 43 | - name: MINIO_ACCESS_KEY 44 | value: mykey 45 | - name: MINIO_SECRET_KEY 46 | value: mysecret 47 | ports: 48 | - containerPort: 9000 49 | volumeMounts: 50 | - name: storage 51 | mountPath: /storage 52 | volumes: 53 | - name: storage 54 | persistentVolumeClaim: 55 | claimName: minio-pvc 56 | -------------------------------------------------------------------------------- /04-05/kube/mongo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: mongo-pvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 256Mi 11 | --- 12 | apiVersion: v1 13 | kind: Service 14 | metadata: 15 | name: mongo 16 | spec: 17 | selector: 18 | app: mongo 19 | ports: 20 | - port: 27017 21 | targetPort: 27017 22 | --- 23 | apiVersion: apps/v1 24 | kind: Deployment 25 | metadata: 26 | name: mongo 27 | spec: 28 | selector: 29 | matchLabels: 30 | app: mongo 31 | template: 32 | metadata: 33 | labels: 34 | app: mongo 35 | spec: 36 | containers: 37 | - name: mongo 38 | image: mongo:6.0.2-focal 39 | ports: 40 | - containerPort: 27017 41 | volumeMounts: 42 | - name: storage 43 | mountPath: /data/db 44 | volumes: 45 | - name: storage 46 | persistentVolumeClaim: 47 | claimName: mongo-pvc 48 | -------------------------------------------------------------------------------- /04-05/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knote", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.17.1", 13 | "marked": "^4.0.10", 14 | "minio": "^7.0.10", 15 | "mongodb": "^3.5.2", 16 | "multer": "^1.4.1", 17 | "pug": "^3.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /04-05/views/index.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title= title 4 | link(rel="stylesheet", href="tachyons.min.css") 5 | body.ph3.pt0.pb4.mw7.center.sans-serif 6 | h1.f2.mb0 #[span.gold k]note 7 | p.f5.mt1.mb4.lh-copy A simple note-taking app. 8 | form(action="/note" method="POST" enctype="multipart/form-data") 9 | ol.list.pl0 10 | li.mv3 11 | label.f6.b.db.mb2(for="image") Upload an image 12 | input.f6.link.dim.br1.ba.b--black-20.ph3.pv2.mb2.dib.black.bg-white.pointer(type="file" name="image") 13 | input.f6.link.dim.br1.ba.bw1.ph3.pv2.mb2.dib.black.bg-white.pointer.ml2(type="submit" value="Upload" name="upload") 14 | li.mv3 15 | label.f6.b.db.mb2(for="description") Write your content here 16 | textarea.f4.db.border-box.hover-black.w-100.measure.ba.b--black-20.pa2.br2.mb2(rows="5" name="description") #{content || ''} 17 | input.f6.link.dim.br1.ba.bw1.ph3.pv2.mb2.dib.black.bg-white.pointer(type="submit" value="Publish" name="publish") 18 | if notes.length > 0 19 | ul.list.pl0 20 | p.f6.b.db.mb2 Notes 21 | each note in notes 22 | li.mv3.bb.bw2.b--light-yellow.bg-washed-yellow.ph4.pv2 23 | p.measure!= note.description 24 | else 25 | p.lh-copy.f6 You don't have any notes yet. 26 | -------------------------------------------------------------------------------- /04_deploying_to_the_cloud.md: -------------------------------------------------------------------------------- 1 | **TL;DR:** In this chapter, you'll create a production-grade Kubernetes cluster on [AWS using Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/), and you will deploy your application to it. 2 | 3 | EKS is a managed Kubernetes service, which means that Amazon Web Services (AWS) is fully responsible for managing the control plane. 4 | 5 | In particular, AWS: 6 | 7 | - Manages Kubernetes API servers and the etcd database. 8 | - Runs the Kubernetes control plane across three availability zones. 9 | - Scales the control-plane as you add more nodes to your cluster. 10 | - Provides a mechanism to upgrade your control plane to a newer version. 11 | - Rotates certificates. 12 | - [And more](https://aws.amazon.com/eks/?whats-new-cards.sort-by=item.additionalFields.postDateTime&whats-new-cards.sort-order=desc&eks-blogs.sort-by=item.additionalFields.createdDate&eks-blogs.sort-order=desc). 13 | 14 | Less timing managing the cluster means that you have more time to focus on developing and deploying applications. 15 | 16 | In this article, you will create an EKS cluster and deploy a Node.js application called Knote — a simple note-taking application. 17 | 18 | The application comes packaged as a Docker container already. 19 | 20 | If it peaks your interest, you can also [learn how to package or test your deplyoment into a local Kubernetes cluster](https://learnk8s.io/deploying-nodejs-kubernetes). 21 | 22 | Here's everything you'll look at today. 23 | 24 | ## Table of contents 25 | 26 | 1. [Logging into Amazon Web Services](#logging-into-amazon-web-services) 27 | 1. [Creating a Kubernetes cluster on AWS](#creating-a-kubernetes-cluster-on-aws) 28 | 1. [Deploying the app](#deploying-the-app) 29 | 1. [Taking into account resource limits](#taking-into-account-resource-limits) 30 | 1. [Cleaning up](#cleaning-up) 31 | 32 | ## Logging into Amazon Web Services 33 | 34 | To use Amazon Web Services (AWS), you need an AWS account. 35 | 36 | _If you already have an AWS account, you can [jump to the next section →](#creating-a-kubernetes-cluster-on-aws)_ 37 | 38 | The first step is [signing up for an AWS account](https://aws.amazon.com/). 39 | 40 | > Note that to create any resources on AWS, you have to provide valid credit card details. 41 | 42 | Next, you should create an AWS access key — this is necessary to access AWS services from the command line. 43 | 44 | To do so, follow these instructions: 45 | 46 | ```slideshow 47 | { 48 | "description": "How to find your AWS Access Key and Secret Access Key", 49 | "slides": [ 50 | { 51 | "image": "assets/sign-in.png", 52 | "description": "[Log in to your AWS Management Console](https://console.aws.amazon.com/console/home)." 53 | }, 54 | { 55 | "image": "assets/console-welcome.jpg", 56 | "description": "You should see your AWS console once you're logged in." 57 | }, 58 | { 59 | "image": "assets/console-welcome-select.jpg", 60 | "description": "Click on your user name at the top right of the page." 61 | }, 62 | { 63 | "image": "assets/console-dropdown.jpg", 64 | "description": "In the drop down there's an item for \"My Security Credentials\"." 65 | }, 66 | { 67 | "image": "assets/console-dropdown-select.jpg", 68 | "description": "Click on \"My Security Credentials\"." 69 | }, 70 | { 71 | "image": "assets/security-credentials-welcome.jpg", 72 | "description": "You should land on Your Security Credentials page." 73 | }, 74 | { 75 | "image": "assets/security-credentials-welcome-select.jpg", 76 | "description": "Click on Access Keys." 77 | }, 78 | { 79 | "image": "assets/create-key.jpg", 80 | "description": "The accordion unfolds the list of active keys (if any) and a button to create a new access key." 81 | }, 82 | { 83 | "image": "assets/create-key-select.jpg", 84 | "description": "Click on \"Create New Access Key\"." 85 | }, 86 | { 87 | "image": "assets/modal-create.jpg", 88 | "description": "A modal window appears suggesting that the key was created successfully." 89 | }, 90 | { 91 | "image": "assets/modal-create-click.jpg", 92 | "description": "Click on \"Show Access Key\" to reveal the access key." 93 | }, 94 | { 95 | "image": "assets/dialog-show-keys.jpg", 96 | "description": "You should see your access and secret key." 97 | }, 98 | { 99 | "image": "assets/dialog-show-keys-select.jpg", 100 | "description": "Please make a note of your keys as you will need those values in the next step." 101 | } 102 | ] 103 | } 104 | ``` 105 | 106 | You should save the access key ID and secret access key in a file named `~/.aws/credentials` as follows: 107 | 108 | ```title=$HOME/.aws/credentials 109 | [default] 110 | aws_access_key_id=[access-key-id] 111 | aws_secret_access_key=[secret-access-key] 112 | ``` 113 | 114 | _Remember to include the `[default]` in the file!_ 115 | 116 | Now, you can [install the aws-cli](https://github.com/aws/aws-cli) 117 | 118 | ```terminal|command=1|title=bash 119 | sudo python -m pip install awscli 120 | ``` 121 | 122 | If you are authenticated, when you run the command below, you should get a valid response as such if you have not created any clusters before. 123 | 124 | ```terminal|command=1|title=bash 125 | aws eks list-clusters --region=us-east-1 126 | 127 | { 128 | "clusters": [] 129 | } 130 | ``` 131 | 132 | _That's it! You're an AWS user now._ 133 | 134 | ## Creating a Kubernetes cluster on AWS 135 | 136 | You will use [Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/) for creating a Kubernetes cluster on AWS. 137 | 138 | Amazon EKS is the managed Kubernetes service of AWS — it is comparable to [Azure Kubernetes Service (AKS)](https://docs.microsoft.com/en-us/azure/aks/) or [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/). 139 | 140 | > **Caution:** AWS isn't free, and the resources that you will create in this section will produce **very reasonable charges** on your credit card. 141 | 142 | In practice, the total costs will be around **USD 0.20 per hour**. 143 | 144 | In other words, using the cluster for 5 hours will set you back around 1 dollars. 145 | 146 | You will create the cluster with a tool called [eksctl](https://github.com/weaveworks/eksctl) — a third-party command-line tool that allows creating an EKS cluster with a single command. 147 | 148 | You can install eksctl according to the instructions in [the official project page](https://github.com/weaveworks/eksctl#usage). 149 | 150 | _With eksctl installed, it's time to create an Amazon EKS cluster._ 151 | 152 | Run the following eksctl command: 153 | 154 | ```terminal|command=1|title=bash 155 | eksctl create cluster --region=eu-west-2 --name=knote 156 | ``` 157 | 158 | The command creates an Amazon EKS Kubernetes cluster with the following properties: 159 | 160 | - Two worker nodes (this is the default) 161 | - The worker nodes are [`m5.large` Amazon EC2 instances](https://aws.amazon.com/ec2/instance-types/) (this is the default) 162 | - The cluster is created in the eu-west-2 region (London) 163 | - The name of the cluster is `knote` 164 | 165 | > You can use any other AWS region [where Amazon EKS is available](https://gist.github.com/weibeld/a0856c16b156f357ac7738d4bef155ee). 166 | 167 | _Please be patient! Creating an EKS cluster usually takes around 15 minutes._ 168 | 169 | **While you wait for the cluster being created, you have some time to think about Amazon EKS.** 170 | 171 | Amazon EKS is a managed Kubernetes service, in the sense that AWS runs the Kubernetes control plane for you. 172 | 173 | That means, AWS runs the master nodes, and you run the worker nodes. 174 | 175 | AWS runs three master nodes in three availability zones in your selected region. 176 | 177 | ```slideshow 178 | { 179 | "description": "EKS multi-master setup", 180 | "slides": [ 181 | { 182 | "image": "assets/eks-1.svg", 183 | "description": "Amazon Elastic Kubernetes Service (EKS) is the managed Kubernetes offering of AWS. It allows you to create a resilient Kubernetes cluster running on the AWS infrastructure." 184 | }, 185 | { 186 | "image": "assets/eks-2.svg", 187 | "description": "AWS creates three master nodes. The three master nodes are deployed in three different availability zones." 188 | }, 189 | { 190 | "image": "assets/eks-3.svg", 191 | "description": "An Application Load Balancer (ALB) distributes the traffic to the three API servers on the master nodes." 192 | }, 193 | { 194 | "image": "assets/eks-4.svg", 195 | "description": "There are also three etcd instances. They sync between themselves. Even if one availability zone is lost, the cluster can still operate correctly." 196 | }, 197 | { 198 | "image": "assets/eks-5.svg", 199 | "description": "There are also three etcd instances. They sync between themselves. Even if one availability zone is lost, the cluster can still operate correctly." 200 | } 201 | ] 202 | } 203 | ``` 204 | 205 | _You have no access to the master nodes._ 206 | 207 | But you have full control over the worker nodes. 208 | 209 | The worker nodes are ordinary Amazon EC2 instances in your AWS account. 210 | 211 | _Once the eksctl command completes Amazon EKS cluster should be ready!_ 212 | 213 | You can list the two worker nodes of your cluster with: 214 | 215 | ```terminal|command=1|title=bash 216 | kubectl get nodes 217 | NAME STATUS ROLES AGE VERSION 218 | ip-192-168-25-57.eu-west-2.compute.internal Ready 23m v1.12.7 219 | ip-192-168-68-152.eu-west-2.compute.internal Ready 23m v1.12.7 220 | ``` 221 | 222 | > Note that you can't list or inspect the master nodes in any way with Amazon EKS. AWS fully manages the master nodes, and you don't need to be concerned about them. 223 | 224 | Since the worker nodes are regular Amazon EC2 instances in your AWS account, you can inspect them in the [AWS EC2 Console](https://eu-west-2.console.aws.amazon.com/ec2/v2/home?region=eu-west-2#Instances). 225 | 226 | You can also inspect the Amazon EKS resource itself in your AWS account in the [AWS EKS Console](https://eu-west-2.console.aws.amazon.com/eks/home?region=eu-west-2#/clusters). 227 | 228 | ![null](assets/aws-eks-console.png) 229 | 230 | As you can see, your Amazon EKS cluster has further related resources — they handle all the aspects that are required for a production-grade cluster such as networking, access control, security, and logging. 231 | 232 | Those resources are created by eksctl. 233 | 234 | > eksctl creates a [CloudFormation](https://eu-west-2.console.aws.amazon.com/cloudformation/home) stack with all resources belonging to your Amazon EKS cluster. 235 | 236 | **You have a production-grade Kubernetes cluster on AWS now.** 237 | 238 | _It's time to deploy the application._ 239 | 240 | ## Deploying the app 241 | 242 | The application that you will deploy on Kubernetes is the following: 243 | 244 | ![Adding images and notes in Knote](assets/knote-add-image.gif) 245 | 246 | The application is made of two parts: 247 | 248 | 1. A front-end written in Node.js and Express. 249 | 1. A MongoDB to persist the data. 250 | 251 | Before we continue, here are the links to the previous chapters of this series: 252 | 253 | - If you want to learn how to develop and package the application in a Docker container, [refer to this chapter of the course.](https://learnk8s.io/developing-and-packaging-nodejs-docker) 254 | - If you need help to set up a local Kubernetes cluster or deploy the application on Kubernetes, [refer to this other chapter.](https://learnk8s.io/deploying-nodejs-kubernetes) 255 | - If you want to know how this app was refactored to scale horizontally, [refer to this chapter instead.](https://learnk8s.io/scaling-nodejs-kubernetes) 256 | 257 | All the code to build and deploy the app is available [in this repository](https://github.com/learnk8s/knote-js/tree/master/05). 258 | 259 | The code contains the YAML Kubernetes definitions for Deployments and Services for minikube. 260 | 261 | _Do these YAML resource definitions still work on Amazon EKS without any changes?_ 262 | 263 | It's time to find out. 264 | 265 | First of all, make sure that you have the three YAML files in the `kube` directory: 266 | 267 | ```terminal|command=1|title=bash 268 | tree . 269 | kube/ 270 | ├── knote.yaml 271 | ├── minio.yaml 272 | └── mongo.yaml 273 | ``` 274 | 275 | > Note that, if you've completed the previous chapter in this course, these are precisely the same YAML files that you deployed to the Minikube cluster. 276 | 277 | Next, submit your configuration to the new Amazon EKS cluster: 278 | 279 | ```terminal|command=1|title=bash 280 | kubectl apply -f kube 281 | ``` 282 | 283 | Watch the Pods being created: 284 | 285 | ```terminal|command=1|title=bash 286 | kubectl get pods --watch 287 | ``` 288 | 289 | _The app seems to be running now._ 290 | 291 | To access the app, you need the public address of the `knote` Service. 292 | 293 | You can get it with this command: 294 | 295 | ```terminal|command=1|title=bash 296 | kubectl get service knote 297 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) 298 | knote LoadBalancer 10.100.188.43 80:31373/TCP 299 | ``` 300 | 301 | The `EXTERNAL-IP` column should contain a fully-qualified domain name. 302 | 303 | _The address allows you to access the app from anywhere._ 304 | 305 | Open the domain name in your web browser. 306 | 307 | > Note that it may take a couple of minutes until AWS DNS resolution is set up. If you get an `ERR_NAME_NOT_RESOLVED` error, try again in a few minutes. 308 | 309 | **You should see your app!** 310 | 311 | Verify that the app works as expected by creating some notes with pictures. 312 | 313 | _Everything should work correctly._ 314 | 315 | So, your hopes came true — you can deploy the same configuration to Amazon EKS! 316 | 317 | _And everything should work the same if you want to deploy it to Azure Kubernetes Service or Google Kubernetes Engine._ 318 | 319 | _But what about scaling?_ 320 | 321 | Go ahead and scale your Knote container to 10 replicas: 322 | 323 | ```terminal|command=1|title=bash 324 | kubectl scale --replicas=10 deployment knote 325 | ``` 326 | 327 | And check if everything still works by creating some more notes with pictures and reloading the page a couple of times. 328 | 329 | _Everything should indeed work as it did before._ 330 | 331 | **You just discovered the beauty of Kubernetes: you can define an application once and run it on any Kubernetes cluster.** 332 | 333 | ## Taking into account resource limits 334 | 335 | There is something else that you should know about scaling Pods. 336 | 337 | The number of Pods running in a given cluster is not unlimited. 338 | 339 | **In particular, there are limits on the maximum number of Pods that can run on a node.** 340 | 341 | On Amazon EKS, these limits depend on the EC2 instance type that you select. 342 | 343 | **Larger instances can host more Pods than smaller instances.** 344 | 345 | The details are described in the [`eni-max-pods.txt`](https://github.com/awslabs/amazon-eks-ami/blob/master/files/eni-max-pods.txt) document from AWS. 346 | 347 | This document defines the maximum number of Pods that can be run on each Amazon EC2 instance type. 348 | 349 | The `m5.large` instance type that you are using for your worker nodes can host up to 29 Pods. 350 | 351 | **You have two worker nodes in your cluster — that means you can run up to 58 Pods in your cluster.** 352 | 353 | _So, what happens when you scale your Pods to 100 replicas?_ 354 | 355 | The replicas that exceed the limit of 58 Pods should be stuck in the _Pending_ state and never run. 356 | 357 | Because no node can run them. 358 | 359 | _Let's test this._ 360 | 361 | First, check how many Pods are running in your cluster right now: 362 | 363 | ```terminal|command=1|title=bash 364 | kubectl get pods --all-namespaces 365 | ``` 366 | 367 | You should see 9 Pods — the 3 Pods that are part of your application (`minio`, `mongo` and `knote`) and 6 system Pods. 368 | 369 | > Kubernetes runs some system Pods on your worker nodes in the `kube-system` namespace. These Pods count against the limit too. The `--all-namespaces` flag outputs the Pods from all namespaces of the cluster. 370 | 371 | _So, how many replicas of the `knote` Pod can you run in the cluster?_ 372 | 373 | Let's do some math. 374 | 375 | 58 is the maximum number of Pods that can run in the cluster. 376 | 377 | You have: 378 | 379 | - 6 system pods 380 | - 1 pod for `minio` 381 | - 1 pod for `mongo` 382 | 383 | That means that you can have **up to 50 replicas** of the `knote` Pod. 384 | 385 | _Let's exceed this limit on purpose to observe what happens._ 386 | 387 | Scale your Knote app to 60 replicas: 388 | 389 | ```terminal|command=1|title=bash 390 | kubectl scale --replicas=60 deployment/knote 391 | ``` 392 | 393 | Wait for the Pods to start up. 394 | 395 | _If your calculations were correct, only 50 of these 60 replicas should be running in the cluster — the remaining ten should be stuck in the Pending state._ 396 | 397 | Let's start by counting the Knote Pods that are _Running_: 398 | 399 | ```terminal|command=1-4|title=bash 400 | kubectl get pods \ 401 | -l app=knote \ 402 | --field-selector='status.phase=Running' \ 403 | --no-headers | wc -l 404 | ``` 405 | 406 | The command should output 50. 407 | 408 | And now the Knote Pods that are _Pending_: 409 | 410 | ```terminal|command=1-4|title=bash 411 | kubectl get pods \ 412 | -l app=knote \ 413 | --field-selector='status.phase=Pending' \ 414 | --no-headers | wc -l 415 | ``` 416 | 417 | The command should output 10. 418 | 419 | **So your calculations were correct.** 420 | 421 | 50 of the 60 replicas are _Running_ — the remaining 10 are _Pending_. 422 | 423 | The ten pending replicas can't run because the maximum number of 58 running Pods in the cluster has been reached. 424 | 425 | You can verify that there are indeed 58 running Pods in the cluster with: 426 | 427 | ```terminal|command=1-4|title=bash 428 | kubectl get pods \ 429 | --all-namespaces \ 430 | --field-selector='status.phase=Running' \ 431 | --no-headers | wc -l 432 | ``` 433 | 434 | The command should output 58. 435 | 436 | To fix the issue, you can scale down the number of replicas to 50: 437 | 438 | ```terminal|command=1|title=bash 439 | kubectl scale --replicas=50 deployment/knote 440 | ``` 441 | 442 | After a while, you should see no _Pending_ Pods anymore — all the replicas should be running. 443 | 444 | **But what if you want a higher number of replicas at all costs?** 445 | 446 | You could add more nodes to your cluster. 447 | 448 | Nothing prevents you from creating a cluster with 10, 20, 30, or 1000 worker nodes. 449 | 450 | > Kubernetes is tested to run reliably with up to [several thousands of nodes and tens of thousands of Pods](https://kubernetes.io/docs/setup/best-practices/cluster-large/). 451 | 452 | Or you could use larger EC2 instances that can host more Pods (see [AWS limits](https://github.com/awslabs/amazon-eks-ami/blob/master/files/eni-max-pods.txt)). 453 | 454 | Nothing prevents you from using `m5.24xlarge` EC2 instances for your worker nodes and have 737 Pods on each of them. 455 | 456 | **Whatever your scaling requirements are, Kubernetes can accommodate them — you just have to design your cluster accordingly.** 457 | 458 | ## Cleaning up 459 | 460 | _Before you leave, you should remember something important:_ 461 | 462 | **Running an Amazon EKS cluster is not free.** 463 | 464 | Running the cluster alone (without the worker nodes) [costs USD 0.20 per hour](https://aws.amazon.com/eks/pricing/). 465 | 466 | > The price stays the same, no matter how many Pods you run on the cluster. 467 | 468 | And running the two `m5.large` worker node [costs USD 0.096 per hour](https://aws.amazon.com/ec2/pricing/on-demand/) for each one. 469 | 470 | **The total amount is around USD 0.40 per hour for running your cluster.** 471 | 472 | While it might not seem a lot, if you forget to delete your cluster, it could add up quickly. 473 | 474 | **That's why you should always delete your Amazon EKS cluster when you don't need it anymore.** 475 | 476 | You can do this conveniently with eksctl: 477 | 478 | ```terminal|command=1|title=bash 479 | eksctl delete cluster --region=eu-west-2 --name=knote 480 | ``` 481 | 482 | The command deletes all AWS resources that belong to your Amazon EKS cluster. 483 | 484 | After the command completes, you can double-check that the AWS resources have been deleted in the AWS Console: 485 | 486 | - [Check the AWS EKS Console](https://console.aws.amazon.com/eks/home) and verify that the Amazon EKS cluster resource has been removed (or is being deleted) 487 | - [Check the AWS EC2 Console](https://console.aws.amazon.com/ec2/v2/home?#Instances) and confirm that the EC2 instances that were your worker nodes have been removed (or are being deleted) 488 | 489 | > When you access the AWS Console, always double-check that you selected the correct region in the top-right corner (e.g. London). If you are in the wrong region, you can't see the resources from another region. 490 | 491 | If you want to work with your cluster again at a later time, you can repeat the same steps: 492 | 493 | 1. Create a new cluster with eksctl 494 | 1. Deploy your app 495 | 1. Delete the cluster with eksctl 496 | 497 | ## Conclusion 498 | 499 | **You've reached the end of this crash course on Kubernetes.** 500 | 501 | Let's recap what you achieved: 502 | 503 | - You wrote a note-taking application in Node.js 504 | - You packaged the app as a Docker image 505 | - You deployed the containerised application to a local Minikube cluster 506 | - You refactored your application to make it stateless and scalable 507 | - You deployed the improved application to a production-grade Kubernetes cluster on AWS 508 | 509 | In the course of this, you learnt about many topics, including: 510 | 511 | - Taking Kubernetes considerations into account as early as coding an application 512 | - How to build Docker images and upload them to Docker Hub 513 | - How to run a containerised application locally in a Docker network 514 | - How to create a local Kubernetes cluster with Minikube 515 | - The declarative resource-based interface of Kubernetes 516 | - Where you can find information about Kubernetes resource objects 517 | - How to write application deployment configurations for Kubernetes 518 | - How statefulness is related to scalability 519 | - How to scale an application on Kubernetes 520 | - How to create a production-grade Kubernetes cluster on AWS using Amazon EKS 521 | - Taking into account resource limits on production Kubernetes clusters 522 | 523 | ## Where to go from here? 524 | 525 | If you want to keep experimenting, you can create a Kubernetes cluster with a different provider, such as [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/) or [Azure Kubernetes Service (AKS)](https://docs.microsoft.com/en-us/azure/aks/) and deploy your application there. 526 | 527 | Since a Kubernetes deployment configuration, once defined, can be deployed to every Kubernetes cluster, you should have no troubles with that. 528 | 529 | During this course, you covered several topics, but you didn't have the time to study them in-depth. 530 | 531 | An obvious place to learn more about Kubernetes is in the [official documentation](https://kubernetes.io/docs/home/) where you can find more about fundamental [concepts](https://kubernetes.io/docs/concepts/), [everyday tasks](https://kubernetes.io/docs/tasks/), or even learn how to [install Kubernetes from scratch](https://kubernetes.io/docs/setup/). 532 | 533 | Finally, the [Learnk8s Academy](https://academy.learnk8s.io) offers a broad range of Kubernetes courses, similar to the one you completed. 534 | 535 | These courses treat various topics in much more depth than this introductory course could provide. 536 | 537 | The Learnk8s Academy courses can also prepare you for the [Certified Kubernetes Administrator (CKA)](https://www.cncf.io/certification/cka/) and [Certified Kubernetes Application Developer (CKAD)](https://www.cncf.io/certification/ckad/) exams. 538 | 539 | **Happy navigating with Kubernetes!** 540 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Learnk8s 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Knote js 2 | 3 |
4 | 5 |

Accompany repository for Kubernetes for developers minicourse

6 |
7 | 8 | The folders in this repository correspond to the exercises in the course. In the course, you will learn how to: 9 | 10 | 1. Develop applications for Kubernetes using Express.js and Node.js and package it using Linux (and Docker) containers. 11 | 1. Set up a local Kubernetes environment with minikube and how to deploy applications in it. 12 | 1. Scale your application to more than a single instance. 13 | 1. Deploy your app in the cloud using Kubernetes, Amazon Web Services (AWS) and their managed Kubernetes offering. 14 | 15 | _Last updated: 18 May 2022_ 16 | -------------------------------------------------------------------------------- /assets/architecture.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/aws-eks-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/aws-eks-console.png -------------------------------------------------------------------------------- /assets/build-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/build-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/build-3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/build-4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/certificate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/certificate.jpg -------------------------------------------------------------------------------- /assets/chart.keyshape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/chart.keyshape -------------------------------------------------------------------------------- /assets/console-dropdown-select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/console-dropdown-select.jpg -------------------------------------------------------------------------------- /assets/console-dropdown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/console-dropdown.jpg -------------------------------------------------------------------------------- /assets/console-welcome-select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/console-welcome-select.jpg -------------------------------------------------------------------------------- /assets/console-welcome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/console-welcome.jpg -------------------------------------------------------------------------------- /assets/create-key-select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/create-key-select.jpg -------------------------------------------------------------------------------- /assets/create-key.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/create-key.jpg -------------------------------------------------------------------------------- /assets/dialog-show-keys-select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/dialog-show-keys-select.jpg -------------------------------------------------------------------------------- /assets/dialog-show-keys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/dialog-show-keys.jpg -------------------------------------------------------------------------------- /assets/dockerfile-image-containers-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/eks-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/ingress.keyshape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/ingress.keyshape -------------------------------------------------------------------------------- /assets/knote-add-image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/knote-add-image.gif -------------------------------------------------------------------------------- /assets/knote-add-notes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/knote-add-notes.gif -------------------------------------------------------------------------------- /assets/minikube-service.keyshape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/minikube-service.keyshape -------------------------------------------------------------------------------- /assets/modal-create-click.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/modal-create-click.jpg -------------------------------------------------------------------------------- /assets/modal-create.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/modal-create.jpg -------------------------------------------------------------------------------- /assets/security-credentials-welcome-select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/security-credentials-welcome-select.jpg -------------------------------------------------------------------------------- /assets/security-credentials-welcome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/security-credentials-welcome.jpg -------------------------------------------------------------------------------- /assets/service-with-ports.svg: -------------------------------------------------------------------------------- 1 | PORT 80PORT 3000PORT 3000 -------------------------------------------------------------------------------- /assets/service.keyshape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/service.keyshape -------------------------------------------------------------------------------- /assets/service.svg: -------------------------------------------------------------------------------- 1 | ServiceDeployment -------------------------------------------------------------------------------- /assets/sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/sign-in.png -------------------------------------------------------------------------------- /assets/stateful-fallback.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/stateful.keyshape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/stateful.keyshape -------------------------------------------------------------------------------- /assets/tetris.keyshape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnk8s/knote-js/8bd040f9449842aaf7a706612a54c7d4073e8f9f/assets/tetris.keyshape --------------------------------------------------------------------------------