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

# 0.実装したもの

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/cd904114-0e42-1430-6801-1ebc94dd5078.png)

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

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

https://docs.aws.amazon.com/ja_jp/transfer/latest/userguide/what-is-aws-transfer-family.html



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

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

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

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

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

公式はこちら

https://repost.aws/ja/knowledge-center/aws-sftp-endpoint-type


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

https://qiita.com/shin_goe/items/c17ac880b5d87a224d9b


https://dev.classmethod.jp/articles/trasfer-for-sftp-endpoint-types-202004/

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

![最初.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/5e4cc3b5-d07b-73d4-6f94-a4ec871b4273.png)



# 2. 実際のコード

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


https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_transfer.CfnServer.html



``` 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/bace2566-9659-1cc9-bbc6-55b27f07ad38.png)

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

![最初.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/f74bec25-9868-bf4d-a814-bd30b2471a9c.png)



<br>


私の勉強不足かもしれませんが、
**TransferFamilyのプライベートIPをコード内で取得する方法が見つからなかった**
ので、NLBからのターゲットグループには、一度デプロイしてから、立ち上がったプライベートIPを手動で確認して追記する形での実装になりました。
良い方法がありましたら教えていただけますと幸いです。

SFTPUserについてもL2に非対応だったのでL1での実装になりました

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_transfer.CfnUser.html#sshpublickeys


# 3. 挙動の確認

作業するディレクトリでキーの作成
```
ssh-keygen -t rsa -b 2048 -f my-sftp-key
```

ssh-keygenについて

https://eigo-gayomenai-engineer.hatenablog.com/entry/2023/06/09/153528

公開鍵については先ほどのコードのこの部分に記載します

```typescript
 // 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接続

:::note info
 私はここでTransferFamily自体を指定して接続できないという凡ミスで数時間溶かしました
:::



![スクリーンショット 2024-12-25 9.30.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/92ea65f3-85a3-8977-2679-8d0ef564f5c3.png)


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

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

![しろぬり.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/ad6f69bb-6504-3d81-a8e0-cb53224a040c.png)



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

```
put localexample.png
```


![スクリーンショット 2024-12-25 9.42.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/fe5b4d60-df20-6b16-7b29-ce03a2a4b7bb.png)

対象のS3にファイルがアップロードできているのを確認
![スクリーンショット 2024-12-25 9.44.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480176/c75d5e79-4623-8897-6e92-8d61953510a5.png)

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