Deploying a ‘Hello World’ application to the Cloud Platform
Overview
The aim of this guide is to walkthrough the process of deploying an application into the Cloud Platform.
Before working through this tutorial, we recommend reading this Cloud Platform tech overview page, which provides a high-level view of the software development lifecycle on the cloud platform.
This guide uses a pre-configured “Hello World” application as an example of how to deploy your own. This application merely returns a static HTML response, and has no dependencies. Later examples will use more representative applications.
We will walk through these steps:
- Create a namespace (aka an “environment”) on the Cloud Platform
- Deploy an instance of a pre-configured “Hello, World” application into your namespace
Then we will demonstrate a typical software development cycle of making and deploying changes to the application:
- Create and authenticate to an ECR
- Make changes to the source code of the application
- Build a new docker image from the demo application
- Tag the image and push it to your ECR
- Update your kubernetes files to deploy the updated application
The process of building an image and pushing it to an ECR will normally be carried out an application’s build pipeline. However, for this initial walkthrough, we will go through these steps manually. Later we will go through an example of setting up a CircleCI job to do this automatically. The steps are similar if you’re using other CI/CD tools such as TravisCI.
Prerequisites
This guide assumes the following:
- Docker is installed and configured.
- Kubectl is installed (
brew install kubectl
on a Mac with Homebrew installed, or follow these instructions) - You have authenticated to the live cluster
- AWS CLI is installed (
brew install awscli
on a Mac with Homebrew installed) - Cloud Platform CLI is installed
Step 1 - Create your namespace
Follow this guide to create a namespace on the Cloud Platform (you don’t need to do any of the “Next steps” from that guide).
Step 2 - Deploy the “Hello, World” application
- Clone the demo application
git clone https://github.com/ministryofjustice/cloud-platform-helloworld-ruby-app
cd cloud-platform-helloworld-ruby-app
The application has a kubectl_deploy
folder containing YAML files which
define an instance of the application for kubernetes.
Before we can deploy the application, we need to make some changes to run it in your new namespace.
Edit your local copy of the kubectl_deploy/ingress.yaml
file, and change the
hostname for the application. The current hostname for the demo application is:
helloworld-rubyapp.apps.live.cloud-platform.service.justice.gov.uk
We need a unique domain name for this instance of the application. There is a “wildcard” domain and SSL certificate available on the Cloud Platform, so we will stick to a domain name which matches:
*.apps.live.cloud-platform.service.justice.gov.uk
Choose a domain name for your application.
We suggest something nobody else is likely to be using, such as:
[your name]-helloworld.apps.live.cloud-platform.service.justice.gov.uk
Replace the two instances of helloworld-rubyapp
in the
kubectl_deploy/ingress.yaml
file with your chosen name.
If you want to use a domain name which doesn’t match the default wildcard domain, you will need to make some more changes. More information on this is available here.
Once you have saved your changes to the kubectl_deploy/ingress.yaml
file, you
can deploy the application using this command:
kubectl -n [namespace name] apply -f kubectl_deploy
This assumes that your current working directory is the root of your copy of the helloworld application repository.
You can think of this command as meaning:
“Hey, kubernetes. Please apply all the YAML files in the kubectl_deploy
directory to the namespace called my-namespace
(or whatever you called it).”
A few seconds later, you should be able to visit:
https://[your domain name].apps.live.cloud-platform.service.justice.gov.uk
…in your web browser, and see the “Hello, World!” message.
The kubernetes YAML files
This is how the YAML files link together:
You can find more deployment config info in the kubernetes developer documentation.
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <ingress-name>
namespace: <namespace-name>
annotations:
external-dns.alpha.kubernetes.io/set-identifier: <ingress-name>-<namespace-name>-<colour>
external-dns.alpha.kubernetes.io/aws-weight: "100"
spec:
tls:
ingressClassName: default
- hosts:
- helloworld-rubyapp.apps.live.cloud-platform.service.justice.gov.uk
rules:
- host: helloworld-rubyapp.apps.live.cloud-platform.service.justice.gov.uk
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: rubyapp-service
port:
number: 4567
(Note - Please change the ingress-name
and namespace-name
values in the above example. The colour
should be green
for ingress in EKS live
cluster)
This tells the cluster that our ingress should accept web traffic to the hostname we
defined, and should route that traffic to port 4567 of the “service” called
rubyapp-service
.
service.yaml
apiVersion: v1
kind: Service
metadata:
name: rubyapp-service
labels:
app: rubyapp-service
spec:
ports:
- port: 4567
name: https
targetPort: 4567
selector:
app: helloworld-rubyapp
This defines the service called rubyapp-service
which listens on port 4567
and forwards web traffic to port 4567 of any pods whose app
label is
helloworld-rubyapp
.
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld-rubyapp
spec:
replicas: 1
selector:
matchLabels:
app: helloworld-rubyapp
template:
metadata:
labels:
app: helloworld-rubyapp
spec:
containers:
- name: rubyapp
image: ministryofjustice/cloud-platform-helloworld-ruby:1.1
ports:
- containerPort: 4567
This creates a single instance (replicas: 1
) of a docker container based on
the docker image ministryofjustice/cloud-platform-helloworld-ruby:1.1
(pulled
from https://hub.docker.com, since no other source is mentioned), which listens
on port 4567, and has the app
label helloworld-rubyapp
.
Updating the application
To update our application, we need to:
- Change the application source code
- Create a new docker image from our updated code
- Push the image to a repository that kubernetes can access
- Update our
deployment.yaml
file with the new image details - Apply the updated yaml file to kubernetes
Everything but the first step would usually be done automatically by your application’s CI/CD pipeline, but we’re doing it manually here for ease of understanding.
Step 3 - Create an Amazon ECR
Our initial deployment of the Hello World application pulls a docker image from Docker Hub - a public docker image repository.
Most projects hosted on the Cloud Platform use Amazon ECR - a service providing private docker image repositories, only accessible by a single team. So, we’re going to use an ECR to store our updated docker image.
Please follow this guide to create an ECR for your namespace.
Step 4 - Change the application code
Edit the app.rb
file, and change “Hello, World!” to something else.
Build the docker image
Now that we have updated the source code, we can build a new docker image (locally).
docker build -t myimage .
You can replace
myimage
with any name you like for your new docker image. Don’t forget the.
at the end of the command.
Authenticating to your docker image repository
Before you can push the image to your ECR, you need to authenticate to it.
To authenticate to your ECR, you will need the access_key_id
and secret_access_key
which were created for you when you created your ECR. To retrieve these, see the this section of this guide.
tl;dr use this command:
cloud-platform decode-secret -n [namespace_name] -s [name of your secret]
Once you have your access_key_id
and secret_access_key
, set up an AWS profile using the AWS cli tool.
aws configure --profile [namespace name]
You can call the profile anything you want.
Supply your credentials when prompted. For region, enter eu-west-2
This is London, which is the region all cloud platform resources are in.
When prompted for Default output format [None]:
, just press enter.
Now, tell the aws command-line tool to use these credentials by running:
export AWS_PROFILE=[profile name]
Where [profile name]
is whatever you entered for --profile
in the aws configure
command.
Login to the repository
The command to login to Amazon ECR is slightly different, depending on your version of the aws
command-line tool. You can find this out by running:
aws --version
For AWS version 1.x use the following command to login to Amazon ECR
$(aws ecr get-login --no-include-email --region eu-west-2)
The output of the
aws ecr...
command is a longdocker login...
command. Including the$(...)
around the command executes this output in the context of the current shell
For AWS version 2.0, use this command:
aws ecr get-login-password --region eu-west-2 \
| docker login --username AWS --password-stdin \
754256621582.dkr.ecr.eu-west-2.amazonaws.com
For either version of the aws cli, the output of the above should include Login Succeeded
to confirm you have authenticated to the docker image repository.
These credential are valid for 12 hours. So, if you are working through this example over a longer period (e.g. the following day), you will have to login again.
Tag the image and push it to your ECR
If you base64 decode the repo_url
of the ECR secret in your namespace, you should get a value something like this:
754256621582.dkr.ecr.eu-west-2.amazonaws.com/my-team/dstest-ecr
We need to tag our docker image with that URL in order to push it to our ECR.
Amazon ECR uses the terms
repository
andimage
in a rather confusing way. Normally, you would think of a docker image repository as holding multiple images, each with a different name, where each image can have multiple tags. Amazon ECR conflates the repository and image - i.e. you can only push images with the same name to a given ECR.So, you can only push an image whose name matches your ECR’s
repo_url
value. You are free to change the tag of the image, and some teams overload the tag value as a way to store multiple completely different docker images in a single ECR.
We need to tag the image you built earlier so it can be pushed to your ECR
docker tag myimage 754256621582.dkr.ecr.eu-west-2.amazonaws.com/[team_name]/[repo_name]:1.0
Replace myimage
with whatever you chose in the docker build
command, and use the repo_url
value from your namespace secret.
Now we can push the image to your ECR:
docker push 754256621582.dkr.ecr.eu-west-2.amazonaws.com/[team_name]/[repo_name]:1.0
Step 5 - Deploy the application
Now that we have a new docker image with our updated software, we can deploy it.
Edit your kubectl_deploy/deployment.yaml
and change the image
from
ministryofjustice/cloud-platform-helloworld-ruby:1.1
to the same value you
used in the docker push
command above.
After saving your changes, apply them to the cluster by repeating the kubectl apply command:
kubectl -n [namespace name] apply -f kubectl_deploy
This will cause kubernetes to launch a new pod using the new docker image, and then delete the old one. In a few seconds, you should see your updated web page.
Add HTTP Basic Authentication
The application can be accessed from the internet at:
https://[your domain name].apps.live.cloud-platform.service.justice.gov.uk
As per the guidance for domain names, our application should have some authentication to prevent citizens accidentally mistaking development websites for live government services. Whilst this isn’t much of a problem with a ‘hello world’ site, it could be an issue for sites using the GDS prototype kit, which look exactly like live services. So, let’s add http basic authentication to our application.
We can do this by amending our Ingress, which is the kubernetes object that routes traffic to our application from the internet. We’ll create an encrypted username and password, and then store that in a kubernetes secret. Then, we will update our Ingress to use basic authentication, and tell it where to find the credentials.
Create the username and password
First we’ll use the htpasswd program to create a one-way hashed username and password. htpasswd is a system program which should be pre-installed on your computer.
To create a username ‘bob’ with password ‘password123’ in a file called ‘auth’, run the following command:
$ htpasswd -cb auth bob password123
Whatever value you use for your password (which should not be ‘password123’ be sure to make a note of it now - it will not be visible again.
Kubernetes secrets are stored as base64-encoded text strings, so we need to run the ‘auth’ file through base64 (which should also be pre-installed):
$ cat auth | base64
This will output the string we need to store in our secret. For the purpose of this tutorial, I’m going to use Ym9iOiRhcHIxJFVXQ1cxWDlvJGt3WDdoMTFZemNYdmVseHE2UFV2VzAK
Please substitute the value you got from the step above, using your own choice of username and password.
Note: only the hash of the password is stored in this string, not the password itself, so it is safe to store this string in a public github repository.
You can now delete the ‘auth’ file - we don’t need it anymore.
Create the kubernetes secret
Create a file called kubectl_deploy/secret.yaml
containing the following:
apiVersion: v1
kind: Secret
metadata:
name: basic-auth
data:
auth: Ym9iOiRhcHIxJFVXQ1cxWDlvJGt3WDdoMTFZemNYdmVseHE2UFV2VzAK
Remember to substitute your base64-encoded credentials string.
The next time we apply our yaml files to our namespace, this file will create a kubernetes secret called ‘basic-auth’. Now we need to configure our Ingress to use it.
Configure the Ingress
To configure our ingress to use basic authentication, we just need to add a couple of lines to the metadata section of kubectl_deploy/ingress.yaml
Replace this:
...
metadata:
name: helloworld-rubyapp-ingress
spec:
...
…with this:
...
metadata:
name: helloworld-rubyapp-ingress
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth
spec:
...
This tells the Ingress what kind of authentication to use, and which kubernetes secret contains the credentials.
Apply the changes
All that remains is to apply our updated yaml files in exactly the same way as we did before, when we deployed the application:
kubectl -n [namespace name] apply -f kubectl_deploy
You should see output like this:
deployment.apps/helloworld-rubyapp unchanged
ingress.networking.k8s.io/helloworld-rubyapp-ingress configured
secret "basic-auth" created
service "rubyapp-service" unchanged
Now, if you reload the browser page showing the ‘Hello world’ message from the application, you will be prompted to enter the username and password.