Counter - A Full Example

6 min read

The code is hosted at https://github.com/harvey-earth/counter-app/ and https://github.com/harvey-earth/counter-infrastructure/. The first repository contains Ruby on Rails code of the actual application that runs on app servers. The second repository contains Terraform and Ansible code that sets up the entire environment to run the code.

Purpose

This is a simple application that just logs how many times someone has viewed it. It runs highly-available, and proves it by telling you which server you’re on.

Ruby on Rails App

To start with deploying an application, you must start with an application to deploy.

Rails is a good, easy choice. It follows the MVC (Model, View, Controller) pattern, so we’ll do those (a little out of order). To start with, I got on the latest stable versions of ruby (3.2.2) and rails (7.1.2) and I ran the magic command: rails new -d postgresql -j esbuild --css bootstrap counter-app to set up the boilerplate.

Model

I had done some design and analysis for this (thanks MIS degree) and I knew I needed 2 models for this. So I created them with the magic of:

bundle exec bin/rails generate model Server name:string:index
bundle exec bin/rails generate model Visit timestamp:datetime server:belongs_to requestip:string

This sets up a model for each server and server visit and associates each visit to a particular server. The Visit.timestamp attribute is not actually necessary, as each model in Rails automatically comes with creation and modification timestamps, but for tutorial purposes we will be setting this ourselves. Next the server needs to know its name, so inside of config/initializers/ I created a hostname.rb file. This sets the variable when the server starts running the application, instead of wasting CPU checking it multiple times by doing this in the controller.

Controller

The controller handles what happens with these models when a route is visited that matches the controller. The magic command here is:

bundle exec bin/rails generate controller Counter index

This sets up a controller in app/controllers/counter_controller.rb and a route to it in config/routes.rb (along with some other boilerplate). In the controller we have the server find itself or create itself in the database. This lets us add new servers seamlessly. Next we set some variables that will be passed to the view. Setting and saving of actual stuff is in a private method to prevent it from being called anywhere other than this controller right here.

The routes file also is set so that this controller/action is at the root path ('/'). There is a healthcheck built in, and then at the end every other path gets redirected to the root path (no 404s).

View

When the controller was created, it also created a view file at app/views/counter/index.html.erb. As this is what the root is set to, this is effectively our homepage. The other important file in play here is app/views/layouts/application.html.erb. The application.html.erb file has content that will be rendered on every page of the application (that should make sense). We will fill this out and class them up with Bootstrap so that it all looks nice.

There are some helper functions in app/helpers/counter_helper.rb (a blank file was created here when the controller was created) that are used in counter/index.html.erb. You’ll also notice some cache statements to optimize how this works. It will cache each server unless the server has a visit that invalidates the cache. How will it know when to invalidate the cache? This is what the touch: true statement in app/models/visit.rb does! This is optimized further by the counter_culture gem to separately reduce getting visit counts, which are expensive for the server to calculate.

Aside from that, we modify some configuration files that will pick up environment variables. We will set the database host, password, and user, along with the redis URL. From there we’re done with release v1.0 of the code and can move on to deploying it.

Terraform

Now that we have an app to deploy, let’s deploy it!

We’ll start with the main.tf file. It sets up the terraform backend, the aws provider, and sets up an SSH key. We’ll need this to SSH into our bastion (and for the bastion to reach the other servers). The last resource is going to take data from the resources that we created and make a dynamic inventory file for ansible to use. But we’ll get back to that later.

Next is the networking.tf file. This sets up a VPC and some subnets. There are also some DNS records created for our bastion server and the load balancer that will serve our app.

The webservers and databases files set up subnets, security groups, and the EC2 instances themselves. Nothing super exciting happening here. We allow SSH traffic from the bastion server so that we can get into our infrastructure if need be, and then allow the particular backends from the application servers.

The bastion server is where the magic starts to happen. We start with a subnet and a rule that allows us to SSH in to it, and set up another instance. But inside the instance creation we use some provisoners. In short, what this does is upload some SSH keys and the ansible code to the bastion server. Data from the created services is used with the inventory.ctmpl file to set the appropriate ansible variables. Then it runs ansible to set up the services on the servers themselves. The ansible code is trivial, so I will not explain it here. Now with a terraform apply you can set up the entire application.

Things to note

Variables

I added my variables.tf file to .gitignore to prevent accidental leaking of secrets. This repository is not actually the one running my infrastructure, as that one is in a private repo. But I have provided a sample variables file that can be used to set your own.

Improvements

There are many things that could be improved with this application. For instance, the entire point of my application is to invalidate the cache that it’s using. The cache won’t really be useful unless I were running dozens, or hundreds, of application servers. There are also many single points of failure, although this does demonstrate HA architecture. The application code is also not automatically deployed, and if a server changes IP addresses from a reboot the ansible code will need to be updated. I could go on for days about this.

But as time is a limiting factor for side-projects, this should be enough to give you a good start on some devops principles and hopefully give you some ideas to create and deploy your own application as automatically as possible.

Previous Advanced