JAWS-UG 初心者支部#24 サーバレスハンズオン勉強会にリモートで参加しました! #jawsug_bgnr

コンバンハ、千葉(幸)です。

「サーバレス...? あぁ、サーバが... レスの...  ...レスの やつね!」

というレベルの私にピッタリな勉強会、「JAWS-UG 初心者支部#24 サーバレスハンズオン勉強会 ~手を動かしながら学ぶサーバーレスはじめの一歩~」が開催されたため、リモート枠で参加しました。

新型コロナウィルスの感染拡大の影響で、多くのイベントが延期・中止の判断を下す中、こちらの勉強会では予定通りの開催という判断をされました。多分に葛藤があったのではと想像します。Facebookグループには以下のような投稿がありました。

新型コロナウイルス(COVID-19)につきまして、各報道機関で連日報道されておりますが、明日のハンズオン勉強会は開催する予定でおります。

参加する皆さんは、お一人お一人が、感染予防施策として咳エチケットへのご配慮や手洗い、必要に応じてマスクを着用する等、ご協力をお願いします。会場の受付やトイレにはポンプ式のアルコール消毒液を常時していますのでご利用ください。 当日体調が悪くなった方や、会社からの指示で会場参加できなくなった方などは、オンライン枠への切替をして頂き参加頂ければと思います!オンライン参加者でも質問しやすいようにslidoなどで質疑応答や交流できるようにしたいと考えています。

また、毎回参加者の皆さんが交流できる場として飲食を伴う懇親会を開催してきましたが、今回は初心者支部としては懇親会は開催なしとさせてください。 それでは明日、オンオフ参加者の皆さんお待ちしております!!!

ぜひ現地で参加してみたいという思いもありつつも、もろもろを鑑みて今回はリモートで参加させていただくことにしました。

リモートでの配信

当日の配信にはtwitchが使用されていました。見るだけであればログインなしで可能です。(チャットの使用は要ログイン)

また、質問はslidoを用いて受け付けており、こちらで書かれた内容が定期的にスピーカーの方に連携され、回答してくれる形式になっていました。

進行の補助にチャット、質問受付にslido、という二本立てで、オンライン参加向けにも配慮がなされており、非常に参加しやすかったです。

2時間で3回くらい、少しだけ固まったかな?というタイミングがありましたが、ほぼ問題なく映像を見ることができました。スピーカーの方以外の会場の音声(話し声、咳など)が入ってくることもありましたが、そこまで気にならず、臨場感があってよいかなとも思いました。

会場の雰囲気はこのような感じだったよう。配信を通じて見るのとはまた違った雰囲気ですね。

タイムテーブル

時間 内容 登壇者
18:30〜 受付開始 -
19:00-19:05 会場諸注意/初心者支部とは/テーマ説明 運営 澤田さん
19:05-20:35 サーバーレスクイックスタート: 手を動かしながら学ぶサーバーレスはじめの一歩 AWSJ 金澤さん
20:35-20:40 バッファ&休憩(ハンズオンの途中で10分程度の休憩を入れます) -
20:40-20:50 初心者支部卒業LT:初心者支部卒業によせて 運営 藤巻さん
20:50-20:55 アンケート回答 -
20:55- 解散 -

サーバーレスクイックスタート: 手を動かしながら学ぶサーバーレスはじめの一歩

当日はこの資料を映しながら、各自でもくもく作業→発表者の方が実演というのを短いスパンで繰り返していく方式でハンズオンが行われました。

ハンズオンでは大まかに3つの構成を作成します。

  • 1.AWS Lambdaで日→英翻訳する
  • 2.翻訳 Web APIを作る
  • 3.文字起こし + 翻訳パイプラインを作る

それぞれで必要となるコードを準備してくれている親切仕様です。

https://github.com/ketancho/aws-serverless-quick-start-hands-on/tree/master/src

1.AWS Lambdaで日→英翻訳する

目指す構成は以下です。Lambdaで実行するコードの中に日本語の文字列を入力してテスト実行し、戻り値で英語で翻訳されたものが返ってくることがゴールです。

マネジメントコンソールを初めて触る人でも分かるよう、操作が逐一丁寧に説明されています。詳細はスライドを参照いただくとして、以下のような観点が学べます。

  • Lambdaコンソール上でコードのテストを実行できること
  • コードに編集をかける度に保存を忘れないこと
  • Lambda関数に関連づけられたIAMロールに、必要なIAMポリシーをアタッチする必要があること

コードとして以下を書いて実行することになります。ハイライト部分の日本語を好きなようにカスタマイズして、英訳が返ってくることを確認します。

import json
import boto3

def lambda_handler(event, context):

    translate = boto3.client('translate')
    input_text = '順調ですか?'

    response = translate.translate_text(
        Text=input_text,
        SourceLanguageCode='ja',
        TargetLanguageCode='en'
    )

    output_text = response.get('TranslatedText')

    return {
        'statusCode': 200,
        'body': json.dumps({
            'output_text': output_text
        })
    }

コード内で使用しているAWS SDKのリファレンスは以下です。 Translate.Client.translate_text

ニワトリの可愛さについて問う文面を訳してみました。インプットが悪いのか、うまく訳してはくれませんでした。

2.翻訳 Web APIを作る

先ほどの手順で作成した構成にAPI Gatewayを組み合わせて、ブラウザ経由で英訳を返してくれる構成を作成します。

Lambda関数に載せるコードは、直接日本語の文字列を書き込んでいた箇所を、クエリ文字列のパラメーターinput_textの値として渡されたものを用いるように変更します。

import json
import boto3

def lambda_handler(event, context):

    translate = boto3.client('translate')
    input_text = event['queryStringParameters']['input_text']

    response = translate.translate_text(
        Text=input_text,
        SourceLanguageCode='ja',
        TargetLanguageCode='en'
    )

    output_text = response.get('TranslatedText')

    return {
        'statusCode': 200,
        'body': json.dumps({
            'output_text': output_text
        })
    }

何となく感覚的に分かるものの、ここでのeventって何だ??と思ったので調べたところ、以下の記述が見つかりました。

event – AWS Lambda はこのパラメータを使用してイベントデータをハンドラーに渡します。このパラメータは通常、Python の dict タイプです。また、list、str、int、float、または NoneType タイプを使用できます。

関数を呼び出すときは、イベントのコンテンツと構造を決定します。AWS のサービスで関数を呼び出す場合、そのイベント構造はサービスによって異なります。詳細については、「他のサービスで AWS Lambda を使用する」を参照してください。

Python の AWS Lambda 関数ハンドラーより

API GatewayによってLambda関数が呼び出される場合に、イベントとして渡されるメッセージの内訳は以下のような構造になっています。ハイライトしている箇所が、今回のコードで取得している箇所です。この例の内容が渡された場合、コードの7行目がinput_text = event['queryStringParameters']['name']であれば、input_textにはmeが挿入されることになります。

{
  "path": "/test/hello",
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, lzma, sdch, br",
    "Accept-Language": "en-US,en;q=0.8",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-Mobile-Viewer": "false",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "US",
    "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
    "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==",
    "X-Forwarded-For": "192.168.100.1, 192.168.1.1",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "pathParameters": {
    "proxy": "hello"
  },
  "requestContext": {
    "accountId": "123456789012",
    "resourceId": "us4z18",
    "stage": "test",
    "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9",
    "identity": {
      "cognitoIdentityPoolId": "",
      "accountId": "",
      "cognitoIdentityId": "",
      "caller": "",
      "apiKey": "",
      "sourceIp": "192.168.100.1",
      "cognitoAuthenticationType": "",
      "cognitoAuthenticationProvider": "",
      "userArn": "",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
      "user": ""
    },
    "resourcePath": "/{proxy+}",
    "httpMethod": "GET",
    "apiId": "wt6mne2s9k"
  },
  "resource": "/{proxy+}",
  "httpMethod": "GET",
  "queryStringParameters": {
    "name": "me"
  },
  "stageVariables": {
    "stageVarName": "stageVarValue"
  }
}

AWS Lambda を Amazon API Gateway に使用する

API Gatewayの[リソース]ペインの内容は、手順に従って作成すると以下のようになります。「なぜPOSTでなくGETのメソッドとして定義しているのか?」については、ブラウザから試行することを想定して、簡単にできるように、とのことでした。明示的に設定している箇所としては、Lambdaプロキシ統合を使用して、Lambdaと関連付けている部分のみです。

この設定をすることで、Lambda関数の画面で、トリガーにAPI Gatewayセットされていることが確認できるようになります。

[アクセス権限]タブから[Resource-based policy]を確認すると、Lambda関数のリソースベースポリシーにおいてAPI Gatewayからのinvokeを許可していることが確認できます。

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "73de3904-3c24-4926-8c35-b7be695e38b8",
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:translate-function",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:execute-api:ap-northeast-1:xxxxxxxxxxxx:rnxxxxxxlf/*/GET/translate"
        }
      }
    }
  ]
}

APIをdevという名称のステージにデプロイします。エンドポイントが生成されるため、この配下の/translate(リソースで指定したパス)に、?input_text=<訳したい日本語>というクエリパラメータを付与してGETすれば、訳された結果が返ってきます。

ニワトリのブキミさについて問う文面を訳してみました。インプットが悪いのか、うまく訳してはくれませんでした。

3.文字起こし + 翻訳パイプラインを作る

これまでの構成とは別に、新規に構築します。インプットのS3バケットに音声ファイルがputされたのをトリガーにLambda関数が実行され、文章に変換された上でアウトプットのS3バケットに出力されます。

インプットとアウトプットのS3バケットを分けたのは、わかりやすさを優先するためとのことです。

今回のLambdaはトリガーとしてS3が設定されます。

Lambda関数のリソースベースポリシーはこのような形に。

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "lambda-daee02fa-7f31-4ca2-b862-9d76d68f9a15",
      "Effect": "Allow",
      "Principal": {
        "Service": "s3.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:transcribe-function",
      "Condition": {
        "StringEquals": {
          "AWS:SourceAccount": "xxxxxxxxxxxx"
        },
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:s3:::20200206-transcribe-input-chiba"
        }
      }
    }
  ]
}

載せるコードは以下です。ハイライト部分のS3バケット名は、アウトプット用のS3バケットに変換する必要があります。

import json
import urllib.parse
import boto3
import datetime

s3 = boto3.client('s3')
transcribe = boto3.client('transcribe')

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    try:
        transcribe.start_transcription_job(
            TranscriptionJobName= datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '_Transcription',
            LanguageCode='en-US',
            Media={
                'MediaFileUri': 'https://s3.ap-northeast-1.amazonaws.com/' + bucket + '/' + key
            },
            OutputBucketName='yyyymmdd-transcribe-output-yourname'
        )
    except Exception as e:
        print(e)
        print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
        raise e

Transcribeに関する処理をしている部分のリファレンスは以下です。TranscribeService.Client.start_transcription_job

eventに注目してみて見ると、コード内の変数bucketkeyが、S3から送信されたイベントの中からバケットの名称とオブジェクトのキーを使用しています。

{
  "Records": [
    {
      "eventVersion": "2.1",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-2",
      "eventTime": "2019-09-03T19:37:27.192Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "AWS:AIDAINPONIXQXHT3IKHL2"
      },
      "requestParameters": {
        "sourceIPAddress": "205.255.255.255"
      },
      "responseElements": {
        "x-amz-request-id": "D82B88E5F771F645",
        "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo="
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1",
        "bucket": {
          "name": "lambda-artifacts-deafc19498e3f2df",
          "ownerIdentity": {
            "principalId": "A3I5XTEXAMAI3E"
          },
          "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df"
        },
        "object": {
          "key": "b21b84d653bb07b05b1e6b33684dc11b",
          "size": 1305107,
          "eTag": "b21b84d653bb07b05b1e6b33684dc11b",
          "sequencer": "0C0F6F405D6ED209E1"
        }
      }
    }
  ]
}

Amazon S3 イベントでの AWS Lambda の使用

インプットのバケットにアップロードする音声ファイルのサンプルは、以下のリンクからダウンロードできます。(アクセスすると音声が流れます。)

https://d1.awsstatic.com/product-marketing/Polly/HelloEnglish-Joanna.0aa7a6dc7f1de9ac48769f366c6f447f9051db57.mp3

ファイルをアップロードするともろもろ処理が走りますが、実行されたTranscribeジョブは以下のように確認できます。

アウトプットS3バケットに出力されたJSONファイルを整形すると、以下のようになっています。(元ファイルは改行なしの1行です。)ハイライトされた箇所が、文字起こしされた内容です。下にずらっと単語ごとの分析結果が並んでいます。こんな風に分析しているんですね。

{
	"jobName": "20200219112729_Transcription",
	"accountId": "xxxxxxxxxxxx",
	"results": {
		"transcripts": [
			{
				"transcript": "Hello. Do you speak a foreign language? One language is never enough."
			}
		],
		"items": [
			{
				"start_time": "0.04",
				"end_time": "0.65",
				"alternatives": [
					{
						"confidence": "0.9139",
						"content": "Hello"
					}
				],
				"type": "pronunciation"
			},
			{
				"alternatives": [
					{
						"confidence": "0.0",
						"content": "."
					}
				],
				"type": "punctuation"
			},
			{
				"start_time": "1.04",
				"end_time": "1.14",
				"alternatives": [
					{
						"confidence": "1.0",
						"content": "Do"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "1.14",
				"end_time": "1.27",
				"alternatives": [
					{
						"confidence": "1.0",
						"content": "you"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "1.27",
				"end_time": "1.59",
				"alternatives": [
					{
						"confidence": "1.0",
						"content": "speak"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "1.59",
				"end_time": "1.65",
				"alternatives": [
					{
						"confidence": "0.9991",
						"content": "a"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "1.65",
				"end_time": "1.99",
				"alternatives": [
					{
						"confidence": "1.0",
						"content": "foreign"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "1.99",
				"end_time": "2.59",
				"alternatives": [
					{
						"confidence": "1.0",
						"content": "language"
					}
				],
				"type": "pronunciation"
			},
			{
				"alternatives": [
					{
						"confidence": "0.0",
						"content": "?"
					}
				],
				"type": "punctuation"
			},
			{
				"start_time": "2.88",
				"end_time": "3.19",
				"alternatives": [
					{
						"confidence": "0.9944",
						"content": "One"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "3.19",
				"end_time": "3.61",
				"alternatives": [
					{
						"confidence": "0.991",
						"content": "language"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "3.61",
				"end_time": "3.75",
				"alternatives": [
					{
						"confidence": "0.991",
						"content": "is"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "3.75",
				"end_time": "4.03",
				"alternatives": [
					{
						"confidence": "1.0",
						"content": "never"
					}
				],
				"type": "pronunciation"
			},
			{
				"start_time": "4.03",
				"end_time": "4.48",
				"alternatives": [
					{
						"confidence": "0.9079",
						"content": "enough"
					}
				],
				"type": "pronunciation"
			},
			{
				"alternatives": [
					{
						"confidence": "0.0",
						"content": "."
					}
				],
				"type": "punctuation"
			}
		]
	},
	"status": "COMPLETED"
}

宿題

ハンズオンの中では取り上げられませんでしたが、作成した構成を基に発展させてみよう!という宿題がいくつか出されました。興味がある方はチャレンジしてみてはいかがでしょうか。

『Option 1:』に関しては、下記ページ「AWS Hands-on for Beginners 〜Serverless 編〜」の「Serverless #1」というセクションで紹介されている内容が該当するそうです。『Option 2:』に関しても近日対応するページが追加されるかも?とのことでした。

https://aws.amazon.com/jp/aws-jp-introduction/aws-jp-webinar-hands-on/

初心者支部卒業LT:初心者支部卒業によせて

2018年から初心者支部の運営に携わってこられた@hirosys_さんが、海外勤務をきっかけに卒業されるとのことで、これまでの取り組みを振り返る発表をなされました。運営に携わるというアウトプットを経て自身が大いに成長できたため、ぜひ皆さんにも何らかの形でアウトプットをして成長につなげて欲しい、楽しんでやってみて欲しいとの内容でした。お疲れ様でした!

飛び込み:オンラインカンファレンス AWS Innovate!

最後にAWSJの方が飛び込みで入ってこられて、熱くAWS Innovateについて語って去っていかれました。

「え?まさかままだ登録されてない方がいるんですか!?」

終わりに

なかなか機会がないと新しいサービスには触らないもので、API Gateway、Transcribe、Translateについてはこれまでノータッチだったため、非常によいきっかけとなりました。リモートで自宅にいながらこのような勉強会に参加できるのは、現地で運営の方が尽力されているからに他ならず、感謝しかありません。

新型コロナウィルスの影響でオフラインでのイベントは開きづらくなっている現状ですが、開催する側にとっても、参加する側にとっても、より手軽にオンラインで実行できるノウハウが溜まっていくきっかけになればよいなと思いました。

こちらからは以上です。