阿部寛のサイトを高速化する

ちまたで阿部寛のサイトが早いと話題になってます。

dev.toと阿部寛のホームページどっちが速いですか?
dev.toと阿部寛のホームページについてちゃんと計測させてくれ

阿部寛のサイトはベストを尽くしてるのか?
それを調べるために、阿部寛のサイトを高速化させてみたいと思います。

目指すべきスピード

最速はローカルのファイルへのアクセスだと思うのでこれを目指したいと思います。
file:///C:/abe_hiroshi/index.html

9baae75ad17a694cadc9a83081cd4df4.gif

ChromeのDeveloper Toolでレンダリング完了が「173ms」でした。
まぁここまでは無理だな…

阿部寛のサイトはどんなもん?

速度はwebpagetest.orgで測ってみます。

nama.png
レンダリング完了時間は「359ms」です。はえーな

S3でホスティングしてみる

サーバーを立てるほどでもないので、S3でWebホスティングしてそこにhtmlと画像を置いてみます。

s3_1.png
s3_2.png

レンダリング完了時間が「698ms」なんで、まだまだ遅いです。

CDN(CloudFront)でキャッシュする

続いてS3のオブジェクトをCloudFrontでキャッシュしてみる。

cf_1.png
cf_2.png

レンダリング完了時間が「379ms」になりました。
やはりCDNは効果絶大です。

Webpを使ってみる

dev.toではCloudinaryという画像ホスティングサイトを使用しており
画像をアップロードすると様々なフォーマットで利用できるようになります。

dev.toでも使用している「Webp」というJPG/PNGよりも圧縮に優れたフォーマットで
大幅なサイズ削減を実現してくれます。
サイズ削減できれば高速化できるはず。早速試してみます。

画像の返還作業は、Cloudinaryでこんな感じの画面で行えます。
webp.png

普通の画像サイズ
http://abehiroshi.s3-website-ap-northeast-1.amazonaws.com/image/abe-top2-4.jpg
64KB

cloudinaryでwebpに変更してみると
https://res.cloudinary.com/morix1500/image/upload/v1510926252/abe-top2-4_n99mu2.webp
18.9KB

cloudinaryのURLをhtmlに埋め込んで計測してみます。
webp_1.png
webp_2.png

はい、全部Aになりました。
しかし、レンダリング完了時間が「1.337s」と遅くなった…なぜでしょう?

このページではHTTP2が有効になってます。
同一ドメインでないと余計なコネクションが発生してしまうのではないかと推測し、
同一ドメインに画像を置いてみることにします。(間違えてたらご指摘おねがいします)

ということでCloudinaryから直接画像を読み込むのではなく、WebpをダウンロードしてS3に配置してみます。
webp_3.png

お、「328ms」になりました。
また読み込みバイト数も「70KB」から「18KB」になりました。
これはいい感じですね。

これで阿部寛サイトを超えることができました!

結論

以下の技術を使用すれば既存の阿部寛サイトを凌駕することが出来ました。
* S3でホスティング
* CDN(CloudFront)でキャッシュ
* HTTP2
* Webp

ここまでしないと超えられない既存サイト…恐ろしい。
Service Workerも使ってみたかったですが、それは今度の機会に。

では!

続きを読む

CloudFrontの署名付きURLをRubyで発行する

概要

画像コンテンツを扱うアプリのバックエンドをRails/EC2/S3/CloudFrontで作った。
コンテンツ保護のためCloudFrontの署名付きURLを使ったが、何やらAWSのSDKとやらが必要だったのでそのメモ。

前提

S3/CloudFrontの設定と署名用のキーペアの作成が終わっていること。
下記を参考にさせてもらったらバッチリできた。

CloudFront+S3で署名付きURLでプライベートコンテンツを配信する
https://dev.classmethod.jp/cloud/aws/cf-s3-deliveries-use-signurl/

しかし、肝心の署名付きURLの発行しようとしたらRubyのサンプルがなくて詰んだ。
泣きながら初心者なりに試行錯誤して見ると一応動いた。

やったこと

AWS SDK をGemで追加して

Gemfile
gem 'aws-sdk-cloudfront'

キーペアのIDとダウンロードした秘密鍵を指定し、署名付きURLを発行する。

url_signer.rb
signer = Aws::CloudFront::UrlSigner.new
  key_pair_id:      "APKA9ONS7QCOWEXAMPLE", # 鍵ID
  private_key_path: "tmp/pk-cloudfront.pem" # 鍵ファイル
)
# 5分間有効な署名付きURLを発行
signed_url = signer.signed_url(
  "https://d111111abcdef8.cloudfront.net/images/image.jpg",
  expires: Time.now.getutc + 5.minute
)

# 発行された署名付きURL
# signed_url = https://d111111abcdef8.cloudfront.net/images/image.jpg?Expires=123456789&Signature=nitfHRCrtziwO2HwPfWw~yYDhUF5EwRunQA-j19DzZrvDh6hQ73lDx~-ar3UocvvRQVw6EkC~GdpGQyyOSKQim-TxAnW7d8F5Kkai9HVx0FIu-5jcQb0UEmatEXAMPLE3ReXySpLSMj0yCd3ZAB4UcBCAqEijkytL6f3fVYNGQI6&Key-Pair-Id=APKA9ONS7QCOWEXAMPLE

これだけだ。

発行したURLでアクセスするとちゃんとコンテンツにアクセスできたし、有効期間(5分)が過ぎた後はちゃんとアクセスが拒否された。
ちなみにexpiresではなくpolicyも指定できるので、もっと複雑な条件も指定できるらしい。

参考

続きを読む

AWSのECSでGPUを使う方法

はじめに

AWSのECSでGPUを使う場合、ECS側ではGPU搭載のインスタンスはデフォルトでは選べません。今のところ自分自身でどうにかするしかありません。その方法を記載します。

1. ECSでクラスターを作成

ECSで新規にクラスターを作成します。ここで設定するクラスター名は後ほど使います。今回の説明ではecs-clusterという名前で説明します。

2. ECSで動かせるGPUのAMIを作成する

AWSの公式ページにあるので、こちらの手順に沿ってAMIの作成まで行います。
GPUワークロードのAMIの作成
こちらの手順の最後にec2-userでsudoをつけずにDockerを叩けるように設定しておくと構築時に便利になります。

$ sudo usermod -g docker ec2-user
$ sudo /etc/init.d/docker restart

上記のコマンド入力後は一度ターミナルから抜けて再接続する必要があります。

3. 作成したGPUのAMIを使ってインスタンス作成

インスタンスの設定

ここではp2.xlargeを使って、インスタンスを立ち上げます。

スクリーンショット 2017-11-17 17.04.37.png

以下の点に注意する必要があります

  • 作成するインスタンスのロール設定はECSインスタンスロールを与えてください。
    作成方法はこちらを参照ください:Amazon ECSインスタンスロール

  • インスタンスの設定タブの下部の高度な詳細でユーザデータを設定する(下記)

#!/bin/bash
echo ECS_CLUSTER=ecs-cluster >> /etc/ecs/ecs.config

4. Dockerfileの構築

ここではGPUを使うためのDockerfileを作り、ECRへプッシュします。
今回はtensorflow-gpu 1.4.0を使います。TensorFlowのバージョンに合わせて、nvidiaのイメージを使いますので、下記のURLからご確認ください。
https://www.tensorflow.org/install/install_sources#common_installation_problems

上記のURLで確認するとcuDNN:6 CUDA:8なのでnvidiaのimageはこのバージョンに沿ったものを使います。

下記のDockerfileは一例なので、各自行いたい構築を設定してください。

Dockerfile
FROM nvidia/cuda:8.0-cudnn6-devel
RUN apt-get update
RUN apt-get install -y  git 
                        vim 
                        build-essential 
                        libncursesw5-dev 
                        libgdbm-dev 
                        libc6-dev 
                        zlib1g-dev 
                        libsqlite3-dev 
                        tk-dev 
                        libssl-dev 
                        openssl 
                        libbz2-dev 
                        libreadline-dev 
                        checkinstall 
                        wget 
                        jq

RUN git clone https://github.com/yyuu/pyenv.git /root/.pyenv

ENV PYENV_ROOT "/root/.pyenv"
ENV PATH "$PYENV_ROOT/bin:$PATH"
ENV PATH "$PYENV_ROOT/shims:$PATH"
RUN pyenv install 3.6.2
RUN pyenv global 3.6.2
RUN pip install tensorflow-gpu==1.4.0

DockerFileを作成したら、ECRへプッシュします。
ECSのリポジトリで新規リポジトリを作成するとプッシュまでの流れが記載されているのでその通りに行います。

個人的にハマった点
tensorflowとtensorflow-gpuは一緒に入れないでください、tensorflowを入れるとCPUで動きます。必ずtensorflow-gpuだけにしてください。

5. タスク定義

ここで重要な部分は下記です。

  • 特権付与をつける
  • ボリュームマウントでホスト側のnvidia関連をマウントする
    ホスト側の「/var/lib/nvidia-docker/volumes/nvidia_driver/latest」とコンテナ側の「/usr/local/nvidia」

以上のことを行えば、ECSでGPUインスタンスを使えて、かつTensorFlowが回せると思います。

続きを読む

CloudWatchで定期的にLambdaを実行して、Slackにメッセージを送信する

今回のお題

Lambdaのトリガーはいくつか設定できますが、今回は毎週月曜日のAM9時にSLackに通知するのが目的なんで、CloudWatchでLambdaに設定した関数を実行する方法を試して見ます。

LambdaにSlack送信するプログラムを作成する

LambdaからWebHookを使用してSlackにメッセージを送信してみる

別記事で書いたこちらを元に作成。
ちょっとだけ手を加えてあります。

slack.py
# coding: UTF-8
import requests
import json
import os
from datetime import datetime

def lambda_handler(event, context):
    now = datetime.now()
    msg = now.strftime("%Y/%m/%d %H:%M:%S")
    msg += u'\r\nCloudWatch実行\r\nlambdaからのPython使ってのSlack送信・・・何たる僥倖・・・!悪魔的発想・・・!'
    # 実行
    send_slack(u'カイジ', u':kaiji:', 'チャンネル名', msg)

# slack送信、仮メソッド
def send_slack(user_name, icon, channel, msg):
    requests.post('WebHookのURL', data = json.dumps({
        'text': msg, # 投稿するテキスト
        'username': user_name, # 投稿のユーザー名
        'link_names': 1, # メンションを有効にする
        'channel': channel, # チャンネル
        'icon_emoji': icon, # アイコン
    }))

分かりやすくするために、Postするメッセージに日付を入れて見ました。
あとはまぁ、よくある悪ふざけですw
テスト実行で、Slackにメッセージが送信されるのは確認済みです。

Lambdaにトリガーを設定する

トリガーを選択します、+ トリガーを追加を押下します。
スクリーンショット 2017-11-16 23.00.07.png

図の空白になっている、左側をクリックします。
スクリーンショット 2017-11-16 23.02.23.png

すると設定できるリストが表示されるので、今回使用する「CloudWatch Events」を選択します。
スクリーンショット 2017-11-16 23.03.07.png

ルールで「新規ルールの作成」を選択し、必要な項目を埋めていきます。
スケジュール式は、今回はテスト的な内容なんで5分ごとに実行する式を記載しました。
スクリーンショット 2017-11-16 23.05.37.png

トリガーの有効化のチェックがON担っているのを確認し、送信を押下する。
スクリーンショット 2017-11-16 23.07.07.png

トリガーの作成完了

スクリーンショット 2017-11-16 23.07.46.png

Slackを確認する

とりあえず問題なく定期的にSlackの送信ができました!!
スクリーンショット 2017-11-16 23.19.25.png

別枠で検証する事柄

時間がおかしいですね・・・?どこの時間ですかね?
とりあえず、日本時間での実行ができるように調べて見ます。

AWSのLambdaのタイムゾーンをUTCから東京に変更
こちらで、Lambdaでの時間についてはできました!

CloudWatchについては別途調査中です。

続きを読む

AWS linux PHP apache mysql wordpress 環境構築

Step1
EC2インスタンス作成

1.png

Step2
TaraTerm使って、linuxサーバ接続する

2.jpg

Step3
yum 更新
$ sudo yum -y update

4.jpg
5.jpg

Step4
Apache2.4インストール

//バージョン確認
$ sudo yum list available | grep httpd

8.jpg

//httpd24インストールする
$ sudo yum -y install httpd24

6.jpg
7.jpg

//結果確認
$ sudo yum list installed | grep httpd24

9.jpg

Step5
PHP7.0インストールする

//バージョン確認
$ sudo yum list available | grep php70
//インストールする
$ sudo yum -y install php70 php70-mbstring php70-pdo
//結果確認
$ sudo yum list installed | grep php70

10.jpg
11.jpg
12.jpg

Step6
mysqlインストールする

//mysql バージョン確認
//mysql インストールする
//結果確認
$ sudo yum list available | grep mysql57
$ sudo yum install mysql
$ sudo yum list installed | grep mysql

20.jpg21.jpg
22.jpg

Step7
apache配置

etc/httpd/con/httpd.confを編集し、以下の2行を追加
(編集する前はhttpd.confバックアップする)
AddType application/x-httpd-php .html .htm .php .phtml
AddType application/x-httpd-php-source .html .htm .phps

・httpd.confバックアップする
$ sudo cp httpd.conf httpd.conf.bak

23.jpg

・httpd.conf編集する
$ vim httpd.conf

24.jpg

・AddTypeのところで下記二行追加
AddType application/x-httpd-php .html .htm .php .phtml
AddType application/x-httpd-php-source .html .htm .phps

25.jpg

・httpd restart
$ sudo service httpd restart

26.jpg

・確認する
var/www/htmlの下でindex.php を作成、下記のコードを追加
<?php
phpinfo();
?>
ブローザにIP入力、下記の画面出ます

28.jpg

Step8
mysql配置

・mysql起動する時、エラー発生しました。
改正:$ sudo yum install mysql57-server

29.jpg

・mysql 起動する
$ service mysqld start

30.jpg

・mysql 登録する

31.jpg

・ユーザ追加
ユーザ名:mysql
パスワード:mysql
mysql> CREATE USER ‘mysql’@’localhost’ IDENTIFIED BY ‘mysql’;

32.jpg
33.jpg

Step9
wordpess配置

・下記のurlから日本語wordpessダウンロードする
https://ja.wordpress.org/install/

var/www/html下に
$ wget https://ja.wordpress.org/wordpress-4.8.3-ja.zip

36.jpg

・解凍する

37.jpg

38.jpg

・エラー発生した
39.jpg

・原因:PHPにmysqlサポート追加されない。

・改正:
$ sudo yum -y install php70-mysqlnd
/etc/php.ini に extension=msql.so 追加

40.jpg
41.jpg

・設定画面が出ます
43.jpg

Step10
wordpess設定

・rootユーザにdatabaseを追加
mysql> create database wordpress;

・rootユーザパスワード修正
mysql>update mysql.user set authentication_string=password(‘root’) where user=’root’;

45.jpg

・wordpessに情報記入
46.jpg

47.jpg

wp-config.php作成
$ cd var/www/html/wordpress

48.jpg

・情報記入
49.jpg
50.jpg
51.jpg

ここまで、以上になります。

追記:
FTP使いたくない場合下記参照

・wp-config.php に書きます
define(‘FS_METHOD’, ‘direct’);
・wordpressフォルダすべてのファイル書き可能に変更
sudo chmod 777 * -R

続きを読む

LambdaからWebHookを使用してSlackにメッセージを送信してみる

今回のお題

WebHookを使用して、Slackにメッセージを表示してみる。

SlackにPOSTしてみる

こちらを参考にさせていただきました。
Slackにincoming webhook経由でpythonからメッセージをPOSTする

lambda_function.py
import requests
import json

def lambda_handler(event, context):
    # slack送信Sample
    requests.post('webhookのURL', data = json.dumps({
        'text': 'Test\r\nHello Slack', # 投稿するテキスト
        'username': 'python_slack', # 投稿のユーザー名
        'link_names': 1, # メンションを有効にする
        'channel': 'test', # チャンネル
        'icon_emoji': 'icon', # アイコン
    }))

すると下記のようなエラーがログに出力されました

Syntax error in module 'lambda_function': Non-ASCII character '\xe9' in file /var/task/lambda_function.py on line 4, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details (lambda_function.py, line 4)

どうやら、UTF-8指定しないで日本語があるとエラーになるようです・・・
そこで、下記のようなソースに変えました

lambda_function.py
# coding: UTF-8
import requests
import json

def lambda_handler(event, context):
    # slack送信Sample
    requests.post('webhookのURL', data = json.dumps({
        'text': 'Test\r\nHello Slack', # 投稿するテキスト
        'username': 'python_slack', # 投稿のユーザー名
        'link_names': 1, # メンションを有効にする
        'channel': 'test', # チャンネル
        'icon_emoji': 'icon', # アイコン
    }))

頭にUTF-8を指定する一行を追加。保存して、再度テストを実行します。

Unable to import module 'lambda_function': No module named requests

違うエラーになりました・・・

【AWS】Lambdaでpipしたいと思ったときにすべきこと

こちらを参考に、requestsを上げるようにします。

VS Codeを使用して、ディレクトリ内にrequestsを入れる

  1. 上記に書いたソースでpyファイルを作成し、VS Codeで開きます。
  2. VS Codeのターミナルを開いて、

    pip install requests -t .

    を実行する。

作業フォルダを圧縮する

スクリーンショット 2017-11-14 23.49.04.png
この階層を丸ごと圧縮します。1つ上の階層でフォルダを圧縮して上げる方法もあるようですが、分かりやすいんでこちらを圧縮します。
名前はとりあえずslack.zipとかにしました。

zipをアップロード

AWSに戻り、コード エントリ タイプで、.ZIPファイルをアップロードを選択します。
スクリーンショット 2017-11-14 23.50.47.png

関数パッケージのアップロードを選択して、先ほどの圧縮ファイルを選択します。
スクリーンショット 2017-11-14 23.52.32.png

ファイルを選択したら、保存を押下します。
スクリーンショット 2017-11-14 23.53.35.png

またエラー・・・

スクリーンショット 2017-11-14 23.54.08.png

どうやら、ハンドラをちゃんと設定する必要があるようです・・・
設定するpyのファイル名(今回はslack.pyなので、slack)と実行するファンクション名を入力し、
再度圧縮ファイルをアップロードします。
スクリーンショット 2017-11-14 23.54.55.png

アップロード完了したので、テスト実行

内容をきちんとした設定内容に変更し、実行します。

lambda_function.py
# coding: UTF-8
import requests
import json

def lambda_handler(event, context):
    # slack送信Sample
    requests.post('webhookのURL', data = json.dumps({
        'text': 'Test\r\nHello Slack', # 投稿するテキスト
        'username': 'python_slack', # 投稿のユーザー名
        'link_names': 1, # メンションを有効にする
        'channel': 'test', # チャンネル
        'icon_emoji': 'icon', # アイコン
    }))

Slackにでました!!
スクリーンショット 2017-11-15 0.02.30.png

続きを読む

サーバーレス(HTML on S3)にGoogle認証付きS3アップローダーを作る

やりたいこと

  • S3上にHTMLとJSファイルをおいて静的webページとして公開
  • webページに入力した内容をcsvにまとめてS3に出力
  • 認証はgoogleのOpenID Connectを使用(roleをつけておく)
  • バケットポリシーでIP制限をかけておく

S3でwebページ公開に関して

以下のサイトを参考にして公開
S3で静的ウェブサイトをホスティングしてみる
バケットポリシーも作っておく

googleのOpen Connect周り

以下のサイトを参考にクライアントID+認証周り作成
Googleの「OpenID Connect」を利用する為の「クライアントID」の取得方法

スクリーンショット 2017-11-13 18.23.27.png
*承認済みのJavaScript生成元を設定せずにしばらくはまっていました…

Googleと連携するroleの作成

AWSのコンソール画面で
IAM > role > roleの作成
と進み、ウェブIDのタブを選択してプロバイダーにGoogleを選択
AudienceにはクライアントIDを入力してpolicyのattachなどを行う
スクリーンショット 2017-11-15 16.49.52.png

例で用いたpolicyはS3の特定のフォルダにのみアクセス可能なものにしました

policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME/output/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME"
            ],
            "Effect": "Allow",
            "Condition": {
                "StringEquals": {
                    "s3:prefix": "output"
                }
            }
        }
    ]
}

いよいよJavascript作成

フォルダ構成

  • BUCKET

    • output
    • HTML
      • index.html
      • favicon.ico
      • logic.js

ログイン周り

以下のURLを参考にJavascriptを作成(ログイン箇所に関しては例をそのまま使用)
ブラウザの JavaScript
Web Federated Identity Examples

index.html
<!DOCTYPE html>
<html>
<head>
    <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
    <title>AWS SDK for JavaScript - Sample Application</title>
    <meta charset="utf-8"/>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</head>

<body>
<div id="results" role="alert"></div>
<nav class="navbar navbar-default">
    <div class="container">
        <!-- 2.ヘッダ情報 -->
        <div class="navbar-header">
            <a class="navbar-brand">S3アップローダー</a>
        </div>
    </div>
</nav>
<div class="container">
    <form>
        <div class="form-group" id="name">
            <label>名前</label>
            <input type="text" name="name" class="form-control" placeholder="申請 太郎" required>
        </div>
        <div class="form-group" id="account_flg">
            <label>アカウント</label>
            <select name="account_flg" class="form-control">
                <option value="不要">不要</option>
                <option value="必要">必要</option>
            </select>
        </div>
        ...
        <button type="submit" onclick="upload_csv();" class="btn btn-primary">申請登録</button>
    </form>
</div>
<span
        id="login"
        class="g-signin"
        data-height="short"
        data-callback="loginToGoogle"
        data-cookiepolicy="single_host_origin"
        data-requestvisibleactions="http://schemas.google.com/AddActivity"
        data-scope="https://www.googleapis.com/auth/plus.login">
</span>
<script>
    //以下ほぼサンプルのコピペ
    //s3 = null;  s3をアップローダーでも使いたいので後々グローバル変数で指定しています
    var clientID = '************.apps.googleusercontent.com'; // Google client ID
    var roleArn = 'arn:aws:iam::************:role/ROLE_NAME';
    document.getElementById('login').setAttribute('data-clientid', clientID);
    function loginToGoogle(response) {
        if (!response.error) {
            AWS.config.credentials = new AWS.WebIdentityCredentials({
                RoleArn: roleArn,
                WebIdentityToken: response.id_token
            });
            s3 = new AWS.S3();
            console.log('You are now logged in.');
        } else {
            console.log('There was a problem logging you in.');
        }
    }
    (function () {
        var po = document.createElement('script');
        po.type = 'text/javascript';
        po.async = true;
        po.src = 'https://apis.google.com/js/client:plusone.js';
        var s = document.getElementsByTagName('script')[0];
        s.parentNode.insertBefore(po, s);
    })();
</script>
<script src="logic.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>

アップローダー周り

logic.js
//ここに必要な項目のIDを書いておく
csv_array = [
    'name',
    'account_flg'
]

//メインのところ(どのcsvを作成するか)
function upload_csv() {
    upload('account', make_csv(csv_array));
}

//配列情報から変数(csv)にヘッダー情報と内容を入れる関数
function make_csv(array) {
    var csv = '';
    for (i in array) {
        var block = document.getElementById(array[i]);
        csv += block.getElementsByTagName("label")[0].innerHTML + ',';
    }
    csv = csv.slice(0,-1);
    csv += '\n';
    for (i in array) {
        var block = document.getElementById(array[i]);
        if (block.getElementsByTagName("select")[0]){
            var num = block.getElementsByTagName("select")[0].selectedIndex;
            csv += block.getElementsByTagName("select")[0].options[num].value + ',';
        } else {
            csv += block.getElementsByTagName("input")[0].value + ',';
        }
    }
    csv = csv.slice(0,-1);
    return csv;
}

//出来上がったcsvファイルをアップロードする関数
function upload(service_name, body) {
    var results = document.getElementById('results');
    var params = {
        Bucket: 'BUCKET_NAME',
        Key: 'output/' + service_name + '_' + timestamp() + '.csv', //名前が被らないようにtimestam付
        ContentType: 'csv',
        Body: body
    };
    s3.putObject(params, function (err, data) {
        results.setAttribute("class", err ? 'alert alert-danger' : 'alert alert-success');
        results.innerHTML = err ? "<strong>ERROR!</strong>" : "<strong>UPLOADED</strong>";
    });
}

//タイムスタンプ作成用関数
function timestamp() {
    var d = new Date();
    var year = d.getFullYear();
    var month = d.getMonth() + 1;
    var day = d.getDate();
    var hour = ( d.getHours() < 10 ) ? '0' + d.getHours() : d.getHours();
    var min = ( d.getMinutes() < 10 ) ? '0' + d.getMinutes() : d.getMinutes();
    return '' + year + month + day + hour + min;
}

終わりに

IP制限をかけておけば簡単に社内サービスを作成できます
単にローカルファイルを上げるだけであれば、htnl上でファイル取得をしてuploadの関数を使えばできます
サーバーレス素晴らしい

続きを読む

Amazon Connect で非通知電話ではなく、番号通知で電話をかける方法

Amazon Connect 本当に日本語のドキュメントが少なくて大変ですね。
参考記事
https://qiita.com/miTsuKow/items/2ec4feec6dc6bc7d6c8b

別サイトに電話をかけるボタンを埋め込んで、そこから電話をかけるためには、
CCPのURLを jsファイルにに書く必要があるのですが、”CCP”ってなに? となってしまう人もいるでしょう。

CCP とは、
Screenshot from Gyazo
この電話マークを押すと出て来るポップアップのことです。

それさえわかれば、割りとすんなり実装できるのではないでしょうか?

僕が引っかかった点としては、
IVRを構築して、番号ごとに問い合わせを振り分け、違う AWS Lambda を起動する時に、
同一リージョンでなおかつ、応答タイムアウトを8秒に設定しないと、デフォルトだと全部失敗することです。
あと、Lambda の成功失敗で条件分岐させることもできるはずなんですが、
Lambda で何らかの成功レスポンスを返さないと、成功として判定されないようです。この部分はサポートに聞いても、何を返せばいいのかドキュメントにも載っていないとのことでした。

さて、一番困った点は、非通知でしか電話が掛けられないことです。
AWSのサポートから電話をかけてもらう時は、番号通知済みで電話がかかってくるのに、なんでConnect からかけると電話がかかってくるのかどうかわからなかったのです。

これをサポートに問い合わせると、オレゴンとアメリカ国内では、番号を通知して電話をかけられるそうです。
しかし、オレゴン 日本、 シドニー 日本、の間では出来ないそうです。

設定には、相手の電話に表示させる番号とOutbound caller ID nameを(これで本当に相手に
自分の名前が通知されるかわからないけど)が選べるようになっていますが、これが特定リージョンとキャリアによっても無効か有効かが変化するそうです。

そもそも、ナンバーディスプレイというのは、掛ける方の電話会社とかかってくる方の電話会社が相互に連携をしていないと、成立しないものです。したがって、Amazon のサポートが使っている電話会社と、Conecct で使っている電話会社が違うので、番号通知がうまくいかないということだそうです。

非通知で電話をかけるコールセンターは怪しまれるでしょうから、問題だと思ってます。
誰かアマゾンの太い御客さんのひとは、東京リージョンで使えるようにしてほしいと要望していただきたいです。東京リージョンから日本にから発信するなら、番号を通知して発信できるでしょう。

続きを読む

AWS PrivateLinkが登場 EC2やELBでどのように使うか

AWS Private link について

先日 AWS Private linkという機能が発表されました。
https://aws.amazon.com/jp/blogs/aws/new-aws-privatelink-endpoints-kinesis-ec2-systems-manager-and-elb-apis-in-your-vpc/

これまでの方式(DynamoDB,S3)はエンドポイントのゲートウェイ経由で各サービスのAPIに接続する形でしたが
新しい方式ではエンドポイントがENIに紐付き、VPCのプライベートIPを持つかたちになります。
この方式のメリットとしては
・セキュリティグループでエンドポイントへのアクセスを管理できるようになる
・エンドポイントにDirect Connectを介してアクセスすることができる
というのが大きいかなと思います。

Private linkに対応するサービス

今回のタイミングで、 以下のサービスが新たにVPCの中から使えるようになりました。
その他のサービスもComing Soonでサポート予定とのこと。

  • Kinesis
  • Service Catalog
  • Amazon EC2
  • EC2 Systems Manager
  • Elastic Load Balancing

EC2やELBでの使いどころ

Kinesisの対応はわかりやすいです。
プライベートなEC2からKinesisにデータプッシュできるとか最高やないか。
でもEC2やELBのエンドポイントって?と私は思ってしまったのでした。
例えば、EC2なら別VPCのパブリックなEC2にプライベートに接続?とかよく分からないことを考え初めてしまった。

基本に立ち返ってドキュメントを確認しました。
新しい方式(インターフェースVPCエンドポイント)についてはまだ日本語のドキュメントはありませんが
原文のドキュメントは以下のURLから参照できます。
http://docs.aws.amazon.com/ja_jp/AmazonVPC/latest/UserGuide/vpce-interface.html

ページ下部のAccessing an AWS Service Through an Interface EndpointにELBの
エンドポイントにAWS CLIからアクセスする例がありました。

aws elbv2 describe-load-balancers --endpoint-url https://vpce-0f89a33420c193abc-bluzidnv.elasticloadbalancing.us-east-1.vpce.amazonaws.com/

単純にAWS CLIやSDKからAPI操作する際に使えば良いイメージでしょうか。
言われてみれば冒頭のBlogのタイトルも ~Amazon EC2 APIs, and ELB APIs in your VPC でした。

EC2でやってみる

まずは単純にプライベートなEC2でAWS CLIを操作してみます。
当然ですが、接続エラーになりました。

$aws ec2 describe-availability-zones

HTTPSConnectionPool(host='ec2.ap-northeast-1.amazonaws.com', port=443): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<botocore.awsrequest.AWSHTTPSConnection object at 0x7f002bf53e10>, 'Connection to ec2.ap-northeast-1.amazonaws.com timed out. (connect timeout=60)'))

PrivateLinkの作成

コンソールのVPCのメニューからエンドポイント → Create Endpoint と進みます。
S3とDynamodbのエンドポイントしか選択できない場合は、一度コンソールの言語設定を英語にすると
以下のような画面が表示されました。

image.png

Service Name: com.amazonaws.ap-northeast-1.ec2 を選択

VPC, subnet, SecurityGroup:それぞれ任意のものを指定します。
SecurityGroupでは443のインバウンドを許可する必要があります。

Enable for this endpoint:
PrivateLinkのエンドポイントはVPCのIPアドレスを使用するため、VPCのプライベートDNSを使用して
AWSサービスのDNS名を上書きすることができます。
チェックを入れると「ec2.ap-northeast-1.amazonaws.com」のVPC内のルックアップが、
作成するエンドポイントのIPアドレスに解決されます。

上記を指定したら、画面下部のCreate endpointを押下します。

動作確認

先ほどと同じく、プライベートなEC2からAWS CLIを実行します。

$ aws ec2 describe-availability-zones
{
    "AvailabilityZones": [
        {
            "State": "available",
            "ZoneName": "ap-northeast-1a",
            "Messages": []
            "RegionName": "ap-northeast-1"
        },
        {
            "State": "available",
            "ZoneName": "ap-northeast-1c",
            "Messages": [],
            "RegionName": "ap-northeast-1"
        }
    ]
}

Enable for this endpointにチェックを入れているので全く同じコマンドで実行することができました。
プライベートDNS名を有効にしない(チェックを入れない)場合は –endpoint-url で
エンドポイント固有に割り当てられているDNS名を指定すると接続することができます。

$ aws ec2 describe-availability-zones --endpoint-url https://vpce-xxxxxxxxxx-xxxxxx.ec2.ap-northeast-1.vpce.amazonaws.com
{
    "AvailabilityZones": [
        {
            "State": "available",
            "ZoneName": "ap-northeast-1a",
            "Messages": [],
            "RegionName": "ap-northeast-1"
        },
        {
            "State": "available",
            "ZoneName": "ap-northeast-1c",
            "Messages": [],
            "RegionName": "ap-northeast-1"
        }
    ]
}

基本的な内容かもしれませんが、参考になれば幸いです。
以上です。

続きを読む

AWS+Reactアプリ作成入門(ログイン後のAdmin編)

AWS+Reactアプリ作成入門(Cognito編)
AWS+Reactアプリ作成入門(S3編)
AWS+Reactアプリ作成入門(DynamoDB編)
AWS+Reactアプリ作成入門(IAM Role編)
AWS+Reactアプリ作成入門(ログイン後のAdmin編)

今回作成したアプリ ==>久喜SNS

 AWS+Reactアプリ作成入門は今回で最後です。書き残したことで重要なところを書きたいと思います。

 今回特に難しかったところはCognitoの使い方でした。一通り「AWS+Reactアプリ作成入門(Cognito編)」に書きましたが、重要な点をまだ書いていません。Reactアプリは複数のComponentファイルから成り立っています。「AWS+Reactアプリ作成入門(Cognito編)」で示したApp Componentを参照してください。App.jsファイルがロードされたときに非ログインユーザとして権限を持ち、LoginView Componentでログインした時にログインユーザとしての権限を持つようになり、Admin ComponentでS3やDynamoDBに画像ファイルやドキュメントをputするわけです。明記しているドキュメントが見つからず、ログイン状態はAdmin.jsでもそのまま保持されるのかが疑問でした。いろいろ試行錯誤した結果、答えはYesです。
 LoginView Componentで一度ログインすれば、その他のComponentのプログラムはログインユーザとして動作します。ユーザIDはAWS.config.credentials.identityIdでグローバルに参照できます。

1.Admin Component

 Admin Componentはログイン後にメニューに現れる管理画面で、画像掲示板の投稿・編集・削除を行う場所です。ログインユーザとしての権限を最大限に発揮できる画面です。特にCognito認証関係のコードを書かずに権限を実行できます。

src/views/Admin.jsの一部
import AWS from "aws-sdk";
import React from 'react';

---

export default class Admin extends  React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = {

---

  _fetch(identityId) {
    const _self = this;
    var dynamo = new AWS.DynamoDB.DocumentClient();
    var param = {
      TableName : tablename,
      ScanIndexForward: false, //queryには効くが、scanには効かない
      KeyConditionExpression : "identityId = :identityId",
      ExpressionAttributeValues : {":identityId" : identityId}
    };
    dynamo.query(param, function(err, data) {
        if (err) {
            console.log("### Error="+err);
        } else {
            //console.log("### data="+JSON.stringify(data.Items));
            _self.setState({items: data.Items});
        }
    });
  }

  componentWillMount() {
    const _self = this;
    const email = localStorage.getItem('email');
    const username = localStorage.getItem('username');
    var identityId = AWS.config.credentials.identityId;

    if( !email ) {
        handleErrorFunc('エラー:ログインしていません');
        return;
    }
    this.setState({identityId: identityId});
    this.setState({email: email});
    this.setState({username: username});
    this._fetch(identityId);
  }

---

  postAdd() {
    const _self = this;
    const identityId = _self.state.identityId;

//-------------------------------
// Date
//-------------------------------
    let uploadTime = 0;
    let uploadDate = "";
    let partitionYear = 0;
    if( !this.state.updateItem ) { //新規投稿
        const date = new Date() ;
        uploadTime = date.getTime();
        uploadDate = toLocaleString(date);
        partitionYear = date.getFullYear();
    } else {                        //編集投稿
        uploadTime = _self.state.updateItem.uploadTime;
        uploadDate = _self.state.updateItem.uploadDate;
        partitionYear = _self.state.updateItem.partitionYear;
    }


//-------------------------------
// S3 put
//-------------------------------
    let filepath = noimage;
    let thumbnail = noimage;
    let fileType = noimage;
    if( _self.state.imageOverwrite && !!_self.state.file) { 
        if (!_self.state.file.name.match(/^[0-9a-zA-Z._-]*$/)) {
            handleErrorFunc('エラー:ファイル名は小文字の英数字と . - _ しか使えません: '+_self.state.file.name );
            return;
        }
        filepath = 'contents/images/'+identityId+'/'+_self.state.file.name;
        thumbnail = filepath.replace(/images/, 'thumbnail');
        fileType = _self.state.file.type;
        console.log("filepath="+filepath);
        var params = {
            Bucket: bucketname,
            Key: filepath,
            ContentType: _self.state.file.type,
            Body: _self.state.file,
            Metadata: {
              data: JSON.stringify({
                identityId: identityId,
                uploadTime: uploadTime,
                uploadDate: uploadDate
              })
            }
        };
        var s3 = new AWS.S3();
        s3.putObject(params, function(err, data) {
            if(err) {
                console.log("Err: upload failed :" +err);
            } else {
                console.log("Success: upload ok");
                let url = 'http://'+bucketname+'.s3-'+appConfig.region+'.amazonaws.com/'+filepath;
                console.log("######11 imgurl="+url);
                _self.setState({imgurl: url});
            }
        });
    } else if ( _self.state.updateItem && !_self.state.imageOverwrite ) { //編集投稿で上書きアップロード無し
        filepath = _self.state.updateItem.filename;
        thumbnail = _self.state.updateItem.thumbnail;
        fileType = _self.state.updateItem.fileType;;    
    }
//-------------------------------
// DynamoDB putItem
//-------------------------------
    const title = escape_html(_self.state.title);
    let story = escape_html(_self.state.story);
    story = story.replace(/((http:|https:)//[x21-x26x28-x7e]+)/gi, "<a href='$1'>$1</a>");


    var docClient = new AWS.DynamoDB.DocumentClient();
    var params = {
        TableName: tablename,
        Item:{
             identityId: identityId, // ★prime partition key
             email: _self.state.email,
             username: _self.state.username,
             filename: filepath,
             thumbnail: thumbnail,
             type: fileType,
             title: title,
             story: story,
             imageOverwrite: _self.state.imageOverwrite,
             mapUse: _self.state.mapUse,
             position: _self.state.position,
             uploadTime: uploadTime, // ★prime & secondary sort key
             uploadDate: uploadDate,
             partitionYear: partitionYear, //★secondary partition key
             refCounter: 0
        }
    };
    docClient.put(params, function(err, data) {
        if(err) {
            console.log("Err: table put :" +err);
        } else {
            console.log("Success: table put ok");
        }
    });

//-------------------------------
// Clear form
//-------------------------------
    this.handlePostNew();

    this._fetch(identityId);

  }

---


 LoginView Componentでログインしているので、このAdmin ComponentでもログインユーザとしてS3やDynamoDBにputする権限を持っています。一応念のためにcomponentWillMount()でログインしているかの確認をしています。localStorageにemailがセットされているか否かで判断しています。LoginView Componentの以下のコードが効いています。再掲載していきます。

    AWS.config.credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId: appConfig.IdentityPoolId,
        Logins : {
            // Change the key below according to the specific region your user pool is in.
            'cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xn9ihTu0b' : result.getIdToken().getJwtToken()
        }
    });

 見直しや、修正を入れること張りますが、以上で「AWS+Reactアプリ作成入門」を終わります。

続きを読む