Recent Posts

Elasticsearch Proxy Rack App

Posted on 15 Nov 2015

For a project at work we use Elasticsearch. It was hosted inside of Kubernetes so we didn’t have a nice way to access the production instance from our browsers. I came up with this small proxy rack app that fowards the request through the Rails app. I have it authenticated via devise so only admins can view the actual instance.

The Rack App

class ElasticsearchForwarder
  def self.call(env)
    new.call(env)
  end

  def call(env)
    request = Rack::Request.new(env)

    case request.request_method
    when "GET", "POST", "OPTIONS", "HEAD", "PUT", "DELETE"
      response = client.send(request.request_method.downcase, request.path_info) do |f|
        f.params = request.params
        f.body = request.body.read
      end

      [response.status, headers(response.headers), [response.body]]
    else
      [405, {}, []]
    end
  end

  private

  def headers(headers)
    cors_headers.merge(headers)
  end

  def cors_headers
    {
      "Access-Control-Allow-Origin" => "*",
      "Access-Control-Allow-Methods" => "OPTIONS, HEAD, GET, POST, PUT, DELETE",
      "Access-Control-Allow-Headers" => "X-Requested-With, Content-Type, Content-Length",
      "Access-Control-Max-Age" => "1728000",
    }
  end

  def client
    @client ||= Faraday.new(AppContainer.elasticsearch_url) do |conn|
      conn.adapter :net_http
    end
  end
end

Make sure to replace AppContainer.elasticsearch_url with your own elasticsearch instance information. Otherwise it is a pretty simple rack app that just uses Faraday to forward your request on to elasticsearch. I set up CORS headers to allow anything, but I found with using the devise authentication it required that you were on the same domain anyways.

Mounting the App

authenticate :user, lambda { |user| user.admin? } do
	mount ElasticsearchForwarder => "/elasticsearch"
end

Here we mount the app inside of a devise authenticate block, making sure the user is an admin and logged in.

Future Improvements

I would like to make this less reliant on the devise cookie. We had to host our own ElasticHQ in order for devise to let requests through. It’s not that bad, but would have been cleaner to not require hosting our own.

Kubernetes Build Script for Rails Apps

Posted on 13 Oct 2015

We’ve so far seen how to put postgres inside of Kubernetes, here is how I was able to put a Rails app inside of Kubernetes. This is my deploy script that builds the docker container, and then pushes it out into the cluster. I’ll show snippets first then the entire script at the end.

Build Docker Image

build_dir=`mktemp -d`
git archive master | tar -x -C $build_dir
echo $build_dir
cd $build_dir

docker build -t $docker_image .
docker tag -f $docker_image $docker_tag
gcloud docker push $docker_tag

This creates a temp directory and creates a clean copy of the repo to build. I don’t want to build in the working directory because it might contain files you don’t want to build into the image. This makes sure it only ever contains what the master branch has.

It also pushes the image up tagged as the git sha. This lets us make sure we’re always deploying the correct code, seen when we update the replication controllers.

Optionally migrate

docker tag -f $docker_image "$docker_image:migration"
gcloud docker push "$docker_image:migration"
kubectl create -f config/kubernetes/migration-pod.yml
echo Waiting for migration pod to start...
sleep 10
while [ true ]; do
	result=`kubectl get pods migration | grep ExitCode`
	if [[ $result == *"ExitCode:0"* ]]; then
		break
	else
		sleep 1
	fi
done
kubectl delete pod migration

This section is done with a switch -m. It pushes to a special tag named migration and then creates a pod that will run rake db:migrate. It pushes to the special tag so that the migration pod can specify that branch and always have the latest code. Below is the yaml that is used to create the migration pod.

config/kubernetes/migration-pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: migration
  labels:
    name: migration
spec:
  restartPolicy: Never
  volumes:
    - name: environment-variables
      secret:
        secretName: app-env
  containers:
    - name: migration
      image: gcr.io/project/app:migration
      imagePullPolicy: Always
      args: ["bundle", "exec", "rake", "db:migrate"]
      volumeMounts:
        - name: environment-variables
          readOnly: true
          mountPath: /etc/env

Update Replication Controllers

kubectl rolling-update app-web --image=$docker_tag --update-period="10s"
kubectl rolling-update app-worker --image=$docker_tag --update-period="10s"

This updates the replication controller’s image. We set the update period to 10 seconds to speed the process up. The default time is 60 seconds. Kubernetes will take out one pod at a time and update to the new docker image. Once it completes the rolling update all pods will be running the newly deployed code.

Update to loading kubernetes secrets

I had to update how I was loading secrets from last time. I replaced foreman.sh with env.sh and changed the docker CMD.

env.sh
#!/bin/bash
ruby env.rb .env /etc/env && source .env && $*

$* is worth pointing out. Bash uses it as a variable for all arguments. In the case of the docker file below it will expand out to:

ruby env.rb .env /etc/env && source .env && foreman start web
Dockerfile
ENTRYPOINT ["./env.sh"]
CMD ["foreman", "start", "web"]

Full Script

#!/bin/bash
set -e

# Get options
migrate=''
deploy=''
while getopts 'md' flag; do
  case "${flag}" in
    m) migrate='true' ;;
    d) deploy='true' ;;
    *) error "Unexpected option ${flag}" ;;
  esac
done

sha=`git rev-parse HEAD`
docker_image="gcr.io/project/app"
docker_tag="$docker_image:$sha"

# Create an clean directory to build
build_dir=`mktemp -d`
current_dir=`pwd`

git archive master | tar -x -C $build_dir
echo $build_dir
cd $build_dir

# Build docker image and push
docker build -t $docker_image .
docker tag -f $docker_image $docker_tag
gcloud docker push $docker_tag

# Migrate if required
if [ $migrate ]; then
  docker tag -f $docker_image "$docker_image:migration"
  gcloud docker push "$docker_image:migration"
  kubectl create -f config/kubernetes/migration-pod.yml
  echo Waiting for migration pod to start...
  sleep 10
  while [ true ]; do
    result=`kubectl get pods migration | grep ExitCode`
    if [[ $result == *"ExitCode:0"* ]]; then
      break
    else
      sleep 1
    fi
  done
  kubectl delete pod migration
fi

if [ $deploy ]; then
  # Update images on kubernetes
  kubectl rolling-update app-web --image=$docker_tag --update-period="10s"
  kubectl rolling-update app-worker --image=$docker_tag --update-period="10s"
fi

# clean up build folder
cd $current_dir
rm -r $build_dir

Collection+JSON with ActiveModel::Serializers

Posted on 29 Sep 2015

For work I’m starting on a new API and I decided to use Collection+JSON after using it at REST Fest. I enjoyed making a ruby client for it, so I figured I would see how it works further than a weekend.

I like to use ActiveModel::Serializers and this DSL has made generating Collection+JSON extremely simple. We are using ActiveModel::Serializers master branch on github. Not the gem currently on Rubygems so you may need to tweak some things to get it working on the older version.

Using The DSL

app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def index
    render :json => orders, :serializer => OrdersSerializer, :meta => {
      :href => orders_url(params.slice(:search, :page, :per)),
      :up => root_url,
      :search => params[:search] || "",
    }
  end

  def show
    render :json => [order], :serializer => OrdersSerializer, :meta => {
      :href => order_url(order.first),
      :up => orders_url,
      :search => "",
    }
  end
end

Here is the controller. Of note is show displaying an array of orders, since there is no single element in Collection+JSON. This was a lot nicer than I expected, it flowed very well.

app/serializers/order_serializer.rb
class OrderSerializer < ActiveModel::Serialier
  include CollectionJson

  attributes :name, :ordered_at

  href do
    meta(:href)
  end
end

Here is the OrderSerializer. It includes CollectionJson which adds the DSL. The important feature is href. I found that passing in the href from the controller is best. I pull the href out from the meta hash. I’m not super sure this is the right way to go, but it’s worked well so far.

app/serializers/orders_serializer.rb
class OrdersSerializer < ActiveModel::Serializer
  include CollectionJson

  link "up" do
    meta(:up)
  end

  query "http://example.com/rels/order-search" do
    href orders_url
    data :search, :value => meta(:search), :prompt => "Order Name"
  end

  template do
    data "name", :value => "", :prompt => "Order Name"
  end

  href do
    meta(:href)
  end

  private

  def include_template?
    true
  end
end

Here we see some more of the DSL. Generating links, queries, and the template. You can see more about these in the code below.

config/initializers/serializers.rb
require 'collection_json'
ActiveModel::Serializer.config.adapter = :collection_json

class ActiveModel::Serializer::ArraySerializer
  include Rails.application.routes.url_helpers
  delegate :default_url_options, :to => "ActionController::Base"
end

class ActiveModel::Serializer
  include Rails.application.routes.url_helpers
  delegate :default_url_options, :to => "ActionController::Base"
end

Here we set the adapter to collection_json and include routes so the serializers can be hypermedia driven. I’m not super stoked about how many times we need to include routes, but I couldn’t figure out a way around it.

The Details

Here are the two files that make the above happen:

app/serializers/collection_json.rb
module CollectionJson
  extend ActiveSupport::Concern

  module RoutesHelpers
    include Rails.application.routes.url_helpers
    delegate :default_url_options, :to => "ActionController::Base"
  end

  module CollectionObject
    def encoding(encoding)
      @encoding = encoding
    end

    def data(name, options = {})
      @data << options.merge({
        :name => name,
      })
    end

    def compile(metadata)
      reset_query
      @meta = metadata
      instance_eval(&@block)
      generate_json
    end

    private

    def meta(key)
      @meta && @meta[key]
    end
  end

  class Query
    include CollectionObject
    include RoutesHelpers

    def initialize(rel, &block)
      @rel = rel
      @block = block
    end

    def href(href)
      @href = href
    end

    private

    def generate_json
      json = {
        :rel => @rel,
        :href => CGI.unescape(@href),
        :data => @data,
      }
      json[:encoding] = @encoding if @encoding
      json
    end

    def reset_query
      @data = []
      @encoding = nil
      @href = nil
    end
  end

  class Template
    include CollectionObject
    include RoutesHelpers

    def initialize(&block)
      @block = block
    end

    private

    def generate_json
      json = { :data => @data }
      json[:encoding] = @encoding if @encoding
      json
    end

    def reset_query
      @data = []
      @encoding = nil
    end
  end

  class_methods do
    include RoutesHelpers

    def link(rel, &block)
      @links ||= []
      @links << {
        :rel => rel,
        :href => block,
      }
    end

    def links
      @links
    end

    def query(rel, &block)
      @queries ||= []
      @queries << Query.new(rel, &block)
    end

    def queries
      @queries
    end

    def template(&block)
      return @template unless block_given?
      @template = Template.new(&block)
    end

    def href(&block)
      define_method(:href) do
        instance_eval(&block)
      end
    end
  end

  def links
    return unless self.class.links.present?
    self.class.links.map do |link|
      link.merge({
        :href => instance_eval(&link[:href]),
      })
    end.select do |link|
      link[:href].present?
    end
  end

  def queries
    return unless self.class.queries.present?
    self.class.queries.map do |query|
      query.compile(@meta)
    end
  end

  def template
    if respond_to?(:include_template?, true) && include_template?
      self.class.template.compile(@meta) if self.class.template.present?
    end
  end

  def meta(key)
    @meta && @meta[key]
  end
end
lib/collection_json.rb
module ActiveModel
  class Serializer
    module Adapter
      class CollectionJson < Base
        def initialize(serializer, options = {})
          super
          @hash = {}
        end

        def serializable_hash(options = {})
          @hash[:version] = "1.0"
          @hash[:href] = serializer.href
          @hash[:links] = serializer.links if serializer.links.present?
          @hash[:queries] = serializer.queries if serializer.queries.present?
          @hash[:template] = serializer.template if serializer.template.present?

          add_items

          {
            collection: @hash,
          }
        end

        def as_json(options = nil)
          serializable_hash(options)
        end

        private

        def add_items
          if serializer.respond_to?(:each)
            serializer.map do |serializer|
              add_item(serializer)
            end
          else
            add_item(serializer) if serializer.object
          end
        end

        def add_item(serializer)
          @hash[:items] ||= []

          item = {}
          item[:href] = serializer.href
          item[:links] = serializer.links if serializer.links.present?

          serializer.attributes.each do |key, value|
            next unless value.present?
            item[:data] ||= []

            data = {
              name: key.to_s,
              value: value,
            }

            item[:data] << data
          end if serializer.attributes.present?

          @hash[:items] << item
        end
      end
    end
  end
end

Kubernetes Secrets to Environment File

Posted on 15 Sep 2015

When hosting a Rails application, I really like to push as much configuration into the environment as possible. This is why I like dotenv and used it pretty extensively in the bramble. With the bramble I stuck the .env file inside of the container. Moving to Kubernetes I didn’t want to continue with this.

Kubernetes has something called a secret that stores key/values and you mount them as a volume in the pod. This works great except I want to continue using dotenv so I don’t write the entire application specifically for Kubernetes.

To manage this I wrote a simple script that converts a secrets folder into an environment file. I run this little script before starting foreman.

env.rb
env = {}

Dir["#{ARGV[1]}/*"].each do |file|
  key = file.split("/").last
  key = key.gsub("-", "_").upcase
  env[key] = File.read(file).strip
end

File.open(ARGV[0], "w") do |file|
  env.each do |key, value|
    file.puts(%{export #{key}="#{value}"})
  end
end
ruby env.rb .env /etc/etc

The file looks at the folder passed in as the second argument. Secret key values have to be hyphenated so we convert hyphens to underscores and uppercase everything. Then write the values to the first file in a format that dotenv accepts.

Secrets

To create the secret you upload the following YAML to kubernetes. Once the secret is created you mount them in a pod.

secrets.yml
apiVersion: "v1"
kind: "Secret"
metadata:
  name: app-env
data:
  rails-env: cHJvZHVjdGlvbg==
  rack-env: cHJvZHVjdGlvbg==
  rails-serve-static-files: dHJ1ZQ==
kubectl create -f secrets.yml

This file creates the secret app-env. Values are base 64 encoded.

app-controller.yml
apiVersion: v1
kind: ReplicationController
metadata:
  name: app
  labels:
    name: app
spec:
  replicas: 3
  selector:
    name: app
  template:
    metadata:
      labels:
        name: app
    spec:
      volumes:
        - name: environment-variables
          secret:
            secretName: app-env
      containers:
        - name: teashop
          image: smartlogic/app
          imagePullPolicy: Always
          ports:
            - containerPort: 5000
          volumeMounts:
            - name: environment-variables
              readOnly: true
              mountPath: /etc/env

This replication controller mounts the secret into /etc/env.

Dockerfile

To use the new script in your Dockerfile change the entrypoint to:

ENTRYPOINT ["./foreman.sh"]

With foreman.sh as:

#!/bin/bash
ruby env.rb .env /etc/env && foreman start ${BASH_ARGV[0]}

Improvements

This isn’t the nicest set up but it is a good stop gap until we can figure something else out. Some places to consider going forward are Keywhiz and etcd. Either of those should be a much better configuration management.

Running Postgres Inside Kubernetes With Google Container Engine

Posted on 31 Aug 2015

I have been playing around with Kubernetes in the last week or so and have really started liking it. One of the main things I wanted to get running was Postgres inside of the cluster. This is particularly challenging because postgres requires a volume that sticks around with the docker container. If it doesn’t you will lose your data.

In researching this I found out kubernetes has a persistence manager that lets you mount Google Compute disks. This also works for AWS EBS volumes if your cluster is hosted over there.

Create the disk

gcloud compute disks create pg-data-disk --size 50GB

Make sure this will be in the same zone as the cluster.

Next attach the disk to an already running instance, possibly a new instance. We only need to temporarily attach the disk so we can format it.

gcloud compute instances attach-disk pg-disk-formatter --disk pg-data-disk

After the disk is attached, ssh into the instance and run the following commands. This will mount and then format the disk with ext4. Then we unmount the drive.

sudo /usr/share/google/safe_format_and_mount -m "mkfs.ext4 -F" /dev/sdb /media/pg-data/
sudo umount /media/pg-data/
gcloud compute instances detach-disk pg-disk-formatter --disk pg-data-disk

Set up Kubernetes

With all of this complete we can start on the kubernetes side. Create the following four files.

postgres-persistence.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pg-data-disk
  labels:
    name: pg-data-disk
spec:
  capacity:
    storage: 50Gi
  accessModes:
    - ReadWriteOnce
  gcePersistentDisk:
    pdName: "pg-data-disk"
    fsType: "ext4"

This creates a persistent volume that pods can mount. Set it up with the same information that you used to create the disk.

postgres-claim.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pg-data-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi

This creates a claim on the persistent volume that pods will use to attach the volume. It should have the same information as above.

postgres-pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: postgres
  labels:
    name: postgres
spec:
  containers:
    - name: postgres
      image: postgres
      env:
        - name: DB_PASS
          value: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
      ports:
        - containerPort: 5432
      volumeMounts:
        - mountPath: /var/lib/postgresql/data
          name: pg-data
  volumes:
    - name: pg-data
      persistentVolumeClaim:
        claimName: pg-data-claim

There are a few important features of this file. We use the persistent claim in the volumes section and mount it in the volumeMounts section. Postgres doesn’t have it’s data dir at the top level of a mount so we move it lower with the environment variable PGDATA. You can also change the password with DB_PASS.

postgres-service.yml
apiVersion: v1
kind: Service
metadata:
  name: postgres
  labels:
    name: postgres
spec:
  ports:
    - port: 5432
  selector:
    name: postgres

This creates a service to easily access the postgres pod from other pods in the cluster. It creates a DNS entry pods can use to connect.

Create resources on Kubernetes

kubectl create -f postgres-persistence.yml
kubectl create -f postgres-claim.yml
kubectl create -f postgres-pod.yml
kubectl create -f postgres-service.yml

Run all of these commands and Postgres will be up and running inside your cluster.

Improvements

This works great accept for creating new databases. I had to attach to the running docker container to create new databases. This isn’t too bad, but could be a lot better.

It would also probably be better to create a replication controller instead of just a regular pod. That way if the pod died it would come back online by itself.

Overall kubernetes has been super fun to work with, especially the version hosted by Google.

Creative Commons License
This site's content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License unless otherwise specified. Code on this site is licensed under the MIT License unless otherwise specified.