A Complete Guide to Deploying Elixir & Phoenix Applications on Kubernetes — Part 3: Deploying to Kubernetes

Rohan Relan
Polyscribe
Published in
5 min readMay 15, 2017

At Polyscribe, we use Elixir and Phoenix for our real-time collaboration and GraphQL API backends and Kubernetes for our deployment infrastructure. In this series, I walk through the setup we used from start to finish to create a system that supports the following:

  • Automatic clustering for Elixir and Phoenix channels
  • Auto-scaling to respond to spikes in demand
  • Service discovery for microservices, including those in other frameworks like Node.js
  • Maintaining the exact same environment between staging and production and easily deploying from staging to production
  • Relatively easy to setup and manage

Other posts in this series — Part 1: Setting up Distillery, Part 2: Docker and Minikube, Part 4: Secret Management, Part 5: Clustering Elixir and Phoenix Channels

At this point we have a Docker image hosting our release that we can manually start. Let’s get this image deployed onto our Minikube Kubernetes cluster. To start, create a k8s directory to hold all the configuration files we’re going to use to describe our deployment. Create a file in k8scalled myapp-deployment.yaml with the following contents:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: myapp-deployment
spec:
replicas: 2
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:release
ports:
- containerPort: 8000
args: ["foreground"]
env:
- name: HOST
value: "example.com"
- name: SECRET_KEY_BASE
value: "highlysecretkey"
- name: DB_USERNAME
value: "postgres"
- name: DB_PASSWORD
value: "postgres"
- name: DB_NAME
value: "myapp_dev"
- name: DB_HOSTNAME
value: "10.0.2.2"

First, notice that we’re using the YAML file format for configuration. We start by specifying the Kubernetes api version and the type of object (Deployment) we’re describing. We use the metadata tag to give this deployment a name.

What follows is the specification of the deployment, which consists of two parts: replicas and template. replicas: 2 indicates that we want two instances (“replicas”) of the “pod” (the Kubernetes term for a group of containers) that we’ll describe in the template section.

In the template, we once again start by providing some metadata — in this case we’ll give each pod created by this template a label app which has the value myapp. We’ll use this label later when we want to specifically select these pods.

Next (line 11), we provide the specification for the pod itself — specifically the containers that make up the pod. A pod can be made up of multiple containers, but in our case we only have one: the container that runs our Elixir release. To describe the container, we give it a name and then identify the Docker image to instantiate to create this container. The ports section will identify the ports we want to expose on the container, and since we’re using port 8000 in our container we use that as the value for containerPort.

For args, we pass ["foreground"]. Remember that in our release container, the entry point is the myapp binary that Distillery builds for our release. We pass it the argument foreground so our application will be the foreground application run by our container. If our application stops or terminates for any reason, Kubernetes will destroy the entire container and bring up a new one for us.

Finally, we pass in the environment variables needed to configure our application in the env section.

Kubernetes uses a declarative model — rather than telling Kubernetes the steps we want to take (eg. create a machine, launch this container etc), we tell it the state that we want and it figures out the steps to get there. Let’s tell it to create our deployment for us. First, make sure that we’re using our Minikube cluster by typing kubectl config set-context minikube. kubectl is the command we use to control Kubernetes clusters, and Minikube automatically created a context for our Minikube cluster named minikube. Now we tell Kubernetes to create the deployment specified in our file with the command kubectl create -f k8s/myapp-deployment.yaml. If everything went well, it should respond with deployment “myapp-deployment” created . (If it can’t find the Docker image, make sure you created the image after executing eval $(minikube docker-env) as described in part 2).

Kubernetes comes with a dashboard that allows you to see and modify the state of the cluster and all its deployments, pods and replicas. You can get the Kubernetes dashboard for your Minikube cluster by using the command minikube dashboard. Explore the dashboard a bit — you should see your deployment running and the two pods that make up your deployment.

We have our deployment with 2 pods running on our cluster, but the pods aren’t easily available externally. The pods can also be destroyed and recreated by Kubernetes, so we wouldn’t want to directly address the pods anyway. To expose the deployment, we need to set up a Service which will give us a consistent way to address our pods. Create a file k8s/myapp-service.yaml with the following contents:

apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
ports:
- port: 8080
targetPort: 8000
protocol: TCP
name: http
selector:
app: myapp
type: LoadBalancer

A plain english translation of this configuration is: Create a service named myapp-service (the metadata tag) that load balances (the type) between all the pods where the app label is myapp (the selector), exposing the port 8000 on the pod as 8080 in the service (the ports section).

Tell Kubernetes to create the service with kubectl create -f k8s/myapp-service.yaml. Minikube is a unique environment in that it can’t create public IPs the same way other Kubernetes services like Google Container Engine can. Instead, we can access the service by running minikube service myapp-service which will open the service’s “external” url in your browser. At this point, you can see your application’s home page being served by your Kubernetes cluster!

What if we update our app? To deploy a new version, rebuild your release by running mix docker.build and mix docker.release. We’ll need to give the new image a new tag so that Kubernetes knows that it’s changed. For this guide we’ll use a simple incrementing version number, but you can use whatever scheme you wish as long as different images get different tags. To retag the new image, run docker tag myapp:release myapp:2 (2 being our version number). To deploy this image, modify the image key in myapp-deployment.yaml to image: myapp:2 . Finally, we tell Kubernetes to apply this new configuration with kubectl apply -f k8s/myapp-deployment.yaml and that’s it! Kubernetes will do a rolling deployment of our new image while gracefully stopping the existing containers.

At this point we have a simple Kubernetes deployment for our Elixir application that could easily be applied to other Kubernetes hosting providers like AWS, GKE and Azure Container Service. In the new few parts we’ll improve this deployment by storing secrets securely, clustering the Elixir nodes so they can talk to each other and making our application public by deploying to GKE.

--

--

Rohan Relan
Polyscribe

Looking for some help bringing up ML or other new technologies within your organization? Shoot me a note at rohan@rohanrelan.com