Tutorial: Run a custom LAMP application on Kubernetes

Introduction

Linux, Apache, MySQL, and PHP, or LAMP, is one of the most popular software stacks powering content on the internet. This tutorial shows you how to set up your own LAMP stack on Kubernetes. The Kubernetes cluster we will build our LAMP stack on is the Quick Start for Kubernetes on AWS.

The LAMP stack takes advantage of several Kubernetes features to provide a robust starting point for a modern PHP application. Our stack supports load-balancing web traffic, horizontal scaling, data persistence, secret management, and rolling updates, among many other Kubernetes benefits.

Please keep in mind that the Kubernetes cluster created by the Quick Start is suitable for non-mission critical applications and for learning how Kubernetes works. It is a single-zone cluster. Finally, the example PHP application in this tutorial is a demo application not meant for production.

This tutorial is intended for readers who want to run their own LAMP application, learn what a standard software stack looks like on Kubernetes, or have a greater degree of control over configuration for a popular PHP application like WordPress. On the other hand, if you’re happy with pre-baked settings, it will be faster to install WordPress with Helm. Similar preconfigured installations exist for many popular PHP apps.

This tutorial shows how to install an example LAMP stack with a demo PHP application. The last step contains a checklist for modifying the instructions to deploy your own application.

During our tests, Kubernetes creation and resource provisioning took about 2-5 minutes on Amazon Web Services (AWS).

Architecture & decisions

This tutorial runs from a Kubernetes Quick Start cluster on AWS.

It can be deployed from four YAML files containing configurations for Kubernetes objects. The four files are secrets.yaml, mysql.yaml, php.yaml, and data-loader-job.yaml. These config files and our PHP application are available on Heptio’s GitHub account.


LAMP architecture diagram depicting underlying Kubernetes cluster, secrets accessible to all containers, 3 PHP + Apache containers running a PHP application and with a load balancer service on the front end, 1 MySQL container running a DB service and with a data volume attached, and 1 temporary data loader container running a data loader job

The example application runs the MySQL database server and PHP/Apache server separately, for scaling. It runs them as Kubernetes Deployments. A Deployment is a Kubernetes object which helps solve a number of devops problems, like scaling and rolling updates.

The application uses a standard MySQL image and a custom PHP image, which are from the public Docker Hub images for MySQL and PHP.

It uses a PersistentVolumeClaim for the database, which is a paid resource in addition to the resources for the initial cluster.

It uses a LoadBalancer as a front end for the three PHP/Apache containers deployed from the custom PHP image. The LoadBalancer is another paid resource.

One single file - secrets.yaml - stores the MySQL users, passwords, and initial database name. Our stack creates a Kubernetes Secret with these values, which are then set as environment variables on the containers. This eliminates the need to store sensitive information with our application.

Finally, database initialization is handled with a one-time Job that runs a (non-public) bash script from our PHP image that downloads MySQL’s Sakila sample database and keeps trying to import it to our MySQL server until it completes successfully. The Job runs only once.

Since our Secret is used in our other objects, we bring it up first. Since our Job will complete successfully only after the MySQL server is up, we bring it up last. However, if we create the objects out of the recommended order, Kubernetes will keep trying to create our objects until prerequisites have come up.

Prerequisites

This tutorial assumes the following:

  • You have a local Linux/Unix environment (like a MacBook)
  • You have successfully created the CloudFormation stacks from the AWS Kubernetes Quick Start (or followed the CLI walkthrough if you prefer)
  • You have copied the resulting kubeconfig file to your local machine, and can successfully run kubectl commands against the cluster (see Step 4 of the setup guide)

Try running kubectl get nodes. Your output should look similar to this:

NAME               STATUS         AGE
ip-10-0-0-0     Ready,master      1h
ip-172-172-172-172 Ready          1h
ip-192-192-192-192 Ready          1h

If it is, your cluster is ready to go! If the output looks substantially different, try running through the setup walkthrough again.

  • (Optional) If you want to publish your own Docker image, make sure you have Docker installed locally and the daemon running (docker ps should not have an error)
  • (Optional) If you want to publish your own Docker image, make sure you have a Docker Hub account, or an account at a private registry, where you can publish the image
  • (Optional) If you want to use your own domain name, be ready to add a CNAME entry to direct it to the AWS load balancer DNS name

Quick start

Create the Kubernetes objects from the 4 YAML files in our LAMP project. Download the files from the LAMP project here.

Update the user and both passwords in secrets.yaml using the echo -n varMyRootPass | base64 example commands to generate the base64 encoding, with your own user and passwords.

The Kubernetes stack will take about 2-5 minutes to provision fully.

kubectl create -f secrets.yaml
kubectl create -f mysql.yaml
kubectl create -f php.yaml
kubectl create -f data-loader-job.yaml

View the application URL with this kubectl get command:

kubectl get service web -o wide

You’ll get a lengthy DNS name in the EXTERNAL-IP column, which will look something like http://xxxxxxxxxxxxxxx.us-east-2.elb.amazonaws.com/.

If you visit that URL in your browser, you will see the phpinfo() page from our PHP image. If you’re not able to connect, wait a few more minutes for AWS to finish provisioning the load balancer and configure its networking.

If you visit http://xxxxxxxxxxxxxxx.us-east-2.elb.amazonaws.com/mysql-connect.php, you should see a list of table names from MySQL’s Sakila sample database and the message {"outcome":true}.

The LAMP example application is now deployed.

To remove this stack with a single command, including all stored data, please run:

kubectl delete all,secrets,jobs,persistentvolumeclaims --selector='heptio.com/example=lamp'

1. (Optional) Build local PHP and Apache Docker image with phpinfo() and MySQL test script

This tutorial calls for a PHP application. We’ve built a PHP image at heptio/example-php-dbconnect that you can download and deploy to your cluster. If you just want to see the Kubernetes cluster in action with Heptio’s demo app, please feel free to skip to the next step.

The rest of this section explains how we built on the default Docker Hub PHP image to create a custom PHP application image. Follow along with the example to help you build your own app.

This section requires:

  • Docker running on your local computer (install Docker). You should be able to run docker ps and get a (potentially empty) list of containers starting with CONTAINER ID, rather than an error
  • A Docker Hub account, so you can publish and later download your application image. (It’s possible to publish to other registries, too; not just Docker Hub)

Our local dev environment’s directory structure:

php/
  Dockerfile

  html/
    index.php
    mysql-connect.php

  script/
    mysql-sakila-data-loader.sh

You can create this directory structure now, or as you create the files. Or, take a look at this PHP project on GitHub.

First, let’s take a look at our Dockerfile. The Dockerfile contains the instructions for building our custom Docker image. (For more information, please visit Docker’s documentation.)

You can view the Dockerfile online in our project, but since it’s pretty short, we’ll also show the contents here:

# https://docs.docker.com/engine/reference/builder/

# base this image on the PHP image that comes with Apache https://hub.docker.com/_/php/
FROM php:7.0-apache

# install mysql-client and curl for our data init script
# install the PHP extension pdo_mysql for our connection script
# clean up
RUN apt-get update \
  && apt-get install -y mysql-client curl \
  && docker-php-ext-install pdo_mysql \
  && apt-get clean \
  && rm -rf /var/cache/apt/archives

# take the contents of the local html/ folder, and mount to /var/www/html/ inside the container
# this is the expected web root of the default website for this server, so put your index.php here
COPY html/ /var/www/html/

# take the contents of the local script/ folder, and mount to /tmp/ inside the container
# we can run one-time scripts, downloads, and other initial processes from /tmp/
COPY script/ /tmp/

Base image

The FROM line indicates that we’re basing our application on the default Docker Hub PHP image, and specifically, the version that includes our web server Apache along with it.

Installed utilities and extensions

We install a few utilities like mysql-client and curl so we can initialize the database. We also install pdo_mysql, an extension for PHP that allows us to make our database connection and queries.

Website files

The application adds two of our own PHP files. One is an index file with phpinfo(), useful for viewing our server’s PHP settings and demonstrating that the PHP server is running. The second script demonstrates how to connect to our MySQL server and show a list of tables from a database. We’ll use MySQL’s Sakila sample database, a standard test database that contains data about movies.

To view the scripts, please visit the project copy of index.php, which is the phpinfo() script, and mysql-connect.php, which is the database connection script.

When we build the image, we import those two scripts from our local dev environment’s html/ directory. Anything in that directory will be imported, so you can add more scripts and subdirectories to your local html/ directory, and it will all be imported when you build your Docker image.

In summary, all content stored in our local html/ directory gets mapped to the /var/www/html/ directory inside the container, which is where Apache and PHP are expecting the content for our default website.

Note: Keeping a phpinfo() page live on your site is insecure; remove after testing.

Scripts

We’ll also add a bash script that can connect to MySQL server and initialize our database. This is done by copying our local dev environment’s script/ directory to /tmp/ in the container. Any content in the local script/ directory will be built as part of our image. So, this is a good place to add more scripts and other one-time-use content to your image.

In our case, this is a single script that downloads MySQL’s Sakila sample database and attempts to keep importing it to our MySQL server until it’s successful. Kubernetes solves some traditional devops headaches here, because it doesn’t matter if the MySQL server or the PHP server is up before this Job runs. It will keep trying to run until our MySQL server is up and the script completes successfully. The MySQL server is covered in a later step. While the script itself is part of our PHP image, it gets run from a Kubernetes Job, not the Kubernetes PHP Deployment. This Job is discussed in a later step.

To view the script, please visit the project copy of mysql-sakila-data-loader.sh.

Checklist for local dev environment directories and files:

  • Dockerfile
  • Web content in html/ directory
  • Scripts in script/ directory

Build the PHP image

Once our directory structure and files exist locally, we can build our Docker image.

From the directory containing our Dockerfile, build the image:

docker build -t heptio/example-php-dbconnect:latest .

Let’s take a look at our PHP app locally. While not strictly necessary, it’s a good idea to check that the image does what we want it to do.

Run the container:

docker run \
  -d \
  -p 8080:80 \
  --name live-app \
  heptio/example-php-dbconnect;

This makes a container named live-app from our Docker image named php-dbconnect and it maps port 80 in the container (which Apache uses to serve the content) to port 8080 outside the container.

The phpinfo() index.php page should work now, because Apache is bundled with PHP. Visit http://localhost:8080 to see the home page, which shows our phpinfo().

You can also visit http://localhost:8080/mysql-connect.php to see the MySQL connection test script. Right now it will show a detailed error message starting with {"outcome":false,"message":"Unable to connect: because the database server doesn’t exist yet.

Great! If everything looks in order, let’s push this image to Docker Hub so it can be available to our Kubernetes cluster. You can choose a different registry instead, if you prefer.

Publish PHP image to Docker Hub

This section provides a brief sequence of commands you can run to build and publish your PHP image. Please read Docker’s walkthrough for publishing images to Docker Hub for more detail and explanation.

Note that images published to Docker Hub are public by default, so use a private registry instead if you want to keep the image private. Read our article, How to: Pull from private registries with Kubernetes, for more information.

Run these commands to build and publish the image. Replace heptio/example-php-dbconnect in the command below with the Docker Hub username, image name, and tag for your image.

docker build -t heptio/example-php-dbconnect:latest .
docker push heptio/example-php-dbconnect

Rebuild and republish the PHP image when you update the application

You should rebuild the PHP image if you change the contents of the html/ directory, or update any of your application’s other files:

docker build -t heptio/example-php-dbconnect:latest .

Stop and remove the old container:

docker stop live-app
docker rm live-app

Run the container again:

docker run \
  -d \
  -p 8080:80 \
  --name live-app \
  heptio/example-php-dbconnect;

Check that the image works locally.

Push the changes to Docker Hub after you make them locally. Use your own image ID.

docker build -t heptio/example-php-dbconnect:latest .
docker push heptio/example-php-dbconnect

Note: We’re using latest for everything here, but in a more robust devops environment you would use versioning for your PHP application image, which Kubernetes supports.

Next we’ll look at the Kubernetes objects required to deploy a LAMP application on our Quick Start Kubernetes cluster on AWS. We’ll start with a Kubernetes Secret that stores our MySQL credentials.

2. Use kubectl to create a Secret to store MySQL credentials

A Kubernetes Secret is a more secure way to store sensitive details about our application, like our MySQL users and passwords.

A Secret can be used multiple times by Kubernetes to pass details to containers running applications, or other objects that require the information.

In our case, Kubernetes will pass our secrets to the MySQL server in the Deployment section of our mysql.yaml file to set up appropriate users on the server, to the PHP server in the Deployment section of our php.yaml file so our PHP script mysql-connect.php can connect to the database, and to a Job in our data-loader-job.yaml file so our one-time-use script mysql-sakila-data-loader.sh can connect to the database and initialize it with actual data.

If you would like to create the Secret from the secrets.yaml file and move on with the tutorial, run this command:

kubectl create -f secrets.yaml

You should see the output:

secret "mysql-credentials" created

That’s it. The secrets are now available to the rest of the Kubernetes cluster, and you can continue to the next step.

The rest of this section explains how the secrets work, and how to create your own.

Secrets get passed to containers as environment variables, so they can be referenced as environment variables from within containers.

Here’s an example that shows how the secret for our database user, varMyDBUser, is created and then used.

An excerpt from our secrets.yaml file:

data:
  user: dmFyTXlEQlVzZXI=

This is varMyDBUser, encoded in base64. You can get the same result with:

echo -n varMyDBUser | base64

Keep in mind that Kubernetes doesn’t actively read the secret from this file every time you need it. You need to run kubectl create -f secrets.yaml for the Kubernetes Secret object to be created. If you change something in the file, you’ll have to run kubectl replace -f secrets.yaml, or make a new Secret and update the references to it. (Note that if you update a Secret for something that has already been been used at the application level, Kubernetes does not know how to update a Secret below the level of other Kubernetes objects, so you would have to update it within your application separately.)

The name of the Secret, in our case, is mysql-credentials, which is also defined in the secrets.yaml file. So, the Secret object mysql-credentials has the specific secret user: dmFyTXlEQlVzZXI=, which is the encoded version of user: varMyDBUser.

One Secret object can have multiple secrets in it. Ours has three: the root password for MySQL, another user for MySQL, and the password for that second MySQL user.

This secret is referenced in a few of our other Kubernetes objects. Take, for example, our PHP Deployment in php.yaml. It has this section:

env:
  - name: MYSQL_USER
    valueFrom:
      secretKeyRef:
        name: mysql-credentials
        key: user

This creates an environment variable for the PHP server that’s getting created in this deployment. The environment variable can be referenced as MYSQL_USER from within the PHP container. It assigns a value to MYSQL_USER from the Secret named mysql-credentials, and chooses the value of user, which in our case is ultimately varMyDBUser.

Now let’s look at how this secret makes it all the way to our application. Inside the PHP container, we can reference the environment variable MYSQL_USER, and it will work like a normal environment variable, meaning that MYSQL_USER is equivalent to varMyDBUser. For example, in our mysql-connect.php script, we use getenv('MYSQL_USER') as the MySQL username in our database connection, and it passes the value varMyDBUser when it connects to MySQL.

For more background, please read the Kubernetes documentation on Secrets.

Create a Kubernetes Secret

For the purposes of this tutorial, we’ve included a working secrets.yaml file in our project, which you can view at that link.

It’s not secure to publish your YAML Secret in a file, but we’ve done so for the sake of the tutorial.

There are actually two ways to create a Secret in Kubernetes. The first way is to create a YAML file with base64-encoded values, and run the kubectl create -f secrets.yaml command, as shown above.

The other way is to use kubectl to generate the Secret directly on your cluster. Read the Kubernetes Secret documentation for more explanation.

Verify that mysql-credentials is in our list of secrets:

kubectl get secrets

Output:

NAME                     TYPE                                  DATA      AGE
mysql-credentials        Opaque                                3         43s

The order in which you create the Kubernetes objects for this application doesn’t actually matter, but the other objects use this Secret, so they will not complete 100% until the Secret is created. Now that our credentials are ready, let’s create our MySQL server.

3. Use kubectl to deploy MySQL

Let’s use kubectl to deploy MySQL server on our Kubernetes cluster.

The only command we need to deploy these Kubernetes objects from our mysql.yaml file is:

kubectl create -f mysql.yaml

You’ll see the following output for a successful deployment:

persistentvolumeclaim "database" created
deployment "mysql" created
service "mysql" created

This creates the MySQL server with persistent data storage, and accessible from within the cluster at mysql.default.svc.cluster.local.

The functional details for our MySQL server are in the Kubernetes YAML configuration file. Let’s unpack the configuration file, so we can understand what Kubernetes is doing to create our MySQL server. The complete file is on GitHub (view mysql.yaml here) and includes section-by-section comments.

The YAML deploys three distinct Kubernetes objects, separated by --- in the file:

  • A PersistentVolumeClaim, which reserves a separate disk for storing data in a more permanent way. This is so the contents of the database remain stored on the same disk, even as the pod running our MySQL server is more ephemeral. Our particular setup requests sane defaults for the storage volume from Amazon Web Services, which gives us an EBS volume. This is a paid resource. Kubernetes can provision this cloud storage resource when configured correctly with the cloud provider. The Heptio Quick Start for Kubernetes on AWS does this for you automatically. Note: If you are not using AWS or you don’t have cloudprovider=aws configured, you must create a PersistentVolumeClaim and PersistentVolume separately. Your cloud provider must support network attached volumes. You will have to name it database and make sure your cluster has permission to attach it.
  • A Deployment, which is the recommended Kubernetes object for managing containers throughout the software release cycle. Our Deployment calls for one pod with one MySQL container, imaged from the default MySQL image from Docker Hub. This is how we deploy MySQL server on our cluster. The Deployment also calls for a volume, that uses the disk from the PersistentVolumeClaim.
  • A Service, which allows for consistent network access to our MySQL server, even as Kubernetes handles pod logistics behind the scenes. Our particular Service makes MySQL accessible to the rest of the cluster using the host mysql.default.svc.cluster.local and port 3306 (default MySQL port). We can use this host from other parts of our app running in the same Kubernetes cluster, like the PHP app we will deploy next.

We recommend reading through the comments in mysql.yaml for a detailed look at the Kubernetes configuration.

At a high level, our configuration takes advantage of Kubernetes in a few key ways.

First, our MySQL installation uses default settings — it uses Docker Hub’s official MySQL image — and is independent of the data we’ll store on it (addressed in a later step). This means it’s a suitable configuration for many MySQL-backed applications.

Second, it mounts a separate storage volume to /var/lib/mysql within the container, so data stored on the volume stays independent of the Kubernetes pod and container.

Third, it uses a Kubernetes Deployment to keep our number of MySQL pods optimized - in this case, we’re deploying just one (replicas: 1). We’ll cover updates in another article, but putting our pod in a Deployment sets us up for success in terms of versioning. Since our MySQL server is backed by an attached volume, and MySQL sharding/replication is beyond the scope of this tutorial, the only supported value for MySQL replicas is replicas: 1.

Fourth, sensitive MySQL data is read from a different Kubernetes object, the Secret we configured earlier, and passed to the pod via environment variables. Our MySQL image expects all of these settings (root password, another user and password, and a new database); see the image documentation for details.

How do I use this MySQL server?

Any application running on the Kubernetes cluster can access the MySQL server over mysql.default.svc.cluster.local on port 3306.

For MySQL credentials, we recommend using the environment variables MYSQL_USER and MYSQL_PASSWORD, as demonstrated with our PHP application in the next step. The environment variables must be imported from the Kubernetes Secret and set in your application container(s). For testing, you can use the user root and password varMyRootPass, or user varMyDBUser and password varMyDBPass directly.

Please generate your own users and passwords and store them in the Secret; using these defaults is not secure.

You can initialize your data from a script, as is shown in a later step.

Or, connect to the server from a shell to check that the MySQL server is up and access it from the command line.

Kubernetes MySQL admin tips

After running kubectl create -f mysql.yaml to deploy MySQL, give your cluster 2-5 minutes to deploy volumes, download the MySQL image, and finish setting things up.

Check to see if the MySQL pod is up with:

kubectl describe pods mysql

Optional - view the pod logs, substituting your own pod name, which can be obtained using the describe command above.

kubectl logs mysql-1259503160-l6kc5

Now, let’s check to see if the MySQL server is up, by using the command line.

Create an ephemeral pod, which stays up for a few minutes for testing, and then removes itself. This one will be built with the MySQL client image, and let us connect with bash so we can run commands.

kubectl run -i -t --rm ephemeral --image=mysql -- /bin/sh -l

Your temporary pod should start up. Press Enter to get a command prompt.

Now, connect to MySQL - the MySQL server that’s running in our other pod.

mysql -h mysql.default.svc.cluster.local -P 3306 -u root -pvarMyRootPass

Note our settings: We’re connecting on the host mysql.default.svc.cluster.local, which is the service we set up in our manifest file. We’re using port 3306 and the username and password from our Secret. (We’re using the username and password directly, since we didn’t pass this ephemeral pod any environment variables from our Secret.)

Now you should see a MySQL prompt. Try showing our databases:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sakila             |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

This indicates that your database server is up and running, and accessible from other pods in the cluster.

Run other MySQL commands as needed. However, we recommend initializing the database programatically, as shown in a later step.

The ephemeral pod will remove itself after few minutes of inactivity with the following output:

mysql> Session ended, resume using 'kubectl attach ephemeral-546807040-rvz86 -c ephemeral -i -t' command when the pod is running
deployment "ephemeral" deleted

You can always run the command to create the MySQL client pod again, if you would like to resume testing.

If you need to destroy and then recreate the deployment, here’s the whole sequence of commands. Note that this tears down the entire setup, including deleting our storage volume with all our data. This would not be the recommended way to update to a new MySQL image, since that can be handled with the Kubernetes Deployment object. Run these commands to tear down, recreate, and then connect to MySQL on Kubernetes:

kubectl delete deployment mysql
kubectl delete service mysql
kubectl delete persistentvolumeclaim database
kubectl create -f mysql.yaml
kubectl describe pods mysql
kubectl logs mysql-1259503160-l6kc5
kubectl run -i -t --rm ephemeral --image=mysql -- /bin/sh -l

To tear down everything even faster, you can run this destructive command:

kubectl delete -f mysql.yaml

Now that our database is in place, let’s deploy our custom PHP application. We’ll use a Kubernetes Deployment object for PHP, too.

4. Use kubectl to deploy PHP

Let’s use kubectl to deploy PHP and Apache on our Kubernetes cluster.

The only command we need to deploy these Kubernetes objects from our php.yaml file is:

kubectl create -f php.yaml

You’ll see the following output for a successful deployment:

deployment "php-dbconnect" created
service "web" created

This creates the PHP and Apache server with a load balancer named web. It may take about 2-5 minutes to provision the load balancer.

The functional details for our PHP server are in the Kubernetes YAML configuration file. Let’s unpack the configuration file, so we can understand what Kubernetes is doing to create our PHP server. The complete file is on GitHub (view php.yaml here) and includes section-by-section comments.

The YAML deploys two distinct Kubernetes objects, separated by --- in the file:

  • A Deployment, like we used for MySQL. Our Deployment calls for three pods with one PHP container each, imaged from the our custom PHP image on Docker Hub. This is how we deploy our PHP application on our cluster.
  • A Service, which allows for consistent network access to our PHP/Apache server, even as Kubernetes handles pod logistics behind the scenes. Our particular Service is a Kubernetes LoadBalancer that uses AWS Elastic Load Balancing. This is a paid resource. It makes our application available on the internet on port 80 (standard web port). We can use the load balancer’s DNS name to access our application over the internet.

We recommend reading through the comments in php.yaml for a detailed look at the Kubernetes configuration.

Our PHP application takes advantage of Kubernetes for load balancing, versioning, and security.

It provisions AWS Elastic Load Balancing as a front end for the application. This works because we have cloudprovider=aws enabled on the cluster. With automatic load balancer provisioning, it’s nearly effortless to have a front end that load balances traffic to our application automatically.

Our Kubernetes Deployment keeps the number of PHP pods optimized to three. This lets Kubernetes load balance traffic to the application. A Deployment also makes rolling updates relatively painless when you publish a new version of the application. Finally, we can scale out even further by updating a single line (replicas: 3) in our configuration file. For example, we could run this command to update to five replicas: kubectl scale --replicas=5 deployment/php-dbconnect.

There’s also no need to store our MySQL credentials anywhere in our application code. Kubernetes reads values from the Secret we configured earlier and passes them to the pod via environment variables. We’ll go over the specific environment variables below.

How do I use this PHP/Apache server?

Our PHP image includes Apache, so the web server is built in. It serves content for the default website from /var/www/html/ in the container. If you go back to the first step in this tutorial, you can read how to add your own application content to this directory.

If you have your own PHP image to use, replace this line in the php.yaml file:

- image: heptio/example-php-dbconnect

Replace this with a link to your own image at your own registry. (If a full URL is not specified, Kubernetes assumes the image is on Docker Hub.)

Kubernetes does not make the container accessible to the public unless we tell it to, which is where the Service part of our deployment comes in.

Once the Service has provisioned our load balancer, you can get its AWS DNS name with the following command:

kubectl get service web -o wide

You’ll get a lengthy DNS name in the EXTERNAL-IP column, which will look something like http://xxxxxxxxxxxxxxx.us-east-2.elb.amazonaws.com/.

If you visit that URL in your browser, you will see the phpinfo() page from our PHP image. If you’re not able to connect, wait a few more minutes for AWS to finish provisioning the load balancer and configure its networking.

If you visit http://xxxxxxxxxxxxxxx.us-east-2.elb.amazonaws.com/mysql-connect.php, you should see the message {"outcome":true}. This means our PHP app has connected to our database successfully. However, it is not showing any table names yet, because we haven’t initialized our database with any data. We’ll do this in the Job step below.

You can use this load balancer DNS name with a CNAME DNS entry, to use your own domain for this application.

To quote AWS’s advice about DNS and load balancers,

Because the set of IP addresses associated with a LoadBalancer can change over time, you should never create an “A” record with any specific IP address. If you want to use a friendly DNS name for your load balancer instead of the name generated by the Elastic Load Balancing service, you should create a CNAME record for the LoadBalancer DNS name, or use Amazon Route 53 to create a hosted zone. For more information, see Using Domain Names With Elastic Load Balancing.

The PHP server has a few environment variables set, related to the MySQL server. MYSQL_USER, MYSQL_PASSWORD, and MYSQL_HOST are all used by our database connection script in a later step. The environment variables are imported from the Kubernetes Secret and set in your application container(s) by Kubernetes when we create the Deployment. For testing access to MySQL, you can also use the user root and password varMyRootPass, or user varMyDBUser and password varMyDBPass directly.

Please generate your own users and passwords and store them in the Secret; using these defaults is not secure.

You can connect to the server from a shell to check that the web server is up and curl it from the command line or execute other bash commands.

Kubernetes PHP admin tips

After running kubectl create -f php.yaml to deploy PHP, give your cluster 2-5 minutes to download the custom PHP image, and get the load balancer set up.

Check to see if the pods are up:

kubectl get pods -l app=php-dbconnect

Describe a specific pod for details:

kubectl describe php-dbconnect-3962733399-sn3th

Optional - view one of the pod’s logs, substituting your own pod name, which can be obtained using the get command above.

kubectl logs php-dbconnect-3962733399-sn3th

Get a bash shell on one of the pods:

kubectl exec -it php-dbconnect-3962733399-sn3th -- /bin/bash

Use curl to see if the web server is responsive from localhost:

curl localhost/mysql-connect.php

At this stage, you should get this response:

{"outcome":true}

Updating the application

For development, if you want to make changes to your PHP application live on the server, it could make sense to scale this Deployment to replicas: 1 instead of replicas: 3. You can do this by running kubectl scale --replicas=1 deployment/php-dbconnect.That way, you’ll know you’re hitting the pod where you’re making the changes, because it’s the only pod. When you’ve got what you want, create a new application image for production use.

Going into a pod on the command line is not a recommended way of updating your application. For that, you should build a new version of your image, publish it a registry, and update your Deployment to roll out the new image. We’ll discuss the recommended way to release a new version of your application in another article.

For our demo application, our imagePullPolicy: Always makes sure we always check for the latest version of the PHP image when creating the Deployment, even if the version tag is the same. In a more robust devops environment, you’d want to take advantage of Kubernetes rolling updates and image versions instead.

If you need to destroy and then recreate the deployment, here’s the whole sequence of commands. Note that this tears down the entire setup, including deleting our load balancer with the static DNS name. This would not be the recommended way to update to a new PHP image, since that can be handled with the Kubernetes Deployment object.

Run these commands to tear down, recreate, and then connect to a bash shell on one of your PHP pods. You will have to use your own pod name instead of php-dbconnect-3962733399-sn3th. The three pod names will be listed in the output from the kubectl get pods -l app=php-dbconnect command:

kubectl delete service web
kubectl delete deployment php-dbconnect
kubectl create -f php.yaml
kubectl get pods -l app=php-dbconnect
kubectl describe php-dbconnect-3962733399-sn3th
kubectl logs php-dbconnect-3962733399-sn3th
kubectl exec -it php-dbconnect-3962733399-sn3th -- /bin/bash

To tear down everything even faster, you can run this destructive command:

kubectl delete -f php.yaml

Next, let’s add some data to our database, using a one-time Kubernetes Job.

5. Use kubectl to initialize sample MySQL data

Many LAMP applications need up-front database configuration. Whether you need to initialize your database schema, install a pre-existing data set, or both, you need a way to run some MySQL commands to import your data.

Use kubectl to execute our data loader Job:

kubectl create -f data-loader-job.yaml

Successful output:

job "mysql-data-loader-with-timeout" created

This Job runs a script, mysql-sakila-data-loader.sh, that’s in a new container built from the same PHP image. When it’s done, our database has been initialized.

View your URL again, which should be something like http://xxxxxxxxxxxxxxx.us-east-2.elb.amazonaws.com/mysql-connect.php, to see the effect on our application. You should see a list of table names beginning with actor. Our LAMP application is now complete.

Our script mysql-sakila-data-loader.sh is also part of our PHP image, and is located in the image’s /tmp directory. While it’s not necessary to have our more permanent website files and our single-use script in the same Docker image, it’s convenient to package them together.

This brief bash script uses curl to download the compressed database files for the Sakila example database from MySQL. This is a standard test database with information about movies.

View the data loader script here. It is thoroughly commented.

It keeps trying to import the data to MySQL until a check on one of the tables completes successfully. The data import is done with mysql-client.

Because the mysql-sakila-data-loader.sh script has set -o errexit, if any commands in the script fail, the whole script returns nonzero.

This is relevant to the Kubernetes Job that is responsible for executing the script. You can view the Job in the data-loader-job.yaml file in our GitHub project. The file is commented section by section.

The data loader Job has restartPolicy: OnFailure, which means that the Job will keep trying to run the script until the script returns successfully - that is, with zero. Once it does complete successfully, the Job will not try to run again.

This means that our stack starts trying to import the data once, keeps trying until it completes successfully, and then doesn’t ever try to import the data again. One of our PHP containers is up while the Job runs, but it doesn’t stay running indefinitely like the containers in our PHP Deployment.

How do I run my own Job?

Write your own script to import data or perform another one-time task on your stack.

Run the script from an image with appropriate utilities and permissions.

In the data-loader-job.yaml file, update the image: heptio/example-php-dbconnect to the image your script is on, and that has the necessary utilities. Update the command: ["/tmp/mysql-sakila-data-loader.sh"] to run your script. If the command is short enough, you can run it from the YAML file instead of putting it in a script (see the Kubernetes documentation about Jobs).

Kubernetes Job admin tips

If you need to reapply the data, first make sure the database is empty.

Delete the Job, and then recreate it.

kubectl delete -f data-loader-job.yaml
kubectl create -f data-loader-job.yaml

Our LAMP application is complete! The next two optional steps show you how to troubleshoot the PHP-to-MySQL connection a little bit, and how to customize this LAMP application further.

5. (Optional) Test the PHP connection to MySQL

If you visit http://xxxxxxxxxxxxxxx.us-east-2.elb.amazonaws.com/mysql-connect.php, you should see a simple list of database names from the Sakila test database, starting with actor.

If the database server is down, or the connection details are not correct, the PHP connection script at http://xxxxxxxxxxxxxxx.us-east-2.elb.amazonaws.com/mysql-connect.php shows:

{"outcome":false,"message":"Unable to connect: PDOException: SQLSTATE[HY000] [2002] Connection timed out in \/var\/www\/html\/mysql-connect.php:7\nStack trace:\n#0 \/var\/www\/html\/mysql-connect.php(7): PDO->__construct('mysql:host=mysq...', 'varMyDBUser', 'varMyDBPass', Array)\n#1 {main}"}

Note that the attempted username and password are displayed in this error message, so you would not want to be this explicit in a production environment! It does demonstrate that the secrets are being used by the script, via our environment variables.

If the database connection works, but there’s no data:

{"outcome":true}

If the database connection works, and there is data (this is the desired state):

actor
actor_info
address
category
city
country
customer
customer_list
film
film_actor
film_category
film_list
film_text
inventory
language
nicer_but_slower_film_list
payment
rental
sales_by_film_category
sales_by_store
staff
staff_list
store
{"outcome":true}

If the connection doesn’t work at all, see if you can connect from the PHP container to the MySQL server.

Get a shell to one of the PHP pods (remember to kubectl get pods -l app=php-dbconnect to see the specific pod names).

Get a bash shell:

kubectl exec -it php-dbconnect-3962733399-sn3th -- /bin/bash

Can you connect to the database server?

mysql -h mysql.default.svc.cluster.local -P 3306 -u root -pvarMyRootPass

The host mysql.default.svc.cluster.local is the service we set up in our manifest file. We’re using port 3306 and the root password varMyRootPass, for troubleshooting.

Now you should see a MySQL prompt. Try showing our databases:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sakila             |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

Does the sakila database appear in the list? If not, you may need to run the data-loader-job.yaml again, as explained in an earlier step.

Finally, let’s take another look at the ways we can use this example LAMP application as a springboard for your own app.

6. (Optional) Customize your LAMP application

This section includes a brief checklist for deploying your own LAMP application based on the stack in this tutorial.

  • Place your own website files in the html/ directory of your Docker development directory. They will be copied to /var/www/html/ when you build your Docker image
  • Download and import your own SQL files in the mysql-sakila-data-loader.sh script
  • Place your mysql-sakila-data-loader.sh script and any other scripts or temporary data needed by your application in the script/ directory of your Docker development directory. They will be copied to /tmp/ when you build your own Docker image
  • Update the Dockerfile with any other software you need to apt-get or any other PHP extensions needed by your application or scripts
  • Build your Docker image
  • Publish your Docker image to Docker Hub (public) or a private registry
  • Update the php.yaml and data-loader-job.yaml files to refer to your image instead of image: heptio/example-php-dbconnect
  • Update the data-loader-job.yaml file to refer to your initial script(s) instead of command: ["/tmp/mysql-sakila-data-loader.sh"]
  • Update the secrets.yaml file to use your own secure users, passwords, and other sensitive information
  • If you change the keys for any of the secrets, update all of the other YAML files to use the new keys
  • Double-check that your secrets, environment variables in the YAML files, and environment variables in your application and scripts all still correspond
  • If you do not use the Service to make MySQL available on mysql.default.svc.cluster.local, update environment variables in php.yaml and data-loader-job.yaml and in your application to correspond to the new MySQL host
  • If you are not using AWS or do not have cloudprovider=aws enabled, you will have to provision a PersistentVolumeClaim named database so your cluster has access, and you will have to provision a LoadBalancer and configure it like the one in the php.yaml file
  • Run kubectl create -f varMyfile.yaml for all the files
  • Run kubectl get service web -o wide and configure DNS so your domain name points to your application

For explanation of these checklist items, please re-read the appropriate section of the tutorial. Details for customizing your application are presented alongside the example app installation.

Conclusion

You now have a load-balanced scalable LAMP application on Kubernetes. With this stack, we get persistent data stored on an attached volume, secret management, and support for rolling updates, among many other Kubernetes benefits.

To remove this stack, including all stored data, please run this destructive command:

kubectl delete all,secrets,jobs,persistentvolumeclaims --selector='heptio.com/example=lamp'

Let us know if you deploy a LAMP application using this tutorial! We’re @heptio on Twitter.