serverless frameworkを使ってデプロイ

serverless frameworkってなんなん

YAMLに設定を書いておくと、CLIでAWSのデプロイ/設定が行えます。
簡単に言うと、デプロイの自動化ができます。

環境

項目 version
node 6.10.2
serverless framework 1.19.0

インストール

serverless frameworkをglobalにインストール。

npm install -g serverless

config credentials

serverless framework docs AWS – Config Credentials

プロジェクト作成

serverless frameworkのコマンドを使用してプロジェクトを作成します。

mkdir serverless-sample
serverless create -t aws-nodejs

以下内容のファイルが作成されます。試してみたら見れますがw

handler.js
'use strict';

module.exports.hello = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};
serverless.yml
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: serverless-sample

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"

provider:
  name: aws
  runtime: nodejs6.10

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# you can add statements to the Lambda function's IAM Role here
#  iamRoleStatements:
#    - Effect: "Allow"
#      Action:
#        - "s3:ListBucket"
#      Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#    - Effect: "Allow"
#      Action:
#        - "s3:PutObject"
#      Resource:
#        Fn::Join:
#          - ""
#          - - "arn:aws:s3:::"
#            - "Ref" : "ServerlessDeploymentBucket"
#            - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

# you can add packaging information here
#package:
#  include:
#    - include-me.js
#    - include-me-dir/**
#  exclude:
#    - exclude-me.js
#    - exclude-me-dir/**

functions:
  hello:
    handler: handler.hello

#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - http:
#          path: users/create
#          method: get
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"

region設定

regionの設定を追記します。

serverless.yml
--- 省略 ---
provider:
  name: aws
  runtime: nodejs6.10
  region: ap-northeast-1
--- 省略 ---

API Gateway設定

上記ロジックを呼ぶエンドポイントの設定を追記。

serverless.yml
--- 省略 ---
functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: serverless-sample
          method: get
--- 省略 ---

デプロイ

serverless frameworkのコマンドを使用してデプロイします。
serverless.ymlに記載の内容でデプロイされます。

serverless deploy

参考

serverless

続きを読む

AnsibleでAWS操作 CloudFrontディストリビューション編

AnsibleでAWS操作シリーズ

  1. aws-cliインストール編
  2. EC2インスタンス編
  3. S3バケット編
  4. CloudFrontディストリビューション編

関連記事

aws-cli コマンド一覧(随時追記)

やりたかったこと

  • CloudFrontディストリビューションの作成
  • S3バケットとの連携
  • GUIを使わずに黒い画面でコマンドを「ッターーン!」してかっこつけたい

やったこと

前提

  • CIサーバー(ansible実行サーバー)構築済み
  • CLIサーバー(aws-cli実行サーバー)構築済み
  • Ansibleインストール済み
  • aws-cliインストール済み
  • 各サーバーへのSSH接続設定済み

${~}は各環境に合わせて値を設定してください。

作業フロー

1. CloudFrontディストリビューションを新規作成

command
ansible-playbook -i inventory/production create-aws-cf-distribution.yml

ディレクトリ構成


├── ansible.cfg
├── create-aws-cf-distribution.yml
├── files
│   └── production
│       └── cf
│           └── distribution.json
├── inventory
│   └── production
│       └── inventory
├── roles
│   └── create-aws-cf-distribution
│       └── tasks
│           └── main.yml
└── vars
    └── all.yml

Ansible構成ファイル

inventory

inventory/production/inventory
[ciservers]
${CIサーバーホスト}

[cliservers]
${CLIサーバーホスト}

[all:vars]
ENV=production

vars

vars/all.yml
AWS:
  S3:
    BUCKET:
      NAME: ${バケット名}

files

files/production/cf/distribution.json
{
  "Id": "",
  "IfMatch": "",
  "DistributionConfig": {
    "Comment": "For S3 Bucket.",
    "CacheBehaviors": {
      "Quantity": 0
    },
    "IsIPV6Enabled": true,
    "Logging": {
      "Bucket": "",
      "Prefix": "",
      "Enabled": false,
      "IncludeCookies": false
    },
    "WebACLId": "",
    "Origins": {
      "Items": [
        {
          "S3OriginConfig": {
            "OriginAccessIdentity": ""
          },
          "OriginPath": "",
          "CustomHeaders": {
            "Quantity": 0
          },
          "Id": "${一意となるID ※1}",
          "DomainName": "${S3バケットの静的サイトホスティングのエンドポイント}"
        }
      ],
      "Quantity": 1
    },
    "DefaultRootObject": "index.html",
    "PriceClass": "PriceClass_All",
    "Enabled": true,
    "DefaultCacheBehavior": {
      "TrustedSigners": {
        "Enabled": false,
        "Quantity": 0
      },
      "LambdaFunctionAssociations": {
        "Quantity": 0
      },
      "TargetOriginId": "${※1で指定したID}",
      "ViewerProtocolPolicy": "allow-all",
      "ForwardedValues": {
        "Headers": {
          "Quantity": 0
        },
        "Cookies": {
          "Forward": "all"
        },
        "QueryStringCacheKeys": {
          "Quantity": 0
        },
        "QueryString": true
      },
      "MaxTTL": ${最大有効TTL},
      "SmoothStreaming": false,
      "DefaultTTL": ${デフォルトTTL},
      "AllowedMethods": {
        "Items": [
          "HEAD",
          "GET"
        ],
        "CachedMethods": {
          "Items": [
            "HEAD",
            "GET"
          ],
          "Quantity": 2
        },
        "Quantity": 2
      },
      "MinTTL": ${最小TTL},
      "Compress": false
    },
    "CallerReference": "${一意となる文字列}",
    "ViewerCertificate": {
      "CloudFrontDefaultCertificate": true,
      "MinimumProtocolVersion": "SSLv3",
      "CertificateSource": "cloudfront"
    },
    "CustomErrorResponses": {
      "Quantity": 0
    },
    "HttpVersion": "http2",
    "Restrictions": {
      "GeoRestriction": {
        "RestrictionType": "none",
        "Quantity": 0
      }
    },
    "Aliases": {
      "Items": [
        "${割り当てたいドメイン ※Route53にて紐付ける際に利用}"
      ],
      "Quantity": 1
    }
  }
}

playbook

create-aws-cf-distribution
- hosts: cliservers
  roles:
    - create-aws-cf-distribution
  vars_files:
    - vars/all.yml

tasks

role/create-aws-cf-distribution/tasks/main.yml
- name: Create Distribution
  shell: |
    aws cloudfront create-distribution \
    --cli-input-json file://files/{{ ENV }}/cf/distribution.json
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

終わりに

これで、S3バケットをCloudFront経由で表示することが可能になります。
キャッシュを有効利用出来るので、 表示スピードが上がりストレスフリー なページを構築出来ます。

設定用のJsonが少し複雑なので、本記事のJsonをベースに調べながらカスタマイズしてもらえればなと思います。

ただ、このままだとCloudFrontのTTLが効いているのでオブジェクトを更新しても 即時反映 がされません。
また、URLについても、 xxxx.cloudfront.net のようなドメインになっています。

前者については、AWS LambdaによってオブジェクトがPutされたことをトリガーにInvalidationをかけることで解決可能です。(毎回手動でやるのは 運用コスト がかかるので自動化をお勧めします)

後者については、Route53を設定することで独自ドメインによるアクセスが可能になります。

上記の作業についても、aws-cliからの動作確認は済んでいるので時間があるときに記事にまとめようと思っています♪

じゃあの。

続きを読む

AnsibleでAWS操作 S3バケット編

AnsibleでAWS操作シリーズ

  1. aws-cliインストール編
  2. EC2インスタンス編
  3. S3バケット編
  4. CloudFrontディストリビューション編

関連記事

aws-cli コマンド一覧(随時追記)

やりたかったこと

  • S3バケットの作成/削除
  • S3バケットへのオブジェクトの転送/削除
  • S3バケットを静的Webサイトホスティング化
  • GUIを使わずに黒い画面でコマンドを「ッターーン!」してかっこつけたい

やったこと

前提

  • CIサーバー(ansible実行サーバー)構築済み
  • CLIサーバー(aws-cli実行サーバー)構築済み
  • Ansibleインストール済み
  • aws-cliインストール済み
  • 各サーバーへのSSH接続設定済み

${~}は各環境に合わせて値を設定してください。

作業フロー

1. S3バケットを新規作成

command
ansible-playbook -i inventory/production create-aws-s3-bucket.yml

2. S3バケットをWebサイトホスティング化

command
ansible-playbook -i inventory/production setup-aws-s3-bucket.yml

3. オブジェクトを転送

command
ansible-playbook -i inventory/production upload-aws-s3-object.yml

4. オブジェクトの転送確認

command
ansible-playbook -i inventory/production view-aws-s3-object.yml

5. オブジェクトの削除

command
ansible-playbook -i inventory/production delete-aws-s3-object.yml

6. バケットの削除

command
ansible-playbook -i inventory/production delete-aws-s3-bucket.yml

ディレクトリ構成


├── ansible.cfg
├── create-aws-s3-bucket.yml
├── delete-aws-s3-bucket.yml
├── delete-aws-s3-object.yml
├── files
│   └── production
│       └── s3
│           └── sample.txt
├── inventory
│   └── production
│       └── inventory
├── roles
│   ├── create-aws-s3-bucket
│   │   └── tasks
│   │       └── main.yml
│   ├── delete-aws-s3-bucket.yml
│   │   └── tasks
│   │       └── main.yml
│   ├── delete-aws-s3-object.yml
│   │   └── tasks
│   │       └── main.yml
│   ├── setup-aws-s3-bucket.yml
│   │   └── tasks
│   │       └── main.yml
│   ├── upload-aws-s3-bucket.yml
│   │   └── tasks
│   │       └── main.yml
│   └── view-aws-s3-object.yml
│       └── tasks
│           └── main.yml
├── upload-aws-s3-object.yml
├── setup-aws-s3-bucket.yml
├── vars
│   └── all.yml
└── view-aws-s3-object.yml

Ansible構成ファイル

inventory

inventory/production/inventory
[ciservers]
${CIサーバーホスト}

[cliservers]
${CLIサーバーホスト}

[all:vars]
ENV=production

vars

vars/all.yml
AWS:
  S3:
    BUCKET:
      NAME: ${バケット名}

playbook

create-aws-s3-bucket
- hosts: cliservers
  roles:
    - create-aws-s3-bucket
  vars_files:
    - vars/all.yml
delete-aws-s3-bucket
- hosts: cliservers
  roles:
    - delete-aws-s3-bucket
  vars_files:
    - vars/all.yml
delete-aws-s3-object
- hosts: cliservers
  roles:
    - delete-aws-s3-object
  vars_files:
    - vars/all.yml
setup-aws-s3-bucket
- hosts: cliservers
  roles:
    - setup-aws-s3-bucekt
  vars_files:
    - vars/all.yml
upload-aws-s3-object
- hosts: cliservers
  roles:
    - update-aws-s3-bucket
  vars_files:
    - vars/all.yml
view-aws-s3-object
- hosts: cliservers
  roles:
    - view-aws-s3-object
  vars_files:
    - vars/all.yml

tasks

role/create-aws-s3-bucket/tasks/main.yml
- name: Create Bucket
  shell: "aws s3 mb s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/delete-aws-s3-bucket/tasks/main.yml
- name: "Remove Bucket"
  shell: "aws s3 rb s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/delete-aws-s3-bucket/tasks/main.yml
- name: "Delete Bucket"
  shell: "aws s3 rb s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/delete-aws-s3-object/tasks/main.yml
- name: "Delete Object"
  shell: "aws s3 rm s3://{{ AWS.S3.BUCKET.NAME }}/sample.txt"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/setup-aws-s3-bucket/tasks/main.yml
- name: "Setup Bucket"
  shell: |
    aws s3 website s3://{{ AWS.S3.BUCKET.NAME }} \
    --index-document index.html
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/upload-aws-s3-object/tasks/main.yml
- name: "Upload Object"
  shell: "aws s3 cp files/{{ ENV }}/s3/sample.txt s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always
role/view-aws-s3-object/tasks/main.yml
- name: "View Objects"
  shell: "aws s3 ls s3://{{ AWS.S3.BUCKET.NAME }}"
  register: result
  changed_when: False

- debug: var=result.stdout_lines
  when: result | success
  tags:
    - always

終わりに

S3のバケット作成やファイル操作も 簡単 に行うことが可能で、 静的サイト として利用する場合のためのindexドキュメントの指定などのオプションもあります。

また、今回は触れませんでしたが転送したファイル単位で パーミッション の設定も出来るので柔軟性もあります。

また、CloudFrontと組み合わせればキャッシュを有効利用してパフォーマンス向上を図ることも可能です。

S3バケットは 静的コンテンツのホスティング簡易ファイルサーバーWordpressの記事をhtml化 したりとかなり汎用性が高く、 料金もかなり良心的 なのでどんどん利用していきましょう♪

じゃあの。

続きを読む

Running Kubernetes Cluster on AWS

Following this guide https://kubernetes.io/docs/getting-started-guides/kops/, set up Kubernetes cluster on AWS using kops.

1. Install kops

According to the guide, kops is:

kops helps you create, destroy, upgrade and maintain production-grade, hig… 続きを読む

ElasticseharchのEC2 Discovery Pluginでクラスタ組むのが捗るはなし

こんばんはー

今回、ElasticsearchのクラスタをAWS上に組む際に便利なブラグインを使ってみたいと思いますー
その名も「EC2 Discovery Plugin」☆彡

調べているとAWS Cloud Pluginの使い方を書いている人は結構いるのですが、5系から提供されているEC2 Discovery Pluginはあんまいないのかなーって思いました。
なので、EC2 Discovery Pluginについてつらつら書きたいと思います( ゚Д゚)ゞビシッ

そもそもEC2 Discovery Pluginは何が便利なん?

Elasticsearchのデフォルトで用意されているZen Discoveryを利用してクラスタを組む際は、Unicastでホスト名やIPアドレス指定で組みます。
ただ、クラウド上の運用を考えるとUnicastはつらい。。
そこで、EC2 Discovery Pluginを使うとSecurityGroupを元にクラスタが組まれるのですー(d゚ω゚d)オゥイェー
Multicast Discovery pluginというのもあったのですが、5系から廃止されました

早速ですが、構成の説明したら実装方法を説明したいと思いますー

インスタンス構成

  • InstanceType:t2.medium
  • OS:Amazon Linux AMI release 2017.03
  • Node: 3台
  • ClusterName: es-cluster

こんな感じの流れで書いちゃいますー

  1. AWS準備

  2. Install Oracle JDK 1.8

  3. Install Elasticsearch

  4. Install Kibana(1台のみ)

  5. Install X-pack Elasticsearch & kibana(1台のみ)

  6. Install EC2 Discovery Plugin

  7. 動作確認

AWS準備

SecurityGroup

ElasticsearchをインストールするインスタンスにアタッチするSecurityGroupです。
ソースは、環境に合わせて設定してください。

Type Protocol Port Source Description
Custom TCP 9200 xx Elasticsearchアクセスポート
Custom TCP 9300 xx ノード間のコミュニケーションポート
Custom TCP 5601 xx KibanaのHTTP用ポート
Custom TCP 22 xx SSH用ポート

IAM Role

以下のPolicyを作成し、インスタンスにアタッチしているIAM RoleにPolicyをアタッチしてください。

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

AWS Configure

サーバにログインしたらAWS Configureの実行をします。
Access KeyとSecret keyは利用しないため、何も入力しません。

$ aws configure
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
Default region name [None]: ap-northeast-1
Default output format [None]: json

下準備が終わったので、ここからElasticsearch周りを進めたいと思いますー

Install Oracle JDK 1.8

$ java -version
java version "1.7.0_141"
OpenJDK Runtime Environment (amzn-2.6.10.1.73.amzn1-x86_64 u141-b02)
OpenJDK 64-Bit Server VM (build 24.141-b02, mixed mode)

### Install Java1.8
$ sudo yum -y install java-1.8.0-openjdk-devel

### alternativesコマンドでJavaのバージョンを切り替える
$ sudo alternatives --config java

There are 2 programs which provide 'java'.

  Selection    Command
-----------------------------------------------
*+ 1           /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java
   2           /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java

Enter to keep the current selection[+], or type selection number: 2

### バージョン確認
$ java -version
openjdk version "1.8.0_141"
OpenJDK Runtime Environment (build 1.8.0_141-b16)
OpenJDK 64-Bit Server VM (build 25.141-b16, mixed mode)

Install Elasticsearch

Elasticsearch5.5.1を入れちゃいたいと思いますー

Install Elasticsearch

### PGP Keyをダウンロード&インストール
$ sudo rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch

### リポジトリの登録
$ sudo vim /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

### Elasticsearchインストール
$ sudo yum -y install elasticsearch

### サービス起動登録
$ sudo chkconfig --add elasticsearch

### 確認
$ sudo chkconfig --list | grep elasticsearch
elasticsearch   0:off   1:off   2:on    3:on    4:on    5:on    6:off

### elasticsearch.yml設定変更
$ sudo vim /etc/elasticsearch/elasticsearch.yml
+ cluster.name: es-cluster
+ network.host: 0.0.0.0
+ http.port: 9200

### サービス起動
$ sudo service elasticsearch start
Starting elasticsearch:                                    [  OK  ]

### 起動確認
$ curl http://localhost:9200
{
  "name" : "fDpNQ4m",
  "cluster_name" : "es",
  "cluster_uuid" : "mayxoDENThSmrUltkXyRWg",
  "version" : {
    "number" : "5.5.1",
    "build_hash" : "19c13d0",
    "build_date" : "2017-07-18T20:44:24.823Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.0"
  },
  "tagline" : "You Know, for Search"
}

Install Kibana

モニタリングしたいのでKibana5.5.1をインストールします。

### Install Kibana
$ sudo yum install -y kibana
### サービス登録
$ sudo chkconfig --add kibana
$ chkconfig --list | grep kibana
kibana          0:off   1:off   2:on    3:on    4:on    5:on    6:off
### Kibana設定
$ sudo vim /etc/kibana/kibana.yml
+ server.host: 0.0.0.0
+ elasticsearch.url: "http://ES_IP_ADDR"
### サービス停止状態のままにしておきます(X-Pack入れたあとに設定変更後に起動します)
$ service kibana status
kibana is not running

Install X-Pack on Elasticsearch

Elasticsearchのノード状態などをモニタリングしたいため、X-Pack5.5をインストールします。

Install X-Pack on Elasticsearch

$ sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install x-pack
-> 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

動作確認するのですが、Securityが有効になったため、エラーが返ってきます。
なので、今回は、Securityを無効にします。
(モニタリング用途で使いたいだけなので―)

ちなみに、X-Packについては、クラスメソッドさんが詳しく書いてますー
クラスメソッド:Elastic Stack X-Pack

X-Pack Security無効化

無効化することで、レスポンスが返ってきます。

### Security無効化
$ vim /etc/elasticsearch/elasticsearch.yml
+ xpack.security.enabled: false

### 動作確認
$ curl -u elastic http://localhost:9200
{
  "name" : "fDpNQ4m",
  "cluster_name" : "es",
  "cluster_uuid" : "mayxoDENThSmrUltkXyRWg",
  "version" : {
    "number" : "5.5.1",
    "build_hash" : "19c13d0",
    "build_date" : "2017-07-18T20:44:24.823Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.0"
  },
  "tagline" : "You Know, for Search"
}

### 再起動
$ service kibana restart
kibana stopped.
kibana started

Install X-Pack on Kibana

KibanaのX-Pack5.5もインストールします。

Install X-Pack on Kibana

$ sudo /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.5.1.zip
Transferring 119276972 bytes....................
Transfer complete
Retrieving metadata from plugin archive
Extracting plugin archive
Extraction complete

Install EC2 Discovery Plugin

ここからEC2 Discovery Pluginをインストールします。
やっとここまできましたね!

$ sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install discovery-ec2
-> Downloading discovery-ec2 from elastic
[=================================================] 100%  
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@     WARNING: plugin requires additional permissions     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* java.lang.RuntimePermission accessDeclaredMembers
* java.lang.RuntimePermission getClassLoader
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
-> Installed discovery-ec2

Elasticsearch.ymlに設定します。

$ sudo vim /etc/elasticsearch/elasticsearch.yml
cluster.name: es-cluster
discovery.zen.hosts_provider: ec2
discovery.ec2.groups: "SECURITY_GROUP_NAME" or "SECURITY_GROUP_ID"
discovery.ec2.availability_zones: [ "ap-northeast-1a", "ap-northeast-1c" ]
cloud.aws.region: ap-northeast-1

### サービス再起動
service elasticsearch restart

### クラスタ状態確認
$ curl http://localhost:9200/_cluster/health?pretty
{
  "cluster_name" : "es-cluster",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 1,
  "number_of_data_nodes" : 1,
  "active_primary_shards" : 0,
  "active_shards" : 0,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 0,
  "delayed_unassigned_shards" : 0,
  "number_of_pending_tasks" : 0,
  "number_of_in_flight_fetch" : 0,
  "task_max_waiting_in_queue_millis" : 0,
  "active_shards_percent_as_number" : 100.0
}

2台目&3台目いくよ!!

2台目も上記と同じで設定を実施しますので割愛しちゃいますm(_ _)m
SecurityGroupは、1台目と同じのを使ってください。
また、ElasticsearchのX-Packは、3台すべてに入れてください。
Kibanaは、インストールしないです。

もろもろ設定して、Elasticsearchを再起動するとログにクラスタが組み込まれたことがわかります。

クラスタに組み込まれているかをログから確認

### Master側のログ
[20xx-xx-xxxx:xx:xx][INFO ][o.e.c.s.ClusterService   ] [fDpNQ4m] added {{gdRR6r1}{gdRR6r17T5iaGPmaO9wgTA}{6l3QNArhSkyBVu08jTfwYQ}{IP_ADDR}{IP_ADDR:9300},}, reason: zen-disco-receive(from master [master {dKgmQfLLg}{IP_ADDR}{IP_ADDR:9300} committed version [15]])

### Node側のログでマスタを検出したことがわかる
$ tail -f /var/log/elasticsearch/es.log
[20xx-xx-xxxx:xx:xx][INFO ][o.e.c.s.ClusterService   ] [gdRR6r1] detected_master {dKgmQfL}{dKgmQfLASM-35x0X0ulSXg}{XE4jgXEzQwqgkSfRypgoLg}{IP_ADDR}{IP_ADDR:9300}, added {{fDpNQ4m}{fDpNQ4mrRR2vN7wiTVjfIg}{aqPbmbhbTkm1X40gH1tylw}{IP_ADDR}{IP_ADDR:9300},{dKgmQfL}{dKgmQfLASM-35x0X0ulSXg}{XE4jgXEzQwqgkSfRypgoLg}{IP_ADDR}{IP_ADDR:9300},}, reason: zen-disco-receive(from master [master {dKgmQfL}{dKgmQfLASM-35x0X0ulSXg}{XE4jgXEzQwqgkSfRypgoLg}{IP_ADDR}{IP_ADDR:9300} committed version [15]])

Kibanaのモニタリングで確認するよ

Kibana [http://KIBANA_IP_ADDR:5601] にアクセスします。

「Monitoring」をクリックし、ElasticsearchにNodeが3台あることがわかりますね。
ステータス:Green

kibana01.png

「Nodes」をクリックし、Nodeの状態を確認できます。

kibana02.png

無事にクラスタを組むことができましたヽ(*゚д゚)ノ

補足#01

AWS Configureの設定とIAM Roleにポリシーをアタッチしないと以下のエラーがでます。
ちなみに、作成したクラスタ名でログが作成されちゃいます。

$ sudo /var/log/elasticsearch/es-cluster.log
[20xx-xx-xxxx:xx:xx][INFO ][o.e.d.e.AwsEc2UnicastHostsProvider] [node_id] Exception while retrieving instance list from AWS API: Unable to execute HTTP request: connect timed out

補足#02

既に存在しているNodeIDのAMIから起動すると、NodeIDがかぶっているためクラスタに組み込むことができないです。。
その際のログが以下です。

### AMIから起動したノード
$ tail -f /var/log/elasticsearch/es.log
[20xx-xx-xxxx:xx:xx][INFO ][o.e.d.z.ZenDiscovery     ] [gdRR6r1] failed to send join request to master [{fDpNQ4m}{fDpNQ4mrRR2vN7wiTVjfIg}{eHfV5HLkRrKo8_FXfgyHDA}{IP_ADDR}{IP_ADDR:9300}{ml.enabled=true}], reason [RemoteTransportException[[fDpNQ4m][IP_ADDR:9300][internal:discovery/zen/join]]; nested: IllegalArgumentException[can't add node {gdRR6r1}{gdRR6r17T5iaGPmaO9wgTA}{hzzXQXB8TM-xQVn9Uj8e2A}{IP_ADDR}{IP_ADDR:9300}{ml.enabled=true}, found existing node {gdRR6r1}{gdRR6r17T5iaGPmaO9wgTA}{9pWjYpL5Tq23yIte7WzMiw}{IP_ADDR}{IP_ADDR:9300}{ml.enabled=true} with the same id but is a different node instance]; ]

さいごに

EC2 Discovery Pluginいかがでしたか?
簡単にクラスタを組むことができたんじゃないかなと思います。

ただ、個人的には、運用を考えると自動復旧できる構成にする必要があると思ってます。
今の状態だとElasticsearchのクラスタ化する時にNodeIDがかぶっちゃうので、AMIからの自動復旧がむずかしい状態です。
なので、Elasticsearchをインストールする前のAMIからプロビジョニングして、クラスタに組み込む必要があるかなと。
(Elasticsearchインストール時にNodeIDが振られる)

うーん。。NodeIDを変更する方法が他にあればいいのですが、誰か知っている人いたら教えてくださいm(_ _)m

てことで、今回は、ElasticsearchのプラグインのEC2 Discovery Pluginについて書かせて頂きました。
ありがとうございましたー

続きを読む

Cognito User Poolsのサインアップ・サインイン画面をiOSで試してみた

Coginit User Poolsにフェデレーション等の新機能が追加されましたが
新機能のサインアップ・サインイン画面をiOSから試してみました。
https://aws.amazon.com/jp/about-aws/whats-new/2017/08/amazon-cognito-launches-general-availability-of-a-built-in-customizable-user-experience-for-sign-in-oauth-2-0-support-and-federation-with-facebook-login-with-amazon-google-and-saml-providers-for-user-pools/

環境

  • xcode
  • aws sdk

作業概要

iOSからCognitoでサインアップ(サインイン)後、払い出されたCredentialの確認のためPollyに喋らせてみたいと思います。

  1. UserPoolを作成する
  2. Identity Poolを作成する
  3. xcodeでコードを書く
  4. Polly実行用にIAM Roleを設定する
  5. Simulatorで実行する

1. UserPoolを作成する

  1. AWSマネジメントコンソールにログインして、Cognito User Poolsを選択する
  2. Pool名(MyPool)を指定して、User Poolを作成する(全てデフォルト設定)
  3. App Clientを追加する
    1. [General settings]-[App Clients]を選択する
    2. Client名(MyApp)を指定して、Clientを追加する(全てデフォルト設定)
  4. App Integrationを設定する
    1. App client settingsを以下のように指定
      AppClientSettings.png
    2. Domain Nameを以下のように指定
      DomainName.png

attention.png

2. Identity Poolを作成する

  1. Federated Identitiesを選択し、User Pools画面から切替
  2. Identity Pool name(MyAppIdentity)とUser Pool IDとApp client idを指定してIdentity Poolを作成
    IdentityPool.png

3. xcodeでコードを書く

参考URL
https://github.com/awslabs/aws-sdk-ios-samples/tree/master/CognitoAuth-Sample
http://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-integrating-user-pools-with-identity-pools.html

Info.plist(抜粋)
<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>amazonaws.com</key>
            <dict>
                <key>NSThirdPartyExceptionMinimumTLSVersion</key>
                <string>TLSv1.0</string>
                <key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
            </dict>
            <key>amazonaws.com.cn</key>
            <dict>
                <key>NSThirdPartyExceptionMinimumTLSVersion</key>
                <string>TLSv1.0</string>
                <key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
            </dict>
        </dict>
    </dict>
    <key>AWS</key>
    <dict>
        <key>CognitoUserPool</key>
        <dict>
            <key>Default</key>
            <dict>
                <key>CognitoUserPoolAppClientId</key>
                <string>****************************</string>
                <key>CognitoUserPoolAppClientSecret</key>
                <string>****************************</string>
                <key>CognitoAuthWebDomain</key>
                <string>https://myapp.auth.us-east-1.amazoncognito.com</string>
                <key>CognitoAuthSignInRedirectUri</key>
                <string>myapp://top</string>
                <key>CognitoAuthSignOutRedirectUri</key>
                <string>myapp://top</string>
                <key>CognitoAuthScopes</key>
                <array>
                    <string>openid</string>
                </array>
            </dict>
        </dict>
    </dict>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>myapp</string>
            </array>
            <key>CFBundleURLName</key>
            <string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
        </dict>
    </array>
AppDelegate.swift(抜粋)
    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        return AWSCognitoAuth.default().application(app, open: url, options: options)
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        let clientId = "App Client IDを指定"
        let clientSecret = "App Client Secretを指定"
        let poolId = "User Pool IDを指定"
        let identityPoolId = "Identity Pool IDを指定"

        let serviceConfiguration = AWSServiceConfiguration(
            region: .USEast1, // リージョンを指定(ここではバージニア)
            credentialsProvider: nil)

        let userPoolConfiguration = AWSCognitoIdentityUserPoolConfiguration(
            clientId: clientId,
            clientSecret: clientSecret,
            poolId: poolId)

        AWSCognitoIdentityUserPool.register(
            with: serviceConfiguration,
            userPoolConfiguration: userPoolConfiguration,
            forKey: "UserPool")

        let pool = AWSCognitoIdentityUserPool(forKey: "UserPool")

        let credentialsProvider = AWSCognitoCredentialsProvider(
            regionType: .USEast1, // リージョンを指定(ここではバージニア)
            identityPoolId: identityPoolId,
            identityProviderManager:pool)

        let configuration = AWSServiceConfiguration(
            region: .USEast1,  // リージョンを指定(ここではバージニア)
            credentialsProvider: credentialsProvider
        )
        AWSServiceManager.default().defaultServiceConfiguration = configuration

        return true
    }

ログインボタンとログアウトボタンを配置する

ViewController.swift
import UIKit
import AWSCognitoAuth
import AWSCognitoIdentityProvider
import AVFoundation
import AWSPolly

class ViewController: UIViewController, AWSCognitoAuthDelegate {
    var audioPlayer = AVPlayer()
    let cognitoAuth = AWSCognitoAuth.default()

    override func viewDidLoad() {
        super.viewDidLoad()
        cognitoAuth.delegate = self as AWSCognitoAuthDelegate
    }

    func getViewController() -> UIViewController {
        return self
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func LoginButtonAction(_ sender: Any) {
        cognitoAuth.getSession(self)  { (session:AWSCognitoAuthUserSession?, error:Error?) in
            if(error != nil) {
                print((error! as NSError).userInfo["error"] as? String)
            }else {
                let message = "<speak>Hello World!</speak>"
                self.talkmessage(message: message)
                print("login")
            }
        }
    }

    @IBAction func LogoutButtonAction(_ sender: Any) {
        cognitoAuth.signOut { (error:Error?) in
            if(error != nil) {
                print((error! as NSError).userInfo["error"] as? String)
            }else {
                print("logout")
            }
        }
    }

    func talkmessage(message:String) {
        let input = AWSPollySynthesizeSpeechURLBuilderRequest()

        input.textType = AWSPollyTextType.ssml
        input.text = message
        input.outputFormat = AWSPollyOutputFormat.mp3
        input.voiceId = AWSPollyVoiceId.mizuki

        let builder = AWSPollySynthesizeSpeechURLBuilder.default().getPreSignedURL(input)

        builder.continueOnSuccessWith(block: { (awsTask: AWSTask<NSURL>) -> Any? in
            let url = awsTask.result!
            self.audioPlayer.replaceCurrentItem(with: AVPlayerItem(url: url as URL))
            self.audioPlayer.play()

            return nil
        })
    }
}

4. Polly実行用にIAM Roleを設定する

  1. AWSマネジメントコンソールにログインして、IAM – Rolesを選択する
  2. 認証されたユーザー用ロール(ここではCognito_MyAppIdentityAuth_Role)に、AmazonPollyFullAccessポリシーをアタッチする

5. Simulatorで実行する

Loginボタンをクリックする
top.png

サインアップ画面とサインイン画面

  1. サインアップ画面
    signup.png

  2. パスワード入力中の画面
    signup_pass.png

  3. Verify画面
    verify.png

  4. サインイン画面
    signin.png

  5. サインイン画面(背景色変更・ロゴ表示)
    modify.png

雑感

今回の機能拡張で、Cognitoは一段と使いやすくなったと思います。

続きを読む

StepFunctionsで始める分散処理Lambda

プロローグ ~こんなことをやりたかった~

業務で実装しているシステムで、定期的にこんな処理をする必要がありました。

  1. DynamoDBからデバイスのIDの一覧を取得する
  2. 各IDについて、DynamoDBにアクセスして生データを取得し、サマライズして別のテーブルに格納

こんなことをLambdaにやらせてたのですが、一つのLambdaファンクションに丸投げしてたのが災いして、デバイスが増えてきたりすると非常に辛い感じになってきました。

ですので、これを機に今まで真面目に触らなかったStepFunctionsに手を出して見ました。

ちなみにまだ実験がてらちょっといじってみたぐらいですので、記事中のソースソースコードはかなりしょぼいです。

出来上がりはこちら

Step Functionsって何?

って方のために手短に説明しますと、 Lambda同士の連携をいい感じに管理するツール と思えばほぼ間違いないです。

スクリーンショット 2017-08-11 14.55.19.png

これはデフォルトで用意されているブループリントの図ですが、視覚的にワークフローを把握しながらLambdaファンクションを組み立てることができます。

また、あるLambdaファンクションの結果に応じて次の処理を切り替えるといったことも簡単にできるようになります。

実装編

大まかな流れ

今回はあくまでStepFunctionsの使用感を知るための実験が主目的なので、処理を簡略化します。

まずは以下のLambdaファンクションを用意します。

  1. LambdaA: 処理対象のIDの一覧を取得(実験なのでIDは適当な文字列をハードコーディング)
  2. LambdaB: 配列で受け取ったID一つ一つについて、LambdaCを非同期で並列にinvokeする
  3. LambdaC: 受け取ったIDをコンソールに出力

LambdaBからLambdaCへの受け渡しもStepFunctionsで行いたいところですが、同一のLambdaを不特定多数並列に立ち上げるのは辛そうなので、ここはLambda内でinvokeします。

よって処理の流れは以下のようになります。

  1. LambdaAでIDの一覧を取得(実験なのでIDは適当な文字列をハードコーディング)
  2. 特に意味はないが10秒ほどスリープする
  3. LambdaBはLambdaAから受け取ったIDの一覧を使い、一つのIDに対して一つのLambdaCを非同期でinvokeする
  4. LambdaCはLambdaBから受け取ったIDをコンソールに出力。処理が並列化されていることをわかりやすくするため、2秒ほどスリープしてからLambdaBに結果を返す
  5. 全てのLambdaCの処理が終わったらLambdaBも終了する

うーん、「Lambda」がゲシュタルト崩壊しそうですね
ではこれらをServerless Frameworkで実装しようかと思います

ここで注意事項

StepFunctionsで定義した一連の処理の流れをStateMachineと言いますが、 State Machineは一度作成すると編集できません。変更したいなら都度削除して作り直さなければいけません。

幸いにして2017年2月にCloudFormationがStepFunctionsをサポートしましたので、これを利用しましょう。

AWSコンソールのGUIで毎回入力し直すよりは楽かと思います。

各種設定ファイル作成

ではServerlessで実装するための設定ファイルを書いていきましょう。

serverless.yml

まずはおなじみのserverless.ymlです

serverless.yml

service: step-test

provider:
  name: aws
  runtime: nodejs6.10

# you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
  memorySize: 256
  timeOut: 30
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "lambda:InvokeFunction"
      Resource:
        - arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-parallel

custom:
  config:
    accountId: "your Account ID" # 自分のAWSアカウントのID
    stage: ${opt:stage, self:provider.stage}

functions:
  # LambdaA
  first:
    handler: handler.first
  # LambdaB  
  second:
    handler: handler.second
    environment:
      TARGET_LAMBDA_ARN: ${self:service}-${self:custom.config.stage}-parallel
  # LambdaC
  parallel:
    handler: handler.parallel
resources: ${file(./resources/state_machine.yml)}

できるだけ使い回せるように色々と変数を使ってます。
変数の基本的な使い方はここを見ていただくとして、いくつかポイントになるところを解説します。

  • ${file(./resources/state_machine.yml)}

    • 別のファイルから読み込みます。これの中身はCloudFormationテンプレートです(後述)
  • stage: ${opt:stage, self:provider.stage}
    • コマンドラインオプションで –stageが与えられればそれを、なければprovidor.stageの値(この場合は”dev”)をデフォルトで使用。Lambdaファンクションの名前に関わります。

あと大事な点として、LambdaBはinvokeするためにLambdaCの名前を知っておく必要があります。
今回は環境変数として設定するようにしてあります。

CloudFormationテンプレート

StateMachineをCloudFormationテンプレートで作成します。
こちらを参考に

resources/state_machine.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create Step Function StateMachine"
Resources:
  InvokeLambdaRole:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Sid: StepFunctionsAssumeRolePolicy
              Effect: Allow
              Principal:
                Service:
                  Fn::Join: [ ".", [ states, Ref: "AWS::Region", amazonaws, com ] ]
              Action: sts:AssumeRole
        Path: /
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
  TestStateMachine:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      RoleArn:
        Fn::GetAtt: [ InvokeLambdaRole, Arn ]
      DefinitionString: |-
        {
          "Comment": "An example of the Amazon States Language using wait states",
          "StartAt": "FirstState",
          "States": {
            "FirstState": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-first",
              "Next": "wait_using_seconds"
            },
            "wait_using_seconds": {
              "Type": "Wait",
              "Seconds": 10,
              "Next": "FinalState"
            },
            "FinalState": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-second",
              "End": true
            }
          }
        }
Outputs:
  StateMachineArn:
    Value:
      Ref: TestStateMachine
  StateMachineName:
    Value:
      Fn::GetAtt: [ TestStateMachine, Name ]


StateMachineの定義の部分を取り出すとこうなります

{
  "Comment": "An example of the Amazon States Language using wait states",
    "StartAt": "FirstState",
    "States": {
      "FirstState": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-first",
        "Next": "wait_using_seconds"
      },
      "wait_using_seconds": {
        "Type": "Wait",
        "Seconds": 10,
        "Next": "FinalState"
      },
      "FinalState": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-second",
        "End": true
      }
  }
}

これに関しては用意されているWaitStateのブループリントをほぼそのまま流用しています。

Lambdaファンクション実装

さて、Lambdaファンクション本体を実装していきますが、上述の通り実験用なのでかなり簡素です。

LambdaA

こいつの役割はIDの一覧を返すことですが、実験用なのでハードコーディングしたIDの配列を返します。
実用化のさいはAPIなりDBなりにアクセスして取得するようになるでしょう

functions/first.js
"use strict";

const firstFunction = (event, context, callback) => {
  console.log("Call First Function");
  callback(null, {
    ids: ["a", "b", "c", "d", "e", "f"]
  });
};

module.exports = firstFunction;

Node.jsで実装する場合は、callbackの第二引数に入れた値がStateMachineに受け渡されます。
他の言語の場合は異なる可能性がありますので、お使いの言語に合わせて実装しましょう。

LambdaB

LambdaBはIDの一覧を分解して、それぞれに対してLambdaCをinvokeします。
LambdaAの結果をどうやって受け取るのかが気になるところでしたが、StepFunctionsで連携させた場合、LambdaAでcallbackの第二引数に入れた値がそのままevent変数として渡されます。

functions/second.js
"use strict";

const AWS = require("aws-sdk");

const secondFunction = (event, context, callback) => {
  console.log("Call Second Function");
  console.log(event);
  const targetLambdaArn = process.env.TARGET_LAMBDA_ARN;

  if (!targetLambdaArn) {
    console.log("no target");
    return callback(null);
  }
  console.log(targetLambdaArn);
  const lambda = new AWS.Lambda();
  const ids = event.ids;

  Promise.all(ids.map((id) => {
    return lambda.invoke({
      FunctionName: targetLambdaArn,
      Payload: JSON.stringify({id: id})
    }).promise();
  })).then(() => {
    return callback(null);
  }).catch((err) => {
    return callback(err);
  });
};

module.exports = secondFunction;

ついでにNode.jsでのlambda.invoke()ですが、FunctionNameはARNでも関数名でもいいようです。基本的に関数名で問題ないでしょうが、より確実性を求めるならARNで指定するのもありでしょう。

LambdaC

LambdaCでは各IDに応じた処理を行います。
実際はそのIDを使ってDBにアクセスみたいな処理になると思いますが、今回は単純にコンソールに出力するだけです。
また、ちゃんと並列になってるかの確認もしたいので、現在のタイムスタンプを出力して2秒ほど待ってからcallbackします。
並列処理になっていれば各タイムスタンプはほぼ同じ時刻を示すはずです。

functions/parallel.js
"use strict";

const moment = require("moment");

const parallelFunction = (event, context, callback) => {
  console.log(event);
  console.log(`now: ${moment().format("X")}`);

  setTimeout(() => {
    callback(null);
  }, 2000)
};

module.exports = parallelFunction;

実行編

ではデプロイします。

sls deploy

うまくいっていればStepFunctionsのコンソールにStateMachineが追加されています。

スクリーンショット 2017-08-11 15.53.21.png

「New execution」をクリックして、最初のファンクションに与えるインプットを入力するとStateMachineが動き出します。

実行中はこんな感じで今どの処理をやってるか確認できます。
この図ですとFirstState(LambdaA)の処理が正常に終了し、Waiting中です。

スクリーンショット 2017-08-11 15.55.55.png

ログ確認

では懸案だったLambdaCの並列分散処理はうまくいっているのか、ログを見てみましょう。

スクリーンショット 2017-08-11 15.57.47.png

ほぼ同時にLogStreamが複数できているので、どうやら分散化はできているようです。

次にコンソールに出力したタイムスタンプを見てみます。
配列内で先頭と末尾であったaとfを比べてみましょう。

スクリーンショット 2017-08-11 16.02.08.png

スクリーンショット 2017-08-11 16.02.39.png

タイムスタンプはmoment().format("X")で出力しているので、秒単位のUNIXタイムスタンプです。
ログを見たところタイムスタンプは同じなので、1秒以内に両者は実行されていたことになります。2秒スリープを入れているのにほぼ同時に実行されているので、並列に実行されているとみなして良いかと思います。

エピローグ ~感想と今後の課題~

さて、初めてStepFunctionsを使ってみたわけですが、当初は「StepFunctionsの中にStateMachineがあって…え〜っと…よくわからん」な感じでしたが、いざ使ってみたらそんなに難しいものではありませんでした。

複雑な処理とかはできるだけLambdaを分割してStepFunctionsで連携させたいところですね。プロダクションにも取り入れるつもりです。

問題なのはStateMachineを実行する手段がコンソールから手動実行かAPIをコールするぐらいで、イベントをトリガーにできないこと。
イベントドリブンで色々やろうとしたら、まずはイベントをトリガーにLambdaを起動し、その中でStateMachineを動かすという手段を取らなければいけません。正直言ってこれはあまりイケてない。

今後の機能追加に期待ですね。

以上、これからStepFunctionsを使おうとしている方の参考になれば幸いです。

参考

続きを読む

JavaによるLambdaをCodepipelineで自動リリースする最小構成サンプル

TL;DR

以下の文書では、node.js でのLambda関数のリリースを自動化する例が出ています。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/automating-deployment.html

本文書では、これとほぼ同じようなアプリをJavaで書き、そのリリースを自動化します。それぞれの要素の意味や役割の説明は最低限に留めます。 (筆者もよくわかってません。えっへん。) 最小の動作サンプルの例示を行います。

本文書ではマネージメントコンソール上で色々手作業で設定を入れさせています。今後のUIの変更で、手順が変わっているかもしれません。ご容赦を。

前提

  • githubにコードが配置してあること。
  • 更新を検出するブランチが決めてあること。
  • Gradleでビルドを行います。
  • 中間ファイルを置くためのS3バケットがあること。例として、auto-release-sampleとします。

準備

ロールの作成

IAM マネージメントコンソールを開き、以下のようにロールを作成します。

項目 備考
名前 cloudformation-lambda-execution-role
ロールタイプ AWS CloudFormation 作成時は「AWS サービスロール」から選択する。作成後のロールの画面では、信頼関係 タブに表示される
アタッチされたポリシー AWSLambdaExecute ロールの画面では、アクセス許可 タブ=>管理ポリシー に表示される

一度ロールを作った後、作成したロールをマネージメントコンソールで開き、アクセス許可 タブ ->インラインポリシー にて、作成するには、ここをクリックしてください。をクリックする。  カスタムポリシー -> 選択 と進んだ後下記内容を記載する。ポリシー名は適宜設定する。

{
    "Statement": [
        {
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketVersioning"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::codepipeline*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "lambda:*"
            ],
            "Resource": [
                "*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "iam:GetRole",
                "iam:CreateRole",
                "iam:DeleteRole"
            ],
            "Resource": [
                "*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "iam:AttachRolePolicy",
                "iam:DetachRolePolicy"
            ],
            "Resource": [
                "*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "iam:PassRole"
            ],
            "Resource": [
                "*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "cloudformation:CreateChangeSet"
            ],
            "Resource": [
                "*"
            ],
            "Effect": "Allow"
        }
    ],
    "Version": "2012-10-17"
}

注:必要最低限の権限になってはいないかもです。今後の本記事の更新でなるべく削って行きたいです。

githubにファイルを配置する

アプリケーションのソースコードをgithubに保存します。

具体的なファイルの中身はサンプルリポジトリを参照してください。
https://github.com/kazurof/minimum-java-lambda-with-codepipeline
ファイルのディレクトリ構成を記載しておきます。

│  build.gradle
│  buildspec.yml
│  gradlew
│  gradlew.bat
│  minimum-lambda-java-model.yaml
│
│
├─gradle
│  └─wrapper
│          gradle-wrapper.jar
│          gradle-wrapper.properties
│
└─src
    └─main
        └─java
            └─codepipelinesample
                    Main.java

補足

buildspec.yml には、利用するS3リポジトリ名が記載されます。(先に、auto-release-sampleとしたもの。)実際に動かしてみる場合は適宜あなたが用意したリポジトリ名に変更してください。

CodePipeline の作成

AWS CodePipeline のマネージメントコンソールを開き、パイプラインを以下のように作成する。

パイプライン名

パイプライン名はわかればなんでも良い。

ソース

項目 備考
ソースプロバイダ GitHub GitHubを選択すると、GitHubリポジトリのURLとブランチを指定し、OAuth認証を行い、CodePipelineから指定したGitHubのリポジトリにアクセスできるようにする。
アクションカテゴリー ソース 作成時は入力を求められない。自動でつけられるものでOK
アクション名 Source 作成時は入力を求められない。自動でつけられる名前でOK
出力アーティファクト名 MyApp 作成時は入力を求められない。自動でつけられる名前でOK

ビルド

項目 備考
ビルドプロバイダ AWS CodeBuild
アクションカテゴリー ビルド 作成時は入力を求められない。
アクション名 CodeBuild 作成時は入力を求められない。

CodeBuildの新しいプロジェクトを作るを選択する。プロジェクト名はわかればなんでも良い。

ビルドプロジェクトの作成

項目
ビルド環境 Ubuntu Java8
ビルド仕様 ソースコードのルートディレクトリの buildspec.yml を使用

注:
– この時、AWS CodeBuild のサービスロールがユーザーに代わり自動的に作成されます。code-build-<ビルドプロジェクト名>-service-role という形になります。
– このビルドのアクションにおいて、以下項目が以下のように自動設定されます。

項目
入力アーティファクト MyApp
出力アーティファクト MyAppBuild

「ビルドプロジェクトの保存」を押下、「次のステップ」を押下してください。

デプロイ

項目 備考
デプロイプロバイダ AWS CloudFormation
アクションモード 変更セットの作成または置換
スタックの名前 MyBetaStack
変更セット名 MyChangeSet
テンプレートファイル packaged-minimum-lambda-java-model.yaml
Capabilities(特徴) CAPABILITY_IAM
ロール名 cloudformation-lambda-execution-role この作業手順の先頭で作ったロールを指定する。
アクションカテゴリー デプロイ 自動で設定される

AWS サービスロールの作成

ロールを新たに作ります。

当該画面の文言を転載:
「AWS CodePipeline がアカウントでリソースを使用するアクセス許可を付与するため、IAM にサービスロールを作成します。」

  • 「ロールの作成」を押下。
  • 「許可」を押下。
  • 「次のステップ」を押下。

ロール名は、「AWS-CodePipeline-Service」になるはずです。(すでにある場合はインラインポリシーに追加される挙動の様子。未確認です。)

パイプラインの確認

今まで入力した内容の確認画面が表示される。「パイプラインの作成」を押下します。

サービスロールを修正

Codebuild 向けに作成されたサービスロールが、用意したS3バケット(auto-release-sample)にアクセスできるように修正する。

今までの手順で、code-build-<ビルドプロジェクト名>-service-roleなるロールが生成されているのでそれを修正する。

  • IAM マネジメントコンソールから、ロール を選択する。
  • code-build-<ビルドプロジェクト名>-service-role を選択する。
  • アクセス許可=> インラインポリシー=> 「表示するインラインポリシーはありません。作成するには、ここをクリックしてください。」 をクリックする。
  • 「Policy Generator」 を選択して 「Select」 を選択する。
  • 「AWS Service」 で、「Amazon S3」 を選択する。
  • 「Actions」 で、「PutObject」 を選択する。
  • 「Amazon Resource Name (ARN)」 に arn:aws:s3:::auto-release-sample* と入力する。(末尾のアスタリスク必要!)
  • 「ステートメントを追加」 を選択して 「Next Step」 を選択する。
  • 「ポリシーの適用」 を選択する。

CodePipelineにLambdaを更新するステップを追加する

  • AWS CodePipeline マネジメントコンソールに移動 =>作成したpipelineを選択
  • 編集ボタンをクリック
  • Stagingの鉛筆アイコンをクリック
  • 既存のアクションの最後にある [+ Action] アイコンを選択する。
  • 以下のように入力
項目
アクションカテゴリー デプロイ
アクション名 execute_cs (わかればなんでも良い)
デプロイプロバイダ AWS CloudFormation
アクションモード 変更セットの実行
スタックの名前 MyBetaStack
変更セット名 MyChangeSet
  • 「更新」を押して保存
  • 「パイプラインの変更を保存」を押してパイプライン全体を保存

自動リリースとLambdaの実行

以上でCodepipeline 作成は終了です。gitリポジトリのmasterブランチに何か修正を入れてみてください。Codepipelineのマネジメントコンソールで、パイプラインが動作するところを確認できます。

正しく終了したら、Lambdaのマネージメントコンソールを開いてください。JavaによるLambda関数が作成されているはずです。テスト実行ができます。

感想

関係する要素技術多いせいか、手順長いですね。。。:sweat_smile:

続きを読む

[JAWS-UG CLI] Amazon Kinesis Firehose re:入門 (6) 関連リソースの削除

この記事について

JAWS-UG CLI専門支部 #90 Kinesis Firehose 復習編で実施するハンズオン用の手順書です。

前提条件

必要な権限

作業にあたっては、以下の権限を有したIAMユーザもしくはIAMロールを利用してください。

  • 以下のサービスに対するフルコントロール権限

    • Kinesis Firehose
    • IAM
    • EC2
    • S3
    • CloudWatch Logs
    • STS
    • (Lambda)
      • データの変換を行う場合
    • (KMS)
      • データの暗号化を行う場合

0. 準備

0.1. リージョンを指定

オレゴンリージョンで実施します。(東京マダー?)

コマンド
export AWS_DEFAULT_REGION="us-west-2"

0.2. 資格情報を確認

コマンド
aws configure list

インスタンスプロファイルを設定したEC2インスタンスでアクセスキーを設定せずに実行した場合、以下のようになります。

結果
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
access_key     ****************QSAA         iam-role
secret_key     ****************c1xY         iam-role
    region                us-west-2              env    AWS_DEFAULT_REGION

0.3. バージョン確認

コマンド
aws --version
結果
aws-cli/1.11.129 Python/2.7.12 Linux/4.9.38-16.33.amzn1.x86_64 botocore/1.5.92

0.4. バージョンアップ(必要に応じて)

コマンド
sudo pip install -U awscli

0.5. 変数の確認

コマンド
cat << ETX

    CF_STACK_NAME: ${CF_STACK_NAME}

ETX

1. Stackの削除

1.1. Stackの削除

Stack名の確認

コマンド
cat << ETX

    CF_STACK_NAME: ${CF_STACK_NAME}

ETX
結果

    CF_STACK_NAME: firehose-jawsug-cli

Stackの確認

コマンド
aws cloudformation describe-stacks 
    --stack-name ${CF_STACK_NAME}
結果
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-west-2:XXXXXXXXXXXX:stack/firehose-jawsug-cli/5d4aac10-7aba-11e7-bd3e-50a686be738e",
            "Description": "JAWS-UG CLI Kinesis Firehose Hands-on",
            "Parameters": [
                {
                    "ParameterValue": "XXXXXXXXXXXX_firehose_jawsug_cli",
                    "ParameterKey": "KeyPairName"
                },
                {
                    "ParameterValue": "10.0.0.0/16",
                    "ParameterKey": "VPCNetworkAddress"
                },
                {
                    "ParameterValue": "10.0.0.0/24",
                    "ParameterKey": "PublicSubnetAddr"
                }
            ],
            "Tags": [],
            "Outputs": [
                {
                    "OutputKey": "PublicIP",
                    "OutputValue": "34.209.250.207"
                },
                {
                    "OutputKey": "IAMRoleARN",
                    "OutputValue": "arn:aws:iam::XXXXXXXXXXXX:role/service-role-firehose"
                },
                {
                    "OutputKey": "S3BucketName",
                    "OutputValue": "firehose-jawsug-cli-s3bucket-1qtuo76w7zanq"
                }
            ],
            "CreationTime": "2017-08-06T15:17:30.587Z",
            "Capabilities": [
                "CAPABILITY_NAMED_IAM"
            ],
            "StackName": "firehose-jawsug-cli",
            "NotificationARNs": [],
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false
        }
    ]
}

Stackの削除

コマンド
aws cloudformation delete-stack 
    --stack-name ${CF_STACK_NAME}

Stackの削除を待機

コマンド
aws cloudformation wait stack-delete-complete 
    --stack-name ${CF_STACK_NAME}

【注意】スタックを完全に削除できない場合があります。原因を解消し再試行してください。

想定される原因として、以下の様なものが考えられます。

  • S3バケットにデータが残っている
  • IAMロールにCloudFormation以外の手段でポリシーをアタッチしている

Stackの確認

コマンド
aws cloudformation describe-stacks 
    --stack-name ${CF_STACK_NAME}
結果
An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id firehose-jawsug-cli does not exist

以上

続きを読む