Testing containers with dependencies with localstack

that.

A second requirement is that we want to make the secret handling transparent to our existing app.

The app will get the secrets injected as environment variables.

We’ll achieve this by adding an ENTRYPOINT to our image that takes care of fetching the secrets.

We’ll be storing our secrets in ASM.

This is how the architecture looks like:Extending the appGiven that we are fully TDD compliant, let’s define the test that proves that our app is successfully returning the secret:describe 'fetches a secret from ASM' do it { wait_for(secret).

to match(/localstack_secret/) }endprivatedef secret command('curl localhost:3000/secret').

stdoutendthis is how the route is implemented in the app:app.

get('/secret', (req, res) => res.

send(`The super secret value is ${process.

env.

SECRET}`))this test will fail because we do not, in fact, provide the secret to the app.

But we will!Providing the secretSo as I said, there will be an entrypoint for this.

We start with the test:describe file('/usr/sbin/entrypoint.

sh') do it { is_expected.

to be_file }endthe entrypoint itself will use the awscli to fetch the secrets.

It looks like this:#!/usr/bin/env bashset -esecret=$(aws –endpoint-url=http://localstack:4584 –region "${AWS_REGION}" secretsmanager get-secret-value –secret-id "${SECRET_KEY}" | jq -r .

SecretString)export SECRET="$secret"exec "$@"Note that for a production image, you might be better served using something like pstore instead.

Regardless, we need to modify our Dockerfile bit to integrate this.

There are some things to do.

We need to add awscli as a dependency and we need to add our entrypoint and make sure it runs:# hadolint ignore=DL3018,DL3013RUN apk update –no-cache && apk add –no-cache bash jq python py-pip curl && pip –no-cache-dir install –upgrade pip awscli && apk -v –purge del py-pip && rm -rf /var/cache/apk/*COPY entrypoint.

sh /usr/sbin/entrypoint.

shENTRYPOINT ["/usr/sbin/entrypoint.

sh"]So everything should be ready!.Now we can just run the tests and see that … they all fail.

Including the old ones that were perfectly green before.

Introducing dependenciesOur container does not work anymore because it is trying to access ASM at runtime and failing.

The dreaded dependencies are sabotaging our tests once again.

A solution like logging into AWS and fetching the real secret feels unsatisfactory.

What else to do?Enter localstack.

Thanks to this awesome project we can mock AWS dependencies and keep our tests running (almost) transparently.

First, we need to orchestrate our app so that:localstack gets started and serves a replica of ASM in port 4584an init container gets executed to inject a secret into ASMour application is booted with localstack as a dependency, so that fetching secrets workswe do this by using Docker Compose.

The full includes the three elements, gracefully orchestrated together:version: '3'services: localstack: container_name: localstack image: localstack/localstackports: – "4584:4584"environment: – DEFAULT_REGION=eu-central-1 – SERVICES=secretsmanagerinit: container_name: init build: .

/initenv_file: .

envdepends_on: – localstacklinks: – localstackapp: container_name: app build: .

/appports: – "3000:3000"env_file: .

envdepends_on: – initlinks: – localstackThe init container is just ruby alpine executing this script:asm = Aws::SecretsManager::Client.

new(region: ENV['AWS_REGION'], endpoint: 'http://localstack:4584')asm.

create_secret(name: ENV['SECRET_KEY'], secret_string: 'localstack_secret')the entrypoint from the beginning needs to account for the url, so we change it to aws –endpoint-url=http://localstack:4584.

We can pick the URL based on the environment for a full example.

With this we are almost done.

We just need to tell ServerSpec to use docker-compose to start up our container setup, which can be done in the rails_helper.

rb:require 'serverspec'require 'docker'require 'docker/compose'require 'rspec/wait'set :backend, :dockerset :docker_container, 'app'RSpec.

configure do |config| config.

wait_timeout = 15 # secondscompose = Docker::Compose.

newconfig.

before(:all) do compose.

up(detached: true, build: true) endconfig.

after(:all) do compose.

kill compose.

rm(force: true) endendAnd that is basically it!.Now our tests run perfectly fine.

We have containers that are interacting with a secret store that have a good test suite to make sure everything runs correctly.

It does not get much more TDD’ish.

What’s next?There is a but (isn’t always one?).

As of today, localstack can only store one secret in its implementation, so if your container relies on fetching more than one secret it won’t be able to find them all.

This has been recently patched in moto, the library underneath, and it is just waiting for the next release of localstack to be available.

After that, no secret will be safe from mocking.

Originally published at hceris.

com.

.. More details

Leave a Reply