ENECHANGE Developer Blog

ENECHANGE開発者ブログ

ENECHANGE での ec2ssh による .ssh/config の自動設定

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 を利用する方法を紹介しました。