BECでインフラ+サーバーサイドエンジニアあたりをやってる森と申します。休日はロードバイクでポタリングしたりイベントに参加したりして過ごしています。 今回はAWSのマネージドサービスであるaws lambda(jvm)を利用してAMIを定期的にバックアップする方法をscalaを使った方法で紹介していこうと思います。 0. 読むべき人とゴール awsを利用しているが、lambdaを使ったことが無い人やこれから使い始めようと思っている人がaws lambaの概要を理解して実践してみる。 1. aws lambdaとは AWS lambdaは、サーバーのプロビジョニングや管理無しでlambda関数と呼ばれるコードを実行できるコンピューティングサービスです。OSやミドルウェアの設定やチューニングを行ったり監視設定を行ったりする必要はありません。 これらはAWS lambdaが面倒を見てくれます。
AWS lambdaはデーモンやサービスなどの常駐型のアーキテクチャでは無く、イベント駆動型のアプリケーション実行アーキテクチャです。上の図で示したようなAWSのマネージドサービスからイベント(S3バケットへの保存、DynamoDBストリームへデータを追加、SNSへのメッセージング、KINESISストリームへのイベント送信)をプッシュ or プルしてlambdaにアップロードしたlambda functionと呼ばれるコードを処理させることができます。 1.1. ユースケース ではどんなユースケースがあるか。例えば以下のように使用することができます。 ・サービスの利用ユーザのアップロードした画像をS3に保存した後、リサイズ処理をしてS3/CloudFrontに保存し直したい。
この場合、S3がイベントをプッシュしlambdaでリサイズ処理をして再度S3/CloudFrontにアップロードするような処理の流れになります。 ・CIのために用意されたインスタンスやBlueGreenデプロイメントのために構築された待機系環境は、会社の定時後には使用しないので自動的に停止+起動するようにしておきたい。
毎朝や定時後など定期的にスケジューリングするイベントはCloudWatchで行います。詳しくは後述しますが、毎朝10:00にイベントを送るようにcronの記述を書くことができます。 後はlambdaで、ElasticBeantalkの環境を削除・構築するようにプログラムしてあげれば実現できます。
1.2. aws lambdaを使用するメリット 先述した通りにaws lambdaはサーバーレスなので、エンジニアがサーバーインスタンスを意識する必要がありません。今まではバッチ処理や、kinesisストリームのコンシューマなどを実行するためのEC2インスタンスを用意する必要がありましたが、この管理コストが省けるというわけです。またEC2インスタンスはコンピューティング性能とその実行時間で料金が決まるため、バッチの実行時以外に稼働させておくとそれだけ無駄に料金を支払う必要がありました。AWS lambdaは関数の実行時間とリクエスト数で料金が決まりますので、実行時間が少なければ少ないほど料金を抑えられます。
2. AMIを定期バックアップするLambda Functionを書いてみる それでは実際に、lambdaを設定していきます。 AMIスナップショットを定期的にバックアップするためのLamda functionを例として作成していきたいと思います。 2.1. 要件と処理の流れ 毎朝10:00に特定のEC2インスタンスのAMIバックアップを登録する
1) cloudwatchがスケジューリングされたイベントを発火 2) lambdaがイベントを受信。aws-java-sdk-ec2を使ってAMIの作成リクエストを送信する。 3) AMIスナップショットが作成されs3に保存される。 2.2. プロジェクト構成
主に記述するのは、太字で示したbuild.sbt, plugins.sbt, AMIRegister.scalaの4ファイルです。Intellijからsbtプロジェクトを作成しました。
.
├── README.md
├── build.sbt
├── project
│ ├── build.properties
│ ├── plugins.sbt
│ └── project
└── src
├── main
│ ├── scala
│ │ └── org
│ │ └── sample
│ │ └── batch
│ │ └── AMIRegister.scala
2.1. plugins.sbt outputは依存関係などを含めた実行可能なjarファイルを作れるようにプラグインを追加します。 以下のようにplugins.sbtファイルにresolversとaddSbtPluginを記述してください。
logLevel := Level.Warn resolvers += Resolver.url("bintray-sbt-plugins", url(" http://dl.bintray.com/sbt/sbt-plugin-releases "))(Resolver.ivyStylePatterns) addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0") 2.2. build.sbt AMIスナップショットを登録するためのSDK(aws-java-sdk-ec2)と、lambda(aws-lambda-java-core)を追加します。適宜versionを最新にしてください。
import sbtassembly.AssemblyKeys
name := "aws-lambda-sample"
version := "1.0"
scalaVersion := "2.11.8"
scalacOptions += "-deprecation"
scalacOptions += "-feature"
libraryDependencies ++= Seq(
"com.amazonaws" % "aws-lambda-java-core" % "1.1.0",
"com.amazonaws" % "aws-lambda-java-events" % "1.3.0",
"com.amazonaws" % "aws-java-sdk-ec2" % "1.11.22"
)
assemblyJarName in assembly := "aws-lambda-sample-0.1-SNAPSHOT.jar"
2.3 AMIRegister.scala instanceIdとアプリケーションを識別するためのprefixを受け取って、prefix-現在日時-uuidの名前でAMIを登録する関数を以下に記述します。
package org.sample.batch
import java.text.SimpleDateFormat
import java.util.{Date, UUID}
import com.amazonaws.ClientConfiguration
import com.amazonaws.auth._
import com.amazonaws.regions.{Region, Regions}
import com.amazonaws.services.ec2.{AmazonEC2, AmazonEC2Client}
import com.amazonaws.services.ec2.model.{CreateImageRequest, CreateImageResult}
import com.amazonaws.services.lambda.runtime.Context
import scala.collection.JavaConverters._
class AMIRegister(instanceId: String, instancePrefix: String) {
def handleRequest(event: Any, context: Context): java.util.Map[String, String] = {
val lambdaLogger : LambdaLogger = context.getLogger()
val formatter = new SimpleDateFormat("yyyyMMdd")
val fluctuation = UUID.randomUUID().toString
val imageName: String = s"${instancePrefix}-${formatter.format(new Date())}-${fluctuation}"
if (instanceId == null || "".equals(instanceId)) throw new IllegalArgumentException("instance id is null or empty.")
val provider : AWSCredentialsProvider = new AWSCredentialsProviderChain(new EnvironmentVariableCredentialsProvider())
val amazonEC2: AmazonEC2 = Region.getRegion(Regions.AP_NORTHEAST_1).createClient(classOf[AmazonEC2Client], provider, new ClientConfiguration())
val createImageRequest: CreateImageRequest = new CreateImageRequest(instanceId, imageName).withDescription("instance backup").withNoReboot(true)
val image :CreateImageResult = amazonEC2.createImage(createImageRequest)
lambdaLogger.log(s"created ami ${image.getImageId()}. instanceId -> ${instanceId}, imageName -> ${imageName}")
Map("result" -> "ok").asJava
}
}
class SampleScheduledAMIRegister extends AMIRegister("i-xxxxxx", "label")
2.3.1 関数シグネチャ lambda functionはeventとcontextを受け取りjava.util.Map[String, String]を返却する関数である必要があります。インターフェースを実装するパターンでは無いので,関数名には何をつけても良いです。
def handleRequest(event: Any, context: Context): java.util.Map[String, String] = {
event引数には、イベントトリガー時にlambdaに送られてくるメッセージの型を設定します。今回は、eventメッセージを読み込まないので使用せずAnyとしています。 例えばここはS3 putであれば以下のようなメッセージが送られて来ます。
{
"Records": [
{
"eventVersion": "2.0",
"eventTime": "1970-01-01T00:00:00.000Z",
"requestParameters": {
"sourceIPAddress": "127.0.0.1"
},
"s3": {
"configurationId": "testConfigRule",
"object": {
"eTag": "0123456789abcdef0123456789abcdef",
"sequencer": "0A1B2C3D4E5F678901",
"key": "HappyFace.jpg",
"size": 1024
},
"bucket": {
"arn": "arn:aws:s3:::mybucket",
"name": "sourcebucket",
"ownerIdentity": {
"principalId": "EXAMPLE"
}
},
"s3SchemaVersion": "1.0"
},
"responseElements": {
"x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH",
"x-amz-request-id": "EXAMPLE123456789"
},
"awsRegion": "us-east-1",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "EXAMPLE"
},
"eventSource": "aws:s3"
}
]
}
このメッセージを受け取る場合には、ANYでは無くaws-lambda-java-eventsパッケージに含まれるcom.amazonaws.services.lambda.runtime.events.S3Eventを指定します。 2.3.2. CloudWatch logsへのロギング contextからロガーを取得し、作成されたimegeのIdなどをロギングしています。 この出力は、CloudWatch logsに蓄積されます。
val lambdaLogger : LambdaLogger = context.getLogger()
~~~
~~~
lambdaLogger.log(s"created ami ${image.getImageId()}. instanceId -> ${instanceId}, imageName -> ${imageName}")
2.3.3. AWSCredentialsProvider
val provider : AWSCredentialsProvider = new AWSCredentialsProviderChain(new EnvironmentVariableCredentialsProvider())
AWS CLIやSDKを使用して、AWSのAPIにアクセスするためにはsecretKey及びaccessKeyの情報が必要です。実際にはBasicAWSCredentialsの引数に記述する方法や、AwsCredentials.propertiesをクラスパスに用意してClasspathPropertiesFileCredentialsProviderに読ませる方法がありますが、これらの方法はソースコード上に直接記述が必要になってしまいます。このような方法は、プロジェクトのソースコードが閲覧できるユーザが誰でもその権限を得ることができてしまうことや、うっかりと誰でもアクセスできる所へコードをアップし鍵を流出させてしまうなどのリスクがあります。またIAM roleの変更のためにリリース作業が必要なり管理コストが増えるということも考えられます。これらの観点からソースコード上にハードコーディングすべきではありません。 AWS lambdaコンテナは、設定したIAM roleのsecretKeyとaccessKeyがシステムの環境変数のAWS_ACCESS_KEYとAWS_SECRET_KEYにそれぞれ設定された状態で起動するようになっています。この設定を利用するためにAWSCredentialsProviderを実装しているEnvironmentVariableCredentialsProviderを使用しています。 2.3.4. AMIスナップショットの登録 こう書くんだってだけの話ですが、withNoRebootはtrueにしておく必要があります。 AWSのコンソール上でもそうですが、そのままやるとサーバーが再起動してしまいます。
val amazonEC2: AmazonEC2 = Region.getRegion(Regions.AP_NORTHEAST_1).createClient(classOf[AmazonEC2Client], provider, new ClientConfiguration())
val createImageRequest: CreateImageRequest = new CreateImageRequest(instanceId, imageName).withDescription("instance backup").withNoReboot(true)
val image :CreateImageResult = amazonEC2.createImage(createImageRequest)
2.4. ビルド 次のを実行すると、targetフォルダの中にjarファイルができてくれます。
sbt assembly
2.5. イベントソースの選択 AWSコンソールからlambdaを選択し、「create a lambda function」ボタンを押してください。すると以下のようにblueprint(イベントソースとlambda functionの設定サンプル)を選択する画面になるので、Filterにcanaryと入力してください。 lambda-canaryがcronでeventを実行するために必要なテンプレートになるので、これを選択してください。
2.6. イベントトリガーの設定 次に、イベントトリガーの設定を行います。 イベントトリガー名(rule name)、イベントの説明(rule description)、Schedule expressionを入力してください。 Schedule expressionでは、cron記述とrateを選択することができます。 トリガーの時刻を決め打ちしたいならばcronを使用してください。 rateは設定した後に即時に実行され以降は、その時点からの時間間隔で実行されます。今回は朝10時にスナップショットを登録するとしたいのでcronを選択します。
cron式は以下のフォーマットで記述します。
cron(Minutes Hours Day-of-month Month Day-of-week Year)
時刻はUTCになっているので日本時間の10時に実行したいのであれば、9時間を差し引いた時刻で記述する必要があります。 毎日朝10時は以下のようになります。
cron(0 1 * * ? *)
詳細な設定値や他のサンプルに関してはこちらを参照してください。
最後にEnable triggerを押してNextを押してトリガーの設定は完了です。
2.7. Lambda functionの設定 lambda functionの設定を行います。 Name, Description, Runtimeを入力してください。 Runtimeには、現在java8, NodeJS, Nodejs 4.3, python 2.7が利用できるようです。 今回はscalaのコードを実行しますのでjava8を選択します。 * javaの場合はAWSコンソール上でプログラムを記述することができません。必ずzip or jarを作成してアップロードする必要があります。
選択すると、lambda function codeをアップロードできるようになると思いますので、sbt assemblyで作成したjar ファイルを選択してください。Handlerは、以下の形式で記述する必要があります。
${package}.${className}::${handlerMethod}
例えば、今回のlambda functionであれば以下のようになります。
org.sample.batch.SampleScheduledAMIRegister::handleRequest
Roleは、lambdaはcloudWatchlogsの権限が必要になるので、以下のような設定を行う必要があります。 「Create a custom role」を選択すれば、作成してくれるようになっているので自分で作成するのが面倒な場合は活用しましょう。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
Roleの作成が完了したら、Nextを押してPreviewし問題無ければ、Create Functionを実行してください。 2.8. テスト アップロードしたjarが意図した通りに動作するかどうかをテストします。AWSコンソールからLambdaを選択し左のタブのFunctionsから作成したLambda function名を選択しましょう。 次のような画面が表示されるのでTestと書かれた青いボタンを押してください。
AWSコンソール -> EC2 -> AMIを確認して新しくAMIスナップショットが作成されたらOKです。お疲れさまでした。
3. まとめ
・AWS lambda はイベント駆動型のコンピューティングサービス ・Lambda functionを作成するだけで利用できるサーバーレスアーキテクチャ 4. その他 4.1. そもそもAMIスナップショットなんで撮ろうと思ったん そもそもステートレスなサービスをEC2で実行しストレージはRDSを使用するという構成であれば、RDSでスナップショットを定期的に取ることができるのでAMIをバックアップする必要は無いかもしれませんが、Gozalと並行で走らせているサービスではRDS利用コストを考慮してEC2内にMySQLを起動させている(サービスの内容とRDSの利用コストを考慮した結果)ので定期的にバックアップする方法が必要でした。