Elastic BeanStalkの環境別のタグ付を忘れたときにどうすんの?って話

1年ぐらい前からBeanStalk使ってます。
なんつうのか、この辺のAWSのマネージドサービスっていうの?ってほんとすごいですよね。
インフラエンジニアなんか必要なくなりますよね。
AWSの機能をガリガリ使って一昔前の3-tier systemなんて、ネットワークの知識もインフラ系のIeasの知識もなくアプリ屋さんがポチポチやってればいつでも復元できるシステム!が出来上がっちゃうんですもんね。
しかもはやりのECSつかったDockerコンテナの仕組みもつかって・・・
「あ〜〜Dockerとか最近ちゃってもう最近開発とか楽でさ〜〜、インフラ屋?いるの?仕事おせえよあいつら」とかほざけるわけです。

俺なんか無職まっしぐらです。

Beanstalkってなんぞや?

さて、まぁ今更ですがBeanStalkってなんぞやって人に簡単に説明しておきますと、
要は初見殺しとも言えるAWSの「CloudFomartion」を”比較的”わかりやすいUIで設定できるというマネージメントシステムです。
基本はネットワーク層+3tier+インスタンススケーリング何かをある程度コントロールしてくれます
更にアプリケーションのデプロイはソースをまとめておくとECR上にコンテナイメージとして作成してくれたり、そこまで必要ない人でもPHP用のAMIを使ってちょっとPHPで動作させたいコンテンツを作ってデプロイしてみたいな事ができます。
この辺、コンソールでポチポチやってると、たしかに時間的にそれなりのリソースを取られますしで開発したアプリケーションのバージョン管理もソレナリにしてくれるので何度も言いますがとても便利なサービスだと思っています。

BeanStalkのアレなところ

こんなに便利そうなBeanStalkなんですが、サーバサイド(アプリケーション開発者)にはなり針を振り気味なサービスなので、少しでも込み入ったことを始めるととたんに出来ないことが出てきます。
そこで、「.ebextentions」というファイルを作ってアプリ層のレポジトリに拡張設定をおいて設定を入れたりする訳です。
まぁ、デフォルトで設定されるCloudFormationの設定項目をアプリ層に置くようにするわけで、インフラ屋のいない会社なんかだと違和感ないというかとても理にかなっているようにみえるんですが、タスクをサーバサイド・インフラサイドと別けているチームなんかだと

インフラ屋がアプリ屋に土下座をしてネットワーク設定なんかを開発アプリレポジトリに入れてもらう

なんていう状態が発生してしまい、なんとなく自分の存在意義を感じられなくなってきたりもします。

そこで、インフラ屋さんは「EBとかつかってらんねぇよ、せめてCloudFormationでやろうぜ・・・(JSON書きたくないけど)」とか言い始めるわけです。

あ、個人的にはterraformに逃げましたw

本題の環境ごとのタグの調べ方について

なんか、閑話休題のボリュームがいつもまとまらず多めになってしまう訳ですが
本当に書きたかったEBで作った環境に対してのタグの後付とかに関してです。

EBで環境を作るとEBにおけるタグを設定しておけば、そのアプリケーションで起動したEC2インスタンスなどには一応Nameタグがついてくれてどの環境かわかりやすくなるのですが、SecurityGroup・ELB/ALBなどには最初にタグを付け忘れたりすると、どの環境で使っているSG・LBなのかを探すのが非常に困難になります。

具体的には例えばSecurityGroupなんかだと下記のようなタグが付くことになります
– awseb-a-3xxxxx2xxxx-stack-AWSEBSecurityGroup-HOGEFUGE1HOGE

まぁ訳わんないですよね。SGに関してはまだNameのタグを設定しておけば自分で任意に環境名をつけられますが
ELBとかだとName=「ユニークな文字列:DNS名」になっているのでAWSのコンソールやawscliコマンドの返り値だけでは一見どの環境用セット用の機材なのかが全くわからない状態になってしまっています。

また、タグ自体も起動段階で設定していないと、環境をすべてEB的に再構築しない限り(つまり環境内のすべてを捨ててもう一度構築し直す処理)を入れないと追加したNameタグなどは設定されない状況に陥ります。

これをあとでチクチクと手作業で入れることになるんですが、
– awseb-a-3xxxxx2xxxx-stack-AWSEBSecurityGroup-HOGEFUGE1HOGE

この文字列はEBの中身を見ているだけだと判別がつかない物なのです。
なぜか
この文字列自体がCloudformationで作られている物だからですwww。
なのでこのELBがどのEBアプリケーションで使われているのか?というのを調べるためには

  • まず、上記の文字列のうち、「awseb-a-3xxxxx2xxxx-stack」までの部分を抜き出し
  • CloudFormationのスタック一覧のFilterをその文字列に入れ
  • 説明欄にちゃんと記載されている「AWS Elastic Beanstalk environment (Name: ‘hogefuge-app’)」を確認するという作業が必要になります。

まぁ調べる方法がある分マシですがね・・・この辺もうデフォルトでNameダグとしてすべての環境に入れてしまってもいいと思うんですけどね

以上、ちょっとしたボヤキでした。
ちなみに、サーバサイドの方の「構築の手間が楽になるからEB使うことを強く推したい」という要望であるプロジェクトでEB使いましたがもう本サービス運用では二度と使いたくありません・・・・

続きを読む

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で完結するのが、アプリ屋にとっては本当うれしいです。

続きを読む

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 を見てください。

続きを読む

Lambdaのポイントメモ

概要

Lambdaで開発する時に、「あれどうだっけな」ってなった時のメモです。

Lambda関数の種類

  • 同期呼び出し

    • ストリームベース

      • Kinesis Streams
      • DynamoDB Streams
    • それ以外
      • Cognito
      • Echo
      • Lex
      • API Gateway
  • 非同期呼び出し
    • S3
    • SNS
    • CloudFormation
    • CloudWatchLogs
    • CloudWatchEvents
    • SES
    • CodeCommit
    • Config

イベントソース

ストリームベースのイベントソース

  • 通常のイベントソースはイベントソースがLambda関数を呼び出す。
  • ストリームベースのイベントソースは、Lambdaがストリームに対してポーリングを行ない、変更や新しいイベントの存在を検出したらレコードを取得し、Lambda関数を実行する。

アクセス権限

アイデンティティベースのポリシー

  • IAMユーザー、IAMグループ、IAMロールに対してアタッチするポリシー

リソースベースのポリシー

  • Lambdaに対してアタッチされるポリシー
  • Lambdaにイベントソースをセットアップして、サービスまたはイベントソースに対して、Lambda関数を呼び出すアクセス権限を付与する。
  • ポリシーの付与には、AWS CLIかAWS SDKを用いて指定する。

クロスアカウントアクセス

  • Lambda関数を所有するアカウントとそれを呼び出すアプリケーションやイベントソースのアカウントが別でも呼び出せる。

テスト

  • ユニットテストはローカルで実行、これ以外はクラウド上で実行する。

    • ユニットテストは比較的手元で実行したい事が多いテストだから。
  • Lambda関数をローカルで実行するた為に必要なもの
    • ローカルで呼び出す為のエントリポイント
    • イベント
    • contextオブジェクト

エントリポイント

  • ローカル環境で実行する際の呼び出しをどの様に行うか。
  • 実行関数を作って、if文で制御して普通に実行する。

イベントデータ

contextオブジェクト

  • contextオブジェクトには実行中のLambda関数のランタイム情報が格納されている。
  • ハンドラ内部からcontextオブジェクトの各プロパティならびにいくつかのメソッドにアクセスが可能。
  • 極論、ランタイム情報へのアクセスが不要であれば、ローカル実行時は特にエミュレートしなくとも良い。
  • 言語毎に用意されているプロパティ名やメソッド名は異なる

VPC

  • Lambda関数からVPC内にあるパブリックなIPを持っていないAWSリソースにアクセスする事が可能。

    • VPC内のみの接続にしてあるRDSの接続も可能
    • インターネットからアクセスがサポートされていないElastiCacheの様なサービスでも呼び出し可能

アクセス制御

  • EC2やRDSのLambda制御を行いたい場合は、セキュリティグループで行う事ができる。

レイテンシ

  • VPC内のリソースへのアクセスはレイテンシ増の原因となりやすい為、アクセスは最低限にするべき。

    • ENIの作成に時間がかかる。

ENIのレイテンシ

  • ENI は新規のリクエストや久しぶりのリクエストが発生した場合自動的にLambdaによって作成される。
  • 初期化処理に時間がかかる。(10~30秒)
  • 2回目以降は普通のレイテンシ

RDSとの接続の場合

  1. RDSにLambdaから直接レコードを書き込むのではなく、一旦DynamoDBにレコードを書き込み、その時点でレスポンスしてします。
  2. その後、DynamoDB ストリームとLambdaを利用して、非同期にRDSへとデータを反映する。

環境変数

  • 関数からアクセス可能な環境変数を定義する事が可能。

    • コードの変更なく動的に設定した値を渡せる。
    • 無駄なハードコーディングが不要になる。
  • 環境変数へのアクセスは言語毎に変わる。

環境変数の利用制限

  • 合計サイズが4KB
  • 文字で始める
  • 英数字とアンダースコアのみ
  • AWS Lambdaが予約する特定のキーセットは利用不可

Lambda関数自体が利用する環境変数

  • 予約語のうち、更新可能なものと更新不可のものがある。

環境変数の暗号化

  • キーと値はAWS KMS で自動的に暗号化されて保存される。

    • 独自のサービスキーを利用することも可能。

      • 別途IAMロールに対してポリシーが必要。

エラーハンドリング

  • 処理が異常終了した場合の振る舞いは、イベントソースのタイプと呼び出しタイプによって異なる。

    • 関数の実行がタイムアウト
    • 関数の呼び出しがスロットリング
    • 関数の処理でエラーor例外が発生

ストリームベースのイベントソースの場合

  • Amazon Kinesis StreamsもしくはDynamoDB Streamsがイベントソースの場合
  • レコードの有効期限が切れるまで再試行され続ける。
    • その間、エラーの発生したシャードからの読み取りはブロックされ、そのレコードのバッチの有効期限が切れるか処理が成功するまで、新しいレコードの読み込みは行われない。
    • レコードの処理順序が保証される。

ストリームベース以外のイベントソースの場合

同期呼び出しの場合

  • 呼び出し元にエラーが返される。
  • 呼び出し元はそのエラーを受け取り、必要に応じてリトライの処理を実装する。

非同期呼び出しの場合

  • 2回自動的にリトライされる。
  • 3回処理が失敗した場合、デッドレターキューが構成されていれば、そのイベントはデッドレターキューとして指定されているSQSのキューもしくはSNSのトピックに送信される。
    • これにより、障害発生時のイベントを取りこぼす事がなくなる
  • デッドレターキューが構成されていなければイベントは破棄される。

デッドレターキュー

  • デッドレターキューは、関数を非同期で呼び出す場合において、2回のリトライ、つまり3回の試行にも関わらず処理が正常終了しなかった場合にそのイベントを指定されたSQSもしくはSNSへと送る仕組み。

    • コードのロジックエラー
    • スロットリング
  • Lambdaでデッドレターキューを設定する前に、キューもしくはトピックを作成しておく必要がある。

終わり

今後も仕様が変わる度にしっかりキャッチアップしていきたいですね。

続きを読む

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

続きを読む

AWS OpsWorksで特定のLayerだけ別ブランチからソースを取得する方法

概要

AWS OpsWorksの場合custom Chef cookbooksの設定がStackに入ってるLayerすべてに同じ設定が適用される。
検証などで特定のレイヤーだけ別のブランチを使いたい場合はLayerのCustom JSONに設定を書くことで別ブランチを使うことができる。

別ブランチのcustom Chef cookbooksを使う方法

スクリーンショット 2017-10-11 23.06.15.png

General SettingsのCustom JSONに設定する。

{
  "opsworks_custom_cookbooks": {
    "scm": {
      "revision": "<branch name>"
    }
  }
}

Appで別ブランチのソースを取得するようにする方法

Appも同様に同じStackに入ってるLayerすべてで同じ設定が適用されるため、特定のLayerだけ別ブランチのソースを使いたい場合はLayerのGeneral SettingsのCustom JSONに設定する。

{
  "deploy": {
    "<app name>": {
      "scm": {
        "revision": "<branch name>"
      }
    }
  }
}

続きを読む

CodeBuildのbuildspec.ymlを別の名前にしたいときの手順

TL;DR

CodeBuildを使っていると、ビルドの成果物が違うからビルドスペックのファイル(buildspec.ymlというもの)の単位で成果物を分けたいということがあります。調べてみると、ドキュメントに1にやり方が載っていたのでやってみました。方向性としては、AWS CLIで変更操作をする手順です。2

(2017-10-13 22:00 追記。 今さっき見てみたら、Consoleでファイル名を指定できるようになっていました。ちょろっと直すならConsoleでいいですね。)

概要

nantoka-buildというプロジェクトのビルドスペックのファイルをnantoka-kantoka-buildspec.ymlに変えたいとします。windowsの例ですが、mac でも linuxでも概ね同じでしょう。

プロジェクトの名前を確認する

Consoleから確認することもできますが、CLIと同じものが見えているか確認する意味も込めてやってみましょう。

>aws codebuild list-projects
{
    "projects": [
        "nantoka-build",
        "kantoka-build-project",
        "test-test-project"
    ]
}

>

各環境とかリージョンとかで色々あると思いますが、Console 
https://ap-northeast-1.console.aws.amazon.com/codebuild/home?region=ap-northeast-1#/projects
で、見えるプロジェクトとおなじビルドプロジェクトが見えていればOKです。nantoka-build以外のプロジェクトも、存在するなら見えていることでしょう。

プロジェクト名を指定して、プロジェクト構造のJSONファイルを取得する

更新の操作はプロジェクト構造のJSONファイルをアップロードする形になります。アップロードするファイルを作るため、現状のファイルをダウンロードします。

>aws codebuild batch-get-projects --names nantoka-build
{
    "projectsNotFound": [],
    "projects": [
        {
            "name": "nantoka-build",
            "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
            "tags": [],
            "artifacts": {
                "packaging": "NONE",
                "type": "CODEPIPELINE",
                "name": "nantoka-artifact"
            },
            "lastModified": 1512341234.783,
            "timeoutInMinutes": 60,
            "created": 1512341234.68,
            "environment": {
                "computeType": "BUILD_GENERAL1_SMALL",
                "privilegedMode": false,
                "image": "aws/codebuild/java:openjdk-8",
                "type": "LINUX_CONTAINER",
                "environmentVariables": []
            },
            "source": {
                "type": "CODEPIPELINE"
            },
            "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3",
            "arn": "arn:aws:codebuild:ap-northeast-1:1234123412341234:project/nantokanantoka"
        }
    ]
}

>

(arnなどは内容を適当にいじっています。表示される項目はあなたのプロジェクトの内容と同じはずです。)

プロジェクト構造のJSONファイルを

コマンドプロンプトに主力されたJSONをファイルに落として修正を入れます。

  • JSONのルートの"projects"の配下を取り出す。
  • "buildspec": "nantoka-kantoka-buildspec.yml"という1行を入れる。JSONなのでカンマ忘れずに。
  • "lastModified" "created" "arn" は消す。JSONの構造が違うというエラーになるので。無くても多分実害ないと想像する次第。本当は正しい書き方あると思いますが未確認です。
update.buildproject.json
{
    "name": "nantoka-build",
    "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
    "tags": [],
    "artifacts": {
        "packaging": "NONE",
        "type": "CODEPIPELINE",
        "name": "nantoka-artifact"
    },
    "timeoutInMinutes": 60,
    "environment": {
        "computeType": "BUILD_GENERAL1_SMALL",
        "privilegedMode": false,
        "image": "aws/codebuild/java:openjdk-8",
        "type": "LINUX_CONTAINER",
        "environmentVariables": []
    },
    "source": {
        "type": "CODEPIPELINE",
        "buildspec": "nantoka-kantoka-buildspec.yml"
    },
    "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3"
}

修正をアップロード

update.buildproject.jsonをカレントディレクトリに置いて、下記コマンドを実行する。

>aws codebuild update-project --cli-input-json file://update.buildproject.json
{
    "project": {
        "name": "nantoka-build",
        "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
        "tags": [],
        "artifacts": {
            "packaging": "NONE",
            "type": "CODEPIPELINE",
            "name": "nantoka-artifact"
        },
        "timeoutInMinutes": 60,
        "environment": {
            "computeType": "BUILD_GENERAL1_SMALL",
            "privilegedMode": false,
            "image": "aws/codebuild/java:openjdk-8",
            "type": "LINUX_CONTAINER",
            "environmentVariables": []
        },
        "source": {
            "type": "CODEPIPELINE",
            "buildspec": "nantoka-kantoka-buildspec.yml"
        },
        "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3"
    }
}

>

Consoleで確認する。

Consoleからプロジェクトを選択し、プロジェクト詳細 -> ビルド仕様の表示 とクリックして進むと、nantoka-kantoka-buildspec.yml と表示されます。


  1. http://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec-ref-name-storage 今のところ日本語にはなってないようです。 

  2. ぶっちゃけた話、Consoleがすぐにでも対応しそうな気もしますが、今時点での手順を残します。 

続きを読む

Serverless FrameworkでAWS Lamda関数を作成する

概要

Serverless Frameworkとは、Lambda、API Gateway、DynamoDBなどを作成、管理、デプロイできるツールです。
Frameworkと付いていますが、ツールです。
この記事では、python3でLambda関数を作成します。

環境

  • CentOS7.2
  • serverless 1.23.0
  • node v6.11.3
  • npm 3.10.10
  • OpenSSL 1.0.2k-fips 26 Jan 2017

npmのインストール

以下の記事を参照
npmのインストール手順

Serverless Frameworkのインストール

slsというディレクトリを作成し、そこで作業を行います。

$ mkdir sls
$ cd sls
$ npm init
$ npm install --save serverless

serverlessコマンドのパスを通します

$ npm bin serverless
$ echo 'export PATH="$HOME/sls/node_modules/.bin/:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile

インストール確認

$ serverless -v
1.23.0

aws credential登録

以下のコマンドで、AWSのキーを登録します。

$ serverless config credentials --provider aws --key XXXXXXXXXXXXEXAMPLE --secret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXEXAMPLEKEY
Serverless: Setting up AWS...
Serverless: Saving your AWS profile in "~/.aws/credentials"...
Serverless: Success! Your AWS access keys were stored under the "default" profile.

Lambda関数の作成

以下のコマンドで、Lambda関数を作成します。

$ serverless create -t aws-python3 -p sample-app

Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/home/vagrant/sls/sample-app"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  ___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.23.0
 -------'

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

オプションの説明ですが、
-pは、Lambda関数名のprefixとなります。

また、-tで実装する言語を選びます。
以下のいずれかを選択します。

  • -t

    • aws-nodejs
    • aws-python
    • aws-python3
    • aws-java-maven
    • aws-java-gradle
    • aws-scala-sbt
    • aws-csharp
    • openwhisk-nodejs

すると、以下のファイルが生成されます。
handler.pyは、Lambda関数のテンプレート、serverless.ymlは設定ファイルになります。

$ ll sample-app/
total 8
-rw-rw-r--. 1 vagrant vagrant  497 Oct 10 04:42 handler.py
-rw-rw-r--. 1 vagrant vagrant 2758 Oct 10 04:42 serverless.yml

関数の情報を設定

serverless.ymlに関数の設定情報が書かれているので、環境合わせて編集します。

serverless.yml
provider:
  name: aws
  runtime: python3.6

# you can overwrite defaults here
- #  stage: dev
-#  region: us-east-1
+  stage: production
+  region: ap-northeast-1

# *snip*

# 関数名などの定義
functions:
-  hello:
-    handler: handler.hello
+  sample-func:
+    handler: handler.main

serverless.ymlで、関数のメソッド名を変更したので、handler.pyも以下のように編集します。

handler.py
-def hello(event, context):
+def main(event, context):

deploy

以下のコマンドでデプロイをします。

$ cd sample-app
$ serverless deploy

Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (389 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............
Serverless: Stack update finished...
Service Information
service: sample-app
stage: production
region: ap-northeast-1
stack: sample-app-production
api keys:
  None
endpoints:
  None
functions:
  sample-func: sample-app-production-sample-func

すると、sample-app-production-sample-funcという名前のLambda関数が作成されます。

Lambda関数の実行

deployが出来たら、以下のコマンドで、Lambda関数を実行することができます。

$ serverless invoke -f sample-func

{
    "statusCode": 200,
    "body": "{"message": "Go Serverless v1.0! Your function executed successfully!", "input": {}}"
}

パラメータを付ける場合は、-dオプションで指定します。
戻り値にinputの項目が増えているのが確認できます。

$ serverless invoke -f sample-func -d '{"key":"value"}'

{
    "statusCode": 200,
    "body": "{"message": "Go Serverless v1.0! Your function executed successfully!", "input": {"key": "value"}}"
}

以下のようなjsonファイルを作成して、パラメータを渡すこともできます。

event.json
{
  "key" : "value"
}

-pオプションでjsonファイルを指定して実行

$ serverless invoke -f sample-func -p event.json
{
    "statusCode": 200,
    "body": "{"message": "Go Serverless v1.0! Your function executed successfully!", "input": {"key": "value"}}"
}

Lambda関数の削除

関数の削除は以下のコマンドで行います。
関連するS3のファイルもすべて消してくれます。
AWS Console上で手動でLambda関数を削除すると、S3のファイルなどが残ってしまいます。

関数のあるディレクトリに移動でして実行します。

$ cd sample-function
$ serverless remove -v

-sオプションで、特定のstageのみを削除することもできます。

$ serverless remove -v -s dev

その他

以下のモジュールで、擬似的にローカルでApi Gateway、DynamoDBを使うことができます。

$ npm install aws-sdk
# 擬似的Api Gateway
$ npm install --save-dev serverless-offline
# 擬似的DynamoDB
$ npm install --save-dev serverless-dynamodb-local

参考

続きを読む

いまからはじめるJavaでAWS Lambda(ラムダ) 前編

いまからはじめるJavaでAWS Lambda(ラムダ)

AWS Lambda関数をJava8で記述し、AWS上 Lambda Functionとしてアップロードし、それをJavaクライアントから呼び出す例をステップ・バイ・ステップで紹介します。

想定読者

  • Javaが書けて、はじめてAWS Lambdaをつかう人
  • いままではnode.jsでLambda関数を作っていたが、わけあってJavaでつくってみようとおもう人(=私のような)

記事構成

TL;DR 前編・後編で書きます

  • 【前編】 JavaでLambda関数(クラウド側)と、クライアント(ローカル側)をお手軽に作る←本稿
  • 【後編】 Lambda関数(クラウド側)の同期型、非同期型の呼び出しタイプ(Invocation Type)と、Javaクライアント側の同期、非同期呼び出し、API Gatewayでの公開

AWS Lambda(ラムダ)とは

  • 自前でサーバーを作ったり、管理したりする必要が無く、コードをAWS Lambdaにあげるだけで各種リクエスト(イベント)の処理が可能な素敵なサービスです。

 http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html
 https://aws.amazon.com/jp/lambda/faqs/

  • AlexaのスキルもAWS Lambdaで作成することができます。

JavaとAWS Lambda

  • JavaでAWS Lambdaの処理(Lambda関数)を記述することが可能
  • Javaクライアントから、直接Lambda関数を呼び出すことも可能
  • Javaに限らないが、API Gatewayというサービスと連携させると、Lambda関数にendpointをつくって公開することができ、Web APIのようにGETやPOSTといったHTTP(S)経由でも呼び出すことが可能

目次

以下のステップを1つずつ説明しつつ実施していきます

  1. AWS Lambda関数をJavaでコーディングする
  2. アップロード用のjarファイルを作る
  3. jarファイルをAWS Lambdaに登録して実行可能(呼び出し可能)にする
  4. AWSコンソール上でテストする
  5. ローカルJavaから、AWS Lambda関数を呼び出しする

1.AWS Lambda関数をコーディングする

Java用ライブラリ読み込み

aws-lambda-java-coreライブラリを追加します。mavenを使う場合は、以下の記述を追加します

maven
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-core</artifactId>
    <version>1.1.0</version>
</dependency>

Lambda関数の中身をコーディングする

ここでは、関数を呼び出すと結果が返るリクエストレスポンス型(RequestResponse)のLambda関数をつくります。

今回はaws-lambda-java-coreライブラリが用意しているRequestHandlerインタフェースを実装して、POJOを入出力できるLambda関数を実装します。

以下は、姓(lastName)と名(firstName)を入力すると、フルネームを出力してくれるLambda関数のサンプルです。

MyLambda.java
package lambda.cloud;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import lambda.cloud.MyLambda.Input;
import lambda.cloud.MyLambda.Output;

public class MyLambda implements RequestHandler<Input, Output> {

  @Override
  public Output handleRequest(Input in, Context context) {

    final Output out = new Output();
    out.in = in;
    out.fullName = in.firstName + "_" + in.lastName;

    return out;
  }

  public static class Input {
    public String firstName;
    public String lastName;
  }

  public static class Output {
    public Input in;
    public String fullName;

  }

}

以下のように、handleRequestメソッドを実装するだけです。引数 Input はリクエスト、戻り値 Output がレスポンスを示します。

 public Output handleRequest(Input in, Context context) {

2. アップロード用のjarファイルを作る

次にLambda関数としてAWS Lambdaで使えるようにするためにはコードをjarファイル(またはzip)にワンパッケージ化してAWS Lambdaにアップロードする必要があります。

このjarファイルは、いまつくったコードの他、依存しているライブラリなどをひとまとめに統合しておく必要があります。

ひとまとめに統合したjarファイル(つまりfat jar)をつくるためにmaven pom.xmlのplugins以下にmaven-shade-pluginを追加しておきます。

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.1.0</version>
        <configuration>
            <createDependencyReducedPom>false</createDependencyReducedPom>
        </configuration>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
            </execution>
        </executions>
    </plugin>

pom.xml

pom.xml全体は、以下のようになります

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>lambda.cloud</groupId>
    <artifactId>mylambda</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>AWS lambda example</name>
    <description>example of AWS lambda
    </description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.1.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

</project>

ソースコードダウンロード

上記サンプルのフルソースコードは以下にあります
https://github.com/riversun/aws_lambda_example_basic_client.git

Eclipseでcloneする場合
File>Import>Git>Projects from Git>Clone URI
https://github.com/riversun/aws_lambda_example_basic_client.git を指定
import as general projectを選択してインポート
– インポート後に、プロジェクト上を右クリックしたメニューでConfigure>Convert to Maven projectを実行

これでEclipse上でMavenプロジェクトとなるので、以後取り扱いが楽になります。

mavenをつかってアップロード用のjarを作成する

  • (1)コマンドラインでjarを作る場合

pom.xmlのあるディレクトリで以下のコマンドを実行します

mvn packgage
  • (2)Eclipse上でjarを作る場合

 Step 1. 右クリックメニューでRun As>Maven buildを選択する
 

 Step 2. Edit configurationsダイアログで、Goalspackage shade:shade と入力しRun
 

(1)(2)いずれの場合でも、/target以下にすべての依存コード・ライブラリを含んだ mylambda-1.0.0.jarが生成されます。

これがAWS Lambdaアップロード用のjarファイルとなります。

3. jarファイルをアップロードしてAWS Lambdaに登録する

AWSコンソールからLambda関数を登録する手順をみていきます

(1)AWS Lambdaを開く
Lambdaサービスを開きます。
img01.png

(2)新しいLambda関数を作る
まだLambda関数をつくっていない場合は以下のような画面になります。

関数の作成(Create function)をクリックします。

img02.png

(3)Lambda関数を[一から作成]する

以下のように設計図(blue print)一覧から選択する画面が出ますが、一から作成(Author from scratch)を選択します。

img03.png

(4)基本的情報画面で名前、ロールを設定する

ここでは、myFunctionという名前のLambda関数をつくっていきます

  • 名前(Name)は「myFunction
  • ロール(Role)は「テンプレートから新しいロールを作成(Create new role from template)
  • ロール名(Role name)は「myRole

入力できたら、関数の作成(Create function)をクリックします

img04.png

(5) jarファイルをアップロードする

img06.png

STEP 1
まず、画面上部にある、ARNを確認しておきます
画面に表示されている、arn:aws:lambda:ap-northeast-1:000000000000:function:myFunction部分をメモしておきます

ご存知ARN(Amazon Resource Name)はAWS リソースを一意に識別するためのもので、Lambda関数実行時にその特定のために使います。

STEP 2
ランタイム(runtime)からJava8を選択する

STEP 3
ハンドラ(Handler)にlambda.cloud.MyLambdaと入力する
ハンドラ名は、パッケージ名.クラス名::メソッド名 のフォーマットで記述します。
さきほど作ったコードにあわせて、パッケージ名が、lambda.cloud、クラス名がMyLambdalambda.cloud.MyLambdaを入力しています。この例では、メソッド名は省略しても動作します。

STEP 4
アップロード(upload)をクリックしてさきほど作ったmylambda-1.0.0.jarをアップロードします。

はい、ここまでで、さきほど自作したLambda関数がAWS Lambdaに登録されました。

4. AWSコンソール上でテストする

アップロードが終わったら、コンソール上からテストしてみます。

(1)テスト用イベントの準備

Lambda関数は 何らかのイベントをトリガーにして起動する という考え方があり、たとえば、S3バケットにオブジェクトが作成されたというイベントをトリガーとして、Lambda関数を発火させる、という使い方があります。

そこで、Lambda関数を実行するための入力のことをイベントと呼びます。

ここでは、イベントをコンソール上から発火させ、Lambda関数の動作を確認します。

画面上部にある、テストイベントの設定(Configure test events)を選択します。

img07.png

すると以下のように、テストイベントの設定(Configure test events)画面が開きますので、ここでテスト用のイベントを作成します。

image08b.jpg

イベント名(Event name)MyEventとして、その下にあるエディットボックスはLambda関数をリクエストするときの本文です。

さきほどつくったPOJOで入出力するLambda関数は、実際の呼び出しではPOJOが自動的にJSONにマップされ、JSONで入出力されます。

そこで以下のようにJSON形式でイベントの本文を入力します

{
  "firstName": "john",
  "lastName": "doe"
}

入力したら、画面下にある作成を押します。

(2)テスト用イベントの実行

いま作成したMyEventを早速実行します。画面右上のテストを押します。

img09.png

ちなみに、「ハンドラーで指定されたファイル名がデプロイパッケージのファイル名と一致しないため、Lambda 関数 「myFunction」はインラインで編集できません。」というメッセージが表示されても気にしなくてOkです。
Lambdaコンソールでは、Java、C# などのコンパイル済み言語のインラインエディタは提供されていません。

(3)実行結果を確認する

テストを押してしばらくすると、結果画面が表示されます。

成功のときは、実行結果: 成功(Execution result:succeeded)と表示されます。その下の▼詳細(details)を押して展開すると結果の詳細を確認できます。

img10.png

Lambda関数の出力も、さきほどのPOJO Outputクラスが以下のようなJSONに変換されます。

入出力用のPOJO
public static class Input {
    public String firstName;
    public String lastName;
  }

  public static class Output {
    public Input in;
    public String fullName;

  }
実行結果
{
  "in": {
    "firstName": "john",
    "lastName": "doe"
  },
  "fullName": "john_doe"
}

5. ローカルJavaから、AWS Lambda関数を呼び出す

さきほど作ったLambda関数 myFunction をJavaプログラムから呼び出します。

ライブラリを読み込む

JavaからAWS Lambda関数をたたく場合には、aws-java-sdk-lambdaライブラリを追加します。mavenを使う場合は、以下を追加します

maven
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-lambda</artifactId>
    <version>1.11.210</version>
</dependency>

Javaクライアント

コードは以下のようになります。(各種設定値はダミーです)

Javaクライアント
public class ExampleLambdaClient {

  public static void main(String[] args) {
    ExampleLambdaClient client = new ExampleLambdaClient();
    client.invokeLambdaFunction();

  }

  private void invokeLambdaFunction() {

    final String AWS_ACCESS_KEY_ID = "ANMNRR35KPTR7PLB3C7D";
    final String AWS_SECRET_ACCESS_KEY = "UKA6EsKY25LJQBEpUvXyYkj8aWKEDnynEZigVPhz";

    AWSCredentials credentials = new BasicAWSCredentials(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY);

    // ARN
    String functionName = "arn:aws:lambda:ap-northeast-1:000000000000:function:myFunction";

    String inputJSON = "{"firstName":"john","lastName": "doe"}";

    InvokeRequest lmbRequest = new InvokeRequest()
        .withFunctionName(functionName)
        .withPayload(inputJSON);

    lmbRequest.setInvocationType(InvocationType.RequestResponse);

    AWSLambda lambda = AWSLambdaClientBuilder.standard()
        .withRegion(Regions.AP_NORTHEAST_1)
        .withCredentials(new AWSStaticCredentialsProvider(credentials)).build();

    InvokeResult lmbResult = lambda.invoke(lmbRequest);

    String resultJSON = new String(lmbResult.getPayload().array(), Charset.forName("UTF-8"));

    System.out.println(resultJSON);

  }
}

AWSCredentials credentials = new BasicAWSCredentials(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY);
  • アクセスキーID(AWS_ACCESS_KEY_ID)とシークレットアクセスキー(AWS_SECRET_ACCESS_KEY)をつかってcredentialsをつくります。

InvokeRequest lmbRequest = new InvokeRequest()
        .withFunctionName(functionName)
        .withPayload(inputJSON);
  • リクエストを生成します。#withFunctionNameでは関数名を指定します。ARN(arn:aws:lambda:ap-northeast-1:000000000000:function:myFunction)を指定するか、関数名(myFunction)を指定します。ここではARNを指定しました

  • #withPayloadでリクエストの本文(JSON)を指定します。
    ここではJSON文字列 “{“firstName”:”john”,”lastName”: “doe”}”;を指定してます。


lmbRequest.setInvocationType(InvocationType.RequestResponse);
  • 呼び出しタイプ(invocation type)を指定します
    ここでは InvocationType.RequestResponse を指定しています。これでRequestResponse型の呼び出しタイプになります。RequestResponseにしておくと、処理結果を受け取ることができます。

InvokeResult lmbResult = lambda.invoke(lmbRequest);
String resultJSON = new String(lmbResult.getPayload().array(), Charset.forName("UTF-8"));
  • Lambda関数を呼び出して、結果(JSON)を受け取ります。

Javaクライアントのソースコード

クライアント側のフルソースコードは以下においてあります
https://github.com/riversun/aws_lambda_example_basic_client.git

まとめ

  • Javaでも比較的簡単にAWS lambdaの関数を記述できました
  • Javaのクライアントから直接 Lambda関数を呼び出せました

サンプルはEclipse(Oxygen)で作成しましたが、特段プラグインなど必要ありません
(AWS SDK Plugin for Eclipseも不要。あれば、もっと手軽になりますが)

  • 後編では、Lambda関数およびJavaクライアント側の同期/非同期についてや、API Gatewayについて書きます。

おまけ

  • 2009年頃ですがコードだけ書いたら後はおまかせ、課金は使った分だけというサービスの元祖「Google App Engine (GAE)」が登場したときは衝撃をうけました。独特の制約が多かったものの、とてもお世話になりました。

  • 今度はその元祖!?が、Google Cloud Functionsを投入しています。今現在はβで実績はこれからだとおもいますが、今後はそちらも選択肢に入ってきそうです。

続きを読む