ラジオ体操の時間にファイル転送したいんだよねぇ 〜AWS Transfer for SFTP〜

記事タイトルとURLをコピーする

あ〜た〜らし〜い、朝が〜きた〜

おはようございます。技術1課の木次です。いっちに〜
皆さんラジオ体操してますか?さんし〜

今週1週間はリモートワーク勤務が推奨されています。(2019年もテレワーク・デイズに参加いたします)
ずーっと自宅だと体に悪いので、期間中はなるべくラジオ体操をやっています。いっちに〜 さんし〜

SFTP で AWS 側にファイル転送

古いシステムやパッケージ連携などで、ファイル転送に SFTP を利用するケースがあります。
SFTP とは SSH File Transfer Protocol の略で、文字通り SSH 経由でファイル転送をするため、従来の FTP に比べてセキュアな通信が可能です。

この SFTP で AWS 側にファイル転送する場合、EC2 インスタンスをたてて、というやり方もありますが、可用性を考えるとやりたくないですよね。
そのような場合、AWS Transfer for SFTP を利用すると幸せになれます。

AWS Transfer for SFTP

昨年の re:Invent 2018 で発表されたマネージドな SFTP サービスです。
転送したファイルは S3 に保存してくれるので、やろうと思えばサーバーレスな環境を構築できます。

気になる金額ですが、1時間ごとの料金になります。(それ以外にデータアップロードとダウンロードの転送料金も発生)
東京リージョンだと、1 時間あたり 0.3USD。常時稼働した場合、1日で 7.2 USD。1月だと約 216 USD です。

特定の時間だけ利用したい

いやー、ファイル転送するのは朝方だけなんだよねぇ。1時間だけでいいんだよ。

オンプレなら難しいですが、AWS なら 1時間だけ 大丈夫です。スケジュール実行した Lambda から AWS Transfer for SFTP を作成・削除してみましょう。
せっかくなので、ラジオ体操中にファイル転送を試してみます。いっちに〜 さんし〜

おおまかな流れは以下になります。

  1. 【CloudWatch】AM 6:00 スケジュール実行
  2. 【Step Functions】Transfer Server を作成
  3. 【Step Functions】50分待機
  4. 【クライアント】AM 06:35 ファイル転送
  5. 【Step Functions】Transfer Server を削除

ポイントは Step Functions。
2 で作成した Transfer Server のサーバーID をステートとして保持し、4 で削除のために利用します。

AWS Step Functions

AWS Step Functions は、視覚的なワークフローを使用して、分散アプリケーションとマイクロサービスのコンポーネントを調整できるマネージドなサービスです。ASL (Amazon States Language) と呼ばれる JSON 形式の言語でワークフローを定義できます。

このワークフロー内で、タスクを定義して Lambda を呼び出します。
Lambda 自身はステートレスなので状態(ステート) を保持できせん。ですが、Step Functions を利用すると、ワークフロー内で値を引き渡すことができます。

ワークフローを下記のように定義します。状態遷移はシンプルに上から下へ流れるだけです。

{
  "StartAt": "CreateServer",
  "States": {
    "CreateServer": {
          "Type": "Task",
          "Resource": "arn:aws:states:::lambda:invoke",
          "Parameters": {
              "FunctionName": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:create-transfer-server:$LATEST",
              "Payload": {
                "Input.$": "$"
              }
          },
          "OutputPath": "$",
          "Next": "WaitForRadioExercises"
      },
    "WaitForRadioExercises": {
      "Type": "Wait",
      "Seconds": 3000,
      "Next": "DeleteServer"
    },
    "DeleteServer": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:delete-transfer-server:$LATEST",
        "Payload": {
          "Input.$": "$"
        }
      },
      "End": true
    }
  }
}

StartAt 項目では、最初に起動するステートを設定し、Next 項目で次に実行するステートを設定します。最後のステートには End 項目に true を設定します。

処理詳細

細かく見ていきましょう。

1. AM 6:00 スケジュール実行

CloudWatch Events で 日本時間 AM 6:00 (0 21 * * ? *) に起動するルールを作成します。UTCなので、そこは注意です。
起動するターゲットとして作成した Step Functions ステートマシン を選択し、入力値として 定数(JSON) を渡します。

{
    "HostedZoneId": "Route53 ホストゾーンID",
    "UserName": "SFTPユーザー名",
    "UserRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/SFTPユーザーロール名",
    "UserDirectory": "S3バケット保存先",
    "SubDomain": "サブドメイン",
    "LoggingRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/CloudWatchログ用ロール名"
}

この入力値は Lambda 内で利用します。

2. Transfer Server を作成

CreateServer ステートでは、ステートタイプに Task を指定して Transfer Server を作成する Lambda を呼び出します。

Transfer は比較的新しいサービスのため、boto3 モジュールと一緒にデプロイしてください。
対応していない場合は [ERROR] UnknownServiceError: Unknown service: 'transfer' と表示されます。

import os
import boto3


transfer = boto3.client('transfer')
route53 = boto3.client('route53')


def create_server(logging_role_arn):
    response = transfer.create_server(
        EndpointType='PUBLIC',
        IdentityProviderType='SERVICE_MANAGED',
        LoggingRole=logging_role_arn
    )
    return response.get('ServerId')


def upsert_cname_record(hosted_zone_id, server_id, sub_domain):
    res1 = route53.get_hosted_zone(
        Id=hosted_zone_id
    )
    host_name = res1['HostedZone']['Name']

    record = sub_domain + '.' + host_name
    region_name = boto3.session.Session().region_name
    target = f'{server_id}.server.transfer.{region_name}.amazonaws.com'

    res2 = route53.change_resource_record_sets(
        HostedZoneId=hosted_zone_id,
        ChangeBatch={
            'Comment': f'{record} -> {target}',
            'Changes': [
                {
                    'Action': 'UPSERT',
                    'ResourceRecordSet': {
                        'Name': record,
                        'Type': 'CNAME',
                        'TTL': 300,
                        'ResourceRecords': [
                            {
                                'Value': target
                            }
                        ]
                    }
                }
            ]
        }
    )
    return res2


def lambda_handler(event, context):
    logging_role_arn = event['Input']['LoggingRoleArn']
    hosted_zone_id = event['Input']['HostedZoneId']
    sub_domain = event['Input']['SubDomain']
    user_name = event['Input']['UserName']
    user_role_arn = event['Input']['UserRoleArn']
    user_directory = event['Input']['UserDirectory']
    ssh_pub_key = os.environ['SSH_PUB_KEY']

    # サーバー作成
    server_id = create_server(logging_role_arn)

    # CNAMEレコード作成
    upsert_cname_record(hosted_zone_id, server_id, sub_domain)

    # ユーザー作成
    transfer.create_user(
        ServerId=server_id,
        UserName=user_name,
        Role=user_role_arn,
        HomeDirectory=user_directory,
        SshPublicKeyBody=ssh_pub_key
    )

    return {
        'Transfer': {
            'ServerId': server_id,
            'HostedZoneId': hosted_zone_id,
            'SubDomain': sub_domain
        }
    }
  • create_server
    • SFTP サーバーを作成します。戻り値のサーバーID は削除時にも利用します。
    • CloudWatch Logs にログ保存するためのロールを指定します。
  • upsert_cname_record
    •  クライアント側からのエンドポイントを固定するため、CNAME レコードセットを作成します。
    • 事前に Route 53 にホストゾーンを作成しておきます。
  • create_user
    •  クライアントから接続するために、SFTP にユーザーを作成します。
    • サンプルのため SSH 公開鍵は環境変数から取得しています。実際には SSM パラメータストア や Secrets Manager を検討した方がいいでしょう。

3. 50分待機

ステートタイプに Wait を指定して、待機する秒数を設定しています。

4. AM 06:35 ファイル転送

他サーバーに sftp を 実行するシェルを用意して、AM 6:35 (35 21 * * *) に実行されるように cron を仕掛けます。

#!/bin/bash

sftp -i 秘密鍵 -o 'StrictHostKeyChecking no' -oPort=22 -b バッチファイル ユーザー名@ホスト名
exit 0

バッチファイルの中身では PUT コマンドを記述します。

5. Transfer Server を削除

import boto3


transfer = boto3.client('transfer')
route53 = boto3.client('route53')


def delete_server(server_id):
    response = transfer.delete_server(
        ServerId=server_id
    )
    return response


def delete_cname_record(hosted_zone_id, server_id, sub_domain):
    res1 = route53.get_hosted_zone(
        Id=hosted_zone_id
    )
    host_name = res1['HostedZone']['Name']
    print(host_name)

    record = sub_domain + '.' + host_name
    region_name = boto3.session.Session().region_name
    target = f'{server_id}.server.transfer.{region_name}.amazonaws.com'

    res2 = route53.change_resource_record_sets(
        HostedZoneId=hosted_zone_id,
        ChangeBatch={
            'Changes': [
                {
                    'Action': 'DELETE',
                    'ResourceRecordSet': {
                        'Name': record,
                        'Type': 'CNAME',
                        'TTL': 300,
                        'ResourceRecords': [
                            {
                                'Value': target
                            }
                        ]
                    }
                }
            ]
        }
    )
    return res2


def lambda_handler(event, context):
    args = event['Input']['Payload']['Transfer']
    server_id = args.get('ServerId')
    hosted_zone_id = args.get('HostedZoneId')
    sub_domain = args.get('SubDomain')

    # サーバー削除
    delete_server(server_id)

    # レコードセット削除
    delete_cname_record(hosted_zone_id, server_id, sub_domain)
  • delete_server
    • SFTP サーバーを削除します。作成したユーザーも一緒に削除されます。
  • delete_cname_record
    • 作成した CNAME のレコードセットを削除します。

実際にラジオ体操やってきた

ということで、ラジオ体操をやっている公園にやってきました。自宅から徒歩30分くらい。程よい距離で散歩にも適しています。

ラジオ体操は 6:30 スタート。
最初は第一体操から始まり、第二体操へ。そして40分頃に終了となります。

1日目

恥ずかしくて端っこから参加。
おいおい結構しんどいぞ、汗だらだら。そしてラジオ体操第二をすっかり忘れてる。。。

2日目

勇気を出して輪の中へ。いつか、あの真ん中のステージに立ちたい。いっちに〜 さんし〜

3日目

より前進。
3日目には恥ずかしさはなくなり、第二体操も8割くらい出来るように。気持ちいい〜。いっちに〜 さんし〜

結果は

さて、ファイルは届いているでしょうか。まずは Step Functions ワークフローの結果を確認します。

実行ステータスは成功 。開始と終了時間も想定通りです。

指定した S3 バケットにファイルが届いていました。更新日時も想定通りです。
やったね。

最後に

ラジオ体操はテレワーク・デイズ期間だけと思っていましたが、体と脳が刺激されて、その後の仕事がはかどるような気がしました。
せっかくなので、リモートワークの日は積極的に行こうと思います。

今日はこのへんで失礼します。いっちに〜 さんし〜