ぺい

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

シェル、シェルスクリプトの使い方まとめ

f:id:tikasan0804:20180710081015p:plain

自分向けメモ。たまに更新していく。

Shellとは

ユーザーが入力したコマンドを解釈し、該当コマンドを実行する。シェルにはモードが2つある。

上記がどういったものかは割愛する。

単純コマンド

$ echo HelloWorld
HelloWorld

コマンド名と任意の引数で構成されるもの。

リダイレクト

上記をリダイレクトさせることが出来るのが、リダイレクトです。(良い言い方がわからない)

標準入力にファイルを入力する例

$ cat stdin
HelloWorld

$ < stdin
HelloWorld

標準出力をファイルに書き込む例

$ cat stdin
HelloWorld
$ cat stdin > out

$ cat out
HelloWorld

標準出力をファイルに追記する例

$ cat stdin
HelloWorld
$ cat stdin >> out2
$ cat stdin >> out2
$ cat out2
HelloWorld
HelloWorld

標準出力・標準エラーを別ファイルに書き込む

ファイル記述子

div:
        echo 1
        2
$ make div 1> log_1 2> log_2
$ cat log_1
echo 1
1
2
$ cat log_2
make: 2: No such file or directory
make: *** [div] Error 1

標準出力・エラーまとめてファイルに書き込む例

err:
        echo HelloWorld
        a
$ make err > log 2>&1
$ cat log
echo HelloWorld
HelloWorld
a
make: a: No such file or directory
make: *** [err] Error 1

パイプ

構文の組み合わせ

treeの出力をlessに渡す

$ tree / | less

標準エラー出力もパイプに渡す

hoge 2>&1 | less

環境変数の一時変更

LANG=C man ls

&&(AND) ||(OR)

パイプライン1 && パイプライン2はパイプライン1の終了ステータスが成功(0)なら、パイプライン2が実行される。
パイプライン1 || パイプライン2はパイプライン1か2のどちらかが成功(0)すればおk.

err:
        echo HelloWorld
        a
$ make err && echo OK
echo HelloWorld
HelloWorld
a
make: a: No such file or directory
make: *** [err] Error 1

$ make err || echo OK
echo HelloWorld
HelloWorld
a
make: a: No such file or directory
make: *** [err] Error 1
OK

シェルスクリプト

行頭便利フォーマット

#!/usr/bin/env bash

# Fail on unset variables and command errors
set -ue -o pipefail

# Prevent commands misbehaving due to locale differences
export LC_ALL=C
#!/bin/sh -e
# オプションつきシェルスクリプト

行頭の意味

シェルスクリプトの行頭に記述する #! で始まる行shebangという

bash絶対パス指定

#!/bin/bash

bashをenvを使って指定

#!/usr/bin/env bash

基本構文

if

引数によっての分岐

#!/bin/bash

if [ $#  -lt 2 ];then # 引数が2個未満
  echo "Usage: $0 file1 file2" 1>&2 # 標準エラー出力に出力
  exit 1
else
  echo "eeyan"
fi
$ sh if.sh
Usage: if.sh file1 file2

$ sh if.sh ls file1
eeyan

case

unameを使ったOS判定の例

$ uname -sr
Darwin 15.6.0
#!/bin/bash

case `uname -sr` in
  Linux*)
    ls -l --full-time "$@";;
  FreeBSD* | NetBSD* | OpenBSD* | Darwin*) # Darwinから始まる任意の文字列
    ls -lT "$@";; # ここが実行される
  SunOS' '5.*)
    ls -E "$@";;
  *) # 何もひっとしない場合
    echo unknown OS 1&>2;;
esac
$ sh case.sh
total 88
-rw-r--r--  1 jumpei  staff   13  7  5 08:23:24 2018 2
-rw-r--r--  1 jumpei  staff  202  7  5 08:24:23 2018 case.sh
-rw-r--r--  1 jumpei  staff  158  7  5 08:18:11 2018 if.sh
-rw-r--r--  1 jumpei  staff   48  7  4 08:44:16 2018 sample.sh
-rw-r--r--  1 jumpei  staff   56  7  4 08:44:35 2018 sample2.sh
-rw-r--r--  1 jumpei  staff   87  7  4 08:52:39 2018 sample3.sh
-rw-r--r--  1 jumpei  staff   47  7  4 20:14:09 2018 sample4.sh
-rw-r--r--  1 jumpei  staff   55  7  4 20:13:57 2018 sample5.sh
-rw-r--r--  1 jumpei  staff  198  7  5 08:03:53 2018 sample6.sh
-rw-r--r--  1 jumpei  staff   69  7  5 07:52:58 2018 sample7.sh
-rw-r--r--  1 jumpei  staff   81  7  5 07:55:30 2018 sample8.sh

for

* はカレントディレクトリのファイル名

#!/bin/bash

for file in *; do
  echo $file
done
$ sh for.sh
LICENSE
README.md
for.sh
for_arg.sh
for_bash.sh
for_jot.sh
for_seq.sh
sample
sh

引数 $@ を使ったループ

#!/bin/bash

for arg in "$@";do
  echo $arg
done

for arg;do # 上と同じ意味
  echo $arg
done
$ sh for_arg.sh a b c
a
b
c
a
b
c

bashzshのプレース展開を使ったループ

#!/bin/bash

for i in {1..10};do
  echo $i
done
$ sh for_bash.sh
1
2
3
4
5
6
7
8
9
10

jotを使ったループ

#!/bin/bash

for i in `jot 10`;do
  echo $i
done
$ sh for_jot.sh
1
2
3
4
5
6
7
8
9
10
for i in `seq 1 10`;do
  echo $i
done
1
2
3
4
5
6
7
8
9
10

サブシェル

リストを()で囲むとサブシェルになり、サブシェルはもとのシェルとは別扱いで実行されます。サブシェルの中での変数の変更、umask値を変えても、サブシェルから出ると戻ります。この挙動を利用して、何らか変更をさせて終わったら、元の状態で処理もしたいなどのシチュエーションでは便利です。シェルスクリプトをリダイレクト、パイプでつないでも同じことはできます。

シェル変数 IFS: に変更して、set コマンドを実行すると : が削除され、 PATH の中身が $1 $2 $3 のように位置パラメータに設定されます。これらの変更はサブシェルを抜けると解除されます。

#!/bin/sh

echo "IFS=$IFS"

(
  IFS=:
  echo "IFS=$IFS"
  set $PATH
  echo $3
)

echo "IFS=$IFS"
$ sh sub_shell.sh
IFS=

IFS=:
/Users/jumpei/.anyenv/envs/rbenv/shims
IFS=

グループコマンド

リストを{}でかこむとグループコマンドと呼ばれる複合コマンドになります。リダイレクトしたり、パイプに接続したり、次のシェル関数の本体として利用できる。グループコマンドはサブシェルと違って、終わった後も影響があります。

コマンドの結果をまとめてlogfileにリダイレクトしている例。

#!/bin/sh

{
  hostname
  date
  who
} > logfile
$ sh group.sh
$ cat logfile
jumpei-no-MacBook-Pro-3.local
2018年 7月 6日 金曜日 08時24分53秒 JST
jumpei   console  Jul  4 07:49
jumpei   ttys001  Jul  4 07:49

シェル関数

シェル関数内の変数は引数渡しのため、位置パラメータ以外はグローバル変数になりますそのため、関数内はローカルにしたい場合は、{} ではなく ()を使って、サブシェルにすれば可能です

#!/bin/bash

greet()
{
  echo "Hello"
}

greet

greet2()
{
  echo $1 "Hello"
}

greet2 A
$ sh func.sh
Hello
A Hello

エラー系TIPS

変数の設定漏れ防止

set -u
変数を宣言していないものを使った時に終了ステータスを失敗(0以外)にしてくれる。

#!/bin/bash

VAL=foo
echo $VAL_TYPO
echo FINISH
$ sh sample.sh; echo $?

FINISH
0

$ cat sample2.sh
#!/bin/bash

set -u

VAL=foo
echo $VAL_TYPO
echo FINISH

$ sh sample2.sh; echo $?
sample2.sh: line 6: VAL_TYPO: unbound variable
1

エラーとしたくないケースを回避

途中まで処理して落としたい。デフォルト値が使える(${parameter:-word})

#!/bin/bash

set -u
if [ -z "${1:-}" ]; then
    echo "HOW TO hoge" >&2
    exit 2
fi
$ sh sample3.sh
HOW TO hoge

エラーになったら中断

set -e

何もつけないとエラーがあっても、最後まで実行される。しかも、ステータスコードも成功になる。

#!/bin/bash

FOO=$(ls --l)
echo $FOO
echo "OK"
$ sh sample4.sh; echo $?
ls: illegal option -- -
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]

OK
0

エラー時に途中で終了し、ステータスコードも失敗が帰ってくるようになる

#!/bin/bash

set -e

FOO=$(ls --l)
echo $FOO
echo "OK"
$ sh sample5.sh; echo $?
ls: illegal option -- -
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
1

エラーを無視したいケース
grepで検索ヒットしない時にエラーステータスを返すので、それをいい感じにしたい。
以下のような感じにするか、コマンドでエラーとしないオプションとかを使うか
またはリストを使ったエラー無視などのパターンがあります。

#!/bin/bash

set -e

if find ./ | grep sample888.sh >/dev/null; then
  echo "Existing sample.sh"
else
  echo "Nothing sample.sh"
fi

rm a || true
rm a || : # trueの代わりに出来る

echo "OK"
$ sh sample6.sh; echo $?
Nothing sample.sh
rm: a: No such file or directory
rm: a: No such file or directory
OK
0

パイプライン内のエラーで中断する

set -eはパイプラインの一番右のコマンドのエラーは正しくエラーとしてくれるが、途中のコマンドのエラーは無視されます。

$ cat sample7.sh
#!/bin/bash

set -e

FOO=$(ls - l "$0" | wc -l )
echo $FOO
echo "OK"
$ sh sample7.sh; echo $?
ls: -: No such file or directory
ls: l: No such file or directory
1
OK
0

set -e -o pipefailをつけるとパイプライン中のエラーを検知してくれる。

#!/bin/bash

set -e -o pipefail

FOO=$(ls - l "$0" | wc -l )
echo $FOO
echo "OK"
$ sh sample8.sh; echo $?
ls: -: No such file or directory
ls: l: No such file or directory
1

エラーは標準エラー出力に出力しよう

#!/bin/bash

echo 'Expect stdout'
echo 'Expect stderr' 1>&2
$ sh ./output.sh >/dev/null
Expect stderr

$ sh ./output.sh 2>/dev/null
Expect stdout

bashに依存しているコマンドは#!/bin/bashと書かない

ubuntu上では以下のスクリプトは動きません。bashに依存しているなら、/bin/bashとしよう。

#!/bin/sh
list=( ($ls) )
echo $list

環境変数によって動かないを避ける

export LC_ALL=C

参考文献

AWS Serverless Application Model(SAM)を使ったサーバーレスアプリ構築

AWS Serverless Application Model(SAM)とは

f:id:tikasan0804:20180703082833p:plain

github.com

AWS Serverless Application Model (以降AWS SAMとする) は、AWS公式のサーバーレスアプリケーションを構築するためのフレームワークです。

以前の記事のServerlessでAWS Lambdaのデプロイをいい感じにする - ぺいで、Serverlessというつーるを紹介したのですが、SAMは、公式サポートなので、AWS以外のサーバーレス使う予定ない人は、基本的にSAMを使う方向で良さそうな気がします。

SAMとServerless Frameworkの違い

今回使ってみて、感じた違いについてまとめました。

Serverless Frameworkは、全部乗せの万能ツールという感じ。AWSに限らず様々なクラウドで統一の設定が使える。また、デプロイまでが本当サクッと出来る。SAMを使って思ったことですが、裏で暗黙的色々いい感じにやってるんだなと思った。
あと、コミュニティが盛んなこともあり、プラグインとかも色々出ているので、

SAMはAWSに特化していることもあり、AWSしか使わないなら、SAM使っとけばいいやんと使ってみて思った。

SAM使ってみる

どんなもんかをひとまず動かして体感してみる。インストールはpipで出来ます。

❯ python -V
Python 3.6.3
❯ pip install aws-sam-cli
...
❯ sam --version
SAM CLI, version 0.4.0

ちなみに、sam init を実行すると見本となるテンプレートが作られる。

SAM CLI のインストール - AWS Lambda

設定内容を追いかけてみる

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

上記の定義は、定型的に入れろとのことをそのまま鵜呑みにすると、バージョン指定という認識で良さそう。

Globals:
  Function:
    Timeout: 5

関数全体に対する設定。今回はタイムアウトまで5秒となっている。

Resources:
  HelloWorldFunction: # 関数名
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello-world/
      Handler: hello-world # Handler名 今回の場合、go build後のファイル名
      Runtime: go1.x
      Tracing: Active # X-Rayによるトラッキングの有効化
      Events: # どういうイベント(トリガー)を設定するか
        CatchAll:
          Type: Api
          Properties:
            Path: /hello
            Method: GET
      Environment: # 環境変数
        Variables:
          PARAM1: VALUE

関数の定義が書かれている。

Outputs:
  HelloWorldAPI:
    Description: "API Gateway endpoint URL for Prod environment for First Function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

  HelloWorldFunction:
    Description: "First Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn

  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

CloudFormationのOutputsセクションの定義。
出力 - AWS CloudFormation

main.go

package main

import (
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

var (
    // DefaultHTTPGetAddress Default Address
    DefaultHTTPGetAddress = "https://checkip.amazonaws.com"

    // ErrNoIP No IP found in response
    ErrNoIP = errors.New("No IP in HTTP response")

    // ErrNon200Response non 200 status code in response
    ErrNon200Response = errors.New("Non 200 Response found")
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    resp, err := http.Get(DefaultHTTPGetAddress)
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    if resp.StatusCode != 200 {
        return events.APIGatewayProxyResponse{}, ErrNon200Response
    }

    ip, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    if len(ip) == 0 {
        return events.APIGatewayProxyResponse{}, ErrNoIP
    }

    return events.APIGatewayProxyResponse{
        Body:       fmt.Sprintf("Hello, %v", string(ip)),
        StatusCode: 200,
    }, nil
}

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

今回の関数。

main_test.go

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/aws/aws-lambda-go/events"
)

func TestHandler(t *testing.T) {
    t.Run("Unable to get IP", func(t *testing.T) {
        DefaultHTTPGetAddress = "http://127.0.0.1:12345"

        _, err := handler(events.APIGatewayProxyRequest{})
        if err == nil {
            t.Fatal("Error failed to trigger with an invalid request")
        }
    })

    t.Run("Non 200 Response", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(500)
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err != nil && err.Error() != ErrNon200Response.Error() {
            t.Fatalf("Error failed to trigger with an invalid HTTP response: %v", err)
        }
    })

    t.Run("Unable decode IP", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(500)
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err == nil {
            t.Fatal("Error failed to trigger with an invalid HTTP response")
        }
    })

    t.Run("Successful Request", func(t *testing.T) {
        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(200)
            fmt.Fprintf(w, "127.0.0.1")
        }))
        defer ts.Close()

        DefaultHTTPGetAddress = ts.URL

        _, err := handler(events.APIGatewayProxyRequest{})
        if err != nil {
            t.Fatal("Everything should be ok")
        }
    })
}

関数に対するテスト。

Makefile

.PHONY: deps clean build

deps:
  go get -u ./...

clean: 
  rm -rf ./hello-world/hello-world
  
build:
  GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world

packageする

次に作成した関数をデプロイ用にパッケージ化します。

❯ aws s3 mb s3://hoge-fuga --region ap-northeast-1

パッケージの置き場所として、S3のバケットが必要なので、任意のバケットを用意するか、既にあるものを使います。作成するときは上のようなコマンドで作成する。

❯ sam package \                          
   --template-file template.yaml \        
   --output-template-file packaged.yaml \ 
   --s3-bucket hoge-fuga     

sam package コマンドで、作成したテンプレートから、パッケージングを実行する。デプロイに必要な情報は、今回の場合だと、outpu-template-fileで指定したpackaged.yamlで作成される。

デプロイ

❯ sam deploy \                   
   --template-file packaged.yaml \
   --stack-name sam-app \         
   --capabilities CAPABILITY_IAM  

sam deploy コマンドで実際にデプロイをします。--template-fileには先程出てきたpackaged.yamlを指定します。--stack-nameはCloudFormationのスタック名で、任意の名前を指定すれば良さそう。--capabilitiesはロールをいい感じにしておいて的なやつ。

f:id:tikasan0804:20180703081118p:plain デプロイ出来た!便利。

ローカル実行

SAMはDockerを使うことで、ローカル実行を可能にしている。

❯ sam local start-api
2018-07-03 08:19:00 Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
2018-07-03 08:19:00 You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2018-07-03 08:19:00  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
2018-07-03 08:19:02 Invoking hello-world (go1.x)
2018-07-03 08:19:02 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:go1.x Docker container image......
2018-07-03 08:19:05 Mounting /Users/jumpei/go/src/github.com/pei0804/serverless-sample/sam/sam-app/hello-world as /var/task:ro inside runtime container
START RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b Version: $LATEST
END RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b
REPORT RequestId: 69d1509b-f4cb-1597-862c-f4abceb3438b  Duration: 1180.19 ms    Billed Duration: 1200 ms        Memory Size: 128 MB     Max Memory Used: 10 MB
2018-07-03 08:19:08 No Content-Type given. Defaulting to 'application/json'.
2018-07-03 08:19:08 127.0.0.1 - - [03/Jul/2018 08:19:08] "GET /hello HTTP/1.1" 200 -
❯ curl 127.0.0.1:3000/hello
Hello, 103.2.249.5

ログはCloudWatchライクなものが出てくる。これは強い。

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作成自動化最高。