AWS Step Functionsを使ってみた

ワークフローシステムではTresure Data社のDigdagを利用したことがあったのですが、Step Functionsの提案なんかもあったので、ちょっと使ってみました。使い道としてはLambdaは5分制限があるので、それを回避することも考えられますね。

やりたいこと

・Step Functionsを使う
・Lambdaを呼ぶ

前提

私のように初心者が初めて作るという方を前提としています。
Lambdaはある程度わかっていること。ちなみに私は実案件で使ったことないので、その程度で大丈夫です。

すごく単純なフロー

こちらを作成します。単純に1Lambdaを実行させるだけです。
image.png

1.LambdaのIAMロールを作成

Lambdaを作成するにはLambda用のロールを作成しとかないといけないので、まずは作成します。すでにロールがある場合は不要です。
IAMのサービスを開き、
image.png

ロールタブからロールの作成をクリック。
image.png

AWSサービスをクリックして、このロールを使用するサービスを選択で、Lambdaを選んで次のステップとします。
image.png

ポリシータイプは何も選択せずに次へのステップとします。
image.png

ロールの名前は適当に決めて、OKです。
image.png

2.Lambdaを作成

Lambda関数を作成します。
image.png

関数の作成をクリック
image.png

関数の名前はなんでもよいです。ランタイムはNode.js 6.10、ロールはデフォルトの既存のロールのままで、既存のロールに上で作成したロールを設定します。
image.png

Lambdaの画面ですが、トリガーは何も設定しません。
image.png

下の方のコードを以下のように書き換えます。
image.png

3.Step Functionsの作成

Step Functionsのステートマシンを作成します。ステートマシンとはまぁ簡単にいうとワークフローです。ワークフローを作成しますといっても同じです。JSONベースで出来ています。

検索からサービスを選択します。
image.png

ステートマシンを初めて作る場合はこのような画面が表示されるので、「今すぐ始める」ボタンを選択します。
image.png

ステートマシンの名前を付けます。ワークフローの名前ですね。とりあえず「HelloWorldStateMachine」と付けました。
image.png

設計図では「Hello World」を選択します。
image.png

コードは以下のように設定します。
・Type→Taskに変更
・Result→Resourceに変更
・値を先ほどlambdaで作ったarnに変更
image.png
arnはResultをResourceに変更して、値のところをクリックすると候補がいくつか出てくるので、関数名で判断できます。
image.png

ビジュアルワークフローは以下のようになります。
今回は関数を呼び出しているだけのシンプルなワークフローです。
AWS Glueでも思ったのですが、ビジュアルワークフローからコードが生成されるとかなり使いやすいんですけどね。
image.png

すべて完了したら「ステートマシンの作成」ボタンを選択します。
実行ロールが自動作成されるため、デフォルトでOKを押します。
image.png

4.実行

新しい実行を選択します。
image.png
入力を修正して実行!
image.png
実行の詳細で出力タブを確認すると、Lambdaで設定したcallbackが返ってきていることがわかります。
image.png

まとめ

今回は単純なフローを作成しましたが、次は分岐があるフローやもう少し意味のあるLambdaで試してみたいです。
参考:
チュートリアル:Lambda ステートマシンを作成する
AWS Step Functionsチュートリアル実践:Lambdaステートマシンの作成と実行 #reinvent

続きを読む

HoneypotのDionaeaでマルウェアを収集しちゃって、APIでスキャンして、結果をビジュアライズしちゃうぞ

はじめに

こんばんはー
Advent Calendar11日目ですね!
そして、間違えて同じ日に二つもエントリーしてしまいましたd(゚∀゚d)ォゥィェ!!

ちなみに、もう一つのQiita記事は、以下なので興味ある人は見て頂ければと思います。

絶対的に使った方がいいLogstashのMultiple Pipelinesについて書いてみた

今年は、ハニーポッターになるべく、いろいろ試してたのですが、Qiita記事を一切書いてませんでした。。
書こう書こうと思ってたら、今年ももう終わり。。
でも、最後の追い上げ!ということで、しっかりと足跡を残したいと思いますー

てことで、今回は、Dionaeaというハニーポットツールを使ってマルウェアを収集して、スキャンとか可視化までをゆるーく書いていきます。
流れはこんな感じです!

  1. 環境について
  2. 全体構成について
  3. Dionaeaのインストール
  4. VirusTotalのAPIを利用するよ!
  5. Logstashで取り込むよ
  6. Kibanaで見てみる

環境について

すべてAWSで構築してます。

  • Dionaea Server

    • Ubuntu Server 14.04 LTS
    • Dionaea
    • Python 2.7.6
  • ElastciStack Server

    • Logstash 6.0
    • logstash-input-s3
    • Elasticsearch 6.0
    • Kibana 6.0

全体構成について

ざっくりと構成について説明しますー

test.png

Dionaeaを配置し、VirusTotalにAPIコールし、マルウェアかどうかをスキャンします。
スキャン結果のjsonファイルをS3に格納し、Logstashがデータを取得します。
Logstashが取得したデータをElasticsearchにストアし、Kibanaがビジュアライズします。
といった感じの構成です!

Dionaeaのインストール

Dionaeaのインストールは、以下の手順でインストール完了です。
公式の手順に則るため、Ubuntuのバージョンは14.04にしてます。

参考:Install Dionaea

$ sudo apt-get update
$ sudo apt-get dist-upgrade
$ sudo add-apt-repository ppa:honeynet/nightly
$ sudo apt-get update
$ sudo apt-get install dionaea
$ sysv-rc-conf dionaea on
$ sysv-rc-conf --list | grep dionaea
$ service dionaea start

Dionaeaについて

Dionaeaは、低対話型のハニーポットのため、危険度は低いです。
ただ、一般的なサーバのセキュリティ対応は実施していることを前提にしてます。
細心の注意を払って対応してくださいね!

Dionaeaが、アタックされるように対象ポートをセキュリティグループでフルオープンにします。

Port Service Source
21/TCP ftp 0.0.0.0/0
42/TCP nameserver 0.0.0.0/0
80/TCP http 0.0.0.0/0
135/TCP msrpc 0.0.0.0/0
443/TCP https 0.0.0.0/0
445/TCP microsoft-ds 0.0.0.0/0
1433/TCP mssql 0.0.0.0/0
3306/TCP mysql 0.0.0.0/0
5060/TCP sip 0.0.0.0/0
5061/TCP sip-tls 0.0.0.0/0

Dionaeaのディレクトリについて

  • /opt/dionaea/var/dionaea/binaries/: マルウェアが配置されるディレクトリ
  • /opt/dionaea/log/: dionaea自身のログを保存
  • /opt/dionaea/wwwroot/: Webのドキュメントルート

VirusTotalのAPIを利用するよ!

VirusTotalは、マルウェアなんじゃないか?!とか、怪しいURLなのでは?!といった時に使用するマルウェアチェックサービスです。
VirusTotalは、マルウェア検出するために50種類以上のエンジンで検査ができます。
しかも、APIも公開されており、スキャン結果を容易に取得できます。
そして、無料!

今回は、VirusTotalのAPIを利用して、Dionaeaに仕込まれたマルウェアをスキャンします。
APIを利用するには、API Keyの取得が必要なため、以下のサイトからアカウント登録を実施してAPI Keyを取得します。

アカウント登録:VirusTotal

注意点ですが、APIは、1分間に4回までしかリクエストできません。
以下のドキュメントを参考にしてください。

参考:Document VirusTotal

あと、森久さんのサイトも非常に参考になります!
VirusTotalだけの話ではなく、ハニーポットに興味ある方は、読んだ方がいいです!

参考:www.morihi-soc.net

APIコール用のスクリプト

Dionaeaのbinariesに仕込まれたマルウェアに対してスキャンを実行するスクリプトです。

#!/usr/bin/env python
import sys
import json
import urllib
import urllib2
import os
import time

files = os.listdir('/opt/dionaea/var/dionaea/binaries/')

for hash in files:
  print hash

  url = "https://www.virustotal.com/vtapi/v2/file/report"
  # API Keyにはアカウント登録した際に取得したKeyを入力する
  params = {"resource": hash, "apikey": "API Key"}

  data = urllib.urlencode(params)
  request = urllib2.Request(url, data)
  response = urllib2.urlopen(request)
  json = response.read()

  with open("{}.json".format(hash), "w") as result:
    result.write(json)

  print "..processing"
  # 1分間に4回までのAPIコール制限があるためスリープを設ける
  time.sleep(20)

結果をjsonファイルで出力します。
出力したファイルをS3マウントした箇所に配置することで、S3にアップロードされます。

Logstashで取り込むよ

ElasticStackはすでにインストール済みを前提にしてます。
また、S3にアクセスするため、Logstashのプラグインの”logstash-input-s3″もインストールしていることとします。
もしインストールされていない場合は、対応お願いします。

Logstash.conf

Logstashのconfですが、データソースがjsonのため、フィルタに書くのはjsonだけです。
めっちゃ楽ですね!
これでElasticsearchにストアされます。

logstash.conf
input {
  s3 {
    tags => "dionaea"
    bucket => "hoge_backet"
    region => "ap-northeast-1"
    prefix => "hoge/"
    interval => "30"
    sincedb_path => "/var/lib/logstash/sincedb_dionaea"
    codec => json
  }
}
filter {
  json {
    source => "message"
  }
}
output {
  elasticsearch {
    hosts => [ "localhost:9200" ]
    index => "vt-logs-%{+YYYYMMdd}"
  }
}

Kibanaで見てみる

それでは、ElasticsearchにストアしたデータをKibanaで確認してみましょう!

FireShot Capture 50 - Kibana_ - http___54.211.11.251_5601_app_kibana#_discover.png

ちゃんとビジュアライズできてますね!

最後に

こんな感じでマルウェアを収集し、スキャン結果をビジュアライズできちゃうのです!
それでは、楽しいお時間をお過ごしくださいーヽ(*゚д゚)ノ

ではでは、明日は、”GoogleHome”についてですね!
めっちゃ楽しみだー

続きを読む

LaravelとElastic Beanstalkでサクッとスタブサーバを作る

概要

とにかく大至急スタブAPIサーバが必要でした。

Laravelのルーティング設定だったらすぐできるな

サーバどうしよう。PHP7のサーバが手元にない

Elastic Beastalkでできるかな

Laravelの準備

Laravelのプロジェクトを新規に作成。

Laravelプロジェクトを作成
composer create-project --prefer-dist laravel/laravel stub-server --no-secure-http

モックなのでルーティングだけ書いて済ませます。

routes/api.php
Route::get('test', function () {
    $res = <<< END
{
  "token": "token"
}
END;
    return response($res)
        ->withHeaders([
            'Content-Type' => 'application/json',
        ]);
});

アプリ側はこれだけです。
リクエストパラメータのバリデーションや、ステータス200以外も返したい場合はこれだと対応できないので、コントローラに書く必要があります。

zipでまとめる

Beanstalkにデプロイするにはいくつか方法がありますが、今回はzipファイルにして管理コンソールからアップします。

zipファイル作成
$ cd stub-server
$ zip ../laravel.zip -r * .[^.]*

Elastic Beanstalkの設定

管理コンソールのBeanstalkの画面で 新しいアプリケーションの作成 をクリックします。
アプリケーション名を入力して 作成 ボタンをクリック

1.jpg

以下のような画面に遷移するので 今すぐ作成しましょう をクリック

2.jpg

開いたウィンドウの ウェブサーバー環境 を選択して 選択 をクリック

3.jpg

URLを決めて 使用可能かチェック をクリックします。使用可能なら下記のようになります。

4.jpg

プラットフォームに PHP 、アプリケーションコードに コードのアップロード を選択し アップロード ボタンをクリックします。

5.jpg

先ほど作成したZIPファイルを選択し アップロード します。

6.jpg

作成が始まります。

7.jpg

5分ほどで作成完了します。 ※現在アプリは削除済みです

8.jpg

ただ、このままではアクセスしても403エラーになるので、最後にドキュメントルートの設定を行います。

9.jpg

10.jpg

設定後、自動的に環境に反映されるのを待って ドメイン/api/XXXXXXXXX にアクセスすると、、、

11.jpg

表示されました😃

今回はここまでですが、HTTPS化したいですね。

続きを読む

動画を探して自動ツイートしてくれるPython製botをAWSに載せてみた(前編)

TL;DR

  • YouTubeから動画を拾ってTweetするbotをPythonで開発し、AWS Lambdaに載せてみました
  • 全2記事です。前編のこちらでは、主にPythonでの開発周りのトピックにフォーカスします
    • TwitterAPIを使ってプログラムからツイートしてみます
    • YouTubeのページを構文解析し、文字列操作を使って動画URLを抽出してみます

動機

新しい職場にて初めてAWSを触ることになったので、これを機にと個人アカウントを取ってみました。チュートリアルだけというのももったいないので、何か自分のためのサービスを作って載せると面白そうです。

で、Twitterのbot開発にはもともと興味があったので、これも前から興味を持ちつつ触ってなかったPythonでbotを作り、lambdaを使って運用してみようと思い立ちました。AWS lambdaは2017年4月からPython3系を扱えるようになったので、心置き無く最新バージョンで書けそうだなー、というのも狙いです。

ユーザーストーリー

毎日の退勤をもう少し楽しみにするために、定時になると自分が興味ありそうなYouTube動画をbotが勝手に検索して、自分のTwitterアカウントに届けてくれるようにしたい。
スクリーンショット 2017-12-06 23.30.33.png

前提

  • 開発にはMacを使用します
  • Pythonは3.6系を使用します
  • pyenvもvirtualenvも使用しません。議論はあろうかと思いますが、個人開発なので。。
  • で、開発環境構築はこちらの記事等を参照しました
  • bot化したいTwitterアカウントはあらかじめ用意してあるものとします

TwitterAPIを使ってプログラムに呟かせる

アクセスキーの取得

bot化したいアカウントでTwitter Application Managementにログインすると、アプリケーションの作成とConsumer Key、及びAccess Tokenの取得ができます。

なお、Appの作成にはTwitterアカウントが電話番号認証済みである必要があります。認証済みでないと怒られるので、エラーメッセージ中のリンクからさらっと済ませておきましょう。

  • Consumer Key
  • Consumer Key Secret
  • Access Token
  • Access Token Secret

以上の4パラメータがあればプログラムからのツイートができます。コピーしてこんな感じのファイルを作っておきましょう。

config.py
CONSUMER_KEY        = "xxxxxxxxxxxxxxxxx"
CONSUMER_SECRET     = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ACCESS_TOKEN        = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
ACCESS_TOKEN_SECRET = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

複数の外部ユーザーからアクセスがあるようなアプリケーションの場合(=「このアプリケーションとの連携を許可しますか?」など出るやつ)はそれぞれの役割についてもう少し説明が必要ですが、今回はある程度一緒くたに考えてしまっても実装に支障ありません。

PythonでOAuth認証

ライブラリの導入と管理

Pythonのライブラリは、パッケージ管理ツールであるpipでインストールできます。仮想環境がない場合、オプション無しで勝手にglobalに入るのがうーん、という感じですがまあそれは置いておいて。

PythonでHttp通信を行うライブラリとしては、requestsがポピュラーなようです。また、今回はTwitterAPIを使うための認証が必要なので、OAuth認証を扱えるライブラリも必須です。ここはrequestsと同じところが公開しているrequests_oauthlibを使用しました。

pip3 install requests requests_oauthlib

さて、インストールはできましたが、今度は開発するプロジェクトがこれらのライブラリに依存していることを表明しておくのがマナーです。js界隈で言うところのpackage.jsonですね。

Pythonでは依存関係を記したrequirements.txtなどを作っておくケースが多いようです。

requirements.txt
requests==2.18.4
requests-oauthlib==0.8.0

ちなみに、pip3 freeze > requirements.txtでインストールされた依存関係をrequirements.txtに吐き出せます。

逆に.txtファイルを元に一括インストールする場合は、-rオプションを用いてpip3 install -r requirements.txtなどと書けます。結構便利です。

つぶやいてみる

first_tweet.py
from requests_oauthlib import OAuth1Session
import config, json

twAuth = OAuth1Session(
  config.CONSUMER_KEY,
  config.CONSUMER_SECRET,
  config.ACCESS_TOKEN,
  config.ACCESS_TOKEN_SECRET)
apiURL = "https://api.twitter.com/1.1/statuses/update.json"
params = { "status": "プログラムにツイートさせてみるテスト" }

res = twAuth.post(apiURL, params = params)
print(json.loads(res.text))

先ほど作ったconfig.pyimportして、これだけ。思ったよりだいぶ手軽です。Twitterにアクセスして実際にツイートされたことを確認しましょう!

また、せっかくなのでレスポンスをjsonライブラリでロードして吐き出してみます。

{'created_at': 'Wed Dec 06 14:00:00 +0000 2017', 'id': 9384076800000000, 'id_str': '9384076800000000', 'text': 'プログラム
にツイートさせてみるテスト', 'truncated': False, 

...(中略)...

'retweeted': False, 'lang': 'ja'}

思ったよりいろんな属性があることがわかりますね。深掘りは公式のリファレンスにて。

YouTubeから動画のURLを拾ってくる

続いて、YouTubeから動画を探してくるパートです。

Webクローリング

この分野では、「クローリング」や「スクレイピング」と言った言葉が有名です。

クローリングとスクレイピング

クローリングはウェブサイトからHTMLや任意の情報を取得する技術・行為で、 スクレイピングは取得したHTMLから任意の情報を抽出する技術・行為のことです。

たとえば、あるブログの特徴を分析したい場合を考えてみましょう。
この場合、作業の流れは

  1. そのブログサイトをクローリングする。
  2. クローリングしたHTMLからタイトルや記事の本文をスクレイピングする。
  3. スクレイピングしたタイトルや記事の本文をテキスト解析する。

というようになります。

今回は、YouTubeをクローリングし、その中から動画のURLをスクレイピングすることになりますね。

Webページのクローリングとスクレイピングを行う際は、それがどんな目的のものであれ、HTMLを構文解析することが必須となります。Pythonでは、これを強力に支援するBeautifulSoupと言うライブラリがあります。執筆時点で最新のbeautifulsoup4を導入してみます。

pip3 install beautifulsoup4

早速使ってみましょう。Qiitaのトップページから<a>タグを探し、その中に含まれるhref属性の値を取得してみます。

crawling.py
import requests
from bs4 import BeautifulSoup

URL = "https://qiita.com/"
resp = requests.get(URL)

soup = BeautifulSoup(resp.text)

# aタグの取得
a_tags = soup.find_all("a", href=True)
for a in a_tags:
    print(a["href"])

結果

/about
https://qiita.com/sessions/forgot_password
https://oauth.qiita.com/auth/github?callback_action=login_or_signup
https://oauth.qiita.com/auth/twitter?callback_action=login_or_signup

・・・(中略)

https://qiita.com/api/v2/docs
https://teams.qiita.com/
http://kobito.qiita.com

いい感じです!

HTMLパーサーについて

さて、先のコードを実際に試すと、HTMLパーサーが明示されていないために警告が出ます。これは実際の解析時に使われるパーサーが実行時の環境に依存するためです。異なる環境下で同じ振る舞いを期待するには、使用するHTMLパーサーを明示してあげる必要があります。

デフォルトではhtml.parserが使われますが、lxmlかhtml5libを導入してこちらを明示してあげるのが無難なようです。このあたりの情報は下記の記事をだいぶ参考にさせていただきました。パーサーの選択だけでなくスクレイピング全般の情報が非常によくまとまっているエントリなので、オススメです。

PythonでWebスクレイピングする時の知見をまとめておく – Stimulator

パーサの良し悪しを考えるとlxmlでチャレンジしてダメならhtml5libを試すのが良さそう。

今回はこの1文に愚直に従ってみます。事前にpip3 install lxml html5libも忘れずに。


import requests
from bs4 import BeautifulSoup

URL = "https://qiita.com/"
resp = requests.get(URL)

+try:
+  soup = BeautifulSoup(resp.text, "lxml")
+except:
+  soup = BeautifulSoup(resp.text, "html5lib")
-soup = BeautifulSoup(resp.text)

# ...以下は先ほどと同様

Crawlerクラスを作ってみる

すでにPythonでオブジェクト指向な書き方を経験している方はこの辺りを飛ばしていただいて構いません。せっかくHTMLを解析してくれるコードができたので、クラスとして書き換えてみます。

crawler.py
import requests
from bs4 import BeautifulSoup

class Crawler:
    def hrefs_from(self, URL):
        a_tags = self.soup_from(URL).find_all("a", href=True)
        return set(map(lambda a:a["href"], a_tags))

    def soup_from(self, URL):
        res_text = requests.get(URL).text
        try:
            return BeautifulSoup(res_text, "lxml")
        except:
            return BeautifulSoup(res_text, "html5lib")

個人的にはインスタンスメソッドの第1引数が常にselfでなければならないのは書く量が増えるので少しもどかしいですね。ハマりポイントにもなりかねない…。

ちなみに、ここではラムダ式を使用し、hrefs_fromメソッドの戻り値の型をsetにしてみました。これは、今回のユースケースを鑑みてリンク先URLの重複を排除した方が便利と判断したためです。出現頻度など解析したい場合はまた改めて設計を考える必要があるでしょう。

継承と、YouTubeへのアクセス

YouTubeをクローリングするにあたって、「検索文字列を与えたら検索結果のページをクローリングし、動画を探してくる」などの機能があると便利そうです。先ほどのクラスを継承して、実装してみます。

tube_crawler.py
import random
import re
from crawler import Crawler

class TubeCrawler(Crawler):

    URLBase = "https://www.youtube.com"

    def hrefs_from_query(self, key_phrase):
        """
        検索文字列を与えると検索結果ページに含まれるhref属性の値を全て返す
        """
        return super().hrefs_from(self.URLBase + 
            "/results?search_query=" + key_phrase.replace(" ", "+"))



    def movies_from_query(self, key_phrase, max_count = 10):
        """
        検索文字列を与えると検索結果ページに含まれる動画のビデオIDを返す
        """
        return self.__select_movies(self.hrefs_from_query(key_phrase), max_count)



    def __select_movies(self, hrefs, max_count):
        """
        privateメソッド。href属性の値のsetからビデオIDのみを返す
        """
        filtered = [ re.sub( "^.*/watch?v=", "", re.sub( "&(list|index)=.*$", "", href )) 
            for href in hrefs if "/watch?v=" in href ]
        return filtered[:min(max_count, len(filtered))]



    def choose(self, movie_ids, prefix = "https://youtu.be/"):
        """
        渡した文字列のリスト(ビデオIDのリストを想定)から1つを選び、prefixをつけて返す
        """
        return prefix + random.choice(movie_ids)

文法的には継承とprivateメソッドの書き方あたりが新しい話題となります。この記事の主題ではないので特段の説明は省きます。

実際に試すとわかるのですが、検索結果のページにノイズとなるリンクが多いばかりか、再生リストへのリンクなど紛らわしいものも多く、その辺を適切に弾いていくのに手こずりました。おかげでfilter関数や正規表現に少し強くなれた気がします。

正規表現についてはこちらの記事をだいぶ参考にしました。

Pythonの正規表現の基本的な使い方

繋げてみる

準備が整ったので検索->ツイートの流れを試してみます。

main.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tube_crawler import TubeCrawler
from tweeter import Tweeter
import config

def main():
    t = TubeCrawler()
    movies = t.movies_from_query("Hybrid Rudiments")
    chosen = t.choose(movies)

    # ツイートする部分をクラス化したもの
    tw = Tweeter()
    tw.reply(config.REPLY_TO, chosen)

if __name__ == '__main__':
    main()

エントリーポイントとなる関数が必要かなー、と思ったので何気なく(そう、本当に何気なく。これで良いと思っていたんですLambdaを使うまでは…)main関数を作成。

直接./main.pyでも呼べるようにこの辺からShebangを記述し始めました。また、末尾はファイル名で直接実行した場合にmain()を呼ぶためのおまじない。Rubyにも似たやつがありますね。あとはターミナルから呼んで動作確認するだけです。

$ ./main.py

実行したところ問題なく動きそうだったので、次回はAWS Lambdaに載せていきます。それなりの尺となったのでこのページはここまでです。お読みいただきありがとうございました。

リンク

続きを読む

はじめてのServerless ✕ Webpack ✕ TypeScript


このエントリーはaratana Advent Calendar 201712日目のエントリーです。

こんばんは!最近Google Home MiniAmazon echo dotを購入したはいいが置き場所に困っている蔭山です。
みなさんはどのような場所に置かれているのでしょうか。。。

前日は新卒エンジニアには決して見えない安定感をお持ちの猿渡くんの「NoSQLについて何か。」という記事でした!
NoSQL?あぁ、聞いたことはある。
みたいな僕でもわかりやすい記事でした!
最近AWSに興味が出始めたところでしたので、ぜひDynamoDBを使って軽い画像投稿サービスでも作ってみます!

さて今回はServerless ✕ Webpack ✕ TypeScriptの組み合わせで使えるように手順をまとめてみたいと思います!

動作環境

今回の動作環境は以下になります!

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1114

$ node -v
v8.2.1

$ serverless -v
1.24.0

$ webpack -v
3.5.6

環境準備

nodeやserverlessのインストールについては下記記事を参考に。。。

とりあえずやってみる

まずはServerlessのプロジェクトを作ってみましょう

$ serverless create -t aws-nodejs -p hogehoge
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/fugafuga/hogehoge"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.24.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"
$ cd ./hogehoge

JSer御用達npmを使って必要なパッケージをダウンロードしましょう。

$ npm init
$ npm install --save-dev serverless-webpack serverless-offline ts-loader typescript webpack

インストールが終わり次第、各種設定を行います。
今回はTypescript -> ES2015へのコンパイルを目的に設定させていただきます。
細かい設定内容に関しては割愛します。

./serverless.yml
service: hogehoge

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

plugins:
- serverless-webpack
- serverless-offline

functions:
  hello:
    handler: handler.hello
    events:
     - http:
         path: hello
         method: get
./webpack.config.js
module.exports = {
  entry: './handler.ts',
  target: 'node',
  module: {
    loaders: [{
      test: /\.ts$/,
      loader: 'ts-loader'
    }]
  },
  resolve: {
    extensions: ['.ts']
  },
  output: {
    libraryTarget: 'commonjs',
    path: __dirname + '/.webpack',
    filename: 'handler.js'
  }
};
./tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs"
  },
  "exclude": [
    "node_modules"
  ]
}

準備はできたので、次はTypeScriptでコーディングしてみましょう!

./handler.ts
export * from './src/ts/functions/hello';
./src/ts/functions/hello.ts
export function hello(event, context, callback): void {
  const response = {
    statusCode: 200,

    headers: {
    },

    body: JSON.stringify({
      "message": "Hello Serverless x Webpack x TypeScript!!"
    })
  };

  callback(null, response);
};

コードが書けたらローカル環境で動作確認

$ sls offline
・・・・・・・・・・・・・・・・
途中は割愛。m(__)m
・・・・・・・・・・・・・・・・
Serverless: Routes for hello:
Serverless: GET /hello

Serverless: Offline listening on http://localhost:3000

きちんと動作するか確認。

$ curl -X GET http://localhost:3000/hello
{"message":"Hello Serverless x Webpack x TypeScript!!"}

動作が問題なければ、早速デプロイしてみましょう!

$ sls deploy
・・・・・・・・・・・・・・・・
途中は割愛。m(__)m
・・・・・・・・・・・・・・・・
api keys:
  None
endpoints:
  GET - https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
functions:
  hello: hogehoge-dev-hello

デプロイが完了したようです。
では早速動作確認。

$ curl -X GET https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"Hello Serverless x Webpack x TypeScript!!"}

ちゃんと動きましたね!

最後に

無理やりTypeScriptを使った感が凄まじいですね。。。申し訳ありません><
僕個人がTypeScriptを使ったことがなかったため使ってみたかったんです

明日は新卒田村くんの「Ctagsで自由な翼を得たVimについて」です!
お楽しみに!

参考

主にこちらの記事を参考にさせて頂きました!ありがとうございますm(__)m

続きを読む

S3をGoでマウントする

S3をファイルシステムとしてマウントするためのデーモンをつくっています。ホームディレクトリ全体を共有できてしまえば、dotfilesまわりのセットアップを頑張らなくてもよいのでは、というのが開発の動機です。
なので、既存のS3を便利に見れるものと言うよりも、セキュアかつパフォーマンスがなるべく(ファイルによらず)フラットで高速なファイルシステムを目指しています。

juntaki/bucketsync: S3 backed FUSE Filesystem written in Go with dedup and encryption.

使い方

まずは、設定ファイルを作成します。$HOME/.bucketsync/config.ymlにファイルができます。S3のクレデンシャルが平文で入ってしまうので取扱は注意です。出力されたファイルには暗号化や圧縮のオプションがありますが、記事公開の時点では実装が間に合ってないため意味はないです。。

bucketsync config --bucket <Bucket name> \
                  --region <Region, e.g. ap-northeast-1> \
                  --accesskey <AWS access key> \
                  --secretkey <AWS secret key> \
                  --password <Password for data encryption>
config.yml
bucket: ""
region: ""
access_key: ""
secret_key: ""
password: ""
logging: production
log_output_path: /home/juntaki/.bucketsync/bucketsync.log
cache_size: 1024
extent_size: 65536
encryption: true
compression: true

あとは適当なディレクトリに、マウントするだけです。

bucketsync mount --dir /path/to/mountpoint

アンマウントするには、このコマンドです。

bucketsync unmount

仕組み

使っているライブラリを中心に、全体的な設計を説明します。

FUSE

Linuxでは、FUSEという仕組みでユーザー空間でファイルシステムを作ってマウントすることが出来ます。
カーネルのFUSEモジュールを使うlibfuseを使って実装する方法もありますが、今回の実装ではGoで書かれたライブラリの、hanwen/go-fuse: FUSE bindings for Goを使っています。go-fuseで定義されているインターフェースを実装すれば、簡単にファイルシステムが作れます。

bucketsyncでは、pathwalkのあたりをガッツリ書きたかったので、pathfsを使っています。なので、実装したインターフェースはpathfs.Filesystemです。

データの格納方法

S3コンソール方式の問題

既存のS3を便利に見れるようにFUSEでマウントしようとすると、(S3のコンソールから見てもそうなってるように)オブジェクトのキーとファイルパスを1対1で対応させると思います。たとえば、マウントポイントからdirディレクトリの下に色々とファイルを格納すると、こんな感じのキーのオブジェクトが複数できます。

/dir/test1.txt
/dir/test2.txt
/dir/test3.txt
/dir/test4.txt

ここで、dirディレクトリをリネームすると、なんと、配下の全オブジェクトをリネームしなくてはなりません。(4つくらいならまだいいですが)
下記にもあるように、これはデータ構造の問題なので、プログラムの工夫でどうにかするにも限界があります。
Support for deep directory rename · Issue #312 · s3fs-fuse/s3fs-fuse

それに加えて、クライアントサイドでオブジェクトの暗号化や圧縮をしようと考えたとき、ランダムリード性と両立させようとすると、それらの実装の選択肢はかなり狭くなります。

bucketsyncのデータ構成

ファイルをS3のオブジェクトとして見ることを諦めると、いろいろな実装が考えられます。bucketsyncでは普通のファイルシステムのようにメタデータのオブジェクトとデータのオブジェクトを分けて格納しています。先程のリネーム問題はディレクトリのメタデータを一つ書き換えれば済むようになります。

メタデータの形式はfilesystem.goに書いています。

image.png

高速化の工夫

ローカルへのキャッシュ

メタデータだけは、pathwalkで頻繁に参照されるのでローカルにLRUでキャッシュします。@cache.go
また、S3へのAPIアクセスはAWS公式SDKを利用しています。
aws/aws-sdk-go: AWS SDK for the Go programming language.

ハッシュ:murmur3 hash

また、データ格納用のオブジェクトにはそのデータのハッシュをキーとしてつけることで、重複排除を実現しています。ハッシュはセキュアでなくてもよいので高速なmurmur3を採用しました。
spaolacci/murmur3: Native MurmurHash3 Go implementation

データのシリアライズ

メタデータはGoのオブジェクトで扱いたいので格納と読み出しで頻繁にSerializeとDeserializeする必要があります。ベンチマークを見ると、JSONのようなリフレクションを必要とする実装より、コード生成してしまうprotocol buffersなどの方が高速なようです。(あるいは、手で作ってしまっても良さそうですが。)
alecthomas/go_serialization_benchmarks: Benchmarks of Go serialization methods
※実装はまだJSONですが・・

あとがき

まだマウントできた!というレベルですが、徐々に使い物になるように改善していきます。

参考

juntaki/bucketsync: S3 backed FUSE Filesystem written in Go with dedup and encryption.
Understanding Filesystem using go-fuse, from scratch // Speaker Deck

続きを読む

[AWS Cloud9]EC2自動停止を検知して別の処理を始める

はじめに

AWS Cloud9はIDEのブラウザを閉じたとき、自動でEC2インスタンスが停止する機能があります。
何分後に、自動停止するかは、設定から変更が可能です。
1.PNG

AWS Cloud9を1週間使うと、EC2インスタンスが自動停止するのが「便利 → 当たり前」という感覚になりました。そして、思いました。自動停止を検知してもっと楽できないかと。

やりたいこと

AWS Cloud9の機能で

  • 検知した情報をトリガーに別の処理を動作させてみる。
  • 今回は、サンプルに以下を動作確認
    • 自動停止のお知らせメール (実装したら、イチイチお知らせが来てウザくなった)
    • RDSの自動停止 (IDEとセットで使ってたので、止め忘れがなくなり非常に便利になった)

自動停止したことを検知

CloudWatchのルールを作成。

  • イベントパターンを選択
  • サービス名:EC2
  • イベントタイプ:EC2 Instance Status-change Notification
  • 特定の状態:インスタンスの停止を設定
  • Specific instance Id(s):AWS Cloud9のインスタンスIDを設定
    2.PNG
{
  "source": [
    "aws.ec2"
  ],
  "detail-type": [
    "EC2 Instance State-change Notification"
  ],
  "detail": {
    "state": [
      "running",
      "stopped"
    ],
    "instance-id": [
      "AWS Cloud9のインスタンスID"
    ]
  }
}

検知した情報をトリガーに別の処理を動作

ターゲットに今回は2つ(SNS(メール送信)とLambda(rds停止)を設定しました。)
SNSは使い道を思いつかなかったのでEメールを設定したのみ。jsonがそのままメールに飛んでくる。

3.PNG

LambdaはついでにIDE起動したときの動作も記述。

  • IDE起動→EC2開始→RDS開始
  • IDE終了→30分後EC2停止→RDS停止
IAMロール
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "rds:StartDBInstance",
                "rds:StopDBInstance"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
lambda_function.py
import boto3

def lambda_handler(event, context):
    dbinstance = 'RDSインスタンス名'
    rds = boto3.client('rds')
    instanceState = event['detail']['state']
    if instanceState == 'stopped':
        result = rds.stop_db_instance(DBInstanceIdentifier = dbinstance)
    else:
        result = rds.start_db_instance(DBInstanceIdentifier = dbinstance)
    print(result)    
  • 開始状態のRDS

4.PNG

  • EC2が停止するイベントが発生

5.PNG

  • RDSが自動で停止
    6.PNG

まとめ

  • CloudWatchを使うことでIDEの終了後、EC2自動停止を検知できました。
  • 後続処理を自動化できることが分かったので、一日の作業終了をブラウザを閉じるだけに出来そうです。

関連

続きを読む

LambdaとCloudWatchでサーバレスなTwitterBot作ってみた

こんにちは。駆け出しインフラエンジニアの(@k4ri474)です。
この記事はVASILY Advent Calendar 2017 11日目の記事です。

Twitterでbotを作ろうと思った時、ざっくり二つ選択肢が頭に浮かびますよね?
巷に溢れているbot作成サービスに登録するか、
はたまた自作するか。

エンジニアの方は、とりあえずプログラミングしてみっか〜となることが多いような気がします。
Twitter APIに関してはもちろん公式ドキュメントがありますし、プログラムもググれば山ほど先人たちのものが見れるので、
なんちゃらキーやらなんちゃらトークンさえ取得できれば、肝心のツイートはチョチョイのジョイかと思います。

ただ、継続的に動かすbotとして考えるとベースのサーバはどうしても必要になるのでちと悩みますよね。
自分のサイトをホスティングしてメールサーバ動かしてついでにbotを回す、などといったような状況ならクラウドでサーバを立てとけばいいんですが、botだけでいい時には少々大げさです。

そこでAWS Lambda + Amazon CloudWatch Eventsを使ってサーバレスに、かつ継続的に実現します。

概要

1postするためのプログラムをpythonで書き、それをLambdaにセットします。
そしてLambdaをCloudWatch Eventsで定期的に発火させ、botの体をなすようにします。

実装(Lambda)

まず、こちらが普通のつぶやきプログラムです。

from requests_oauthlib import OAuth1Session

CK = 'CONSUMER_KEY'
CS = 'CONSUMER_SECRET'
AT = 'ACCESS_TOKEN'
AS = 'ACCESS_TOKEN_SECRET'

URL = 'https://api.twitter.com/1.1/statuses/update.json'

tweet = "Hello World"
session = OAuth1Session(CK, CS, AT, AS)

params = {"status": tweet }
session.post(URL, params = params)

Twitterに接続するにはOAuth認証が必要なのでrequests_oauthlib(https://github.com/requests/requests-oauthlib)
というPython用のOAuth認証ライブラリを利用しました。
Lambdaではライブラリとプログラムコードを一まとめにzip圧縮する必要があるので、プログラムと同じディレクトリにインストールしておきます。

% pip install requests requests_oauthlib -t ./

各種キーは苦労して取得したら、それらを各々セットしていただければあとはつぶやきをいじって実行するだけです。

さて、上のプログラムをLambda用に書き換えます。
Lambdaから関数を呼び出してもらうためには、ハンドラー関数を作成する必要があります。
パラメータとして eventcontext を取る関数で、Lambdaを呼び出したイベントデータやランタイム情報を内部で使えますが、今回はシンプルに使わない方向でいきます。

from requests_oauthlib import OAuth1Session

CK = 'CONSUMER_KEY'
CS = 'CONSUMER_SECRET'
AT = 'ACCESS_TOKEN'
AS = 'ACCESS_TOKEN_SECRET'

URL = 'https://api.twitter.com/1.1/statuses/update.json'

def my_handler(event, context):
    tweet = "Hello World"
    session = OAuth1Session(CK, CS, AT, AS)

    params = {"status": tweet }
    session.post(URL, params = params)

これで準備は完了です。
こちらのファイル群をzip化し、コマンドでアップロードしてみます。

% aws lambda create-function \
--region ap-northeast-1 \
--function-name "sample" \
--runtime "python3.6" \
--role "MY_ROLE" \
--handler "sample.my_handler" \
--zip-file "fileb://./sample.zip" \
--profile PROFILE

注意したいのはhandlerオプションです。構文は実行ファイル名.ハンドラー関数名ですので、作成したプログラム名とハンドラー関数名に合わせて適宜編集してください。

また、セットするroleにCloudWatchLogsの権限を与えておくと実行結果をログ出力できるようになるのでオススメです。
今回は定義済みのポリシー、arn:aws:iam::aws:policy/CloudWatchLogsFullAccessをアタッチしたロールを事前に作成していたので、そちらをセットしてみました。
ポリシーは以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

以上でLambdaへのアップロードは完了です。

実装(CloudWatch Events)

さて、あとはこのLambda関数を定期実行するだけです。
まずはCloudWatch Eventsをトリガーにセットします。

スクリーンショット 2017-12-10 23.09.08.png

Lambdaのダッシュボードから関数を選択し、設定画面でトリガーを設定します。上図のようにCloudWatch Eventsをトリガーにセットしたら、下にスクロールしてトリガーの詳細を設定します。

スクリーンショット 2017-12-10 23.09.56.png

例としてはこんな感じでしょうか。
ルールタイプをスケジュール式に選択すると、おなじみのcron記述で定期実行の間隔をコントロールできます。
僕は試しにもう一方のrate式を使ってみることにしました。

このような感じで設定して保存すると無事トリガーとして機能します。
お疲れ様です。

成果物

このままのプログラムでは全く同じ内容をつぶやくばかりで面白くないので、
プログラムをちょちょっといじってimgurから猫画像のリンクを拾ってきてpostする感じのbotを作ってみました。

https://twitter.com/k4ri474/status/937562270476812290

自分の猫postで心が洗われます。
皆さんもサーバレスTwitter botでぜひ遊んでみてくださいb

続きを読む

Docker + Nginx + Let’s EncryptでHTTPS対応のプロキシサーバーを構築する

Docker上にNginxコンテナをプロキシサーバーとして構築し、Let’s EncryptでHTTPS対応しました。構築にあたって かなり苦戦した ので、そのノウハウを記事としてまとめました。

「Nginx」とは

Apacheなどの従来のWebサーバーは、クライアントの数が多くなるとサーバーがパンクする 「C10K問題(クライアント1万台問題)」 を抱えていました。「Nginx」はこの問題を解決するために誕生した、静的コンテンツを高速配信するWebサーバーです。2017年10月現在、そのシェアは Apacheとほぼ同等 となっています。

wpid-wss-share13.png

Webサーバー シェア
Micosoft IIS 49.44%
Apache 18.78%
Nginx 18.40%

「Let’s Encrypt」とは

「Let’s Encrypt」は すべてのWebサーバへの接続を暗号化する ことを目指し、SSL/TLSサーバ証明書を 無料 で発行する認証局(CA)です。シスコ、Akamai、電子フロンティア財団、モジラ財団などの大手企業・団体がスポンサーとして支援しています。


本稿が目指すシステム構成

本稿ではAmazon EC2、Dockerコンテナを使用して以下のようなシステムを構築することを目標とします。

DockerでNgixのプロキシサーバーを構築する.png

前提条件

  • 独自ドメインを取得していること(本稿で使用するドメインはexample.comとします)
  • IPv4パブリックIP(Elastic IP)がEC2インスタンスに設定されていること
  • EC2インスタンスにDocker、docker-composeがインストールされていること

事前に準備すること

DockerでHTTPS対応のプロキシサーバーを構築するにあたり、事前に以下の設定をしておく必要があります。

  • EC2のインバウンドルールで443ポートを開放する
  • DNSのAレコードを設定する
  • プロキシ用のネットワークを構築する

EC2のインバウンドルールで443ポートを開放する

インバウンドルールを以下のように設定し、443ポートを外部へ公開します。

タイプ プロトコル ポート範囲 ソース
HTTPS TCP 443 0.0.0.0/0
HTTPS TCP 443 ::/0

DNSのAレコードを設定する

DNSの設定方法は利用しているドメイン取得サービスによって異なります。例えばバリュードメインの場合、DNSの設定方法は「DNS情報・URL転送の設定 | VALUE-DOMAIN ユーザーガイド」に記載されています。

DNSのAレコードを以下のように設定します。xx.xx.xx.xxにはEC2インスタンスに割り当てられているIPv4パブリックIPを設定します。

a @ xx.xx.xx.xx
a www xx.xx.xx.xx

上記設定は以下を意味します。

  • example.com(サブドメイン無し)をIPアドレスxx.xx.xx.xxにポイントする
  • www.example.com をIPアドレスxx.xx.xx.xxにポイントする

プロキシ用のネットワークを構築する

プロキシサーバーとWebサーバー間のネットワークは外部との通信を行う必要がありません。そこで
プロキシサーバーとWebサーバー間の 内部ネットワーク を構築するため、EC2のインスタンスにログインし、以下のコマンドを入力します。

$ docker network create --internal sample_proxy_nw

上記コマンドは以下を意味します。

  • --internal: ネットワーク外との通信が行えないネットワークを作成します。
  • sample_proxy_nw: 任意のネットワーク名です。

以下のコマンドを入力し、ネットワークの設定情報がコンソールに出力されていることを確認しましょう。

$ docker network inspect sample_proxy_nw

Dockerコンテナの定義ファイルを作成する

事前準備が完了したら、Dockerコンテナの定義ファイルを作成しましょう。本稿におけるディレクトリ構成は以下のとおりです。

/path/to/dir/

.
├── docker-compose.yml // プロキシサーバーとWebサーバーのコンテナを定義するファイル
└── proxy
    ├── default.conf // プロキシサーバー上にあるNginxのデフォルト定義ファイル
    ├── Dockerfile // プロキシサーバーのイメージを構築するためのファイル
    └── entrypoint.sh // プロキシサーバーにSSL証明書を取得するためのファイル

以下では、各ファイルの内容を解説します。

./docker-compose.yml

docker-compose.ymlでは、以下のコンテナを定義しています。

  • proxy: プロキシサーバー(Nginxベース)
  • web1: Webサーバー(httpdベース)
  • web2: Webサーバー(httpdベース)
version: '3'
services:
  proxy:
    build: ./proxy
    tty: true
    image: sample_proxy
    container_name: sample_proxy
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    ports:
      - "443:443"
    volumes:
      - '/srv/letsencrypt:/etc/letsencrypt'
    networks:
      - default
      - sample_proxy_nw
    depends_on:
      - "web1"
      - "web2"
    command: ["wait-for-it.sh", "sample_web1:80", "--", "./wait-for-it.sh", "sample_web2:80"]
  web1:
    image: httpd
    container_name: sample_web1
    tty: true
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - sample_proxy_nw
  web2:
    image: httpd
    container_name: sample_web2
    tty: true
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - sample_proxy_nw
networks:
  proxy_nw:
    external: true

上記コマンドは以下を意味します。

  • サービスproxyports: 外部からのHTTPSアクセスとproxyサーバーの内部ポートを疎通させるため、443:443を定義します。
  • サービスproxyvolumes: /srv/letsencrypt:/etc/letsencryptを定義します。/etc/letsencryptLet’s Encryptで取得した証明書が生成されるディレクトリ です。
  • networks: 上述の説明で生成したsample_proxy_nwを各サービス(proxy, web1, web2)に定義します。
  • depends_on: コンテナの起動順序を制御するオプションです。 Nginxのproxy_passに設定されているWebサーバーが起動していない状態でプロキシサーバーが起動した場合にエラーとなる ため、web1, web2を設定します。

./proxy/default.conf

./proxy/default.confはNginxのデフォルト定義ファイル(/etc/nginx/conf.d/default.conf)を書き換えるためのファイルです。

server{

    server_name example.com www.example.com;

    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    location / {
        proxy_pass    http://sample_web1/;
    }

    location /example/ {
        proxy_pass    http://sample_web2/;
    }

}

上記設定は以下を意味します。

  • server_name: ユーザーから要求されるHTTPリクエストのヘッダに含まれるHostフィールドとserver_nameが一致した場合、該当するサーバ設定を採用します。Nginxではキャッチオールサーバーとして_を定義することもできますが、 certbot-autoがサーバー情報を正しく取得することができない ため、上記のようにドメイン名を入力します。
  • location: ルートディレクトリ(example.com/)とサブディレクトリ(example.com/example/)にアクセスした際の振り分け先URIを設定します。proxy_passには、http://[コンテナ名]/を設定します。コンテナ名はdocker-compose.ymlのcontainer_nameで設定した名前となります。
    また、http://sample_web1/のように 末尾に/を入れる ことに注意しましょう。例えばlocation /example/において、プロキシパスの末尾に/が含まれていない(http://sample_web2)場合、振り分け先は http://sample_web2/example/となってしまいます。

./proxy/Dockerfile

FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf
RUN apt-get update && apt-get install -y \
        wget && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh
ADD https://dl.eff.org/certbot-auto /usr/local/bin/certbot-auto
RUN chmod a+x /usr/local/bin/certbot-auto
RUN certbot-auto --os-packages-only -n
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

上記設定は以下を意味します。

  • ADD https://dl.eff.org/certbot-auto /usr/local/bin/certbot-auto: Let’s Encryptが発行するSSL/TLSサーバ証明書を自動で取得・更新するツール「 certbot-auto 」をダウンロードします。

./proxy/entrypoint.sh

#!/bin/bash
certbot-auto --nginx -d example.com -d www.example.com -m your-account@gmail.com --agree-tos -n
certbot-auto renew
/bin/bash

上記設定は以下を意味します。

  • --nginx: プロキシサーバーにNginxを使用する場合のオプションです。default.confの設定を自動的に書き換えます。(2017年12月現在、アルファ版のプラグイン)
  • -d example.com -d www.example.com: SSL/TLSサーバ証明書の取得を申請するドメイン名を指定します。
  • -m your-account@gmail.com: アカウントの登録や回復などに使用する電子メールアドレスを指定します。
  • --agree-tos: Let’s Encryptの利用規約に同意します。
  • -n: インタラクティブ設定をオフにします。
  • ./certbot-auto renew: 3ヶ月で失効する SSL/TLSサーバ証明書を自動で更新します。

以下のコマンドを入力してentrypoint.shに 実行権限を付与する ことを忘れないようにしましょう。

$ chmod +x entrypoint.sh

Dockerコンテナを起動する

それでは以下のコマンドを入力してDockerコンテナを起動しましょう。

docker-compose up -d

しばらく時間をおいてから、以下のコマンドを入力します。

docker-compose logs

以下のように出力されていれば成功です。

-------------------------------------------------------------------------------
Congratulations! You have successfully enabled https://example,com and
https://www.example.com

You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=example.com
https://www.ssllabs.com/ssltest/analyze.html?d=www.example.com
-------------------------------------------------------------------------------

HTTPSでアクセスする

ブラウザを起動し、実際に以下のURLにアクセスしてみましょう。

Chromeブラウザの場合はデベロッパーツール > Security > View certificateからSSL/TLSサーバ証明書を確認することができます。

「発行元: Let’s Encrypt Authority X3」となっているはずです。

続きを読む

AWS SDK for JavaScript with Angular で Upload to S3.

欠員が出たということで、穴埋めさせていただきます。

概要

本記事は、AngularAWS SDK for JavaScriptを利用して、S3にファイルをアップロードするという内容です。
Angular メインですので、AWSサービスの使い方や設定については割愛いたします。ご了承ください。

環境

$ uname -a
Linux ip-10-4-0-188 4.9.62-21.56.amzn1.x86_64 #1 SMP Thu Nov 16 05:37:08 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ ng -v

    _                      _                 ____ _     ___
   /    _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
  / △  | '_  / _` | | | | |/ _` | '__|   | |   | |    | |
 / ___ | | | | (_| | |_| | | (_| | |      | |___| |___ | |
/_/   __| |_|__, |__,_|_|__,_|_|       ____|_____|___|
               |___/

Angular CLI: 1.5.5
Node: 8.9.1
OS: linux x64
Angular: 5.0.0
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

@angular/cli: 1.5.5
@angular-devkit/build-optimizer: 0.0.35
@angular-devkit/core: 0.0.22
@angular-devkit/schematics: 0.0.41
@ngtools/json-schema: 1.1.0
@ngtools/webpack: 1.8.5
@schematics/angular: 0.1.10
@schematics/schematics: 0.0.10
typescript: 2.4.2
webpack: 3.8.1
package.json
{
  "name": "qiita",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^5.0.0",
    "@angular/common": "^5.0.0",
    "@angular/compiler": "^5.0.0",
    "@angular/core": "^5.0.0",
    "@angular/forms": "^5.0.0",
    "@angular/http": "^5.0.0",
    "@angular/platform-browser": "^5.0.0",
    "@angular/platform-browser-dynamic": "^5.0.0",
    "@angular/router": "^5.0.0",
    "core-js": "^2.4.1",
    "rxjs": "^5.5.2",
    "zone.js": "^0.8.14"
  },
  "devDependencies": {
    "@angular/cli": "1.5.5",
    "@angular/compiler-cli": "^5.0.0",
    "@angular/language-service": "^5.0.0",
    "@types/jasmine": "~2.5.53",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "~6.0.60",
    "codelyzer": "^4.0.1",
    "jasmine-core": "~2.6.2",
    "jasmine-spec-reporter": "~4.1.0",
    "karma": "~1.7.0",
    "karma-chrome-launcher": "~2.1.1",
    "karma-cli": "~1.0.1",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~3.2.0",
    "tslint": "~5.7.0",
    "typescript": "~2.4.2"
  }
}

手順

1) AWS SDK for JavaScriptをインストール

$ npm i --save-prod aws-sdk

2) src/app/tsconfig.app.json を編集

tsconfig.app.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "es2015",
-    "types": []
+    "types": ["node"]
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ]
}

参照: Usage_with_TypeScript

3) S3アップロード用コンポネントを作成

$ ng g component s3-upload
  create src/app/s3-upload/s3-upload.component.css (0 bytes)
  create src/app/s3-upload/s3-upload.component.html (28 bytes)
  create src/app/s3-upload/s3-upload.component.spec.ts (643 bytes)
  create src/app/s3-upload/s3-upload.component.ts (280 bytes)
  update src/app/app.module.ts (408 bytes)

4) 各種ファイルを編集

src/app/app.component.html

+ <app-s3-upload></app-s3-upload>

src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import { S3UploadComponent } from './s3-upload/s3-upload.component';
import { S3Service } from './s3-upload/s3.service';

@NgModule({
  declarations: [
    AppComponent,
    S3UploadComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
  ],
  providers: [
    S3Service,
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

src/app/s3-upload/s3-upload.html

<div class="form">
  <div class="form-group">
    <label class="col-xs-2">select file</label>
    <div class="col-xs-10">
      <input type="file" (change)="upload($event)">
    </div>
  </div>
</div>
<div class="debug_print">
  <p>httpUploadProgress {{ httpUploadProgress | json }}</p>
</div>

src/app/s3-upload/s3-upload.component.ts

import { Component, OnInit } from '@angular/core';
import { S3Service } from './s3.service';
import * as AWS from 'aws-sdk';

@Component({
  selector: 'app-s3-upload',
  templateUrl: './s3-upload.component.html',
  styleUrls: ['./s3-upload.component.css']
})
export class S3UploadComponent implements OnInit
{
  public httpUploadProgress: {[name: string]: any} = {
    ratio : 0,
    style : {
      width: '0',
    }
  };


  /**
   * @desc constructor
   */
  constructor(private s3Service: S3Service)
  {
    this.s3Service.initialize()
      .subscribe((res: boolean) => {
        if (! res) {
          console.log('S3 cognito init error');
        }
      })
  }


  /**
   * @desc Angular LifeCycle
   */
  ngOnInit()
  {
    this.s3Service.progress
      .subscribe((res: AWS.S3.ManagedUpload.Progress) => {
        this.httpUploadProgress.ratio = res.loaded * 100 / res.total;
        this.httpUploadProgress.style.width = this.httpUploadProgress.ratio + '%';
      });
  }


  /**
   * @desc file upload
   */
  public upload(event: Event): void
  {
    this.httpUploadProgress.ratio = 0;
    this.httpUploadProgress.style.width = '0';
    this.s3Service.onManagedUpload((<HTMLInputElement>event.target).files[0]);
  }
}

src/app/s3-upload/s3.service.ts

import { Injectable, EventEmitter } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import * as AWS from 'aws-sdk';
import { AWS_ENV } from '../../environments/environment';

@Injectable()
export class S3Service
{
  private s3: AWS.S3;
  public progress: EventEmitter<AWS.S3.ManagedUpload.Progress> = new EventEmitter<AWS.S3.ManagedUpload.Progress>();


  constructor(private http: HttpClient) { }


  /**
   * @desc set CognitoIdentityId
   * 
   * @return string IdentityId: ex) ap-northeast-1:01234567-9abc-df01-2345-6789abcd
   */
  public initialize(): Observable<boolean>
  {
    return this.http.get('/assets/cognito.php')
      .map((res: any) => {
        // resに対する例外処理を追加する
        // ...

        // Amazon Cognito 認証情報プロバイダーを初期化します
        AWS.config.region = AWS_ENV.region;
        AWS.config.credentials = new AWS.CognitoIdentityCredentials({
          IdentityId: res.results[0].IdentityId,
        });
        this.s3 = new AWS.S3({
          apiVersion: AWS_ENV.s3.apiVersion,
          params: {Bucket: AWS_ENV.s3.Bucket},
        });
        return true;
      })

      .catch((err: HttpErrorResponse) => {
        console.error(err);
        return Observable.of(false);
      });
  }

  /**
   * @desc AWS.S3
   */
  public onManagedUpload(file: File): Promise<AWS.S3.ManagedUpload.SendData>
  {
    let params: AWS.S3.Types.PutObjectRequest = {
      Bucket: AWS_ENV.s3.Bucket,
      Key: file.name,
      Body: file,
    };
    let options: AWS.S3.ManagedUpload.ManagedUploadOptions = {
      params: params,
      partSize: 64*1024*1024,
    };
    let handler: AWS.S3.ManagedUpload = new AWS.S3.ManagedUpload(options);
    handler.on('httpUploadProgress', this._httpUploadProgress.bind(this));
    handler.send(this._send.bind(this));
    return handler.promise();
  }

  /**
   * @desc progress
   */
  private _httpUploadProgress(progress: AWS.S3.ManagedUpload.Progress): void
  {
    this.progress.emit(progress);
  }

  /**
   * @desc send
   */
  private _send(err: AWS.AWSError, data: AWS.S3.ManagedUpload.SendData): void
  {
    console.log('send()', err, data);
  }
}

src/environments/environment.ts

// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.

export const environment = {
  production: false
};

export const AWS_ENV = {
  region: 'YOUR_REGION',
  s3: {
    apiVersion: 'YOUR_VERSION',
    Bucket: 'YOUR_BACKET',
  },
};

5) ビルド&実行&確認

test.png

$ aws s3 ls s3://YOUR_BACKET/
2017-12-10 08:13:54   10485760 10MB.bin

解説

AWS SDKを使ってファイルをアップロードするには、PutObject()を使うのが手っ取り早いですが
JSからファイルをアップロードするときには、UI/UXの観点からプログレスを表示してあげるのがよいので
それに対応したメソッドである、ManagedUpload()を利用しました。

5.0.0 では、zoneを意識することなく、プログレスがきちんとレンダリングされましたので
プログレスバーの実装は容易に行なえます。

以上、穴埋め記事でした。

7日目は、@segawm さんです。

続きを読む