ENECHANGE に勤務している cuzic こと 川西です。
最近は、子供(6歳)とマインクラフトで遊んだりしています。
私は 1時間ほどゲームするだけで、ヘトヘトで、グッタリです。
子どもはずっと遊んでいて、すごいな、と思います。
はじめに
ENECHANGE はサーバインフラとして全面的に AWS を採用しています。
ENECHANGE は多数のプロジェクトが同時並行で進行しています。プロジェクトごとに顧客、ステークホルダーが異なる関係上、 AWS の VPC という機能を利用して、ネットワークを個別に構築し、互いの疎通ができないようにセキュリティの高いネットワークインフラを構築しています。
サーバの SSH キーについても、セキュリティを十分に担保するため、プロジェクトごとに個別に設定しており、あるプロジェクトのサーバ群の SSH キーを持っていても、他のプロジェクトのサーバにはログインできないようにしています。
VPC (ネットワーク)が分割しているのに関連して、踏み台(bastion)サーバも複数あり、どのプロジェクトのサーバにログインしたいかによって使うべき踏み台サーバを切り替える必要があるように設計しています。これにより相互に通信ができないようにしており、たとえ万一の事態でも被害を極小化・限定的になるように設計しています。
このようなネットワーク設計、SSHキーの運用は、セキュリティの確保を目的としたものですが、複数のプロジェクトに関わるメンバーにとっては、プロジェクトごとに異なる SSH キー、異なる踏み台(bastion)サーバを使い分ける必要があり、利便性を犠牲にした設計となっていました。
■ .ssh/config の利用
.ssh/config という設計ファイルを記述することで、ある程度ログインで使う踏み台サーバ、SSH キー自動判別して適切に利用することができます。
例えば、
Host project-a-web-server HostName 172.xx.xxx.xxx ProxyCommand ssh -W %h:%p project-a-bastion user ec2-user IdentityFile /home/cuzic/.ssh/project-a.pem
という設定を記述すれば、
ssh project-a-web-server
とするだけで、対応する踏み台(bastion)サーバ project-a-bastion 、ssh キー project-a.pem を使って、ログインすることができます。
このように .ssh/config の記述によりある程度解決することができます。
しかしながら、たとえば ENECHANGE ではオートスケーリング、Elastic Beanstalk を活用して、運用されているサーバが存在します。
そのようなサーバの場合、短いライフサイクルで、インスタンスが入れ替わり、IPアドレスもどんどん変化します。
設定ファイルを適切にメンテナンスし続けるのは大変な手間がかかります。
しかしながら、.ssh/config を適切にメンテナンスしないと、 eb ssh で、対象サーバにログインすることができず、運用が非常に面倒になってしまいます。
ec2ssh による .ssh/config の自動生成
.ssh/config の維持運用をよりラクにするため、 ENECHANGE では ec2ssh という gem を使って、解決しました。
ec2ssh とは
ec2ssh を使うことで、AWS に API で問合せして得られたインスタンスの情報を元にして、.ssh/config を自動生成することができます。 例えば、下記のように書かれた .ssh/config があったとして、
Host github.com User git IdentityFile ~/.ssh/id_rsa
そこで
ec2ssh init ec2ssh update
と実行すると、後述する .ec2ssh の設定内容に従って最新の AWS のインスタンスのメタ情報に基づいた .ssh/config を自動生成できます。
ENECHANGE で行った ec2ssh の設定
ENECHANGE では具体的には下記のような .ec2ssh ファイルを作成しました。
(一部、プロジェクト名などは書き換えています)
def get(name)
`aws configure get #{name}`.chomp
end
def aws_access_key_id
ENV.fetch('AWS_ACCESS_KEY_ID', get("aws_access_key_id"))
end
def aws_secret_access_key
ENV.fetch('AWS_SECRET_ACCESS_KEY', get("aws_secret_access_key"))
end
# Name タグが設定されていないインスタンスは無視
reject do |instance|
!instance.tag('Name')
end
# running と stopped の状態のインスタンスに限定
filters([
{ name: 'instance-state-name', values: ['running', 'stopped'] }
])
aws_keys(
default: {
'ap-northeast-1' =>
Aws::Credentials.new(
aws_access_key_id,
aws_secret_access_key
)
},
)
# 下記、実際の適用例は、もう少し複雑ですが、説明のために簡略化しています
$bastions = Hash[*%w(
project-a project-a-bastion
project-b project-b-bastion
project-c project-c-bastion
)]
# ec2ssh では下記の host_line に `<<` した内容が .ssh/config に書き出されます。
host_line <<'END'
<%-
# ENECHANGE では Service というタグに、どのプロジェクト向けかの識別子が格納されている運用になっています。
bastion = $bastions.fetch(tag("Service"), "common-bastion")
key = ["", ".pem"].map do |ext|
# key_name は、 `Aws::EC2::Instance` クラスのメソッドです。
# ほか、public_dns_name、public_ip_address なども同様に、 `Aws::EC2::Instance` クラスのメソッドです。
"#{ENV['HOME']}/.ssh/#{key_name}#{ext}"
end.find do |filename|
File.file?(filename)
end
-%>
<%- if key -%>
<% if !public_dns_name.empty? %>
Host <%= tag('Name') %> <%= public_dns_name %>
hostname <%= public_ip_address %>
Match host <%= public_ip_address %>
user ec2-user
IdentityFile <%= key %>
<% else %>
Host <%= tag('Name') %> <%= private_dns_name %>
HostName <%= private_ip_address %>
Match host <%= private_ip_address %>
ProxyCommand ssh -W %h:%p <%= bastion %>
user ec2-user
IdentityFile <%= key %>
<% end %>
<%- end -%>
END
このような設定を書くことで、 たとえば、下記のような .ssh/config ファイルが生成されます。
# public_dns_name がある場合 Host project-a-bastion ec2-54-95-xxx-yyy.ap-northeast-1.compute.amazonaws.com hostname 54.95.xxx.yyy Match host 54.95.xxx.yyy user ec2-user IdentityFile ~/.ssh/summit-oem.pem # public_dns_name がない場合 Host project-a-private-server ip-172-25-xxx-yyy.ap-northeast-1.compute.internal HostName 172.25.xxx.yyy Match host 172.25.xxx.yyy ProxyCommand ssh -W %h:%p project-a-bastion user ec2-user IdentityFile ~/.ssh/project-a.pem
このような設定ファイルを記述することで、eb ssh 等でも SSH できるようになります。
内部的には下記のような動作が行われます。
eb ssh を実行する
`ip-172-25-xxx-yyy.ap-northeast-1.compute.internal` に SSH が行われる
Host、Hostname の config 設定により 172.25.xxx.yyy に変換される
172.25.xxx.yyy に Match するため、 project-a の bastion 、SSH キーの設定が適用される
終わりに
この記事では、プロジェクトごとに SSH キー、踏み台(bastion)サーバを個別に設定する必要がある状況で、 .ssh/config を自動的に生成・管理するため、 ec2ssh gem を利用する方法を紹介しました。