2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CDKにて、TransferFamily(内部アクセスが可能なVPCエンドポイントタイプ)を実装する

Last updated at Posted at 2024-12-25

0.実装したもの

image.png

内部アクセスが可能なVPCエンドポイントタイプの構成でTransferFamilyを実装しました。
こちらの構成はNLBのセキュリティグループによってIP制限をかけることが可能です。

1. TransferFamilyについて

TransferFamilyとはAWS ストレージサービスとの間でファイルを転送できる安全な転送サービスです。SFTPに対応しています。SAP試験でSFTPと出てきたら脳死で選択すれば大体正解になるやつです。

サーバーの実装方法には現在以下の3パターンがあります。

PUBLIC : インターネットに公開。接続元制限は不可。

VPC (Internet Facing) : インターネットに公開。接続元制限可能。

VPC (Internal) : VPC内部向けに公開。接続元制限可能。

詳細な説明については割愛しますが以下の記事がわかりやすかったです。

公式はこちら

こちらの記事がとてもわかりやすかったです。

今回はIP制限を付与する必要があったので、VPC (Internal) (内部アクセスが可能なVPCエンドポイントタイプ)にて実装を行いました。

最初.png

2. 実際のコード

悲しいことにTransferFamilyは現在(2024/12)L2コンストラクトに対応していなかったのでL1にて実装することになりました。

transferfamily.ts
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.ts
// 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:を記載すると「インターネット向け」になります。

imaqwwge.png

コメントアウトのまま(記載しない)と内部向けとなります

最初.png


私の勉強不足かもしれませんが、
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自体を指定して接続できないという凡ミスで数時間溶かしました

スクリーンショット 2024-12-25 9.30.50.png

sftp -i my-sftp-key sftp-user@NLBのドメイン

初回接続のため、クライアントはこのサーバーを「本当に信頼していいかどうか」を確認してきます。yesと回答するとSFTP通信が開始されます

しろぬり.png

sftpコマンドを実行したカレントディレクトリにある、localexample.pngをput

put localexample.png

スクリーンショット 2024-12-25 9.42.29.png

対象のS3にファイルがアップロードできているのを確認
スクリーンショット 2024-12-25 9.44.00.png

これでSFTP通信でS3にファイルを送信することができるようになりました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?