Deploying Application Updates to Kubernetes

Tue Apr 25, 2017 - 1100 Words

Deploying Application Updates to Kubernetes

Now that the meal planning application has been deployed to the Google Cloud we will need to be able to deploy our new changes to it. In this tutorial, we will go through just what it takes to roll out updates to the applications in our Kubernetes cluster.

Goals:

  • Deploy application updates for our Rails application in Google Cloud.
  • Determine how to go about deploying database migrations.

Now that we have an application running in our Kubernetes cluster we will want to keep on deploying our development efforts to it. Even though we’re currently working with a Ruby on Rails application there aren’t really any unique concerns that we have to deal with. The flow for deploying most web applications will be the same:

  1. Create updated image (new tag) with the most up to date code.
  2. Upload image to a repository.
  3. Update Deployment to use the new image tag.
  4. Apply changes to our Kubernetes cluster.

Enhancing the Rails Application

We need to make some changes before we can actually deploy anything useful. For our case, we’re going to be adding a new model to our application. This particular change will be good for us because it will require us to make a change to the database schema. To get started, we will use the Rails model generator to create most of the files that we need (making some tweaks as we go). To run the generator make sure you either have rails 5 installed locally or execute it from within the app Docker container. First, let’s modify the generator configuration so it doesn’t create more than we want:

config/application.rb

# require statements left out of example
module MealPlanner
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    config.generators do |g|
      g.test_framework :minitest, spec: true
      g.helper false
      g.javascript false
      g.stylesheet false
    end
  end
end

Now we can start the Docker containers and create the model:

$ docker-compose up -d
$ docker-compose run --rm app rails g model Ingredient name:string

Before we run our migration we’re going to tweak this a little so that we can ensure a few ingredients exist.

db/migrate/20170424103552_create_ingredients.rb

class CreateIngredients < ActiveRecord::Migration[5.0]
  def up
    create_table :ingredients do |t|
      t.string :name

      t.timestamps
    end

    Ingredient.create(name: "Thyme")
    Ingredient.create(name: "Olives")
    Ingredient.create(name: "Lettuce")
  end

  def down
    drop_table :ingredients
  end
end

Next, we’ll migrate the database:

$ rails db:migrate

Lastly, we will add a temporary piece of data to the homepage stating the number of ingredients so that we can ensure that this is deployed later.

app/views/welcome/show.html.erb

<h1>Welcome</h1>

<p>Existing recipes: <%= Recipe.count %></p>
<p>Unique ingredients: <%= Ingredient.count %></p>

Now we’ve made a significant enough change to deploy.

Database Migrations in a Containerized Application

How should we go about actually deploying this new database migration? We can’t deploy the code first and then manually migrate the database because it could lead to users attempting to access the application getting a missing migration error. There are a few ways to handle this:

  1. Don’t ship database migrations with code changes. Migrate the database additively before ever publishing the code that requires the change.
  2. Add migration as part of the container start process in production.

We’re going to go with the second option in our case, but it’s worth mentioning that this is the more “dangerous” option. We won’t have the means to “rollback” a database migration if it’s automatically done as part of the deployment. We need to make sure that the migrations that we write are always safe to run on deployment. As your application grows it is probably a better idea to transition to using the first strategy.

script/start

#!/bin/bash -e

if [[ -a /tmp/puma.pid ]]; then
  rm /tmp/puma.pid
fi

if [[ $RAILS_ENV == "production" ]]; then
  rake assets:precompile
  rake db:migrate
  mkdir -p /usr/share/nginx/html
  cp -R public/* /usr/share/nginx/html
  mkdir -p /etc/nginx/conf.d/
  cp site.conf /etc/nginx/conf.d/default.conf
fi

rails server -b 0.0.0.0 -P /tmp/puma.pid

Packaging Up a New Image

Now that we have all of the code that we need in place it’s time to update our image. The image that I’m currently using is us.gcr.io/aesthetic-genre-165010/mealplan:1.0.0, but you will have a different one (watch the previous tutorial get up to speed with our Kubernetes cluster set up). Let’s build the image now:

docker build -f Dockerfile.prod -t coderjourney/mealplan:1.1.0 .
docker tag coderjourney/mealplan:1.1.0 us.gcr.io/aesthetic-genre-165010/mealplan:1.1.0

With the image built and tagged properly, we can now publish this to the Google Container Registry.

$ gcloud docker -- push us.gcr.io/aesthetic-genre-165010/mealplan:1.1.0

Deploying the Image Change to Kubernetes

We now have an image to work with and all we have left to do is to update our Deployment to use it. We’ll make a change to the file now.

deployments/mealplan.yml

# Only showing updated `image` line
image: us.gcr.io/aesthetic-genre-165010/mealplan:1.1.0

Using what we’ve done up to this point we would use kubectl create in order to build the proper object, but since the object already exists we would see this error if we tried that.

$ kubectl create -f deployments/mealplan.yml
Error from server (AlreadyExists): error when creating "deployments/mealplan.yml": deployments.extensions "mealplan" already exists

Thankfully, create is not the only tool that kubectl gives us. What we’re really doing here is “applying” a change to the deployment, so for that, we can use the apply command:

$ kubectl apply -f deployments/mealplan.yml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
deployment "mealplan" configured

We receive a warning here because we should have actually created the objects originally using the apply command (and that’s what you should use moving forward). When first starting out the create command makes more sense than “applying” to nothing.

Looking at the public IP for our frontend service, we should see that our 3 ingredients are being mentioned on the homepage:

Homepage with ingredient count

Recap

In this tutorial, you went through making a routine change with migration to a Rails application and looked at how to deploy those changes. The big takeaways from this tutorial should be the pros & cons of how we deploy migrations and the addition of apply to your Kubernetes tool belt.