remko de knikker
Nov 30, 2018 · 11 min read

Previously, see here, I created an FDIC-API application similar to the FDIC Developer Portal. In this tutorial, I want to deploy that FDIC API application to a Kubernetes cluster using a deployment with autoscaling.

Steps:

  • Requirements
  • Create a Kubernetes Cluster
  • Get the FDIC-API Source Code
  • Setup CouchDB on Localhost
  • Run the FDIC-API with Docker
  • Setup Current-Context for Kubernetes
  • Create Kubernetes Namespace and Secrets
  • Add Kubernetes Configuration for CouchDB
  • Add Kubernetes Configuration for FDIC-API
  • Add Kubernetes Auto-Scaling

Requirements

Create a Kubernetes Cluster

If you do not have a IBM Cloud account yet, you can create one for free. You can create a free 30-day Kubernetes cluster,

  • Go to IBM Cloud and log in to your free account,
  • In the Dashboard, click the ‘Create resource’ button, to go to the Catalog,
  • Or directly from the Catalog, under Compute, Infrastructure, select the Kubernetes Service,
  • Click the ‘Create’ button,
  • Make sure to select location ‘Dallas’ for the ‘Free’ cluster type to appear as an option,
  • Click ‘Create resource’ or go back to the Catalog again, and click the ‘Container Registry’ window to make sure the registry is created and enabled as well,
  • Select ‘Dallas’ for location, so your registry is in the same location as your free Kubernetes cluster created earlier,
  • Click the ‘Create Cluster’ button,

Get the FDIC-API Source Code

Now, download the source code for the FDIC-API application. We built the FDIC-API application in an earlier tutorial, here.

$ wget https://github.com/remkohdev/fdic-api/archive/v0.1.0.tar.gz -O fdic-api-v0.1.0.tar.gz 
$ tar -xzf fdic-api-v0.1.0.tar.gz
$ cd fdic-api-0.1.0

You need to change the IP to your local machine’s IP address, because localhost will resolve to the container itself instead of the CouchDB container. To find your local machine’s IP run,

$ ipconfig getifaddr en0
192.168.1.5

Edit the datasource configuration file ‘src/server/datasources.json’,

{
"db": {
"name": "db",
"connector": "memory"
},
"couchdb": {
"name": "couchdb",
"connector": "couchdb2",
"url": "http://couchdbadmin:couchdbadmin@192.168.1.5:5984",
"database": "fdic_institutions2"
}
}

Setup CouchDB on Localhost

To setup and run the required dependency CouchDB on localhost, do the following,

$ docker run -d --restart always --hostname localhost --name couchdb -p 5984:5984 -e COUCHDB_USER=couchdbadmin -e COUCHDB_PASSWORD=couchdbadmin -v "$(pwd)"/couchdb-data:/opt/couchdb/data couchdbUnable to find image 'couchdb:latest' locally
latest: Pulling from library/couchdb
bc9ab73e5b14: Downloading [==> ] 1.834MB/45.31MB
42dc232908d3: Download complete
effb777c3c01: Downloading [===> ] 2.768MB/38.88MB
38df29dbabab: Download complete
0a226ee2941e: Waiting
...
Digest: sha256:7d60986a0b43015711121909c50b94dd6a1a6520f8f4bc72f52109b9ea8a17ad
Status: Downloaded newer image for couchdb:latest
23aa7cbc73cf66fc9cb9650109e617d132a911e52fa14803034b8bb1080f9fe0
$ curl -X PUT http://couchdbadmin:couchdbadmin@localhost:5984/fdic_institutions2
{"ok":true}
$ curl -X PUT http://couchdbadmin:couchdbadmin@localhost:5984/_users
{"ok":true}
$ curl -X PUT http://couchdbadmin:couchdbadmin@localhost:5984/_replicator
{"ok":true}
$ curl -X POST -H "Content-Type: application/json" "http://couchdbadmin:couchdbadmin@localhost:5984/fdic_institutions2/_bulk_docs" -d '@data/INSTITUTIONS2.json'$ curl -X PUT -H "Content-Type: application/json" -d '{
"views": {
"name-view": {
"map": "function(doc){ if(doc.NAME){ emit(doc.NAME, doc); }}"
}
},
"language": "javascript"
}' http://couchdbadmin:couchdbadmin@localhost:5984/fdic_institutions2/_design/name
{"ok":true,"id":"_design/name","rev":"1-960fb88bfde563744b82ac8e909c7185"}
$ curl -X PUT -H "Content-Type: application/json" -d '{
"views": {
"asset-view": {
"map": "function(doc){ if(doc.ASSET){ emit(parseInt(doc.ASSET.replace(\/,\/g, \"\")), doc); }}"
}
},
"language": "javascript"
}' http://couchdbadmin:couchdbadmin@localhost:5984/fdic_institutions2/_design/asset
{"ok":true,"id":"_design/asset","rev":"1-9f7027e11ac7614d78ce7b75774b9979"}
$ curl -X PUT -H "Content-Type: application/json" -d '{
"views": {
"name-asset-view": {
"map": "function(doc){ if(doc.NAME && doc.ASSET){ emit(doc.NAME, doc.ASSET); }}"
}
},
"language": "javascript"
}' http://couchdbadmin:couchdbadmin@localhost:5984/fdic_institutions2/_design/name-asset
{"ok":true,"id":"_design/name-asset","rev":"1-12fc244778f26c46aa7e9d379b2843bf"}
$ curl -X PUT -H "Content-Type: application/json" -d '{
"views": {
"fed-rssd-view": {
"map": "function(doc){ if(doc.FED_RSSD){ emit(doc.FED_RSSD); }}"
}
},
"language": "javascript"
}' http://couchdbadmin:couchdbadmin@localhost:5984/fdic_institutions2/_design/fed-rssd
{"ok":true,"id":"_design/fed-rssd","rev":"1-66abb2645d9b728bb853bda3e5ab5ed6"}

Open the admin console for CouchDB at http://localhost:5984/_utils, and login with credentials ‘couchdbadmin/couchdbadmin’.

Run the FDIC-API with Docker

Now the CouchDB dependency is running, you can start the FDIC-API application locally using the utility bash script,

$ sh docker-run.sh
=====>docker stop
Error response from daemon: No such container: fdic-api
Error: No such container: fdic-api
=====>docker build
Sending build context to Docker daemon 78.91MB
Step 1/8 : FROM node:latest
...
e41bca16dd60: Pull complete
Digest: sha256:3f682d97d9615eb1fe7d42489b309592b34b3aceab47b6008489dbd2a713b02c
Status: Downloaded newer image for node:latest
---> e6825fa3bd20
Step 2/8 : WORKDIR /usr/src/app
---> Running in fca8d0d6fa43
Removing intermediate container fca8d0d6fa43
---> ac2a1cf0efa2
Step 3/8 : COPY ./src/package*.json ./
---> 7da71a8552e8
Step 4/8 : RUN npm install
---> Running in 4d325589a1a2
added 466 packages from 463 contributors and audited 2860 packages in 78.616s
found 0 vulnerabilities
Removing intermediate container 4d325589a1a2
---> 97644e6043b4
Step 5/8 : RUN npm audit fix
---> Running in dfb905efa94e
up to date in 2.166s
fixed 0 of 0 vulnerabilities in 2860 scanned packages
Removing intermediate container dfb905efa94e
---> f6576257d311
Step 6/8 : COPY ./src .
---> 68817196a713
Step 7/8 : EXPOSE 3000
---> Running in 2338296049c7
Removing intermediate container 2338296049c7
---> 179aeb64eb7d
Step 8/8 : CMD [ "npm", "start" ]
---> Running in 94066cb56f7f
Removing intermediate container 94066cb56f7f
---> 17f418fc357b
Successfully built 17f418fc357b
Successfully tagged fdic-api:latest
=====>docker run
bf783c3e86405dc7e639d18238b389dce17dbca9ba50f9cf2a3cfeda8d1c9897

View your running Docker containers,

$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bf783c3e8640 fdic-api "npm start" 2 minutes ago Up 2 minutes 0.0.0.0:3000->3000/tcp fdic-api
23aa7cbc73cf couchdb "tini -- /docker-ent…" 30 minutes ago Up 30 minutes 4369/tcp, 9100/tcp, 0.0.0.0:5984->5984/tcp couchdb

Query to test,

$ curl -X GET --header 'Accept: application/json' 'http://localhost:3000/api/Banks/getBanks?name=Citibank%20(Maryland)%2C%20National%20Association'{"total_rows":10183,"offset":1636,"rows":[{"id":"c4bbe0021546cc1e3885e21043000860","key":"Citibank (Maryland), National Association","value":{"_id":"c4bbe0021546cc1e3885e21043000860","_rev":"1-b890346b3bfb63d986e9473bb7bcef6f","STNAME":"Maryland",
...

Set Current-Context for Kubernetes

Set the current-context to the IBM Cloud Cluster using the IBM Cloud CLI,

$ ibmcloud login -a https://api.ng.bluemix.net
API endpoint: https://api.ng.bluemix.net
Email> remkohdev@us.ibm.com
Password>
Authenticating...
OK
$ ibmcloud cs region-set us-south
1. REMKO DE KNIKKER's Account
Select an account:
Enter a number: 1
Targeted account REMKO DE KNIKKER's Account
Targeted resource group default
API endpoint: https://api.ng.bluemix.net
Region:
us-south
User:
remkohdev@us.ibm.com
Account:
REMKO DE KNIKKER's Account
Resource group:
default
CF API endpoint:
Org:
Space:
OK
$ ibmcloud cs cluster-config ibmcloud-cluster
OK
The configuration for ibmcloud-cluster was downloaded successfully.
Export environment variables to start using Kubernetes.
export KUBECONFIG=/Users/user1/.bluemix/plugins/container-service/clusters/ibmcloud-cluster/kube-config-hou02-ibmcloud-cluster.yml
  • Set the KUBECONFIG environment variable by copying and running the output ‘export KUBECONFIG=’ command from the previous command,
$ export KUBECONFIG=/Users/user1/.bluemix/plugins/container-service/clusters/ibmcloud-cluster/kube-config-hou02-ibmcloud-cluster.yml
$ ibmcloud cr login
Logging in to 'registry.ng.bluemix.net'...
Logged in to 'registry.ng.bluemix.net'.
OK
  • Check your current-context,
$ kubectl config view
apiVersion: v1
clusters:
- cluster:
certificate-authority: ca-hou02-ibmcloud-cluster.pem
server: https://c5.dal12.containers.cloud.ibm.com:20428
name: ibmcloud-cluster
contexts:
- context:
cluster: ibmcloud-cluster
namespace: default
user: remkohdev@us.ibm.com
name: ibmcloud-cluster
current-context: ibmcloud-cluster

Add Namespace and Secrets

Now we have the FDIC-API application running locally, and we have set the kubectl context, we are ready to add the Kubernetes resource files to deploy the application to our Kubernetes cluster of the current-context.

Create a directory ‘stable/fdic-api/templates’ in which to save the Kubernetes resource files,

$ mkdir -p stable/fdic-api/templates
$ mkdir -p stable/couchdb/templates

First, create the namespace and secrets that we need for our deployment, I use my own username as the namespace reference, but feel free to choose your own. Just be consistent in replacing ‘remkohdev-ns’ by your own,

$ kubectl create namespace remkohdev-ns
namespace/remkohdev-ns created

Secret

Create a new file ‘stable/couchdb/templates/secret-couchdb-credentials.yaml’,

apiVersion: v1
kind: Secret
metadata:
name: couchdb-credentials
namespace: remkohdev-ns
labels:
app: couchdb
type: Opaque
stringData:
username: couchdbadmin
password: couchdbadmin

Run kubectl to create the secret,

$ kubectl create -f stable/couchdb/templates/secret-couchdb-credentials.yaml
secret/couchdb-credentials created

Alternatively, you can create a secret directly from the commandline,

$ kubectl create secret generic couchdb-credentials -n remkohdev-ns --from-literal=username=couchdbadmin --from-literal=password=couchdbadmin

If you have the username and password saved in a file, use the ‘ — from-file’ option instead of ‘ — from-literal’ option.

ImagePullSecret

Copy the Bluemix secret to the non-default namespace, so Kubernetes can pull images,

$ kubectl get secret bluemix-default-secret -o yaml | sed 's/default/remkohdev-ns/g' | kubectl -n remkohdev-ns create -f -
secret/bluemix-remkohdev-ns-secret created

I now have created the secret ‘couchdb-credentials’ that are used by the FDIC-API container and other containers potentially to obtain the credentials to access CouchDB. I also created ‘bluemix-default-secret’ that is used by the Kubernetes cluster to pull images from the private registry.

$ kubectl get secrets -n remkohdev-ns
NAME TYPE DATA AGE
bluemix-remkohdev-ns-secret kubernetes.io/dockerconfigjson 1 24s
couchdb-credentials Opaque 2 42s
default-token-bf92w kubernetes.io/service-account-token 3 1m

Add Kubernetes Configuration for CouchDB

Deployment

Create a new deployment resource file ‘stable/couchdb/templates/deployment.yaml’,

apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: couchdb-deployment
namespace: remkohdev-ns
labels:
app: couchdb
spec:
replicas: 1
selector:
matchLabels:
app: couchdb
template:
metadata:
labels:
app: couchdb
spec:
containers:
- name: couchdb
image: apache/couchdb
volumeMounts:
- mountPath: /opt/couchdb/data
name: couchdb-data
ports:
- name: couchdb
protocol: TCP
containerPort: 5984
env:
- name: COUCHDB_USER
valueFrom:
secretKeyRef:
name: couchdb-credentials
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-credentials
key: password
volumes:
- name: couchdb-credentials-secret-volume
secret:
secretName: couchdb-credentials
- name: couchdb-data
hostPath:
path: /data
type: DirectoryOrCreate

Create the deployment for couchdb,

$ kubectl create -f stable/couchdb/templates/deployment.yaml
deployment.apps/couchdb-deployment created

Service

Create a new file ‘stable/couchdb/templates/service.yaml’,

apiVersion: v1
kind: Service
metadata:
name: couchdb-svc
namespace: remkohdev-ns
labels:
app: couchdb
spec:
type: NodePort
ports:
- name: couchdb
protocol: TCP
port: 5984
targetPort: 5984
selector:
app: couchdb

If you want to expose CouchDB to the public, create the service for couchdb,

$ kubectl create -f stable/couchdb/templates/service.yaml
service/couchdb-svc created

Initialize the Database

Use the Public IP of the cluster (e.g. 173.193.92.121) and service port (e.g. 30156) to initialize the database.

To find the Public IP of the cluster,

$ kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="ExternalIP")].address}'

Or browse to the ‘Worker Nodes’ tab of the cluster overview in Bluemix, and get the Cluster IP from the ‘Public IP’ column.

To find the exposing port, i.e. NodePort, of the service,

$ kubectl describe service -n remkohdev-ns couchdb-svcName:                     couchdb-svc
Namespace: remkohdev-ns
Labels: app=couchdb
Annotations: <none>
Selector: app=couchdb
Type: NodePort
IP: 172.21.180.228
Port: couchdb 5984/TCP
TargetPort: 5984/TCP
NodePort: couchdb 30156/TCP
Endpoints: 172.30.136.185:5984
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>

Make sure to replace the IP and port in the following commands with your own,

$ curl -X PUT http://couchdbadmin:couchdbadmin@173.193.92.121:30156/fdic_institutions2$ curl -X POST -H "Content-Type: application/json" "http://couchdbadmin:couchdbadmin@173.193.92.121:30156/fdic_institutions2/_bulk_docs" -d '@data/INSTITUTIONS2.json'$ curl -X PUT -H "Content-Type: application/json" -d '{
"views": {
"name-view": {
"map": "function(doc){ if(doc.NAME){ emit(doc.NAME, doc); }}"
}
},
"language": "javascript"
}' http://couchdbadmin:couchdbadmin@173.193.92.121:30156/fdic_institutions2/_design/name
$ curl -X PUT -H "Content-Type: application/json" -d '{
"views": {
"asset-view": {
"map": "function(doc){ if(doc.ASSET){ emit(parseInt(doc.ASSET.replace(\/,\/g, \"\")), doc); }}"
}
},
"language": "javascript"
}' http://couchdbadmin:couchdbadmin@173.193.92.121:30156/fdic_institutions2/_design/asset
$ curl -X PUT -H "Content-Type: application/json" -d '{
"views": {
"name-asset-view": {
"map": "function(doc){ if(doc.NAME && doc.ASSET){ emit(doc.NAME, doc.ASSET); }}"
}
},
"language": "javascript"
}' http://couchdbadmin:couchdbadmin@173.193.92.121:30156/fdic_institutions2/_design/name-asset
$ curl -X PUT -H "Content-Type: application/json" -d '{
"views": {
"fed-rssd-view": {
"map": "function(doc){ if(doc.FED_RSSD){ emit(doc.FED_RSSD); }}"
}
},
"language": "javascript"
}' http://couchdbadmin:couchdbadmin@173.193.92.121:30156/fdic_institutions2/_design/fed-rssd

Add Kubernetes Configuration for FDIC-API

Deployment

Create a new file ‘stable/fdic-api/templates/deployment.yaml’,

apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: fdic-api-deployment
namespace: remkohdev-ns
labels:
app: fdic-api
spec:
replicas: 1
selector:
matchLabels:
app: fdic-api
template:
metadata:
labels:
app: fdic-api
spec:
containers:
- name: fdic-api
image: registry.ng.bluemix.net/remkohdev-ns/fdic-api:latest
ports:
- name: http
protocol: TCP
containerPort: 3000
env:
- name: COUCHDB_USERNAME
valueFrom:
secretKeyRef:
name: couchdb-credentials
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-credentials
key: password
envFrom:
- configMapRef:
name: fdic-api-configmap
imagePullSecrets:
- name: bluemix-remkohdev-ns-secret

Service

Create a new file ‘stable/fdic-api/templates/service.yaml’,

apiVersion: v1
kind: Service
metadata:
name: fdic-api-svc
namespace: remkohdev-ns
labels:
app: fdic-api
spec:
type: NodePort
ports:
- name: http
protocol: TCP
port: 3000
targetPort: 3000
selector:
app: fdic-api

ConfigMap

Create a new file ‘stable/fdic-api/templates/configMap.yaml’,

apiVersion: v1
kind: ConfigMap
metadata:
name: fdic-api-configmap
namespace: remkohdev-ns
labels:
app: fdic-api
data:
NODE_ENV: development
COUCHDB_SERVER: couchdb-svc.remkohdev-ns
COUCHDB_PORT: "5984"
COUCHDB_PROTOCOL: http

Deployment Bash Script

Create a bash script to deploy the FDIC-API application, ‘k8s-deploy.sh’ to run the kubectl commands to create the above Kubernetes configuration,

echo '---------------------------'
echo '=====>deploy fdic-api<====='
echo '---------------------------'
echo '=====>build fdic-api<====='
docker build --no-cache -t fdic-api .
echo '=====>tag fdic-api<====='
docker tag fdic-api:latest registry.ng.bluemix.net/remkohdev-ns/fdic-api:latest
echo '=====>login to registry<====='
# make sure you are logged in before you run this script
# ibmcloud login -a https://api.ng.bluemix.net
# ibmcloud cs region-set us-south
# ibmcloud cs cluster-config ibmcloud-cluster
# export KUBECONFIG=/...
# ibmcloud cr login
echo '=====>push fdic-api<====='
docker push registry.ng.bluemix.net/remkohdev-ns/fdic-api:latest
echo '=====>delete fdic-api-configmap'
kubectl delete configmap -n remkohdev-ns fdic-api-configmap
echo '=====>create fdic-api-configmap'
kubectl create -n remkohdev-ns -f stable/fdic-api/templates/configmap.yaml
echo '=====>delete fdic-api-deployment<====='
kubectl delete deployment -n remkohdev-ns fdic-api-deployment
# while resource still exists wait
rc=$(eval 'kubectl get deployment -n remkohdev-ns fdic-api-deployment')
while [ ! -z "$rc" ]
do
rc=$(eval 'kubectl get deployment -n remkohdev-ns fdic-api-deployment')
done
echo '=====>create fdic-api-deployment<====='
kubectl create -f stable/fdic-api/templates/deployment.yaml
echo '=====>delete fdic-api-svc<====='
kubectl delete svc -n remkohdev-ns fdic-api-svc
# while resource still exists wait
rc=$(eval 'kubectl get svc -n remkohdev-ns fdic-api-svc')
while [ ! -z "$rc" ]
do
rc=$(eval 'kubectl get svc -n remkohdev-ns fdic-api-svc')
done
echo '=====>create fdic-api-svc<====='
kubectl create -f stable/fdic-api/templates/service.yaml
echo '-----------------------------'
echo '=====>fdic-api deployed<====='
echo '-----------------------------'

Before you run the deploy script, change the ‘src/server/datasources.json’ file to point to the previously created CouchDB service by referring to the Kubernetes domain name of the service and the namespace instead of its IP address,

"couchdb": {
"name": "couchdb",
"connector": "couchdb2",
"url": "http://couchdbadmin:couchdbadmin@couchdb-svc.remkohdev-ns:5984",
"database": "fdic_institutions2"
}

Now run the deploy bash script,

$ sh k8s-deploy.sh

Use the Public IP of the cluster and find the NodePort of the fdic-api service,

$ k describe service -n remkohdev-ns fdic-api-svcName:                     fdic-api-svc
Namespace: remkohdev-ns
Labels: app=fdic-api
Annotations: <none>
Selector: app=fdic-api
Type: NodePort
IP: 172.21.86.181
Port: http 3000/TCP
TargetPort: 3000/TCP
NodePort: http 32421/TCP
Endpoints: 172.30.136.138:3000
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>

Test the deployment with the query,

$ curl -X GET --header 'Accept: application/json' 'http://173.193.92.121:32421/api/Banks/getBanks?name=Citibank%20(Maryland)%2C%20National%20Association'{"total_rows":10183,"offset":1636,"rows":[{"id":"58c1099c03c6a2dd7e1026e527000efe","key":"Citibank (Maryland), National Association","value":{"_id":"58c1099c03c6a2dd7e1026e527000efe","_rev":"1-b890346b3bfb63d986e9473bb7bcef6f","STNAME":"Maryland","CERT":25295
...

You have now a working Kubernetes deployment of our FDIC-API application.

Add Kubernetes Auto-Scaling

Horizontal Pod Autoscaling (HPA)

To add auto-scaling to the fdic-api deployment, you can create a Horizontal Pod Autoscaling (HPA). Create a file ‘stable/fdic-api/templates/hpa.yaml’,

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: fdic-api-hpa
namespace: remkohdev-ns
spec:
maxReplicas: 10
minReplicas: 2
scaleTargetRef:
apiVersion: extensions/v1beta1
kind: Deployment
name: fdic-api-deployment
targetCPUUtilizationPercentage: 80

Run the following kubectl command to create the HPA,

$ kubectl create -f stable/fdic-api/templates/hpa.yaml
horizontalpodautoscaler.autoscaling/fdic-api-hpa created

After a few seconds, you should see that the minReplicas property triggers Kubernetes to scale the deployment for fdic-api to 2 pods. The controller loop checks for changes every 30 seconds by default.

Create a new file ‘stable/couchdb/templates/hpa.yaml’,

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: couchdb-hpa
namespace: remkohdev-ns
spec:
maxReplicas: 10
minReplicas: 2
scaleTargetRef:
apiVersion: extensions/v1beta1
kind: Deployment
name: couchdb-deployment
targetCPUUtilizationPercentage: 80

To create the Horizontal Pod Autoscaler for CouchDB, run

$ kubectl create -f stable/couchdb/templates/hpa.yaml
horizontalpodautoscaler.autoscaling/couchdb-hpa created

Congratulations! You should now have an auto-scalable Kubernetes deployment of the FDIC-API application. If you check your Kubernetes dashboard, you should see that Kubernetes now has autoscaled the number of pods for CouchDB and FDIC-API to 2 per deployment,

Get the source code from https://github.com/remkohdev/fdic-api/releases/tag/v0.2.0

NYC⚡️DEV

NYC Developer Community

remko de knikker

Written by

Developer Advocate @IBMDeveloper @IBMCloud — stateless tech humanist, serpent in the shepherds mouth — Dutch NY-er

NYC⚡️DEV

NYC Developer Community

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade