Terraformでtask-definisionを消したらエラーがで続けた…

task-definitionをコンソールから停止

tfファイルを更新してもtask-definitionが更新されないので試しに現状動いているものを停止してみた

error.sh
$ terraform apply
aws_ecs_service.registry: ServiceNotActiveException: Service was not ACTIVE.
    status code: 400, request id:*****

後に更新されない理由が判明したがエラーが消えてくれない

原因

ECSのtaskは停止しても削除されず、INACTIVE状態で残るためにtfstateファイルから同じものを見続けることが判明
一時的に参照するtask-definitionを動いてるものに指定 => apply => 元に戻す でも効果なし

ecs.tf
resource "aws_ecs_service" "service_redash_blt" {
  name = "${terraform.env}-****"
  cluster = "${aws_ecs_cluster.****.id}"
  task_definition = "${aws_ecs_task_definition.****.arn}"
  desired_count = 1
  deployment_minimum_healthy_percent = 50
  deployment_maximum_percent = 100
  iam_role = "${aws_iam_role.****.arn}"

  load_balancer {
    elb_name = "${aws_elb.****.name}"
    container_name = "nginx"
    container_port = 80
  }
}

解決策

結局task-definitionのfamily自体を変更
terraformで作ったものをコンソールからいじってはいけないと痛感…

続きを読む

AWS関連用語まとめ

個人的に今勉強中のAWS関連の用語をなるべく簡単にまとめてみました。(語弊がある部分もあるかもしれませんが個人的に理解できる形で落とし込んでみました)。今後も随時追加予定

EC2

仮想サーバ

S3

Amazon Simple Storage Service。オンラインストレージサービス

EBS

Amazon Elastic Block Store。オンライン外付けストレージ

ELB

Elastic Load Barancing。ロードバランサー

EIP

Elastic IP。固定のグローバルIP

RDS

Relational Database Service。クラウド型RDBMS

AMI

Amazon Machine Image。ソフトウェア構成(オペレーティングシステム、アプリケーションサーバー、アプリケーションなど)を記録したテンプレート

IAM

AWS Identity and Access Management。AWSへのアクセスを安全に制御するための仕組み

VPC

Virtual Private Cloud。仮想プライベートクラウド。

CloudFormation

環境設定に関してソースコード化したものを読み込んでAWS上に環境構築してくれるサービス(インフラをコード化してくれるなんてすごいね)

CodePipeline

システムのリリースにおけるプロセスを監視し、自動化・視覚化してくれるサービス。GitHubでMasterブランチへのマージを検知し→ビルド→デプロイ作業を自動で行ってくれる、みたいな事ができる

CodeBuild

CodePipelineで連携している機能の一部。自動でビルドしてくれる

CodeDeploy

CodePipelineで連携している機能の一部。自動でデプロイしてくれる

続きを読む

リザーブドインスタンスが購入できなかった時の話

概要

利用したことなかったけど、試しにリザーブドインスタンスを購入してみよう…!と、カートに入れて購入に進んだところ、以下のエラーメッセージが表示されました。

エラー : Your current quota does not allow you to purchase the required number of reserved instances (Service: AmazonEC2; Status Code: 400; Error Code: ReservedInstancesLimitExceeded; Request ID: ***********)

リザーブドインスタンスが購入できなかった時の画像.png

結論だけ、書く。

サポートの方から訊いたのですが、あまりAWSの利用実績がないアカウントだと、支払い方法が「 前払いなし (No Upfront)」の状態ではリザーブドインスタンスの購入が出来ないようになっているそうです。
支払い方法を「 一部前払い (Partial Upfront)」か「 全額前払い (All Upfront)」に変えた上で購入してね、とのことでした。

納得。
思い切って「 全額前払い 」でリザーブドインスタンス購入。チャリーン。

補足

なお、EC2 リザーブドインスタンス購入上限は、「アベイラビリティーゾーンごと、1 か月あたり 20 のリザーブドインスタンスと、20 のリージョンリザーブドインスタンス。」だそうで、ひと月の中でこの数を超えて購入したい場合は、サポートに購入上限の緩和リクエストが必要となります。

参考:AWS サービス制限 – アマゾン ウェブ サービス

続きを読む

Serverless FrameworkでEC2をスケジュール起動/停止するテンプレート(Lambda[java])

Serverless Framework

はじめに

  • コンテナ付いてる昨今は、久しくServerless Framework触って無かったのですが、EC2を8時に起動して、20時半に停止する要件が浮上したので、サクッとslsで作りました。
  • ソースはGithubで公開してます。
  • 至極簡単な内容なので、速攻実現出来ると思ってます。

環境のセットアップ

Serverless FrameworkでDeploy

ソースの取得

  • 以下のGithubからソースを取得します。
$ git clone https://github.com/ukitiyan/operation-ec2-instance.git

STS(Eclipse)にインポート

  • STSを起動して、Project Explorer -> 右クリック -> Maven -> Existing Maven Projectsで先程Githubから取得した「operation-ec2-instance」フォルダを選択します。

serverless.yml の修正 + Build

  • serverless.ymlのL37 周辺の設定を適宜修正します。

    • rate: AWS Lambda – Scheduled EventのCron書式
      を参考に UTC で記載
    • instanceId: 対象インスタンスのinstanceIdを記載
    • scheduleは、縦に増やせるので複数インスタンスに対応できます。(それを踏まえて環境変数でinstanceIdを指定してません)
serverless.yml
- schedule:
    rate: cron(30 11 * * ? *)
    input:
      goal: stop
      instanceId: i-XXXXXXXXXXXXXXXXX
- schedule:
    rate: cron(0 23 * * ? *)
    input:
      goal: start
      instanceId: i-XXXXXXXXXXXXXXXXX
  • プロジェクトを右クリック -> Run As -> Maven Install でビルドします。
  • target配下にoperation-ec2-instance.1.0.0.jarが出来上がります。

Deploy

  • 例のごとく、以下のコマンド一発です。
$ cd operation-ec2-instance
$ serverless deploy -v
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
・
・
Serverless: Stack update finished...
Service Information
service: operation-ec2-instance
stage: prod
region: ap-northeast-1
api keys:
  None
endpoints:
  None
functions:
  aws-java-maven-prod-hello: arn:XXXXXXXX
  • 以下のJsonでコンソールから test するか、設定時間になるまで待って、問題ないか気にしておきましょう。
{
  "goal": "stop",
  "instanceId": "i-XXXXXXXXXXXXXXXXX"
}

まとめ

  • まぁ、やっつけですが。速攻実現出来ました。
  • STSで完結するのが、アプリ屋にとっては本当うれしいです。

続きを読む

RedashをAMIから起動してみた

公式AMIからインスタンスの起動

https://redash.io/help-onpremise/setup/setting-up-redash-instance.html

から AMI を選択します。Region ap-northeast-1 の AMI リンクをクリックするとインスタンスタイプを選択する画面に遷移するので、適当なインスタンスタイプを選択します。

1年間の無料枠を使っているので無料で使える t2.micro にしました。

キーペアを設定して起動します。

アクセス設定

マネジメントコンソールからインスタンスのセキュリティーグループのインバウンドで HTTP アクセスを適切に許可します。

これでブラウザからインスタンスのIPにアクセスすればページが表示されます。

internal_server_error.png

Internal Server Error(500エラー)ですね。サービス側に問題がありそうなので確認します。

サーバ側設定

起動したインスタンスに SSH ログインします。

とりあえずサービスを再起動してみます。

$ sudo supervisorctl restart all
redash_server: stopped
redash_celery_scheduled: stopped
redash_celery: stopped
redash_celery: started
redash_server: started
redash_celery_scheduled: started

再度ブラウザをリロードしてみますが、Internal Server Error に変化はありません。

supervisord のログを確認します。

$ less /var/log/supervisor/supervisord.log

が、怪しそうなログは出ていません。Redash が使っているサービスの起動状態をひとつひとつ確認します。

PostgreSQL のプロセスを確認します。

$ ps aux | grep -i postgres
ubuntu   21547  0.0  0.0  12948   932 pts/0    S+   13:47   0:00 grep --color=auto -i postgres

見つからないので、PostgreSQL が起動していない事が原因のようです。ログを確認します。

$ less /var/log/postgresql/postgresql-9.5-main.log
2017-10-16 13:32:30 UTC [1352-1] LOG:  database system was shut down at 2017-08-13 12:39:56 UTC
2017-10-16 13:32:30 UTC [1352-2] LOG:  MultiXact member wraparound protections are now enabled
2017-10-16 13:32:30 UTC [1351-1] LOG:  database system is ready to accept connections
2017-10-16 13:32:30 UTC [1356-1] LOG:  autovacuum launcher started
2017-10-16 13:32:31 UTC [1361-1] [unknown]@[unknown] LOG:  incomplete startup packet
2017-10-16 13:34:33 UTC [1351-2] LOG:  received fast shutdown request
2017-10-16 13:34:33 UTC [1351-3] LOG:  aborting any active transactions
2017-10-16 13:34:33 UTC [1705-1] redash@redash FATAL:  terminating connection due to administrator command
2017-10-16 13:34:33 UTC [1704-1] redash@redash FATAL:  terminating connection due to administrator command
2017-10-16 13:34:33 UTC [1356-2] LOG:  autovacuum launcher shutting down
2017-10-16 13:34:34 UTC [1353-1] LOG:  shutting down
2017-10-16 13:34:34 UTC [1353-2] LOG:  database system is shut down
2017-10-16 13:34:53 UTC [19851-1] FATAL:  could not map anonymous shared memory: Cannot allocate memory
2017-10-16 13:34:53 UTC [19851-2] HINT:  This error usually means that PostgreSQL's request for a shared memory segment exceeded available memory, swap space, or huge pages. To reduce the request size (currently 148488192 bytes), reduce PostgreSQL's shared memory usage, perhaps by reducing shared_buffers or max_connections.

FATAL エラーが出てますね。その前にログの時刻が見づらいので、タイムゾーンを日本時間に変更します。

$ sudo timedatectl set-timezone Asia/Tokyo
$ date
Mon Oct 16 22:53:17 JST 2017

JST に変わりました。

先ほどの PostgreSQL のエラーは割り当てられたメモリサイズが大きいというもののようです。

$ sudo vi /etc/postgresql/9.5/main/postgresql.conf

#max_connections = 100
max_connections = 50

#shared_buffers = 128MB
shared_buffers = 32MB

少ないですね。再起動します。

$ sudo /etc/init.d/postgresql restart
Restarting postgresql (via systemctl): postgresql.service.

プロセスを確認します。

$ ps aux | grep -i postgres
postgres 21785  0.0  1.5 186840 15880 ?        S    23:02   0:00 /usr/lib/postgresql/9.5/bin/postgres -D /var/lib/postgresql/9.5/main -c config_file=/etc/postgresql/9.5/main/postgresql.conf
postgres 21787  0.0  0.3 186840  3832 ?        Ss   23:02   0:00 postgres: checkpointer process
postgres 21788  0.0  0.3 186840  3832 ?        Ss   23:02   0:00 postgres: writer process
postgres 21789  0.0  0.3 186840  3832 ?        Ss   23:02   0:00 postgres: wal writer process
postgres 21790  0.0  0.6 187244  6104 ?        Ss   23:02   0:00 postgres: autovacuum launcher process
postgres 21791  0.0  0.3 148544  3128 ?        Ss   23:02   0:00 postgres: stats collector process
ubuntu   21808  0.0  0.1  12948  1092 pts/0    S+   23:02   0:00 grep --color=auto -i postgres

起動した!

ブラウザからアクセスすると。

welcome_redash.png

表示された!

Admin ユーザを登録すればとりあえず使えるようになります。

このままだとメール設定などが出来ていなので、必要であればヘルプ

https://redash.io/help/

などを参考にします。

ちなみに

最初に試した時は、次のような en_US.UTF-8 ロケールが無いというエラーが出ていました。

2017-10-09 08:38:28 UTC [2801-1] redash@redash FATAL:  database locale is incompatible with operating system
2017-10-09 08:38:28 UTC [2801-2] redash@redash DETAIL:  The database was initialized with LC_COLLATE "en_US.UTF-8",  which is not recognized by setlocale().
2017-10-09 08:38:28 UTC [2801-3] redash@redash HINT:  Recreate the database with another locale or install the missing locale.

確認すると確かに無い。

$ locale -a
C
C.UTF-8  # en_US.utf8 がない
POSIX

そのため、次のようにして en_US.UTF-8 をインストールして起動しました。

$ sudo locale-gen en_US.UTF-8
$ locale -a
C
C.UTF-8
en_US.utf8
POSIX

この記事を書くために最初から通して試してみたら en_US.utf8 ローカルが入っていて、再現しませんでした。

Issue を立てたのですが、勘違いだった可能性が高いのでそっと閉じました。

続きを読む

AWSのIAMロール・ポリシーをソースコード管理するのに役立つツールライブラリ

AWS の IAM ロールとポリシーをどうやって管理していますか?

JSON ファイルでロールとポリシー定義を管理できるのに便利なツール(npmライブラリ)を作りました。
github.com/tilfin/aws-iam-policy-tool

できること

  • ローカルの JSON ファイルと IAM のロールとポリシー定義の相互インポート・エクスポート
  • ローカルの定義ファイルに沿って IAM ロールとポリシーが構成されているかの検証
  • 正規表現マッチするカスタムポリシーの複数削除(AWS Management Console では一個ずつしか消せない)

できないこと

  • ロールのインラインポリシーはサポートしていない

どのように使えるのか

  • IAM ロールとポリシー定義を JSON ファイルでソースコード同様にGit等のリポジトリで管理できる。
  • npm ライブラリなので Lambda から定期的にデプロイ環境のロールとポリシーが正しいかの検証ができる。

定義ファイルの仕組み

  • IAM のブラウザ上で編集する JSON とほぼ同じです。
  • デプロイ環境別にリソース名に prefix/suffix を付けている場合は ENV としておくことで、コマンド実行時に引数で指定します。
  • AWS アカウント ID も ACCOUNT_ID でコマンド実行時に引数指定することで変えることができます。

ロール

{
    "Role": {
        "RoleName": "yourapp-ec2-api-ENV",
        "Path": "/",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "ec2.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    },
    "AttachedPolicies": [
        {
            "PolicyName": "yourapp-s3-storage-ENV",
            "PolicyArn": "arn:aws:iam::ACCOUNT_ID:policy/yourapp-s3-storage-ENV"
        }
    ]
}

ポリシー

ポリシー名は内容に含まれないため JSON ファイル名そのものが適用されます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::yourapp-storage-ENV/*"
        }
    ]
}

導入と使い方

npm からインストールします。

$ npm install -g aws-iam-policy-tool

現状のロールを ./roles とポリシーを ./policies にそれぞれ書き出します。

$ awsiamtool export-role ./roles
$ awsiamtool export-policy ./policies

各ファイルの ARN 内の AWS アカウント ID の部分を ACCOUNT_ID に置換します。動作環境を示すもの (例. dev, staging など) が名前に含まれていれば、ENV に置換します。ポリシーの場合はファイル名もリネームします。

検証してみます。

$ awsiamtool validate-role -i 111122223333 -e dev roles
$ awsiamtool validate-policy -i 111122223333  -e dev policies

本番環境に定義を作成します。

$ AWS_PROFILE=prod awsiamtool import-policy -i 111122224444 -e prd policies
$ AWS_PROFILE=prod awsiamtool import-role -i 111122224444 -e prd roles

※ポリシー修正時 import-policy に上書きオプションを --overwrite を指定します。

ライブラリとして使う(別の変数を適用するには)

現状コマンド引数ではサポートしていませんがライブラリとして使えば可能です。

const awsIamPolicyLib = require('aws-iam-policy-tool');

const opts = {
  json: true,
  overwrite: true
};

const varSets = {
  ACCOUNT_ID: '000011112222',
  ENV: 'stg',
  COMMON_ACCOUNT_ID: '333344445555'
};

awsIamPolicyLib.importPolicy('./policies', varSets, opts);
.then(() => {
  return awsIamPolicyLib.importRole('./roles', varSets, opts);  
})
.then(() => { console.info('Importing done') })
.catch(err => { console.error(err) });

その他、コマンドの実行結果例などは README を見てください。

続きを読む

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

続きを読む