ぺい

渋谷系アドテクエンジニアの落書き

ServerlessでAWS Lambdaのデプロイをいい感じにする

Serverless Frameworkとは

f:id:tikasan0804:20180627074913p:plain

github.com

通常AWS Lambdaを使う上で面倒な手順としてある。

  • 関数を作成する
  • AWS マネジメントコンソールからLambdaの管理画面にアクセスする
  • 関数をアップロードする(圧縮するとかもある)
  • トリガーとなるイベントの設定など

上に書いたような手間を解消してくれるのが今回紹介する「Serverless Framework」(以下Serverless)です。

http://tikasan.hatenablog.com/entry/2018/06/27/083316

AWSしか使わない人へ

Serverless Frameworkは様々なクラウドへのサーバーレスをアプリケーションの展開を考えている人にはうってつけですが、AWSしか使わないなら、AWS SAMがおすすめでした。

tikasan.hatenablog.com

インストール

Serverlessのインストールにはnode(v4以上)の環境が必要です。

❯ node -v
v10.0.0

❯ npm install -g serverless
...

+ serverless@1.27.3
added 310 packages in 19.063s

❯ serverless -v
1.27.3

IAMユーザーの作成

ServerlessはAWS APIを使って操作をします。なので、Adminstrator Acessを付与したServerless用のユーザーの作成をして、そのユーザーを使って色々な操作をすることになります。(defaultの自分のアカウントでも良いですが)

f:id:tikasan0804:20180627072752p:plain

f:id:tikasan0804:20180627072756p:plain

f:id:tikasan0804:20180627072800p:plain

❯ vim ~/.aws/credentials

[default]
aws_access_key_id =  hoge
aws_secret_access_key = fuga

[serverless]
aws_access_key_id = hoge2
aws_secret_access_key = fuga2
❯ vim ~/.aws/config

[default]
output = json
region = ap-northeast-1

[serverless]
output = json
region = ap-northeast-1

credentialsどうする問題は、公式が結構頑張って書いてくれてるので、そこ見たら良さそう。
Serverless Framework - AWS Lambda Guide - Credentials
今回は、専用のユーザーを作成するパターンでやっていきます。

事始め

さっそくServerlessのプロジェクトを作成してみましょう。ここからは長ったらしい serverless ではなく、短縮形の sls を使っていきます。slsには、templeateを作成する機能があり、これを使うと良い感じのsampleを見ることが出来ます。

選べるテンプレートは以下の通り。(すごい)

 Template "true" is not supported. Supported templates are: "aws-nodejs", "aws-nodejs-typescript", "aws-nodejs-ecma-script", "aws-python", "aws-python3", "aws-groovy-gradle", "aws-java-maven", "aws-java-gradle", "aws-kotlin-jvm-maven", "aws-kotlin-jvm-gradle", "aws-kotlin-nodejs-gradle", "aws-scala-sbt", "aws-csharp", "aws-fsharp", "aws-go", "aws-go-dep", "azure-nodejs", "fn-nodejs", "fn-go", "google-nodejs", "kubeless-python", "kubeless-nodejs", "openwhisk-java-maven", "openwhisk-nodejs", "openwhisk-php", "openwhisk-python", "openwhisk-swift", "spotinst-nodejs", "spotinst-python", "spotinst-ruby", "spotinst-java8", "webtasks-nodejs", "plugin" and "hello-world".
~/go/src/github.com/pei0804/serverless-sample/serverless master*
❯ sls create --template aws-go
Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.27.3
 -------'

Serverless: Successfully generated boilerplate for template: "aws-go"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name

~/go/src/github.com/pei0804/serverless-sample/serverless master*
❯ ls
Makefile       hello          serverless.yml world

では、何が書かれているか見てみましょう。

hello/main.go

よくあるhello worldサンプル

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
)

type Response struct {
    Message string `json:"message"`
}

func Handler() (Response, error) {
    return Response{
        Message: "Go Serverless v1.0! Your function executed successfully!",
    }, nil
}

func main() {
    lambda.Start(Handler)
}

world/main.go

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
)

type Response struct {
    Message string `json:"message"`
}

func Handler() (Response, error) {
    return Response{
        Message: "Okay so your other function also executed successfully!",
    }, nil
}

func main() {
    lambda.Start(Handler)
}

Makefile

デプロイ先でも動くようにというお気持ちの入ったbuildコマンドが書かれていました。

build:
  go get github.com/aws/aws-lambda-go/lambda
  env GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go
  env GOOS=linux go build -ldflags="-s -w" -o bin/world world/main.go

serverless.yml

slsに関する設定が色々書いてある。ひとまず、serviceの名前と、regionだけ変えておく。

# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: hello <-- ここ自分のつけたい名前を適当につける
# service: aws-go # NOTE: update this with your service name

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"

provider:
  name: aws
  runtime: go1.x

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1
  region: ap-northeast-1 <-- 東京リージョンにしておく

# you can add statements to the Lambda function's IAM Role here
#  iamRoleStatements:
#    - Effect: "Allow"
#      Action:
#        - "s3:ListBucket"
#      Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#    - Effect: "Allow"
#      Action:
#        - "s3:PutObject"
#      Resource:
#        Fn::Join:
#          - ""
#          - - "arn:aws:s3:::"
#            - "Ref" : "ServerlessDeploymentBucket"
#            - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

package:
 exclude:
   - ./**
 include:
   - ./bin/**

functions:
  hello:
    handler: bin/hello
  world:
    handler: bin/world

#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
# events:
#    events:
#      - http:
#          path: users/create
#          method: get
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
#      - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"

デプロイと実行

~/go/src/github.com/pei0804/serverless-sample/serverless master* 16s
❯ make build
go get github.com/aws/aws-lambda-go/lambda
env GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go
env GOOS=linux go build -ldflags="-s -w" -o bin/world world/main.go

~/go/src/github.com/pei0804/serverless-sample/serverless master*
❯ sls 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 (4.5 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
........................
Serverless: Stack update finished...
Service Information
service: hello
stage: dev
region: ap-northeast-1
stack: hello-dev
api keys:
  None
endpoints:
  None
functions:
  hello: hello-dev-hello
  world: hello-dev-world

~/go/src/github.com/pei0804/serverless-sample/serverless master*
❯ sls invoke -f hello
{
    "message": "Go Serverless v1.0! Your function executed successfully!"
}

~/go/src/github.com/pei0804/serverless-sample/serverless master*
❯ sls invoke -f world
{
    "message": "Okay so your other function also executed successfully!"
}

~/go/src/github.com/pei0804/serverless-sample/serverless master*
❯ sls logs -f hello
START RequestId: 7dbebc56-7995-11e8-be4f-4788beaf19d2 Version: $LATEST
END RequestId: 7dbebc56-7995-11e8-be4f-4788beaf19d2
REPORT RequestId: 7dbebc56-7995-11e8-be4f-4788beaf19d2  Duration: 0.75 ms   Billed Duration: 100 ms     Memory Size: 1024 MB    Max Memory Used: 27 MB

動いているっぽい。この時点で既に便利!!

f:id:tikasan0804:20180627081459p:plain マネジメントコンソールからも確認出来た。

stageとかprofileをcliからいじる

設定ファイルにハードコーディングせずに、stageとかprofile変えたいなーっていうアレ。やり方としては、sreverless.ymlのproviderのところに設定を加えれば良い感じになる。
内容としては、--profileに指定がなければdefaultを使う。--stageに指定がなければdevという感じ。

service: hello

provider:
  name: aws
  runtime: go1.x
  profile: ${opt:profile, self:custom.defaultProfile}
  stage: ${opt:stage, self:custom.defaultStage}
  region: ap-northeast-1
custom:
  defaultStage: dev
  defaultProfile: default

package:
 exclude:
   - ./**
 include:
   - ./bin/**

functions:
  hello:
    handler: bin/hello
  world:
    handler: bin/world

cliでオプションで渡す。

~/go/src/github.com/pei0804/serverless-sample/serverless master* 15s
❯ sls deploy --profile serverless --stage production
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 (4.5 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
........................
Serverless: Stack update finished...
Service Information
service: hello
stage: production
region: ap-northeast-1
stack: hello-production
api keys:
  None
endpoints:
  None
functions:
  hello: hello-production-hello
  world: hello-production-world

f:id:tikasan0804:20180627082914p:plain いい感じ!!

とりあえず、今回はここまで、次回はロジックとハンドラ分けたり、もうちょっと踏み込んだ内容やってみる。
前はapexを使ったりしていたけど、serverlessもかなり良さそう。

AWS Athena + Glueを使ったデータ分析(パーティション自動化)

ログは見れる状態にして価値が出る

f:id:tikasan0804:20180625084333p:plain

ログは雑に集めてるんだけど、見れる状態になっていないというのは、ストレージ料金を食ってるだけのゴミなので、破棄するか見れるようにするのですが、見れるようにするのは、そもそも結構大変だったりする。そこでFirehose + Athena + Glueを組み合わせて良い感じにしたいと思います。

データの保存形式をどうするか

色々ある。今回はJSONを使います。

Firehose ログをS3にまとめてPUTする

tikasan.hatenablog.com

Athena 集めた生ログに集計をかける

aws.amazon.com

AthenaはSQLでデータ集計を実行できるサーバーレスサービスです。使い方に関しては以下の公式ドキュメントを見ればわかります。
ざっくり説明すると、AthenaはCreate TableでS3のデータに対する参照を作成して、SQLクエリで集計が出来ます。なので、Drop TableやInsertクエリしたとしても、実データを操作することは出来ません。(READだけって割り切れるので安全といえば安全)
S3のデータをAmazon Athenaを使って分析する | Amazon Web Services ブログ

Glue テーブル自動作成

aws.amazon.com

AWS Glue は抽出、変換、ロード (ETL) を行う完全マネージド型のサービスで、お客様の分析用データの準備とロードを簡単にします。AWS マネジメントコンソールで数回クリックするだけで、ETL ジョブを作成および実行できます。AWS Glue では、AWS に保存されたデータを指定するだけで AWS Glue によるデータ検索が行われ、テーブル定義やスキーマなどの関連するメタデータAWS Glue データカタログに保存されます。カタログに保存されたデータは、すぐに検索、クエリ、ETL で使用できます。AWS Glue では、データ変換とデータのロードプロセスを実行するコードが生成されます。

そして、今回はこのAthenaのテーブルを作成するのが、Glueです。
Glueでよく出てくるETLというワードについて補足。これは、Extract Transform Loadのことです。一言でいうと、データ再集計という意味です。

Glueを使う理由

Athenaは検索をかける時に、パーティションを切ることで、スキャン範囲を狭めて、コストを軽減、検索完了時間を短縮が出来るのですが、このパーティションを切る作業が微妙に面倒。(パーティションの範囲が増えるごとに切らないといけない) そのあたりの面倒な作業をGlueは自動的に良い感じにしてくれる。

Glueクローラーの作成

f:id:tikasan0804:20180626201308p:plain

Add information about your crawler

Crawler name
クローラーの名前をつけます。今回は雑に「firehose」とします。

Add a data store

Choose a data store
JDBCかS3を選べます。今回は「S3」を選択します

Include path
データを保存しているS3の場所。今回でいうと、「s3://put-by-firehose/firehose」になる。

Choose an IAM role

Create an IAM roleを選択して、適当な名前を指定するといい感じに出来上がります。

内容は、AWSGlueServiceRoleという公式のロールと、以下のようなS3に対しての権限が付与されたロールが作成されました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::put-by-firehose/firehose/*"
            ]
        }
    ]
}

Frequency

Glueをどれくらいの頻度で動かすかを指定出来ます。今回は、テスト的に動かすだけなので、「Run on demand」にします。

Database

どういう名前でAthenaのdatabaseを作成するか、または既にあるdatabaseを使うかを選択出来ます。今回はdatabaseを作成したいので、Add databaseで「firehose」を作成して、それをそのまま指定しました。

Review

f:id:tikasan0804:20180625083153p:plain

実行

CrawlerをRunする

f:id:tikasan0804:20180625083325p:plain f:id:tikasan0804:20180625083345p:plain

Athenaで確認する

f:id:tikasan0804:20180626075639p:plain

f:id:tikasan0804:20180626075751p:plain

テーブルは確かにいい感じになってるけど。。。。名前がああああ。 この名前をいい感じに出来る方法知ってる人居たら教えてください。
現状、僕が知っている方法だと、定期的にパーティションを切るLambdaを実行するという方法しか思いつかない。(それGlue使わなくていいやんという話になる)

Amazon Kinesis Data Firehose 使い方

f:id:tikasan0804:20180624115811p:plain

Amazon Kinesis Data Firehose(長いのでFirehoseとします)

aws.amazon.com

どういうものかというのは、公式の素晴らしい説明文で大体わかります。

ストリーミングデータをデータストアや分析ツールに確実にロードする最も簡単な方法です。ストリーミングデータをキャプチャして変換し、Amazon S3Amazon Redshift、Amazon Elasticsearch Service、Splunk にロードして、現在お使いの既存のビジネスインテリジェンスツールやダッシュボードで、ほぼリアルタイムに分析することができます。

使ったユースケース

弊社のプロダクト(広告配信システム系)でまあまあなリクエストを受けているLambdaのログを見れる状態にしたいという場面で使いました。

Lambdaが各々でS3にログをPUTするぜみたいなことをしていた時に、ファイル数多すぎて、BQに入れれないし、Athenaで見るにしてもログ多すぎて検索終わるのを待つと、日が暮れる(正確にはエラーになった)となっていました。
そこに、Kinesis Firehose使って複数のLambdaが吐くログを固めてS3にPUTすることで、少し待てばAthenaでログを見れるようになりました。(ファイル数が減ってファイルIOが減ったから)

また、S3にPUTする時に、時間ごとにフォルダを自動で作ってくれるので、パーティションを切りたい時も同じルールを適用出来るのはいいなーと思いました。

Stream作成

Step 1: Name and source

Delivery stream name
ストリーム名。今回の設定値は「firehose」

Source

  • Direct PUT or other sources
    レコードを配信ストリームに直接送信する場合、またはAWS IoT、CloudWatchログ、CloudWatchイベントからレコードを送信する場合は、このオプションを選択します。
  • Kinesis stream
    Kinesis streamの流し先として、Firehoseを選べます。今回のユースケースでは使いませんでしたが、これも便利そう。

今回の設定値は、Lambdaから直接流す形なので、「Direct PUT or other sources」。

Step 2: Process records

Transform source records with AWS Lambda
Lambda 関数を呼び出して、受信した送信元データを変換してから送信先に配信できます。Kinesis Data Firehose のデータ変換は、配信ストリームの作成時に有効にすることができます。 とあるように、流れてくるデータを何か手を加えたい時は、これを使うと良さそう。 Amazon Kinesis Data Firehose のデータ変換 - Amazon Kinesis Firehose

今回の設定値は、「Disabled」。

Convert record format
データのフォーマットをJSONから何かへみたいな感じの変換が出来る。

以下の2つが用意されてる。また、AWS Glueとも連携が出来るようです。(別記事で調査する予定) - Apache Parquet - Apache ORC

今回の設定値は、「Disabled」。

Step 3: Choose destination

Select destination
保存先を設定する。保存先として以下の4つがある。

今回の設定値は、「Amazon S3」。

S3 destination

  • S3 bucket
    保存先のS3を選ぶ。
  • Prefix
    FirehoseがPUTする時のフォルダ名と認識すればおk。

今回は、Create newして、「put-by-firehose」というバケットを作成。また、Prefixは「firehose/」とした。こうすることで、put-by-firehose/firehose/firehoseがよしなにという形にしてくれる。

Step 4: Configure settings

S3 buffer conditions
受信レコードをS3バケットに転送する前にバッファリングしてくれる。その時の設定をする。条件のいずれかが満たされると、PUTされる。 - Buffer size
1-128 MBの間でバッファするサイズを決めれる
- Buffer interval
60-900秒でPUTする間隔を設定出来る

今回は、どちらも最小値の「1MB」と「60秒」とする。もし、リアルタイム制が要求されない場合は、もっと高い数値を設定することになる。

S3 compression and encryption

  • S3 compression
    どういう圧縮形式でS3にPUTするかというもの。
  • S3 encryption S3上のデータを暗号化するか

今回は、「GZIP」で圧縮し、暗号化については「Disabled」にした。

Error logging
エラーをCloudWatchログを出力する。

今回は使わないので、「Disabled」とした。

IAM role

Create newして、いい感じにしてくれます。デフォで作成される権限は以下のような感じ。まあ、公式によるものだから、オーバーな権限はないし最低限になっている。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "glue:GetTableVersions"
      ],
      "Resource": "*"
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "s3:AbortMultipartUpload",
        "s3:GetBucketLocation",
        "s3:GetObject",
        "s3:ListBucket",
        "s3:ListBucketMultipartUploads",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::put-by-firehose",
        "arn:aws:s3:::put-by-firehose/*",
        "arn:aws:s3:::%FIREHOSE_BUCKET_NAME%",
        "arn:aws:s3:::%FIREHOSE_BUCKET_NAME%/*"
      ]
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "lambda:InvokeFunction",
        "lambda:GetFunctionConfiguration"
      ],
      "Resource": "arn:aws:lambda:ap-northeast-1:851669633371:function:%FIREHOSE_DEFAULT_FUNCTION%:%FIREHOSE_DEFAULT_VERSION%"
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:ap-northeast-1:hoge:log-group:/aws/kinesisfirehose/:log-stream:*"
      ]
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": [
        "kinesis:DescribeStream",
        "kinesis:GetShardIterator",
        "kinesis:GetRecords"
      ],
      "Resource": "arn:aws:kinesis:ap-northeast-1:hoge:stream/%FIREHOSE_STREAM_NAME%"
    },
    {
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt"
      ],
      "Resource": [
        "arn:aws:kms:region:accountid:key/%SSE_KEY_ARN%"
      ],
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "kinesis.%REGION_NAME%.amazonaws.com"
        },
        "StringLike": {
          "kms:EncryptionContext:aws:kinesis:arn": "arn:aws:kinesis:%REGION_NAME%:hoge:stream/%FIREHOSE_STREAM_NAME%"
        }
      }
    }
  ]
}

Step 5: Review

f:id:tikasan0804:20180624141537p:plain

StatusがActiveなったら使える状態になります。
f:id:tikasan0804:20180624141928p:plain

Lambda -> Firehose

ロール作成

今回は、LambdaからFirehoseへPUTしたいので、AWSLambdaMicroserviceExecutionRoleAWSLambdaBasicExecutionRoleのロール以外に、インラインで以下のような雑なロールを付加しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "firehose:PutRecord",
                "firehose:PutRecordBatch"
            ],
            "Resource": "arn:aws:firehose:*:*:deliverystream/*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "firehose:DescribeDeliveryStream",
                "firehose:ListDeliveryStreams"
            ],
            "Resource": "*"
        }
    ]
}

関数作成

import boto3


def lambda_handler(event, context):
    firehose = boto3.client('firehose')
    firehose.put_record(
        DeliveryStreamName='firehose',
        Record={
            'Data': b'{"user_id": "a", "event": "AAA"}\n'
        }
    )
    firehose.put_record(
        DeliveryStreamName='firehose',
        Record={
            'Data': b'{"user_id": "b", "event": "BBB"}\n'
        }
    )
    firehose.put_record(
        DeliveryStreamName='firehose',
        Record={
            'Data': b'{"user_id": "c", "event": "CCC"}\n'
        }
    )

今回はLambdaから叩いていますが、もちろんローカルからの実行でも動きます。

PUTしてみる

Lambdaを実行すると、以下のようにサクッと出来上がりました!便利! PUTするだけで、ある程度固めてくれるのは相当ありがたい。

put-by-firehose/firehose/2018/06/24/05/firehose-1-2018-06-24-05-47-40-34054ccb-1fa3-47d6-b05f-749b51d032a5.gz

f:id:tikasan0804:20180624145709p:plain

集めたデータどうする

記事の冒頭でも書きましたが、Athenaでいい感じに見れようにしたりも出来るんですが、体力が尽きたので、次の記事で紹介することにします。

Packerで作成されたSnapshotを消す

便利なんだけど放置しがちなAMIたちを消す

f:id:tikasan0804:20180621205204p:plain

Packerはイメージの管理に非常に便利です。弊社でも、AWSのAMIの作成時に使っています。

tikasan.hatenablog.com

ただ、AMIは放置していると無限に作成されてしまうので、いくら安いストレージ料金であっても、結構溜め込んでいるとまあまあな値段になります。そこで、いい感じに消す作業をしたので、その時の作業内容をまとめてみました。
※AMIを消す処理は、正確には登録解除ですが、ここでは面倒なので削除というワードを使っています。

この作業をする上での前提条件

  • インスタンスに紐付いていないAMIは消してよい
  • AMIに紐付いていないスナップショットでも必要なものはある
  • 消すとしてもAMIは保険として、一週間は残したい
  • 今後もPackerでAMIを作成する
  • トライアンドエラーでAMIを何度か作り直すことがある
  • AutoScallingの起動設定に使うAMIはPackerで作っている
  • AutoScallingの起動設定に使っているAMIは消してはいけない
  • スナップショットはAMIに紐付いていると消せない(これはスナップショットの仕様)

他にも細々としたやつはありますが、ざっくり書くと上のような感じ。

Packerの機能でどうにか出来ない?

出来ればPackerで備わっている機能でどうにかしたいよねー。わかるー。ってことで探しました。それっぽいコマンドは見つかりました。

Amazon EBS - Builders - Packer by HashiCorp

  • force_deregister (boolean) - Force Packer to first deregister an existing AMI if one with the same name already exists. Default false.
  • force_delete_snapshot (boolean) - Force Packer to delete snapshots associated with AMIs, which have been deregistered by force_deregister. Default false.

google翻訳にかけると

  • force_deregister(boolean) - 同じ名前の既存のAMIが既に存在する場合、Packerが既存のAMIを最初に登録解除するように強制します。 デフォルトはfalseです。
  • force_delete_snapshot(boolean) - Force_deregisterによって登録解除されたAMIに関連付けられたスナップショットを削除するようにPackerに指示します。 デフォルトはfalseです。

一瞬使えそうと思ったのですが、AMIを新規に作成した時に、必ず正しいとも限らないので、せめて何世代か前のAMIは残しておきたいとか、あとはAutoScallingの起動設定に使っているAMIが消されると色々面倒。そこを自動で頑張るとかも考えたけどコスト高いのでやめ。

Packerのプラグイン

github.com

もしかしたら、Packerプラグインで何か良いのあるか!?って考えていたらありました。最高。このプラグインは、AMIを作成した際に、何世代か前のAMIを自動で削除してくれるというもの。良さそうだ!ってなりましたが・・・。 機能そのものは問題なく動き、素晴らしいものでした。しかしながら、トライアンドエラーとか繰り返している内に、意図せず消えてほしくない世代のものが消えてしまうかもしれないという点がああああというのと、容量食っているスナップショットをどうにかしたい問題もあり、使うのをやめました。

シェルスクリプトでどうにかするか

結局ここに落ち着いてぐぬぬというお気持ちなんですが、Jenkinsのジョブで定期実行するシェルスクリプトを作成しました。(結局それかいってやつ)

ここ、一旦、前提条件をどうやってクリアするかを考えた内容を雑に書く。

インスタンスに紐付いていないAMIを消す

消そうとしているAMI IDが動いているインスタンスに紐付いていないかをチェックすれば良い。

$ aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" "Name=image-id,Values=hoge"

AutoScallingに使っているAMIは消してはいけない

これは、LaunchConfigurationに紐付いているAMI IDを見れば言いわけですが、弊社では使っているAutoScallingは必ず1つ以上のインスタンスが動いていたので、つまり、上のインスタンスに紐付いているかチェックでカバーが出来る。

スナップショットはAMIに紐付いていると消せない

これは、弊社の前提条件ではなく、スナップショットの前提としてあるので、AMIを先に掃除しないと、スナップショットのお掃除は出来ない。

消すとしてもAMIは保険として、一週間は残したい

これ余裕じゃーんと思ったのですが、describe-images には日付フィルタがなかったので、jqコマンドでゴリゴリ頑張ることになった。

delete_date_range=2016-06-12
aws ec2 describe-images --owner self | \
  jq -r ".Images[] |  select(.CreationDate < \"$delete_date_range\")" | \
  jq -r ".ImageId" | tee ami-id.txt

AMIに紐付いていないスナップショットでも必要なものはある

これが地味に面倒な前提条件としてあった。解決策は実に雑ではあるけど、Packerが作成したスナップショットには何も設定していないと、説明の部分に Created by CreateImage(hogehoge) のような内容があり、これが書いてある=Packerが作ったものと判定することにした。
逆に言うと、これから導入を考えている人は、Packerが作ったものと断定出来るTagとかを付与するのが良さそう。

$ aws ec2 describe-snapshots  --owner self --filter "Name=description,Values=Created by CreateImage(*"
  • AutoScallingの起動設定に使うAMIはPackerで作っている
  • 今後もPackerでAMIを作成する

上記は、ここまでの前提条件クリアすればあまり気にしなくて良い事案。

出来たシェルスクリプト

日本語を豊富に使っているけど、まあ良いではないか。tee の部分とかはどっちでも良かったけど、一応出した感じになっている。 以下はジェンキンスのジョブで定期実行しています。また、スクリプトは同一ジョブ内でstepを分けたような感じで管理しています。 パラメーターを色々切り替えれたりとか考えましたが、あまり頑張らない感じで完成とした。

#!/bin/sh
# 動いているインスタンスに紐付きがないかつ、作成から1週間以上のAMIを削除する
delete_day_limit=7
delete_date_range=`date -d "$delete_day_limit day ago" '+%Y-%m-%d'`

echo "AMI ID取得"
aws ec2 describe-images --owner self | \
  jq -r ".Images[] |  select(.CreationDate < \"$delete_date_range\")" | \
  jq -r ".ImageId" | tee ami-id.txt

for deregister_ami_id in `cat ami-id.txt`; do
  if [ `aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" "Name=image-id,Values=$deregister_ami_id" --output=text | wc -l` -eq 0 ]; then
    aws ec2 deregister-image --image-id $deregister_ami_id
    echo "AMI ID = $deregister_ami_id は登録解除しました 稼働しているインスタンスに紐付きがないため"
  else
    echo "AMI ID = $deregister_ami_id はスキップしました 稼働しているインスタンスと紐付きがあったため"
  fi
done
#!/bin/sh
# Packerが作成したAMIとの紐付きがないスナップショットを削除
echo "\nPackerが作成したSnapshot ID取得"
aws ec2 describe-snapshots  --owner self --filter "Name=description,Values=Created by CreateImage(*" | \
  jq -r '.Snapshots[].SnapshotId' | \
  tee snapshot_id.txt

for delete_snapshot_id in `cat snapshot_id.txt`;do
  if [ `aws ec2 describe-images --filter "Name=block-device-mapping.snapshot-id,Values=$delete_snapshot_id" --output=text | wc -l` -eq 0 ]; then
    aws ec2 delete-snapshot --snapshot-id $delete_snapshot_id
    echo "Snapshot ID = $delete_snapshot_id を削除しました AMIとの紐付きがないため"
  else
    echo "Snapshot ID = $delete_snapshot_id をスキップしました AMIとの紐付きがあるため"
  fi
done

結果

キレイさっぱり消えて、そこそこの月々の費用を削減出来た。
消す作業大変だけど、定期的にやっていこう。

ていうか、jq コマンドすごすぎじゃない?

PackerでAWS AMIを自動で作成する

AWS AMIの管理は面倒

AWSでEC2(IaaS)使ったアプリケーションを構築する時に、AMIの管理が地味に面倒です。例えば、AWS側から提供されているAMIをそのまま使うと、OSがデフォルトに近い状態なので、アプリケーションの実行に必要なものを準備する必要があります。

そこで、実行するために必要な準備を完了させているAMIを作成して、それを使ってインスタンスを作成して、アプリケーションコードを反映するだけで、すぐに使える状態にするということをします。

ただ、これをするのが地味に面倒です。やり方とかは以下の記事に詳しくあります。 tikasan.hatenablog.com

ざっくりいうと、出来上がってるインスタンスからスナップショットを作成して、スナップショットからAMIを作成して、AMIを使ってインスタンスを作成するという手順を踏む必要があります。

これらを自動でやってくれるのが、Packerです。

Packerとは

f:id:tikasan0804:20180621205204p:plain

AWSマネジメントコンソールを使わず、AMIの作成の手順を単純化してくれます。また、プロビジョニング機能があったり、並列実行が出来たりするそうです。

アーティファクト

Packerは最終生成物として、アーティファクトを作成します。アーティファクトとは、Packer独自の抽象化されたマシンイメージの概念のことです。
AWSでいうところのAMIやAMI IDや管理情報。 アーティファクトの作成には、以下の機能を使って作成することができます。(番号は実行順序です)

  1. ビルダー(builder)
    マシンイメージの生成。JSONでテンプレートを定義し、自動で良い感じにしてくれる
  2. プロビジョナー(provisioner)
    マシンイメージ内のミドルウェアやアプリケーションのインストールや設定などを行う。ここではシェルスクリプトはもちろんChefなどの構成管理ツールを実行することもできます
  3. ポスト・プロセッサー(post-processor
    ここでは、最終的な生成物をどうするかということをやる(圧縮とかアップロードとか)

実際にやってみる

インストールはここ見ればわかる Install Packer - Getting Started - Packer by HashiCorp

$ touch hello_ami.json
{
  "variables": {
    "aws_access_key": "hoge",
    "aws_secret_key": "fuga"
  },
  "builders": [
    {
      "type": "amazon-ebs",
      "access_key": "{{user `aws_access_key`}}",
      "secret_key": "{{user `aws_secret_key`}}",
      "region": "ap-northeast-1",
      "source_ami": "ami-06128816a1c781a57",
      "instance_type": "t2.micro",
      "ssh_username": "root",
      "ami_name": "hello_ami {{timestamp}}"
    }
  ]
}
❯ packer build hello_ami.json
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: hello_ami 1529395142
    amazon-ebs: Found Image ID: ami-06128816a1c781a57
==> amazon-ebs: Creating temporary keypair: packer_5b28b7c6-9716-5682-837a-45051c4baae6
==> amazon-ebs: Creating temporary security group for this instance: packer_5b28b7c7-913b-2b6d-fa7d-f2dcf8a51770
==> amazon-ebs: Authorizing access to port 22 from 0.0.0.0/0 in the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: "Name": "Packer Builder"
    amazon-ebs: Instance ID: i-0284843a27b02a39c
==> amazon-ebs: Waiting for instance (i-0284843a27b02a39c) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance, attempt 1
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: hello_ami 1529395142
    amazon-ebs: AMI: ami-02749c4f55b57389c
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
ap-northeast-1: ami-02749c4f55b57389c

f:id:tikasan0804:20180619171155p:plain AMIが作成できました!便利すぎる!w

f:id:tikasan0804:20180619171442p:plain 作成するために使ったEC2インスタンスは自動的に削除までされています。

www.packer.io とりあえず、雑に作成しましたが、Builderに必要な権限やクレデンシャルの設定方法は公式見れば分かります。

プロビジョニングは以下のような感じで軽く書ける。

{
  "variables": {
    "aws_access_key": "hoge",
    "aws_secret_key": "fuga"
  },
  "builders": [
    {
      "type": "amazon-ebs",
      "access_key": "{{user `aws_access_key`}}",
      "secret_key": "{{user `aws_secret_key`}}",
      "region": "ap-northeast-1",
      "source_ami": "ami-06128816a1c781a57",
      "instance_type": "t2.micro",
      "ssh_username": "root",
      "ami_name": "hello_ami {{timestamp}}"
    }
  ],
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "sleep 30",
        "sudo apt-get update",
        "sudo apt-get install -y redis-server"
      ]
    }
  ]
}

sleep30を最初に書く理由は、ちょっと準備出来るまで余裕ほしいという文脈らしい。なるほど。 www.packer.io

AMI作成自動化最高。

毎日目標を立てないから、頑張れる

毎日目標立てると未達になった時だるい

社会人生活を始めると、結果を出さなきゃーとか、自分で立てた目標達成しなきゃーとか色々あると思います。例えば自分は以下のような日々の目標があります。

  • 早寝早起き(22:00 ~ 06:30)
  • 朝勉強(1~2時間くらい)
  • 定時退社

加えて会社での仕事の目標とかもあったり、社会人って大変!(楽しいですけどね)
これらの目標を真面目に毎日達成しようと思うと、以下のようなことがよく起きる。

早寝早起き目標

  1. 早寝早起きするぞおおお
  2. 頑張ろうとした初日に、飲み会の予定入った
  3. 帰宅が遅くなる
  4. 早寝早起き出来なくて萎える

朝勉強

  1. 朝勉強するぞおおおお
  2. 前日のボルダリングでしんどい
  3. 朝起きれないしんどい
  4. 朝勉強出来なくて萎える

こういうことがあったりしたので、毎日目標を持つことをやめました。

一週間くらいの目標にする

毎日目標を立てない代わりに、一週間スパンで目標を立てることで、さっき書いたような、イレギュラーによる目標失敗パターンでいちいち萎えないようにした。どういうことかというと、週の5日間の中で、3日以上は目標達成出来たらいいや、くらいにしておくという単純な話。こうすることで、目標を達成しやすくした。すると、萎えた気持ちが発生しにくくなり、結果的に立てた目標が継続達成出来るようになった。しかも、飲み会やボルダリングを盛大に楽しめるようになり、変なストレスを感じなくなった。

頑張り過ぎようとすると、折れちゃうからバランス大事。習慣になるまでは、ゆるく頑張る方が良さそう。

AWS EC2バックアップ

EC2でよくやるアレ

検証したい時とかによくやるので、自分のためにメモがてらにまとめた。

SnapshotからAMI

f:id:tikasan0804:20180611205512p:plain バックアップしたいインスタンスIDをメモしておく

f:id:tikasan0804:20180611205515p:plain 該当のEBSを見つけて

f:id:tikasan0804:20180611205518p:plain スナップショットを取る

f:id:tikasan0804:20180611205525p:plain 適当な名前をつける(日付とか用途を入れるとgood)

f:id:tikasan0804:20180611210227p:plain 完了画面で出てくるスナップショットのリンクを押すと作ったやつが出てくる

f:id:tikasan0804:20180611205532p:plain スナップショットからAMIを焼く

f:id:tikasan0804:20180611205538p:plain 適宜設定する
Linux AMI 仮想化タイプ - Amazon Elastic Compute Cloud
仮想化タイプについては、HVM(ハードウェアアシストの仮想化)とPV(準仮想化)があるっぽいですが、HVM方式のがパフォーマンス良くて、利用料金が安いみたいです。
ちなみに、EBSのボリューム内容が違ったりするので、PVからHVMに切り替えるとかが出来ないみたいなので、特別PV使いたい理由が無ければHVMが無難っぽい。

f:id:tikasan0804:20180611205544p:plain 作成したAMIからEC2インスタンス作成

EC2から直接AMI

f:id:tikasan0804:20180611205546p:plain インスタンス選んでイメージ作成で後は適宜設定する

スナップショットからAMI焼くべき?

特別理由が無ければ、スナップショットからAMIを焼く方がいいっぽい。

何故か?という理由とかは上のリンクで大体説明してくれている。この記事では、スナップショットとAMIの違いについて簡単におさらいしてみる。

  • スナップショット:EBSのある瞬間の状態のコピー
  • AMI:EC2インスタンスを作る時に使うディスクイメージ

EC2インスタンスはディスクボリュームとして、「EBS (Elastic Block Store)」というストレージ・デバイスを使っている。このEBSにスナップショットという、ある瞬間の状態を保存する機能があります。

スナップショットは非常に便利で、2回目以降に作成するスナップショットは増分だけを保存してくれます。よって、複数のスナップショットを作成したとしても増分の保存だけで済みます。ちなみに、このスナップショットは、AWSのS3に保存されているので、安心感はある。(イレブン・ナインの堅牢性)

AMIはスナップショットのような増分バックアップとかは出来ません。まるごとイメージにして焼くので、容量的にあまりおいしくないです。ただ、上に貼ったリンクのように、特定の条件下では威力を発揮するようです。

注意点

  • スナップショットは、EC2インスタンスが稼働中でも作成出来ますが、書き込み途中でスナップショットが取られる可能性がある。(稼働中の場合は、整合性は保証されない)
  • リストア時のセキュリティグループは、デフォルトになるので、適宜設定する必要がある(インスタンス作成時に設定出来る)