はじめに
この記事では「[AWS] Private Subnet内のEC2インスタンスにローカルPCのターミナルからSession Managerでアクセスする」の内容を改善し、もっと簡単に環境を構築できる手順を解説します。
そして、何がセキュアかというと、Private Subnetに配置したEC2インスタンスに直接接続できる、という点に注目したいと思います。そう、踏み台サーバが不要なのです。
事前準備
まず、最初にクライアント環境に以下のツールが必要ですので、必要に応じてご用意ください。
また、AWSのクレデンシャル情報も事前に設定してくおいてください。
環境変数
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
もし必要であれば、
AWS_DEFAULT_REGION
も設定してください。
もしくは、
$ aws configure
で設定しても、どちらでも構いません。
キーペアの作成
クライアントからssh/scpでアクセスするためのキーペアを作成します。
この手順は、既にキーペアが存在し、ローカル環境に.pemファイルをダウンロード済みの場合はスキップして構いません。
$ aws ec2 create-key-pair --key-name [キーペア名] --query 'KeyMaterial' --output text > [ローカル保存する.pemファイル名]
例
$ aws ec2 create-key-pair --key-name ssm-key --query 'KeyMaterial' --output text > ssm-key.pem
作成したファイルは、アクセス権を変更しておいてください。
$ chmode 400 ssm-key.pem
CloudFormationの実行
テンプレート
構成概説
以下テンプレートは、以下の環境を構築します。
- VPC
- Subnet(Private Subnetとして作成)
- ルートテーブル(EndPointにのみルーティング)
- セキュリティグループ(443のみ許可)
- エンドポイント(ssm、ssmmessages、ec2messages、s3)
- EC2インスタンス
- EBS
AWSTemplateFormatVersion: "2010-09-09"
Description:
Create Session Manager Environment
Metadata:
"AWS::CloudFormation::Interface":
ParameterGroups:
- Label:
default: "Project Name Prefix"
Parameters:
- ProjectPrefix
- Label:
default: "Network Configuration"
Parameters:
- VPCCIDR
- PrivateSubnetCIDR
- Label:
default: "EC2"
Parameters:
- AmiID
- InstanceType
- KeyPair
ParameterLabels:
VPCCIDR:
default: "VPC CIDR"
PrivateSubnetCIDR:
default: "PrivateSubnet CIDR"
Parameters:
ProjectPrefix:
Type: String
Default: "SSM-ACCESS-ENV"
VPCCIDR:
Type: String
Default: "10.1.0.0/16"
PrivateSubnetCIDR:
Type: String
Default: "10.1.100.0/24"
AmiID:
Type: String
Default: "ami-0053d11f74e9e7f52"
InstanceType:
Type: String
Default: "t3.xlarge"
KeyPair:
Type: String
Default: "ssm-key"
Resources:
VPC:
Type: "AWS::EC2::VPC"
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsSupport: "true"
EnableDnsHostnames: "true"
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub "${ProjectPrefix}-vpc"
PrivateSubnet:
Type: "AWS::EC2::Subnet"
Properties:
AvailabilityZone: !Sub "${AWS::Region}a"
CidrBlock: !Ref PrivateSubnetCIDR
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${ProjectPrefix}-private-subnet"
PrivateRouteTable:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${ProjectPrefix}-private-route"
PrivateSubnetRouteTableAssociation:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
SubnetId: !Ref PrivateSubnet
RouteTableId: !Ref PrivateRouteTable
PrivateSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription : "Private SecurityGroup"
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${ProjectPrefix}-security-group"
SecurityGroupIngress:
- IpProtocol: "tcp"
FromPort: "443"
ToPort: "443"
CidrIp: !Ref VPCCIDR
SecurityGroupEgress:
- CidrIp: "127.0.0.1/32"
IpProtocol: "-1"
EndPointSSM:
Type: "AWS::EC2::VPCEndpoint"
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref PrivateSecurityGroup
ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm"
SubnetIds:
- !Ref PrivateSubnet
VpcEndpointType: "Interface"
VpcId: !Ref VPC
EndPointSSMMessages:
Type: "AWS::EC2::VPCEndpoint"
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref PrivateSecurityGroup
ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages"
SubnetIds:
- !Ref PrivateSubnet
VpcEndpointType: "Interface"
VpcId: !Ref VPC
EndPointEC2Messages:
Type: "AWS::EC2::VPCEndpoint"
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !Ref PrivateSecurityGroup
ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages"
SubnetIds:
- !Ref PrivateSubnet
VpcEndpointType: "Interface"
VpcId: !Ref VPC
EndPointS3:
Type: "AWS::EC2::VPCEndpoint"
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
VpcEndpointType: "Gateway"
VpcId: !Ref VPC
EC2IAMRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: !Sub "${ProjectPrefix}-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- "ec2.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
EC2InstanceProfile:
Type: "AWS::IAM::InstanceProfile"
Properties:
Path: "/"
Roles:
- Ref: EC2IAMRole
InstanceProfileName: !Sub "${ProjectPrefix}-PROFILE"
EC2SecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription : "EC2 SecurityGroup"
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${ProjectPrefix}-ec2-security-group"
SecurityGroupIngress:
- IpProtocol: "tcp"
FromPort: "22"
ToPort: "22"
CidrIp: "0.0.0.0/0"
EC2Instance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType
SubnetId: !Ref PrivateSubnet
ImageId: !Ref AmiID
SecurityGroupIds:
- !Ref EC2SecurityGroup
IamInstanceProfile: !Ref EC2InstanceProfile
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: 100
VolumeType: gp2
EbsOptimized: true
SourceDestCheck: true
KeyName: !Ref KeyPair
Tags:
- Key: Name
Value: !Sub "${ProjectPrefix}-ec2"
Outputs:
VPC:
Value: !Ref VPC
Export:
Name: !Sub "${ProjectPrefix}-vpc"
VPCCIDR:
Value: !Ref VPCCIDR
Export:
Name: !Sub "${ProjectPrefix}-vpc-cidr"
PrivateSubnet:
Value: !Ref PrivateSubnet
Export:
Name: !Sub "${ProjectPrefix}-private-subnet"
PrivateRouteTable:
Value: !Ref PrivateRouteTable
Export:
Name: !Sub "${ProjectPrefix}-private-route"
PrivateSecurityGroup:
Value: !Ref PrivateSecurityGroup
Export:
Name: !Sub "${ProjectPrefix}-private-securitygroup"
EndPointSSM:
Value: !Ref EndPointSSM
Export:
Name: !Sub "${ProjectPrefix}-endpoint-ssm"
EndPointSSMMessages:
Value: !Ref EndPointSSMMessages
Export:
Name: !Sub "${ProjectPrefix}-endpoint-ssmmessages"
EndPointEC2Messages:
Value: !Ref EndPointEC2Messages
Export:
Name: !Sub "${ProjectPrefix}-endpoint-ec2messages"
EndPointS3:
Value: !Ref EndPointS3
Export:
Name: !Sub "${ProjectPrefix}-endpoint-s3"
EC2SecurityGroup:
Value: !Ref EC2SecurityGroup
Export:
Name: !Sub "${ProjectPrefix}-ec2-security-group"
EC2Instance:
Value: !Ref EC2Instance
Export:
Name: !Sub "${ProjectPrefix}-ec2"
以下の内容をファイルに保存してください。以降、テンプレートファイル名を「ssmenv.yml」という前提で記述します。
スタックの作成
$ aws cloudformation create-stack --stack-name SSMEnvStack --template-body file://ssmenv.yml --capabilities CAPABILITY_NAMED_IAM
{
"StackId": "arn:aws:cloudformation:ap-northeast-1:767054379442:stack/DPM-DATA-MIGRATION-ENV-Stack/0e0b65d0-f268-11ea-be88-06e06928d272"
}
もし、KeyPareを、上記で作成したものと違うものを指定する場合は、コマンドの最後に
--parameters ParameterKey=KeyPair,ParameterValue="キーペア名"
のようにしてデフォルトのパラメータ名を上書きしてください。
他に上書き可能なパラメータは、以下の通りです。
パラメータ名 | 設定値 | デフォルト値 |
---|---|---|
ProjectPrefix | 各リソースに設定する名称の接頭子 | SSM-ACCESS-ENV |
VPCCIDR | VPCのCIDR | 10.1.0.0/16 |
PrivateSubnetCIDR | SubnetのCIDR | 10.1.100.0/24 |
AmiID | EC2のAMI ID | ami-0053d11f74e9e7f52 |
InstanceType | EC2のインスタンスタイプ | t3.xlarge |
KeyPair | EC2のキーペア名 | ssm-key |
なお、AMIは、Amazon Linux 2 64bitを前提にしていますが、リージョンによってはIDが異なります。上記は、ap-northeast-1が前提です。
また、上記以外のEBSのボリュームサイズなどは、適宜テンプレートを修正してください。
sshの準備
sshコマンドでSession Manager経由でEC2インスタンスに接続するためには
ホームディレクトリ/.ssh/config
に、以下の記述を行います。
host i-* mi-*
ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
ssh実行
sshでEC2インスタンスに接続するには、以下の構文で行います。
$ ssh -i [pemファイル] ec2-user@[EC2インスタンスのインスタンスID]
です。
例
$ ssh -i ssm-key.pem ec2-user@i-0af4afe4df0b3900b
__| __|_ )
_| ( / Amazon Linux 2 AMI
___|\___|___|
https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-10-1-100-148 ~]$
EC2インスタンスのインスタンスID
以下のいずれかの方法で取得できます。
-
マネジメントコンソールのEC2のインスタンスの一覧から取得する
-
AWS Cliでインスタンス一覧にフィルターをかけて取得する
$ aws ec2 describe-instances --filters "Name=tag:Name,Values={ProjectPrefixの値}-ec2" --query "Reservations[].Instances[].InstanceId"
例
$ aws ec2 describe-instances --filters "Name=tag:Name,Values=SSM-ACCESS-ENV-ec2" --query "Reservations[].Instances[].InstanceId"
[
"i-0af4afe4df0b3900b"
]
```
まとめ
CloudFormationで構成しているため、スタックを削除すると、全て綺麗に消えてくれます。
今回はVPCごと作成するようになっていますが、少しテンプレートをいじることで、既存のVPCに接続することももちろんできます。
その場合は、EndPointの接続先になるサブネットなどにご注意ください。