0.実装したもの
内部アクセスが可能なVPCエンドポイントタイプの構成でTransferFamilyを実装しました。
こちらの構成はNLBのセキュリティグループによってIP制限をかけることが可能です。
1. TransferFamilyについて
TransferFamilyとはAWS ストレージサービスとの間でファイルを転送できる安全な転送サービスです。SFTPに対応しています。SAP試験でSFTPと出てきたら脳死で選択すれば大体正解になるやつです。
サーバーの実装方法には現在以下の3パターンがあります。
PUBLIC : インターネットに公開。接続元制限は不可。
VPC (Internet Facing) : インターネットに公開。接続元制限可能。
VPC (Internal) : VPC内部向けに公開。接続元制限可能。
詳細な説明については割愛しますが以下の記事がわかりやすかったです。
公式はこちら
こちらの記事がとてもわかりやすかったです。
今回はIP制限を付与する必要があったので、VPC (Internal) (内部アクセスが可能なVPCエンドポイントタイプ)にて実装を行いました。
2. 実際のコード
悲しいことにTransferFamilyは現在(2024/12)L2コンストラクトに対応していなかったのでL1にて実装することになりました。
import { CfnServer, CfnUser } from "aws-cdk-lib/aws-transfer";
import { Construct } from "constructs";
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as elb from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { EnvName } from "../../parameter";
import { createRemovalPolicy } from "../utils";
import { IpTarget } from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
interface TransferFamilyProps {
vpc: ec2.Vpc; // 別のコンストラクトでVPC構築済み
envName: EnvName;
}
export class TransferFamily extends Construct {
public readonly bucket: s3.Bucket;
constructor(scope: Construct, id: string, props: TransferFamilyProps) {
super(scope, id);
const { vpc, envName } = props;
// TransferFamilyPrivateサブネットを選択
const transferFamilySubnet = vpc.selectSubnets({
subnetGroupName: "TransferFamilyPrivate", // 別のコンストクラトにて専用のサブネットに名前をつけておいて、それを使用しています。
}).subnets[0];
const transferFamilyPrivateSubnetID = transferFamilySubnet.subnetId;
// NLB。現状SFTPサーバーで自動作成されるVPCエンドポイントを取得する方法がないため、一度デプロイした後、エンドポイントIDを直接記載してデプロイする必要があります。
const nlb = new elb.NetworkLoadBalancer(this, 'NLB', {
vpc,
internetFacing: true,
});
const listener = nlb.addListener('Listener', {
port: 22,
});
listener.addTargets('Target', {
port: 22,
targets: [new IpTarget('10.1.x.xxx')] //TransferFamilyのプライベートIPをデプロイしてから追記する必要がある。
});
const nlbSecurityGroup = new ec2.SecurityGroup(
this,
'NLB-SG',
{ vpc, allowAllOutbound: true },
);
nlbSecurityGroup.addIngressRule(
ec2.Peer.ipv4('203.0.xxx.x/24'), //NLBのセキュリティグループでIP制限
ec2.Port.tcp(22),
'Allow inbound SSH access from specific IP range'
);
const cfnLoadBalancer = nlb.node.defaultChild as elb.CfnLoadBalancer; // セキュリティグループをNLBに適用(L2に対応していない)
cfnLoadBalancer.addPropertyOverride('SecurityGroups', [
nlbSecurityGroup.securityGroupId,
]);
// ENIに付与するためのセキュリティグループ
const securityGroup = new ec2.SecurityGroup(this, 'SftpSecurityGroup', {
vpc: vpc,
description: 'Security group for SFTP server',
allowAllOutbound: true,
});
securityGroup.addIngressRule(
ec2.Peer.ipv4('0.0.0.0/0'),
ec2.Port.tcp(22),
'Allow SFTP access only from NLB security group'
);
const securityGroupId = securityGroup.securityGroupId;
// ElasticIPを作成すると、VPC (Internet Facing)タイプになる。
// const eip = new ec2.CfnEIP(this, 'ElasticIP', {
// domain: 'vpc',
// });
// CloudWatch Logs用のIAMロール
const loggingRole = new iam.Role(this, 'TransferLoggingRole', {
assumedBy: new iam.ServicePrincipal('transfer.amazonaws.com'),
});
// ログ記録用のポリシーをロールに付与
loggingRole.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
],
resources: ['*'],
}));
// TransferFamily VPC(internal access) 2024/11現在L2に対応していない
const sftpServer = new CfnServer(this, 'sftpServer', {
domain: 'S3',
endpointType: 'VPC',
identityProviderType: 'SERVICE_MANAGED', //Transfer Family自体でユーザーの認証情報(ユーザー名とパスワード)を管理
loggingRole: loggingRole.roleArn,
protocols: ['SFTP'],
endpointDetails: {
// addressAllocationIds: [eip.attrAllocationId],
securityGroupIds: [securityGroupId],
subnetIds: [transferFamilyPrivateSubnetID],
vpcId: vpc.vpcId,
},
});
// TransferFamilyの宛先のS3バケット
const sftpBucket = new s3.Bucket(this, 'SFTPBucket', {
bucketName: 'example-transferfamily-bucket-' + envName,
removalPolicy: RemovalPolicy.RETAIN,
});
sftpBucket.grantReadWrite(loggingRole);
// TransferFamilyのユーザー
const sftpUser = new CfnUser(this, 'SFTPUser', {
serverId: sftpServer.attrServerId,
userName: 'sftp-user',
homeDirectoryType: 'LOGICAL',
homeDirectoryMappings: [{
entry: '/', // ユーザーが見る論理パス
target: `/${sftpBucket.bucketName}`, // 実際の S3 ARN パス
}],
sshPublicKeys: ["ssh-rsa hogehogheohgeohogegho"], // 公開鍵
role: loggingRole.roleArn,
});
this.bucket = sftpBucket;
}
}
メインはここです
// TransferFamily VPC(internal access) 2024/11現在L2に対応していない
const sftpServer = new CfnServer(this, 'sftpServer', {
domain: 'S3',
endpointType: 'VPC',
identityProviderType: 'SERVICE_MANAGED', //Transfer Family自体でユーザーの認証情報(ユーザー名とパスワード)を管理
loggingRole: loggingRole.roleArn,
protocols: ['SFTP'],
endpointDetails: {
//addressAllocationIds: [eip.attrAllocationId],
securityGroupIds: [securityGroupId],
subnetIds: [transferFamilyPrivateSubnetID],
vpcId: vpc.vpcId,
},
});
ここのendpointTypeで最初に説明したタイプの指定を行うことができます。
endpointType: 'PUBLIC' のように書くと、PUBLICタイプとすることができます。
先述の通りVPCタイプには「インターネット向け」と「内部」がありますが、その場合はどちらもendpointType: 'VPC' と記載します。そして、endpointDetailsにコメントアウトしているaddressAllocationIds:を記載すると「インターネット向け」になります。
コメントアウトのまま(記載しない)と内部向けとなります
私の勉強不足かもしれませんが、
TransferFamilyのプライベートIPをコード内で取得する方法が見つからなかった
ので、NLBからのターゲットグループには、一度デプロイしてから、立ち上がったプライベートIPを手動で確認して追記する形での実装になりました。
良い方法がありましたら教えていただけますと幸いです。
SFTPUserについてもL2に非対応だったのでL1での実装になりました
3. 挙動の確認
作業するディレクトリでキーの作成
ssh-keygen -t rsa -b 2048 -f my-sftp-key
ssh-keygenについて
公開鍵については先ほどのコードのこの部分に記載します
// TransferFamilyのユーザー
const sftpUser = new CfnUser(this, 'SFTPUser', {
serverId: sftpServer.attrServerId,
userName: 'sftp-user',
homeDirectoryType: 'LOGICAL',
homeDirectoryMappings: [{
entry: '/', // ユーザーが見る論理パス
target: `/${sftpBucket.bucketName}`, // 実際の S3 ARN パス
}],
sshPublicKeys: ["ssh-rsa hogehogheohgeohogegho"], // 公開鍵はここに記載
role: loggingRole.roleArn,
});
this.bucket = sftpBucket;
}
秘密鍵をおいたディレクトリにて、NLBのドメインに対してSFTP接続
私はここでTransferFamily自体を指定して接続できないという凡ミスで数時間溶かしました
sftp -i my-sftp-key sftp-user@NLBのドメイン
初回接続のため、クライアントはこのサーバーを「本当に信頼していいかどうか」を確認してきます。yesと回答するとSFTP通信が開始されます
sftpコマンドを実行したカレントディレクトリにある、localexample.pngをput
put localexample.png
これでSFTP通信でS3にファイルを送信することができるようになりました。