AwsEc2Environment
A gem that makes it easier to interact with and deploy Ruby projects that are hosted on EC2 instances in AWS.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add aws_ec2_environment
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install aws_ec2_environment
Usage
Use AwsEc2Environment.from_yaml_file
to create a new representation of your
EC2 environment from a config file:
ec2_env = AwsEc2Environment.from_yaml_file("./aws.yml", :production)
# this will ensure that any post-connection cleanup is handled, such as terminating
# any SSM port forwarding sessions that are active
at_exit { ec2_env.stop_ssh_port_forwarding_sessions } if ec2_env.config.use_ssm
# this will return a list of hosts for sshing, handling any pre-connection setup
# such as starting port forwarding sessions for each instance if SSM is enabled.
ec2_env.hosts_for_sshing
Configuration
This is the most basic configuration you can have:
production:
aws_region: ap-southeast-2
ssh_user: deploy
filters:
- name: 'instance-state-name'
values: ['running']
- name: 'tag:Name'
values: ['MyWebsiteProductionAppServerAsg']
All the top level properties are required, and the filters
key holds an array
of filters that are used with the
DescribeInstances
API endpoint.
With bastion hosts
You can specify filters for a bastion instance too:
production:
aws_region: ap-southeast-2
ssh_user: deploy
filters:
- name: 'instance-state-name'
values: ['running']
- name: 'tag:Name'
values: ['MyWebsiteProductionAppServerAsg']
bastion_instance:
ssh_user: bastion
filters:
- name: 'instance-state-name'
values: ['running']
- name: 'tag:Name'
values: ['MyWebsiteProductionBastionAsg']
Note that the filters should result in one instance being returned, otherwise an error will be thrown.
If you use the same user as your application servers, you can pass an array of filters as the value of the top-level property:
production:
aws_region: ap-southeast-2
ssh_user: deploy
filters:
- name: 'instance-state-name'
values: ['running']
- name: 'tag:Name'
values: ['MyWebsiteProductionAppServerAsg']
bastion_instance:
- name: 'instance-state-name'
values: ['running']
- name: 'tag:Name'
values: ['MyWebsiteProductionBastionAsg']
With SSM
If your instances have the SSM Agent (preinstalled on some AMIs), you can use SSM to connect directly to instances even if they're in a private subnet, via port forwarding:
production:
aws_region: ap-southeast-2
ssh_user: deploy
ssm_host: 'ec2.#{id}.local.ackama.app'
use_ssm: true
filters:
- name: 'instance-state-name'
values: ['running']
- name: 'tag:Name'
values: ['MyWebsiteProductionAppServerAsg']
This requires the
aws
CLI andsession-manager-plugin
to be installed locally. These both come preinstalled on GitHub Actions runners, and are otherwise easy to install manually.
You can also specify an alternative hostname to use instead of 127.0.0.1
with
the ssm_host
property - this is useful when working with tools like Capistrano
that only log the host name, so this property can let you ensure each instance
can be identified in the logs.
This property should be a host that resolves to 127.0.0.1
, and you can inject
the instance id with #{id}
.
Ackama provides
ec2.*.local.ackama.app
for this
With Capistrano
# ./Capfile
# ...
require "aws_ec2_environment"
# ./config/deploy/production.rb
set :rails_env, "production"
set :branch, "production"
ec2_env = AwsEc2Environment.from_yaml_file("./aws.yml", :production)
at_exit { ec2_env.stop_ssh_port_forwarding_sessions } if ec2_env.config.use_ssm
= {}
if ec2_env.use_bastion_server?
[:proxy] = Net::SSH::Proxy::Command.new(ec2_env.build_ssh_bastion_proxy_command)
end
set :ssh_options,
role(:app, ec2_env.hosts_for_sshing, user: ec2_env.config.ssh_user)
With custom port forwarding
You can also use the SsmPortForwardingSession
class directly to do port
forwarding, which can be useful for things like custom rake tasks:
require "aws_ec2_environment"
task :forward_port, %i[instance_id remote_port local_port] => :environment do |_, args|
# trap ctl+c to make things a bit nicer (otherwise we'll get an ugly stacktrace)
# since we expect this to be used to terminate the command
trap("SIGINT") { exit }
logger = Logger.new($stdout)
instance_id = args.fetch(:instance_id)
remote_port = args.fetch(:remote_port)
local_port = args.fetch(:local_port, nil)
session = AwsEc2Environment::SsmPortForwardingSession.new(
instance_id,
remote_port,
local_port:,
logger:
)
at_exit { session.close }
local_port = session.wait_for_local_port
local_alias = "ec2.#{instance_id}.local.ackama.app:#{local_port}"
logger.info "Use #{local_alias} to communicate with port #{remote_port} on #{instance_id}"
loop { sleep 1 }
end
AWS Authentication and Permissions
Since this gem interacts with AWS, it must be configured with credentials - see here for how to do that.
We recommend using OpenID Connect to authenticate with AWS when running in GitHub Actions.
The credentials must be for an identity that is allowed to perform the
ec2:DescribeInstances
action. If you're using SSM you must also allow the
ssm:StartSession
and ssm:TerminateSession
actions.
Here is a sample IAM policy document that grants these actions conditionally in accordance with the principle of least privilege:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowDescribingInstances",
"Effect": "Allow",
"Action": "ec2:DescribeInstances",
"Resource": "*"
},
{
"Sid": "AllowStartingPortForwardingSessions",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ssm:*::document/AWS-StartPortForwardingSession"
},
{
"Sid": "AllowStartingNewSessionsOnTaggedEC2Instances",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ec2:*:account-id:instance/*",
"Condition": {
"StringEquals": {
"ssm:resourceTag/Environment": "Production",
"ssm:resourceTag/Name": "MyWebsiteProductionAppServerAsg"
}
}
},
{
"Sid": "AllowTerminatingOwnSessions",
"Effect": "Allow",
"Action": "ssm:TerminateSession",
"Resource": "arn:aws:ssm:*:account-id:session/*",
"Condition": {
"StringLike": {
"ssm:resourceTag/aws:ssmmessages:session-id": "${aws:username}"
}
}
}
]
}
Remember to replace "account-id" in the above document with the ID of your AWS account!
If you are using a federated identity (such as GitHub's OpenID Connect provider), then you will need to replace
${aws:username}
with${aws:userid}
- see here for more.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run
rake spec
to run the tests. You can also run bin/console
for an interactive
prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To
release a new version, update the version number in version.rb
, and then run
bundle exec rake release
, which will create a git tag for the version, push
git commits and the created tag, and push the .gem
file to
rubygems.org.
Contributing
Contributions are welcome. Please see the contribution guidelines for detailed instructions.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in this project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.