Lambda + ALBでAPIを構築してみた(Photocreate Advent Calendar 19日目 )
Lambda + ALBでAPIを構築してみた(Photocreate Advent Calendar 19日目 ):
エンジニア の @mackagy1 です。
この記事は Photocreate Advent Calendar 2018 の 19日目 の記事です。
WEBサイトやアプリで住所入力フォームに郵便番号を入力すると住所が自動補完されるUIありますよね。
仕組み的には単純で、郵便番号を入力したときに住所検索するAPIを呼び出して、そのレスポンスをフォームに表示するという流れになると思います。
こういうちょっとしたAPIを作るためにわざわざWEBサーバをたててそこにAPIのっけて・・というのは大げさな感じがします。
ということでLambdaを使って手軽に住所検索のAPIを作ってみたいなと思ってやってみました。
クライアント → ALB → Lambda → DynamoDB
DynamoDBには郵便番号と住所情報をつっこんでおきます。
今回はとりあえず1レコード入れて動作確認してみます。
こんな方が読むと何か得るものがあると思います。
まずはServerless Frameworkをインストールします。(以降、Serverless)
ServerlessはLambda関数の作成、デプロイ、ローカルでのデバッグをらくちんにしてくれる優れもののフレームワークです。アプリケーションのコードと簡単な設定ファイルを用意すればすぐにAWS上に公開することもできます。Lambda開発をとても便利にしてくれます。
Serverlessは以下のようにインストールします。
以下のように表示されれば成功です
尚、slsというエイリアスが用意されています。
Serverlessはサービスという単位で開発します。
zipcode-appディレクトリが作成され、中に以下のファイルが作成されます。
コメント行が長いので有効な行だけ抽出してます。
ファンクションとしてhelloが定義されていますね。
また、handler.jsはデフォルトで以下のようになっています。
helloファンクションの実体です。
「Hello World」的なレスポンスを返却するようになっていますね。
これでサービスの作成は完了です。
尚、サービス作成時に使用可能なtemplateは以下で確認可能です。
使いたい言語に応じて指定するテンプレートを切り替えましょう。
今回はnodeを使います。
続いて、AWSのアカウントの設定を行います。
Serverlessでは、credential(~/.aws/credentials)を利用しています。
今回はまだこのファイルがないので作成します。
keyとsecretには開発に使用するawsアカウントの適切な値を設定してください。
成功すると
一通り準備ができたので本題に入ります。
改めて今回のAPIでやりたいことを整理します。
今回はマスタ管理にDynamoDBを利用します。
ということでDynamoDBをローカルで動かせるようにします。
これもServerlessを使うことで簡単に実現することができます。
必要となるのは以下のツール、プラグインとなります。
サービスのルートディレクトリで以下を実行します。
次にserveless.ymlに以下を追記します。
一番下とかでOKです。
この状態でslsコマンドを実行するとdynamodb系のサブコマンドが増えていることがわかります。
次にserverless.ymlにDynamodbのテーブル定義+@を追加します。
よくわからない箇所は公式ドキュメントをご覧ください。
※注1:serverless-dynamodb-localがまだBillingMode:PAY_PER_REQUEST(オンデマンドモード)の指定に対応していないissueがあるため(2018/12/19現在)、ローカルにDynamoDB作るときはProvisionedThroughput(プロビジョニングモードになります)を指定します。awsにデプロイする時はBillingMode:PAY_PER_REQUEST指定にすることを忘れずに!!
テーブル名は動作させる環境によって変えたいので接頭辞を変数化しています。
${self:provider.stage}には、以下の箇所で定義したstageの値が入ります。
ここも変数化されててややこしいのですが、
今回、ローカル開発の環境をdev、デプロイ先の環境をstage(ややこしいですがオプションのstageとは違います)として考えてください。
ここまで準備ができたら以下のコマンドでDynamoDB Localをインストールします。
インストールしたら起動します。
これで最後に出てくる
次にテーブルを作成します。
実際にテーブルが作成されているか確認してみましょう。
作成されてますね!
これまたaws-cliを使います。
郵便番号と紐づく住所を登録します。
登録できたか確認します。
登録されてますね!
やっとコード書きます。環境変数を使うのでserverless.ymlも編集します。
コードは以下の通りです。devとstageで分ける必要がある箇所はところどころ環境変数を利用しています。
レスポンスにいろいろ情報を格納していますがALBから返却する際に必要となる情報です。詳しくは公式ドキュメントをご覧ください。
続いて、serveless.ymlの完成形です。全体を載せます。
まずはローカルで正しくデータが取得できるか試してみましょう。
invokeの後にlocalをつけることでローカルの環境で動作します。
また、パラメータの郵便番号をパスで受け取るように定義してあるので
とても簡単にデプロイできます。
これだけです。
serverless.ymlに定義されている内容でLambda関数の作成、DynamoDBのテーブル作成、IAMロールの設定、ソース一式を格納するS3バケットの作成など全て行ってくれます。内部的にはCloud Formationが使用されているようです。
1点、前述した通りデプロイ先の環境はstageとして定義しているので、stageオプションでstageを指定してください。
ローカルで登録した時とほぼ一緒です。
テーブル名の接頭辞がdevではなく、stageになっている点に注意してください。
取得してみます。
取得できましたね!
デプロイした関数を呼び出すには、slsのinvokeコマンドを利用します。
無事結果が返却されました!
尚、invokeコマンドは下記のようになっています。
最後にALBからLambdaを呼び出せるようにします。
まずALBを作成します。
ルーティングの設定で「ターゲットの種類」にLambda関数を選択してください。
次にターゲットの登録でリストからLambda関数を選択します。
これで準備完了です!
さっそくブラウザから呼び出してみます。
・・・502が返却されますね。
どうやらログをみてみるとLambdaの
の箇所で「event.pathParameters.zipcodeなんてないよ」と怒られているようです。
あらためてドキュメントを読み直してみると確かにeventとしてpathParametersが送られてこないようです・・。queryStringParametersはちゃんと渡ってきているのになぜ・・・。
ちなみに記載を省略しましたが、APIGatewayを経由したLambda呼び出しの場合はちゃんとevent.pathParametersが送られてきて正しく動作していました。
これは困った、、
しかし幸いなことにパスは送られてきているようです。
散々調べたのですが解決方法が見つからず苦渋の決断としてコードの一部を以下のように変更しました。
これで無事動作しました・・
素直にqueryStringParametersを利用した方が綺麗かもしれませんね・・。
本記事を書き終えた日の夜、たまたま勉強会でawsの中の人にお会いすることができました。そこで、pathParametersの件を聞いてたところ、APIGatewayはAPIを構築のためのもの(それだけではありませんが)であるためRESTで利用されるであろう、pathParametersに対応しているが、ALBはそうではないためまだ対応していないとの旨を教えていただきました。納得。ありがとうございました。
ちなみにALBの後ろにLambdaを置けるメリットとしては、たとえば既存システム改修の際、APIを呼び出すパスを変えずに、特定のリクエストの時だけ処理をLambda流すというように、徐々にServerlessな構成にバックエンドを置き換えていくことができるなどが考えられそうです。
いかがでしたでしょうか。ServerlessはLambda開発をとても快適にしてくれる一品だと思うのでまだ利用したことのない方は、是非試してみてください。
尚、今回の記事で紹介したserverless.ymlの設定は動作確認するための必要最低限なものになります。
実際にプロダクション環境で運用していくにはもう少し設定をいろいろやらないとワークしないので、また機会があれば紹介したいと思います。
フォトクリエイトでは新しい技術に興味がある方からの ご応募をお待ちしております。
エンジニア の @mackagy1 です。
この記事は Photocreate Advent Calendar 2018 の 19日目 の記事です。
はじめに
WEBサイトやアプリで住所入力フォームに郵便番号を入力すると住所が自動補完されるUIありますよね。仕組み的には単純で、郵便番号を入力したときに住所検索するAPIを呼び出して、そのレスポンスをフォームに表示するという流れになると思います。
こういうちょっとしたAPIを作るためにわざわざWEBサーバをたててそこにAPIのっけて・・というのは大げさな感じがします。
ということでLambdaを使って手軽に住所検索のAPIを作ってみたいなと思ってやってみました。
構成
クライアント → ALB → Lambda → DynamoDBDynamoDBには郵便番号と住所情報をつっこんでおきます。
今回はとりあえず1レコード入れて動作確認してみます。
対象
こんな方が読むと何か得るものがあると思います。- Lambdaの開発方法に興味がある
-
ALBのターゲットにLambdaが追加になったのが気になっている
いざ開発
Serverless Framework
まずはServerless Frameworkをインストールします。(以降、Serverless)ServerlessはLambda関数の作成、デプロイ、ローカルでのデバッグをらくちんにしてくれる優れもののフレームワークです。アプリケーションのコードと簡単な設定ファイルを用意すればすぐにAWS上に公開することもできます。Lambda開発をとても便利にしてくれます。
Serverlessは以下のようにインストールします。
$ npm install serverless -g
$ serverless -v 1.34.1
$ sls -v 1.34.1
サービスの作成
Serverlessはサービスという単位で開発します。$ sls create --template aws-nodejs --path zipcode-app Serverless: Generating boilerplate... Serverless: Generating boilerplate in "/Users/shotaro.shimizu/develop/zipcode-app" _______ __ | _ .-----.----.--.--.-----.----| .-----.-----.-----. | |___| -__| _| | | -__| _| | -__|__ --|__ --| |____ |_____|__| \___/|_____|__| |__|_____|_____|_____| | | | The Serverless Application Framework | | serverless.com, v1.34.1 -------' Serverless: Successfully generated boilerplate for template: "aws-nodejs"
--path
オプションにはサービスのルートディレクトリ名を、--template
にはLambdaで使用する言語のテンプレートを指定します。zipcode-appディレクトリが作成され、中に以下のファイルが作成されます。
- serverless.yml
- handler.js
コメント行が長いので有効な行だけ抽出してます。
service: zipcode-app # NOTE: update this with your service name provider: name: aws runtime: nodejs8.10 #本記事執筆時点のLambdaで対応しているnodeの最新バージョンは8.10なので注意してください functions: hello: handler: handler.hello
また、handler.jsはデフォルトで以下のようになっています。
'use strict'; module.exports.hello = async (event, context) => { return { statusCode: 200, body: JSON.stringify({ message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }), }; // Use this code if you don't use the http event with the LAMBDA-PROXY integration // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; };
「Hello World」的なレスポンスを返却するようになっていますね。
これでサービスの作成は完了です。
尚、サービス作成時に使用可能なtemplateは以下で確認可能です。
$ sls create --help --template / -t .................... Template for the service. Available templates: "aws-clojure-gradle","aws-clojurescript-gradle","aws-nodejs", "aws-nodejs-typescript", "aws-alexa-typescript", "aws-nodejs-ecma-script", "aws-python", "aws-python3", "aws-groovy-gradle", "aws-java-maven", "aws-java-gradle","aws-kotlin-jvm-maven","aws-kotlin-jvm-gradle", "aws-kotlin-nodejs-gradle", "aws-scala-sbt", "aws-csharp", "aws-fsharp", "aws-go", "aws-go-dep", "aws-go-mod", "aws-ruby", "azure-nodejs", "cloudflare-workers", "cloudflare-workers-enterprise", "fn-nodejs", "fn-go", "google-nodejs", "kubeless-python", "kubeless-nodejs", "openwhisk-java-maven", "openwhisk-nodejs", "openwhisk-php", "openwhisk-python", "openwhisk-ruby", "openwhisk-swift", "spotinst-nodejs", "spotinst-python", "spotinst-ruby", "spotinst-java8", "plugin" and "hello-world"
今回はnodeを使います。
AWSアカウントの設定
続いて、AWSのアカウントの設定を行います。Serverlessでは、credential(~/.aws/credentials)を利用しています。
今回はまだこのファイルがないので作成します。
$ sls config credentials --provider aws --key EXAMPLE --secret EXAMPLEKEY Serverless: Setting up AWS... Serverless: Saving your AWS profile in "~/.aws/credentials"... Serverless: Success! Your AWS access keys were stored under the "default" profile.
成功すると
~/.aws/credentials
に認証情報が作成されます。$ cat ~/.aws/credentials [default] aws_access_key_id = EXAMPLE aws_secret_access_key = EXAMPLEKEY
DynamoDB Local
一通り準備ができたので本題に入ります。改めて今回のAPIでやりたいことを整理します。
- 郵便番号をAPIに渡すと、対応する住所が返却される
今回はマスタ管理にDynamoDBを利用します。
インストール
ということでDynamoDBをローカルで動かせるようにします。これもServerlessを使うことで簡単に実現することができます。
必要となるのは以下のツール、プラグインとなります。
- DynamoDB Local
- AWSが公式で提供しているローカルでDynamoDBを動かすツールです
- serverless-dynamodb-local
- Serverlessプラグイン、上記DynamoDB Localのインストールやテーブル作成を自動的に行ってくれます。
サービスのルートディレクトリで以下を実行します。
$ yarn add --dev serverless-dynamodb-local info No lockfile found. [1/4] �� Resolving packages... / ** 長いので略 **/ ✨ Done in 8.85s.
一番下とかでOKです。
plugins: - serverless-dynamodb-local custom: dynamodb: start: port: 8000
$ sls Commands * You can run commands with "serverless" or the shortcut "sls" * Pass "--verbose" to this command to get in-depth plugin info * Pass "--no-color" to disable CLI colors * Pass "--help" after any <command> for contextual help Framework * Documentation: https://serverless.com/framework/docs/ /** 長いので略 **/ dynamodb ...................... undefined dynamodb migrate .............. Creates local DynamoDB tables from the current Serverless configuration dynamodb seed ................. Seeds local DynamoDB tables with data dynamodb start ................ Starts local DynamoDB dynamodb remove ............... Removes local DynamoDB dynamodb install .............. Installs local DynamoDB /** 長いので略 **/
よくわからない箇所は公式ドキュメントをご覧ください。
provider: name: aws runtime: nodejs8.10 stage: ${opt:stage, self:custom.defaultStage} ### 追記 region: ap-northeast-1 ### 追記 ### ここから追記 ### resources: Resources: Addresses: Type: AWS::DynamoDB::Table Properties: TableName: ${self:provider.stage}_addresses AttributeDefinitions: - AttributeName: zipcode AttributeType: S KeySchema: - AttributeName: zipcode KeyType: HASH ProvisionedThroughput: ##### 注1 ReadCapacityUnits: 1 WriteCapacityUnits: 1 ### ここまで追記 ### custom: dynamodb: start: port: 8000 defaultStage: dev ### 追記
テーブル名は動作させる環境によって変えたいので接頭辞を変数化しています。
TableName: ${self:provider.stage}_addresses
provider: stage: ${opt:stage, self:custom.defaultStage} #これ
opt:stage
はオプションでstageを利用した時の値が入ります。stageはdeployや、invokeする時の環境を指定するオプションです。stageが指定されなかった場合、self:custom.defaultStage
の値が利用されます。これは以下で定義されています。custom: defaultStage: dev #これ
ここまで準備ができたら以下のコマンドでDynamoDB Localをインストールします。
$ sls dynamodb install
$ sls dynamodb start Serverless: Load command config Serverless: Load command config:credentials Serverless: Load command create Serverless: Load command install Serverless: Load command package Serverless: Load command deploy Serverless: Load command deploy:function Serverless: Load command deploy:list /** 略 **/ Serverless: Load command dynamodb:install Serverless: Invoke dynamodb:start Dynamodb Local Started, Visit: http://localhost:8000/shell
http://localhost:8000/shell
にアクセスしてコンソールが表示されれば起動成功です。
テーブル作成
次にテーブルを作成します。$ sls dynamodb migrate Serverless: DynamoDB - created table dev_addresses
$ aws dynamodb list-tables --endpoint-url http://localhost:8000 { "TableNames": [ "dev-addresses" ] }
テストデータ登録
これまたaws-cliを使います。郵便番号と紐づく住所を登録します。
$ aws dynamodb put-item --table-name dev_addresses --item '{"zipcode":{"S":"2160007"},"address":{"S":"神奈川県川崎市宮前区小台"}}' --endpoint-url http://localhost:8000
$ aws dynamodb get-item --table-name dev_addresses --key '{"zipcode":{"S":"2160007"}}' --endpoint-url http://localhost:8000 { "Item": { "zipcode": { "S": "2160007" }, "address": { "S": "神奈川県川崎市宮前区小台" } } }
Handlerの実装
やっとコード書きます。環境変数を使うのでserverless.ymlも編集します。コードは以下の通りです。devとstageで分ける必要がある箇所はところどころ環境変数を利用しています。
handler.js
var AWS = require('aws-sdk'); var dynamo = new AWS.DynamoDB.DocumentClient( { region: process.env["DYNAMO_DB_REGION"], endpoint: process.env["DYNAMO_DB_ENDPOINT"] } ); module.exports.address = async (event) => { var params = { TableName: `${process.env["STAGE"]}_addresses`, Key: {"zipcode": event.pathParameters.zipcode} }; return dynamo.get(params).promise().then((data) => { if (data.Item === undefined) { var error404 = { message : "Resource not found" }; return response = { statusCode: 404, statusDescription: '404 Resource Not Found', isBase64Encoded: false, headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: `${JSON.stringify(error404)}` }; } return response = { statusCode: 200, statusDescription: '200 OK', isBase64Encoded: false, headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: `${JSON.stringify(data.Item)}` }; }).catch((err) => { console.error(`[Error]: ${JSON.stringify(err)}`); var error500 = { message : "Internal Server Error" }; return response = { statusCode: 500, isBase64Encoded: false, statusDescription: '500 Internal Server Error', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: `${JSON.stringify(error500)}` }; });
続いて、serveless.ymlの完成形です。全体を載せます。
serverless.yaml
service: zipcode-app provider: name: aws runtime: nodejs8.10 stage: ${opt:stage, self:custom.defaultStage} region: ap-northeast-1 environment: STAGE: ${self:provider.stage} ### 全体で使用できる環境変数追加 ### ここからIAMロールの定義追加 ### iamRoleStatements: - Effect: Allow Action: - dynamodb:GetItem Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.stage}_*" ### ここまでIAMロールの定義追加 ### ### ここからまるっと書き換え ### functions: address: handler: handler.address environment: ${self:custom.environment.${self:provider.stage}} events: - http: path: zipcode/{zipcode} method: get resources: Resources: Addresses: Type: AWS::DynamoDB::Table Properties: TableName: ${self:provider.stage}_addresses AttributeDefinitions: - AttributeName: zipcode AttributeType: S KeySchema: - AttributeName: zipcode KeyType: HASH BillingMode: PAY_PER_REQUEST # 注1で説明したようにProvisionedThroughputの指定から変更 plugins: - serverless-dynamodb-local custom: dynamodb: start: port: 8000 defaultStage: dev ### ここから追記 ### environment: dev: DYNAMO_DB_REGION: localhost DYNAMO_DB_ENDPOINT: http://localhost:8000 stage: DYNAMO_DB_REGION: ap-northeast-1 DYNAMO_DB_ENDPOINT: dynamodb.ap-northeast-1.amazonaws.com ### ここまで追記 ###
ローカルでの動作確認
まずはローカルで正しくデータが取得できるか試してみましょう。$ sls invoke local -f address --data '{ "pathParameters": {"zipcode":"2160007"}}' { "statusCode": 200, "body": "{\"zipcode\":\"2160007\",\"address\":\"神奈川県川崎市宮前区小台\"}" }
また、パラメータの郵便番号をパスで受け取るように定義してあるので
--data
でパスパラメータを渡しています。
AWSヘのデプロイ
とても簡単にデプロイできます。$ sls deploy --stage stage Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service .zip file to S3 (16.36 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... .......... Serverless: Stack update finished... Service Information service: zipcode-app stage: stage region: ap-northeast-1 stack: zipcode-app-stage api keys: None endpoints: GET - https://bzeiy8uas9.execute-api.ap-northeast-1.amazonaws.com/stage/zipcode/{zipcode} functions: address: zipcode-app-stage-address layers: None
serverless.ymlに定義されている内容でLambda関数の作成、DynamoDBのテーブル作成、IAMロールの設定、ソース一式を格納するS3バケットの作成など全て行ってくれます。内部的にはCloud Formationが使用されているようです。
1点、前述した通りデプロイ先の環境はstageとして定義しているので、stageオプションでstageを指定してください。
DynamoDBにデータ登録
ローカルで登録した時とほぼ一緒です。$ aws dynamodb put-item --table-name stage_addresses --item '{"zipcode":{"S":"2160007"},"address":{"S":"神奈川県川崎市宮前区小台"}}'
取得してみます。
$ aws dynamodb get-item --table-name stage_addresses --key '{"zipcode":{"S":"2160007"}}' { "Item": { "zipcode": { "S": "2160007" }, "address": { "S": "神奈川県川崎市宮前区小台" } } }
Lambdaを呼び出す
デプロイした関数を呼び出すには、slsのinvokeコマンドを利用します。$ sls invoke -f address -s stage --data '{ "pathParameters": {"zipcode":"2160007"}}' { "statusCode": 200, "body": "{\"zipcode\":\"2160007\",\"address\":\"神奈川県川崎市宮前区小台\"}" }
尚、invokeコマンドは下記のようになっています。
$ sls invole --help Plugin: Invoke invoke ........................ Invoke a deployed function invoke local .................. Invoke function locally --function / -f (required) ......... The function name --stage / -s ....................... Stage of the service --region / -r ...................... Region of the service --path / -p ........................ Path to JSON or YAML file holding input data --type / -t ........................ Type of invocation --log / -l ......................... Trigger logging data output --data / -d ........................ Input data --raw .............................. Flag to pass input data as a raw string
-f
でファンクションのaddressを指定したことになります。
ALBとLambdaを繋げる
最後にALBからLambdaを呼び出せるようにします。まずALBを作成します。
ルーティングの設定で「ターゲットの種類」にLambda関数を選択してください。
これで準備完了です!
動作確認
さっそくブラウザから呼び出してみます。https://****.amazonaws.com/zipcode-app-stage-address/zipcode/2160007
どうやらログをみてみるとLambdaの
hanlder.js
var params = { TableName: `${process.env["STAGE"]}_addresses`, Key: {"zipcode": event.pathParameters.zipcode} };
あらためてドキュメントを読み直してみると確かにeventとしてpathParametersが送られてこないようです・・。queryStringParametersはちゃんと渡ってきているのになぜ・・・。
ちなみに記載を省略しましたが、APIGatewayを経由したLambda呼び出しの場合はちゃんとevent.pathParametersが送られてきて正しく動作していました。
これは困った、、
苦渋の決断
しかし幸いなことにパスは送られてきているようです。散々調べたのですが解決方法が見つからず苦渋の決断としてコードの一部を以下のように変更しました。
var zipcode; if (event.pathParameters === undefined) { zipcode = event.path.split("/").pop(); // pathをsplitして最後の文字列を郵便番号に } else { zipcode = event.pathParameters.zipcode; } var params = { TableName: `${process.env["STAGE"]}_addresses`, Key: {"zipcode": zipcode} };
素直にqueryStringParametersを利用した方が綺麗かもしれませんね・・。
後日談的なもの
本記事を書き終えた日の夜、たまたま勉強会でawsの中の人にお会いすることができました。そこで、pathParametersの件を聞いてたところ、APIGatewayはAPIを構築のためのもの(それだけではありませんが)であるためRESTで利用されるであろう、pathParametersに対応しているが、ALBはそうではないためまだ対応していないとの旨を教えていただきました。納得。ありがとうございました。ちなみにALBの後ろにLambdaを置けるメリットとしては、たとえば既存システム改修の際、APIを呼び出すパスを変えずに、特定のリクエストの時だけ処理をLambda流すというように、徐々にServerlessな構成にバックエンドを置き換えていくことができるなどが考えられそうです。
まとめ
いかがでしたでしょうか。ServerlessはLambda開発をとても快適にしてくれる一品だと思うのでまだ利用したことのない方は、是非試してみてください。尚、今回の記事で紹介したserverless.ymlの設定は動作確認するための必要最低限なものになります。
実際にプロダクション環境で運用していくにはもう少し設定をいろいろやらないとワークしないので、また機会があれば紹介したいと思います。
フォトクリエイトでは新しい技術に興味がある方からの ご応募をお待ちしております。
コメント
コメントを投稿