Continuous deployment Ruby application to Minikube with Google's Skaffold
Tags: ruby, kubernetes, development, skaffold
Published: 2020-01-21 18:44:39

---
title: Continuous deployment Ruby application to Minikube with Google's Skaffold
published: true
description: Simple guide to automate continuous deployment of Ruby application with Google's Skaffold
tags: #ruby #kubernetes #development #skaffold
cover_image: https://thepracticaldev.s3.amazonaws.com/i/5dv7ow38mj97il71a748.png
---

[Kubernetes](http://kubernetes.io/) has made it very easy to deploy and scale applications to the cloud than ever. Still, the development process has not evolved at the same speed.

Today, most developers try to either run parts of the infrastructure locally with docker or test these integrations directly in the cluster via CI jobs or the "`docker build`, `docker push`, `kubectl apply`" cycle. Manual deployment is painful and incredibly slow.

In this article, we will see how to set up continuous deployment of simple Ruby application to Minikube to reduce the development cycle time using the [Google's Skaffold tool](https://skaffold.dev/).

Skaffold handles the workflow for building/pushing Docker images and deploying your application. It helps to reuse your actual application production configuration (Kubernetes manifests, Helm chart, Docker Compose config) for the local continuous development cycle. Skaffold also supports file sync to prevent the full container rebuild/redeploy.


# Preparation

First of all, let's install the required tooling.

## Install Minikube

Here are the commands to run for Mac, more details could be found [here](https://kubernetes.io/docs/tasks/tools/install-minikube/):

```bash
$ brew install minikube
$ minikube version
minikube version: v1.5.2
commit: 792dbf92a1de583fcee76f8791cff12e0c9440ad-dirty
$ minikube start

$ eval $(minikube docker-env)
$ kubectl config use-context minikube

```

The line `eval $(minikube docker-env)` set the docker client to use Minikube's Docker engine. It allows us to avoid docker image pushing and simplifies the full redeploy cycle.

We've also changed to actual `kubectl` context to minikube.


## Install Skaffold

Here are the documentation details of [Skaffold installing](https://skaffold.dev/docs/install/), for running on Mac just use:

```bash
$ brew install skaffold
```

# Ruby/Rack application

We will use the simplest Rack-application from the [Skaffold's github](https://github.com/GoogleContainerTools/skaffold/tree/master/examples/ruby):

```bash
$ git clone git@github.com:GoogleContainerTools/skaffold.git
$ cd examples/ruby
```

## Skaffold dev

Just run `skaffold dev` and you will see similar output:

```bash
$ skaffold dev
Listing files to watch...
 - ruby-example
Generating tags...
 - ruby-example -> ruby-example:v1.1.0-118-g92f37b200-dirty
Checking cache...
 - ruby-example: Not found. Building
Found [minikube] context, using local docker daemon.
Building [ruby-example]...
Sending build context to Docker daemon  6.144kB
Step 1/6 : FROM ruby:2.7
 ---> fb53c5f433da
Step 2/6 : WORKDIR /app
 ---> Using cache
 ---> 0c2d04fabca0
Step 3/6 : ADD Gemfile* ./
 ---> Using cache
 ---> 06d6623561fc
Step 4/6 : RUN bundle install
 ---> Using cache
 ---> 9bbcf218dda5
Step 5/6 : ADD . ./
 ---> Using cache
 ---> 478cca6b9c4f
Step 6/6 : CMD ["bundle","exec","puma"]
 ---> Running in 434b1923c060
 ---> 988d9e9e7bf4
Successfully built 988d9e9e7bf4
Successfully tagged ruby-example:v1.1.0-118-g92f37b200-dirty
Tags used in deployment:
 - ruby-example -> ruby-example:988d9e9e7bf4b44beb62a8aaa7104cdf407a8523db0bdc7fe331600d374ba274
   local images can't be referenced by digest. They are tagged and referenced by a unique ID instead
Starting deploy...
 - service/ruby created
 - deployment.apps/ruby created
Watching for changes...
[ruby-75c5b89895-rd2lb ruby] Puma starting in single mode...
[ruby-75c5b89895-rd2lb ruby] * Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
[ruby-75c5b89895-rd2lb ruby] * Min threads: 0, max threads: 16
[ruby-75c5b89895-rd2lb ruby] * Environment: development
[ruby-75c5b89895-rd2lb ruby] * Listening on tcp://0.0.0.0:9292
[ruby-75c5b89895-rd2lb ruby] Use Ctrl-C to stop
```

At that time Skaffold tool has built the Docker image, tagged it with a specific tag, and deployed application to Minikube cluster. Then it  started to wait for the source file changes.

The result could be seen here:

```bash
$ curl -s $(minikube service ruby --url)
Hello Skaffold!
```

## Hot reload

As configured at the [Skaffold config](https://github.com/GoogleContainerTools/skaffold/blob/master/examples/ruby/skaffold.yaml#L10-L11) - it watches for `*.rb` files and reloads all the changes rights into the container sources. The sample Rack application also supports for reloading with help of the `rack-unreloader` ruby-gem.

If we change the `app.rb` file to return other string, like

```ruby
class App
  def self.call(env)
    [ 200, {"Content-Type" => "text/html"}, ["Hello Skaffold!!!"]]
  end
end
```

We will see skaffold sync files into the container:

```bash
Syncing 1 files for ruby-example:988d9e9e7bf4b44beb62a8aaa7104cdf407a8523db0bdc7fe331600d374ba274
Watching for changes...
```
As the Rack application supports file reloading, it will now produce the different output:

```bash
$ curl -s $(minikube service ruby --url)
Hello Skaffold!!!
```

## Automatic redeploy

If we change others files, such as `backend/Dockerfile`, Skaffold will autmatically rebuild the Docker image, and redeploy the application:

```bash
echo "gem 'oj'" >> backend/Gemfile
```

We'll see full redeploy:

```bash
Generating tags...
 - ruby-example -> ruby-example:v1.1.0-118-g92f37b200-dirty
Checking cache...
 - ruby-example: Not found. Building
Found [minikube] context, using local docker daemon.
Building [ruby-example]...
Sending build context to Docker daemon  6.144kB
Step 1/6 : FROM ruby:2.7
 ---> fb53c5f433da
Step 2/6 : WORKDIR /app
 ---> Using cache
 ---> 0c2d04fabca0
Step 3/6 : ADD Gemfile* ./
 ---> Using cache
 ---> 7b4671e7469d
Step 4/6 : RUN bundle install
 ---> Using cache
 ---> 0331311c04f3
Step 5/6 : ADD . ./
 ---> 67e2cf138ed3
Step 6/6 : CMD ["bundle","exec","puma"]
 ---> Running in fa04b480dd8f
 ---> 31f2e1f54283
Successfully built 31f2e1f54283
Successfully tagged ruby-example:v1.1.0-118-g92f37b200-dirty
Tags used in deployment:
 - ruby-example -> ruby-example:31f2e1f5428397d5d4aa1f2d2328ff859e01d970f304f2f9dbb9f4ba8d0c8968
   local images can't be referenced by digest. They are tagged and referenced by a unique ID instead
Starting deploy...
 - deployment.apps/ruby configured
Watching for changes...
[ruby-75c5b89895-rd2lb ruby] - Gracefully stopping, waiting for requests to finish
[ruby-75c5b89895-rd2lb ruby] === puma shutdown: 2020-01-20 11:49:02 +0000 ===
[ruby-75c5b89895-rd2lb ruby] - Goodbye!
[ruby-5b74868996-ckg24 ruby] Puma starting in single mode...
[ruby-5b74868996-ckg24 ruby] * Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
[ruby-5b74868996-ckg24 ruby] * Min threads: 0, max threads: 16
[ruby-5b74868996-ckg24 ruby] * Environment: development
[ruby-5b74868996-ckg24 ruby] * Listening on tcp://0.0.0.0:9292
[ruby-5b74868996-ckg24 ruby] Use Ctrl-C to stop
```

Change the `app.rb` then to return json string:
```ruby
require 'oj'

class App
  def self.call(env)
    json = { "message" => "hello" }
    [ 200, {"Content-Type" => "text/html"}, [Oj.dump(json)]]
  end
end
```

After the `app.rb` file synchronization, the app would return:

```bash
$ curl -s $(minikube service ruby --url)
{"message":"hello"}
```

## Features

File sync and automatic `rebuild/redeploy` are the core.

But, Skaffold has many essential features for Kubernetes-native development:
 - policy-based image tagging
 - resource port-forwarding
 - supports multiple profiles
 - supports Helm, Kustomize, Docker Compose manifests

## Conclusion

Skaffold is simple and lightweight tool, easy to get up and running for local development! It automates deployments, but for comfortable development I like it to have any other features, like backward file sync.