example: adds production example

This commit is contained in:
Sameer Naik
2017-09-20 15:28:49 +05:30
parent 1dea4a897e
commit 8817da7be1
14 changed files with 487 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
kubernetes.yml
README.md

30
bitnami/node/example/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://docs.npmjs.com/cli/shrinkwrap#caveats
node_modules
# Debug log from npm
npm-debug.log

View File

@@ -0,0 +1,11 @@
FROM bitnami/node:6-prod
ENV NODE_ENV="production"
COPY . /app
WORKDIR /app
RUN npm install
CMD ["npm", "start"]

View File

@@ -0,0 +1,132 @@
# Example Application
## TL;DR
```bash
$ kubectl create -f https://raw.githubusercontent.com/bitnami/bitnami-docker-node/master/example/kubernetes.yml
```
## Introduction
This example demostrates the use of the `bitnami/node` image to create a production build of your node application.
For demonstration purposes we'll bootstrap a [ExpressJS](https://expressjs.com) application, build a image with the tag `bitnami/node-example` and deploy it on a [Kubernetes](https://kubernetes.io) cluster.
## Generate the application
The example application is an [ExpressJS](https://expressjs.com) application bootstrapped using [express-generator](https://www.npmjs.com/package/express-generator).
```bash
$ express --git --css less example/
```
## Build and Test
To build a production Docker image of our application we'll use the `bitnami/node:6-prod` image, which is a production build of the Bitnami Node Image optimized for size.
```dockerfile
FROM bitnami/node:6-prod
ENV NODE_ENV="production"
COPY . /app
WORKDIR /app
RUN npm install
CMD ["npm", "start"]
```
We use the above `Dockerfile` to `COPY` the example application at the `/app` path of the container and install the npm module dependencies with the command `npm install`. Finally the Express application is start with `npm start`.
To build the Docker image, execute the command:
```bash
$ docker build -t bitnami/node-example:0.0.1 example/
```
Since the `bitnami/node:6-prod` image is optimized for production deployments it does not include any packages that would bloat the image.
```console
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
bitnami/node-example 0.0.1 0d43bbca1cd2 22 seconds ago 193MB
```
You can now launch and test the image locally.
```console
$ docker run -it --rm -p 3000:3000 bitnami/node-example:0.0.1
> example@0.0.1 start /app
> node ./bin/www
```
Finally, push the image to the Docker registry
```bash
$ docker push bitnami/node-example:0.0.1
```
## Deployment
The `kubernetes.yml` file from the `example/` folder can be used to deploy our `bitnami/node-example:0.0.1` image to a Kubernetes cluster.
Simply download the Kubernetes manifest and create the Kubernetes resources described in the manifest using the command:
```console
$ kubectl create -f kubernetes.yml
ingress "example-ingress" created
service "example-svc" created
configmap "example-configmap" created
persistentvolumeclaim "example-data-pvc" created
deployment "example-deployment" created
```
From the output of the above command you will notice that we create the following resources:
- [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)
- [Service](https://kubernetes.io/docs/concepts/services-networking/service/)
- [Volume](https://kubernetes.io/docs/concepts/storage/volumes/)
+ [ConfigMap](https://kubernetes.io/docs/concepts/storage/volumes/#projected)
+ [PersistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/volumes/#persistentvolumeclaim)
- [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
> **Note**
>
> Our example application is stateless and does not store any data or does not require any user configurations. As such we do not need to create the `ConfigMap` or `PersistentVolumeClaim` resources. Our `kubernetes.yml` creates these resources strictly to demostrate how they are defined in the manifest.
## Accessing the application
Typically in production you would access the application via a Ingress controller. Our `kubernetes.yml` already defines a `Ingress` resource. Please refer to the [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) documentation to learn how to deploy an ingress controller in your cluster.
> **Hint**
>
> https://kubeapps.com/charts/stable/nginx-ingress
The following are alternate ways of accessing the application, typically used during application development and testing.
Since the service `example-svc` is defined to be of type `NodePort`, we can set up port forwarding to access our web application like so:
```bash
$ kubectl port-forward $(kubectl get pods -l app=example -o jsonpath="{ .items[0].metadata.name }") 3000:3000
```
The command forwards the local port `3000` to port `3000` of the Pod container. You can access the application by visiting the http://localhost:3000.
> **Note:**
>
> If your using minikube, you can access the application by simply executing the following command:
>
> ```bash
> $ minikube service example-svc
> ```
## Health Checks
The `kubernetes.yml` manifest defines default probes to check the health of the application. For our application we are simply probing if the application is responsive to queries on the root resource.
You application can define a route, such as the commonly used `/healthz`, that reports the application status and use that route in the health probes.

View File

@@ -0,0 +1,48 @@
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var lessMiddleware = require('less-middleware');
var index = require('./routes/index');
var users = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(lessMiddleware(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
app.use('/users', users);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

90
bitnami/node/example/bin/www Executable file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('example:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

View File

@@ -0,0 +1,112 @@
apiVersion: v1
items:
- apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: example-ingress
labels:
app: example
annotations:
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
backend:
serviceName: example-svc
servicePort: 80
tls:
- apiVersion: v1
kind: Service
metadata:
name: example-svc
labels:
app: example
spec:
ports:
- name: http
port: 80
targetPort: http
selector:
app: example
type: NodePort
status:
loadBalancer: {}
- apiVersion: v1
kind: ConfigMap
metadata:
name: example-configmap
labels:
app: example
data:
config.js: |-
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-data-pvc
annotations: {}
labels:
app: example
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
status: {}
- apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: example-deployment
labels:
app: example
spec:
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
app: example
spec:
containers:
- name: example
image: bitnami/node-example:0.0.1
ports:
- containerPort: 3000
name: http
livenessProbe:
httpGet:
path: /
port: http
httpHeaders:
- name: Host
value: app.example.com
initialDelaySeconds: 15
readinessProbe:
httpGet:
path: /
port: http
httpHeaders:
- name: Host
value: app.example.com
initialDelaySeconds: 5
resources: {}
volumeMounts:
- mountPath: /app/config.js
name: example-config
subPath: config.js
- mountPath: /app/data
name: example-data
restartPolicy: Always
volumes:
- name: example-config
configMap:
name: example-configmap
- name: example-data
persistentVolumeClaim:
claimName: example-data-pvc
status: {}
kind: List
metadata: {}

View File

@@ -0,0 +1,18 @@
{
"name": "example",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.17.1",
"cookie-parser": "~1.4.3",
"debug": "~2.6.3",
"express": "~4.15.2",
"jade": "~1.11.0",
"less-middleware": "~2.2.0",
"morgan": "~1.8.1",
"serve-favicon": "~2.4.2"
}
}

View File

@@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
module.exports = router;

View File

@@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

View File

@@ -0,0 +1,5 @@
extends layout
block content
h1= title
p Welcome to #{title}

View File

@@ -0,0 +1,7 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content