AWS Transfer Family の サーバー + ユーザー + ワークフロー 一式をCloudFormationで作成する

2022.06.14

いわさです。

Transfer Family の マネージドワークフローを構築する機会が最近多くて、サーバーとユーザー含めて作っては壊してってのを繰り返しているのですが、さすがにCloudFormationテンプレートでも用意するか...となりました。

そして、マネージドワークフローのテンプレートがまだ記事で見当たらなかったので一式を公開したいと思います。

作るもの

サーバープロトコルはSFTPで、IDプロバイダーはサービスマネージドのものを構築します。
そして、エンドポイントタイプはVPCでパブリックIPを付与しており、ローカルからのみにIPアドレス制限を行えるように構成してみます。

ワークフローは、実はステップの種類ごとに設定方法が異なるので、コピー・カスタム・削除の3つのステップを組み合わせてみました。

作ったもの

まず、テンプレート全体はこちらのリポジトリに置いておきます。

サーバー

Transfer Family サーバー本体の部分です。
EndpointDetailsでVPCの構成およびIPアドレスの関連付けを行っています。
サービスマネージドユーザーを使うので、IdentityProviderTypeのみ指定しています。

Transfer Familyは自動でCloudWatch Logsへログ出力してくれるので、AWSTransferLoggingAccessマネージドポリシーをアタッチしたIAMロールを指定しています。

後ほどワークフローの中身を見ていきますが、サーバーのWorkflowDetailsでワークフローIDとワークフロー実行ロールを指定します。
CloudFormationで今回実装して気づいたんですが、OnUploadというトリガーに設定を行っていますね。
本日時点ではファイルアップロード時のみワークフローが実行されますが、将来別のトリガーも実装される可能性を感じます。楽しみです。

:
  SFTP:
    Type: AWS::Transfer::Server
    Properties: 
      Domain: S3
      Protocols: 
        - SFTP
      EndpointType: VPC
      EndpointDetails: 
        AddressAllocationIds:
          - !GetAtt EipTransfer.AllocationId
        SecurityGroupIds: 
          - !Ref SecurityGroup
        SubnetIds: 
          - !Ref PublicSubnet1
        VpcId: !Ref VPC
      IdentityProviderType: SERVICE_MANAGED
      LoggingRole: !GetAtt TransferLoggingRole.Arn
      WorkflowDetails:
        OnUpload:
          - WorkflowId: !Ref Workflow
            ExecutionRole: !GetAtt WorkflowExecutionRole.Arn
:
  TransferLoggingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: 
              Service: transfer.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSTransferLoggingAccess
:

ユーザー

こちらはユーザーを作成してます。
スタック作成時のパラメータで公開鍵を設定するようにしています。

テンプレートから切り離すことで複数ユーザー向けに汎用的に利用出来そうです。
Transfer Familyユーザーに、S3バケットへのアクセス権限をIAMロールで渡す必要があります。
ここではさぼってAmazonS3FullAccessを指定しまっているのでご注意ください。

実際には、対象のS3バケットやプレフィックスのみなど絞り込むべきです。

Parameters: 
:
  UserName:
    Type: String
    Default: user1
  UserPublicKey:
    Type: String
Resources: 
:
  TransferUser:
    Type: AWS::Transfer::User
    Properties:
      ServerId: !GetAtt SFTP.ServerId
      HomeDirectory: !Sub "/${S3Bucket}/${UserName}"
      HomeDirectoryType: "PATH"
      Role: !GetAtt TransferExecutionRole.Arn
      UserName: !Ref UserName
      SshPublicKeys: 
        - !Ref UserPublicKey
  TransferExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: 
              Service: transfer.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties: 
      AccessControl: Private
      BucketName: !Sub s3-${AWS::StackName}-${AWS::AccountId}-${AWS::Region}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
:

ワークフロー

ここでは以前ご紹介した新機能のマネージドワークフローを構成しています。
Stepsプロパティで複数のステップを構成していて、それぞれにTYPExxxStepDetailsを指定しています。
このStepDetailはTYPEごとに違っていて、かつドキュメントではJSON形式との指定がありますが、具体的なパラメータが記載されていません。

このあたりはCloudFormationのテンプレートリファレンスよりも、ユーザーガイドのAPIリファレンスが参考になります。

CreateWorkflow-AWS Transfer Family

マネージドワークフローではふたつのIAMロールが登場します。

ひとつはワークフローの実行ロールです。ここではS3バケットのファイルコピーと削除、そしてLambdaの実行が出来る必要があります。

ふたつめはLambda関数の実行ロールです。
カスタムステップのLambda関数では、Transfer Familyに対して、ワークフローステップのステータスを送信してやる必要があります。CloudFormationのカスタムリソースに似ています。
ですので、最低でもTransfer Familyへのsend_workflow_step_stateが出来る権限が必要です。

:
  Workflow:
    Type: AWS::Transfer::Workflow
    Properties: 
      Steps: 
        - Type: COPY
          CopyStepDetails: 
            Name: !Sub ${AWS::StackName}-copy-step
            DestinationFileLocation:
              S3FileLocation:
                Bucket: !Ref S3Bucket
                Key: "${transfer:UserName}/copy.dat"
            OverwriteExisting: "TRUE"
            SourceFileLocation: "${previous.file}"
        - Type: CUSTOM
          CustomStepDetails: 
            Name: !Sub ${AWS::StackName}-custom-step
            Target: !GetAtt lambdaFunction.Arn
            TimeoutSeconds: 60
            SourceFileLocation: "${previous.file}"
        - Type: DELETE
          DeleteStepDetails:
            Name: !Sub ${AWS::StackName}-delete-step
            SourceFileLocation: "${original.file}"
  WorkflowExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: 
              Service: transfer.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
        - arn:aws:iam::aws:policy/AmazonS3FullAccess

  lambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${AWS::StackName}-transfer-customstep
      Role: !GetAtt lambdaExecutionRole.Arn
      Handler: index.lambda_handler
      Runtime: python3.8
      Code:
        ZipFile: |
          import json
          import boto3
          transfer = boto3.client('transfer')
          def lambda_handler(event, context):
            print(json.dumps(event))
            transfer_response = transfer.send_workflow_step_state(
                WorkflowId=event['serviceMetadata']['executionDetails']['workflowId'],
                ExecutionId=event['serviceMetadata']['executionDetails']['executionId'],
                Token=event['token'],
                Status='SUCCESS'
            )
            return {
              'statusCode': 200,
              'body': json.dumps(transfer_response)
            }
  lambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: 
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AWSTransferFullAccess
:

デプロイと実行

リソースは期待どおり作成されていますね。
SFTPクライアントからファイルをアップロードしてみます。

$ sftp -i user1 user1@52.192.236.242
Connected to 52.192.236.242.
sftp> put hoge1.txt
Uploading hoge1.txt to /s3-hogesftp-123456789012-ap-northeast-1/user1/hoge1.txt
hoge1.txt                                                                                     100%   15     0.4KB/s   00:00

ファイルがアップロードされ、マネージドワークフローが動作してオリジンファイルが削除されていますね。ヨシ!

さいごに

本日は、Transfer Family の CloudFormation 対応リソース一式を作ってデプロイしてみました。
Transfer Familyで構築したい時はとりあえずこのテンプレートをベースに修正していってみようかなと思ってます。

途中触れていますが、注意事項があって、IAMロールが登場しすぎてマネージドポリシーばかり使ってしまいました。マネージドポリシーが悪いというより権限が大きいまま放置しているので良くないですね。
もし参考にされる方がいらっしゃいましたら、その点ご認識頂き必要に応じてより厳格なポリシーを付与して頂ければと思います。