はじめに
Transfer Familyを利用したFTP連携に触れる機会があったので、これを機にまとめてみたいと思います。
目次
Transfer Familyとは
Transfer Familyを利用する際の注意事項
検証構成
構成の前提
事前準備
検証①(VPCエンドポイント経由でFTP転送するパターン)
NLBを挟む構成を考えてみた
検証②(NLBを各AZに構築したRoute53のフェールオーバー構成パターン)
所感
Transfer Familyとは
AWSユーザガイドに書かれているように一言で言うと「AWSストレージサービスとの間でファイルを送受信できる安全な転送サービスです。」です。SFTPやFTPなどのプロトコルに対応しているマネージドサービスと言う感じです。
Transfer Familyを利用する際の注意事項
FTPを利用する場合、Transfer Familyの制約がいくつかあります。利用の際はご注意ください。
パッシブモードのみサポートやイメージ/バイナリモードのみがサポートなどあります。
いざ触ってみるとこの辺にハマりました・・・(最初に制約は見るべし!)
検証構成
下記の構成を構築してみました。
構築作業は事前作業の中で記載します。
構成の前提
- VPC1とVPC2をpeeringした構成にしてプライベート環境を構築しました。
- Transfer Familyも色々な認証方法と配置先(S3/EFS)が選べますが、シンプルなカスタムIDプロバイダー(Lambda)+S3を利用した構成にしました
- 検証用のEC2からFTP操作します。
- NAT GatewayとInternet Gatewayを作ったのはEC2にモジュールをインストールに便利だったためで、FTP検証にはなくても問題ないです。
事前準備
VPC1環境構築
基本的にCloudFormationで構築しました。ググればこの辺は大体出てきます。先人の方に感謝です。
少し脱線しますが、この辺をAmazon Qに聞いてみたらフォーマットが崩れる・・・
VPC1を構築するYAML
AWSTemplateFormatVersion: 2010-09-09
Resources:
CfVPC1:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.1.0.0/16
EnableDnsSupport: "true"
EnableDnsHostnames: "true"
Tags:
-
Key: Name
Value: CfVPC1
CfVPC1PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.1.1.0/24
MapPublicIpOnLaunch: true
VpcId: !Ref CfVPC1
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ""
Tags:
- Key: Name
Value: CfVPC1PublicSubnet
CfVPC1PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.1.2.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref CfVPC1
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: ""
Tags:
- Key: Name
Value: CfVPC1PrivateSubnet
CfVPC1InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: CfVPC1InternetGateway
AttachCfInternetGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId : !Ref CfVPC1InternetGateway
VpcId: !Ref CfVPC1
CfVPC1RouteTableForPublicSubnet:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref CfVPC1
Tags:
- Key: Name
Value: CfVPC1RouteTableForPublicSubnet
CfVPC1RouteForPublicSubnet:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref CfVPC1RouteTableForPublicSubnet
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref CfVPC1InternetGateway
CfVPC1AssocciateRouteTableForPublicSubnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref CfVPC1RouteTableForPublicSubnet
SubnetId: !Ref CfVPC1PublicSubnet
NatGatewayEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
CfVPC1NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt:
- NatGatewayEIP
- AllocationId
SubnetId: !Ref CfVPC1PublicSubnet
Tags:
- Key: Name
Value: CfVPC1NatGateway
CfVPC1RouteTableForPrivateSubnet:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref CfVPC1
Tags:
- Key: Name
Value: CfVPC1RouteTableForPrivateSubnet
CfVPC1RouteForPrivateSubnet:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref CfVPC1RouteTableForPrivateSubnet
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref CfVPC1NatGateway
CfVPC1AssocciateRouteTableForPrivateSubnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref CfVPC1RouteTableForPrivateSubnet
SubnetId: !Ref CfVPC1PrivateSubnet
NW環境ができたら、検証用のEC2(linux)とVPCエンドポイント経由でアクセスできるSSM環境を構築するCloudFormationを流します。
VPC1のEC2とSSMを構築するYAML
AWSTemplateFormatVersion: 2010-09-09
Parameters:
VPCId:
Type: String
SubnetId:
Type: String
KeyPairName:
Type: String
LinuxLatestAmi:
Description: "EC2 AMI image SSM path"
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
AllowedValues:
- /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
Resources:
VPC1EC2SG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Bastion EC2 Security Group"
VpcId: !Ref VPCId
Tags:
- Key: Name
Value: private-ec2-bastion-sg
VPC1EndpointSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Bastion Endpoint Security Group"
VpcId: !Ref VPCId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref VPC1EC2SG
Tags:
- Key: Name
Value: private-endpoint-bastion-sg
IamRole:
Type: AWS::IAM::Role
Properties:
RoleName: private-ec2-bastion-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref IamRole
VPC1NewKeyPair:
Type: "AWS::EC2::KeyPair"
Properties:
KeyName: !Ref KeyPairName
VPC1EC2Instance:
Type: "AWS::EC2::Instance"
Properties:
Tags:
- Key: Name
Value: private-bastion
ImageId: !Ref LinuxLatestAmi
InstanceType: t3.medium
KeyName: !Ref VPC1NewKeyPair
DisableApiTermination: false
EbsOptimized: false
SecurityGroupIds:
- !Ref VPC1EC2SG
SubnetId: !Ref SubnetId
IamInstanceProfile: !Ref InstanceProfile
VPC1SSMVPCEndpoint:
Type: "AWS::EC2::VPCEndpoint"
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssm'
VpcId: !Ref VPCId
SubnetIds:
- !Ref SubnetId
SecurityGroupIds:
- !Ref VPC1EndpointSG
VPC1SSMMessagesVPCEndpoint:
Type: "AWS::EC2::VPCEndpoint"
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssmmessages'
VpcId: !Ref VPCId
SubnetIds:
- !Ref SubnetId
SecurityGroupIds:
- !Ref VPC1EndpointSG
VPC1EC2MessagesVPCEndpoint:
Type: "AWS::EC2::VPCEndpoint"
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ec2messages'
VpcId: !Ref VPCId
SubnetIds:
- !Ref SubnetId
SecurityGroupIds:
- !Ref VPC1EndpointSG
VPC2環境構築
こちらも同じく、CloudFormationで構築しました。
VPC2のNWを構築するYAML
AWSTemplateFormatVersion: 2010-09-09
Resources:
CfVPC2:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.2.0.0/16
EnableDnsSupport: "true"
EnableDnsHostnames: "true"
Tags:
-
Key: Name
Value: CfVPC2
CfVPC2PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.2.1.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref CfVPC2
AvailabilityZone: "ap-northeast-1a"
Tags:
- Key: Name
Value: CfVPC2PrivateSubnet1
CfVPC2PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.2.2.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref CfVPC2
AvailabilityZone: "ap-northeast-1c"
Tags:
- Key: Name
Value: CfVPC2PrivateSubnet2
CfVPC2PrivateSubnet3:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.2.3.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref CfVPC2
AvailabilityZone: "ap-northeast-1a"
Tags:
- Key: Name
Value: CfVPC2PrivateSubnet3
CfVPC2PrivateSubnet4:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.2.4.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref CfVPC2
AvailabilityZone: "ap-northeast-1c"
Tags:
- Key: Name
Value: CfVPC2PrivateSubnet4
VPC peering構築
この辺から触り慣れていないので、手作業で作りました。
VPC1とVPC2をピアリング設定を行います。
同一アカウントですのでリクエストの承認は右上のアクションタブから承認を押すだけです。
VPC1のルートテーブルにVPC2のCIDRを追加しました。
送信先 | ターゲット |
---|---|
10.2.0.0/16 | ピアリングのID |
VPC2のルートテーブルにVPC1のプライベートサブネットを追加しました。
送信先 | ターゲット |
---|---|
10.1.2.0/24 | ピアリングのID |
Route53のプライベートホストゾーン構築
検証②で利用するため、適当なプライベートホストゾーンを設定します。
Transfer Familyで利用するセキュリティグループの構築
インバウンドルールに関しては21、8192-8200ポートをアクセスさせたいところから開けておきます。
今回はVPC1のプライベートサブネットとVPC2のプライベートサブネットからアクセスできるようにしています。
アウトバウンドルールは全部開けです。
Transfer fmilyで利用するS3の構築
FTP連携されてきたファイルを保存するストレージ用にS3バケットを構築します。
特に追加の設定は必要なく、デフォルトの設定にしています。
Transfer FmailyのFTP認証で利用するIAMロールとポリシーの構築
先ほど構築したS3バケットに対してアクセスできる権限を設定をします。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::transferfamily-test-bucket",
"arn:aws:s3:::transferfamily-test-bucket/*"
]
},
{
"Effect": "Deny",
"Action": [
"s3:DeleteBucket",
"s3:CreateBucket"
],
"Resource": [
"*"
]
}
]
}
「カスタム信頼ポリシー」の設定でIAMロールを作成します。
信頼されたエンティティは下記にしています。
先ほど構築したIAMポリシーをIAMロールにアタッチします。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "transfer.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Transfer Family、VPCエンドポイント、Lambdaの構築
Transfer FamilyのカスタムIDプロバイダーを利用するスタックテンプレートはAWSからサンプル提供があるので、それを使います。
サンプルのCloudFormationにプライベート環境で利用するため、必要なネットワーク情報の追加、利用するストレージの追加、対象プロトコルを書き加えました。
Cloudformationを実行時に指定するパラメータは下記になります。
キー | 値 |
---|---|
CreateServer | True |
SecretsManagerRegion | ap-northeast-1 |
SubnetIds | VPC2の中でエンドポイントを配置したいサブネットを指定 |
VpcId | VPC2のVPCを指定 |
VPC2にTransfer Family、VPCエンドポイント、Lambdaを構築するYAML
AWSTemplateFormatVersion: '2010-09-09'
Description: A basic template that uses AWS Lambda with an AWS Transfer Family server
to integrate SecretsManager as an identity provider. It authenticates against an
entry in AWS Secrets Manager of the format SFTP/username. Additionally, the secret
must hold the key-value pairs for all user properties returned to AWS Transfer Family.
You can also modify the AWS Lambda function code to update user access.
Parameters:
CreateServer:
AllowedValues:
- 'true'
- 'false'
Type: String
Description: Whether this stack creates a server internally or not. If a server is created internally,
the customer identity provider is automatically associated with it.
Default: 'true'
SecretsManagerRegion:
Type: String
Description: (Optional) The region the secrets are stored in. If this value is not provided, the
region this stack is deployed in will be used. Use this field if you are deploying this stack in
a region where SecretsManager is not available.
Default: ''
SecurityGroup:
Type: AWS::EC2::SecurityGroup::Id
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
VpcId:
Type: AWS::EC2::VPC::Id
Conditions:
CreateServer:
Fn::Equals:
- Ref: CreateServer
- 'true'
NotCreateServer:
Fn::Not:
- Condition: CreateServer
SecretsManagerRegionProvided:
Fn::Not:
- Fn::Equals:
- Ref: SecretsManagerRegion
- ''
Outputs:
ServerId:
Value:
Fn::GetAtt: TransferServer.ServerId
Condition: CreateServer
StackArn:
Value:
Ref: AWS::StackId
Resources:
TransferServer:
Type: AWS::Transfer::Server
Condition: CreateServer
Properties:
Domain: S3
Protocols:
- FTP
Properties:
EndpointType: VPC
EndpointDetails:
VpcId: !Ref VpcId
SecurityGroupIds:
- !Ref SecurityGroup
SubnetIds:
- !Select [0, !Ref SubnetIds]
- !Select [1, !Ref SubnetIds]
IdentityProviderDetails:
Function:
Fn::GetAtt: GetUserConfigLambda.Arn
IdentityProviderType: AWS_LAMBDA
LoggingRole:
Fn::GetAtt: CloudWatchLoggingRole.Arn
CloudWatchLoggingRole:
Description: IAM role used by Transfer to log API requests to CloudWatch
Type: AWS::IAM::Role
Condition: CreateServer
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- transfer.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: TransferLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:DescribeLogStreams
- logs:PutLogEvents
Resource:
Fn::Sub: '*'
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: LambdaSecretsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
Fn::Sub:
- arn:${AWS::Partition}:secretsmanager:${SecretsRegion}:${AWS::AccountId}:secret:aws/transfer/*
- SecretsRegion:
Fn::If:
- SecretsManagerRegionProvided
- Ref: SecretsManagerRegion
- Ref: AWS::Region
GetUserConfigLambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile:
Fn::Sub: |
import os
import json
import boto3
import base64
from botocore.exceptions import ClientError
def lambda_handler(event, context):
resp_data = {}
if 'username' not in event or 'serverId' not in event:
print("Incoming username or serverId missing - Unexpected")
return response_data
# It is recommended to verify server ID against some value, this template does not verify server ID
input_username = event['username']
input_serverId = event['serverId']
print("Username: {}, ServerId: {}".format(input_username, input_serverId));
if 'password' in event:
input_password = event['password']
if input_password == '' and (event['protocol'] == 'FTP' or event['protocol'] == 'FTPS'):
print("Empty password not allowed")
return response_data
else:
print("No password, checking for SSH public key")
input_password = ''
# Lookup user's secret which can contain the password or SSH public keys
resp = get_secret("aws/transfer/" + input_serverId + "/" + input_username)
if resp != None:
resp_dict = json.loads(resp)
else:
print("Secrets Manager exception thrown")
return {}
if input_password != '':
if 'Password' in resp_dict:
resp_password = resp_dict['Password']
else:
print("Unable to authenticate user - No field match in Secret for password")
return {}
if resp_password != input_password:
print("Unable to authenticate user - Incoming password does not match stored")
return {}
else:
# SSH Public Key Auth Flow - The incoming password was empty so we are trying ssh auth and need to return the public key data if we have it
if 'PublicKey' in resp_dict:
resp_data['PublicKeys'] = resp_dict['PublicKey'].split(",")
else:
print("Unable to authenticate user - No public keys found")
return {}
# If we've got this far then we've either authenticated the user by password or we're using SSH public key auth and
# we've begun constructing the data response. Check for each key value pair.
# These are required so set to empty string if missing
if 'Role' in resp_dict:
resp_data['Role'] = resp_dict['Role']
else:
print("No field match for role - Set empty string in response")
resp_data['Role'] = ''
# These are optional so ignore if not present
if 'Policy' in resp_dict:
resp_data['Policy'] = resp_dict['Policy']
if 'HomeDirectoryDetails' in resp_dict:
print("HomeDirectoryDetails found - Applying setting for virtual folders")
resp_data['HomeDirectoryDetails'] = resp_dict['HomeDirectoryDetails']
resp_data['HomeDirectoryType'] = "LOGICAL"
elif 'HomeDirectory' in resp_dict:
print("HomeDirectory found - Cannot be used with HomeDirectoryDetails")
resp_data['HomeDirectory'] = resp_dict['HomeDirectory']
else:
print("HomeDirectory not found - Defaulting to /")
print("Completed Response Data: "+json.dumps(resp_data))
return resp_data
def get_secret(id):
region = os.environ['SecretsManagerRegion']
print("Secrets Manager Region: "+region)
client = boto3.session.Session().client(service_name='secretsmanager', region_name=region)
try:
resp = client.get_secret_value(SecretId=id)
# Decrypts secret using the associated KMS CMK.
# Depending on whether the secret is a string or binary, one of these fields will be populated.
if 'SecretString' in resp:
print("Found Secret String")
return resp['SecretString']
else:
print("Found Binary Secret")
return base64.b64decode(resp['SecretBinary'])
except ClientError as err:
print('Error Talking to SecretsManager: ' + err.response['Error']['Code'] + ', Message: ' + str(err))
return None
Description: A function to lookup and return user data from AWS Secrets Manager.
Handler: index.lambda_handler
Role:
Fn::GetAtt: LambdaExecutionRole.Arn
Runtime: python3.11
Environment:
Variables:
SecretsManagerRegion:
Fn::If:
- SecretsManagerRegionProvided
- Ref: SecretsManagerRegion
- Ref: AWS::Region
GetUserConfigLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:invokeFunction
FunctionName:
Fn::GetAtt: GetUserConfigLambda.Arn
Principal: transfer.amazonaws.com
SourceArn:
Fn::If:
- CreateServer
- Fn::GetAtt: TransferServer.Arn
- Fn::Sub: arn:${AWS::Partition}:transfer:${AWS::Region}:${AWS::AccountId}:server/*
Transfer Familyで利用するSecrets Managerの構築
CloudFormationで作られたLambdaのコードを見ると、認証するシークレットIDは下記の形式なので
「aws/transfer/[Transfer FamilyのサーバID]/[FTPで利用するユーザ名]」で作成する。
# Lookup user's secret which can contain the password or SSH public keys
resp = get_secret("aws/transfer/" + input_serverId + "/" + input_username)
必要なシークレットの値はガイドの通り、設定します。
キー | 値 |
---|---|
Role | 先ほど作成したIAMロール名(ARN) |
Password | FTPで利用するパスワードを指定 |
HomeDirectory | 作成したS3のバケット名 |
ストレージをS3ではなくEFSにする場合は他にも追加のパラメータが必要になります。
検証①(VPCエンドポイント経由でFTP転送するパターン)
問題なく、FTPでファイル転送ができました。
[root@ip-10-1-2-248 ~]# ftp vpce-07f513a873b84f0f5-mva0f0dr.vpce-svc-0c6fc1c14ceb5b0d8.ap-northeast-1.vpce.amazonaws.com
Trying 10.2.4.125...
Connected to vpce-07f513a873b84f0f5-mva0f0dr.vpce-svc-0c6fc1c14ceb5b0d8.ap-northeast-1.vpce.amazonaws.com (10.2.4.125).
220 Service ready for new user.
Name (vpce-07f513a873b84f0f5-mva0f0dr.vpce-svc-0c6fc1c14ceb5b0d8.ap-northeast-1.vpce.amazonaws.com:root): hogehoge
331 User name okay, need password for hogehoge.
Password:
230 User logged in, proceed.
Remote system type is UNIX.
ftp>
ftp> bin
200 Command TYPE okay.
ftp> put test.txt
local: test.txt remote: test.txt
227 Entering Passive Mode (10,2,4,125,32,5)
150 File status okay; about to open data connection.
226 Transfer complete.
10 bytes sent in 3.4e-05 secs (294.12 Kbytes/sec)
ftp>
ftp> exit
221 Goodbye.
[root@ip-10-1-2-248 ~]#
無事にファイルがS3に格納されました。
NLBを挟む構成を考えてみた
本構成でもリージョンDNSに向けてアクセスする分には、AZ障害に対応できる可用性はとれています。
しかし、連携元単位でアクセス先を絞りたい、固定IPでアクセスしたい、プロトコル毎にアクセスを止めたいなど運用によってはNLBを挟んだ方が良いケースも出てくるかもしれません。しかしFTPを利用する上で、パッシブモードでの制約があるのでマルチAZ構成ではNLBを構築できないため、冗長構成を取りつつTransfer Familyを利用できないか考えてみます。
マルチAZ構成のNLBではFTP転送ができない
マルチAZ構成でNLBの構築します。
リスナーはTCPで21,8192-8200の通信が必要なので、ポート毎にターゲットグループを作成し、ターゲットはIP指定でVPCエンドポイントのIPを指定します。
検証構成は下記のようになります。
もちろんこの構成ではFTP通信ができません。Transfer FamilyのPassiveIPを指定する必要があり、デフォルトはAutoですが、マルチAZ構成のNLBでは設定するIPが決まらないためです。
シングルAZ構成のNLBではFTP転送できる
NLBをシングルAZ構成で構築する以外は上記と同じです。クロスゾーン負荷分散はOFFにしているので、宛先のターゲットはNLBを構築したAZと同じVPCエンドポイントのIPを指定します。
またTransfer FamilyのPassiveIPをNLBのIPアドレスを指定します。
Transfer FamilyのPassiveIPを設定を反映させるにはTransfer Familyの「停止」→「起動」が必要になります。
NLBのIPを調べるには、「ネットワークインターフェイス」からNLBのENIを指定して確認します。
問題なく、FTPでファイル転送ができましたが、シングルAZ構成になっているので可用性は劣ります。
[root@ip-10-1-2-248 ~]# ftp nlb-test-single-az-1a-f3c7d008f1afcdde.elb.ap-northeast-1.amazonaws.com
Connected to nlb-test-single-az-1a-f3c7d008f1afcdde.elb.ap-northeast-1.amazonaws.com (10.2.1.151).
220 Service ready for new user.
Name (nlb-test-single-az-1a-f3c7d008f1afcdde.elb.ap-northeast-1.amazonaws.com:root): hogehoge
331 User name okay, need password for hogehoge.
Password:
230 User logged in, proceed.
Remote system type is UNIX.
ftp>
ftp> bin
200 Command TYPE okay.
ftp> put test.txt
local: test.txt remote: test.txt
227 Entering Passive Mode (10,2,1,151,32,3)
150 File status okay; about to open data connection.
226 Transfer complete.
10 bytes sent in 2.9e-05 secs (344.83 Kbytes/sec)
ftp>
ftp>
ftp> exit
221 Goodbye.
[root@ip-10-1-2-248 ~]#
検証②(NLBを各AZに構築したRoute53のフェールオーバー構成パターン)
NLBでAZ障害の可用性が担保できるように各AZにNLBを配置してみました。Transfer FamilyのPassiveIPの設定上、NLB1に紐づくTransfer FamilyのサーバとNLB2に紐づくTransfer Familyのサーバを構築する必要がありました。Lambdaや配置先のS3は同じものでいいのですが、SecretsManagerのIDにはTransfer FamilyのIDを設定する必要があるので、各々のTransfer Familyのサーバに対応するように作る必要がありました。
また検証用のEC2からは同じDNSで引っ張りたいので、プライベートホストゾーンでフェールオーバー構成(プライマリ:NLB1、セカンダリ:NLB2)にしました。
Route53のヘルスチェックはプライベート環境なのでCloudwatch アラームで「HealthyHostCount」を設定しました。
検証構成は下記のようになります。
通常時にプライベートなDNSでFTP転送した時とnsloolupの結果は下記です。プライマリ側のNLBに対してFTPアクセスしていることがわかり、FTP転送できることがわかりました。
[root@ip-10-1-2-248 ~]# nslookup cname-nlb1-nlb2.ftp-transferfamily.com
Server: 10.1.0.2
Address: 10.1.0.2#53
Non-authoritative answer:
cname-nlb1-nlb2.ftp-transferfamily.com canonical name = nlb-test-single-az-1a-f3c7d008f1afcdde.elb.ap-northeast-1.amazonaws.com.
Name: nlb-test-single-az-1a-f3c7d008f1afcdde.elb.ap-northeast-1.amazonaws.com
Address: 10.2.1.151
[root@ip-10-1-2-248 ~]#
[root@ip-10-1-2-248 ~]#
[root@ip-10-1-2-248 ~]# ftp cname-nlb1-nlb2.ftp-transferfamily.com
Connected to cname-nlb1-nlb2.ftp-transferfamily.com (10.2.1.151).
220 Service ready for new user.
Name (cname-nlb1-nlb2.ftp-transferfamily.com:root): hogehoge
331 User name okay, need password for hogehoge.
Password:
230 User logged in, proceed.
Remote system type is UNIX.
ftp>
ftp> bin
200 Command TYPE okay.
ftp>
ftp> put test.txt
local: test.txt remote: test.txt
227 Entering Passive Mode (10,2,1,151,32,2)
150 File status okay; about to open data connection.
226 Transfer complete.
10 bytes sent in 3.3e-05 secs (303.03 Kbytes/sec)
ftp>
ftp>
ftp> exit
221 Goodbye.
[root@ip-10-1-2-248 ~]#
Route53でフェールオーバー後にアクセスした際の挙動をみてみようと思います。
FISを利用してゾーン障害を起こしてもよかったのですが、手間だったのでCloudwatchアラームの設定でエラーを起こしました。
フェールオーバー後にプライベートなDNSでFTP転送した時とnsloolupの結果は下記です。セカンダリ側のNLBに対してFTPアクセスしていることがわかり、FTP転送できることがわかりました。これでゾーン障害があったとしても可用性がとれた構成になりました。
[root@ip-10-1-2-248 ~]# nslookup cname-nlb1-nlb2.ftp-transferfamily.com
Server: 10.1.0.2
Address: 10.1.0.2#53
Non-authoritative answer:
cname-nlb1-nlb2.ftp-transferfamily.com canonical name = nlb-test-single-az-1c-7a7a8bcb1d9a7ecc.elb.ap-northeast-1.amazonaws.com.
Name: nlb-test-single-az-1c-7a7a8bcb1d9a7ecc.elb.ap-northeast-1.amazonaws.com
Address: 10.2.2.64
[root@ip-10-1-2-248 ~]#
[root@ip-10-1-2-248 ~]# ftp cname-nlb1-nlb2.ftp-transferfamily.com
Connected to cname-nlb1-nlb2.ftp-transferfamily.com (10.2.2.64).
220 Service ready for new user.
Name (cname-nlb1-nlb2.ftp-transferfamily.com:root): hogehoge
331 User name okay, need password for hogehoge.
Password:
230 User logged in, proceed.
Remote system type is UNIX.
ftp>
ftp> bin
200 Command TYPE okay.
ftp> put test.txt
local: test.txt remote: test.txt
227 Entering Passive Mode (10,2,2,64,32,4)
150 File status okay; about to open data connection.
226 Transfer complete.
10 bytes sent in 3.4e-05 secs (294.12 Kbytes/sec)
ftp>
ftp> exit
221 Goodbye.
[root@ip-10-1-2-248 ~]#
所感
FTP連携のニーズは減ってきているとは感じるものの、連携元の制約等でどうしてもFTPを利用しなければいけない場面は出てくるかと思います。ただNLBまで配置した構成を考えると複雑になってしまうので、実装するかはコストと要相談かと思います。EC2にFTPサーバを立てて運用するよりは、マネージドサービスを利用した方が運用は楽になると思いますので、誰かの助けになれば幸いです。