After using
Terraform
for a while, it became a little tedious to
keep updating ~/.ssh/config every time something changed
on my EC2 instances.
I wondered if this could somehow be automated from within Terraform. Here’s the little solution I came up with.
Given a list of host structures this little module writes an SSH
configuration file in ~/.ssh. It supports using either plain SSH or
SSH tunnelled over SSM Session Manager. The host SSH port is also
configurable since, for reasons, I sometimes need to use a
non-standard port number.
This solution assumes some flavour of Unix (MacOS in my case), Terraform, AWS, and having an AWS CLI profile configured.
Usage
Assuming variables.tf and main.tf are in a module called ssh-config,
and the modules host-web1 and host-web2 return suitable host
data structures, you’d use it something like this:
module "ssh-config" {
source = "../ssh-config"
config_prefix = "web"
aws_profile = "web_profile"
hosts = [module.host-web1.host, module.host-web2.host]
}The generated configuration (~/.ssh/web_config) will look something like:
# Generated by Terraform
Host web1
IdentityFile ~/.ssh/web.pem
User ec2-user
ProxyCommand sh -c "aws --profile web_profile ssm start-session --target i-a89d4be23b593efc8 --document-name AWS-StartSSHSession --parameters 'portNumber=22'"
Host web2
IdentityFile ~/.ssh/web.pem
User ec2-user
HostName web2.example.org
Port 22This can be included in the default ~/.ssh/config config file
with an include clause: include web_config.
variables.tf
config_prefix is used as the first part of the name of the
SSH configuration file, which is created as ~/.ssh/<config_prefix>_config.
aws_profile is the name of the
AWS profile
used to setup the SSM Session Manager tunnel. It’s not used if you’re not
tunnelling SSH over SSM.
hosts is a list of Terraform objects. I hope the attribute names are
all fairly self-explanatory. I have another custom module to
create EC2 instances which returns this structure, so everything fits
together nicely.
# Copyright (c) 2022 Cinnabar Services Pty Ltd. All Rights Reserved.
variable "config_prefix" {
description = "Prefix to add to ssh config file"
type = string
}
variable "aws_profile" {
description = "AWS profile to use for tunnelling SSH over SSM"
type = string
default = ""
}
variable "hosts" {
description = "List of hosts"
type = list(object({
instance_id : string,
hostname : string,
public_ip : string,
ssh_port : number,
ssh_over_ssm : bool,
private_key_file : string,
user : string
}
))
}main.tf
This file creates the actual SSH config file ‘resource’. Within the ‘here document’ template, it iterates over the list of host structures and generates the appropriate configuration for each host.
# Copyright (c) 2022 Cinnabar Services Pty Ltd. All Rights Reserved.
# Write out ssh config for hosts
resource "local_file" "ssh_config" {
filename = pathexpand(
"~/.ssh/${var.config_prefix}_config"
)
file_permission = "0644"
content = <<EOT
# Generated by Terraform
%{for host in var.hosts}
Host ${host.hostname}
IdentityFile ${host.private_key_file}
User ${host.user}
%{ if host.ssh_over_ssm }
ProxyCommand sh -c "aws --profile ${var.aws_profile} ssm start-session --target ${host.instance_id} --document-name AWS-StartSSHSession --parameters 'portNumber=${host.ssh_port}'"
%{ else }
HostName ${host.public_ip}
Port ${host.ssh_port}
%{ endif }
%{endfor}
EOT
}Note
The SSH config file is created with permissions
0644. This shouldn’t be an issue since it only references the private keys (which should be restricted to0600), but you may opt for a more restrictive mode.
This probably isn’t a drop-in module that you can immediately use in your own Terraform setups, but I hope the general technique is useful.