この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
おはようございます、もきゅりんです。
今回は特にひねりもなく、下記弊社ブログ記事で紹介された構成をCloudFormation一撃で構築するという内容になります。
構成は下図です。
構成内容
- NLB用のパブリックサブネット
- VPCエンドポイント
- VPCエンドポイント用のセキュリティグループ
- NLBのログ用のS3バケット
- SFTPのためのS3バケット
- SFTPサーバ
- SFTPサーバのログロール
- SFTPサーバを利用するユーザーのロール
- NLB
- VPCEndpointのNetworkInterfaceIdsからPrivateIPを取得するためのカスタムリソースのLambda関数
- 指定IPからのSSH接続以外を拒否するNACL
前提
利用するにあたっての前提条件は以下です。
- VPCおよびプライベート、パブリックルートテーブルは構築済みであること
(OutputsでVPCIDなどを出力していることを前提としていますが、実際に利用する際はうまく微調整して頂ければと思います。) - NACLはサブネットが対象のため、影響範囲が広いのでNLB用のサブネットを別で作成しています。
- 出力先S3バケット名を指定しますが、バケット名にAWSアカウント番号を付与します。
- Cloudformation削除時はS3バケット内のファイルを削除済みにしないとS3バケット削除できません。必要に応じて、DeletionPolicyをつけて削除除外してください。
パラメータ
以下を指定する仕様となります。
- NLBを配置するサブネットのCIDR表記
- SSH接続を許可するIP
テンプレート
AWSTemplateFormatVersion: 2010-09-09
Description: S3 and Access Restricted AWS Transfer for SFTP
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
NameTagPrefix:
Type: String
Default: test
Description: Prefix of Name tags.
LowerNameTagPrefix:
Type: String
Default: test
Description: Prefix of Lowercase Name tags For S3 BucketName.
ENV:
Type: String
Default: stg
Description: Prefix of Env tags.
NLBSubnet1CidrBlock:
Type: String
Description: NLBSubnet1 CidrBlock.
NLBSubnet2CidrBlock:
Type: String
Description: NLBSubnet2 CidrBlock.
AllowIP:
Type: String
Description: Allowed IP
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
# Subnets
# ------------------------------------------------------------#
NLBSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [0, 'Fn::GetAZs': { Ref: 'AWS::Region' }]
CidrBlock: !Ref NLBSubnet1CidrBlock
Tags:
- Key: Name
Value: !Sub ${NameTagPrefix}-${ENV}-NLBSubnet1
VpcId:
Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc'
NLBSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref NLBSubnet1
RouteTableId:
Fn::ImportValue: !Sub ${NameTagPrefix}-${ENV}-public-rtb
NLBSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [1, 'Fn::GetAZs': { Ref: 'AWS::Region' }]
CidrBlock: !Ref NLBSubnet2CidrBlock
Tags:
- Key: Name
Value: !Sub ${NameTagPrefix}-${ENV}-NLBSubnet2
VpcId:
Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc'
NLBSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref NLBSubnet2
RouteTableId:
Fn::ImportValue: !Sub ${NameTagPrefix}-${ENV}-public-rtb
# ------------------------------------------------------------#
# SecurityGroup
# ------------------------------------------------------------#
SFTPEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub '${NameTagPrefix}-${ENV}-SFTP-VPCEndpoint-sg'
GroupDescription: !Sub '${NameTagPrefix}-${ENV}-SFTP-VPCEndpoint-sg'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref NLBSubnet1CidrBlock
Description: SSH From NLBSunbnet1
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref NLBSubnet2CidrBlock
Description: SSH From NLBSunbnet2
VpcId:
Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc'
Tags:
- Key: Name
Value: !Sub '${NameTagPrefix}-${ENV}-SFTP-VPCEndpoint-sg'
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------#
VPCTransferForSFTPEndpoint:
Type: AWS::EC2::VPCEndpoint
DependsOn: SFTPEndpointSecurityGroup
Properties:
VpcEndpointType: Interface
SubnetIds:
- !Ref NLBSubnet1
- !Ref NLBSubnet2
SecurityGroupIds:
- !Ref SFTPEndpointSecurityGroup
ServiceName: !Sub com.amazonaws.${AWS::Region}.transfer.server
VpcId:
Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc'
# # ------------------------------------------------------------#
# # S3 Bucket For SFTP
# # ------------------------------------------------------------#
HomeS3bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-sftp-for-test
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
# # ------------------------------------------------------------#
# # IAM Role
# # ------------------------------------------------------------#
# SFTPサーバに適用するIAMRole
SFTPRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: transfer.amazonaws.com
Action: sts:AssumeRole
SFTPPolicy:
Type: 'AWS::IAM::Policy'
Properties:
PolicyName: SFTPPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetBucketLocation
Resource: !GetAtt HomeS3bucket.Arn
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObjectVersion
- s3:DeleteObject
- s3:GetObjectVersion
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref HomeS3bucket
- /*
Roles:
- !Ref SFTPRole
SFTPLogRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: transfer.amazonaws.com
Action: sts:AssumeRole
SFTPLogPolicy:
Type: 'AWS::IAM::Policy'
Properties:
PolicyName: CWLForSFTPPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:DescribeLogStreams
- logs:CreateLogGroup
- logs:PutLogEvents
Resource: '*'
Roles:
- !Ref SFTPLogRole
# CustomResourceのLambdaに適用するIAMRole
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
LambdaPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: LambdaPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ec2:*
- logs:*
Resource: '*'
Roles:
- !Ref LambdaRole
# # ------------------------------------------------------------#
# # SFTP Server
# # ------------------------------------------------------------#
SFTPServer:
Type: AWS::Transfer::Server
Properties:
EndpointType: VPC_ENDPOINT
EndpointDetails:
VpcEndpointId: !Ref VPCTransferForSFTPEndpoint
IdentityProviderType: SERVICE_MANAGED
LoggingRole: !GetAtt SFTPLogRole.Arn
# # ------------------------------------------------------------#
# # S3 Bucket For NLB Logs
# # ------------------------------------------------------------#
S3NLBLogBacket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-nlblog
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
Tags:
- Key: Name
Value: !Ref ENV
LifecycleConfiguration:
Rules:
- Id: !Sub ${LowerNameTagPrefix}-${ENV}-Log-Rules
Status: Enabled
ExpirationInDays: 400
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-nlblog
PolicyDocument:
Statement:
- Sid: 'AWSLogDeliveryWrite'
Effect: 'Allow'
Principal:
Service: delivery.logs.amazonaws.com
Action:
- 's3:PutObject'
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-nlblog
- '/*'
Condition:
StringEquals:
s3:x-amz-acl: bucket-owner-full-control
- Sid: AWSLogDeliveryAclCheck
Effect: 'Allow'
Principal:
Service: delivery.logs.amazonaws.com
Action:
- 's3:GetBucketAcl'
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- !Sub ${LowerNameTagPrefix}-${ENV}-${AWS::AccountId}-nlblog
# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
LambdaFunction:
Type: 'AWS::Lambda::Function'
DeletionPolicy: 'Delete'
Properties:
Code:
ZipFile: !Sub |
import cfnresponse
import json
import boto3
def lambda_handler(event, context):
print('REQUEST RECEIVED:\n' + json.dumps(event))
responseData = {}
if event['RequestType'] == 'Delete':
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
return
if event['RequestType'] == 'Create':
try:
ec2 = boto3.resource('ec2')
enis = event['ResourceProperties']['NetworkInterfaceIds']
for index, eni in enumerate(enis):
network_interface = ec2.NetworkInterface(eni)
responseData['IP' + str(index)] = network_interface.private_ip_address
print(responseData)
except Exception as e:
responseData = {'error': str(e)}
cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
return
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
Handler: index.lambda_handler
Role: !GetAtt LambdaRole.Arn
Runtime: python3.7
Timeout: 10
# # ------------------------------------------------------------#
# # Custom Resource
# # ------------------------------------------------------------#
GetPrivateIPs:
DependsOn:
- VPCTransferForSFTPEndpoint
Type: Custom::GetPrivateIPs
Properties:
ServiceToken: !GetAtt LambdaFunction.Arn
NetworkInterfaceIds: !GetAtt VPCTransferForSFTPEndpoint.NetworkInterfaceIds
# # ------------------------------------------------------------#
# # NLB
# # ------------------------------------------------------------#
NlbEIP1:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NlbEIP2:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NLB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
DependsOn: NlbEIP2
Properties:
Name: !Sub ${NameTagPrefix}-${ENV}-nlb
LoadBalancerAttributes:
- Key: access_logs.s3.enabled
Value: true
- Key: access_logs.s3.bucket
Value: !Ref 'S3NLBLogBacket'
- Key: access_logs.s3.prefix
Value: !Sub '${NameTagPrefix}-${ENV}-nlb'
SubnetMappings:
- AllocationId: !GetAtt 'NlbEIP01.AllocationId'
SubnetId: !Ref 'NLBSubnet1'
- AllocationId: !GetAtt 'NlbEIP2.AllocationId'
SubnetId: !Ref 'NLBSubnet2'
Tags:
- Key: ENV
Value: !Ref ENV
Type: network
NLBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
TargetType: ip
Targets:
- Id: !GetAtt GetPrivateIPs.IP0
- Id: !GetAtt GetPrivateIPs.IP1
Port: 22
Protocol: TCP
VpcId:
Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc'
NLBListenerSSH:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref 'NLBTargetGroup'
LoadBalancerArn: !Ref 'NLB'
Port: '22'
Protocol: TCP
# # ------------------------------------------------------------#
# # NACL
# # ------------------------------------------------------------#
NLBNACL:
Type: AWS::EC2::NetworkAcl
Properties:
Tags:
- Key: Name
Value: !Sub '${NameTagPrefix}-${ENV}-NLB-NACL'
VpcId:
Fn::ImportValue: !Sub '${NameTagPrefix}-${ENV}-vpc'
NACLEntry100:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NLBNACL
RuleNumber: 100
Protocol: 6
PortRange:
From: '22'
To: '22'
RuleAction: allow
Egress: false
CidrBlock: !Ref AllowIP
NACLEntry101:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NLBNACL
RuleNumber: 101
Protocol: 6
PortRange:
From: '22'
To: '22'
RuleAction: allow
Egress: false
CidrBlock: !Ref NLBSubnet1CidrBlock
NACLEntry102:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NLBNACL
RuleNumber: 102
Protocol: 6
PortRange:
From: '22'
To: '22'
RuleAction: allow
Egress: false
CidrBlock: !Ref NLBSubnet2CidrBlock
NACLEntry200:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NLBNACL
RuleNumber: 200
Protocol: 6
PortRange:
From: '22'
To: '22'
RuleAction: deny
Egress: false
CidrBlock: '0.0.0.0/0'
NACLEntry300:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NLBNACL
RuleNumber: 300
Protocol: -1
PortRange:
From: '-1'
To: '-1'
RuleAction: allow
Egress: false
CidrBlock: '0.0.0.0/0'
NACLEntryOut100:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NLBNACL
RuleNumber: 100
Protocol: '-1'
RuleAction: allow
Egress: true
CidrBlock: '0.0.0.0/0'
PortRange:
From: '-1'
To: '-1'
NACLAssociation1:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref NLBSubnet1
NetworkAclId: !Ref NLBNACL
NACLAclAssociation2:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref NLBSubnet2
NetworkAclId: !Ref NLBNACL
最後に
これまでカスタムリソースにちゃんと向き合う機会がなかったため、勉強する良い機会になりました。
以上です。
どなたかのお役に立てば幸いです。