API GatewayのLambda連携をAWS CLIからやってみる

API Gateway

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS CLI が 2015/10/26 リリースのバージョン 1.9.0 から API Gateway に対応しました。

API Gateway が発表されたのは 2015 年 7 月ですので、CLI のリリースまでに三ヶ月以上を要しました。 CLI 対応記念として、getting started ドキュメントにある次のチュートリアルを AWS CLI から実行してみます。

演習: Lambda 関数 - API Gateway を使用して、カスタム API を作成し、一連の AWS Lambda 関数に接続した後に、API から Lambda 関数を呼び出します。

AWS CLI は生の REST API をガシガシ叩くだけですので、よりアプリケーション開発者フレンドリーに API Gateway&Lambda で開発したい方は、素直に JAWS フレームワークなどをご検討ください。

チュートリアルのゴール

「MyDemoAPI」という API Gateway を用意し、パス(リソース)「/mydemoresource」に GET/POST メソッドを定義します。

GET メソッドは Lambda 関数「GetHelloWorld」を、 POST メソッドは Lambda 関数「GetHelloWithName」 を invoke します。

api-gateway-lambda-tutorial overview

作成した API を test ステージにデプロイし 定義した GET/POST メソッドにリクエストします。

以下の流れで作業します。

  1. AWS CLI をインストールする
  2. API を作成する
  3. リソースを作成する
  4. Lambda 関数を作成する
  5. GET メソッドを作成してテストする
  6. POST メソッドを作成してテストする
  7. API をデプロイする
  8. API をテストする
  9. クリーンアップ

注意事項

API Gateway を AWS CLI から操作するとハマるかもしれない箇所を3点ほど先に述べます。

一つ目。API Gateway の REST API は Hypertext Application Language(HAL) がベースになっており、これまでのような XML ではなく JSON でやり取りします(HAL 自体は XML もサポートします)。 AWS CLI の HAL サポートはまだこなれていないので、細かい動作が胡散臭いです。 暖かく見守ってください(すでにバグを3つほど踏抜きました)。

二つ目。今回のチュートリアルでは CLIENT -> API Gateway -> Lambda と連携します。 CLIENT -> API Gateway のメソッド(--http-method)によらず、API Gateway は POST メソッド(--integration-http-method)で Lambda 関数を invoke します。

三つ目。API Gateway が Lambda を invoke できるように Lambda 関数のパーミッションを設定しましょう。 パーミッションを設定し忘れると "Execution failed due to configuration error: Invalid permissions on Lambda function" というエラーが発生します。 マネージメントコンソールから操作する際は、パーミッション設定の確認ダイアログが表示されます。

それでは本題に戻ります。

1. AWS CLI をインストールする

AWS CLI からコマンド実行するため、なにはともあれ AWS CLI をインストールします。 AWS CLI はパッケージ管理ツールの pip からインストールします。

$ pip install awscli --upgrade
$ aws --version
aws-cli/1.9.2 Python/2.7.10 Darwin/14.5.0 botocore/1.3.2

AWS CLI のバージョンが少なくとも 1.9.2 以上であることを確認してください。

API Gateway のサービス名は apigateway です。 $ aws apigateway help を叩くと、大量のコマンド一覧のヘルプ画面が出てくるはずです。

$ aws apigateway help
APIGATEWAY()                                                      APIGATEWAY()


       apigateway -

       Amazon API Gateway helps developers deliver robust, secure and scalable
       mobile and web application backends. Amazon API Gateway allows develop-
       ers to securely connect mobile and web applications to APIs that run on
       AWS Lambda, Amazon EC2, or other publicly addressable web services that
       are hosted outside of AWS.

       o create-api-key
       o create-base-path-mapping
       o create-deployment
       o create-domain-name
       o create-model
       o create-resource
       o create-rest-api
       o create-stage
       o delete-api-key
       o delete-base-path-mapping
       o delete-client-certificate
       o delete-deployment
       o delete-domain-name
       o delete-integration
       o delete-integration-response
       o delete-method
       o delete-method-response
       o delete-model
       o delete-resource
       o delete-rest-api
       o delete-stage
       o flush-stage-cache
       o generate-client-certificate
       o get-account
       o get-api-key
       o get-api-keys
       o get-base-path-mapping
       o get-base-path-mappings
       o get-client-certificate
       o get-client-certificates
       o get-deployment
       o get-deployments
       o get-domain-name
       o get-domain-names
       o get-integration
       o get-integration-response
       o get-method
       o get-method-response
       o get-model
       o get-model-template
       o get-models
       o get-resource
       o get-resources
       o get-rest-api
       o get-rest-apis
       o get-sdk
       o get-stage
       o get-stages
       o help
       o put-integration
       o put-integration-response
       o put-method
       o put-method-response
       o test-invoke-method
       o update-account
       o update-api-key
       o update-base-path-mapping
       o update-client-certificate
       o update-deployment
       o update-domain-name
       o update-integration
       o update-integration-response
       o update-method
       o update-method-response
       o update-model
       o update-resource
       o update-rest-api
       o update-stage


                                                                  APIGATEWAY()

help コマンドをのぞいても 67 個あります。賑やかですね。

コマンドリファレンスはこちらです http://docs.aws.amazon.com/cli/latest/reference/apigateway/index.html

2. API を作成する

API の作成は create-rest-api API を使います。 API 名は "MyDemoAPI" です。

$ aws apigateway create-rest-api --name MyDemoAPI --description "This is my API for demonstration purposes"
{
    "id": "asdfqwer",
    "name": "MyDemoAPI",
    "description": "This is my API for demonstration purposes",
    "createdDate": 1446272225
}
$ aws apigateway get-rest-apis # API 一覧を確認
{
    "items": [
        {
            "description": "This is my API for demonstration purposes",
            "createdDate": 1446272225,
            "id": "asdfqwer",
            "name": "MyDemoAPI"
        }
    ]
}
$ REST_API_ID="asdfqwer"

なお name が重複していても作成可能です。

最終的に API は REST_API_ID.execute-api.REGION.amazonaws.com のホスト名でアクセスすることになります。

3. リソースを作成する

API 作成直後はルート(/)リソースしか存在しません。

$ aws apigateway get-resources --rest-api-id $REST_API_ID
{
    "items": [
        {
            "path": "/",
            "id": "xzw05h7r0h"
        }
    ]
}
$ ROOT_ID="xzw05h7r0h"

ルート直下にリソース mydemoresource を追加します。

リソースの作成は create-resource API を使います。

親となるリソースルートを --parent-id で指定します。

$ aws apigateway create-resource --rest-api-id $REST_API_ID --parent-id $ROOT_ID --path-part "mydemoresource"
{
    "path": "/mydemoresource",
    "pathPart": "mydemoresource",
    "id": "zzzzzz",
    "parentId": "xxxxxxxx"
}
$ aws apigateway get-resources --rest-api-id $REST_API_ID
{
    "items": [
        {
            "path": "/",
            "id": "xxxxxxxx"
        },
        {
            "path": "/mydemoresource",
            "id": "zzzzzz",
            "pathPart": "mydemoresource",
            "parentId": "xxxxxxxx"
        }
    ]
}

マネジメントコンソールと異なり、pathpathPart を個別に指定できないようです。

4. Lambda 関数を作成する

チュートリアルに従い、Lambda 関数を2つ(GetHelloWithName と GetHelloWithName)作成してください。

$ aws lambda list-functions
{
  "Functions": [
    {
      ...
      "FunctionName": "GetHelloWithName", 
      "FunctionArn": "arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWithName", 
      ...
    }, 
    {
      ...
      "FunctionName": "GetHelloWorld", 
      "FunctionArn": "arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld", 
      ...
    },
  ]
}

FunctionArn はあとで利用します。

5. GET メソッドを作成してテストする

リソース mydemoresource に GET すると、Lambda 関数 GetHelloWorld を呼び出すように GET メソッドを定義します。

マネージメントコンソールでは次の画面に相当します。

api-get-lambda-integration setting screen

  • Method Request(クライアントから API Gateway へのリクエスト)
  • put-integration(API Gateway から Lambda へのリクエスト)
  • put-integration-response(Lambda から API Gateway へのレスポンス)
  • put-method-response(API Gateway からクライアントへのレスポンス)

という4項目をそれぞれ AWS CLI から定義します。

Black Blet Tech シリーズ Amazon API Gateway の P.49 から先も合わせてご確認ください。

Method Request(クライアントから API Gateway へのリクエスト)

リソース /mydemoresource に対して GET メソッドを用意します(--http-method GET)。 認証なしでリクエストできるようにするため --authorization-type None とします。

http://docs.aws.amazon.com/cli/latest/reference/apigateway/put-method.html

$ aws apigateway put-method \
  --rest-api-id $REST_API_ID \
  --resource-id $RESOURCE_ID \
  --http-method GET \
  --authorization-type NONE \
  --no-api-key-required \
  --request-parameters {}
{
    "apiKeyRequired": false,
    "httpMethod": "GET",
    "authorizationType": "NONE",
    "requestParameters": {}
}

put-integration(API Gateway から Lambda へのリクエスト)

  • API Gateway から Lambda 関数へのリクエストを定義します。
  • インテグレーション先が AWS のサービスのため、--type AWS とします。
  • API Gateway から Lambda 関数へは POST でリクエストします (--integration-http-method POST)。 Lambda 関数は --uri で指定します。

uri は "arn:aws:apigateway:REGION:lambda:path/2015-03-31/functions/LAMBDA_FUNCTION_ARN/invocations" という形をしていて、 具体的には "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld/invocations" というようになります。

$ aws apigateway put-integration \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method GET \
      --integration-http-method POST \
      --type AWS \
      --uri "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld/invocations"
{
    "httpMethod": "POST",
    "type": "AWS",
    "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld/invocations",
    "cacheNamespace": "zzzzzz"
}

put-method-response(API Gateway からクライアントへのレスポンス)

API Gateway からクライアントへのレスポンスを定義します。 レスポンスは JSON 形式のため --response-models '{"application/json": "Empty"}' とします。

"Empty" はモデルの名前です。

モデルは API ごとに存在し CLI からは $ aws apigateway get-models --rest-api-id $REST_API_ID で確認出来ます。

$ aws apigateway put-method-response \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method GET \
      --status-code 200 \
      --response-models '{"application/json": "Empty"}'
{
    "responseModels": {
        "application/json": "Empty"
    },
    "statusCode": "200"
}

put-integration-response(Lambda から API Gateway へのレスポンス)

Lambda から API Gateway へのレスポンスを定義します。

API Gateway ではテンプレートと言う概念があり、Lambda 関数のレスポンスに対してスキーマ変換出来ます。

http://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html

--response-templates 引数でこのテンプレートを指定します。

今回はスキーマ変換せず、 JSON をそのまま返すので --response-templates '{"application/json": null}' としたいところなのですが、AWS CLI にバグが あるため、ワークアラウンドとして --response-templates '{"application/json": ""}' と空文字を指定します。

BUG URL https://github.com/aws/aws-cli/issues/1610

$ aws apigateway put-integration-response \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method GET \
      --status-code 200 \
      --response-templates '{"application/json": null}'

Parameter validation failed:
Invalid type for parameter responseTemplates.application/json, value: None, type: <type 'NoneType'>, valid types: <type 'basestring'>

# workaround
$ aws apigateway put-integration-response \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method GET \
      --status-code 200 \
      --response-templates '{"application/json": ""}'
{
    "statusCode": "200",
    "responseTemplates": {
        "application/json": null
    }
}

Lambda 関数のパーミッション変更

最後に、API Gateway が Lambda 関数を invoke できるように Lambda 関数のパーミッションを変更します。

lambda add-permission API を使います。

  • --action"lambda:InvokeFunction"
  • --principalapigateway.amazonaws.com
  • --source-arn"arn:aws:execute-api:ap-northeast-1:ACOUNT_ID:API_ID/*/HTTP_METHOD/RESOURCE_NAME"

とします。

$ aws lambda add-permission --function-name GetHelloWorld \
  --statement-id f3385cd3-c2f8-49e5-ba72-d2801e5f93d8 \
  --action "lambda:InvokeFunction" \
  --principal  apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:ap-northeast-1:1234567890:asdfqwer/*/GET/mydemoresource"
{
    "Statement": "{\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:execute-api:ap-northeast-1:1234567890:asdfqwer/*/GET/mydemoresource\"}},\"Action\":[\"lambda:InvokeFunction\"],\"Resource\":\"arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"apigateway.amazonaws.com\"},\"Sid\":\"f3385cd3-c2f8-49e5-ba72-d2801e5f93d8\"}"
}
$ aws lambda get-policy --function-name GetHelloWorld | jq -r .Policy  | jq .
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:execute-api:ap-northeast-1:1234567890:asdfqwer/*/GET/mydemoresource"
        }
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld",
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Sid": "f3385cd3-c2f8-49e5-ba72-d2801e5f93d8"
    }
  ],
  "Id": "default"
}

GET メソッドのテスト実行

ここで定義した mydemoresource リソースの GET メソッドを確認します。

まずは定義を確認確認します。

$ aws apigateway get-method --rest-api-id $REST_API_ID --resource-id $RESOURCE_ID --http-method GET
{
    "apiKeyRequired": false,
    "httpMethod": "GET",
    "methodIntegration": {
        "integrationResponses": {
            "200": {
                "responseTemplates": {
                    "application/json": null
                },
                "statusCode": "200"
            }
        },
        "cacheKeyParameters": [],
        "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld/invocations",
        "httpMethod": "POST",
        "cacheNamespace": "zzzzzz",
        "type": "AWS"
    },
    "requestParameters": {},
    "methodResponses": {
        "200": {
            "responseModels": {
                "application/json": "Empty"
            },
            "statusCode": "200"
        }
    },
    "authorizationType": "NONE"
}

GET メソッドを実際に呼び出しましょう。

$ aws apigateway test-invoke-method --rest-api-id $REST_API_ID --resource-id $RESOURCE_ID --http-method GET --path-with-query-string ''
{
    "status": 200,
    "body": "{\"Hello\":\"World\"}",
    "log": "Execution log for request test-request...[snip]...Successfully completed execution\n",
    "latency": 1111,
    "headers": {
        "Content-Type": "application/json"
    }
}

status が 200 で body の内容から期待どおりのレスポンスがかえってきています。

6. POST メソッドを作成してテストする

GET メソッドと同じ要領でリソース mydemoresource に POST すると Lambda 関数 GetHelloWithName を呼び出すように POST メソッドを定義します。

  • Method Request(クライアントから API Gateway へのリクエスト)
  • put-integration(API Gateway から Lambda へのリクエスト)
  • put-integration-response(Lambda から API Gateway へのレスポンス)
  • put-method-response(API Gateway からクライアントへのレスポンス)

という4項目をそれぞれ AWS CLI から定義します。

Black Blet Tech シリーズ Amazon API Gateway の P.49 から先も合わせてご確認ください。

Method Request(クライアントから API Gateway へのリクエスト)

リソース /mydemoresource に対して POST メソッドを用意します。(--http-method POST) 認証なしでリクエストできるようにするため --authorization-type None とします。

$ aws apigateway put-method \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method POST \
      --authorization-type NONE \
      --no-api-key-required \
      --request-parameters {}
{
    "apiKeyRequired": false,
    "httpMethod": "POST",
    "authorizationType": "NONE",
    "requestParameters": {}
}

put-integration(API Gateway から Lambda へのリクエスト)

GET の時と同じです。

$ aws apigateway put-integration \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method POST \
      --integration-http-method POST \
      --type AWS \
      --uri "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWithName/invocations"
{
    "httpMethod": "POST",
    "type": "AWS",
    "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWithName/invocations",
    "cacheNamespace": "zzzzzz"
}

put-integration-response(Lambda から API Gateway へのレスポンス)

GET の時と同じです。

$ aws apigateway put-method-response \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method POST \
      --status-code 200 \
      --response-models '{"application/json": "Empty"}'
{
    "responseModels": {
        "application/json": "Empty"
    },
    "statusCode": "200"
}

put-method-response(API Gateway からクライアントへのレスポンス)

GET の時と同じです。

$ aws apigateway put-integration-response \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method POST \
      --status-code 200 \
      --response-templates '{"application/json": ""}'
{
    "statusCode": "200",
    "responseTemplates": {
        "application/json": null
    }
}

Lambda 関数のパーミッション変更

最後に、API Gateway が Lambda 関数を invoke できるように Lambda 関数のパーミッションを変更します。

GET の時とは

  • --function-name(Lambda 関数名)
  • --source-arn(GET から POST への変更)

が変わっています。

$ aws lambda add-permission --function-name GetHelloWithName \
  --statement-id 72e0a706-02e8-479f-affb-0a2dcc5d4a29 \
  --action "lambda:InvokeFunction" \
  --principal  apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:ap-northeast-1:1234567890:asdfqwer/*/POST/mydemoresource"

POST メソッドのテスト実行

ここで定義した mydemoresource リソースの POST メソッドを確認します。

$ aws apigateway get-method --rest-api-id $REST_API_ID --resource-id $RESOURCE_ID --http-method POST
{
    "apiKeyRequired": false,
    "httpMethod": "POST",
    "methodIntegration": {
        "integrationResponses": {
            "200": {
                "responseTemplates": {
                    "application/json": null
                },
                "statusCode": "200"
            }
        },
        "cacheKeyParameters": [],
        "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWithName/invocations",
        "httpMethod": "POST",
        "cacheNamespace": "zzzzzz",
        "type": "AWS"
    },
    "requestParameters": {},
    "methodResponses": {
        "200": {
            "responseModels": {
                "application/json": "Empty"
            },
            "statusCode": "200"
        }
    },
    "authorizationType": "NONE"
}

POST メソッドを実際に呼び出しましょう。 POST リクエスト時のボディーは --body で渡します。

$ aws apigateway test-invoke-method \
      --rest-api-id $REST_API_ID \
      --resource-id $RESOURCE_ID \
      --http-method POST \
      --path-with-query-string '' \
      --body '{"name":"John"}'
{
    "status": 200,
    "body": "{\"Hello\":\"John\"}",
    "log": "Execution log for request test-request...Successfully completed execution\n",
    "latency": 69,
    "headers": {
        "Content-Type": "application/json"
    }
}

レスポンスの body が {"Hello":"John"} となっており、POST リクエスト内容が反映されています。

7. API をデプロイする

定義したAPIを "test" ステージにデプロイします。

https://REST_API_ID.execute-api.REGION.amazonaws.com/STAGE_NAME/RESOURCE_NAME の形式でアクセスできるようになります。

$ aws apigateway create-deployment \
      --rest-api-id $REST_API_ID \
      --stage-name test \
      --stage-description "This is a test" \
      --description "Calling Lambda functions walkthroug"
{
    "description": "Calling Lambda functions walkthroug",
    "id": "aaaaaa",
    "createdDate": 1446288905
}
$ aws apigateway get-deployments --rest-api-id $REST_API_ID
{
    "items": [
        {
            "createdDate": 1446288905,
            "id": "aaaaaa",
            "description": "Calling Lambda functions walkthroug"
        }
    ]
}
$ aws apigateway get-stages --rest-api-id $REST_API_ID
{
    "item": [
        {
            "description": "This is a test",
            "stageName": "test",
            "cacheClusterEnabled": false,
            "cacheClusterStatus": "NOT_AVAILABLE",
            "deploymentId": "aaaaaa",
            "lastUpdatedDate": 1446288905,
            "createdDate": 1446288905,
            "methodSettings": {}
        }
    ]
}

8. API をテストする

curl から GET/POST それぞれでリソース mydemoresource にアクセスします。

GET メソッド

$ curl https://$REST_API_ID.execute-api.ap-northeast-1.amazonaws.com/test/mydemoresource
{"Hello":"World"}

POST メソッド

$ curl -H "Content-Type: application/json" -X POST -d "{\"name\": \"John\"}" https://$REST_API_ID.execute-api.ap-northeast-1.amazonaws.com/test/mydemoresource
{"Hello":"John"}

期待通りですね。

9. クリーンアップ

最後に API を削除します。

$ aws apigateway delete-rest-api --rest-api-id $REST_API_ID
$ aws apigateway get-rest-apis
{
    "items": []
}

Hypertext Application Language(HAL) について

API Gateway の REST API Reference にあるように、この API は JSON HAL をしゃべります。

The Amazon API Gateway web service is a resource-based API that uses Hypertext Application Language (HAL). http://docs.aws.amazon.com/apigateway/api-reference/

試しにcreate-rest-api API を --debug オプション付きで実行してみましょう。

$ aws apigateway create-rest-api --name dummy --debug
...
2015-10-31 19:59:18,003 - MainThread - botocore.vendored.requests.packages.urllib3.connectionpool - DEBUG - "POST /restapis HTTP/1.1" 201 120
2015-10-31 19:59:18,004 - MainThread - botocore.parsers - DEBUG - Response headers: {'x-amzn-requestid': '6c872d07-7fbe-11e5-bcdc-93557855b8e7', 'date': 'Sat, 31 Oct 2015 10:59:14 GMT', 'content-length': '120', 'content-type': 'application/json'}
2015-10-31 19:59:18,004 - MainThread - botocore.parsers - DEBUG - Response body:
{"createdDate":1446289154,"description":"This is my API for demonstration purposes","id":"fdp48380xl","name":"DemoAPI"}
...

確かに

  • HTTP ステータス 201:Created
  • 'content-type': 'application/json'
  • レスポンスボディー {"createdDate":1446289154,"description":"This is my API for demonstration purposes","id":"fdp48380xl","name":"DemoAPI"}

という JSON が返ってきていますね。

これまでの XML との決裂をわかった上で API Gateway だけ独自路線で HAL をねじ込んできた API Gateway のプロダクトマネージャーやアーキテクトは相当なやり手と思われ、周りを巻き込んだリリースまでの道のりについてどこかで語って欲しいですね。

まとめ

生 REST API は辛い。

参考

AWS Cloud Roadshow 2017 福岡

  • Rui Peng

    Thank you for sharing your knowledge.
    It is a surprise to me that I, as a Chinese, can read your article without many difficulties. I can only speak Chinese and English. But I can understand your article. Thank you Japanese!