ちょっと話題の記事

ApexでAWS Lambdaファンクションを管理する

2016.03.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

以前AWS LambdaファンクションをGulpでデプロイというブログを書きましたが、今回はAWS Lambdaファンクションの管理ツールApexをご紹介したいと思います。

Apexとは

@TJ Holowaychukさんが中心となって開発されている、AWS Lambdaファンクションをビルド、デプロイ、管理するためのツールです。Mediumでも語られていますが、TJ Holowaychukさんはサーバーレスなアーキテクチャが実現できるAWS Lambdaに魅力を感じつつも、AWS Lambdaのユーザビリティの低さに不満を持っており、その問題を解決するためにApexの開発に至ったようです。

Apexの特徴

  • AWS Lambdaがネイティブにサポートしていない言語をサポート(本ブログ記事執筆時点ではGolangをサポート)
  • バイナリから簡単にインストール可能(CI/CDへの組み込みが容易)
  • apexコマンド実行時のフックをサポート(ビルド時にコードをトランスパイルする、デプロイ時にコードの構文チェックを実行する、など)
  • "バッテリー同梱"(多くの機能が標準で組み込まれている)、それらの機能を選択して使うことができる
  • プロジェクト単位でファンクションとリソースの管理が可能
  • 設定の継承、オーバーライドが可能(プロジェクトレベルでの設定を、ファンクションレベルで継承/オーバーライドできる)
  • apexコマンド実行時にJSONストリームを渡せる
  • 透過的なデプロイパッケージ(zipファイル)の作成(デプロイ実行時に自動的にzipファイルが生成される)
  • Terraformのインテグレーション(AWSリソースの作成にTerraformが利用出来る)
  • 「.apexignore」で特定ファイルをデプロイパッケージからが除外できる
  • ファンクションのロールバックに対応
  • ファンクションのログをTailで参照可能
  • ファンクションのデプロイの並列実行
  • Dry-runでデプロイ前にファンクションの変更箇所を確認可能
  • VPCサポート

個人的にはGolangのサポートが目を引きますが、その他にも便利機能が満載です。詳しくは後ほど触れたいと思います。

サポートされているランタイム

本ブログ記事執筆時点の最新版であるv0.7.2でサポートされているランタイムは以下の4つです。

  • Node.js
  • Golang
  • Python
  • Java

Apexを使ってみる

それでは実際にApexの使い方について見ていきたいと思います。今回は以下の環境でApexの動作を確認しました。

  • OS : OS X Yosemite 10.11.4
  • Apex : v0.7.2

インストール

以下を実行すると、/usr/local/binディレクトリ下にapexのバイナリファイルがインストールされます。

$ curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh

動作確認のため、apexのバージョンを確認してみましょう。

$ apex version
Apex version 0.7.2

アップグレードは以下のコマンドで簡単に実行できます。

$ apex upgrade

OS Xの他、Linux、OpenBSD、Windows用のバイナリが用意されています。

AWSのクレデンシャル

Apexを使うには、ApexにAWSのクレデンシャル情報を渡す必要があります。これにはAWS CLIのそれと同じ方法が使えます。

(1) 環境変数にクレデンシャル情報を設定する

  • AWS_ACCESS_KEY アクセスキー
  • AWS_SECRET_KEY シークレットアクセスキー
  • AWS_REGION リージョン

(2) ~/.aws/config~/.aws/credentialsにクレデンシャル情報を記載する

[default]
output = json
region = ap-northeast-1
[default]
aws_access_key_id = XXXXXXXXXX
aws_secret_access_key = XXXXXXXXXX

(3) コマンド実行時にプロファイルを指定する

~/.aws/credentialsに[default]以外のプロファイルを設定している場合は、apexコマンド実行時にプロファイルを指定することもできます。この場合リージョンの設定は~/.aws/configからは読み込まれないようで、あらかじめ環境変数AWS_REGIONで設定しておくか、apexコマンド実行時に--regionフラグで指定する必要があります。

$ apex --profile <プロファイル名> --region <リージョン名> deploy

コマンド実行時に毎回プロファイル名とリージョン名を渡すのは若干面倒なので、複数のクレデンシャル情報を使い分ける場合は、direnvなどを使ってプロジェクトディレクトリ毎に(1)の環境変数を設定するのが良いかと思います。

Apexプロジェクトの作成

v0.7.0でプロジェクトジェネレーターの機能が追加されました。これを使ってApexプロジェクトの雛形を作成します。プロジェクト用のディレクトリを作成&移動し、apex initコマンドを実行します。

$ mkdir apex-sample
$ cd  apex-sample
$ apex init

始めにProject nameProject descriptionを入力します。Project nameはAWS Lambdaファンクションのプレフィックスとして使用されるます(この設定は後で変更することが可能です)。

Enter the name of your project. It should be machine-friendly, as this
is used to prefix your functions in Lambda.

  Project name: apex-sample

Enter an optional description of your project.

  Project description: apex-sample

「Apexの特徴」でも触れましたが、v0.7.0からApexにTerraformがインテグレーションされており、Terraformを使ってAWSリソースを作成できるようになっています(apex infraというコマンドが追加されており、これがTerraformコマンドのエイリアスのような形になっています)。

以降の入力項目は、Terraformを使う場合と使わない場合とで異なります。なお本ブログ記事ではTerraformについては解説しませんので、Terraformについて知りたい方は以下のブログエントリなどを参照ください。

Terraformを使わない場合

AWS Lambdaファンクションに割り当てるIAMロールの入力を求められるので、あらかじめ作成しておきます。

## no を選択
Would you like to manage infrastructure with Terraform? (yes/no) no

## AWS Lambdaファンクションに割り当てるIAMロールのarnを入力する
Enter IAM role used by Lambda functions.

  IAM role: arn:aws:iam::5xxxxxxxxxxx:role/apex_lambda_execution

  [+] creating ./project.json
  [+] creating ./functions

Setup complete!

Next step:
  - apex deploy - deploy example function

Terraformを使う場合

前提条件として、事前にTerraformをインストールしておく必要があるのでご注意ください。またTerraformのstateファイルをS3で管理する場合は事前にS3のバケットを作成しておく必要があります。

## yes を選択
Would you like to manage infrastructure with Terraform? (yes/no) yes

  [+] creating ./project.json
  [+] creating ./functions
  [+] creating ./infrastructure
  [+] fetching modules

## TerraformのstateファイルをS3にストアする場合は yes を選択
Would you like to store Terraform state on S3? (yes/no) yes

Enter the S3 bucket name for managing Terraform state (bucket needs to exist).

## stateファイルをS3にストアする場合は、ストア先にS3バケット名を入力(バケットはあらかじめ作成しておく)
S3 bucket name: apex-terraform-state

  [+] setting up remote state in bucket "apex-terraform-state"

Setup complete!

Next steps:
  - apex infra plan - show an execution plan for Terraform configs
  - apex infra apply - apply Terraform configs
  - apex deploy - deploy example function

プロジェクトディレクトリ

apex initで作成されたプロジェクトディレクトリを見てみます。

Terraformを使わない場合

.
├── functions
│   └── hello
│       └── index.js
└── project.json

project.jsonがプロジェクト全体の設定です。必須なのはname(プロジェクト名)です。その他メモリやタイムアウト値などのファンクションのデフォルト値をここで定義します。設定可能な項目については公式ドキュメントを参照ください。

project.json

{
  "name": "apex-sample",
  "description": "apex-sample",
  "memory": 128,
  "timeout": 5,
  "role": "arn:aws:iam::5xxxxxxxxxxx:role/apex_lambda_execution",
  "environment": {}
}

functionsディレクトリ以下にファンクション毎にディレクトリを作成し、その中にファンクション本体(index.jsなど)を作成していきます(デフォルトで、サンプルとしてNode.jsのhelloファンクションが作成されます)。

ファンクション毎の設定はfunction.jsonファイルで定義します。このfunction.jsonはファンクション毎のディレクトリ(helloなど)に配置します。必須項目はdescriptionruntimememorytimeoutroleの5つです。ただしdescription以外の定義はproject.jsonから継承できるので、project.jsonで定義済みのデフォルト値を使う場合はfunction.jsonでの定義は不要です。

project.jsonで未定義の項目がある場合、またはproject.jsonの定義をオーバーライドした場合はfunction.jsonに定義を追加します。

定義できる項目はほぼproject.jsonと同じです。こちらも詳しくは公式ドキュメントを参照ください。

function.json

{
  "description": "Node.js example function",
  "runtime": "nodejs",
  "memory": 256,
  "timeout": 10
}

Terraformを使う場合

.
├── functions
│   └── hello
│       └── index.js
├── infrastructure
│   ├── main.tf
│   └── modules
│       └── iam
│           ├── iam.tf
│           └── outputs.tf
└── project.json

Terraformを使うことを選択した場合はinfrastructureディレクトリが作成されます。中身はまんまTerraformです。デフォルトでCloudWatch Logsへのフルアクセス権限が付与されたIAMロールを作成するためのtfファイルが作成されるので、この状態でapex infra applyを実行すると新規に「lambda_function」という名前のIAMロールが作成され、outputとしてIAMロールのarnが表示されます。

それ以外のファイルやディレクトリ構成は基本的にTerraformを使わない場合と同じですが、Terraformでロールを新規作成することを想定しているためproject.jsonroleの定義は手動で追加する必要があるのでご注意ください。

project.json

{
  "name": "apex-sample",
  "description": "apex-sample",
  "memory": 128,
  "timeout": 5,
  "environment": {}
}

ファンクションのデプロイ

試しにデフォルトで作成されるhelloファンクションをデプロイしてみます。index.jsを以下のように書き換えます(後ほどファンクションの動作確認をするため)。

index.js

console.log('starting function')
exports.handle = function(e, ctx) {
  console.log('processing event: %j', e)
  ctx.succeed({ hello: e.hello })
}

helloディレクトリ下に以下のようなfunction.jsonを作成します。

function.json

{
  "description": "Node.js example function",
  "runtime": "nodejs"
}

apex deploy <ファンクション名>を実行すると指定したファンクションがデプロイされます。

$ apex deploy hello

• creating function         function=hello
• created alias current     function=hello version=1
• function created          function=hello name=apex-sample_hello version=1

apex deployのようにファンクション名を指定しなかった場合はfunctionsディレクトリ以下の全てのファンクションがデプロイされます。

ファンクションの実行

apex invokeでファンクションを実行できます。プロジェクトディレクトリ直下に以下のようなevent.jsonを作成して、これを渡してみます。

{
  "hello": "world"
}
$ apex invoke hello < event.json

{"hello":"world"}

このほかにもイベントを渡す方法がいくつかありますので、詳しくは公式ドキュメントを参照ください。

ファンクション一覧の表示

apex listで、プロジェクトに含まれるファンクションの一覧が表示されます(デプロイされていないものも含めて表示されます)。

$ apex list

hello
  description: Node.js example function
  runtime: nodejs
  memory: 128mb
  timeout: 5s
  role: arn:aws:iam::551480077585:role/apex_lambda_execution
  handler: index.handle
  current version: 1

デプロイされていないファンクションについてはcurrent versionは表示されません。

ファンクションの削除

apex delete <ファンクション名>でデプロイしたファンクションを削除することができます。ファンクション名を指定しなかった場合は全てのファンクションが削除対象となります。

$ apex delete hello

Are you sure? (yes/no) yes
   • deleting                  function=hello
   • function deleted          function=hello

ファンクションのビルド

apex deployを実行すると自動的にビルド処理(zipファイルの作成)が実行されるので通常はビルド処理を単体で実行する必要はありませんが、作成されたzipファイルの中身を確認したい場合にはapex buildでビルド処理のみを実行します。

$ apex build hello > out.zip

ファンクションのロールバック

ファンクションを以前のバージョンにロールバックしたい場合はapex rollback <ファンクション名> <バージョン番号>を実行します。バージョン番号を指定しなかった場合は1つ前のバージョンにロールバックされます。

$ apex rollback hello

• rolling back              function=hello
• rollback to version: 9    function=hello
• function rolled back      current version=9 function=hello

ログの表示

apex logs <ファンクション名>でファンクションの実行ログを表示できます。ファンクション名を指定しなかった場合は全てのファンクションのログが表示されます。

$ apex logs hello

/aws/lambda/apex-sample_hello 2016-03-22T17:51:05.173Z  770qu830atjmfhrq    start simple
/aws/lambda/apex-sample_hello START RequestId: a6184505-f056-11e5-8fe2-51cd1249c703 Version: 9
/aws/lambda/apex-sample_hello 2016-03-22T17:51:05.209Z  a6184505-f056-11e5-8fe2-51cd1249c703    processing event: {"hello":"world"}
/aws/lambda/apex-sample_hello END RequestId: a6184505-f056-11e5-8fe2-51cd1249c703
/aws/lambda/apex-sample_hello REPORT RequestId: a6184505-f056-11e5-8fe2-51cd1249c703    Duration: 35.33 ms  Billed Duration: 100 ms     Memory Size: 128 MB Max Memory Used: 8 MB

--filterでログをフィルタリングしたり(例えば--filter errorでerrorのみ表示するなど)、--startでログの表示期間を指定することも可能です(--start 1hで直近1時間のログを表示など。デフォルトでは直近5分のログが表示されます)。

メトリクスの表示

apex metrics <ファンクション名>でファンクションの実行回数、実行時間などのメトリクスを表示することができます。ファンクション名を指定しなかった場合は全てのファンクションのメトリクスが表示されます。

$ apex metrics hello

  hello
    invocations: 5
    duration: 60ms
    throttles: 0
    error: 0

Dry-run

apexコマンドに--dry-runフラグをつけると、文字通りdry runが実行可能です。デプロイ前のhelloファンクションがある状態でapex deploy --dry-runを実行すると以下のように出力されます。

$ apex deploy hello --dry-run

  + function apex-sample_hello
    runtime: nodejs
    memory: 128
    timeout: 5
    handler: index.handle

  + alias apex-sample_hello
    alias: current
    version: 1

+はリソースが作成されることを表します。

ファンクションのデプロイ後にfunction.jsonmemoryの設定を128から256に変えてapex deploy --dry-runを実行してみます。

$ apex deploy hello --dry-run

~ config apex-sample_hello
   memory: 128 -> 256

 + alias apex-sample_hello
   alias: current
   version: $LATEST

~はリソースが更新されることを表します。

--dry-runapex deleteでも使えます。リソースの削除は-で表されます。

$ apex delete hello --dry-run
Are you sure? (yes/no) yes
  - function apex-sample_hello

フック

以下のフックがサポートされており、apexのライフサイクルの中で任意のコマンドを実行することが可能です。

  • build ビルド実行中(使用例:コンパイルを実行する)
  • deploy デプロイ実行前(使用例:テスト、構文チェック)
  • clean デプロイ実行後(使用例:ビルドで生成されたファイルを削除する)

これらのフックはproject.jsonまたはfunction.jsonで定義します。具体的な使用方法として、Apexのgithubレポジトリにdeployフックを使用してbrowserifyを実行する例があります。 この例ではproject.jsonbuildフックでbrowserifyでindex.jsをmain.jsにトランスパイルし、cleanフックでそのmain.jsを削除する、という処理が定義されています。

環境変数

Apexでは環境変数を使うことが可能です。環境変数はファンクションのデプロイ時(apex deploy実行時)に--envフラグで定義します。

以下は環境変数「LOGGLY_TOKEN」を定義して「using-env」ファンクションをデプロイする例です。

$ apex deploy --env LOGGLY_TOKEN=15xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx using-env

実態としては、ビルド時に.env.json_apex_index.jsが生成され、デプロイパッケージ(zipファイル)に同梱される形となっています。

.env.json

{"LOGGLY_TOKEN":"15xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}

_apex_index.js

try {
  var config = require('./.env.json')
  for (var key in config) {
    process.env[key] = config[key]
  }
} catch (err) {
  // ignore
}

exports.handle = require('./index').handle

ファンクション内では、Node.jsであればprocess.env.LOGGLY_TOKENで、Golangであればos.Getenv("LOGGLY_TOKEN")で値を取得します。

index.js

console.log('start using-env LOGGLY_TOKEN=%s', process.env.LOGGLY_TOKEN)
exports.handle = function(e, ctx) {
  console.log('processing event: %j', e)
  ctx.succeed({
    user_name: process.env.LOGGLY_TOKEN
  })
}

環境変数は--envフラグで定義する意外にproject.jsonまたはfunction.jsonenvironmentで定義することも可能です。

function.json

{
  "description": "Node.js example function using environment variables",
  "runtime": "nodejs",
  "memory": 256,
  "timeout": 10,
  "environment": {
    "LOGGLY_TOKEN": "15xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  }
}

.apexignore

.apexignoreで、特定のファイルをデプロイパッケージ(zipファイル)から除外することができます。書式は.gitignoreと同じです。例えばGolangのソースファイルをデプロイパッケージから除外するには以下のような.apexignoreを作成して、プロジェクトディレクトリまたはファンクション毎のディレクトリに配置します。

.apexignore

*.go

なお、デフォルトで.apexignore自身とfunction.jsonはデプロイパッケージから除外される設定になっているので.apexignoreで定義する必要はありません。

まとめ

"Batteries included"の言葉通り豊富な機能を持ったApexですが、とてもシンプルで使いやすいと感じました。Terraformとのインテグレーションも今後の進化が楽しみです。

Apexのgithubレポジトリには各ランタイム毎のサンプルプロジェクトがあるので、GolangTerraformでAPI Gatewayを作成する例も試してみたいと思います。

参考