OpenFaaSを使ってGo言語でFunctionを書いて、AWSに展開したDocker環境にデプロイするまで

OpenFaaSは聞いたことがあるでしょうか。
まだ生まれて半年ほどしか経っていませんが、GithubStar数は6000を超える勢いで成長している有望なフレームワークです。

2017年10月18日に行われたServerless Meetup Osaka #4でOpenFaaSのことを聞いたので早速試してみました。

どこまでやるかというと、
OpenFaaSのAuthorであるAlex Ellisのブログで紹介されている、OpenFaaSでGo言語の関数を動かして、さらにクラウドに展開するところまでやってみたいと思います。

今回やってみて、日本語の情報がほぼ皆無で、英語でも情報がかなり少なかったので、丁寧に手順を載せておきます。

OpenFaaSいいところ

まとめとして先に良かったことを書いておきます。

  • Dockerイメージになるなら何でも動かせる
  • とはいえ、メジャーな言語のテンプレートを用意している
  • 手軽さは既存のFunction as a Serviceと変わらない

スケール・コストなどの観点はチュートリアルでは評価しきれないので、言及しません。

Faas CLIのインストール

Docker CE 17.05以上がインストールされていることが必要です。

コマンドでさくっとインストールできます。

curl -sSL https://cli.openfaas.com | sudo sh

brewコマンドが使えるなら、

brew install faas-cli

Selection_002.png

OpenFaaSデプロイ環境をローカルに追加

Kubernetesに比べると簡単に扱えると感じたDocker Swarmにデプロイします。

まずはDocker Swarm自体の初期化して、managerとworkerを動作させます。

docker swarm init

以下のコマンドでFaaSスタックをデプロイします。

git clone https://github.com/openfaas/faas && \
  cd faas && \
  git checkout 0.6.5 && \
  ./deploy_stack.sh

このデプロイした環境はどこにいるかというと、Docker Service (Swarm Mode) として動いているので、docker service lsのコマンドで確認できます。

image.png

ローカルのデプロイ環境にはhttp://localhost:8080でアクセスできるので開くと、次のようなFunction管理ポータルが表示されます。
Linuxの場合はhttp://127.0.0.1:8080で開く必要があるかもしれません。

Selection_006.png

Go言語のインストール

こちらの公式インストールガイドを参考にインストールしてください。

v1.8.3かそれ以降が必要です。
gvmでインストールしても問題ありません。

Selection_003.png

$GOPATHが設定されているか確認しておきます。
Selection_004.png

OpenFaaSプロジェクトの生成

まずはプロジェクトフォルダーを作成します。

mkdir -p $GOPATH/src/functions
cd $GOPATH/src/functions

続いてFaaS CLIを使ってプロジェクトテンプレートを生成します。
名前は参考記事通りgohashです。

faas-cli new --lang go gohash

Selection_005.png

プロジェクトの中身を確認する

gohash.ymlファイルにはテンプレートで作成されたFunctionとローカルの実行環境についての設定が書き込まれています。

gohash.yml
provider:
  name: faas
  gateway: http://localhost:8080

functions:
  gohash:
    lang: go
    handler: ./gohash
    image: gohash

gohash/handler.goにはHello Worldなコードが書かれています。

gohash/handler.go
package function

import (
    "fmt"
)

// Handle a serverless request
func Handle(req []byte) string {
    return fmt.Sprintf("Hello, Go. You said: %s", string(req))
}

そしてtemplate内には、各言語のテンプレートもありますが、Go言語のものがちゃんとあります。
main.goではOpenFaaSの仕様通り、 STDIN を受け取って、対応するFunctionを呼び出した結果を STDOUT に送るというシンプルな作りとなっています。

template/go/main.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"

    "handler/function"
)

func main() {
    input, err := ioutil.ReadAll(os.Stdin)
    if err != nil {
        log.Fatalf("Unable to read standard input: %s", err.Error())
    }

    fmt.Println(function.Handle(input))
}

Dockerfileが各言語の環境ごとに用意されていて、ミニマルな実行環境を作成し、開発者が用意したFunctionコードをコンテナ内に格納してから最後にWatchdogを起動するという動きになっているようです。

WatchdogはGo言語で書かれた小さなHttpサーバーです。

template/go/Dockerfile
FROM golang:1.8.3-alpine3.6
# ---------- 略 -----------
WORKDIR /go/src/handler
COPY . .
# ---------- 略 -----------
CMD ["./fwatchdog"]

まずはデプロイしてみる

テンプレートを導入した時点で、環境構築が無事に完了しているか確認するために、HelloWorldのままデプロイしてみます。

faas-cli build -f gohash.yml
faas-cli deploy -f gohash.yml

BuildはDockerのイメージをダウンロードするところから始まるので、最初の実行は1分ほど待つかもしれません。

とくにエラーが表示されずに、
Selection_009.png
という風な表示がされたら成功です。

http://localhost:8080にアクセスしてgohashが追加されているか確認します。
左のFunction一覧からgohashを見つけたら適当なRequest bodyを打ち込んで INVOKE します。

Selection_007.png

これで無事にデプロイできることが確認できました。

Functionを実装していく

今回はgo1.9を使ったので、コンテナ内のgoも同じバージョンにしておきます。
今回のコードでは変更しなくても特に問題にはならないと思います。
コンテナ生成Dockerfileを書き換えてしまいます。

template/go/Dockerfile変更前
FROM golang:1.8.3-alpine3.6
template/go/Dockerfile変更後
FROM golang:1.9-alpine3.6

go言語のパッケージ管理ツールとしてdepをインストールします。

go get -u github.com/golang/dep/cmd/dep

そして、Goのデータ(Struct)からハッシュを生成するstructhashdepを使ってインストールします。
ただし、faas-cliがDockerコンテナをビルドするときに、template/go/をワーキングディレクトリとしてビルドを行うため、depの実行はこのディレクトリに移動して行う必要があります。

cd template/go/
dep init
dep ensure -add github.com/cnf/structhash

こうすると、Gopkg.lockGopkg.tomlvernder/template/go/以下に生成されます。
ちなみに、gvm環境だとdep ensureでライブラリがうまくインストールされないことがありますが、goのコードがビルドされるのはコンテナ内なので、一応そのまま進めても大丈夫です。

きちんと開発を進める場合はgvm linkthisdepがしっかりと使えるようにします。

それでは、受け取った文字列データからハッシュを生成するコードに変更しましょう。

gohash/handler.go
package function

import (
    "fmt"

    "github.com/cnf/structhash"
)

// S is sample struct
type S struct {
    Str string
    Num int
}

// Handle a serverless request
func Handle(req []byte) string {
    s := S{string(req), len(req)}

    hash, err := structhash.Hash(s, 1)
    if err != nil {
        panic(err)
    }
    return fmt.Sprintf("%s", hash)
}

プロジェクトのデプロイ

では、作成したコードをデプロイしてみましょう。

faas-cli build -f gohash.yml
faas-cli deploy -f gohash.yml

ポータルの実行結果はFunctionのデプロイで新しいものに置き換えたので結果が変わっています。

Selection_010.png

また、もちろんAPI URLも用意されているので、直接呼び出すこともできます。

curl -X POST -d "てすとめっせーじ" http://localhost:8080/function/gohash

Selection_011.png

テストを追加する

テンプレートのDockerfileにはgo testでテストを実施しているのですが、今のところテスト用のgoファイルは生成されないようです。

今は自分で作りましょう。

touch gohash/handler_test.go
gohash/handler_test.go
package function

import "testing"

func TestHandleReturnsCorrectResponse(t *testing.T) {  
    expected := "v1_b8dfcbb21f81a35afde754b30e3228cf"
    resp := Handle([]byte("Hello World"))

    if resp != expected {
        t.Fatalf("Expected: %v, Got: %v", expected, resp)
    }
}

テストを作ったところで、わざとgohash Functionを間違えて修正してしまいましょう。

handler.go修正前
hash, err := structhash.Hash(s, 1)
handler.go修正ミス
hash, err := structhash.Hash(s, 2)

この状態で再びビルドを実行します。

faas-cli build -f gohash.yml

ちゃんとビルド中にテストで失敗してくれます。
Selection_012.png

コードを元に戻してビルドが成功することを確認します。

DockerHubにビルドしたイメージをPUSH

リモート環境でOpenFaaSを動作させるためには、FunctionのDockerイメージをDockerHubまたは別のレジストリに登録しておく必要があります。

まずは、[DockerHubのユーザ名]/gohashとなるように、gohash.ymlを書き換えます。

gohash.yml書換前
    image: gohash
gohash.yml書換後
    image: gcoka/gohash

Docker Hubの登録が済んでいれば、

docker login

でログインし、

faas-cli push -f gohash.yml

でビルドしたイメージをPUSHします。

AWSにデプロイしてみる

AWS EC2コンソールで SSH KeyPairsを作成しておいてください。
ssh-addもお忘れなく。

ssh-add ~/.ssh/yourkey.pem

AWSにDockerをデプロイするためのテンプレートが用意されているので、ここにアクセスして、
Deploy Docker Community Edition (CE) for AWS (stable)をクリックします。
use your existing VPCを選択すると、Docker用ネットワークの設定をいろいろやらないといけなくなり、VPC内のネットワーク構築の知識が必要となるようです。

https://docs.docker.com/docker-for-aws/#docker-community-edition-ce-for-aws

以下の設定は環境に合わせて変更が必要です。

  • SSHキーにKeyPairsで作成したものを指定。

また、このテンプレートでのCloudFormation実行には、以下のCreateRoleが必要です。
正確な一覧はこちら

  • EC2 instances + Auto Scaling groups
  • IAM profiles
  • DynamoDB Tables
  • SQS Queue
  • VPC + subnets and security groups
  • ELB
  • CloudWatch Log Group

特に設定は変更していません。
デプロイを試すだけなので、Swarm ManagerとWorkerの数は1ずつにしました。

Selection_029.png

CloudFormationのStackデプロイが完了したら、デプロイ結果のOutputsタブから、Swarm Managerのインスタンスへのリンクが参照できるので、開きます。
Selection_030.png

Selection_031.png

このインスタンスにSSH接続を行います。
ユーザーはdockerを指定します。

ssh docker@54.159.253.49

OpenFaaSのスタックを導入するために、Gitが必要なので、インストールします。

中身はAlpine Linuxなので、apkコマンドでパッケージをインストールします。

sshコンソール
sudo apk --update add git

ローカルでにインストールしたときと同じコマンドですが再掲。

sshコンソール
git clone https://github.com/openfaas/faas && \
  cd faas && \
  git checkout 0.6.5 && \
  ./deploy_stack.sh

ではOpenFaaSスタックが無事デプロイされているか確認します。

AWSに構築したテンプレートはインターネットからはLoadBalancerを通してアクセスできるので、LoadBalancerのURLを調べます。DNS name: に表示されているものがそれです。

Selection_034.png

URLがわかったら、ポート8080を指定してアクセスします。

Selection_035.png

うまくいっていることを確認したらAWSにデプロイします。
--gatewayオプションを使えばgohash.ymlファイルを書き換える必要がありません。

faas-cli build -f gohash.yml --gateway http://your.amazon.aws.loadbalancer.url:8080

Selection_038.png

Selection_039.png

無事にAWSで動きました。

トラブルシューティング(詰まったところ)

faas-cli buildしてもコード変更が反映されない

--no-cacheをつけることで、すべてのビルドをやり直してくれます。

faas-cli build -f gohash.yml --no-cache

structhashパッケージがvenderディレクトリにコピーされない

$GOPATHが正しく設定されているか確認してください。

gvmを使ってgoをインストールしている場合は、$GOPATH/src/functionsgvm linkthisを実行すると解決するかもしれません。

AWS CloudFormationのデプロイに失敗する

use your existing VPCのテンプレートを使っている場合は、使わないでVPCの作成もDockerテンプレートに任せてください。

AWSにデプロイしたFunctionを実行しても500: Internal Server Errorになる

Docker HubにDockerイメージをPUSHしたか確認してください。

ちゃんと動いているかわからない・・・

docker serviceとして動いているのでdockerコマンドからいくつか情報を得ることができます。

指定したFunctionのプロセス動作情報を表示
docker service ps gohash
指定したFunctionのログを表示
docker service logs gohash
gatewayのログを表示
docker service logs func_gateway

最後に

Docker SwarmはMicrosoft Azure Container Serviceもサポートしているのですが、完全なマネージドの場合OpenFaaSが必要とするDockerバージョンが足りていないようです。

今回はDocker Swarmに対してデプロイしましたが、Kubernetesもサポートしているので、そちらも試してみたいと思います。
Kubernetesの場合は、Swarmよりもホスティング対応しているクラウドがいくつかあるようなので、期待が持てますね。

情報がとても少ないので、どんどん試してどんどん情報公開していきましょう!

続きを読む

AWS Elasticsearch ServiceがVPC対応したので、lambda→ESをVPCで括ってみた

VPC Support for Amazon Elasticsearch Service Domains – Amazon Elasticsearch Service http://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/es-vpc.html

FireShot_Capture_065_-_Amazon_Elasticsearch__-_https___ap-northeast-1_console_aws_amazon_com_es_home.png

やること

  1. VPCの設定をする
  2. Elasticsearch Serviceを新規作成してVPC設定する
    (既存のドメインをVPC対応にする・VPCを切り替える方法はまだわからない)
  3. lambdaにVPCを設定する
  4. テスト

VPC設定

  1. プライペートとパブリックの2パターンサブネットを作成する
  2. パブリックサブネット内でNATゲートウェイを作成する
  3. NAT,インターネットゲートウェイをルートテーブルに割り当てる
  4. ESに設定するセキュリティグループを設定する

ES設定

  1. 新しいドメインの作成
  2. ネットワーク設定でVPC, サブネット(プライペート), セキュリティグループを設定する
  3. アクセスポリシーをVPC用に設定
    FireShot_Capture_066_-_Amazon_Elasticsearch__-_https___ap-northeast-1_console_aws_amazon_com_es_home.png

lambda設定

  1. ロールにVPCアクセス権限をアタッチする
  2. lambdaをプライペートサブネットに配置する

API Gateway設定

(疎通確認だけなら、lambda関数に繋がればよいかと)

疎通確認

ESはVPC内なので、外のネットワークからはアクセスできない
そのため、Kibana見れない状態…
VPC内にWindows立ち上げるとかEC2入れてcurlで叩いてみるとか

lambda内のログに出力するなりで確認

はまりどころ

lambdaにパプリックネットワークのサブネットを付けてしまっていた為、外に接続できなかった
原因をAPI Gatewayかと思い、メソッドを設定したものの解決せず時間がかかってしまった

もしかして

ESへのアクセスを制限したいだけであれば、lambdaをVPC内に設置してNAT Gateway経由でESにアクセスすればIP許可だけで十分だったのでは…?

続きを読む

shell scriptでEC2インスタンス内から自分についているタグを取得する

ID=`curl -s http://169.254.169.254/latest/meta-data/instance-id`
TAG_VALUE=`aws ec2 describe-tags --filter "{"Name":"resource-id","Values": ["$ID"]}" --query 'Tags[?Key==`TAG_NAME`][Value]' --region ap-northeast-1 --output text`

autoscalinggroupのtagを付ける設定で起動直後は取れないことがあるのでwhileとかで回そう

続きを読む

DynamoDBの予約語一覧を無理やり動的に取得してみた

DynamoDBの予約語を対処するために一覧が欲しくなったのですが、いちいちプログラム内で定義したくなかったので、AWSの予約語一覧ページから一覧を取得するサービスを作りました。

前提

  1. AWSが公開しているDynamoDB予約語一覧のWebページ( http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html )をスクレイピングして予約語一覧を抽出します。
  2. 実装はAWS Lambda (Python3)、Webサービスとして動作させるためにAPI Gatewayを利用します。

結果

https://github.com/kojiisd/dynamodb-reserved-words

DynamoDB予約語一覧ページの確認

今回は以下のページをParseしたいと思います。

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/ReservedWords.html

HTMLを見てみると、Parseしたい箇所はcodeタグで囲まれているようで、しかもこのページ内でcodeタグが出現するのは1度だけのようです。

スクリーンショット 2017-10-15 18.03.18.png

これであればすぐにパースできそうです。

HTMLの取得とParse

BeautifulSoupを利用すれば、簡単に実装ができます。BeautifulSoupのインストールは他のページでいくらでも紹介されているので、ここでは省略します。

import sys
import os


sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))
from bs4 import BeautifulSoup
import requests

URL = os.environ['TARGET_URL']

def run(event, context):

    response = requests.get(URL)
    soup = BeautifulSoup(response.content, "html.parser")

    result_tmp = soup.find("code")
    if result_tmp == None:
        return "No parsing target"

    result = result_tmp.string

    return result.split('n');

とりあえず申し訳程度にパースできない場合(AWSのページがcodeタグを使わなくなった時)を想定してハンドリングしています。

API Gatewayを定義してサービス化

コードが書けてしまえばAPI Gatewayを定義してサービス化するだけです。

設定は非常に単純なので割愛で。

スクリーンショット 2017-10-15 18.12.11_deco.png

アクセスしてみる

実際にアクセスしてみます。

$ curl -X GET https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod
["", "ABORT", "ABSOLUTE", "ACTION", "ADD", "AFTER", "AGENT", "AGGREGATE", "ALL", "ALLOCATE", "ALTER", "ANALYZE", "AND", "ANY", "ARCHIVE", "ARE", "ARRAY", "AS", "ASC", "ASCII", "ASENSITIVE", "ASSERTION", "ASYMMETRIC", "AT", "ATOMIC", "ATTACH", "ATTRIBUTE", "AUTH", "AUTHORIZATION", "AUTHORIZE", "AUTO", "AVG", "BACK", "BACKUP", "BASE", "BATCH", "BEFORE", "BEGIN", "BETWEEN", "BIGINT", "BINARY", "BIT", "BLOB", "BLOCK", "BOOLEAN", "BOTH", "BREADTH", "BUCKET", "BULK", "BY", "BYTE", "CALL", "CALLED", "CALLING", "CAPACITY", "CASCADE", "CASCADED", "CASE", "CAST", "CATALOG", "CHAR", "CHARACTER", "CHECK", "CLASS", "CLOB", "CLOSE", "CLUSTER", "CLUSTERED", "CLUSTERING", "CLUSTERS", "COALESCE", "COLLATE", "COLLATION", "COLLECTION", "COLUMN", "COLUMNS", "COMBINE", "COMMENT", "COMMIT", "COMPACT", "COMPILE", "COMPRESS", "CONDITION", "CONFLICT", "CONNECT", "CONNECTION", "CONSISTENCY", "CONSISTENT", "CONSTRAINT", "CONSTRAINTS", "CONSTRUCTOR", "CONSUMED", "CONTINUE", "CONVERT", "COPY", "CORRESPONDING", "COUNT", "COUNTER", "CREATE", "CROSS", "CUBE", "CURRENT", "CURSOR", "CYCLE", "DATA", "DATABASE", "DATE", "DATETIME", "DAY", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DEFINE", "DEFINED", "DEFINITION", "DELETE", "DELIMITED", "DEPTH", "DEREF", "DESC", "DESCRIBE", "DESCRIPTOR", "DETACH", "DETERMINISTIC", "DIAGNOSTICS", "DIRECTORIES", "DISABLE", "DISCONNECT", "DISTINCT", "DISTRIBUTE", "DO", "DOMAIN", "DOUBLE", "DROP", "DUMP", "DURATION", "DYNAMIC", "EACH", "ELEMENT", "ELSE", "ELSEIF", "EMPTY", "ENABLE", "END", "EQUAL", "EQUALS", "ERROR", "ESCAPE", "ESCAPED", "EVAL", "EVALUATE", "EXCEEDED", "EXCEPT", "EXCEPTION", "EXCEPTIONS", "EXCLUSIVE", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXPLAIN", "EXPLODE", "EXPORT", "EXPRESSION", "EXTENDED", "EXTERNAL", "EXTRACT", "FAIL", "FALSE", "FAMILY", "FETCH", "FIELDS", "FILE", "FILTER", "FILTERING", "FINAL", "FINISH", "FIRST", "FIXED", "FLATTERN", "FLOAT", "FOR", "FORCE", "FOREIGN", "FORMAT", "FORWARD", "FOUND", "FREE", "FROM", "FULL", "FUNCTION", "FUNCTIONS", "GENERAL", "GENERATE", "GET", "GLOB", "GLOBAL", "GO", "GOTO", "GRANT", "GREATER", "GROUP", "GROUPING", "HANDLER", "HASH", "HAVE", "HAVING", "HEAP", "HIDDEN", "HOLD", "HOUR", "IDENTIFIED", "IDENTITY", "IF", "IGNORE", "IMMEDIATE", "IMPORT", "IN", "INCLUDING", "INCLUSIVE", "INCREMENT", "INCREMENTAL", "INDEX", "INDEXED", "INDEXES", "INDICATOR", "INFINITE", "INITIALLY", "INLINE", "INNER", "INNTER", "INOUT", "INPUT", "INSENSITIVE", "INSERT", "INSTEAD", "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO", "INVALIDATE", "IS", "ISOLATION", "ITEM", "ITEMS", "ITERATE", "JOIN", "KEY", "KEYS", "LAG", "LANGUAGE", "LARGE", "LAST", "LATERAL", "LEAD", "LEADING", "LEAVE", "LEFT", "LENGTH", "LESS", "LEVEL", "LIKE", "LIMIT", "LIMITED", "LINES", "LIST", "LOAD", "LOCAL", "LOCALTIME", "LOCALTIMESTAMP", "LOCATION", "LOCATOR", "LOCK", "LOCKS", "LOG", "LOGED", "LONG", "LOOP", "LOWER", "MAP", "MATCH", "MATERIALIZED", "MAX", "MAXLEN", "MEMBER", "MERGE", "METHOD", "METRICS", "MIN", "MINUS", "MINUTE", "MISSING", "MOD", "MODE", "MODIFIES", "MODIFY", "MODULE", "MONTH", "MULTI", "MULTISET", "NAME", "NAMES", "NATIONAL", "NATURAL", "NCHAR", "NCLOB", "NEW", "NEXT", "NO", "NONE", "NOT", "NULL", "NULLIF", "NUMBER", "NUMERIC", "OBJECT", "OF", "OFFLINE", "OFFSET", "OLD", "ON", "ONLINE", "ONLY", "OPAQUE", "OPEN", "OPERATOR", "OPTION", "OR", "ORDER", "ORDINALITY", "OTHER", "OTHERS", "OUT", "OUTER", "OUTPUT", "OVER", "OVERLAPS", "OVERRIDE", "OWNER", "PAD", "PARALLEL", "PARAMETER", "PARAMETERS", "PARTIAL", "PARTITION", "PARTITIONED", "PARTITIONS", "PATH", "PERCENT", "PERCENTILE", "PERMISSION", "PERMISSIONS", "PIPE", "PIPELINED", "PLAN", "POOL", "POSITION", "PRECISION", "PREPARE", "PRESERVE", "PRIMARY", "PRIOR", "PRIVATE", "PRIVILEGES", "PROCEDURE", "PROCESSED", "PROJECT", "PROJECTION", "PROPERTY", "PROVISIONING", "PUBLIC", "PUT", "QUERY", "QUIT", "QUORUM", "RAISE", "RANDOM", "RANGE", "RANK", "RAW", "READ", "READS", "REAL", "REBUILD", "RECORD", "RECURSIVE", "REDUCE", "REF", "REFERENCE", "REFERENCES", "REFERENCING", "REGEXP", "REGION", "REINDEX", "RELATIVE", "RELEASE", "REMAINDER", "RENAME", "REPEAT", "REPLACE", "REQUEST", "RESET", "RESIGNAL", "RESOURCE", "RESPONSE", "RESTORE", "RESTRICT", "RESULT", "RETURN", "RETURNING", "RETURNS", "REVERSE", "REVOKE", "RIGHT", "ROLE", "ROLES", "ROLLBACK", "ROLLUP", "ROUTINE", "ROW", "ROWS", "RULE", "RULES", "SAMPLE", "SATISFIES", "SAVE", "SAVEPOINT", "SCAN", "SCHEMA", "SCOPE", "SCROLL", "SEARCH", "SECOND", "SECTION", "SEGMENT", "SEGMENTS", "SELECT", "SELF", "SEMI", "SENSITIVE", "SEPARATE", "SEQUENCE", "SERIALIZABLE", "SESSION", "SET", "SETS", "SHARD", "SHARE", "SHARED", "SHORT", "SHOW", "SIGNAL", "SIMILAR", "SIZE", "SKEWED", "SMALLINT", "SNAPSHOT", "SOME", "SOURCE", "SPACE", "SPACES", "SPARSE", "SPECIFIC", "SPECIFICTYPE", "SPLIT", "SQL", "SQLCODE", "SQLERROR", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING", "START", "STATE", "STATIC", "STATUS", "STORAGE", "STORE", "STORED", "STREAM", "STRING", "STRUCT", "STYLE", "SUB", "SUBMULTISET", "SUBPARTITION", "SUBSTRING", "SUBTYPE", "SUM", "SUPER", "SYMMETRIC", "SYNONYM", "SYSTEM", "TABLE", "TABLESAMPLE", "TEMP", "TEMPORARY", "TERMINATED", "TEXT", "THAN", "THEN", "THROUGHPUT", "TIME", "TIMESTAMP", "TIMEZONE", "TINYINT", "TO", "TOKEN", "TOTAL", "TOUCH", "TRAILING", "TRANSACTION", "TRANSFORM", "TRANSLATE", "TRANSLATION", "TREAT", "TRIGGER", "TRIM", "TRUE", "TRUNCATE", "TTL", "TUPLE", "TYPE", "UNDER", "UNDO", "UNION", "UNIQUE", "UNIT", "UNKNOWN", "UNLOGGED", "UNNEST", "UNPROCESSED", "UNSIGNED", "UNTIL", "UPDATE", "UPPER", "URL", "USAGE", "USE", "USER", "USERS", "USING", "UUID", "VACUUM", "VALUE", "VALUED", "VALUES", "VARCHAR", "VARIABLE", "VARIANCE", "VARINT", "VARYING", "VIEW", "VIEWS", "VIRTUAL", "VOID", "WAIT", "WHEN", "WHENEVER", "WHERE", "WHILE", "WINDOW", "WITH", "WITHIN", "WITHOUT", "WORK", "WRAPPED", "WRITE", "YEAR", "ZONE "]

うまくできました。

まとめ

思いつきで作成してみましたが、AWSのページが閉鎖されたりHTML構造が変更されない限り、仕様変更などは気にせずに使えそうです。

あとBeautifulSoup初めて使いましたが、かなり便利。

続きを読む

[JAWS-UG CLI] Alexa Skills Kit Command Line Interface (ASK CLI)のセットアップ

Alexa Skills Kit Command Line Interface (ASK CLI)をセットアップします。

オリジナル手順:https://developer.amazon.com/ja/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html#step-2-install-and-initialize-ask-cli

前提条件

1. nodeのインストール (nodebrewを使う場合)

コマンド
curl -L git.io/nodebrew | perl - setup
結果
Fetching nodebrew...
Installed nodebrew in $HOME/.nodebrew

========================================
Export a path to nodebrew:

export PATH=$HOME/.nodebrew/current/bin:$PATH
========================================

~/.bashrcにPATHを追記します。

.bashrcに追記
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bashrc
コマンド
. ~/.bashrc 
コマンド
which nodebrew
結果(例)
/Users/taro/.nodebrew/current/bin/nodebrew

nodeをバイナリでインストールします。(v6.10.0の例)

コマンド
nodebrew install-binary v6.10.0
コマンド
nodebrew use v6.10.0
node -v
結果(例)
v6.10.0

自動的にnpmもインストールされているはずです。

コマンド
which npm
結果(例)
/Users/taro/.nodebrew/current/bin/npm
コマンド
npm -v
結果(例)
3.10.10

2. ASK CLIのインストール

コマンド
npm install -g ask-cli
コマンド
which ask
結果(例)
<HOMEディレクトリ>/.nodebrew/current/bin/ask

3. ASKの初期化

コマンド
ask init --no-browser
出力
There is no AWS credentials setup yet, do you want to continue the initialization? (Default: False)
入力
Y
出力
Paste the following url to your browser:
<URL>

? Please enter your Authorization Code:
  • をコピーしてブラウザのURL欄に貼り付けます。
  • Authorization Codeが表示されるので、黒い枠の中のテキストをコピーします。
  • 上記でターミナルに表示されている”Please enter your Authorization Code:”の後ろにペーストし、エンターキーを押します。
出力
Tokens fetched and recorded in ask-cli config.
Vendor ID set as XXXXXXXXXXXX

Profile [default] initialized successfully.

4. 事後確認

コマンド
ask init -l
結果(例)
Profile              Associated AWS Profile
  [default]                 ** NULL **

~/.ask/cli_configに認証情報が保存されています。

完了

続きを読む

logmonとALBのdrainingでリロード攻撃(F5アタック)へのパッシブ対策

以下の記事で紹介した小ネタの、具体的な利用例です。

1. 対策の内容

リロード攻撃(F5アタック)への対策としては、前段のApacheにmod_dosdetectorやmod_evasiveを入れる、WAFを導入する等があります。
このように、前段ですべて対応できれば良いのですが、

  • リロード攻撃を行っているのが正規ユーザである
  • リクエストによって(また、ユーザ毎に処理に必要なデータ量の多寡によって)レスポンスタイムにばらつきがある

というような場合、なかなか前段だけでの対処は難しいのではないかと思います。
そこで、Webアプリケーションサーバ(Tomcatなど)の処理が詰まってしまったときに、サービスの完全停止を避けるためにWebアプリケーションサーバの再起動を行うことがあると思いますが、

  • 「処理が詰まった」といっても、詰まり始めのうちは「特定の遅い処理」だけが「詰まる」のであり、それ以外のリクエストに対する処理は正常にレスポンスを返すことができている
  • 正常なレスポンスを返すことができるリクエストまで、再起動で中断するのは(なるべく)避けたい

ということで、Apacheのgraceful restartのような処理をしよう…というのが今回の内容です。

※きちんとgracefulな処理をするためには、Webアプリケーションサーバは複数台必要です。

2. ポリシー・IAM Roleの準備

まずは、先ほどの記事にある通り、以下の作業を行います。

  • ポリシーを設定する
  • 設定したポリシーをEC2用IAM Roleにアタッチする
  • そのIAM RoleをEC2(Webアプリケーションサーバ)にアタッチする

なお、今回のケースでは、「ec2:DescribeInstances」に対する権限は不要ですので、この部分はカットしても良いでしょう。

3. EC2上の設定

以下の記事を参考に、EC2(Webアプリケーションサーバ)にlogmonを導入します。

logmon.confには、以下の内容を設定します。

  • 1行目 : 監視対象のログファイル(Apacheのエラーログなら「:/var/log/httpd/error_log」など)
  • 2行目 : 監視対象のキーワード(正規表現/Tomcatのレスポンスが返らない場合を拾うのなら「[error] (70007)The timeout specified has expired」にマッチする内容)
  • 3行目 : 先の記事に示されているとおり

続いて、最初の記事で紹介したスクリプト「aws_utils.sh」を配置し(私の例では「/usr/local/sbin/」内)、「get_instance_id()」の部分だけ以下の内容に置き換えます。

aws_utils.sh(変更部分のみ)
#######################################
# 自身のインスタンス ID を取得する
# Returns:
#   INSTANCE ID
#######################################
get_my_instance_id() {
  instance_id=`/usr/bin/curl http://169.254.169.254/latest/meta-data/instance-id`
  if [ -z "$instance_id" ]; then
    echo 'host not found'
    exit 2
  fi
  echo ${instance_id}
}

※draining(登録解除の遅延)時間の長さに合わせて「SLEEP」の秒数も調整します。

それから、crontabから一定間隔で呼び出すスクリプト(私の例では「/usr/local/sbin/check_count.sh」)を配置します。

check_count.sh
#! /bin/sh

# スクリプトをインポートする
. /usr/local/sbin/aws_utils.sh

# トリガ判定
if [ `cat /tmp/logmon_count` -ge 20 ]; then

  # 閾値越え -> logmonサービス停止
  /sbin/service logmon stop

  # 二重トリガ起動防止(countを0に)
  echo 0 > /tmp/logmon_count

  # ALBでターゲットグループから外す
  ALB_TARGET_GROUP_ARN=('arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/YYYY/zzzzzzzzzzzzzzzz' 'arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/YYYY/zzzzzzzzzzzzzzzz')
  INSTANCE_ID=$(get_my_instance_id)

  for arn in ${ALB_TARGET_GROUP_ARN[@]}
  do
    alb_deregister ${arn} ${INSTANCE_ID}
  done

  # ALBでdraining完了待ち
  for arn in ${ALB_TARGET_GROUP_ARN[@]}
  do
    alb_waiter ${arn} ${INSTANCE_ID} 'unused' > /dev/null
  done

  # Webサービス停止
  /sbin/service tomcat8 stop

  /bin/sleep 10

  # サービス起動
  /sbin/service tomcat8 start

  /bin/sleep 10

  /sbin/service logmon start

  # ALBでターゲットグループに戻す
  for arn in ${ALB_TARGET_GROUP_ARN[@]}
  do
    alb_register ${arn} ${INSTANCE_ID}
  done

  # ターゲットグループに戻ったことを確認する
  for arn in ${ALB_TARGET_GROUP_ARN[@]}
  do
    alb_waiter ${arn} ${INSTANCE_ID} 'healthy' > /dev/null
  done

fi

# countを0に
echo 0 > /tmp/logmon_count

if文の「20」は閾値です。適切な値に調整してください。
この例ではWebアプリケーションサーバとしてTomcat8を使っていますが、適切なものに置き換えてください。Tomcatの場合は、draining前後でPrintClassHistogramの出力などもしておくと良いです。

また、この例ではEC2(Webアプリケーションサーバ)を複数のターゲットグループ(配列「ALB_TARGET_GROUP_ARN」)に登録しています。
それぞれのターゲットグループでdraining時間が違う場合は、時間が短いものを先に記述すると良いです(後述の通りログを記録する場合は特に)。
1つの場合は配列にせず、for文で回す必要もありません。

なお、この例ではログを /dev/null に捨てていますが、実際に使うときにはきちんとログファイルに記録しておいたほうが良いです(時刻などとあわせて)。

最後に、このスクリプトを、実行ユーザ(rootなど)のcrontabに登録します(私の例では1分間隔で実行⇒閾値は1分当たりのカウントに対して設定)。このとき、1行目に「SHELL=/bin/bash」を挿入しておきます。

crontab登録例
SHELL=/bin/bash

*/1 * * * * /bin/sh /usr/local/sbin/check_count.sh

設定できたら、カウントファイル(私の例では「/tmp/logmon_count」)に閾値以上の値を書き出して、正しくdraining→ターゲットから削除→Webアプリケーションサーバ再起動→ターゲットに登録が行われるか、確認します。

テスト
echo 20 > /tmp/logmon_count

4. 注意点

drainingすると再起動には時間が掛かるので、Webアプリケーションサーバは最低でも4台程度は必要です。

続きを読む

ALB・EC2小ネタ/AWS CLIでALB配下のTGにEC2インスタンスを登録/削除するためのポリシー(メモ)

kakakakakkuさんが以下のブログ記事で公開されているALBのターゲットグループにEC2インスタンスを登録/削除するシェルスクリプトを実行するために必要なポリシーです。

1. 指定するポリシー

以下のポリシーを登録し、スクリプトを動かすEC2インスタンスに付与するIAM Roleにアタッチします。

AlbRegistrationPolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:RegisterTargets"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances"
            ],
            "Resource": "*"
        }
    ]
}

特に何のひねりもないですが、AWSユーザーガイドの以下のページにある通り、

  • ALBの3つのAPIアクション
  • EC2の1つのAPIアクション(これはkakakakakkuさんの記事中「aws_utils.sh」の「get_instance_id()」で使用)

とも、リソースレベル権限の詳細な指定に対応していないので、Resourceには「*」を指定する必要があります(最初見落としていて、AWSパートナーさんに教えていただきました)。
本番(プロダクト)環境とステージング/開発環境を同一のアカウントで運用している場合は、指定するターゲットグループのARNを間違えないよう注意が必要です。

2. おまけ1

私の場合、登録/削除するEC2インスタンス自身でAWS CLIのスクリプトを実行しているので、前述の「get_instance_id()」は使わず、curlで http://169.254.169.254/latest/meta-data/instance-id をGETしてインスタンスIDを取得しています。
その場合は「ec2:DescribeInstances」の権限は不要です。

3. おまけ2

crontabから呼び出すスクリプトの場合、初期設定のままでは「.」や「source」で「aws_utils.sh」をインポートできないので、1つのスクリプトにまとめるか、以下の記事を参考にしてインポートできるようにします。

続きを読む

AWSでServerlessの環境をCIするための選択肢を調べたメモ

利用するサービスはAWSに限定するとした場合に開発・テスト・デプロイを継続するための選択肢をいくつか調べてみました。

開発したいものは、「API Gateway – Lambda – DynamoDB」で構成されるWebサービスとします。

正直なところ、対象像を少し絞らないと選択肢が多すぎて好みの問題になりそうなので・・・

注意

調査用のメモです。

実際に全ての選択肢で開発をしてみたわけではなく、入門記事やドキュメントを少しみた程度で判断しています。

そのため正確性に欠ける内容となっている可能性が高いことをご了承ください。

共通点

開発ツールで対応していない部分についてのデプロイについては、AWS CLIで対応していくことになるでしょう。

LocalStack

モックサービスの対応数にまず驚いた。

https://github.com/localstack/localstack

LocalStack はローカルでAWSのサービスのモックを動かしてしまうというツールです。
DockerコンテナでAWS各種サービスを一気に立ち上げることができるので、ローカル環境で開発を完結させることが出来ます。

これ自体にはAWSを管理する機能はなく、Lambdaをローカル環境で開発してテストするときに、ローカルで同時にAPI Gateway + DynamoDBを動かしたいという場合に必要となりそうです。
DynamoDB自身はAmazonからDynamoDB Localが提供されているので、どちらを使うかは検証が必要でしょう。

起動コマンドも簡単で、一発で全て立ち上げることが出来ます。

docker-compose up

macの場合は、$TMPDIRにシンボリックリンクがある場合、少しコマンドを変える必要があるようです。

TMPDIR=/private$TMPDIR docker-compose up

DynamoDB Local

ローカル開発用のDynamoDB。
ほとんどのLambda開発ツールがAPI Gatewayもサポートしていることから、DynamoDBを接続するローカル環境ではLocalStackよりはこちらを使用することになるのではないかと。公式提供でもあるし。

ダウンロードとインストールガイドはこちら

aws-sam-local

SAM は Serverless Application Modelのことで、aws-sam-localはAmazon公式がサポートしているローカル開発環境です。今はまだbetaですが、近いうちに良くなりそうです。

https://github.com/awslabs/aws-sam-local

AWS Blogで利用例が紹介されていて、DynamoDB Localを使う場合についても少し触れられています。
https://aws.amazon.com/jp/blogs/news/new-aws-sam-local-beta-build-and-test-serverless-applications-locally/

作成したLambda FunctionをローカルDockerコンテナで実行したり、現時点ではPythonとNode.jsはデバッグ出来るようです。(自分で試したところ、上手くいかなかった)

また、Lambda関数を単純に実行するだけではなく、ローカルのAPI Gatewayを通して実行を確認できます。

PostManで簡単にAPIの動作検証が行えたり、実際のHTTPアクセスの形でLambda関数の検証がローカルで行えます。
また、実行時間やメモリ消費量が表示されるため、AWSにデプロイする前に関数の効率化が出来ます。

Serverless Framework

現状で一番正解に近いと思います。

https://github.com/serverless/serverless

こちらのQitta記事こっちのQiita記事が参考になりそうです。
DynamoDB Localと連携するためのプラグインが存在し、serverless.ymlファイルにAWS上での構成を記載していき、そのままAWSにデプロイ出来るようです。

Apex

Go言語でLambdaが書ける!!!

純粋にLambdaの開発だけで見ればとても良さそうです。
ただし、ローカル実行はサポートしておらず、AWSリソースの操作もLambdaに限定されています。
その辺りは、Terraformで補う考えのようです。

公式

https://github.com/apex/apex

chalice

Python用のマイクロサービスフレームワークです。

https://github.com/aws/chalice

次の様なコードで、APIを定義できます。

chalice
@app.route('/resource/{value}', methods=['PUT'])
def put_test(value):
    return {"value": value}

デプロイでAPI Gateway, Lambdaに反映されるため、コードの見通しが良くなりそうです。
IAM Rolesなどは自動生成される様なので、とにかく簡単にコードを書いて、すぐデプロイということが出来そうです。

インストールからコード記述、デプロイまでの操作がHelloworldならこんなに簡単に出来るようです。

$ pip install chalice
$ chalice new-project helloworld && cd helloworld
$ cat app.py

from chalice import Chalice

app = Chalice(app_name="helloworld")

@app.route("/")
def index():
    return {"hello": "world"}

$ chalice deploy
...
https://endpoint/dev

$ curl https://endpoint/api
{"hello": "world"}

Zappa

こちらも、とにかく簡単にPythonで作成した関数をAWSにデプロイ出来ることがウリのようです。

https://github.com/Miserlou/Zappa

gifアニメーションで実演が付いています。(README.mdから引用しました)
README.md

Zappa Demo Gif

REST APIを作成する以外にも、AWS Eventsに対応するものや、Asynchronous Task Executionといって、別のLamdba関数を非同期に呼び出すことが出来る機能を持っているようです。
というか、chaliceに比べると非常に多彩。

また、ZappaはWSGI(Web Server Gateway Interface)をサポートしているので、他のアプリケーションサーバーにデプロイすることも可能です。

chalice vs Zappa

こちらに比較記事がありました。

続きを読む

【HashiCorp Vault】Dynamic Secrets (Secret Backend) の Lease 機能を試す

はじめに

Dynamic Secrets (Secret Backend) の Lease 機能について動作を確認しましたので備忘録として記録しておきます。

やったこと

  1. Vaultが作成したAWSユーザの有効期限(TTL)を確認する
  2. Lease機能を利用して有効期限(TTL)を設定する
  3. Lease機能を利用してAWSユーザの有効期限(TTL)を延長する

HashiCorp Vaultとは?

HashiCorpが提供している機密情報の管理ツールです。

Dynamic Secrets (Secret Backend) とは

VaultにAWSやDatabaseなどのアカウントを管理させる機能です。

この機能を利用することでVaultを通じてAWSなどのユーザ作成、削除を実施できるようになります。
有効期間が経過したユーザについてはVaultが自動で削除を行ってくれます。

参考

公式サイト

Vault by HashiCorp

ドキュメント

AWS Secret Backend – HTTP API – Vault by HashiCorp
/sys/leases – HTTP API – Vault by HashiCorp

過去の投稿

HashiCorp Vault の Dynamic Secrets (Secret Backend) を試す – Qiita


事前準備

VaultをDevモードで起動

$ vault server -dev

...
    export VAULT_ADDR='http://127.0.0.1:8200'
...
Root Token: dae8c23c-3749-0286-abc5-fbdf57b634f1

環境変数の設定

$ export VAULT_ADDR='http://127.0.0.1:8200'
$ export VAULT_TOKEN='dae8c23c-3749-0286-abc5-fbdf57b634f1'

AWS Backendをマウント

$ vault mount aws
Successfully mounted 'aws' at 'aws'!

AWSのTokenを設定

$ vault write aws/config/root 
    access_key=AKIAIFOCRMPWZMVED74Q 
    secret_key=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
Success! Data written to: aws/config/root

ポリシーを設定

policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1426528957000",
      "Effect": "Allow",
      "Action": [
        "ec2:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}
$ vault write aws/roles/deploy policy=@policy.json
Success! Data written to: aws/roles/deploy

1. Vaultが作成したAWSユーザの有効期限(TTL)を確認する

初期状態でLease設定や有効期限(TTL)を確認します。

Lease設定を確認する

Lease設定を確認しても、未設定のため取得できません。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    $VAULT_ADDR/v1/aws/config/lease

{"errors":[]}

VaultにAWSユーザを作成させる

この状態(Lease未設定)でAWSユーザを作成せた場合、lease_duration": 2764800となっています。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    $VAULT_ADDR/v1/aws/creds/deploy

{
  "request_id": "333374cd-1d0e-6015-adc9-85656d78a1c9",
  "lease_id": "aws/creds/deploy/6ceb69a0-249a-e65e-ff9a-9996a85bed45",
  "renewable": true,
  "lease_duration": 2764800,
  "data": {
    "access_key": "AKIAJIFBBLEAXHS4NQNQ",
    "secret_key": "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",
    "security_token": null
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

Lease設定を確認

AWSユーザのLease設定を確認すると、

  • “issue_time” : “2017-10-08T17:30:30.873476458+09:00”
  • “expire_time”: “2017-11-09T17:30:30.873476739+09:00”

となっており、有効期限(TTL)が 2764800秒 = 32日間 であることがわかりました。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    --request PUT 
    --data '{ "lease_id": "aws/creds/deploy/6ceb69a0-249a-e65e-ff9a-9996a85bed45" }' 
    $VAULT_ADDR/v1/sys/leases/lookup

{
  "request_id": "21234bd6-e722-8993-22f7-175f2c4d8dc6",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "expire_time": "2017-11-09T17:30:30.873476739+09:00",
    "id": "aws/creds/deploy/6ceb69a0-249a-e65e-ff9a-9996a85bed45",
    "issue_time": "2017-10-08T17:30:30.873476458+09:00",
    "last_renewal": null,
    "renewable": true,
    "ttl": 2764126
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

2. Lease機能を利用して有効期限(TTL)を設定する

初期設定では有効期限(TTL)が長いので、短い有効期限(TTL)を設定して、AWSユーザが実際に削除されるのか確認したいと思います。

Lease設定

lease30mlease_max12hに設定します。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    --request POST 
    --data '{ "lease": "30m",  "lease_max": "12h" }' 
    $VAULT_ADDR/v1/aws/config/lease

Lease設定を確認すると、lease, lease_maxが指定した値になっています。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    $VAULT_ADDR/v1/aws/config/lease 

{
  "request_id": "3b09ce08-b6b6-848f-3622-f3711d1a7d26",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "lease": "30m0s",
    "lease_max": "12h0m0s"
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

AWSユーザ作成

Lease設定後にAWSユーザを作成させると、lease_duration": 1800となっています。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    $VAULT_ADDR/v1/aws/creds/deploy

{
  "request_id": "5413657e-78e4-4128-e7c0-ed61c6158bc8",
  "lease_id": "aws/creds/deploy/5d7f8036-762b-8fcb-fcea-7bb7f1842f13",
  "renewable": true,
  "lease_duration": 1800,
  "data": {
    "access_key": "AKIAJCVNQTVWYDKAEIMA",
    "secret_key": "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",
    "security_token": null
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

AWSユーザのLease設定を確認すると、

  • “issue_time” : “2017-10-08T17:52:06.741853365+09:00”
  • “expire_time”: “2017-10-08T18:22:06.741853771+09:00”

となっており、有効期限(TTL)が1800秒 = 30分となってることがわかります。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    --request PUT 
    --data '{ "lease_id": "aws/creds/deploy/5d7f8036-762b-8fcb-fcea-7bb7f1842f13" }' 
    $VAULT_ADDR/v1/sys/leases/lookup

{
  "request_id": "e528ba92-53f0-cee0-973d-069612a24207",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "expire_time": "2017-10-08T18:22:06.741853771+09:00",
    "id": "aws/creds/deploy/5d7f8036-762b-8fcb-fcea-7bb7f1842f13",
    "issue_time": "2017-10-08T17:52:06.741853365+09:00",
    "last_renewal": null,
    "renewable": true,
    "ttl": 1646
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

AWSユーザの削除確認

AWSの管理コンソールを確認したところ、expire_timeに到達したタイミングでVaultが作成したAWSユーザ(vault-root-deploy-1507452723-604)が削除されました。

expire_time到達前
vault_lease01.png

expire_time到達後
vault_lease02.png

3. Lease機能を利用してAWSユーザの有効期限(TTL)を延長する

Lease機能には有効期限(TTL)を延長(renew)させる機能があるので、こちらの動作も確認してみます。

AWSユーザ作成

前回と同様にVaultにAWSユーザを作成させます。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    $VAULT_ADDR/v1/aws/creds/deploy

{
  "request_id": "15e22c37-ff2f-2277-6bc5-0834f4dd4eba",
  "lease_id": "aws/creds/deploy/f822cb89-98d6-0cd5-05e7-69020bda05b9",
  "renewable": true,
  "lease_duration": 1800,
  "data": {
    "access_key": "AKIAIGFBKDIZ6VU7UOUA",
    "secret_key": "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",
    "security_token": null
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

前回と同様にAWSユーザの有効期限(TTL)は30分間です。

  • “issue_time” : “2017-10-08T18:34:39.368521987+09:00”
  • “expire_time”: “2017-10-08T19:04:39.368522185+09:00”
$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    --request PUT 
    --data '{ "lease_id": "aws/creds/deploy/f822cb89-98d6-0cd5-05e7-69020bda05b9" }' 
    $VAULT_ADDR/v1/sys/leases/lookup

{
  "request_id": "f1848a4a-7f3f-8e0c-90cf-7024dd242c35",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "expire_time": "2017-10-08T19:04:39.368522185+09:00",
    "id": "aws/creds/deploy/f822cb89-98d6-0cd5-05e7-69020bda05b9",
    "issue_time": "2017-10-08T18:34:39.368521987+09:00",
    "last_renewal": null,
    "renewable": true,
    "ttl": 1753
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

有効期限(TTL)の延長

Lease機能を利用してAWSユーザの有効期限(TTL)を3600秒(30分)延長させます。

$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    --request PUT 
    --data '{ "lease_id": "aws/creds/deploy/28def48b-c990-d1d4-1691-610a8f2be0e9", "increment": 3600 }' 
    $VAULT_ADDR/v1/sys/leases/renew

{
  "request_id": "86f059ec-1680-c886-2ade-3e15635ed38e",
  "lease_id": "aws/creds/deploy/28def48b-c990-d1d4-1691-610a8f2be0e9",
  "renewable": true,
  "lease_duration": 3600,
  "data": null,
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

Lease設定を確認するとexpire_timeが更新されてます。

issue_timeから+3600秒(30分)されるわけではなく、延長(renew)コマンドを実行した時点(last_renewal)から3600秒(30分)延長されるようです。

延長前

  • “issue_time” : “2017-10-08T18:34:39.368521987+09:00”
  • “expire_time”: “2017-10-08T19:04:39.368522185+09:00”

延長後

  • “issue_time” : “2017-10-08T18:27:57.65077363+09:00”
  • “last_renewal”: “2017-10-08T18:35:52.918241764+09:00”
  • “expire_time” : “2017-10-08T19:35:52.918241648+09:00”
$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    --request PUT 
    --data '{ "lease_id": "aws/creds/deploy/28def48b-c990-d1d4-1691-610a8f2be0e9" }' 
    $VAULT_ADDR/v1/sys/leases/lookup
{
  "request_id": "f8e035a6-a72b-395a-aca0-641ed7c747ce",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "expire_time": "2017-10-08T19:35:52.918241648+09:00",
    "id": "aws/creds/deploy/28def48b-c990-d1d4-1691-610a8f2be0e9",
    "issue_time": "2017-10-08T18:27:57.65077363+09:00",
    "last_renewal": "2017-10-08T18:35:52.918241764+09:00",
    "renewable": true,
    "ttl": 3556
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

有効期限(TTL)の最大値を確認

lease_max12hに設定していたので12時間を超える有効期限(TTL)は設定できなくなっています。

有効期限(TTL)を 43200秒(1日)延長(renew)させても、expire_timeissue_timeの12時間後となっており、lease_maxを超えた有効期限(TTL)が設定できないことがわかります。

  • “issue_time” : “2017-10-08T18:27:57.65077363+09:00”
  • “last_renewal”: “2017-10-08T18:38:06.850822545+09:00”
  • “expire_time” : “2017-10-09T06:27:57.650782301+09:00”
$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    --request PUT 
    --data '{ "lease_id": "aws/creds/deploy/28def48b-c990-d1d4-1691-610a8f2be0e9", "increment": 43200 }' 
    $VAULT_ADDR/v1/sys/leases/renew

{
  "request_id": "9ed0ca65-18ee-2186-689a-1569f4f9e058",
  "lease_id": "aws/creds/deploy/28def48b-c990-d1d4-1691-610a8f2be0e9",
  "renewable": true,
  "lease_duration": 42590,
  "data": null,
  "wrap_info": null,
  "warnings": null,
  "auth": null
}
$ curl 
    --header "X-Vault-Token: $VAULT_TOKEN" 
    --request PUT 
    --data '{ "lease_id": "aws/creds/deploy/28def48b-c990-d1d4-1691-610a8f2be0e9" }' 
    $VAULT_ADDR/v1/sys/leases/lookup

{
  "request_id": "00d1c93f-431d-61f0-a116-b5d8a57af815",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "expire_time": "2017-10-09T06:27:57.650782301+09:00",
    "id": "aws/creds/deploy/28def48b-c990-d1d4-1691-610a8f2be0e9",
    "issue_time": "2017-10-08T18:27:57.65077363+09:00",
    "last_renewal": "2017-10-08T18:38:06.850822545+09:00",
    "renewable": true,
    "ttl": 42560
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

続きを読む

AWSでElastic Stack

はじめに

Elastic Stackについて触る機会があったので、メモを残します。
2017/09月時点の作業です。

Elastic Stackはログの解析・可視化等ができるものなのですが、今回その説明は省かせてもらいますので、公式等をご確認下さい。

作りたい構成

AWSのVPC環境にElasticsearchを仮想マシン3台にインストールしクラスタ化します。
その後、どれか一台にkibanaをインストールしてクラスタの状態をブラウザから確認します。

今回作りたい構成は以下のようなイメージです。
ES01.jpeg

AWS側の設定について

AWS側の設定は以下のようにしました。

VPC:192.100.0.0/24
VPC Subnet:192.100.0.0/27
仮想マシン:Amazon Linux
仮想マシンプラン:m4.large
仮想マシン台数:3台
srv1:192.100.0.4/EIP付き
srv2:192.100.0.5/EIP付き
srv3:192.100.0.6/EIP付き

セキュリティグループ
フルオープンはセキュリティ的に問題があるのである程度絞っています。
・仮想マシン操作用
SSH (22) TCP (6) 22 [sshアクセスするIPアドレス]
・Elasticsearchへのアクセス用
カスタム TCP ルール TCP (6) 9200 [ElasticsearchへアクセスするIPアドレス]
・Elasticsearchの内部通信用
カスタム TCP ルール TCP (6) 9300 192.100.0.0/27
・kibanaへのアクセス用
カスタム TCP ルール TCP (6) 5601 [kibanaへアクセスするIPアドレス]

それでは作ります。

構築作業

1.事前準備

1.1 java8(openJDK)のインストール(仮想マシン3台共通作業)

Amazon Linuxでインストールされているjavaのバージョンは1.7.0がインストールされていますので、1.7.0を削除し、1.8.0をインストールします。
デバッグは使うことは無いと思いますがお作法として入れておきます。

# yum -y remove java-1.7.0-openjdk
# yum -y install java-1.8.0-openjdk-devel
# yum -y install java-1.8.0-openjdk-debuginfo --enablerepo=*debug*

1.2 ElasticsearchをyumでインストールできるようにGPGキーのインポートと、リポジトリの作成(仮想マシン3台共通作業)

# rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
# vi /etc/yum.repos.d/elastic.repo
[elasticsearch-5.x]
name=Elasticsearch repository for 5.x packages
baseurl=https://artifacts.elastic.co/packages/5.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

2.Elasticsearchのインストールと起動

Elasticsearchをクラスターモードで起動します。

2-1.Elasticsearchのインストール(仮想マシン3台共通作業)

インストールはありがたいことにとても簡単です。

# yum install -y elasticsearch

インストール後のElasticsearchのディレクトリ状況は以下のような感じになっていました。

/var/run/elasticsearch
/var/log/elasticsearch
/var/lib/elasticsearch
/var/lib/yum/yumdb/e/e7a7bc22f961d4dd3889c0cac8f668512fe3d2c0-elasticsearch-5.6.2-1-noarch
/var/lib/yum/repos/x86_64/latest/elasticsearch-5.x
/var/cache/yum/x86_64/latest/elasticsearch-5.x
/etc/elasticsearch
/etc/elasticsearch/elasticsearch.yml
/etc/sysconfig/elasticsearch
/etc/rc.d/init.d/elasticsearch
/usr/lib/systemd/system/elasticsearch.service
/usr/lib/sysctl.d/elasticsearch.conf
/usr/lib/tmpfiles.d/elasticsearch.conf
/usr/share/elasticsearch
/usr/share/elasticsearch/lib/elasticsearch-5.6.2.jar
/usr/share/elasticsearch/modules/reindex/elasticsearch-rest-client-5.6.2.jar
/usr/share/elasticsearch/bin/elasticsearch
/usr/share/elasticsearch/bin/elasticsearch.in.sh
/usr/share/elasticsearch/bin/elasticsearch-keystore
/usr/share/elasticsearch/bin/elasticsearch-systemd-pre-exec
/usr/share/elasticsearch/bin/elasticsearch-translog
/usr/share/elasticsearch/bin/elasticsearch-plugin

2.2.elasticsearch.ymlの設定(仮想マシン毎の設定)

elasticsearch.ymlはデフォルトの設定は全てコメント化されていますので、
追加したい内容を追記すればOKでした。
追加する内容は以下です。

cluster.name [cluster-name]:クラスタ名。入力しないとデフォルトの名前になる。
node.name [node-name]:elasticsearchノードの名前。自身の識別に利用する。
discovery.zen.ping.unicast.hosts ["IPADDRESS","IPADDRESS","IPADDRESS"]:クラスタを構成する相手ホスト名をユニキャストで検索する。(3台構成なので3つ書く)
network.host: 0.0.0.0:通信許可設定。通信の制御はAWSのセキュリティグループで行う為、こちらは全許可で記載します。

上記の設定を各サーバのelasticsearch.ymlに記載します。

srv1
# vi /etc/elasticsearch/elasticsearch.yml
cluster.name: my-cluster
node.name: node001
network.host: 0.0.0.0
discovery.zen.ping.unicast.hosts: ["192.100.0.4","192.100.0.5","192.100.0.6"]
srv2
# vi /etc/elasticsearch/elasticsearch.yml
cluster.name: my-cluster
node.name: node002
network.host: 0.0.0.0
discovery.zen.ping.unicast.hosts: ["192.100.0.4","192.100.0.5","192.100.0.6"]
srv3
# vi /etc/elasticsearch/elasticsearch.yml
cluster.name: my-cluster
node.name: node003
network.host: 0.0.0.0
discovery.zen.ping.unicast.hosts: ["192.100.0.4","192.100.0.5","192.100.0.6"]

2.3.Elasticsearchの起動(仮想マシン3台共通作業)

各サーバのElasticsearchサービスを起動します。この時同じタイミングで起動コマンドを打つと、ノードの認識がうまくいかない場合があります。サービスの起動完了を待ちながら1台ずつ起動します。

# /etc/init.d/elasticsearch start

2.4.クラスタ状態の確認

仮想マシン3台の内、どのサーバでも構いませんので以下コマンドを発行し、ノードの状態を確認します。

# curl localhost:9200/_cat/nodes
192.100.0.5 8 43 0 0.00 0.00 0.00 mdi - node002
192.100.0.6 7 43 0 0.00 0.02 0.00 mdi - node003
192.100.0.4 8 44 0 0.00 0.01 0.04 mdi * node001
※クラスタ化ができていない場合は自分のノード名しか表示されません。

クラスタ化がとても簡単に成功しました。
他にもログを確認することでノードが追加されたか確認することができます。
以下のようにログが出力されます。

/var/log/elasticsearch/my-cluster.log
[2017-xx-xxTxx:xx:xx,043][INFO ][o.e.g.GatewayService     ] [node001] recovered [0] indices into cluster_state
[2017-xx-xxTxx:xx:xx,815][INFO ][o.e.c.s.ClusterService   ] [node001] added {{node002}{UTINV4wuTd6UfgLByoG4gQ}{E5ptnPZ0SG-xc629ayXK_Q}{192.100.0.5}{192.100.0.5:9300},}, 
reason: zen-disco-node-join[{node002}{UTINV4wuTd6UfgLByoG4gQ}{E5ptnPZ0SG-xc629ayXK_Q}{192.100.0.5}{192.100.0.5:9300}]

2.5.自動起動設定(仮想マシン3台共通作業)

無事クラスタ化ができましたので、Elasticsearchサービスを自動起動するよう設定しておきます。

# chkconfig --add elasticsearch
# chkconfig --list elasticsearch
elasticsearch   0:off   1:off   2:on    3:on    4:on    5:on    6:off

3.Kibanaのインストールとx-packのインストール

次に可視化ツールのKibanaをインストールします。
また、クラスタの状態をモニタリングする為、x-packを導入します。
作業はsrv1で実施しました。

3.1.kibanaのインストール

Kibanaもインストールはとても簡単です。

# yum -y install kibana

3.2.kibana.ymlの編集

kibanaは5.0.0からリモートホストからのアクセスを受け入れない為、リモートホストからの接続が必要な場合は設定ファイル「/etc/kibana/kibana.yml」の「server.host」の修正が必要です。今回はブラウザから確認したいので、修正を行いました。

# vi /etc/kibana/kibana.yml
server.host: "0.0.0.0"

kibana.ymlもデフォルトの設定は全てコメント化されていますので、
追加したい内容を追記すればOKでした。

3.3.Kibanaの起動

# /etc/init.d/kibana start

3.4.kibanaへのアクセス

ブラウザから1号機のURLにアクセスしてみます。

http://[srv1のEIP]/5601

とりあえずはページが表示されれば問題ありません。本来の目的にはまだ届いていない為、画像は割愛します。

3.5.自動起動設定

kibanaも自動起動するようにしておきます。

# chkconfig --add kibana
# chkconfig --list kibana
kibana          0:off   1:off   2:on    3:on    4:on    5:on    6:off

3.6.x-packのインストール(仮想マシン3台共通作業)

今回の目的である、kibanaでクラスタの状態を確認する為にx-packプラグインをインストールしていきます。まずはelasticsearch用のx-packをインストールします。

kibanaを停止後、x-packインストールします。

# /etc/init.d/kibana stop
# /usr/share/elasticsearch/bin/elasticsearch-plugin install x-pack

途中y(yes)の入力が必要な為、全文を乗せておきます。

-> Downloading x-pack from elastic
[=================================================] 100%??
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@     WARNING: plugin requires additional permissions     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* java.io.FilePermission \.pipe* read,write
* java.lang.RuntimePermission accessClassInPackage.com.sun.activation.registries
* java.lang.RuntimePermission getClassLoader
* java.lang.RuntimePermission setContextClassLoader
* java.lang.RuntimePermission setFactory
* java.security.SecurityPermission createPolicy.JavaPolicy
* java.security.SecurityPermission getPolicy
* java.security.SecurityPermission putProviderProperty.BC
* java.security.SecurityPermission setPolicy
* java.util.PropertyPermission * read,write
* java.util.PropertyPermission sun.nio.ch.bugLevel write
* javax.net.ssl.SSLPermission setHostnameVerifier
See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html
for descriptions of what these permissions allow and the associated risks.

Continue with installation? [y/N]y
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@        WARNING: plugin forks a native controller        @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
This plugin launches a native controller that is not subject to the Java
security manager nor to system call filters.

Continue with installation? [y/N]y
-> Installed x-pack

3.7.elasticsearchのx-packのsecurityを無効化する。(仮想マシン3台共通作業)

x-packには様々な機能が付属しており、securityが有効だとちゃんと設定を行わないとKibanaが起動できません。今回の目的には関係が無い為、securityを無効化することで回避します。

# vi /etc/elasticsearch/elasticsearch.yml
xpack.security.enabled: false
※上記を最下行に追記

追記後Elasticsearchを再起動して反映
# /etc/init.d/elasticsearch restart

3.8.kibanaのx-packのインストール

Kibana用x-packをインストールします。

# /usr/share/kibana/bin/kibana-plugin install x-pack
Attempting to transfer from x-pack
Attempting to transfer from https://artifacts.elastic.co/downloads/kibana-plugins/x-pack/x-pack-5.6.2.zip
Transferring 119528829 bytes....................
Transfer complete
Retrieving metadata from plugin archive
Extracting plugin archive
Extraction complete
Optimizing and caching browser bundles...
Plugin installation complete

途中止まっちゃったかな?と思いたくなるような反応が無い時間がありましたが、気にせず放置しておけば大丈夫です。

3.9.Kibanaの起動

設定が完了しましたのでKibanaを起動します。

# /etc/init.d/kibana start

設定後のプラグインの状態とライセンスの状態がどうなっているかメモしておきます。
コマンドは3台のいずれかのサーバで実行すれば確認できます。

プラグイン状態
# curl -XGET -u elastic:changeme 'http://localhost:9200/_cat/plugins?v'
name    component         version
node003 analysis-kuromoji 5.6.2
node003 x-pack            5.6.2
node002 analysis-kuromoji 5.6.2
node002 x-pack            5.6.2
node001 analysis-kuromoji 5.6.2
node001 x-pack            5.6.2
ライセンス状態
# curl -XGET -u elastic:changeme 'http://localhost:9200/_xpack/license'
{
  "license" : {
    "status" : "active",
    "uid" : "7977deb4-d253-4ef4-8fd1-bf01e1d86315",
    "type" : "trial",
    "issue_date" : "2017-xx-xxTxx:xx:xx.537Z",
    "issue_date_in_millis" : 1506590395537,
    "expiry_date" : "2017-xx-xxTxx:xx:xx.537Z",
    "expiry_date_in_millis" : 1509182395537,
    "max_nodes" : 1000,
    "issued_to" : "my-cluster",
    "issuer" : "elasticsearch",
    "start_date_in_millis" : -1
  }
}

4.kibanaからクラスタ状態を確認

それでは最後にkibanaの画面を確認します。

ブラウザから1号機のURLにアクセスします。
http://[srv1のEIP]:5601

表示された画面の左ペインにある、Monitoringを選択します。
es-monitoring.png

遷移後の画面の真ん中にある、Noedsを選択します。
es-monitoring2.png

Kibanaの画面上からクラスタの状態が確認できました。
es-monitoring3.png

Kibanaの画面上から確認できるようになると、すごくできた感があります。
次回は肝心のログデータの取り込みなんかをメモしたいと思います。

続きを読む