Amazon’s ECS (EC2 Container Service) is a great way to manage and deploy docker containers. We at Lumos Labs were drawn to it for its ease of setup and tight integration with other AWS services. Using ECS’s blue-green deployment, Application Load Balancers, and CloudWatch, we were able to quickly stand up production-level services running at scale. Unfortunately, triggering deployments consistently and reliably became an unexpected pain point.
To answer to this problem, we built and open-sourced Broadside, a command-line tool that lets developers confidently manage deployments for any kind of application.
Example Usage
Assume your application has a configured broadside.conf.rb
:
Broadside.configure do |config|
config.application = 'railsapp'
config.default_docker_image = 'quay.io/lumoslabs/railsapp'
config.targets = {
production_web: {
cluster: 'production-cluster',
scale: 4,
command: ['bundle', 'exec', 'unicorn', '-c', 'config/unicorn.conf.rb'],
env_file: '.env.production',
predeploy_commands: [
['bundle', 'exec', 'rake', 'db:migrate']
]
},
staging_worker: {
cluster: 'staging-cluster',
scale: 1,
command: ['bundle', 'exec', 'rake', 'resque:work'],
env_file: '.env.staging'
}
}
end
Running:
broadside deploy short --target staging_worker --tag feature_branch
would deploy the feature_branch
tagged image from quay.io/lumoslabs/railsapp
to the existing ECS service called railsapp_staging_worker
at a scale of 1,
importing any environment variables defined in .env.staging
into the ECS task definition.
When using the following command and flags:
broadside deploy full --target production_web --tag v2.0
Broadside will first launch one-off tasks sequentially running the configured predeploy_commands
, then deploy to the railsapp_production_web
service at a scale of 4.
How it works
Under the hood, Broadside is simply leveraging ECS’s blue green deployment. The ECS service is updated to use a new task definition revision created with the provided tag and configurations. Deployment events from AWS are continually polled until the service reaches a stable state (i.e. the new containers are able to start and respond to health checks). In the event of a timeout or error, Broadside will simply mark the deployed revision as “inactive” and the previous revision will be deployed. This will ensure that the most recent task definition revision is always the configuration that is in service.
Other Useful Commands
Running many applications on a large cluster can complicate administration. For this reason we developed the following utility commands to manage deployed services:
broadside targets
- displays a summarized table of information for all deploy targets for the current applicationbroadside ssh --target example_target
- SSH onto the host machine running the docker containerbroadside bash --target example_target
- brings up a bash shell inside the container directlybroadside status --target example_target
- displays the service’s instances that current containers are running on, environment variables, and much morebroadside logtail --target example_target
- tails logs on a running container for the specified service
Running one-off commands
Broadside allows you to spin up a container with a given tag, execute a command on it, and then spin it down. For example, if you wanted to run a database migration using a specific version of the application, you could run:
broadside run --command 'bundle exec rake db:migrate' --tag v1.9.8 --target example_target
Broadside will start an ECS task running bundle exec rake db:migrate
that will display logs upon completion.
Bootstrapping a new service
and task_definition
In our prior examples, it was assumed you had already defined a service
and initial task_definition
in the Amazon GUI. With the bootstrap command, you can tell Broadside to do that for you by running:
broadside bootstrap --target example_target
Simply configure your broadside.conf.rb
with the additional values in service_config
and task_definition_config
:
Broadside.configure do |config|
config.application = 'myapp'
config.default_docker_image = 'quay.io/example/image_path'
config.aws.ecs_default_cluster = 'mycluster'
config.targets = {
example_target: {
scale: 2,
command: ['java', '-cp', '*:.', 'com.example.application.myapp.StartApp'],
env_file: '.env.production'
service_config: {
deployment_configuration: {
minimum_healthy_percent: 50
}
},
task_definition_config: {
container_definitions: [
{
cpu: 512,
memory: 2048
}
]
}
}
}
end
An initial task_definition
will be created with additional configurations at the task_definition_config
key, which accepts valid ECS task_definition
options.
Following that, a service called myapp_example_target
will be created with the configuration at the :service_config
key, which may contain any valid ECS service options.
Postscript
We have found Broadside to dramatically simplify a lot of the workflow involved with using ECS. If you use ECS, consider trying out Broadside and let us know what you think!