Overview
This document will show how to use docker tooling to create application for IOx. Specifically, we will create a docker image to run a simple nodejs based HTTP server and create an IOx application from it.
Pre-requisites
- Your development machine
- ioxclient (> 1.4.0) installed
- Docker >= 1.12 installed (preferred would be 1.26)
- Good understanding of IOx application development concepts, IOx package descriptor etc.,
$ docker -v
Docker version 1.12.0, build 8eab29e
$ ioxclient --version
ioxclient version 1.4.0.0
Sample Code
This sample application code is maintained here.
Clone this repo and use branch master
/tmp $ git clone https://github.com/CiscoIOx/docker-nodejs.git -b master
Some parts of the code may be reproduced in this document to give more context. But always use the above git repo and branch for the latest code.
Procedure Overview
Broadly we will cover the following:
- Creating a nodejs http server application using docker tools and alpine base image.
- Testing your application locally using docker tools
- Creating IOx application from the docker image created in the previous steps
- Adding an IOx device to ioxclient
- Deploy and test the application on IOx device
Getting started
We will write a simple nodejs http server that responds with "Hello World" for all incoming requests. Using alpine as a base image, we will create a local docker image with our application.
Developing the application
Setup project directory
~ $ mkdir docker-nodejs
~ $ cd docker-nodejs
Writing nodejs http server
Here is the sample code.
$ vi server.js
// Handle SIGINT/SIGTERM
var sighandler = function() {
console.log('Exiting..');
process.exit();
}
process.on( "SIGINT", sighandler );
process.on( "SIGTERM", sighandler );
// Overload console.log and console.error to log to a file and also to stdout
// If running in CAF environment, use the CAF_APP_LOG_DIR location to write the log file
var cafLogDir = process.env.CAF_APP_LOG_DIR
var path = require('path');
var lf = "server.log"
if (cafLogDir) {
lf = path.join(cafLogDir, lf)
}
var fs = require('fs');
var util = require('util');
var logFile = fs.createWriteStream(lf, { flags: 'a' });
var logStdout = process.stdout;
console.log("Setting up logging to file " + lf)
console.log = function () {
logFile.write(util.format.apply(null, arguments) + '\n');
logStdout.write(util.format.apply(null, arguments) + '\n');
}
console.error = console.log;
// Load the http module to create an http server.
var http = require('http');
// Configure our HTTP server to respond with Hello World to all requests.
var server = http.createServer(function (request, response) {
console.log("Request from " + (request.headers['x-forwarded-for'] || request.connection.remoteAddress))
response.writeHead(200, {"Content-Type": "text/plain"});
response.end("Hello World!\n");
console.log("Response sent..")
});
// Listen on port 8000, IP defaults to 127.0.0.1
server.listen(8000, "0.0.0.0");
// Put a friendly message on the terminal
console.log("Server running at http://0.0.0.0:8000/");
This is a simple nodejs based http server that does:
- Setup signal handlers to shutdown gracefully
- Inspect CAF_APP_LOG_DIR environment variable and setup logging to a file accordingly
- Start HTTP Server on port 8000.
- Log the source of request and respond with "Hello World!"
Creating a docker image.
Let us create a docker image with the above application in it. We will use alpine:3.3 as the base image, install nodejs and setup our application.
Below is a Dockerfile that accomplishes these tasks:
$ vi Dockerfile
FROM alpine:3.3
RUN apk add --update nodejs
COPY server.js /server.js
EXPOSE 8000
CMD ["node", "/server.js"]
Note: You could use a Dockerfile or build the image interactively (run the base alpine:3.3 image with /bin/sh, run the commands in a shell and later commit the changes into a new image.)
Now let us build an image from this Dockerfile and tag it with a name (samplenode:1.0
).
$ docker build -t samplenode:1.0 .
Sending build context to Docker daemon 52.74 kB
Step 1 : FROM alpine:3.3
3.3: Pulling from library/alpine
6c123565ed5e: Pull complete
Digest: sha256:4fa633f4feff6a8f02acfc7424efd5cb3e76686ed3218abf4ca0fa4a2a358423
Status: Downloaded newer image for alpine:3.3
---> 47cf20d8c26c
Step 2 : RUN apk add --update nodejs
---> Running in 0c556899468a
fetch http://dl-cdn.alpinelinux.org/alpine/v3.3/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.3/community/x86_64/APKINDEX.tar.gz
(1/4) Installing libgcc (5.3.0-r0)
(2/4) Installing libstdc++ (5.3.0-r0)
(3/4) Installing libuv (1.7.5-r0)
(4/4) Installing nodejs (4.3.0-r0)
Executing busybox-1.24.2-r0.trigger
OK: 29 MiB in 15 packages
---> c1f115a02166
Removing intermediate container 0c556899468a
Step 3 : COPY server.js /server.js
---> f77370819a90
Removing intermediate container 381987c09125
Step 4 : EXPOSE 8000
---> Running in 1436288d9ab0
---> 2369ba05a990
Removing intermediate container 1436288d9ab0
Step 5 : CMD node /server.js
---> Running in 85f4e588f395
---> 2f3245169743
Removing intermediate container 85f4e588f395
Successfully built 2f3245169743
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
samplenode 1.0 2f3245169743 6 hours ago 24.55 MB
alpine 3.3 47cf20d8c26c 11 weeks ago 4.797 MB
Run the image locally and test
Let us run the image locally to make sure it works fine.
$ docker run -ti -p 8000:8000 samplenode:1.0
Setting up logging to file server.log
Server running at http://0.0.0.0:8000/
Request from 172.17.0.1
Response sent..
The server responded to an incoming request as expected. Things seem fine!
Writing package descriptor file
The developer will also need to write a package descriptor file specifying requirements for the application. Here is the package.yaml
file.
$ vi package.yaml
descriptor-schema-version: "2.2"
info:
name: SampleNodeApp
description: "Simple Docker Style app that runs a nodejs server"
version: "1.0"
author-link: "http://www.cisco.com"
author-name: "Cisco Systems"
app:
# Indicate app type (vm, paas, lxc etc.,)
cpuarch: "x86_64"
type: docker
resources:
profile: c1.small
network:
-
interface-name: eth0
ports:
tcp: [8000]
# Specify runtime and startup
startup:
rootfs: rootfs.tar
target: ["node", "/server.js"]
Few things to notice:
- Descriptor schema version is 2.2. This is the minimum version that supports docker style apps.
- Note that the cpuarch is x86_64. Alpine based apps can only run on x86_64 bit machines.
- App type is
docker
- The required port (8000) to be opened is specified under network->ports.
rootfs.tar
is the name of the file containing the docker image (output ofdocker save
command). More details in the next sections.- Command to be run when starting up the app is ["node", "/server.js"]. Note that server.js was copied to "/" of rootfs.
Creating an IOx application package
Now that we have created the required docker image (samplenode:1.0) and package.yaml, it is time to create an IOx application package from these artifacts. There are two ways to go about it.
Using ioxclient convenience command
ioxclient
(>= 1.4.0) provides a convenience command that generates an IOx application package from a docker image and package.yaml file.
$ ioxclient docker package -h
NAME:
package - Package an existing docker image as an IOx application
USAGE:
command package [command options] <docker_image> <project_dir>
DESCRIPTION:
Usage: ioxclient docker package <image_name> <project_dir>
image_name
-> is a valid docker image name.
project_dir
-> should be a directory location that has the required IOx files.
At a minimum, must have package.yaml file. Can also contain other
files such as package_config.ini etc.,
Example: docker package cisco/alpine:3.3 .
image_name is cisco/alpine:3.3
project_dir is "." signifying current directory
OPTIONS:
--use-targz Use this to use gz compression on package
Let us use this command. First navigate to the project directory.
$ pwd
/home/<user>/projects/devnet/samples/docker-nodejs
$ ls
Dockerfile package.yaml README.md server.js
$ ioxclient docker package samplenode:1.0 .
Currently active profile : local
Command Name: docker-package
Attempting to save docker image samplenode:1.0
Running command : [docker save -o rootfs.tar samplenode:1.0]
Checking if package descriptor file is present..
Created Staging directory at : /tmp/<user>/607483656
Copying contents to staging directory
Checking for application runtime type
Couldn't detect application runtime type
Creating an inner envelope for application artifacts
Excluding .git/HEAD
...
...
Generated /tmp/<user>/607483656/artifacts.tar.gz
Calculating SHA1 checksum for package contents..
Root Directory : /tmp/<user>/607483656
Output file: /tmp/<user>/840476359
Path: artifacts.tar.gz
SHA1 : dacba40d3e3773061ee00c23d7a3a8d0f1cb24c6
Path: package.yaml
SHA1 : 8528e35786058253da12646a45723e95e80ef9d0
Generated package manifest at package.mf
Generating IOx Package..
Package docker image samplenode:1.0 at /home/<user>/projects/devnet/samples/docker-nodejs/package.tar
The package.tar
is an IOx application package that can be used to deploy on an IOx platform.
**NOTE: The package.yaml uses rootfs.tar
as the name of startup->rootfs. This is essential, since ioxclient
saves the docker image with the name rootfs.tar
.
Manual packaging
If you paid attention to the output of the previous ioxclient
command, it is internally doing a docker save
operation. You could do the same thing manually. Illustrating here to provide more details of the packaging operation.
# Save the docker image to rootfs.tar
$ docker save -o rootfs.tar samplenode:1.0
$ ls
Dockerfile package.yaml README.md rootfs.tar server.js
# Now run the regular ioxclient package command.
$ ioxclient package .
Currently active profile : local
Command Name: package
Checking if package descriptor file is present..
Created Staging directory at : /tmp/<user>/618070125
Copying contents to staging directory
Checking for application runtime type
Couldn't detect application runtime type
Creating an inner envelope for application artifacts
Excluding .git/HEAD
...
...
Generated /tmp/<user>/618070125/artifacts.tar.gz
Calculating SHA1 checksum for package contents..
Root Directory : /tmp/<user>/618070125
Output file: /tmp/<user>/031973096
Path: artifacts.tar.gz
SHA1 : 7d87d73f19c963c682ecdbd29ca7ae5803dd3243
Path: package.yaml
SHA1 : 8528e35786058253da12646a45723e95e80ef9d0
Generated package manifest at package.mf
Generating IOx Package..
Package generated at /home/<user>/projects/devnet/samples/docker-nodejs/package.tar
Deploying and testing on the target platform
Ensure that your target platform supports docker style applications. Refer to Platform Matrix for details about your platform. This tutorial uses IR829 as the target IOx platform.
We will use ioxclient
to demonstrate deployment and lifecycle management of this application. You can do the same using Local Manager or Fog Director.
Add your device to ioxclient
$ ioxclient profiles create
Active Profile : default
Enter a name for this profile : h829
Your IOx platform's IP address[127.0.0.1] : 10.78.106.191
Your IOx platform's port number[8443] :
Authorized user name[root] : root
Password for root :
Local repository path on IOx platform[/software/downloads]:
URL Scheme (http/https) [https]:
API Prefix[/iox/api/v2/hosting/]:
Your IOx platform's SSH Port[2222]:
Activating Profile h829
Install, Activate and Test on the device
Install the app we just created on the target platform.
$ ioxclient application install samplenode ./package.tar
Currently active profile : h829
Command Name: application-install
Installation Successful. App is available at : https://10.78.106.191:8443/iox/api/v2/hosting/apps/samplenode
Successfully deployed
Let us activate with a bridge network and start the application.
# Activate with a bridge network
$ cat activation.json
{
"resources": {
"profile": "c1.small",
"network": [{"interface-name": "eth0", "network-name": "iox-bridge0"}]
}
}
$ ioxclient application activate samplenode --payload activation.json
Currently active profile : h829
Command Name: application-activate
Payload file : activation.json. Will pass it as application/json in request body..
App samplenode is Activated
$ ioxclient app start samplenode
Currently active profile : h829
Command Name: application-start
App samplenode is Started
Let us get the ip address of the application.
$ ioxclient application info samplenode | grep networkInfo -A 13
"networkInfo": {
"eth0": {
"ipv4": "192.168.1.45",
"ipv6": null,
"libvirt_network": "dpbr_0",
"mac": "52:54:dd:d8:c0:5d",
"mac_address": "52:54:dd:d8:c0:5d",
"network_name": "iox-bridge0",
"port_mappings": {
"tcp": [
[
8000,
8000
]
This tells us that the app's ip address is 192.168.1.45
and the external port is 8000
as well (since it is attached to bridge network). To access the apps apis from routers WAN interface, appropriate NAT rules on IOS has to be configured.
Login to your IR829 router's console and ensure that the nat rules are configured.
$ telnet 10.78.106.191
Trying 10.78.106.191...
Connected to 10.78.106.191.
Escape character is '^]'.
User Access Verification
Password:
IR800-191>en
Password:
IR800-191#conf t
Enter configuration commands, one per line. End with CNTL/Z.
IR800-191(config)#ip nat inside source static tcp 192.168.1.45 8000 interface Vlan1 8000
IR800-191(config)#end
IR800-191#sh run | inc nat
ip nat inside
ip nat outside
ip nat inside source list NAT_ACL interface Vlan1 overload
ip nat inside source static tcp 192.168.1.6 8443 interface Vlan1 8443
ip nat inside source static tcp 192.168.1.6 22 interface Vlan1 2222
ip nat inside source static tcp 192.168.1.6 40000 interface Vlan1 40000
ip nat inside source static tcp 192.168.1.45 8000 interface Vlan1 8000
We should now be able to access the app's APIs via router's WAN IP:8000.
$ curl http://10.78.106.191:8000/
Hello World!
Hurray! Our nodejs app just responded to our request with "Hello World!".
The rest of the application development and management concepts remain same as that of other application types.
Summary
As part of this tutorial we saw:
- Using docker tools and image to build a nodejs http server app.
- Writing package descriptor file for docker style apps.
- Packaging a docker image into an IOx application package.
- Deploying this package to an IR829 router and testing the application functionality.