Define Kubernetes Objects using Yaml

Tue Mar 21, 2017 - 1200 Words

You’ve taken Kubernetes for a spin using the command line utility, but eventually you’re going to need set up more customized pods & deployments. In this tutorial, you’ll learn how to set up YAML files to create custom Kubernetes objects.

Goals:

  • Create a deployable image for a Rails application
  • Generate a ConfigMap to define environment variables in our pods
  • Configure deployment for our Rails application

Our goal for today is to get a Rails application running with Kubernetes. To get the application up and running we need to figure out how to use environment variables with Kubernetes, how to package the application to run with Kubernetes, and lastly how to define our configuration.

Using the Minikube Docker Server

While we’re working locally using minikube we’re going to need to be building our applications into images that we can run in pods. We’ll do this using the docker build command, but by default, our docker client won’t be talking to the minikube Docker.

We’ll get around this by using a line similar to how we set which docker-machine to use:

$ eval $(minikube docker-env)

This will set our Docker client to use the proper server.

Building our Rails Image

We’ll be using the meal planning application that can be found here. Download this application to follow along.

From within the application’s directory, let’s build the image and give it a meaningful name:

$ docker build -t meal_plan:1 .

We’re giving this a version number of 1 since it’s our first time packaging it up. Having a version number like this allows us to ensure that we’re deploying the proper version.

Remember: latest is not a version number.

Starting the Database on a Separate Docker Host

We’re not going to be running the database within Kubernetes just yet, but we still need one to be running so our application can connect to it. We’re going to run the database from the VM that Kubernetes is running in so we can easily connect our application to it. You would never actually do this, but we don’t have time in this tutorial to coverage running Postgres in Kubernetes.

Before we start it, we’ll need to expose the database port to the Docker host so that our Kubernetes cluster can access it:

docker-compose.prod.yml

# Only showing the `prod_db` portion of the file
  prod_db:
    image: postgres
    env_file: .env.prod
    ports:
      - "5432:5432"
    volumes:
      - db-data:/var/lib/postgresql/db-data

Now with that changed we can start the database using:

$ script/prod up -d prod_db

To make it easier on ourselves we should also make sure that the database exists before continuing.

$ script/prod run --rm prod_app rake db:create db:migrate

Creating a Rails Deployment

Now that we have an image we could create a deployment using the kubectl run command, but that can’t be peer reviewed and might be hard to replicate so we’re going to create this deployment as a YAML file.

deployments/mealplan.yml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mealplan
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mealplan
        tier: backend
        track: stable
    spec:
      containers:
        - name: mealplan
          image: "meal_plan:1"
          ports:
            - name: rails
              containerPort: 3000

Besides the image declaration, you’ll notice that we use mealplan without and underscore, this is because the underscore is not allowed. This will create an application container for us, but it’s not going to work without the additional configuration values that we had been storing in the .env file.

Going through this file. We define the type of Kubernetes object using the kind key, set the name as a metadata subkey, and then we set the spec for the deployment. The spec is a lot like what we would define as part of a docker-compose.yml file. The notable differences here are the replicas setting, the template key, and the fact that we’re able to name the port. The labels allow us to specify ways to classify our objects.

For us to utilize the environment variables that we have been using we’re going to set up a ConfigMap, which is another object in Kubernetes that you can use to hold configuration data.

Creating the Meal Plan ConfigMap

ConfigMaps exist within Kubernetes and allow us to set specific keys (literals) or entire files. Unfortunately, our application isn’t set up to read an entire file in as environment variables (you could use dotenv for that). We’re going to need to set up the values from our .env.prod as literal values within a ConfigMap.

$ kubectl create configmap mealplan-config \
--from-literal=postgres_user=meal_planner \
--from-literal=postgres_password=dF1nu8xT6jBz01iXAfYDCmGdQO1IOc4EOgqVB703 \
--from-literal=postgres_host=$(minikub ip) \
--from-literal=rails_env=production \
--from-literal=rails_log_to_stdout=true \
--from-literal=secret_key_base=7a475ef05d7f1100ae91c5e7ad6ab4706ce5d303e6bbb8da153d2accb7cb53fa5faeff3161b29232b3c08d6417bd05686094d04e22950a4767bc9236991570ad

We can make sure that we set everything properly by looking at the data section of the ConfigMap resource:

$ kubectl get configmap mealplan-config -o yaml
apiVersion: v1
data:
  postgres_host: 192.168.99.101
  postgres_password: dF1nu8xT6jBz01iXAfYDCmGdQO1IOc4EOgqVB703
  postgres_user: meal_planner
  rails_env: production
  rails_log_to_stdout: "true"
  secret_key_base: 7a475ef05d7f1100ae91c5e7ad6ab4706ce5d303e6bbb8da153d2accb7cb53fa5faeff3161b29232b3c08d6417bd05686094d04e22950a4767bc9236991570ad
kind: ConfigMap
metadata:
  creationTimestamp: 2017-03-19T21:08:25Z
  name: mealplan-config
  namespace: default
  resourceVersion: "12711"
  selfLink: /api/v1/namespaces/default/configmaps/mealplan-config
  uid: 86160c7a-0e7a-11e7-99a4-080027b11ce5

Adding Configuration to the Meal Plan Deployment

With the config map created we can now use these in our deployment file.

deployments/mealplan.yml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: mealplan
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: mealplan
        tier: backend
        track: stable
    spec:
      containers:
        - name: mealplan
          image: "meal_plan:1"
          ports:
            - name: rails
              containerPort: 3000
          env:
            - name: POSTGRES_USER
              valueFrom:
                configMapKeyRef:
                  name: mealplan-config
                  key: postgres_user
            - name: POSTGRES_PASSWORD
              valueFrom:
                configMapKeyRef:
                  name: mealplan-config
                  key: postgres_password
            - name: POSTGRES_HOST
              valueFrom:
                configMapKeyRef:
                  name: mealplan-config
                  key: postgres_host
            - name: RAILS_ENV
              valueFrom:
                configMapKeyRef:
                  name: mealplan-config
                  key: rails_env
            - name: RAILS_LOG_TO_STDOUT
              valueFrom:
                configMapKeyRef:
                  name: mealplan-config
                  key: rails_log_to_stdout
            - name: SECRET_KEY_BASE
              valueFrom:
                configMapKeyRef:
                  name: mealplan-config
                  key: secret_key_base

For each environment variable we need to set the name as it would be exposed in the environment, all uppercase, and then we tell the deployment where it should pull this value. In this case, we want to use values from a ConfigMap so we’re using the configMapKeyRef option, providing the name of the ConfigMap that we created, and then specifying the key from the ConfigMap.

The configuration story here is a little tedious, but it does require us to be explicit. Now we can create our deployment using the kubectl create command and our deployment file:

$ kubectl create -f deployments/mealplan.yml

Exposing our Rails Application with a Server

To ensure that our application is running we should expose it using a Kubernetes service. Services can also be configured using configuration files so we’ll create that now.

$ mkdir services

services/mealplan.yml

kind: Service
apiVersion: v1
metadata:
  name: mealplan
spec:
  selector:
    app: mealplan
    tier: backend
  ports:
    - protocol: TCP
      port: 80
      targetPort: rails
  type: LoadBalancer

Setting up the Service works a lot like setting up the deployment earlier. One thing to notice is that we’re setting the targetPort using the port name that we created in our deployment. The selector section is important because that tells the Service how to find the pods to direct traffic to.

Let’s create the service using the same kubectl create command as before with our new file:

$ kubectl create -f services/mealplan.yml

Get the URL for this service from minikube:

$ minikube service mealplan --url
http://192.168.99.100:32261

If all went well, visiting that URL in your web browser should show you the unstyled home page of our meal planning application.

Rails on Kubernetes

Recap

In this tutorial, you created Kubernetes objects to run a Ruby on Rails application in a repeatable way. This didn’t cover even close to everything that we’ll be able to do with Kubernetes, but it is a pretty good start. If you would like to improve this, you should move some of the configuration values into Kubernetes Secrets. In the next tutorial, we’ll cover running stateful applications (like databases) in Kubernetes.

The code from this tutorial can be found here.