Angular+Cognitoのユーザー認証付きSPAのサンプル

はじめに

Angularで作るSPAにCognito認証を組み込むサンプルを作りました。
ログイン・ログアウトだけではつまらないので、ログイン時に使えるS3ファイルアップロード機能も追加しています。
awslabsから公開されているaws-cognito-angular2-quickstartというサンプルが大変参考になりました。

作っていく

AWS側の準備

S3バケットの作成

Cognito Identity Pool に設定するロールの設定に、アクセス許可をするS3の情報を記載する必要があるので、事前にS3バケットを作成しておきます。
今回は、 angular-cognito-s3-file-uploader という名前のバケットを作成しました。

バケットポリシーと、CORSの設定は以下のとおりです。

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": [
                "s3:ListBucket",
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::angular-cognito-s3-file-uploader/*",
                "arn:aws:s3:::angular-cognito-s3-file-uploader"
            ]
        }
    ]
}
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
    <AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Cognito User Pool作成

AWS Consoleにログインし、Cognito -> 「ユーザープールの管理」-> 「ユーザープールを作成する」を選択します。

1.png

今回は作成するアプリケーション名と同様の名前でユーザープールを作成しました。「angular-cognito-s3-file-uploader」
「ユーザープールをどのように作成しますか?」では、「デフォルトを確認する」を選択しました。

内容を確認して、「プールの作成」をクリックします。

2.png

「アプリクライアント」-> 「アプリクライアントの追加」からアプリクライアントを追加します。今回は「angular-cognito-s3-file-uploader-client」という名前にしました。
次のように内容を入力して、「アプリクライアントの作成」をクリックします。

3.png

4.png

Cognito Identity Pool の作成

Cognito -> 「フェデレーテッドアイデンティティの管理」を選択します。
選択すると、「新しい ID プールの作成」の画面が表示されるので、次のように設定して、「プールの作成」をクリックします。
認証プロバイダーにはCognitoを選択して、先ほど作成したユーザープールIDとアプリクライアントIDを入力します。

5.png

割り当てるIAMロールを設定する画面が表示され、新規で作成するか既存のロールを割り当てるかを選択することが出来ます。今回は、次のようなIAMロールを新規で作成しました。
s3:ListBucket、s3DeleteObject、GetObject、PutObjectを新たに追加しています。Resourceには対象のS3バケットを指定します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*",
                "cognito-identity:*",
                "s3:ListBucket"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::angular-cognito-s3-file-uploader/",
                "arn:aws:s3:::angular-cognito-s3-file-uploader/*"
            ]
        }
    ]
}

ここまでの手順で作成したリソースの次の情報を今後の手順で使用します。

  • S3バケット名
  • Pool ID
  • アプリクライアント ID
  • IdentityPoolId

Angular側の実装

雛形作成

angular-cliを使って、アプリの雛形を作成していきます。

ng new angular-cognito-s3-file-uploader

必要なパッケージをインストールします。

$ npm i --save amazon-cognito-identity-js
$ npm i --save aws-sdk

必要なサービスを作成

  • cognito.service (cognito関連のサービス)
  • s3.service (s3操作関連のサービス)
$ ng g service service/cognito
$ ng g service service/s3

必要なコンポーネントを作成

  • signup.component (サインアップ画面)
  • login.component (ログイン画面)
  • upload.component (ファイルアップロード画面)
  • filelist.component (ファイルリスト表示画面)
$ ng g component signup
$ ng g component login
$ ng g component upload
$ ng g component files

プロジェクトの設定

type定義を追加
(aws-sdkを使うためにnodeを定義)

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

AWSのリソース情報を設定ファイルに記述します。

  • IdentityPoolId
  • Pool ID
  • アプリクライアント ID
  • S3バケット名
environment.ts
export const environment = {
  production: false,

  region: 'ap-northeast-1',

  identityPoolId: 'ap-northeast-1:XXXXXXXX-YYYY-XXXX-YYYY-XXXXXXXXXXXX',
  userPoolId: 'ap-northeast-1_XXXXXXXXX',
  clientId: 'YYYYYYYYYYYYYYYYYYYYYYYYYY',

  bucketName: 'angular-cognito-s3-file-uploader'
};

cognito.service

Cognito認証関連を行うサービスを実装します。
amazon-cognito-identity-jsと、aws-sdkからそれぞれ必要なモジュールをインポートします。
コンストラクタでは、AWS SDKのconfigにCognito関連の情報を設定します。

cognito.service.ts
import { Injectable } from '@angular/core';
import { environment } from './../../environments/environment';
import {
  CognitoUserPool,
  CognitoUserAttribute,
  CognitoUser,
  AuthenticationDetails,
  CognitoIdentityServiceProvider } from 'amazon-cognito-identity-js';
import * as AWS from 'aws-sdk';

@Injectable()
export class CognitoService {
  private userPool: CognitoUserPool;
  private poolData: any;
  public cognitoCreds: AWS.CognitoIdentityCredentials;

  constructor() {
    AWS.config.region = environment.region;
    AWS.config.credentials = new AWS.CognitoIdentityCredentials({
      IdentityPoolId: environment.identityPoolId
    });
    this.poolData = {
      UserPoolId: environment.userPoolId,
      ClientId: environment.clientId
    };
    this.userPool = new CognitoUserPool(this.poolData);
  }
}
...

続きは、以後順を追って実装していきます。

s3.service

s3操作を行うサービスを実装します。
コンストラクタでは、AWS SDKのconfigにCognito関連の情報を設定します。
また、cognitoサービスをDIして、getCredentials() というメソッドから認証情報を受け取るようにしました。

s3.service.ts
import { Injectable } from '@angular/core';
import { environment } from './../../environments/environment';
import { CognitoUserPool, CognitoUserAttribute, CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
import * as AWS from 'aws-sdk';
import { CognitoService } from './cognito.service';

@Injectable()
export class S3Service {
  private s3: AWS.S3;

  constructor(
    private cognito: CognitoService
  ) {
    this.cognito.getCredentials().then((data) => {
      AWS.config.credentials = data;
    }).catch((err) => {
      console.log(err);
    });
    const clientParams: any = {
      region: environment.region,
      apiVersion: '2006-03-01',
      params: { Bucket: environment.bucketName }
    };
    this.s3 = new AWS.S3(clientParams);
  }
}
...

こちらも続きは、以後順を追って実装していきます。

signup.compnent

次にサインアップページを作成します。
まずは、cognito.service認証関係のメソッドを実装していきます。

signUpメソッドは、formから受け取ったメールアドレスとパスワードでをCognitoUserPoolに登録します。
戻り値として実行結果をPromiseで返します。
正常に実行された場合、入力したメールアドレスに次のような確認コードが送信されます。

cognito.service.ts
  signUp(username: string, password: string): Promise<any> {
    const dataEmail = { Name: 'email', Value: username };
    const attributeList = [];
    attributeList.push(new CognitoUserAttribute(dataEmail));
    return new Promise((resolve, reject) => {
      this.userPool.signUp(username, password, attributeList, null, (err, result) => {
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      });
    });
  }

  confirmation(username: string, confirmation_code: string): Promise<any> {
    const userData = { Username: username, Pool: this.userPool };
    const cognitoUser = new CognitoUser(userData);
    return  new Promise((resolve, reject) => {
      cognitoUser.confirmRegistration(confirmation_code, true, (err, result) => {
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      });
    });
  }

属性を持つ配列attributeListには、必須属性となっているemailを設定しています。
参考にしたaws-cognito-angular2-quickstartでは、Signup画面と確認コード入力画面をroutringで別画面にしていますが、今回は同一の画面で画面遷移無しで確認コードの入力を求めるように作りました。
「あとで確認コードの入力する」といったことは考えていないので、必要であれば別途実装する必要があります。

6.png

confirmationメソッドは、前記の手順で受信した確認コードformから受取、アカウントの確認を行います。

ビュー側は、次のように至ってシンプルです。

siginup.component.html
<div class="signup" *ngIf="!successfullySignup">
  <form [formGroup]="signupForm" (ngSubmit)="onSubmitSignup(signupForm.value)">
    <label>Email: </label>
    <input type="email" formControlName="email">
    <label>Password: </label>
    <input type="password" formControlName="password">
    <button type="submit">Submit</button>
  </form>
</div>

<div class="confirmation" *ngIf="successfullySignup">
  <form [formGroup]="confirmationForm" (ngSubmit)="onSubmitConfirmation(confirmationForm.value)">
      <label>Email: </label>
      <input type="email" formControlName="email">
    <label>Confirmation Code: </label>
    <input type="text" formControlName="confirmationCode">
    <button type="submit">Confirm</button>
  </form>
</div>

先ほど実装した、signUpconfirmationメソッドを呼び出すビューモデル部分です。

siginup.component.ts
  onSubmitSignup(value: any) {
    const email = value.email, password = value.password;
    this.cognito.signUp(email, password)
    .then((result) => {
      console.log(result);
      this.successfullySignup = true;
    }).catch((err) => {
      console.log(err);
    });
  }

  onSubmitConfirmation(value: any) {
    const email = value.email, confirmationCode = value.confirmationCode;
    console.log(email);
    this.cognito.confirmation(email, confirmationCode)
    .then((result) => {
      return console.log(result) || this.router.navigate(['/login']);
    }).catch((err) => {
      console.log(err);
    });
  }

login.component

まずは、cognito.service認証関係のメソッドを実装していきます。

cognito.service.ts
  signUp(username: string, password: string): Promise<any> {
    const dataEmail = { Name: 'email', Value: username };
    const attributeList = [];
    attributeList.push(new CognitoUserAttribute(dataEmail));
    return new Promise((resolve, reject) => {
      this.userPool.signUp(username, password, attributeList, null, (err, result) => {
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      });
    });
  }

  confirmation(username: string, confirmation_code: string): Promise<any> {
    const userData = { Username: username, Pool: this.userPool };
    const cognitoUser = new CognitoUser(userData);
    return  new Promise((resolve, reject) => {
      cognitoUser.confirmRegistration(confirmation_code, true, (err, result) => {
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      });
    });
  }

upload.component

s3.serviceにファイルアップロードを行うメソッドを作成します。

s3.service.ts
  uploadFile(file: any): Promise<any> {
    const params = {
      Bucket: environment.bucketName,
      Key: file.name,
      ContentType: file.type,
      Body: file,
      StorageClass: 'STANDARD',
      ACL: 'private' };
    return this.s3.upload(params).promise();
  }

ファイルインプットが変更されたときに実行されるonInputChange、アップロードボタンが押されたときに実行されるonClickUpload、ログアウトを行うonClickLogoutを作成します。

upload.component.ts
  onInputChange(event: any) {
    const files = event.target.files;
    this.uploadFile = files[0];
  }

  onClickUpload() {
    if (this.uploadFile) {
    this.s3.uploadFile(this.uploadFile).then((data) => {
      if (data) {
        this.uploadResult = 'アップロードが完了しました。';
      }
    }).catch((err) => {
      console.log(err);
    });
    } else {
      this.uploadResult = 'ファイルが選択されていません。';
    }
  }

  onClickLogout() {
    this.cognito.logout();
    this.router.navigate(['/login']);
  }

ビュー側は次のような感じです。

upload.component.html
<div class="usermenu">
  <h2>ユーザーメニュー</h2>
  <button [routerLink]="['/files']">ファイル一覧</button>
  <button (click)="onClickLogout()">ログアウト</button>
</div>

<div class="fileupload">
  <h2>アップロード</h2>
  <input type="file" accept="*/*" (change)="onInputChange($event)">
  <button (click)="onClickUpload()">アップロード</button>
  <p *ngIf="uploadResult !== ''">アップロード結果: {{uploadResult}}</p>
</div>

filelist.component

s3.serviceにファイル一覧を取得するgetFileListとファイルをダウンロードするgetFileメソッドを作成します。ココらへんは、AWS SDKにPromiseを返すAPIが用意されているので、パラメータを渡して呼ぶだけです。

s3.service.ts
  getFileList(): Promise<AWS.S3.ListObjectsOutput> {
    const params = { Bucket: environment.bucketName };
    return this.s3.listObjects(params).promise();
  }

  getFile(key: string): Promise<AWS.S3.GetObjectOutput> {
    const params = { Bucket: environment.bucketName, Key: key };
    return this.s3.getObject(params).promise();
  }

ngOnInitでページが表示されるタイミングで、アップロード済みのファイル一覧を取得します。
ファイル一覧のファイル名をクリックした際に呼ばれるonClickFileメソッドでは、s3から取得したデータをhtml5リンクのdownload属性を使ってダウンロードさせます。

files.component.ts
  ngOnInit() {
    this.s3.getFileList().then((data) => {
      if (data) {
        this.remoteFiles = data.Contents;
      }
    }).catch((err) => {
      console.log(err);
    });
  }

  onClickFile(item: any) {
    this.s3.getFile(item.Key).then((data) => {
      const blob = new Blob([data.Body], { type: data.ContentType });
      const url = window.URL.createObjectURL(blob);
      const linkElement = document.createElement('a');
      linkElement.download = item.Key;
      linkElement.href = url;
      linkElement.click();
    }).catch((err) => {
      console.log(err);
    });
  }

ビュー側は次のような感じです。

files.component.html
<div class="usermenu">
  <h2>ユーザーメニュー</h2>
  <button [routerLink]="['/upload']">アップロード</button>
  <button (click)="onClickLogout()">ログアウト</button>
</div>

<div class="filelist">
  <h2>ファイル一覧</h2>
  <table class="filelist-table">
    <thead>
      <tr>
        <th>ファイル名</th>
        <th>更新日</th>
        <th>サイズ</th>
      </tr> 
    </thead>
    <tbody>
      <tr *ngFor="let item of remoteFiles">
        <td><a (click)="onClickFile(item)">{{item.Key}}</a></td>
        <td>{{item.LastModified}}</td>
        <td>{{item.Size}}</td>
      </tr>
    </tbody>
  </table>
</div>

ここまでで、必要な実装が完了しました✨

動作確認

起動

$ npm start

ブラウザから http:localhost:4200にアクセス

デモ

動画を見ても分かるように、ログイン後にページ遷移してもログイン状態が保たれていることが分かるかと思います。
また、ログアウト後に直URLでファイル一覧にアクセスした場合も、内容が表示されないことが分かるかと思います。

所感

今回はフロントエンドのフレームワークにAngularを使いましたが、AWS SDK自体はjavascript実装なので、他のフロントエンドのフレームワークとの組み合わせも簡単にできるかと思います。ただ、AngularCLIで作るDI可能なサービスはこの規模のでもアプリを作る際にもかなり重宝するので、Cognito+Angularで認証付きSPAを作るのはかなりオススメです🐣

参考

冒頭でも書いたaws-cognito-angular2-quickstartというサンプル
https://github.com/awslabs/aws-cognito-angular2-quickstart

amazon-cognito-identity-jsの詳細解説
http://tarepan.hatenablog.com/entry/cognito_UserPools_session_management

シンプルな実装でわかりやすかったです
https://qiita.com/jobbin/items/d2fc0f714eb1f1cfc965

続きを読む

DockerでRocket.chatを構築し、hubotも連携させる

備忘録のために載せておきます。
※Dockerやhubotは他に詳しい記事があるので触れません。

Rocket.chatって?

Slackライクなチャットツール。
https://rocket.chat/

一年ほど前、コミュニケーションツールとしてチャットをプロジェクトで導入する際に
サーバインストール型のチャットツールを探してこれに行きつきました。
周りで誰も使ったことがありませんでしたが、とりあえずお試しで入れたのが経緯。

なんだかんだで1年半ほどプロジェクト内で運用しています。

Dockerレシピ

rocketchat:
  image: rocketchat/rocket.chat:latest
  environment:
    - MONGO_URL=mongodb://mongodb/rocketchat
    - ROOT_URL=http://localhost:80
  links:
    - mongodb
  ports:
    - 80:3000

hubot:
  image: rocketchat/hubot-rocketchat
  environment:
    - PORT=5080
    - ROCKETCHAT_URL=[任意のドメイン]:80
    - ROCKETCHAT_ROOM=
    - LISTEN_ON_ALL_PUBLIC=true
    - ROCKETCHAT_USER=[hubot用ユーザID]
    - ROCKETCHAT_PASSWORD=[hubot用パスワード]
    - BOT_NAME=[任意のhubot名]
    - EXTERNAL_SCRIPTS=hubot-help,hubot-seen,hubot-links,hubot-diagnostics,hubot-reddit,hubot-bofh,hubot-bookmark,hubot-shipit,hubot-maps,hubot-cron,hubot-jenkins-notifier
    - HUBOT_JENKINS_URL=[連携するjenkinsのURL]
    - HUBOT_JENKINS_AUTH=[jenkinsのアカウント:パスワード]
  volumes:
    - /usr/local/share/hubot/scripts:/home/hubot/scripts
    - /etc/localtime:/etc/localtime:ro
  links:
    - rocketchat:rocketchat
  ports:
    - 3001:5080

mongodb:
   image: mongo
   ports:
     - 27017
   volumes:
     - /srv/docker/mongodb/db:/data/db

一部解説

下記はJenkinsのジョブをhubot内から実行するための設定です。

    - EXTERNAL_SCRIPTS=hubot-help,hubot-seen,hubot-links,hubot-diagnostics,hubot-reddit,hubot-bofh,hubot-bookmark,hubot-shipit,hubot-maps,hubot-cron,hubot-jenkins-notifier
    - HUBOT_JENKINS_URL=[連携するjenkinsのURL]
    - HUBOT_JENKINS_AUTH=[jenkinsのアカウント:パスワード]

下記はRocket.chatのログはmongoDB内に格納されるため
コンテナを消してもログを残すためにマウントの設定を入れています。
ホスト側のパスはもちろん任意です。

   volumes:
     - /srv/docker/mongodb/db:/data/db

運用する中で起きた問題

ID管理

開発環境には他サービスも並行して動いており
導入したチャットでも新たにID管理するのは面倒だから何とかならない?
といったことがありました。

対処として、Rocket.Chatには特定サービスとのoAuth認証機能が備わっていましたので
今回のケースではバージョン管理として既に使用していたGitlabに集約することに。

キャプチャ.PNG

hubotコンテナが落ちる

hubotには導入したプラグイン次第で様々なコマンドを使用させることができます。
メンバーの利用状況を見ていると、通知機能(タスク登録)がよくつかわれていたようです。
Cron形式で登録することができ、お昼や定例などのアラーム代わりに使っているのが見受けられました。

ある日突然、hubotが反応しなくなりdocker ps -a で状態を見ると、hubotコンテナが落ちていました。
docker logs にてログを出力したところ、チャットの全ルームの過去ログが流れ出しました。

原因はおそらく、割り当てたメモリ枯渇ではないかなと推測しています。
ちなみにDockerはAWSのEC2上で起動しております。

対処として、docker rm [コンテナ名] でコンテナ削除を行い、docker-compuse up -d
で起動しました。
hubotが持っているログは不要なのでばっさり切り捨て。

ただこの方法だと、上記にありました通知機能で登録したタスクがすべてなくなってしまいますので
コアな運用に用いている場合はhubotコンテナもホストにマウントするなど、一工夫必要かなと思います。

続きを読む

[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に認証情報が保存されています。

完了

続きを読む

CloudFront に SSL 証明書を導入する際のポイントまとめ

CloudFront に SSL を入れる際に気をつける項目のまとめです。

2017年9月時点では、特に S3 との連携で大きな制約があり、将来的には改善されていくのではと思います。現在の状況を確認してください。

SNI か専用 IP か

まず、ブラウザと CloudFront 間の通信で、SNI (Server Name Indication) が使えるかどうか検討します。

ガラケーやWindowsXP の IE は SNI に対応していません。これらの古い環境でサイトが見えなくても構わないかどうかを確認します。

SNI 非対応のブラウザに対応するには、専用 IP を使う必要があります。専用 IP は $600/月かかります。

専用 IP 独自 SSL の料金体系はシンプルです。SSL 証明書ごとの専用 IP アドレスに伴う追加コストのため、CloudFront ディストリビューションに関連付けられた各独自 SSL 証明書ごとに、毎月 600 USD をお支払いいただきます。

SNI は無料です。

SNI 独自 SSL 証明書管理には、前払い料金や最低月額料金は不要です。お客様にお支払いいただくのは、Amazon CloudFront のデータ転送と HTTPS リクエストに対する通常料金のみです。

証明書の取得について

どのドメインでどんな証明書が必要か、証明書は有料か無料か、を確認します。

証明書の取得方法には、以下の選択肢があります。

  1. 代理店などから購入する (有料)
  2. ACM (AWS Certificate Manager) を使う (無料、ただし EC2 にはインストール不可)
  3. Let’s Encrypt を使う (無料、ただし Amazon Linux 未対応)
  4. etc.

CloudFront – オリジン間を HTTPS にする

通常は、ビューア – CloudFront 間だけでなく、 CloudFront – オリジン間も通信を暗号化します。(表向き暗号化している風を装って、裏を平文で流して良いのかは微妙な問題です…)

CloudFront とオリジンの両方で、証明書が必要になる可能性があります。

オリジンが EC2 の場合、 ELB を入れるか検討する

ACM で取得した証明書を EC2 にインストールすることはできません。

https://aws.amazon.com/jp/certificate-manager/faqs/

Q: ACM により提供された証明書を使用できるのは、AWS のどのサービスですか?

ACM は AWS の以下のサービスと連携できます。
• Elastic Load Balancing – Elastic Load Balancing のドキュメントを参照してください。
• Amazon CloudFront – CloudFront のドキュメントを参照してください。
• Amazon API Gateway – API Gateway のドキュメントを参照してください。
• AWS Elastic Beanstalk – AWS Elastic Beanstalk のドキュメントを参照してください。

https://aws.amazon.com/jp/certificate-manager/faqs/

Q: Amazon EC2 インスタンスや自分のサーバーに証明書を使用できますか?

いいえ。現在のところ、ACM により提供される証明書は、AWS の特定のサービスのみで使用できます。

EC2 で ACM を使いたい場合は、CloudFront – ELB – EC2 の構成にします。証明書が無料になる代わりに、ELB の費用が発生します。

S3 に証明書を入れる必要はない

  • Amazon S3 は SSL/TLS 証明書を提供するため、その必要はありません。

この日本語訳はいまいち何を言っているのかわかりませんが、、

  • Amazon S3 provides the SSL/TLS certificate, so you don’t have to.

「S3 が証明書を提供するので、あなたが(提供する)必要はない」という意味です。

今のところ、S3 に独自の証明書を入れる手段はありません。オリジンに S3 の CNAME を指定することもできません。

http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html

以下の形式を使用してバケットを指定しないでください。

  • Amazon S3 のパススタイル (s3.amazonaws.com/bucket-name)
  • Amazon S3 の CNAME (ある場合)

CloudFront – S3 間の HTTPS には大きな制約がある

S3 をウェブサイトエンドポイントとしてオリジンに設定すると、HTTPS が使えなくなります。

Amazon S3 バケットがウェブサイトエンドポイントとして構成されている場合、オリジンとの通信に HTTPS を使用するように CloudFront を構成することはできません。Amazon S3 はその構成で HTTPS 接続をサポートしていないためです。

ウェブサイトエンドポイントは、

http://example-bucket.s3-website-ap-northeast-1.amazonaws.com/

のような URL での配信方法です。「静的ウェブサイトホスティング」の設定です。この URL をオリジンに設定すると、CloudFront – S3 間で HTTPS が使えません。

ウェブサイトエンドポイントは、以下の機能で必要です。

  • /foo/bar//foo/bar/index.html を返す
  • 独自のエラー画面を表示する
  • リクエストを全てリダイレクトする (例えば、www なしドメインを www ありにリダイレクトする)

ウェブサイトエンドポイントを使わず、S3オリジンで設定すると、サイトのトップ / (Default Root Object) 以外、index.html が見えるようにはなりません。

CloudFrontではDefault Root Objectという設定項目でIndex Documentを指定することができますが、これはRootへのアクセス時(例:http://example.com/) に対してのみ有効であり、サブディレクトリ(例:http://example.com/foo/) に対しては有効になりません。そのため、階層構造のあるWEBサイトをホストする際には不向きだと思われます。

ウェブサイトエンドポイントを使う場合は、裏が暗号化されていなくてもいいのか、バレなければいいのか、を検討しなければならなくなります。。ちなみに、オリジンが S3 であることは Server レスポンスヘッダで判別できます。S3 がウェブサイトエンドポイントかどうかは、/foo/bar/ の挙動で判別できます。

また、ウェブサイトエンドポイントにすると、Origin Access Identity を使うことができなくなります。バケット名を推測して、S3 に直接アクセスされる可能性があります。

ワイルドカード証明書の取得を検討する

CloudFront にもオリジンにも、ワイルドカード証明書が使えます。例えば、以下のような組み合わせが考えられます。

  • CloudFront *.example.com → Origin *.example.com

    • 1つのワイルドカード証明書を使う
  • CloudFront www.example.com → Origin www.example.com
    • 1つの非ワイルドカードの証明書を使う
    • CloudFront から Host ヘッダを転送する場合のみ、オリジンの証明書で Host ヘッダのドメイン名が使える
  • CloudFront www.example.com → Origin *.example.com
    • 表は高い非ワイルドカード証明書を使い、裏は安いワイルドカード証明書で済ませる
  • CloudFront www.example.com → Origin origin.example.com
    • 表と裏と、別々に非ワイルドカード証明書を購入する
    • または、証明書に別名 (SAN) を設定する
  • CloudFront www.example.com → Origin origin.example.jp
    • オリジンは .example.com でなくてもよい。DNS の自由が効かない場合、裏は Route53 で独自のドメインを取得すると便利

詳細は以下のドキュメントを参照してください。

カスタムオリジンを使用する場合、オリジンの SSL/TLS 証明書の共通名フィールドにドメイン名が含まれ、さらにサブジェクト代替名フィールドにもドメイン名がいくつか含まれることがあります。 (CloudFront は証明書ドメイン名にワイルドカード文字を使用できます)。

証明書のドメイン名のうち 1 つは、オリジンドメイン名に指定したドメイン名と一致する必要があります。ドメイン名が一致しない場合、CloudFront は、ビューワーに HTTP ステータスコード 502 (Bad Gateway) を返します。

内部の証明書を安くあげる

エンドユーザの目に触れないオリジン用の証明書にまでコストをかける必要があるかどうかを検討します。安価な証明書でも十分かもしれません。

オリジンが ELB なら ACM が使えます。

DNS を操作する権限があるか確認する

表側の DNS を操作する権限がない場合、オリジンに Route53 で取得した独自ドメインを設定すると、自由度が上がります。

オリジンに独自のドメインを使う場合は、その証明書が必要です。

リリース前の確認方法を検討する

リリース前に CloudFront に別のドメインを割り当てて確認する場合は、ワイルドカード証明書があったほうが便利です。

例えば https://staging.example.com/ のような URL を CloudFront に設定します。

Naked ドメインを使うか確認する

https://example.com/ のような Naked ドメイン (Zone Apex) を使うかどうか確認します。

Route53 を使わずに、Naked ドメインを CloudFront や ELB や S3 に割り当てることはできません。CloudFront を通さず、例えば EC2 で直接リクエストを受ける必要が出てきます。

Naked ドメインへのリクエストを EC2 で受ける場合

Naked ドメインへのリクエストを EC2 で直接受ける場合は、ACM が使えません。ACM で取得した証明書は EC2 にインストールできないからです。

証明書の購入が必要になる可能性があります。

Naked ドメインの DNS に Route53 を使っている場合

Route53 でエイリアスを使うと、CloudFront に Naked ドメイン (Zone Apex) を設定できます。

この場合は、Naked ドメインの証明書取得に ACM が使えます。

ワイルドカード証明書を使う場合、ワイルドカードに Naked ドメインは含まれないので、*.example.com に別名で example.com を設定します。

既存の証明書がある場合は SAN を確認する


既に証明書があって、足りない証明書を購入する場合。

既存の証明書の別名 (SAN) が何になっているのかを確認します。特に Naked ドメインなど、必要なドメインが SAN に設定されているかもしれません。

証明書をインポートするリージョンに注意する

CloudFront の証明書は、バージニア北部リージョンに入れる必要があります。

ACM で証明書を取得する場合にも、バージニア北部リージョンを選ぶ必要があります。

ビューワーと CloudFront との間で HTTPS を必須にするには、証明書をリクエストまたはインポートする前に AWS リージョンを 米国東部(バージニア北部) に変更する必要があります。

CloudFront とオリジンとの間で HTTPS を必須にする場合、オリジンとして ELB ロードバランサーを使用しているなら、任意のリージョンで証明書をリクエストまたはインポートできます。

例えば、オリジンが東京の ELB だとしたら、東京とバージニア北部リージョンの両方の ACM で証明書を取得 (またはインポート) します。

ACM の本人確認について

ACM で証明書を取得する場合は、本人確認 (取得意思の確認) メールが、証明書のドメイン宛てに送信されます。

Whois 情報の更新状態をチェックする

本人確認メールは、公開されている Whois の連絡先アドレスにも送信されます。

Whois に正しいアドレスが公開されている場合は、そのアドレスでメールを受け取るのが簡単です。

SES で ACM の認証を通す

ドキュメントの通りにやれば、ACM の確認メールを SES で受信するのは、さほど難しくありません。

SES の受信設定を行い、DNS に MX レコードを設定、受信したメールを SNS トピックで受け取るようにして、そのトピックに任意のメールアドレスをサブスクライブします。

ルールセットがアクティブになっているかどうかに注意する

SES のメール受信設定には、「ルールセット」というバージョン管理の仕組みがあります。

現行のルールセットをコピーし、編集し、アクティブなルールセットを入れ替える、という手順で設定を変更します。Inactive Rule Sets に新しいルールセットを足しても何も起こりません。

続きを読む

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

はじめに

HashiCorpのVaultが提供しているDynamic Secrets (Secret Backend) 機能について公式ドキュメントを参考に動作を確認しましたので備忘録として残しておきます。

やったこと

Vaultの Dynamic Secrets (Secret Backend) 機能を利用してAWSユーザの作成・削除を行う。

HashiCorp Vaultとは?

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

Dynamic Secrets (Secret Backend) とは

VaultにAWSやDatabaseなどのユーザを管理させる機能です。

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

参考

公式サイト

Vault by HashiCorp

ドキュメント

Dynamic Secrets – Getting Started – Vault by HashiCorp
AWS Secret Backend – Vault by HashiCorp


事前準備

VaultをDevモードで起動

DevモードではVaultを停止させるとデータが消えてしまいますが、今回は動作確認のためDevモードで起動します。

$ vault server -dev

...
    export VAULT_ADDR='http://127.0.0.1:8200'

環境変数の設定

$ export VAULT_ADDR='http://127.0.0.1:8200'

AWS Backendをマウント

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

AWSユーザの作成

vaultがAWSを操作するためのユーザ(vault)を作成します。

本来であれば目的に応じたポリシーを付与するべきですが、今回は動作確認のため「AdministratorAccess」ポリシーを適用しました。

作成したユーザの「アクセスキー ID」と「シークレットアクセスキー」はvaultで利用するためメモしておきます。

vault01.png

vault02.png

vault03.png

vault04.png

vault05.png


AWS Backendの設定

Vault用に準備したAWSユーザの「アクセスキー ID」と「シークレットアクセスキー」をaws/config/rootに登録します。

$ vault write aws/config/root 
    access_key=AKIAJDCIHPTOLCKLM6WA 
    secret_key=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

Success! Data written to: aws/config/root

aws/config/rootに書き込んだ情報は読み取れなくなります。

$ vault read aws/config/root

Error reading aws/config/root: Error making API request.

URL: GET http://127.0.0.1:8200/v1/aws/config/root
Code: 405. Errors:

* 1 error occurred:

* unsupported operation

ロールの作成

Getting Started を参考にjsonファイルを作成します。
Actionにec2:*という記載があるので、EC2を操作するアカウントが作成されそうです。

policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1426528957000",
      "Effect": "Allow",
      "Action": [
        "ec2:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

作成したjsonファイルを指定してロールを作成します。

$ vault write aws/roles/deploy policy=@policy.json

Success! Data written to: aws/roles/deploy

AWSユーザの作成

ロールを指定してAWSユーザを作成させます。
レスポンスとして作成したユーザの「アクセスキー ID」と「シークレットアクセスキー」が返却されます。

$ vault read aws/creds/deploy

Key             Value
---             -----
lease_id        aws/creds/deploy/2cfb960b-7f2e-74c4-d351-917401d27f3b
lease_duration  768h0m0s
lease_renewable true
access_key      AKIAIYMLSSQ434JIRMUQ
secret_key      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
security_token  <nil>

AWSの管理画面を確認すると新しいユーザ(vault-root-deploy-1507372260-9128)が作成されて、EC2を操作するアクセス権限をもってることがわかります。

vault06.png

vault07.png

vault08.png


AWSユーザの削除

lease_idを指定してユーザを削除します。

$ vault revoke aws/creds/deploy/2cfb960b-7f2e-74c4-d351-917401d27f3b

Success! Revoked the secret with ID 'aws/creds/deploy/2cfb960b-7f2e-74c4-d351-917401d27f3b', if it existed.

AWSの管理画面でもVaultが作成したユーザ(vault-root-deploy-1507372260-9128)が削除されたことがわかります。

vault09.png

補足

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

続きを読む

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

続きを読む

AWSの前日コストをSlackに通知してみた。概算でなくて正確な値で。

概要

弊社ではSmoozというブラウザアプリを作っていて、一部機能をEC2のスポットインスタンスで運用しています。
ですが、ユーザー数の変動などでインスタンス数がかなり上下し、コスト変動が激しいです(うれしい悲鳴?)。
結構お金がかかるので気にはなるが、毎日コストエクスプローラーで確認するのが面倒。
どうにかしてみました。

問題点

ぐぐってみると、

同じニーズはたくさんあるようで、Lambdaを使ってCloudWatchのBillingを取得してSlackに通知というのが王道のようです。

ということで真似てやってみたのですが、、、

あれ、実際の額と結構違わない?

ClougWatchのBillingはあくまでも概算なので、正確な値と異なります。
これだとあんま役に立たないよ、ということでコストエクスプローラーからスクレイピングでコストを取得するようにしました。

ざっくり説明

Seleniumでコストエクスプローラーにアクセスして、
前日のコストを取得してそれをSlackに流すようなスクリプトをEC2上で毎日17時に動かしています。

EC2上でHeadless Chromeを動かす

Xvfbを使っても良かったのですが、せっかくHeadless Chromeが出たということで
試してみました。環境構築に関する記事はネット上にたくさんあるのですが、↓が一番サクッと導入できました。

headless chromeをPythonのseleniumから動かして引数を考えた (Ubuntu 16.04)

ちなみにAmazon Linx上でHeadless Chromeを動かす記事もいくつかあったので試したのですが、
うまくいかずUbuntuで動かしてます。
コスト的にAmazon Linux上で動かしたい、、、誰かサクッとできた人いれば教えてください。

Pythonでのスクレイピング部分

サービスとそこまで関係ないので適当に書いてます笑
スクレイピングに慣れ親しんだ方なら瞬殺かと。

#!/usr/bin/python3

import os
import datetime
import slackweb
from time import sleep
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# headless chrome
CHROME_BIN = "/usr/bin/chromium-browser"
CHROME_DRIVER = os.path.expanduser('/usr/bin/chromedriver')

options = Options()
options.binary_location = CHROME_BIN
options.add_argument('--headless')
options.add_argument('--window-size=1280,3000')

driver = webdriver.Chrome(CHROME_DRIVER, chrome_options=options)

# ログイン
driver.get("https://{ACCOUNT_ID}.signin.aws.amazon.com/console")
driver.find_element_by_id('username').send_keys({USENAME})
driver.find_element_by_id('password').send_keys({PASSWORD})
driver.find_element_by_id('signin_button').click()
sleep(3)

driver.get("https://console.aws.amazon.com/cost-reports/home?region=ap-northeast-1#/savedReports")
sleep(3)

# デイリーレポートを選択
driver.find_element_by_partial_link_text('Daily').click()
sleep(3)

# slackにpostするときに使う
current_url = driver.current_url

# 期間を昨日から昨日、つまり昨日だけにする
driver.find_element_by_xpath('//*[@class="picker-dropdown"]').click()

# 前日の日付
yesterday = datetime.date.today() -datetime.timedelta(1)
yesterday_str = yesterday.strftime('%m/%d/%Y')

# from
elem = driver.find_element_by_xpath('//label[text()="From"]/following::input')
elem.clear()
elem.send_keys(yesterday_str)
sleep(1)

# to
elem = driver.find_element_by_xpath('//label[text()="To"]/following::input')
elem.clear()
elem.send_keys(yesterday_str)
sleep(1)

# 適用
elem = driver.find_element_by_xpath('//div[text()="Apply"]').click()
sleep(1)

# コスト取得
cost = driver.find_element_by_xpath('//*[@id="tr-total"]/td[2]').text

# slackに投稿
slack = slackweb.Slack(url={Webhook_URL})
yesterday_str2 = yesterday.strftime('%Y/%m/%d')
url = 'https://console.aws.amazon.com/cost-reports/home?region=ap-northeast-1'
text = u"{yesterday_str2}のコストは${cost}です。/ {current_url}".format(**locals())

slack.notify(text=text)
driver.quit()

Slackにはこんな感じでPostされます

image.png

(おまけ)コストエクスプローラーはいつ更新されるのか?

コストエクスプローラーに馴染みががある人はわかると思いますが、
一体いつ更新されるのか謎ですよね。

AWSのサポートに問い合わせたところ、
24時間ごとに少なくとも一度はデータを更新する。
ただし、正確な更新時間は非公開とのこと。

それじゃあ困る、、、
とうことでcronを1時間おきに設定して、更新時間を追ってみました。
朝と夕方、翌日に更新されてるみたい。

コストエクスプローラーの日付はUTCでして、
JST 9:00をすぎて昨日のコストを見に行くと、実際に請求される額の半分程度。
その日の16時ごろにまた更新されて実際に請求される額の9割程度。

そしてまた翌日にも更新されて実際の額になっているようです。

続きを読む

MapDをAWS上で試してみた

MapDとは

https://www.mapd.com/
地図データを扱うことに長けたデータベース製品であり、データを可視化するVisual Analytics Platformを持つ。
データベースの検索から地図上への表示までがとても高速なのが売り。
Enterprise Edition(有償)とCommunity Edition(無償)があり、どちらもAMIが提供されているため、AWS環境があれば簡単に触ることができる。
今回はCommunity EditionのAMIを試してみる。
ちなみに、AMIには既にデモデータが入っているため、どれだけ高速にデータを可視化できるのか試すことができる。

インスタンス作成上限の制限緩和申請

MapD Community EditionのAMIは次の3つのインスタンスタイプでしか作成できない。

  • p2.xlarge
  • p2.8xlarge
  • p2.16xlarge

しかし、これらのタイプのインスタンスの作成上限はデフォルトで0になっているようで、このままではインスタンスを作成できない。
そのため、AWSコンソールにログインし、EC2サービスを開いて、左上にある”制限”という項目から、今回利用したいインスタンスの上限制限を緩和する申請を出さなければならない。
私の場合は申請してから1日後にインスタンスを作成できるようになった。

費用

Community Editionの費用

インスタンスタイプ 1時間当たりの費用
p2.xlarge $1.542
p2.8xlarge $12.336
p2.16xlarge $24.672

2017年10月時点

導入手順

インスタンスの作成

  1. AWSコンソールにログインし、EC2サービスを開く
  2. インスタンスの作成をクリックし、左側のペインからAWS Marketplaceを開く
  3. AWS Marketplace 製品の検索にMapDと入力し、Community Editionを選択する
  4. インスタンスタイプはp2.xlargeを選択した
  5. ネットワークは必要に応じて設定し、ストレージはデフォルトのままとした
  6. セキュリティグループはデフォルトの設定に加えてHTTPSを通すように設定した

ElasticIPとパブリックDNS

  1. ElasticIPを使ってインスタンスにグローバルIPを割り当てる
  2. パブリックDNSが表示されればok
  3. されない人は、VPCの設定から、DNS解決とDNSホスト名を「はい」に設定する

早速触ってみる

https://<public_dns>:8443

上記のURLにアクセスすると、次のような画面になります。
(安全な接続ではありませんというエラーが出ても無視して進む)
image.png

ここでPASSWORDにインスタンスIDを入力するとログインできます。
ログイン後の画面はこんな感じ
image.png

デモデータはニューヨークのタクシー情報のようです。
画面をスクロールすると、右側の表示がリアルタイムに切り替わっていく。
(右上にデータ件数が表示されているのですが、この件数をほぼリアルタイムで可視化できる!)
右側の棒グラフをクリックすると、そのデータだけ地図上に表示できたりします。
image.png

まとめ

  • MapDの立ち上げは、ものの15分ぐらいでできた
  • デモデータも入っているし、可視化まで簡単にできるので、どの程度の性能なのか見たい人にオススメ

続きを読む

CloudFrontキャッシュを削除するシェルスクリプトを書いてみた。

AWS CLI を使えば、 AWS Console へアクセスすることなく
CloudFrontキャッシュの削除も行えます。

削除対象が多い場合はその対象を記したjsonファイルを用意して、
AWS CLI からそのjsonファイルを利用して CloudFrontキャッシュ の削除を行うのですが、
毎回書き換えが必要な項目がjsonファイル内にあります。

手作業で毎回書き換えてましたがさすがに面倒になってきたので
シェルスクリプトを書きました。

Macユーザーなので Macでの作業想定で書きます。

概要

AWS Consoleアクセスすれば、
Cloud Frontのキャッシュ手動削除はできるのですが、
面倒なので AWS CLI を使ってシェルスクリプトから
削除できるようにしたものです。

必要な準備

AWS CLI のローカルインストール

詳しいインストール手順はこちら

macOSの場合は Python 2.6.3 以降が必要なので

$ curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
$ sudo python get-pip.py

で pip をインストールしてから

$ sudo pip install awscli

して

$ aws help

でヘルプが出ればOKです。

AWS CLI の設定

CLI利用するのに専用の configure 設定を一度行う必要があります。
これも上記URLに設定方法があるのですが、

  • AWS Access Key ID(以下 AKID)
  • AWS Secret Access Key(以下 SAK)
  • region name
  • output format

の4つが必要です。
この設定プロファイルは

~/.aws/credentials # AKID と SAK

~/.aws/config # region name と output format

に入ります。

プロファイル名を付ければ複数のプロファイルを格納できます。
この2ファイルは1回設定しちゃえばあとはいじらなくてOKです!

↓こんな感じ

~/.aws/credentials
[profile hogehoge]
aws_access_key_id=****************      # AKID
aws_secret_access_key=****************  # SAK
~/.aws/config
[profile hogehoge]
region=ap-northeast-1                   # region name
output=json                             # output format

invalidation-batch の準備

削除対象を指定するバッチファイル(json)を作成します。
↓こんな感じです。

invbatch.json
{
    "Paths"  : {
        "Quantity": 5,
        "Items"  : [
            "/",
            "/sp/",
            "/css/main.css",
            "/js/libs.js",
            "/js/main.js"
        ]
    },
    "CallerReference": "YYYYMMDD-HHNNSS"
}

Itemsに削除対象を DocumentRoot からのパスで指定します。
ワイルドカード()も使えます。
この指定方法は **AWS Console
* での指定方法と一緒です。

注意点

CallerReference は毎回ユニークな値を入れておかなければいけません。
これが意外と面倒なのでこの部分の書き換え込みでシェルスクリプト化しました。

Note:
削除対象が変われば ItemsQuantity も変えなきゃです、、

このファイルは削除対象の変更がなければ、1回設定しちゃえばあとはいじらなくてOKです!

キャッシュ削除用シェルスクリプト設定

シェルスクリプトに実行権限$ chmod a+x ***を与えておいて、実行します。
中身は↓こんな感じです。

clear_cache.sh
#!/bin/bash

# 設定用変数
dist_id="*******************"   # AWS CloudFrontのDistribution ID
batch_json="invbatch.json"      # nvalidation-batchファイル名
profile="hogehoge"              # プロファイル名
url="http://hogehoge.jp"        # サイトURL

# 実行部分
echo "Move directory ..."
cd `dirname $0`
echo "Invbalidation-batch json update"
echo `less $batch_json_base | jq '.CallerReference |= "'$profile'_'$time_stamp'"'` > $batch_json
echo "CloudFront cache clear ..."
echo `aws cloudfront create-invalidation --distribution-id $dist_id --invalidation-batch "file://$batch_json" --profile $profile`

※ jq使ってjsonの書き換えを行なっているのでjqのインストールが必要です。

jqのインストール方法
https://stedolan.github.io/jq/download/

コード内のコメントにありますが、

  • AWSのCloudFrontDistribution ID
  • nvalidation-batch ファイル名
  • プロファイル名
  • サイトURL

が必要です。

このファイルは1回設定しちゃえばあとはいじらなくてOKです!

キャッシュ削除シェルスクリプト実行

$ ./clear_cache_shupure.sh
Move directory ...
Invbalidation-batch json update
CloudFront cache clear ...
{ "Invalidation": { "Status": "InProgress", "InvalidationBatch": { "Paths": { "Items": [ "/images/girls/*.png", "/", "/images/sakabar/*.png", "/images/events/*.jpg", "/sp/gekijo/", "/sp/sakabar/", "/gekijo/", "/sp/", "/images/girls/0924.png", "/css/main.css", "/sakabar/" ], "Quantity": 11 }, "CallerReference": "shupure_1506515279" }, "Id": "I2N11BY0PH6UI9", "CreateTime": "2017-09-27T12:28:03.125Z" }, "Location": "https://cloudfront.amazonaws.com/2017-03-25/distribution/E1JZGHRP8PFLBD/invalidation/I2N11BY0PH6UI9" }
CURL check ...
 % Total  % Received % Xferd Average Speed  Time  Time   Time Current
                 Dload Upload  Total  Spent  Left Speed
 0 50672  0   0  0   0   0   0 --:--:-- --:--:-- --:--:--   0
 X-Amz-Cf-Id: eHRIZxCp2k5-aecSw1ABCI-OB7aPtK_EEs4y0elK8bucqB6e1B-zrA==

CloudFront キャッシュ削除のレスポンスjsonが改行されてないのが気持ち悪いですが、
InProgressが表示されていればキャッシュ削除スタートしてます。

キャッシュ削除されてるかはcurlなどでレスポンスヘッダを確認してください。

$ curl -I http://hogehoge.jp

続きを読む

ダイジェスト認証時のELBヘルスチェックではまった。。。ELBのヘルスチェックを認証から外す。

はじめに

ELB⇒EC2の構成で、ダイジェスト認証を行っているサーバーに対してヘルスチェックを行う際、
ずっとステータスがOutofServiceで、解決するのに時間がかかてつぃまった。。。

原因

ヘルスチェック対象のサーバーに、Digest認証やBasic認証がある場合、ヘルスチェックも妨害されてしまう。。。
Apacheのアクセスログに、401エラーが出ていました。
401エラーは認証を必要としているエラーです。
Apacheのステータスコードについてはこちら

解決策

ヘルスチェックの通信を除外するように、設定ファイルに追記しました!
基本はlocationタブ内の範囲を、設定ファイルに追記すれば大丈夫なはずです。

/etc/httpd/conf/httpd.confだけ使っていれば、httpd.confに追記します。
バーチャルホストを使用しているのであれば、各バーチャルホストの設定ファイルに書いていきましょう。
※バーチャルホストを使用している場合は、httpd.confに書くとすべてに適用される可能性があるので気を付けてください。

# vi /etc/httpd/conf.d/virtualhost.conf


●変更前

<VirtualHost *:80>
    ServerName {{ 任意のサーバー名 }}:80
    DocumentRoot /var/www/html
    DirectoryIndex index.php
    RewriteEngine on
    RewriteCond $1 !^/(index.php|img|js|fonts|css|robots.txt|favicon.ico|.well-known)
    <Directory /var/www/html>
        AuthType Digest
        AuthName "Digest Auth"
        AuthUserFile "/var/www/.htdigest"
        Require valid-user
    </Directory>
</VirtualHost>


●変更後

<VirtualHost *:80>
    ServerName {{ 任意のサーバー名 }}:80
    DocumentRoot /var/www/html
    DirectoryIndex index.php
    RewriteEngine on
    RewriteCond $1 !^/(index.php|img|js|fonts|css|robots.txt|favicon.ico|.well-known|health.html) ★リダイレクトの設定がある場合のみ。ヘルスチェックで使用するファイル名を追記!(「.」の前に「」が必要)
    <Directory /var/www/html>
        AuthType Digest ★認証方法(Basic認証ならBasic)
        AuthName "Digest Auth" ★認証設定時に設定した認証名
        AuthUserFile "/var/www/.htdigest" ★認証情報が記載されたファイルパス
        Require valid-user
    </Directory>

    <Location /> ★下記(<location>~</location>)を追記!
        AllowOverride all
        Satisfy Any
        SetEnvIf Request_URI "/health.html" healthcheck ★"ここはヘルスチェックで使用するファイル名"
        Order Deny,Allow
        Deny from all
        Allow from env=healthcheck
        AuthType Digest ★認証方法(Basic認証ならBasic)
        require valid-user
        AuthName "Digest Auth" ★認証設定時に設定した認証名
       AuthUserFile "/var/www/.htdigest" ★認証情報が記載されたファイルパス
    </Location>
</VirtualHost>


●文法確認
# httpd -t


●設定反映
# systemctl restart httpd

はまったところ

リダイレクト設定が入っていることで、解消に時間がかかってしまいました。。。
⇒単に気が付くのが遅かっただけです。。。

下記の設定で、任意のファイル名を追記することでアクセス許可することができます。
アプリの仕様上、静的ファイルへのアクセスを制限するために下記設定を入れていたようです。。。。
RewriteCond $1 !^/(index.php|img|js|fonts|css|robots.txt|favicon.ico|.well-known|health.html)
リダイレクトについて、まだ全然わかっていないので、別の機会にまとめます。

参考にしたURL

続きを読む