Bundler / Docker / alpine for a super small container

Took me a while to figure out how to get this running without installing ruby-dev or tools to compile native extensions, keeping my container nice and small.

Updated: using builtin packages for bundler + io-console now since they are smaller then installing manually and the logic is simpler
(apk add –update ruby ruby-io-console ruby-bundler)
… if latest bundler is needed below Dockerfile might still be useful.

FROM alpine

RUN apk add --update ruby && rm -rf /var/cache/apk/*

ENV BUNDLER_VERSION 1.12.3
RUN gem install bundler -v $BUNDLER_VERSION --no-ri --no-rdoc

# bundler wants some library that needs core extensions ... but it won't compile
RUN mkdir /usr/lib/ruby/gems/2.2.0/gems/bundler-$BUNDLER_VERSION/lib/io
RUN touch /usr/lib/ruby/gems/2.2.0/gems/bundler-$BUNDLER_VERSION/lib/io/console.rb

# bundler does not want to install as root
RUN bundle config --global silence_root_warning 1

RUN mkdir /app
WORKDIR /app

ADD Gemfile .
ADD Gemfile.lock .
RUN bundle

Testing SuckerPunch/Celluloid vs ActiveRecord Transactions

Celluloid runs in a new thread, so it runs on a new transaction. Therefore we cannot test what was done in this transaction.

If you are not using any Celluloid callbacks then a simple unthreaded baseclass can help:

class BackgroundJobTestable
  def perform
    ...
end

class BackgroundJob < BackgroundJobTestable
  include SuckerPunch::Job
  workers 2
end

An alternative way to fix this is to disable AR multithreading support via a mocking library like mocha … this will break any code that truly runs in parallel, but if you only run 1 code path at a time this should work fine.

ActiveRecord::Base.stubs(connection: ActiveRecord::Base.connection)

Parallelizing SparkleFormation execution

We generate each templates CloudFormation .json file and check them in to spot subtle diffs when refactoring. We also validate all configs on every PR. Doing this serially takes a lot of time, but it can be parallelized easily, while also avoiding ruby boot overhead.

This took us from ~5 minutes runtime to 30s, enjoy!

desc "Validate templates via cloudformation api"
task :validate do
  each_template do |template|
    execute_sfn_command :validate, template
  end
end

desc "Generates cloudformation json files"
task :generate, [:pattern] do |_t, args|
  pattern = /#{args[:pattern]}/ if args[:pattern]
  previous = Dir["generated/*.json"]

  used = each_template do |template|
    next if pattern && template !~ pattern
    output = execute_sfn_command :print, template

    generated = "generated/#{File.basename(template.sub('.rb', '.json'))}"
    File.write generated, output
    generated
  end

  (previous - used).each { |f| File.unlink(f) } unless pattern
end

...

require 'parallel'
require 'timeout'

def each_template(&block)
  # preload slow requires
  require 'bogo-cli'
  require 'sfn'

  # run in parallel, but isolated to avoid caches from being reused
  options = {in_processes: 10, progress: "Progress", isolation: true}
  Parallel.map(Dir["templates/**/*.rb"], options, &block)
end

private

def execute_sfn_command(command, template, *args)
  Timeout.timeout(20) do
    capture_stdout do
      Sfn::Command.const_get(command.capitalize).new({
        defaults: true, # do not ask questions about parameters
        file: template,
        retry: {type: :flat, interval: 1} # we will run into rate limits, ignore them quickly
      }, args).execute!
    end
  end
rescue StandardError
  # give users context when something failed
  warn "bundle exec sfn #{command} #{args.join(" ")} --defaults -f #{template}"
  raise
end

def capture_stdout
  old = $stdout
  $stdout = StringIO.new
  yield
  $stdout.string
ensure
  $stdout = old
end

Thoughts on sparkleformation

sfn provides a few nice features, but it is really hard to deal with all the other craziness that goes on …

method_missing (typos result in silently swallowed errors)
https://github.com/sparkleformation/sparkle_formation/pull/112

components being loaded multiple times
https://github.com/sparkleformation/sparkle_formation/issues/114

cli silently catching all kinds of template errors making debugging impossible for anyone not knowing how to open a gem …
https://github.com/sparkleformation/sfn/issues/168
Solution: use .sfn.rb

very little testing going on + no CI setup …
https://github.com/sparkleformation/sparkle_formation/pull/110

ruby code is written in a non-standard style

forcing a `Bundler.require` which makes everything slow when there are lots of gems in the Gemfile https://github.com/sparkleformation/sfn/issues/170
Solution: add a :sfn group that only includes sfn gems

PR comments being ignored https://github.com/sparkleformation/sparkle_formation/pull/117

It adds a lot of unnecessary work and bugs that it tries to re-invent libraries that already exist (thor/optparse/etc for cli interfaces) and fog for api abstraction.

I don’t mean to hate on chrisroberts … he fixed lots of bugs / is helpful / drives this project forward , I just feel like this project has basic flaws in it’s foundation that should be fixed instead of adding more and more features on top.

I want to like/use this project, but this makes it really hard … the more templates we add / the more members we introduce to sfn the worse I feel about this whole situation … and the more I want to replace it with something sane/verbose that ‘just works’ instead of being magically broken.