あ〜た〜らし〜い、朝が〜きた〜
おはようございます。技術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 を作成・削除してみましょう。
せっかくなので、ラジオ体操中にファイル転送を試してみます。いっちに〜 さんし〜
おおまかな流れは以下になります。
- 【CloudWatch】AM 6:00 スケジュール実行
- 【Step Functions】Transfer Server を作成
- 【Step Functions】50分待機
- 【クライアント】AM 06:35 ファイル転送
- 【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 バケットにファイルが届いていました。更新日時も想定通りです。
やったね。
最後に
ラジオ体操はテレワーク・デイズ期間だけと思っていましたが、体と脳が刺激されて、その後の仕事がはかどるような気がしました。
せっかくなので、リモートワークの日は積極的に行こうと思います。
今日はこのへんで失礼します。いっちに〜 さんし〜