Kubernetes/Kubernetes Workshop/Refactoring code using Kubernetes features

From Wikitech

Overview

At the end of this module, you should be able to:

  • Refactor applications using K8s features.

Step 1 - Refactoring Variables From the Book App

In Module 3, you used K8s to run a PHP/MySQL application. In this module, you will improve that application by adding K8s features to its configuration. The application has four configuration parameters that are hard-coded. Pulling configurations from an external source, such as a repository, is a best practice. The following items may cause you concern:

  • What database server should I use?
  • Name of the database.
  • The login name.
  • The secret phrase.

ConfigMaps and Secrets are used by K8s to hold the above values. You deploy ConfigMaps and Secrets as YAML files.

Refactor variables (kind: ConfigMap) and secrets (kind: Secrets) from the book app: You will start refactoring from the database server. You will also have to declare a configuration mapping.

bookdbappconfigmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
 name: bookdbappconfigmap
data:
 database: bookdbserver

In the file above, you used the environment variable database to connect to the service that will host the bookdb database called bookdbserver.

Note: You have to change the deployment YAML file to enforce the ConfigMap (see the file a bit later after we added the secret as well).

index.php

<?php
$conn = new mysqli("bookdbserver", "bookdb", "bookdbpass", "books");
// Check connection
if ($conn->connect_error) {
 die("Connection failed: " . $conn->connect_error);
}
$sql = "SELECT count(*) AS total FROM books";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
 $count = $result->fetch_assoc();
 echo $count['total'];
 $offset = rand(1,$count['total']-1);
 $sql2 = "SELECT title, author from books LIMIT ".$offset.",1";
 $result2 = $conn->query($sql2);
 if ($result2->num_rows > 0) {
   echo "<HTML><BODY><TABLE>";
   // output data of each row
   while($row = $result2->fetch_assoc()) {
     echo "<TR><TD>".$row['title']."</TD><TD>".$row['author']."</TD></TR>";
   }
   echo "</TABLE></BODY></HTML>";
 }
} else {
 echo "0 results";
}
$conn->close();
?>
  • You also affect changes in the bookdbappsecrets.yaml file.

bookdbappsecrets.yaml

apiVersion: v1
kind: Secret
metadata:
 name: bookdbappcredentials
type: Opaque
data:
  db-password: Ym9va2RicGFzcw==

Note: The value has to be base64 encoded. Also, the key db-password (unlike the ConfigMap scenario) cannot be used directly, but must be mapped in the deployment file first.

Step 2 - Refactoring the Deployment Script

In the deployment file, you have to notify the application about mapping configmaps and secrets. This will be done in the spec section, as you will see in the code snippet below:

bookdbappdeployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
 name: <name>
 labels:
   app: <name>
spec:
 replicas: 1
 strategy:
   type: RollingUpdate
 selector:
   matchLabels:
     app: <name>
 template:
   metadata:
     labels:
       app: <name>
   spec:
     containers:
       - name: <name>
         image: <docker-username>/<image-name>:latest
         imagePullPolicy: Always
         envFrom:
           - configMapRef:
               name: bookdbappconfigmap
         env:
           - name: BOOKDBPASS
             valueFrom:
               secretKeyRef:
                 name: bookdbappcredentials
                 key: db-password

bookdbdeployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
 name: <name>
 labels:
   app:<name>
spec:
 replicas: 1
 strategy:
   type: RollingUpdate
 selector:
   matchLabels:
     app: <name>
 template:
   metadata:
     labels:
       app: <name>
   spec:
     containers:
       - name: <name>
         image: <docker-username>/<docker-image>:<tag>
         imagePullPolicy: Always
         resources:
           limits:
             cpu: 500m
           requests:
             cpu: 200m

Note: In the index.php file below, you will reference the BOOKDBPASS mapped in the deployment file above.

index.php

<?php
// params are server, user, pass, database
$conn = new mysqli($_ENV["database"], "bookdb", $_ENV["BOOKDBPASS"], "books");
// Check connection
if ($conn->connect_error) {
 die("Connection failed: " . $conn->connect_error);
}
$sql = "SELECT count(*) AS total FROM books";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
 $count = $result->fetch_assoc();
 echo $count['total'];
 $offset = rand(1,$count['total']-1);
 $sql2 = "SELECT title, author from books LIMIT ".$offset.",1";
 $result2 = $conn->query($sql2);
 if ($result2->num_rows > 0) {
   echo "<HTML><BODY><TABLE>";
   // output data of each row
   while($row = $result2->fetch_assoc()) {
     echo "<TR><TD>".$row['title']."</TD><TD>".$row['author']."</TD></TR>";
   }
   echo "</TABLE></BODY></HTML>";
 }
} else {
 echo "0 results";
}
$conn->close();
?>

Note: You can also change a few lines of code, to verify that you are running the newly deployed version. You can change the line

echo "<HTML><BODY><TABLE>";

to

echo "<HTML><HEAD><TITLE>configmaps</TITLE></HEAD><BODY><TABLE>";

This code snippet includes a title in the resulting HTML page.

Step 3 - Running the Refactored Code

Now, you can test your refactoring. First, you will rebuild the application with the new index.php file and upload the container to Docker Hub, and lastly, you will create a new deployment.

Note:

  • You will need to include the Dockerfile from the demo-section in Module 4.
  • Build and push your image to DockerHub.
  • Ensure all deployments, pods, hpa, etc are deleted.

Create your deployment and services. You can get the deployment and services scripts not given in this module from Module 3. Give app and name similar values:

$ minikube start
$ kubectl create -f bookdbdeployment.yaml
$ kubectl create -f bookdbservice.yaml
$ kubectl create -f bookdbappconfigmap.yaml
$ kubectl create -f bookdbappsecrets.yaml
$ kubectl create -f bookdbappdeployment.yaml
$ kubectl create -f bookdbappservice.yaml
$ minikube service bookdbapp --url
  • Access the URL via browser or curl
$ curl <URL>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <!--
    Modified from the Debian original for Ubuntu
    Last updated: 2022-03-22
    See: https://launchpad.net/bugs/1966004
  -->
………………………………………

Step 4 - Debugging

Troubleshooting Guide

If you encounter an error, you can take the following steps:

  • First, get a hold on all available services and deployments:
$ kubectl get configmaps
$ kubectl get secrets
$ kubectl get pods
$ kubectl logs
$ kubectl get services
$ kubectl get deployments
$ minikube service bookdbapp --url 
172.17.0.2:30576
curl http://172.17.0.2:30576           #shows apache default page
curl http://172.17.0.2:30576/index.php #gives an error message
Connection failed: No such file or directory
  • Since you got an error from the scenario above, the first point of call is the logs:
$ kubectl get pods
bookdbapp-69b79b6f7-bmptf  #assumed container id
$ kubectl logs bookdbapp-69b79b6f7-bmptf
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.18.0.7. Set the 'ServerName' directive globally to suppress this message
  • Next, review the apache error log. Unfortunately, this setup does not redirect that log to stdout. So, you have to log into the pod and look at the error log file:
$ kubectl exec -it bookdbapp-69b79b6f7-bmptf /bin/bash
root@bookdbapp-69b79b6f7-bmptf:~# tail /var/log/apache2/error.log
[Fri Sep 18 05:37:05.795586 2020] [php7:warn] [pid 13] [client 172.18.0.1:53581] PHP Warning:  mysqli::__construct(): (HY000/2002): No such file or directory in /var/www/html/index.php on line 3
  • You see from the error message that the error stems from line 3.
root@bookdbapp-69b79b6f7-bmptf:~# head -3 /var/www/html/index.php
<?php
// params are server, user, pass, database
$conn = new mysqli($_ENV["database"], "bookdb", $_ENV["BOOKDBPASS"], "books");
  • The issue is with the MySQL connection string. Change the connect string to the previously hardcoded values:
<?php
// params are server, user, pass, database
//$conn = new mysqli($_ENV["database"], "bookdb", $_ENV["BOOKDBPASS"], "books");
$conn = new mysqli("bookdbserver", "bookdb", "bookdbpass", "books");
  • To verify if this works, make a curl request to the application's IP address:
$ curl http://<ip_address>/index.php
  • It would be nice to have a phpinfo page to check if the environment contains the referenced values.
  • Exit the container and check the host's settings for Minikube. curl is not very good but works with html2text. links is also a good alternative.
$ links http://172.17.0.2:30576/phpinfo.php or
$ curl http://172.17.0.2:30576/phpinfo.php | html2text | grep database

You notice that the setting exists but can the PHP script access the setting? To debug this, you can change the value of the connection string to what it was before and add an echo command to see what happens. Take the steps below:

  • Edit the index.php file:

index.php

<?php
// params are server, user, pass, database
echo $_ENV["database"];
$conn = new mysqli($_ENV["database"], "bookdb", $_ENV["BOOKDBPASS"], "books");
<?php
// params are server, user, pass, database
echo "before";
echo $_ENV["database"];
echo "after";
$conn = new mysqli($_ENV["database"], "bookdb", $_ENV["BOOKDBPASS"], "books");
// Check connection
….
  • Run the file:
$ php <file_name>.php
beforeafterConnection failed: No such file or directory
  • From the error message, it seems that the _ENV file is empty. A quick Google search of “$_ENV not accessible php 7.4” finds a reference to a setting in the php.ini file in a comment in the first link at:
Add E to the variables_order setting
root@bookdbapp-69b79b6f7-bmptf:vi /etc/php/7.4/apache2/php.ini
; This directive determines which super global arrays are registered when PHP
; starts up. G,P,C,E & S are abbreviations for the following respective super
; globals: GET, POST, COOKIE, ENV and SERVER. There is a performance penalty
; paid for the registration of these arrays and because ENV is not as commonly
; used as the others, ENV is not recommended on productions servers. You
; can still get access to the environment variables through getenv() should you
; need to.
; Default Value: "EGPCS"
; Development Value: "GPCS"
; Production Value: "GPCS";
; http://php.net/variables-order
variables_order = "EGPCS"

Note: Do not stop Apache, as this will kill the container. Instead run apachectl -k graceful.

  • Run:
$ curl http://<ip_address>/index.php

Your application now runs successfully.

Note: You can use getenv() instead of $_ENV[] in the php code as it will always work. Another point to note is how the application scaled through local testing.

Next Module

Module 6: Building a production-ready cluster

Previous Module