Pythonしか書きたくない人が、AWS上にWEBアプリを作ってみました。

# 成果物

![Jul-22-2019 07-31-03](https://i.imgur.com/HSwWAqX.gif)

# モチベーションと概要
- Windows, Macの両方で使えるような社内ツールを作りたい。
    - Pythonが手軽だがUIも無いし、パッケージのインストールなど環境依存の問題がある…。
    - WEBで完結していたらいいんじゃないか。
    - しかしWEB系の言語を全く知らないし、そちらに勉強コストをかける（精神的）余裕もない。
    - AWS Lambdaを使えばPythonで実際の処理を行えそうだ！
- 筆者はHTMLからAWSに渡ってド素人です。なのでPython以外は極力お手軽に行きたい方針です。
- 見よう見まねの構成図は以下の通り。
    - 今回使用したAWSのサービスは`S3`・`API Gateway`・`AWS Lambda`です。
    - 図は[draw.io](https://www.draw.io/)で作成。AWS用の図があり、WEBで完結している便利ツール。
    - なんかいっぱいあって把握が大変そうに見えますが、一旦理解したあとは目的のLambdaの処理（Pythonで書ける）に注力できると思います。

![](https://i.imgur.com/7NbbmKg.png)

| 使うもの | 役割  |
| --- | --- |
| HTML | 入力フォームを提供する |
| Javascript| 入力フォームの内容を持って、API GatewayへHTTPリクエストを行う |
| API Gateway | リクエストされた内容からパラメータを取得してLambdaへjson形式で渡す。|
| AWS Lambda| 与えられたパラメータの処理をここで行う。返り値はAPI Gatewayに渡す。（入力と逆の流れ）|

# 実装
## Lambda関数（Python）の作成
### 参考
- [コンソールで Lambda 関数を作成する](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/getting-started-create-function.html)

### 作業例
- 関数の作成をクリック。

![-w1155](https://i.imgur.com/stGd6X0.jpg)

- 今回は一から作成、を選択。

![-w1071](https://i.imgur.com/ATu6Qt7.jpg)

- 関数名を入力。
- ランタイム（関数を記述する言語）にはPythonを指定します。
- 実行ロールは目的の動作に必要なものを指定します。
    - S3上のファイルにアクセスをしたい場合などは、このときに詳細にロールを設定できます
- 関数の作成をクリック。

![-w1053](https://i.imgur.com/JnVs8KC.jpg)

- Lambda関数を記述します。
- 今回は`メッセージ`が入力されたら、`「メッセージ」という文章をLambdaで受信しました。`と返すだけのシンプルな処理をPythonで記述します。
    
``` Python:limbda_function.py
import json

def lambda_handler(event, context):
    message = event['input']
    edited_message = "「" + message + "」という文章をLambdaで受信しました。"
    
    return {
        'statusCode': 200,
        'body': json.dumps(edited_message)
    }
```

- 引数`event`のキーに`input`を取っていますが、これはAPI Gatewayの所で設定するキーです。

``` Python
 message = event['input']
```

- 返り値は以下の辞書型。
- 具体的な値はキー`body`の値に、json形式で返します。

``` Python
return {
        'statusCode': 200,
        'body': json.dumps(edited_message)
    }
```

- プログラムの作成が終わったら保存を行います。

![-w1079](https://i.imgur.com/NdHBoxE.jpg)

- 右上のテストから、Lambda単体で関数の動作をテストすることができます。

![-w1096](https://i.imgur.com/u2z2sH0.jpg)

- 右上を選択するとテストパターンを作成画面となります。
- 先程のコードの`event`の値を設定しています。
- キー`input`を設定します。

![-w862](https://i.imgur.com/hos9G5J.jpg)

- 作成したテストパターンでテストします。
- ログが表示され、Lambda関数の返り値を確認することができます。
- 漢字が符号位置になっていますが、恐らく所望の返り値となっていることが見てとれます。

![-w1051](https://i.imgur.com/B5uYT4R.jpg)

- Lambda側の作業は以上です。

## API Gatewayの作成
### 参考
- [Amazon API Gateway で Lambda 統合を使用して、REST API を作成します。](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-getting-started-with-rest-apis.html)
    - これに関しては公式ドキュメントが一番充実していると感じました。
- [ゼロから作りながら覚えるAPI Gateway環境構築](https://dev.classmethod.jp/cloud/aws/getting-start-api-gateway/)
- [curl コマンド 使い方メモ](https://qiita.com/yasuhiroki/items/a569d3371a66e365316f)
    - テストにてターミナルからAPIを叩くときの参考にしました。
    
### 作業例
- 入力パラメータを1つ受け取る、GETリクエストを作成していきます。
- 新規APIの作成を行います。
- 新しいAPIを選択して、APIを作成をクリックします。

![-w1193](https://i.imgur.com/6QWyhDA.jpg)

- リソースの作成を選択。

![-w602](https://i.imgur.com/gPHmVhR.jpg)

- リソース名を入力。
- またこのとき「API Gateway CORS を有効にする」にチェックを入れておきます。
    - TODO:簡単な説明
- リソースの作成をクリック。
- この時点でHTTPリクエストを送るURLの準備をしたことになります。
- 続いてGETリクエストを受け取る設定を行います。

![-w1183](https://i.imgur.com/Q5iDyjj.jpg)

- メソッドの作成、を選択

![-w623](https://i.imgur.com/8TYagDT.jpg)

- GET、を選択

![-w266](https://i.imgur.com/Ht1VRcS.jpg)

- GETリクエストで呼び出すLambda関数、先程作成した関数を指定します。
- 保存をクリックします。

![-w991](https://i.imgur.com/LKRrYL8.jpg)

- OKを選択。

![-w885](https://i.imgur.com/U6ud5aB.jpg)

- なんかややこしいですが、ざっくり以下の通り。正しいところは公式ドキュメントを参照ください。
    - （画像の細かい見た目が違いますが、別の作業時に取ったスクリーンショットなので無視してください）
    - [Amazon API Gateway で Lambda 統合を使用して、REST API を作成します。](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-getting-started-with-rest-apis.html)

![-w1253](https://i.imgur.com/3efg1Ln.jpg)

- 統合リクエストを選択します。

![-w963](https://i.imgur.com/6xOv9DX.jpg)

- マッピングテンプレートの追加を行います。
    - ここではLambdaにわたすjsonパラメータの設定を行います。
- 「 テンプレートが定義されていない場合 (推奨) 」を選択
- Content-Typeに`application/json`を追加
- 下記の通り入力します。
    - Lambda関数の引数で使用していた`event['input']`は、このときのキーとなっています。
- 入力が終わったら保存を選択します。

``` Python
{
    "input": "$input.params('param1')"
}
```

![-w765](https://i.imgur.com/jkDSe4T.jpg)

- Lambda同様に、API Gateway単体でテストを行えます。
- テストを選択します。

![-w1072](https://i.imgur.com/VoNdIYQ.jpg)

- 先程マッピングテンプレートで設定した通り、`param1=hello`と渡してみると、うまくLambda関数から返り値を受け取れていることがわかります。

![-w846](https://i.imgur.com/oi7T0un.jpg)

- APIのデプロイを行い、実際にHTTPリクエストをネットで行えるようにします。

![-w344](https://i.imgur.com/dwXlpMA.jpg)

- ステージを作成します。
- デプロイを選択します。
    - ちなみに、API Gatewayの設定を変更した場合、都度デプロイし直さないと反映されないようです。変に嵌らないように注意しましょう。

![-w607](https://i.imgur.com/ecujUQY.jpg)

- GETを選択すると呼び出しのURLが作成されています。

![-w1337](https://i.imgur.com/3C2Jmbs.jpg)

- 上記のURLのあとにパラメータを付与することで、HTTPリクエストを行うことができます。
- `URL` + `?` + `キー（URLエンコーディングした値）`
- ターミナルで確認してみると、Lambdaで処理した値が帰ってきていることが分かります。

``` Bash
curl -X GET 'https://<ユーザ固有値>.execute-api.us-east-1.amazonaws.com/prod/mywebapplicationtestapi?param1=Hello'
{"statusCode": 200, "body": "\"\\u300cHello\\u300d\\u3068\\u3044\\u3046\\u6587\\u7ae0\\u3092Lambda\\u3067\\u53d7\\u4fe1\\u3057\\u307e\\u3057\\u305f\\u3002\""}
```

- もちろんブラウザでもURLを入力することでも確認できます。

```
https://<ユーザ固有値>.execute-api.us-east-1.amazonaws.com/prod/mywebapplicationtestapi?param1=Hello
```

![-w1173](https://i.imgur.com/Gn709J1.jpg)

### 作業例＿APIを呼び出すための認証キーを作成
- 今の状態だとURLが分かる人全てが利用できてしまいます。
- そのため、HTTPリクエストを行う際の認証用のAPIキーを設定します。
- サイドバーの「APIキー」を選択し、「アクション>APIキーの作成」を選択します。

![-w524](https://i.imgur.com/KZueUMd.jpg)

- 名前を入力し保存します。

![-w904](https://i.imgur.com/G50B2fM.jpg)

- 作成したAPIキーを控えておきます。後述のHTTPリクエストを行う際に使用します。

![-w899](https://i.imgur.com/kA0rxJV.jpg)

- 先程作成したGETメソッドを選択し、「メソッドリクエスト」を開きます。

![-w1330](https://i.imgur.com/1GqkU1y.jpg)

- 「APIキーの必要性」を`true`にします。

![-w902](https://i.imgur.com/NQIhDHS.jpg)

- 再度「APIのデプロイ」を行います。
- 設定を変更した際には、都度「APIのデプロイ」を行わないと反映されないようなので、注意ポイントです。

![-w345](https://i.imgur.com/rgAUUAm.jpg)

- 最後の仕上げとして、APIキーの設定する場合、「使用量プラン」を設定する必要があります。
- 今回は簡便化のため、スロットリング・クォータは設定しないこととします。

![-w1429](https://i.imgur.com/Zq2N2Dk.jpg)

![-w1191](https://i.imgur.com/3sO92PP.jpg)

![-w998](https://i.imgur.com/cU0YPXO.jpg)

- ターミナルからHTTPリクエストを行いAPIキーが設定されているかどうかを確認します。
- まず以前行ったHTTPリクエストをそのまま投げてみます。
- すると`{"message":"Forbidden"}`と返され、APIキーがないためにHTTPリクエストに失敗していることが分かります。

``` Bash
curl -X GET 'https://<ユーザ固有値>.execute-api.us-east-1.amazonaws.com/prod/mywebapplicationtestapi?param1=Hello'
{"message":"Forbidden"}
```

- 次に下記の通りヘッダにAPIキーを設定したHTTPリクエストを行います。
- 希望する値が返ってきているならば、APIキーの設定がうまく行っていることが確認できます。

``` Bash
curl -X GET 'https://<ユーザ固有値>.execute-api.us-east-1.amazonaws.com/prod/mywebapplicationtestapi?param1=Hello' -H 'x-api-key:<APIキー>'
{"statusCode": 200, "body": "\"\\u300cHello\\u300d\\u3068\\u3044\\u3046\\u6587\\u7ae0\\u3092Lambda\\u3067\\u53d7\\u4fe1\\u3057\\u307e\\u3057\\u305f\\u3002\""}
```

- 以上でAPI Gatewayの設定は完了です。
- あとはHTMLでAPI Gatewayを呼び出すためのUIを作成していきます。
    - （ここまでが本編で、後はおまけに近いですね…）

## WEBページの作成（HTML）
### 参考
- [HTMLのフォーム](https://www.kanzaki.com/docs/html/htminfo31.html#S5)
- [HTMLElement.value の代わりに、innerHTML または textContent を使う](http://outcesticide.hatenablog.com/entry/2015/12/08/221553)

### 実装
- HTMLは下記のような超簡単なページを作成します。

![-w638](https://i.imgur.com/1o1sArc.jpg)



``` HTML
<!DOCTYPE html>
<heml lang="ja">
<head>
    <meta charset="utf-8">
    <title>私のWebアプリケーション</title>
</head>
<body>

<article>
    <h1>API Gatewayを呼び出すツール</h1>
    <p>入力した文章をAWS Lambdaで処理します。Outputに処理した結果が表示されます。</p>
    <section>
        <h2>Input</h2>
        <textarea id="input_textfield" name="input" rows="4" cols="60"></textarea>
        <br>
        <input type="submit" value="変換" onclick="send_message();">
        <script src="my_util.js"></script>
        <h2>Output</h2>
        <p id="output_label">（ここに結果が表示されます）</p>
    </section>
</article>
</body>
</heml>
```

- 入力のテキストボックスと出力のテキストに、id要素（`input_textfield`と`output_label`）を設定しています。
    - Javascript内ではこれを使用してオブジェクトを取得することで、入力を受け取ったり出力の表示をしています。

``` HTML
<textarea id="input_textfield" name="input" rows="4" cols="60"></textarea>
```

``` HTML
<p id="output_label">（ここに結果が表示されます）</p>
```

- ボタン押下時の処理は下記の通りです。
- 具体的な処理は同一フォルダ内にあるJavascriptファイル（`my_util.js`の`send_message();`)に記載しています。
    - HTMLに結果を動的に表示したいので、ボタンを押下したときの処理をJavascriptファイル書くようにしています。

``` HTML
<input type="submit" value="変換" onclick="send_message();">
<script src="my_util.js"></script>
```

## WEBページの作成（Javascript）

### 参考
- [JavaScriptで動的にWebページを書き換える【初心者向け】](https://qiita.com/sho_/items/a668088cd6c398096666)
    - javascriptで指定したidのテキストを書き換える方法。
- [JavaScriptでAPIを呼び出す方法を現役エンジニアが解説【初心者向け】](https://techacademy.jp/magazine/19615)
    - HTTPリクエストの投げ方はここを参照しました。。
- [【JavaScript入門】文字列を改行する方法(改行コード/「\n」/置換)](https://ikeh1024.hatenablog.com/entry/2019/07/09/204520)
- [【メモ】XMLHttpRequestのイベントについて](https://qiita.com/ShinyaKato/items/64b6726c361f5377b0f3)

### 実装

- コード全体は下記の通りです。
- （TODO：javascriptファイル内にAPIキーが直打ちになってしまっていますが、社内ツールということでご容赦を…）
    - （ログインで認証させて…の実装が必要になるのでしょうか？）

``` JavaScript
const send_message = () => {
    // URLを作成
    let input_label = document.getElementById("input_textfield");
    var parameter = input_label.value;
    parameter = parameter.replace(/\r?\n/g, '\\r\\n'); // 改行コードを入れるとAWSでの処理が怪しかったので、文字列に置換している（TODO:改善）
    parameter = encodeURI(parameter);
    console.log(parameter);
    
    request_url = "https://<ユーザ固有値>.execute-api.us-east-1.amazonaws.com/prod/mywebapplicationtestapi?param1=";   
    request_url = request_url + parameter;
    console.log(request_url);
    
    // リクエストオブジェクトの作成
    var request = new XMLHttpRequest();
    request.open('GET', request_url, true);
    request.setRequestHeader("x-api-key", "<APIキー>");
    request.responseType = 'json';

    // リクエストが成功したときに呼ばれる関数
    request.onload = function () {
      var json_data = this.response;
      var return_message = JSON.parse(json_data["body"]);
      // 結果をhtmlに表示する
      let output_label = document.getElementById("output_label");
	  output_label.innerText = return_message
    };
    
    request.send(); // URLリクエストを送信する
}
```

- 前にターミナルで行ったのと同様に、API Gatewayで作成したURLにパラメータを付与した文字列を作成します。
    - `URL` + `?<キー名>=<URLエンコーディングした値>`
- スペース等を考慮して、パラメータ文字列にはURLエンコーディングを行っておきます。
    - （TODO：改行コードをURLエンコーディングして渡すと、JSONデータへの変換に失敗しているのかLambdaからの返り値がnilになってしまいました。今回は置換でお茶を濁していますが、AWS側をどう変えれば良いのでしょうか…？）

``` JavaScript
let input_label = document.getElementById("input_textfield");
var parameter = input_label.value;
parameter = parameter.replace(/\r?\n/g, '\\r\\n'); // 改行コードを入れるとAWSでの処理が怪しかったので、文字列に置換している（TODO:改善）
parameter = encodeURI(parameter);
```

``` JavaScript
request_url = "https://<ユーザ固有値>.execute-api.us-east-1.amazonaws.com/prod/mywebapplicationtestapi?param1=";   
request_url = request_url + parameter;
```

- HTTPリクエストのオブジェクトを作成します。
- オブジェクトには、HTTPリクエストの種類（今回は`GET`）・リクエストURL、またヘッダにAPIキーの情報を与えています。

``` JavaScript
// リクエストオブジェクトの作成
var request = new XMLHttpRequest();
request.open('GET', request_url, true);
request.setRequestHeader("x-api-key", "<APIキー>");
request.responseType = 'json';
```

- リクエストオブジェクトの成功したときの処理を実装します。
- API Gatawayからの返り値は以下のようにJSONデータとして受け取ります。
- 今回必要なのはキー`body`の値です。これをHTMLのテキストへ表示してやります。
- また今回は実装していませんが、例えば`request.onerror`でエラーハンドリングをしておくとデバッグ時に便利でした。個人で使うものなので自分が便利なように実装したら良いと主ます。

``` JavaScript
// リクエストが成功したときに呼ばれる関数
request.onload = function () {
  var json_data = this.response;
  var return_message = JSON.parse(json_data["body"]);
  // 結果をhtmlに表示する
  let output_label = document.getElementById("output_label");
  output_label.innerText = return_message
};
```

## WEBページのHTTPリクエストのテストを行う際の注意点（Chromeの場合）
- ログは検証のConsoleを参照します。
    - Javascriptの`console.log()`はここに表示されます。

![-w1115](https://i.imgur.com/E7HFUgw.jpg)

- ChromeローカルファイルのWEBページからHTTPリクエストを行うと、APIリクエストでエラーが発生します。

![-w1260](https://i.imgur.com/DFuoGRD.jpg)


- CORS(クロスドメイン)が原因らしいです。
- ローカルファイルからAPIリクエストを送信するにはセキュリティを外さないといけなく、下記のコマンドでChromeを立ち上げる必要があります。
    - [【解説付き】chromeでXMLHttpRequestをローカルのファイルで行う方法](https://qiita.com/growsic/items/a919a7e2a665557d9cf4)

``` :Macの場合
open /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir
```

# まとめ
- WEBページでは単純な入力だけをさせ、具体的な処理をLambdaに任せることで、Pythonで具体的な処理を書けるようになりました。
- また残りの作業として、WEBページをS3へ公開し、社内のIPアドレスのみでアクセスするように設定することで、社内で使えるWEBツールの完成となります。
## S3の参考になりそうなページ

- [S3の静的ウェブサイトホスティング（2019年03月31日現在）](https://www.tech-note.info/entry/aws-s3-static-website-hosting-201903)
    - バケットポリシーの変更で参考に。
- [例: 静的ウェブサイトをセットアップする](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/HostingWebsiteOnS3Setup.html)
    -  公式のリファレンス。情報が古いのかバケットポリシー設定で躓きます。