この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
本日の課題
こんにちは植木和樹です。先日Sophos-UTMを使ったVPNサーバーをご紹介しました。EC2上にVPNサーバーを構築することで、外部からSSL-VPNで接続できるようにはなったのですが、この構成には問題があり、Sophosサーバーに障害があった場合VPN接続ができなくなってしまいます。
Sophosのハードウェアアプライアンス製品では、冗長構成用ネットワークポートを用いてActive-Activeや、Active-StandbyといったHA構成にすることができます。しかしAWSのMarketPlaceで提供されているSophos-UTMソフトウェアではこの機能が使えません。
そこで本日はSPOF(Single Point of Failure:単一障害点)となってしまっているサーバーをAZをまたいだ冗長構成にしたいと思います。堅牢なシステム構成のお手本といえば「CDP(Cloud Design Pattern)」ですね。今回は次の3つを利用します。
動作と構成図
正常時
モバイル端末はSophosサーバーとSSL-VPN接続を行います。SSL-VPN接続が確立すると、モバイル端末はSophosサーバーから10.242.2.0/24のIPアドレスを割り当てられServerと通信します。ServerはRouteTableで設定したルーティング情報に基いてレスポンスをSophosサーバーに返します。こうしてモバイル端末とServerはVPN越しに通信することができています。
異常時
AZ-aのSophosサーバーに異常が発生した場合はAZ-cのSophosサーバーを起動し、EIPを付け替えます。さらにRouteTableを書き換え10.242.2.0/24宛のパケットを新しいSophosサーバーに返すようにします。
CDPとの対応は次のようになります。
- AMIの取得/新しいサーバーの起動・・・Stampパターン(サーバの複製)
- EIPの付け替え・・・Floating IPパターン(IPアドレスの動的な移動)
- RouteTableの書き換え・・・High Availability NATパターン(冗長化されたNATインスタンス)
実現方法(aws-cliサンプルスクリプト)
異常発生時の各種処理にはaws-cliを使用します。シェルスクリプトにしておけば監視システムに組み込め、自動化しやすいためです。
必要なもの
- aws-cli/1.2.0 (1.0 以上なら問題ないと思います)
- jq/1.2
事前に定義する変数
対象のリソースを探すためにタグを活用しています。検索するタグの値は以下のように定義しておきます。
# SophosにつけるEIP
FLOATING_IP=54.XXX.XXX.XXX
# SSL-VPN接続時にクライアントに割り当てるIPアドレスレンジ
SSL_VPN_CIDR=10.242.2.0/24
# VPCのタグ(Name)
VPC_NAME=my-vpc
# VPCサブネットのタグ(Name)
SUBNET_NAME=public
# Sophos EC2インスタンスのタグ(Name)
EC2_NAME=sophos
# 取得したAMIにつけるタグ(Name)
AMI_NAME=sophos
# Sophos EC2インスタンスに設定するセキュリティグループ名
GROUP_NAME=sophos
AMIの取得
AMIの取得は次のように行います。
<
ol>
# EC2インスタンスをタグで検索してInstance-IDを取得する
instance_id=$(aws ec2 describe-instances | \
jq -s -r '.[].Reservations[].Instances[] |
select(
.Tags[].Key == "Name"
and .Tags[].Value == "'${EC2_NAME}'"
and .State.Name == "running"
) | .InstanceId')
# AMIを取得する
aws ec2 create-image --instance-id ${instance_id} --name sophos_$(date +'%Y%m%d')
# 取得したAMIにタグをつける
aws ec2 create-tags --resources ${ami_id} --tags Key=Name,Value=${AMI_NAME}
新しいサーバーの起動
新しいサーバーの起動は次のように行います。
- 事前にEC2インスタンスにタグをつけておく
- EC2インスタンスをタグで検索してInstance-IDを取得する
- Instance-IDを指定して古いインスタンスを停止する
- 取得済みのAMIから新しいEC2を別AZで起動する
# EC2インスタンスをタグで検索してInstance-IDを取得する
instance_id=$(aws ec2 describe-instances | \
jq -s -r '.[].Reservations[].Instances[] |
select(
.Tags[].Key == "Name"
and .Tags[].Value == "'${EC2_NAME}'"
and .State.Name == "running"
) | .InstanceId')
# Instance-IDを指定して古いインスタンスを停止する
aws ec2 stop-instances --instance-ids ${instance_id}
# 現在のAZを調べる
current_az=$(aws ec2 describe-instances | \
jq -s -r '.[].Reservations[].Instances[] |
select(.InstanceId == "'${instance_id}'") |
.Placement.AvailabilityZone')
# EC2を起動するAZを変更する
placement=$(echo "ap-northeast-1a" | tr 'ac' 'ca')
# 取得済みのAMIのIDを取得する
ami_id=$(aws ec2 describe-images --owners self | \
jq -r '.Images[] |
select(
.Tags[].Key == "Name"
and .Tags[].Value == "'${AMI_NAME}'"
) | .ImageId')
# 取得済みのAMIから新しいEC2を別AZで起動する
new_instance_id=$(aws ec2 run-instances \
--image-id ${ami_id} \
--placement ${placement} | \
jq -r '.Instances[0].InstanceId')
# EC2にタグをつける
aws ec2 create-tags --resources ${new_instance_id} --tags Key=Name,Value=${EC2_NAME}
# セキュリティグループIDを取得する
group_id=$(aws ec2 describe-security-groups | \
jq -r '.SecurityGroups[] |
select(.GroupName == "'${GROUP_NAME}'") |
.GroupId')
# 新しいEC2にセキュリティグループを設定する
aws ec2 modify-instance-attribute --instance-id ${new_instance_id} --groups ${group_id}
# 新しいEC2のSourceDestCheckを無効にする(VPNゲートウェイとして動くように)
aws ec2 modify-instance-attribute \
--instance-id ${new_instance_id}} \
--source-dest-check '{ "Value" : false }'
EIPの付け替え
EIPの付け替えは次のように行います。
- 事前に決めたフローティングIPアドレスからEIPのAllocation-IDを取得する
- 新しいEC2インスタンスにEIPを紐付ける
# 事前に決めたフローティングIPアドレスからEIPのAllocation-IDを取得する
allocation_id=$(aws ec2 describe-addresses | \
jq -r '.Addresses[] |
select(.PublicIp == "'${FLOATING_IP}'") |
.AllocationId')
# EC2インスタンスをタグで検索してInstance-IDを取得する
instance_id=$(aws ec2 describe-instances | \
jq -s -r '.[].Reservations[].Instances[] |
select(
.Tags[].Key == "Name"
and .Tags[].Value == "'${EC2_NAME}'"
and .State.Name == "running"
) | .InstanceId')
# 新しいEC2インスタンスにEIPを紐付ける
aws ec2 associate-address \
--allocation-id ${allocation_id} \
--instance-id ${instance_id}
RouteTableの書き換え
RouteTableの書き換えは次のように行います。
- 事前にVPCにタグをつけておく
- 事前にVPCサブネットにタグ(サブネット名)をつけておく
- VPCをタグで検索しVPC-IDを取得する
- VPC-IDとVPCサブネット名でサブネットに紐付いたRouteTableを検索してRouteTable-IDを取得する
- RouteTableのRouteを書き換える
# VPCをタグで検索しVPC-IDを取得する
vpc_id=$(aws ec2 describe-vpcs | \
jq -r '.Vpcs[] |
select(
.Tags[].Key == "Name"
and .Tags[].Value == "'${VPC_NAME}'"
) | .VpcId')
# VPC-IDとVPCサブネット名でサブネットに紐付いたRouteTableを検索してRouteTable-IDを取得する
routetable_id=$(aws ec2 describe-route-tables | \
jq -r '.RouteTables[] |
select(
.VpcId == "'${vpc_id}'"
and .Tags[].Key == "Name"
and .Tags[].Value == "'${SUBNET_NAME}'"
) | .RouteTableId')
# RouteTableのRouteを書き換える
aws ec2 replace-route \
--route-table-id ${routetable_id} \
--destination-cidr-block ${SSL_VPN_CIDR} \
--instance-id ${new_instance_id}
まとめ
上記のシェルスクリプトをひとつずつ実行することで、あるアベイラビリティゾーンで動いていたSophosサーバーを、異なるアベイラビリティゾーンで起動しなおし、VPN接続を再度確立することができました。
単純な例ですがCDPとaws-cliの実例として参考になればと思います。
なお実際に運用で使用するためには、適切なタイミングでのAMI自動取得や、サーバーの異常検知→スクリプト自動実行の仕組みを検討しなければなりません。Zabbixなどの統合監視ツールなどとの連携も考慮しながら運用の自動化を進めていただければと思います。