DynamoDB StreamをLambda(Python)で処理する時の共通事前処理を考える

AWSリソース同士の連携でLambdaをつかうのは便利だけどeventが毎度複雑でたいへん。
今回はDynamoDB StreamをPythonで拾うとき、主要な処理以外をなんとか簡潔に書けるようにしたいチャレンジ。

DynamoDB ストリーム と AWS Lambda のトリガー – Amazon DynamoDB

共通処理の仕様

このあたりの処理を使いまわせば楽になりそう。

  • 対応する関数(lambda含む)を登録して、レコードごとに実行したい

    • 呼ばれる側はコンテキスト決め打ちでよいので、分岐とか不要
  • Itemのデータを取り出しやすくする
    • ついでにDynamoおなじみの型付きでくるので、PythonのDictに変換する
  • (Option) 例外処理

で、エントリーポイントは、こんな風に書けたらよいかなと。

handler(予定)
def lambda_handler(event, context):
    ds = DynamoStreamDispatcher(event)
    ds.on_insert.append(lambda rec: print(rec.event_name)) # lambda OK
    ds.on_remove.append(after_remove1)
    ds.on_remove.append(after_remove2) #複数の処理 OK
    ds.on_modify.append(after_modify)


    ds.dispatch()
    return True

ハンドラを登録してdispatchする感じ。

ざっくり作ってみる

とりあえず当初にきめたlambda_handlerに書きたい内容を実装してみた。

lambda_function.py
from __future__ import print_function

import json
from boto3.dynamodb.types import TypeDeserializer
deser = TypeDeserializer()

print('Loading function')


class DeRecord:
    ## Itemをデシリアライズしたもの
    def __init__(self, rec):
        self.event_name = rec['eventName']
        self.old = self._desi(rec['dynamodb'].get('OldImage'))
        self.new = self._desi(rec['dynamodb'].get('NewImage'))

    def _desi(self, image):
        d = {}
        if image:
            for key in image:
                d[key] = deser.deserialize(image[key])
        return d


class DynamoStreamDispatcher:
    def __init__(self, event):
        self.on_insert = []
        self.on_remove = []
        self.on_modify = []
        self.records   = []
        for r in event['Records']:
            # レコードはdictに加工しちゃう。
            self.records.append(DeRecord(r))

        self.raw = event

    def dispatch(self):
        """
        synced dispatcher
        """
        results = []
        for r in self.records:
            try:
                for runner in getattr(self, 'on_' + r.event_name.lower()):
                    results.append(runner(r))
            except AttributeError:
                print("Unknown event " + r.event_name)
                continue

        return results


## ここから、個別Lambda用処理の関数サンプル。引数は加工済みのレコード
def after_remove1(rec):
    print("deleted")
    return None

def after_remove2(rec):
    print(rec.old)
    return None


def after_modify(rec):
    print("key updated...")
    print(rec.old['Message'])
    print(rec.new['Message'])
    return None


def lambda_handler(event, context):
    ds = DynamoStreamDispatcher(event)
    ds.on_insert.append(lambda rec: print(rec.event_name))
    ds.on_remove.append(after_remove1)
    ds.on_remove.append(after_remove2)
    ds.on_modify.append(after_modify)

    ds.dispatch()
    return True

Sample event templateからDynamoDB Update(※末尾に付属)を流してみる。

START RequestId: 6ed79996-0ecc-11e7-8985-db0ca21254c3 Version: $LATEST
INSERT
key updated...
New item!
This item has changed
deleted
{u'Message': u'This item has changed', u'Id': Decimal('101')}
END RequestId: 6ed79996-0ecc-11e7-8985-db0ca21254c3

登録した関数がそれぞれ実行されてOK。

ほぼ自分用ライブラリだけど、PyPIに似たようなのがなければ登録して使いまわそうかな。
あとは差分とかをうまいこと格納したいね。


付録: DynamoDB Updateのサンプルイベント

{
  "Records": [
    {
      "eventID": "1",
      "eventVersion": "1.0",
      "dynamodb": {
        "Keys": {
          "Id": {
            "N": "101"
          }
        },
        "NewImage": {
          "Message": {
            "S": "New item!"
          },
          "Id": {
            "N": "101"
          }
        },
        "StreamViewType": "NEW_AND_OLD_IMAGES",
        "SequenceNumber": "111",
        "SizeBytes": 26
      },
      "awsRegion": "us-west-2",
      "eventName": "INSERT",
      "eventSourceARN": "arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899",
      "eventSource": "aws:dynamodb"
    },
    {
      "eventID": "2",
      "eventVersion": "1.0",
      "dynamodb": {
        "OldImage": {
          "Message": {
            "S": "New item!"
          },
          "Id": {
            "N": "101"
          }
        },
        "SequenceNumber": "222",
        "Keys": {
          "Id": {
            "N": "101"
          }
        },
        "SizeBytes": 59,
        "NewImage": {
          "Message": {
            "S": "This item has changed"
          },
          "Id": {
            "N": "101"
          }
        },
        "StreamViewType": "NEW_AND_OLD_IMAGES"
      },
      "awsRegion": "us-west-2",
      "eventName": "MODIFY",
      "eventSourceARN": "arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899",
      "eventSource": "aws:dynamodb"
    },
    {
      "eventID": "3",
      "eventVersion": "1.0",
      "dynamodb": {
        "Keys": {
          "Id": {
            "N": "101"
          }
        },
        "SizeBytes": 38,
        "SequenceNumber": "333",
        "OldImage": {
          "Message": {
            "S": "This item has changed"
          },
          "Id": {
            "N": "101"
          }
        },
        "StreamViewType": "NEW_AND_OLD_IMAGES"
      },
      "awsRegion": "us-west-2",
      "eventName": "REMOVE",
      "eventSourceARN": "arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899",
      "eventSource": "aws:dynamodb"
    }
  ]
}

参考:

続きを読む

awsでruby on railsとapacheとの連携構築方法

すべての手順

必要なミドルウェアのインストール

$ sudo yum update
$ sudo yum install curl-devel ncurses-devel gdbm-devel readline-devel sqlite-devel ruby-devel
$ sudo yum install gcc gcc-c++ openssl-devel zlib-devel make patch git gettext perl rpm-build libxml2

rbenvのインストール

$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:${PATH}"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile
$ env | grep RBENV
RBENV_SHELL=bash
$ rbenv --version
rbenv 1.1.0-2-g4f8925a

ruby2.4.0 のインストール

$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv install -l
.....
2.3.0-dev
  2.3.0-preview1
  2.3.0-preview2
  2.3.0
  2.3.1
  2.3.2
  2.3.3
  2.4.0-dev
  2.4.0-preview1
  2.4.0-preview2
  2.4.0-preview3
  2.4.0-rc1
  2.4.0
  2.5.0-dev
  jruby-1.5.6
  jruby-1.6.3
  jruby-1.6.4
  jruby-1.6.5
  jruby-1.6.5.1
...
$ rbenv install -v 2.4.0
$ rbenv rehash
$ rbenv global 2.4.0
$ ruby -v
 ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]

Railsのインストール

$ gem update --system
$ gem install bundler --no-rdoc --no-ri
$ gem install --no-ri --no-rdoc rails
$ rbenv rehash    
$ rails -v
Rails 5.0.2
$ gem install bundler

sqlite3のインストール

$ wget https://www.sqlite.org/2017/sqlite-autoconf-3170000.tar.gz
$ tar xvzf sqlite-autoconf-3170000.tar.gz
$ cd ./sqlite-autoconf-3170000
$ ./configure
$ make
$ make install
$ source ~/.bash_profile
$ rm -rf sqlite-autoconf-317000*
$ sqlite3 --version
; 3.17.0 2017-02-13.....

Hello World アプリ作成

$ cd ~
$ rails new hello_world
$ cd hello_world
$ vim Gemfile
$ bundle install
- # gem 'therubyracer', platforms: :ruby
+ gem 'therubyracer', platforms: :ruby
$ bundle exec rails s -e production
=> Booting Puma
=> Rails 5.0.2 application starting in production on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.4.0-p0), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

apache2.4のインストール

$ sudo yum install httpd24 httpd24-devel

Passengerのインストール

$ gem install passenger
$ rbenv rehash
$ sudo dd if=/dev/zero of=/swap bs=1M count=1024
$ sudo mkswap /swap
$ sudo swapon /swap
$ passenger-install-apache2-module
.....
linking shared-object passenger_native_support.so

--------------------------------------------
Almost there!

Please edit your Apache configuration file, and add these lines:

   LoadModule passenger_module /home/ec2-user/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/passenger-5.1.2/buildout/apache2/mod_passenger.so
   <IfModule mod_passenger.c>
     PassengerRoot /home/ec2-user/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/passenger-5.1.2
     PassengerDefaultRuby /home/ec2-user/.rbenv/versions/2.4.0/bin/ruby
   </IfModule>

After you restart Apache, you are ready to deploy any number of web
applications on Apache, with a minimum amount of configuration!
....

Apacheの設定

$ sudo chown -R apache. ~/hello_world

$ sudo vim /etc/httpd/conf.d/passenger.conf
+   LoadModule passenger_module /home/ec2-user/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/passenger-5.1.2/buildout/apache2/mod_passenger.so
+   <IfModule mod_passenger.c>
+     PassengerRoot /home/ec2-user/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/passenger-5.1.2
+     PassengerDefaultRuby /home/ec2-user/.rbenv/versions/2.4.0/bin/ruby
+   </IfModule>
$ sudo vim /etc/httpd/conf.d/apps.conf
 <VirtualHost *:80>
    ServerAlias ip address
    ServerName ip address

    <Directory "/home/ec2-user/hello_world/public/">
        PassengerAppRoot /home/ec2-user/hello_world
        RailsEnv production        
        Options -MultiViews
        Options Indexes FollowSymLinks
        Require all granted
    </Directory>
 </VirtualHost>

Apacheの起動

$ sudo service httpd start
$ sudo service httpd status

続きを読む

p2インスタンス上にCUDA, cuDNNを用いたChainer環境を構築する

やったこと

AWSのp2.xlargeにCUDA, cuDNNを利用可能なChainer環境を作成してみました。

前提

以下のバージョンを利用しました。(実施時点のものです)

version
OS Ubuntu14.04
pyenv 1.0.8
Anaconda 4.3.0
python 3.6.2
CUDA 8.0
cuDNN v5.1
Chainer 1.2.21

必要なライブラリのインストール

sudo apt-get install -y git gcc make openssl libssl-dev libbz2-dev libreadline-dev libsqlite3-dev

pyenvのインストール

pyenvのインストールは以下を参考にしました。
http://qiita.com/y__sama/items/5b62d31cb7e6ed50f02c

git clone https://github.com/yyuu/pyenv.git ~/.pyenv
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
source ~/.bashrc

Anacondaのインストール

Anacondaのインストールは以下を参考にしました。
http://qiita.com/y__sama/items/5b62d31cb7e6ed50f02c

pyenv install -l | grep anaconda
pyenv install anaconda3-4.3.0
pyenv rehash
pyenv global anaconda3-4.3.0
echo 'export PATH="$PYENV_ROOT/versions/anaconda3-4.3.0/bin/:$PATH"' >> ~/.bashrc
source ~/.bashrc
conda update conda
python --version

CUDAのインストール

wget https://developer.nvidia.com/compute/cuda/8.0/Prod2/local_installers/cuda-repo-ubuntu1404-8-0-local-ga2_8.0.61-1_amd64-deb
sudo dpkg -i cuda-repo-ubuntu1404-8-0-local-ga2_8.0.61-1_amd64-deb
sudo apt-get update
sudo apt-get install cuda -y
echo 'export CUDA_HOME=/usr/local/cuda' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${CUDA_HOME}/lib64' >> ~/.bashrc
echo 'export PATH=$PATH:${CUDA_HOME}/bin' >> ~/.bashrc

cuDNNのインストール

別途nvidiaのサイトから取得。

mkdir .cudnn
tar xvzf ./cudnn-8.0-linux-x64-v5.1.tgz
mv ./cuda ./.cudnn
mv ./.cudnn/cuda ./.cudnn/5.1
sudo ln -s ~/.cudnn/5.1/include/cudnn.h /usr/local/cuda/include/cudnn.h
sudo ln -s ~/.cudnn/5.1/lib64/libcudnn* /usr/local/cuda/lib64/
sudo chmod a+r /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn*

Chainerのインストール

pip install chainer

動作確認

mkdir mnist
wget https://raw.githubusercontent.com/pfnet/chainer/master/examples/mnist/train_mnist.py ./mnist
# GPU使用
time python ./mnist/train_mnist.py -g 0 -e 10
# GPU不使用
time python ./mnist/train_mnist.py -g -1 -e 10

実行時間に差が出たら成功です。

続きを読む

Rundeckで参照権限追加(readonly,gest)

投稿の目的

Rundeckで、アプリ開発者様に参照権限を追加する。
既に本番稼動しているRundeckを停止しないで、追加できることを目指す。

背景

Rundeckインストール直後はadmin権限しかない。
危険かつ責任所在が不明確になるので、参照権限がないと後々面倒。

手順

1./etc/realm.propertiesにユーザ追加(guests)

admin定義の直下あたりに追加すると良い。

/etc/realm.properties
guests:guests,user

この状態でもログインできるが、見れるプロジェクトが無い。
スクリーンショット 2017-03-19 19.02.15.png

2.guests権限を定義する。

2-1.定義ファイル雛形を作る。(adminのコピー)

# cp admin.aclpolicy guests.aclpolicy

2-2.guest.aclpolicyを編集

ポイントは「参照権限」と書いている行と「username:」でgusetを指定している部分

guests.aclpolicy
description: Project settings for guest user.
context:
  project: 'XXXX_Test' # XXXX_Testは、参照権限を付与したいプロジェクト
for:
  resource:
    - allow: [read] # allow read/create all kinds 参照権限
  adhoc:
    - allow: [read] # allow read/running/killing adhoc jobs 参照権限
  job:
    - allow: [read] # allow read/write/delete/run/kill of all jobs参照権限
  node:
    - allow: [read] # allow read/run for all nodes参照権限
by:
  username: guests

---

description: Rundeck settings for guest user.
context:
  application: 'rundeck'
for:
  resource:
    - allow: '*' # allow create of projects
  project:
    - allow: '*' # allow view/admin of all projects
  project_acl:
    - allow: '*' # allow admin of all project-level ACL policies
  storage:
    - allow: '*' # allow read/create/update/delete for all /keys/* storage content
by:
  username: guests

2-3.ログインして確認

プロジェクトが見えるようになりました。
スクリーンショット 2017-03-19 19.24.40.png
ジョブを選択しても右上に緑色「Run Job Now」のボタンがありません。成功です!!
スクリーンショット 2017-03-19 19.26.33.png
実行権限のあるユーザでログインしなおすと緑色「Run Job Now」のボタンが表示されます。
スクリーンショット 2017-03-19 19.27.19.png

結びに代えて

より良い方法ございましたらご教示願います。

続きを読む

terraform v0.9で追加された "env" を使って複数環境を作成してみる

update terraform v0.9

アップデート内容については、ここに書いてあります。
https://www.hashicorp.com/blog/terraform-0-9/

やはり個人的には、 env による複数環境の構築・運用のしやすさがどうなるのかが気になったのでさっそく試してみました。

サンプルで作成したレポジトリ

今回作成したレポジトリはこちらにあります。
https://github.com/Twinuma/terraform0.9-sample

説明

env に関してはここにドキュメントがあるのでこちらを見てください。
https://www.terraform.io/docs/state/environments.html

簡単に説明すると、
terraform env new dev と実行すると、カレントディレクトリに terraform.tfstate.d/dev が出来上がります。

次にterraform env select dev と実行すると、カレントディレクトリに .terraform/environment が出来上がります。このファイルには現在の環境情報が書いてます。この場合は dev と記載されている。

apply コマンドを実行すると terraform.tfstate.d/dev配下に terraform.tfstateが出来上がります。

今回私がやってみた環境を分ける方法

vpc.tf
resource "aws_vpc" "vpc" {
  cidr_block = "${terraform.env == "dev" ? var.dev_vpc_cidr : terraform.env == "stg" ? var.stg_vpc_cidr : var.prod_vpc_cidr}"
  enable_dns_hostnames = true
  tags {
    Name = "${terraform.env}-${var.vpc_name}"
    env = "${terraform.env}"
  }
}
terraform.tfvars
vpc_name = "vpc"
dev_vpc_cidr = "10.2.0.0/16"
stg_vpc_cidr = "10.1.0.0/16"
prod_vpc_cidr = "10.0.0.0/16"
terraform env select dev

terraform plan -var 'access_key=xxxxx' -var 'secret_key= xxxxx'

terraform apply -var 'access_key= xxxxx' -var 'secret_key= xxxxx'

このようにやれば一つのtfファイル内で条件分岐で環境分けができました。
なにげにタグにもちゃんと入れられるのも嬉しい。

もっとスマートにできる方法があるかもしれないので、もしよかったらコメントで教えてくれると嬉しいですm(_ _)m

続きを読む

Serverless FrameworkとS3で超簡単な投票システムを作った話

はじめに

社内でいつも通り仕事をしていると、約1週間で簡単な投票アプリを作ってくれないか?というオファーを受け、構成を考えているときに「Serverless Frameworkを使えばいいじゃん」と神のお告げが降ってきましたので、触ってみることにしました。

最終目標

スマホから投票できて、最終的にCSVでまとまった投票データを吐き出す

構成はこんな感じ

青枠の部分がServerlessFramework

serverless-img.PNG

大まかな流れ

  1. S3に投票フォームをアップロードしてバケットごとWeb公開する
  2. フォームの投票内容をAPIGateWayに向かってPOSTする
  3. APIGateWayがLambdaをキックする
  4. Lambdaが受け取ったJSONをDynamoDBに書き込む
  5. 完了!

さあ、はじめてみようか

書いていく前に・・・今回ServerlessFrameworkを触るにあたって@hiroshik1985様のとことんサーバーレス①:Serverless Framework入門編を参考にさせていただきました。

ServerlessFrameworkのインストール

npmで入れちゃいます

$ npm install -g serverless

AWS Credentialsを設定する

ServerlessFramework用のIAMを作成して、AWSConfigureに登録します

$ aws configure
AWS Access Key ID [None]: 先ほど作成したIAMのアクセスキー
AWS Secret Access Key [None]: 先ほど作成したIAMのシークレットアクセスキー
Default region name [None]: ap-northeast-1
Default output format [None]: ENTER

プロジェクト作成

serverless用のディレクトリを作成し、そのディレクトリの中で下記のコマンド実行
今回はnode.jsで
※serverlessコマンドはインストール時に用意されるslsエイリアスを使うと便利です

$ sls create --template aws-nodejs --name vote
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.8.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"

こんなのが表示されたら成功。なんかかっこいい・・・
これでディレクトリ内にhandler.jsやserverless.ymlが作成される

はじめてのデプロイ

デプロイのコマンドはこちら

$ sls deploy -v

特に怒られれなければデプロイ成功です。AWSのコンソール画面ですでにLambda等が立ち上がっていると思います

Lambdaファンクションを書くよ

Lambdaファンクションは基本的にhandler.jsに書きます。
たとえばこんな風に

handler.js
import AWS from 'aws-sdk'

AWS.config.update({region: 'ap-northeast-1'})

const db = new AWS.DynamoDB.DocumentClient()

export const register = (event, context, callback) => {
    const body = JSON.parse(event.body)
    const params = {
        TableName: "names",
        Key: {
            id: body.employeeNumber
        },
        UpdateExpression: "set #Group1 = :vote1, #Group2 = :vote2, #Group3 = :vote3",
        ExpressionAttributeNames: {
            "#Group1": "voteGroup1",
            "#Group2": "voteGroup2",
            "#Group3": "voteGroup3"
        },
        ExpressionAttributeValues: {
            ":vote1": body.voteGroup1,
            ":vote2": body.voteGroup2,
            ":vote3": body.voteGroup3
        },
        ReturnValues: "UPDATED_NEW"
    }



    try {
        db.update(params, (error, data) => {
            if (error) {
                callback(null, {
                    statusCode: 400,
                    headers:{ "Access-Control-Allow-Origin" : "*" },
                    body: JSON.stringify({message: 'Failed.', error: error, params: params})
                })
            }
            callback(null, {
                statusCode: 200,
                headers:{ "Access-Control-Allow-Origin" : "*" },
                body: JSON.stringify({message: 'Succeeded!', params: params})
            })
        })
    } catch (error) {
        callback(null, {
            statusCode: 400,
            headers:{ "Access-Control-Allow-Origin" : "*" },
            body: JSON.stringify({message: 'Failed.', error: error, params: params})
        })
    }
}

フォームからPOSTメソッドでJSON形式のデータをDynamoDBに格納します
送られてくるJSONデータは{“employeeNumber”: “001”, “voteGroup1”: “1”, “voteGroup2”: “2”, “voteGroup3”: “3”}のような形で送られてくることを想定しています

そのほか設定を書くよ

serverless.ymlにDynamoDBやiamRoleなどの設定を書きます
たとえばこんな風に

serverless.yml

service: vote

provider:
  name: aws
  runtime: nodejs4.3
  stage: dev
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: "Allow"
      Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/*"
      Action:
        - "dynamodb:*"

plugins:
  - serverless-webpack

functions:
  register:
    handler: handler.register
    events:
      - http:
         path: names
         method: post
         cors: true

resources:
  Resources:
    hello:
      Type: "AWS::DynamoDB::Table"
      Properties:
        TableName: names
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

これで再度デプロイするとLambdaファンクションが設定され、DynamoDBにテーブルが作成されます

試しに実行だ

デプロイ後に表示されるAPIGateWayのエンドポイントに対してcurlでリクエストを送信します
※「 https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names 」はAPIGateWayのエンドポイントです


curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"employeeNumber": "001", "voteGroup1": "1", "voteGroup2": "2", "voteGroup3": "3"}'  https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names

問題なくリクエストが送信されれば、DynamoDBにテータが格納されていると思います

フォームの作成

あまり手の込んだフォームをコーディングしている時間がなかったので、シンプルにまとめました
CSSは「Material Design Lite」というCSSフレームワークを使用し、AjaxでAPIGatewayのエンドポイントに対してHTTPリクエストを投げています

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>投票アプリ</title>
    <script type="text/javascript" src="jquery-3.1.1.min.js"></script>
    <script type="text/javascript" src="vote.js"></script>
    <script type="text/javascript" src="mdl/material.min.js"></script>
    <link rel="stylesheet" href="mdl/material.min.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="common.css">
</head>
<body>

<!-- Always shows a header, even in smaller screens. -->
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
    <header class="mdl-layout__header">
        <div class="mdl-layout__header-row">
            <!-- Title -->
            <span class="mdl-layout-title">投票アプリ</span>
            <!-- Add spacer, to align navigation to the right -->
            <div class="mdl-layout-spacer"></div>
        </div>
    </header>
    <main class="mdl-layout__content">
        <div id="page-content" class="mdl-grid">
            <div class="mdl-cell mdl-cell--12-col">
                社員番号:<br>
                <select name="employee_number" class="employee-number">
                    <option value="">選択してください</option>
                    <option value="001">001</option>
                    <option value="002">002</option>
                    <option value="003">003</option>
                    <option value="004">004</option>
                    <option value="005">005</option>
                    <option value="006">006</option>
                </select>
                <span class="font-red">※必須</span>
            </div>
            <div class="mdl-cell mdl-cell--12-col">
                投票するグループを<span class="font-red">3つ</span>選択してください<span class="font-red">※必須</span>
                <br>
                1位:
                <select name="vote_group1" class="vote-group1">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
                <br>
                2位:
                <select name="vote_group2" class="vote-group2">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
                <br>
                3位:
                <select name="vote_group3" class="vote-group3">
                    <option value="">選択してください</option>
                    <option value="1">グループ1</option>
                    <option value="2">グループ2</option>
                    <option value="3">グループ3</option>
                    <option value="4">グループ4</option>
                    <option value="5">グループ5</option>
                    <option value="6">グループ6</option>
                    <option value="7">グループ7</option>
                    <option value="8">グループ8</option>
                    <option value="9">グループ9</option>
                </select>
                <br>
            </div>
            <div class="mdl-cell mdl-cell--12-col">
                <button id="vote-button"
                        class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored">投票
                </button>
            </div>

            <div class="mdl-cell mdl-cell--12-col">
                <textarea id="response" disabled></textarea>
            </div>

        </div>
    </main>
</div>
</body>
</html>
common.css

.font-red {
    color: red;
    font-weight: bold;
    font-size: 16px;
}
vote.js

$(function () {
    $("#response").html("Response Values");

    /**
     * 投票ボタンを押したときの処理
     */
    $("#vote-button").click(function () {
        var url = 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/names';

        var employeeNumber = $(".employee-number").val();
        if (employeeNumber === "") {
            alert("社員番号は必須です");
            return;
        }

        /**
         * 選択肢の登録数のバリデーション
         */
        var voteGroup = {
            group1: $(".vote-group1").val(),
            group2: $(".vote-group2").val(),
            group3: $(".vote-group3").val()
        };

        var count = 0;
        $.each(voteGroup, function (key, val) {
            if (val !== "") {
                count++;
            }
        });
        if (count < 3) {
            alert("グループを3つ選択してください");
            return
        }

        /**
         * 重複チェック
         */
        if(voteGroup.group1 === voteGroup.group2){
            alert("同じクループは選択できません");
            return
        }
        if(voteGroup.group1 === voteGroup.group3){
            alert("同じクループは選択できません");
            return
        }
        if(voteGroup.group2 === voteGroup.group3){
            alert("同じクループは選択できません");
            return
        }

        var JsonData = {
            employeeNumber: employeeNumber,
            voteGroup1: voteGroup.group1,
            voteGroup2: voteGroup.group2,
            voteGroup3: voteGroup.group3
        };

        alert(JSON.stringify(JsonData));

        $.ajax({
            type: 'post',
            url: url,
            data: JSON.stringify(JsonData),
            contentType: 'application/JSON',
            dataType: 'JSON',
            scriptCharset: 'utf-8',
            success: function (data) {
                window.location.href = "thankYou.html";
            },
            error: function (data) {

                // Error
                alert("error");
                alert(JSON.stringify(data));
                $("#response").html(JSON.stringify(data));
            }
        });
    });

});

これらのファイルをWeb公開したS3バケットにアップロードし、S3のエンドポイントにアクセスし画面を確認します
あとはフォームの項目を入力し、「投票ボタン」をクリックすると・・・
無事成功すればDynamoDBにPOSTしたJSONが追加されています
また要望により、同じ社員番号で投票すると内容を更新できるようにしています

さいごに

いかんせん1週間とはいえ通常業務の合間を縫ってやっていたので、かなり雑な処理になっていると思います(汗)
しかも最後のCSV吐き出しはコンソールから生成するというなんともいけてない感じ・・・
次回いつ使うかわからないけど、改善に励んでいこうとおもっております!

ちなみにServerlessFrameworkで作成した環境を消去するときは下記を実行すればよいみたいです


sls remove

もっといい感じになったらまた記事書き直すんだ・・・

続きを読む

[EC2] SSH 接続時のログインメッセージにインスタンスタイプとかリージョンとか表示する

対象は Amazon Linux で動いてる EC2。
SSH接続して作業するときに、このインスタンスってどのインスタンスタイプで動いてたっけ?って知りたいときとかに便利。

こんな感じのシェルスクリプトを /etc/update-motd.d/40-instance-meta とかって名前で保存して実行権限与えておいてください。

/etc/update-motd.d/40-instance-meta
#!/bin/sh
instance_type=$(curl -s http://169.254.169.254/latest/meta-data/instance-type/)
az=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone)
region=$(echo ${az} | sed -e 's/[a-z]$//g')

cat << EOF
 INSTANCE TYPE : ${instance_type}
 REGION : ${region} (${az})

EOF

そのあとで update-motd コマンドを root 権限で実行すると、次回 SSH ログイン時からログインメッセージ変わります。あら便利

Last login: Wed Mar 15 15:05:40 2017 from xxx.xxx.xxx.xxx
      ___         _            __
     / _ | __ _  (_)_ _  ___  / /____
    / __ |/  ' / /  ' / _ / __/ _ 
   /_/ |_/_/_/_/_/_/_/_/___/__/___/

https://aws.amazon.com/amazon-linux-ami/2016.09-release-notes/

 Nginx 1.11.10, PHP 7.1.2, Percona MySQL 5.6.35, WP-CLI 1.1.0

 amimoto     https://www.amimoto-ami.com/
 digitalcube https://en.digitalcube.jp/

 INSTANCE TYPE : t2.small
 REGION : ap-northeast-1 (ap-northeast-1c)

続きを読む

「AWS IoTのデータをKibanaに表示する」をやってみた。2017/3版

AWS IoTのデータをKibanaに表示するを実際にやってみました。

記事の投稿以降、AWS IoTが直接Elasticsearch Serviceと連携できる様になったとのことで、そのあたりも試しました。

【アップデート】 AWS IoT が Elasticsearch Service と CloudWatch に連携できるようになりました
https://aws.amazon.com/jp/blogs/news/aws-iot-update/

なんと、サーバー側はノンコーディングです。

Elasticsearchインスタンスを立ち上げる

元記事と基本的に同じです。
まずは、立ち上がりが遅いElasticsearchを先に立ち上げておきましょう。
Domain nameを plant-sensor、Instance typeはt2.micro.elasticsearchが選べなかったのでt2.small.elasticsearch (Free tire eligible)、Instance countは 1、Strage typeは EBS に設定します。

image

Kibana のURLをクリックするとKibana4の画面が表示されます。EndpointのURLはAWS IoTで使います(自動で設定されます)。

AWS IoTでThingやRuleを作成

元記事の通りしようとしても、画面構成が違うのか、メニューが見つかりません。なので、順を追って解説します。

image
(こんな感じでしたっけ?)

「Get started」を押します。そして、左メニューの[Registory]->[Things]を選択します。
「Register a thing」を押します。

image

plant-sensor という名前のThingを作成。

image

左メニューの[Security]を選び、「Create certificate」を選択します。

image

色々ダウンロードできますので、一旦すべてダウンロードした後、「Activate」を押します。

ダウンロードできるもの

  • A certificate for this thing
  • A public key
  • A private key
  • A root CA for AWS IoT

続いてPolicyを作成。トップ画面の左メニューの[Policies]を選び、「Create a Policy」を選択します。

image

名前をつけ、とりあえずActionはiot:*としました。Resource ARNについてですが、キャプチャではtopic/replaceWithATopicとなってますが、plant/sensorsまたは*/*などにに変えましょう。

image
image

certificateとPolicyの紐付け
左メニューの[Certificates]を再度選び、先程作成したceritificateを選択。「Actions」メニューの「Attatch policy」を選ぶ。

image

同様に、「Attatch thing」も行います。

最後にRuleを作成します。トップ画面の左メニューの[Rules]を選び、「Create a rule」を選択します。

image

今回は plant/sensors というトピック名でデータを飛ばそうと思うので、Topic filterに plant/sensors を設定します。

image

image

Actionの指定は、「Add Action」ボタンを押して行います。

image

2017/3/14時点で以下のActionが選べます。一番下の「Elasticsearch Service」を選びます。

image
image

Elasticsearch Service用の設定画面が出ます。
IDに${newuuid()}、Indexにtimestamp、Typeにtimestampと指定し、IAMロールも追加します。

image

これで、Thing, Certificate, Policy, Ruleが作成できました。元記事のように一覧では表示されないようです。

仮想的なIoTデバイスを作成

元記事の通り、plant-sensor.jsを作成します。
そして実行です。

実行してみましょう。

$ npm init
$ npm install --save aws-iot-device-sdk
$ node plant-sensor.js
connect
{"timestamp":"2017-03-14T15:19:47.401Z","humidity":45,"temperature":19,"lux":32701,"moisture":309}
{"timestamp":"2017-03-14T15:19:48.405Z","humidity":43,"temperature":19,"lux":33473,"moisture":309}
{"timestamp":"2017-03-14T15:19:49.406Z","humidity":44,"temperature":19,"lux":30713,"moisture":295}
{"timestamp":"2017-03-14T15:19:50.408Z","humidity":42,"temperature":20,"lux":31499,"moisture":296}
{"timestamp":"2017-03-14T15:19:51.414Z","humidity":46,"temperature":20,"lux":30687,"moisture":315}
{"timestamp":"2017-03-14T15:19:52.417Z","humidity":46,"temperature":20,"lux":31960,"moisture":302}
{"timestamp":"2017-03-14T15:19:53.420Z","humidity":45,"temperature":20,"lux":30782,"moisture":301}

Kibanaでダッシュボードを作成する

Kibanaを起動すると、エラー画面のような形となりますが、初期設定ができていないからだと思います。

image

Index name or patternのところにtimestampといれると、設定できます。

image

Discoverタブを見てみると、ちゃんとデータが来ているのを確認できます。

image

では、チャートを作ってみましょう。Visualizeタブで、以下の様な感じでグラフを作成します。

image

最後にダッシュボードを作成します。Dashboardタブを選択して、Add visualization で先ほど作成したチャートをポンポンと選択していくだけです!

image

終わりに

元記事を作成された、@hideyuki さん、勝手に更新版を投稿してしまいました。ありがとうございます。

続きを読む

HugoとGithub Pagesでブログ開設。新規投稿時の画像を引っ張ってくるシェルスクリプトを書いてみた。

本記事は、http://jkkitakita.com の投稿の転記記事です。

1. 本記事のゴール

  1. HugoとGithub Pagesを使って、ブログを開設する

    1. http://jkkitakita.com/
  2. 新規投稿時のシェルスクリプトを考える。

2. 前提

  1. 私は、新卒二年目(もうすぐ、三年目)
  2. インフラエンジニアをやった後、現在、スクラムマスター。
  3. なので、そこまで、技術的な知識はない。

3. はじめていきましょう!

  1. HugoとGithub Pagesを使って、ブログを開設する

Hugo

hugo.png

Github Pages

https://www.youtube.com/watch?v=2MsN8gpT6jY

もうここは
色々な方々がブログでまとめてくださっているので問題ないかなと。
yewtonさんの記事が参考になりそうです。
(参考にさせていただきました。ありがとうございます。)
https://www.yewton.net/2016/02/02/blog-with-hugo/

とりあえず、私のHugo + Github Pagesの構成は
こちら

残課題として、ドメイン周りとか整理する必要があるかも?
誰かアドバイスがあれば、お願いいたします。。。

~ ❯❯❯ dig jkkitakita.com

; <<>> DiG 9.8.3-P1 <<>> jkkitakita.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2013
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 4, ADDITIONAL: 8

;; QUESTION SECTION:
;jkkitakita.com.            IN  A

;; ANSWER SECTION:
jkkitakita.com.     300 IN  A   192.30.252.153
jkkitakita.com.     300 IN  A   192.30.252.154

;; AUTHORITY SECTION:
jkkitakita.com.     93078   IN  NS  ns-1622.awsdns-10.co.uk.
jkkitakita.com.     93078   IN  NS  ns-1310.awsdns-35.org.
jkkitakita.com.     93078   IN  NS  ns-708.awsdns-24.net.
jkkitakita.com.     93078   IN  NS  ns-191.awsdns-23.com.

;; ADDITIONAL SECTION:
ns-191.awsdns-23.com.   40099   IN  AAAA    2600:9000:5300:bf00::1
ns-708.awsdns-24.net.   38549   IN  AAAA    2600:9000:5302:c400::1
ns-1310.awsdns-35.org.  59218   IN  AAAA    2600:9000:5305:1e00::1
ns-1622.awsdns-10.co.uk. 66592  IN  AAAA    2600:9000:5306:5600::1
ns-191.awsdns-23.com.   40099   IN  A   205.251.192.191
ns-708.awsdns-24.net.   38549   IN  A   205.251.194.196
ns-1310.awsdns-35.org.  59218   IN  A   205.251.197.30
ns-1622.awsdns-10.co.uk. 46709  IN  A   205.251.198.86

;; Query time: 16 msec
;; SERVER: 2400:2410:8be2:1d00:1111:1111:1111:1111#53(2400:2410:8be2:1d00:1111:1111:1111:1111)
;; WHEN: Sun Mar 12 23:57:59 2017
;; MSG SIZE  rcvd: 377

memo

  1. Hugo関連

    1. themeは、kakawaitさんのhugo-tranquilpeak-themeを使わせてもらった。
      https://themes.gohugo.io/hugo-tranquilpeak-theme/
    2. そのままだと、archivesページがうまく表示されなかったので、layout/taxonomy/archive.htmlを作成した。(themes/hugo-tranquilpeak-theme/layouts/taxonomy/archive.terms.htmlから複製)
    3. 日本語(ja)だとやっぱり色々だめかな。(ex.placeholderでないとか。)
      ↓は、にしておいた方が良さそう。

      1. languageCode = "en-us"
      2. defaultContentLanguage = "en-us"
  2. ドメイン関連

    1. 192.30.252.153192.30.252.154は、Github Pagesのドメイン
      https://help.github.com/articles/setting-up-an-apex-domain/
    2. ドメインは、お名前.comで管理
    3. DNS関連は、AWS Route53。(Aレコード)
  3. 残課題

    1. サブドメインの方が、Github Pagesとしては、良い?
      https://help.github.com/articles/about-supported-custom-domains/
    2. CDNの整備
    3. Hugoの知識不足。
    4. ネタ不足。笑

2.新規投稿時のシェルスクリプトを考える。

なんかただ作成するだけだと寂しいから
「綺麗な画像が欲しい!」と思って
無料画像的なのを引っ張ってくるスクリプトつくってみた。
(これダメだったら、誰か指摘してください。。笑)

ざっくりやったことの流れ

  1. pixabayにアカウント登録
    https://pixabay.com/ja/
  2. APIkey発行
  3. シェルの作成
    1. 記事作成(hugo new)
    2. curl で 画像を取得
    3. hugo用にワンライナーで整形
    4. sedで新規作成した記事へ挿入
  4. 完成
post.sh
#!bin/bash

num=`expr $RANDOM % 20`
DATE_TIME=`date '+%Y%m%d%H%M'`

hugo new post/$1.md
image=`curl 'https://pixabay.com/api/?key=${Key}&q=landscape&image_type=photo&pretty=true' | jq -r '.hits['$num'].webformatURL' | cut -c7-`

gsed -i -e "2i coverImage = \"$image\"" content/post/$DATE_TIME.md
gsed -i -e "2i thumbnailImage = \"$image\"" content/post/$DATE_TIME.md

memo

  1. ランダムで20個生成する感じになっているが、同じ画像が出ることがある。
  2. sedでうまくいかなかったので、gsedをinstallした。
    (参考)http://cross-black777.hatenablog.com/entry/2015/02/23/214337
  3. そもそもブログの画像、ライセンス、著作権の勉強しないとかなと思った。
    “(/へ\*)”))ウゥ、ヒック
  4. Hugoさんのlogoは、なんかいけそうだと思ったので、使わせてもらいました。

4. さいごに

Qiitaとかでは
ちょこちょこ投稿していましたが
自分で開設してみたいと思い、始めました。

ちょこちょここのサイト自体も
updateされるかなと思いますが
お気になさらずに。。。。笑

まぁ気楽に色々と書いていきたいと思いますので
宜しくお願いいたします😀

続きを読む