7. Containerized Application Development

In this lab, we will declare and create a cohesive multi-application stack with services through Kubernetes.

Chapter Details
Chapter Goal Declare, deploy, and verify a containerized application
Chapter Sections

7.1. Application Architecture

In this lab we will deploy the ghost blogging and online publication platform with a MySQL backend. The applications will reside within separate Pods. We will declare and create a Service for MySQL which ghost will use to connect to the database. We will also use a Kubernetes secret to hold our database connection information such as username, passwords, and database name.

Since MySQL is persisting the data on behalf of ghost, we will provision a persistent volume that will be used to house the data for MySQL.

To be able to access the ghost application from outside the Kubernetes cluster, we will also create a Service for ghost to allow for access.

Finally, to manage our application as a single unit we will create a new namespace for it. This allows us to to use the same configuration files and resource names to launch multiple instances of our application.

7.2. Placing Workloads In Separate NameSpaces

Create a new Kubernetes namespace to isolate application resources.

Step 1 Create a new namespace and kubectl context for it:

$ kubectl create namespace myblog
namespace "myblog" created

$ kubectl config set-context myblog --cluster=kubernetes --user=kubernetes-admin --namespace=myblog
Context "myblog" created.

Step 2 Check resources in the new namespace:

$ kubectl get all -n myblog
No resources found.

Step 3 Switch your default context to the new one:

$ kubectl config use-context myblog
Switched to context "myblog".

7.3. Kubernetes Secrets

Kubernetes secrets allow users to define sensitive information outside of containers and expose that information to containers through environment variables as well as files within Pods. In this section we will declare and create secrets to hold our database connection information that will be used by ghost to connect to its backend database.

Step 1 Create a text file with format of key=value that holds the MySQL secrets:

$ cat <<EOF > ghost-secrets.txt
admin-password=super-secret
dbname=ghost_db
username=student
password=secret
EOF

Step 2 Create the kubernetes secret:

$ kubectl create secret generic ghost-secrets --from-env-file=ghost-secrets.txt
secret "ghost-secrets" created

Step 3 Verify creation and get details:

$ kubectl describe secrets/ghost-secrets
Name:         ghost-secrets
Namespace:    myblog
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
admin-password:  12 bytes
dbname:          8 bytes
password:        6 bytes
username:        7 bytes

7.4. Create a Persistent Volume

We will use a Persistent Volume (PV) to provide the underlying storage for our database. In our environment, we will use a HostPath device, which will just pass a directory from the host into a container for consumption. This type of volume is only appropriate for a single node test environment, as there is no guarantee Pods will get recreated where the storage was originally allocated when multiple worker nodes are deployed.

If you are using a hosted Kubernetes environment, such as on Google Cloud Platform, the PV can be created as GCE Persistent disks rather than HostPath. Since the implementation of the PV is hidden from the usage, the application remains portable by just changing the PV type to GCEPersistentDisk, AWSElasticBlockStore, AzureDisk, or other supported types.

Step 1 Create a Kubernetes persistent volume and verify its status is set to Available:

$ kubectl create -f - <<EOF
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv1
spec:
  capacity:
    storage: 5Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: /data/pv1
    type: DirectoryOrCreate
EOF
persistentvolume "pv1" created

Step 2 Verify that the status of the PV is set to Available:

$ kubectl describe pv pv1
Name:            pv1
Labels:          <none>
Annotations:     <none>
StorageClass:
Status:          Available
Claim:
Reclaim Policy:  Retain
Access Modes:    RWO
Capacity:        5Gi
Message:
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /data/pv1
    HostPathType:  DirectoryOrCreate
Events:            <none>

7.5. Create a Persistent Volume Claim

Before launching the MySQL application we will create and bind a Persistent Volume Claim (PVC) that will host the database instance for ghost.

Step 1 Create the PVC:

$ kubectl apply -f - <<EOF
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: ghost-pv-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
EOF
persistentvolumeclaim "ghost-pv-claim" created

Step 2 Verify that the status of the PVC is set to Bound:

$ kubectl describe pvc ghost-pv-claim
Name:          ghost-pv-claim
Namespace:     k8s-user
StorageClass:
Status:        Bound
Volume:        pv1
Labels:        <none>
Annotations:   pv.kubernetes.io/bind-completed=yes
               pv.kubernetes.io/bound-by-controller=yes
Finalizers:    []
Capacity:      5Gi
Access Modes:  RWO
Events:        <none>

7.6. Create a Service for MySQL

Pods are ephemeral. They come and go as needed with each newly created Pod receiving a new and different IP address. In order for Pods to reliably communicate with one another, we need an IP address that is decoupled from that of a Pod and never changes, and this is exactly what Kubernetes services offer.

Step 1 Create a kubernetes Service to provide a stable IP address for the MySQL application:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: mysql-internal
spec:
  ports:
    - port: 3306
      protocol: TCP
      targetPort: 3306
  selector:
    app: mysql
EOF
service "mysql-internal" created

Step 2 Verify the service properties:

$ kubectl describe svc/mysql-internal
Name:              mysql-internal
Namespace:         k8s-user
Labels:            <none>
Selector:          app=mysql
Type:              ClusterIP
IP:                10.96.173.112
Port:              <unset>  3306/TCP
TargetPort:        3306/TCP
Endpoints:         <none>
Session Affinity:  None
Events:            <none>

From the output above, we can verify the service IP address is allocated but the service currently has no Endpoints. The Endpoints will be filled in when the Selector returns matching pods.

7.7. Create the MySQL Deployment

We are now ready to create the MySQL application. In a real production environment MySQL should be deployed as a galera cluster using kubernetes StatefulSet to achieve high-availability (HA) for the database. However, in our lab we will use a single replica and a kubernetes Deployment instead.

Step 1 Create the mysql-deployment:

$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: mysql
        track: production
    spec:
      containers:
      - name: mysql
        image: mysql:5.6
        ports:
        - containerPort: 3306
        volumeMounts:
        - mountPath: "/var/lib/mysql"
          name: mysql-pv
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: ghost-secrets
              key: admin-password
        - name: MYSQL_USER
          valueFrom:
            secretKeyRef:
              name: ghost-secrets
              key: username
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: ghost-secrets
              key: password
        - name: MYSQL_DATABASE
          valueFrom:
            secretKeyRef:
              name: ghost-secrets
              key: dbname
      volumes:
        - name: mysql-pv
          persistentVolumeClaim:
            claimName: ghost-pv-claim
EOF
deployment "mysql-deployment" created

In the above we are specifying our standard information such as container name, image to use, and exposed port to access. In the pod template spec we are also referencing the ghost-pv-claim PVC previously created, and mounting it at /var/lib/mysql where MySQL creates its database instances. Knowing this requires a good understanding of how MySQL works.

Additionally, we are also declaring environment variables to initialize this MySQL instance. The MySQL image we are using that is available on Docker Hub supports environment variable injection. The four environment variables we are initializing are defined and used within the Docker image itself. The values we are setting these environment variables to are all referencing different keys we set in our Secret earlier on. When this container starts up, we will automatically have MySQL configured with the desired root user password, and the database will be created with access granted for our user.

Step 2 Verify that the deployment was created successfully:

$ kubectl get deployments
NAME              DESIRED   CURRENT   UP-TO-DATE  AVAILABLE   AGE
mysql-deployment   1         1         1            1           2m

Step 3 Let’s verify the device was properly mapped into the container and the database was successfully created:

$ kubectl get pods -o wide
NAME                                READY     STATUS    RESTARTS   AGE       IP                NODE
mysql-deployment-<id>               1/1       Running   0          2m        192.168.166.XXX   node1

$ kubectl exec -it mysql-deployment-<id> bash
root@mysql-deployment:/# mount | grep /var/lib/mysql
/dev/xvda1 on /var/lib/mysql type ext4 (rw,relatime,discard,data=ordered)

root@mysql-deployment:/# mysql -u root -p
Enter password: super-secret

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| ghost_db           |
| mysql              |
| performance_schema |
+--------------------+
4 rows in set (0.00 sec)

mysql> show grants for student;
+--------------------------------------------------------------------------------------------------------+
| Grants for student@%                                                                                   |
+--------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'student'@'%' IDENTIFIED BY PASSWORD '*14E65567ABDB5135D0CFD9A70B3032C179A49EE7' |
| GRANT ALL PRIVILEGES ON `ghost_db`.* TO 'student'@'%'                                                  |
+--------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

mysql> exit
Bye
root@mysql-deployment:/# exit
exit

7.8. Create a Service for ghost

The first thing required is to create a stable IP address to expose the ghost application to external users. For this, we will need a service of type NodePort or LoadBalancer. In this example, we will use NodePort since our deployment is not integrated with any external loadbalancers.

Kubernetes can integrate with Load Balancing services offered by platforms such as GCE and AWS. If you are using either of those, then that would be one approach to take to take advantage of the load balancing functionality offered in those platforms.

Step 1 Create a service of type NodePort:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: ghost-service
  labels:
    app: ghost
    track: production
spec:
  type: NodePort
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: ghost
    track: production
EOF
service "ghost-service" created

Step 2 Verify the service status and note down the assigned port number (in our case 31760):

$ kubectl describe svc/ghost-service
Name:                     ghost-service
Namespace:                myblog
Labels:                   app=ghost
                          track=production
Selector:                 app=ghost,track=production
Type:                     NodePort
IP:                       ...
Port:                     <unset>  8080/TCP
TargetPort:               8080/TCP
NodePort:                 <unset>  31760/TCP
Endpoints:                <none>
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

Save the port number in and environment variable:

$ GhostPort=$(kubectl get svc ghost-service -o jsonpath={..nodePort})

7.9. Create the Ghost ConfigMap

Our application pod comprises of the ghost container plus an nginx container that acts as a reverse-proxy for ghost. The golden images for ghost and nginx are available on Docker Hub, but require configuration injection. Before launching the application we will create the kubernetes ConfigMap needed for both. We can create seperate ConfigMap for each container, or one ConfigMap for the pod and access the keys separately. In this example we opt for creating a single ConfigMap for the pod.

Step 1 Create a directory to hold all the ConfigMap:

$ mkdir ghost-configs

Step 2 Create an nginx configuration to reverse-proxy for ghost:

$ cat <<EOF > ghost-configs/nginx-ghost.conf
server {
  listen 8080;
  server_name example.com;

  location / {
    proxy_set_header X-Real-IP  \$remote_addr;
    proxy_set_header Host   \$http_host;
    proxy_pass       http://127.0.0.1:2368;
  }
}
EOF

Step 3 Create a ghost config file to set its url and database settings. Our configuration changes are highlighted below. Note the environment parameter substitutions in the url. $PublicIP is predefined in your lab. $GhostPort was defined above after creation of ghost-service.

The database connection settings are also initialized by environment variables, but these environment variables are set within the container process. MYSQL_INTERNAL_SERVICE_HOST will be defined automatically by kubernetes for every container launched after the creation of the mysql-internal service. This is how we are telling the ghost application to access its database through the Kubernetes service we created in the previous section. The remaining variables, GHOST_DB_USER, GHOST_DB_PASSWORD, and GHOST_DB_NAME, we will set in the pod template spec using the ghost-secrets secret:

$ cat <<EOF > ghost-configs/ghost-config.js
var path = require('path'),
    config;

config = {
    // ### Production
    production: {
        url: 'http://my-ghost-blog.com',
        mail: {},
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(process.env.GHOST_CONTENT, '/data/ghost.db')
            },
            debug: false
        },
        server: {
            host: '0.0.0.0',
            port: '2368'
        }
    },

    // ### Development **(default)**
    development: {
        // The url to use when providing links to the site, E.g. in RSS and email.
        // Change this to your Ghost blog's published URL.
        url: 'http://${PublicIP}:${GhostPort}',

        // #### Database
        database: {
            client: 'mysql',
            connection: {
              host    : process.env.MYSQL_INTERNAL_SERVICE_HOST,
              user    : process.env.GHOST_DB_USER,
              password  : process.env.GHOST_DB_PASSWORD,
              database  : process.env.GHOST_DB_NAME,
              charset : 'utf8'
            },
            debug: true
        },

        // #### Server
        server: {
            host: '0.0.0.0',
            port: '2368'
        },

        // #### Paths
        // Specify where your content directory lives
        paths: {
            contentPath: path.join(process.env.GHOST_CONTENT, '/')
        }
    },

};

module.exports = config;
EOF

Step 4 Create the ghost-configs/docker-entrypoint.sh:

#!/bin/bash
set -e

cp /tmp/ghost/config.js /var/lib/ghost

# allow the container to be started with `--user`
if [[ "$*" == npm*start* ]] && [ "$(id -u)" = '0' ]; then
        chown -R user "$GHOST_CONTENT"
        exec gosu user "$BASH_SOURCE" "$@"
fi

if [[ "$*" == npm*start* ]]; then
        baseDir="$GHOST_SOURCE/content"
        for dir in "$baseDir"/*/ "$baseDir"/themes/*/; do
                targetDir="$GHOST_CONTENT/${dir#$baseDir/}"
                mkdir -p "$targetDir"
                if [ -z "$(ls -A "$targetDir")" ]; then
                        tar -c --one-file-system -C "$dir" . | tar xC "$targetDir"
                fi
        done

        if [ ! -e "$GHOST_CONTENT/config.js" ]; then
                sed -r '
                        s/127\.0\.0\.1/0.0.0.0/g;
                        s!path.join\(__dirname, (.)/content!path.join(process.env.GHOST_CONTENT, \1!g;
                ' "$GHOST_SOURCE/config.example.js" > "$GHOST_CONTENT/config.js"
        fi
fi

exec "$@"

Step 5 Create the ConfigMap for our application pod :

$ kubectl create configmap ghost-v0.11-config --from-file=ghost-configs
configmaps "ghost-v0.11-config" created

Check the settings of the ConfigMap:

$ kubectl describe cm ghost-v0.11-config
Name:         ghost-v0.11-config
Namespace:    myblog
Labels:       <none>
Annotations:  <none>

Data
====
ghost-config.js:
----
var path = require('path'),
    config;

config = {
    // ### Production
    production: {
        url: 'http://my-ghost-blog.com',
        mail: {},
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(process.env.GHOST_CONTENT, '/data/ghost.db')
            },
            debug: false
        },
        server: {
            host: '0.0.0.0',
            port: '2368'
        }
    },

    // ### Development **(default)**
    development: {
        // The url to use when providing links to the site, E.g. in RSS and email.
        // Change this to your Ghost blog's published URL.
        url: 'http://13.57.234.247:31760',

        // #### Database
        database: {
            client: 'mysql',
            connection: {
              host    : process.env.MYSQL_INTERNAL_SERVICE_HOST,
              user    : process.env.GHOST_DB_USER,
              password  : process.env.GHOST_DB_PASSWORD,
              database  : process.env.GHOST_DB_NAME,
              charset : 'utf8'
            },
            debug: true
        },

        // #### Server
        server: {
            host: '0.0.0.0',
            port: '2368'
        },

        // #### Paths
        // Specify where your content directory lives
        paths: {
            contentPath: path.join(process.env.GHOST_CONTENT, '/')
        }
    },

};

module.exports = config;

nginx-ghost.conf:
----
server {
  listen 8080;
  server_name example.com;

  location / {
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header Host   $http_host;
    proxy_pass       http://127.0.0.1:2368;
  }
}

Events:  <none>

7.10. Create the Ghost Deployment

With MySQL database deployed, ghost-service and ghost-configs defined, we are ready to deploy our application. Ghost is a fully open source, adaptable platform for building and running an online blog or publication, built on Node.js with an Ember.js admin client, a JSON API, and a theme API powered by Handlebars.js.

Step 1 Create a file named ghost-deployment.yaml with the below content:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ghost-v0.11
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ghost
      track: production
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: ghost
        track: production
    spec:
      containers:
      - name: nginx
        image: nginx:1.15
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: nginx-conf
          mountPath: /etc/nginx/conf.d
      - name: ghost
        image: ghost:0.11
        command: ["/tmp/ghost/bin/docker-entrypoint.sh"]
        args: ["npm", "start"]
        ports:
        - containerPort: 2368
        env:
        - name: GHOST_DB_USER
          valueFrom:
            secretKeyRef:
              name: ghost-secrets
              key: username
        - name: GHOST_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: ghost-secrets
              key: password
        - name: GHOST_DB_NAME
          valueFrom:
            secretKeyRef:
              name: ghost-secrets
              key: dbname
        volumeMounts:
        - name: config-js
          mountPath: /tmp/ghost
        - name: docker-entrypoint
          mountPath: /tmp/ghost/bin
      volumes:
      - name: nginx-conf
        configMap:
          name: ghost-v0.11-config
          items:
          - key: nginx-ghost.conf
            path: ghost.conf
      - name: docker-entrypoint
        configMap:
          name: ghost-v0.11-config
          defaultMode: 0777
          optional: true
          items:
          - key:  docker-entrypoint.sh
            path:  docker-entrypoint.sh
      - name: config-js
        configMap:
          name: ghost-v0.11-config
          defaultMode: 0777
          optional: true
          items:
          - key: ghost-config.js
            path: config.js

In the highlighted sections above, we can see how container environment variables GHOST_DB_USER, GHOST_DB_PASSWORD, and GHOST_DB_NAME, using ghost-secrets. Also highlighted are volumes of type configMap defined by ghost-v0.11-config and mounted to the nginx and ghost containers at mount path of /etc/nginx/conf.d/ghost.conf and /var/lib/ghost/config.js respectively:

Step 2 Create and verify the deployment:

$ kubectl apply -f ghost-deployment.yaml
deployment "ghost-deployment" created

$ kubectl get deployments
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
ghost-v0.11        1         1         1            1           5m
mysql-deployment   1         1         1            1           45m

Step 3 Open up your browser and navigate to http://<lab IP>:<ghost port>. You can also administer your ghost application at http://<Public IP>:<Ghost Port>/ghost.

Step 4 Scale your application:

$ kubectl scale deployment ghost-v0.11 --replicas=2
deployment "ghost-v0.11" scaled

$ kubectl get deployments ghost-v0.11
NAME          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
ghost-v0.11   2         2         2            2           10m

Notice the addition of the new EndPoints to the ghost-service:

$ kubectl describe service ghost-service
Name:                     ghost-service
Namespace:                myblog
Labels:                   app=ghost
                          track=production
Selector:                 app=ghost
Type:                     NodePort
IP:                       10.108.153.254
Port:                     <unset>  8080/TCP
TargetPort:               8080/TCP
NodePort:                 <unset>  31760/TCP
Endpoints:                192.168.104.4:8080,192.168.166.131:8080
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

Congratulations on deploying a containerized multi-tier application stack with Kubernetes!

7.11. Launch Multiple Workloads

Before we launch another instance of ghost we have a design decision to make regarding the database. Should each instance have its own database, or should we share a single MySQL database for all the instances? In a production enviroment, where we have already taken the trouble of deploying MySQL in HA mode, it makes better sense to share a single large MySQL database among multiple instances of ghost.

Although our database in this lab is not HA, we will base our approach on this design principle. So our first step is to create a new MySQL database for the new ghost instance. This should be done by a database administrator, because in this model the database is shared, and technically outside of the ghost deployment.

Step 1 As the DBA create a new database:

$ kubectl exec -it --context=myblog mysql-deployment-<id> -- bash
root@mysql-deployment-<id>:/# mysql -u root -psuper-secret
mysql> create database yourblog_db;
mysql> grant all on yourblog_db.* to 'customer'@'%' identified by 'customersecret';
mysql> exit

Test access to the new database:

$ kubectl exec -it --context=myblog mysql-deployment-5d8556dbb8-zlj9f -- mysql -u customer -pcustomersecret -e "show databases;"
+--------------------+
| Database           |
+--------------------+
| information_schema |
| yourblog_db        |
+--------------------+

Share just the credentials for this database with the developer:

$ cat <<EOF > yourblog-secrets.txt
dbname=yourblog_db
username=customer
password=customersecret
EOF

Step 2 As the yourblog developer create a new Kubernetes namespace called yourblog and kubectl context for it:

$ kubectl create namespace yourblog
namespace "yourblog" created

$ kubectl config set-context yourblog --cluster=kubernetes --user=kubernetes-admin --namespace=yourblog
Context "yourblog" created.

$ kubectl config use-context yourblog
Switched to context "yourblog".

Step 3 Create the kubernetes secret:

$ kubectl create secret generic yourblog-secrets --from-env-file=yourblog-secrets.txt
secret "yourblog-secrets" created

Step 4 Create a service of type NodePort:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: yourblog
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    app: ghost
    track: custom
EOF
service "yourblog" created

Step 5 Verify the service status and note down the assigned port number (in our case 32556):

$ kubectl describe svc/yourblog
Name:                     yourblog
Namespace:                yourblog
Selector:                 app=ghost,track=custom
Type:                     NodePort
IP:                       10.105.68.126
Port:                     <unset>  8080/TCP
TargetPort:               8080/TCP
NodePort:                 <unset>  32556/TCP
Endpoints:                <none>
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

Save the port number in and environment variable:

$ GhostPort=$(kubectl get svc yourblog -o jsonpath={..nodePort})

Step 6 Create the ConfigMap for our application pod. We can reuse the ghost-configs/nginx-ghost.conf as is, but have to make changes to ghost-configs/ghost-config.js:

$ cp -r ghost-configs yourblog-configs

Generate a new ghost-config.js with different url and database host. Notice that we are switching from service discovery by environment variable, to service discovery through DNS lookup for host, because the former is not possible in the case of services in other namespaces:

$ cat <<EOF > yourblog-configs/ghost-config.js
var path = require('path'),
    config;

config = {
    // ### Production
    production: {
        url: 'http://my-ghost-blog.com',
        mail: {},
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(process.env.GHOST_CONTENT, '/data/ghost.db')
            },
            debug: false
        },
        server: {
            host: '0.0.0.0',
            port: '2368'
        }
    },

    // ### Development **(default)**
    development: {
        // The url to use when providing links to the site, E.g. in RSS and email.
        // Change this to your Ghost blog's published URL.
        url: 'http://${PublicIP}:${GhostPort}',

        // #### Database
        database: {
            client: 'mysql',
            connection: {
              host    : 'mysql-internal.myblog',
              user    : process.env.GHOST_DB_USER,
              password  : process.env.GHOST_DB_PASSWORD,
              database  : process.env.GHOST_DB_NAME,
              charset : 'utf8'
            },
            debug: true
        },

        // #### Server
        server: {
            host: '0.0.0.0',
            port: '2368'
        },

        // #### Paths
        // Specify where your content directory lives
        paths: {
            contentPath: path.join(process.env.GHOST_CONTENT, '/')
        }
    },

};

module.exports = config;
EOF

Create the ConfigMap for our application pod :

$ kubectl create configmap yourblog-cm --from-file=yourblog-configs
configmap "yourblog-cm" created

Step 7 Create a file named yourblog-deployment.yaml with the below content:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: yourblog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ghost
      track: custom
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: ghost
        track: custom
    spec:
      containers:
      - name: nginx
        image: nginx:1.15
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: nginx-conf
          mountPath: /etc/nginx/conf.d
      - name: ghost
        image: ghost:0.11
        command:
        - /tmp/ghost/bin/docker-entrypoint.sh
        args: ["npm", "start"]
        ports:
        - containerPort: 2368
        env:
        - name: GHOST_DB_USER
          valueFrom:
            secretKeyRef:
              name: yourblog-secrets
              key: username
        - name: GHOST_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: yourblog-secrets
              key: password
        - name: GHOST_DB_NAME
          valueFrom:
            secretKeyRef:
              name: yourblog-secrets
              key: dbname
        volumeMounts:
        - name: config-js
          mountPath: /tmp/ghost
        - name: docker-entrypoint
          mountPath: /tmp/ghost/bin
      volumes:
      - name: nginx-conf
        configMap:
          name: yourblog-cm
          items:
          - key: nginx-ghost.conf
            path: ghost.conf
      - name: docker-entrypoint
        configMap:
          name: yourblog-cm
          defaultMode: 0777
          items:
          - key:  docker-entrypoint.sh
            path:  docker-entrypoint.sh
      - name: config-js
        configMap:
          name: yourblog-cm
          defaultMode: 0777
          items:
          - key: ghost-config.js
            path: config.js

Launch the new application:

$ kubectl apply -f yourblog-deployment.yaml
deployment "ghost-deployment" created

Step 8 Access your new ghost deployment in your browser.

7.12. Delete A Workload

One benefit of using namespaces is the ability to delete all resources in it by just deleting the namespace. This simplifies deleting of applications with multiple resources.

Step 1 Delete the yourblog workload:

$ kubectl delete ns yourblog

Step 2 Delete the myblog workload:

$ kubectl delete ns myblog

Step 3 List deployed resources in all namespaces:

$ kubectl get all --all-namespaces

Step 4 What happened to the PersistenVolumes?:

$ kubectl get pv

PersistentVolumes are not part of the namespace and are not deleted. PersistentVolumeClaims are deleted when we delete the namespace. Since our ReclaimPolicy was to Retain, the PersistentVolumes are Released for manual cleanup process and will not be available for another claim. So our database content is safe even though the MySQL application is gone.

Step 5 Switch back to the kubernetes_admin context:

$ kubectl config use-context kubernetes-admin@kubernetes
Switched to context "kubernetes-admin@kubernetes".

Checkpoint

  • Create and use a secret
  • Declare containers with environment variables
  • Declare a persistent volume, persistent volume claim, and volume mount
  • Use configuration injection to customize applications
  • Link pods through services