Route53のヘルスチェック情報で月間合計ダウンタイムと稼働率を計算してみた

はじめに AWSチームのすずきです。 AWSが提供するDNSサービスのRoute53が備えるヘルスチェックを利用して、 Webサービスの月間の合計ダウンタイム時間を求め、 SLAなどの基準となる月間稼働率の計算を試みる機 […] 続きを読む

AWSの料金表を自動作成したい

AWS の料金一覧画面から Google スプレッドシートに値をコピペするのに疲れたので、なんとか EC2 の料金表を自動生成できないかと思いました。試行錯誤の結果、ようやくデータ構造までは把握できました。

AWS Price List Service API

AWS Price List Service API (AWS Price List Service)で AWS の料金データを取得できます。今回は取っ掛かりとして AWS CLI を使うので、Pricing サブコマンドでやることになります。

この API は、使えるエンドポイントが以下に限られます。

このため、.aws/config は以下のようにしています。

$ cat .aws/config
[default]
output = json
region = us-east-1

Pricing で使えるサブコマンドは、以下の3種類です。

サブコマンド名 機能
describe-services サービスとその属性名の一覧
get-attribute-values サービス毎の属性値の一覧
get-products 価格の一覧

describe-services

describe-services を何も指定せずに呼び出すと、サービスとその属性名の一覧がずらずらと表示されます。なお便宜上、(JSONではなく)テキスト形式で出力しています。

$ aws pricing describe-services --output=text | head -5
SERVICES        AWSBudgets
ATTRIBUTENAMES  productFamily
ATTRIBUTENAMES  servicecode
ATTRIBUTENAMES  groupDescription
ATTRIBUTENAMES  termType

いろんなサービスがありますね。。

$ aws pricing describe-services --output=text | grep ^SERVICES | wc -l
92
$ aws pricing describe-services --output=text | grep ^SERVICES | head -5
SERVICES        AWSBudgets
SERVICES        AWSCloudTrail
SERVICES        AWSCodeCommit
SERVICES        AWSCodeDeploy
SERVICES        AWSCodePipeline

今回目的とするサービス(EC2)の正式なサービス名を調べます。

$ aws pricing describe-services --output=text | grep -i ec2
SERVICES        AmazonEC2

これ以降は、コマンドを発行する際にサービスコードを明示することで、なるべくデータ量を減らすようにします。

AmazonEC2 サービスで使えそうな属性名の候補を調べます。

$ aws pricing describe-services --output=text --service-code AmazonEC2 | wc -l
62
$ aws pricing describe-services --output=text --service-code AmazonEC2 | 
  grep -Ei '(type|cpu|ecu|memory)'
ATTRIBUTENAMES  volumeType
ATTRIBUTENAMES  locationType
ATTRIBUTENAMES  ecu
ATTRIBUTENAMES  gpuMemory
ATTRIBUTENAMES  elasticGpuType
ATTRIBUTENAMES  memory
ATTRIBUTENAMES  vcpu
ATTRIBUTENAMES  termType
ATTRIBUTENAMES  instanceType
ATTRIBUTENAMES  usagetype

AmazonEC2 に限っても、ATTRIBUTENAME(属性名)だけで 62 個もある…。

get-attribute-values

次に “instanceType” という属性名に着目して、その属性値の候補を取得します。

$ aws pricing get-attribute-values --output=text --service-code AmazonEC2 
  --attribute-name instanceType | wc -l
122
$ aws pricing get-attribute-values --output=text --service-code AmazonEC2 
    --attribute-name instanceType | grep -E 't[12]'
ATTRIBUTEVALUES t1.micro
ATTRIBUTEVALUES t2.2xlarge
ATTRIBUTEVALUES t2.large
ATTRIBUTEVALUES t2.medium
ATTRIBUTEVALUES t2.micro
ATTRIBUTEVALUES t2.nano
ATTRIBUTEVALUES t2.small
ATTRIBUTEVALUES t2.xlarge

インスタンスタイプだけで 122 個…。この調子ですべての組み合わせを表示しようとすると、表のサイズが爆発するのは明白です。API サーバにも無意味に負荷をかけそうなので、最初はとりあえず t1.micro の Linux インスタンス(かつ東京リージョンのみ)に絞ってやってみています。

get-products

このサブコマンドで価格の値が取れるのですが、そのままでは使い勝手がよくありません。

$ aws pricing get-products --service-code AmazonEC2  
  --filters Type=TERM_MATCH,Field=instanceType,Value=t1.micro 
            Type=TERM_MATCH,Field=operatingSystem,Value=Linux 
            'Type=TERM_MATCH,Field=location,Value=Asia Pacific (Tokyo)' 
  > t1-micro-linux-tokyo.txt
$ cat t1-micro-linux-tokyo.txt
{
    "PriceList": [
        "{"product":{"productFamily":"Compute Instance","attributes":{"memory(以下、死ぬほど長い文字列)...

jq というコマンドを通すといい感じに整形してくれるらしいのですが、どうもうまくパースしてくれません。いろいろと試してみたところ、配列の中身全体が “” で囲まれているのが問題のようです。ということで、この引用符を外してやると、うまく見えるようになりました。ついでにバックスラッシュも外します。

$ cat t1-micro-linux-tokyo.txt | 
  sed -e 's/\//g' -e 's/"{/{/g' -e 's/}"/}/g' | 
  jq
{
  "PriceList": [
    {
      "product": {
        "productFamily": "Compute Instance",
        "attributes": {
          "memory": "0.613 GiB",
          "vcpu": "1",
          "instanceType": "t1.micro",
          "tenancy": "Shared",
          "usagetype": "APN1-BoxUsage:t1.micro",
          "locationType": "AWS Region",
          "storage": "EBS only",
          "normalizationSizeFactor": "0.5",
          "instanceFamily": "Micro instances",
          "operatingSystem": "Linux",
          "servicecode": "AmazonEC2",
          "physicalProcessor": "Variable",
          "licenseModel": "No License required",
          "ecu": "Variable",
          "currentGeneration": "No",
          "preInstalledSw": "NA",
          "networkPerformance": "Very Low",
          "location": "Asia Pacific (Tokyo)",
          "servicename": "Amazon Elastic Compute Cloud",
          "processorArchitecture": "32-bit or 64-bit",
          "operation": "RunInstances"
        },
        "sku": "ERVWZ4V3UBYH4NQH"
      },
      "serviceCode": "AmazonEC2",
      "terms": {
        "OnDemand": {
          "ERVWZ4V3UBYH4NQH.JRTCKXETXF": {
            "priceDimensions": {
              "ERVWZ4V3UBYH4NQH.JRTCKXETXF.6YS6EN2CT7": {
                "unit": "Hrs",
                "endRange": "Inf",
                "description": "$0.026 per On Demand Linux t1.micro Instance Hour",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.JRTCKXETXF.6YS6EN2CT7",
                "beginRange": "0",
                "pricePerUnit": {
                  "USD": "0.0260000000"
                }
              }
            },
            "sku": "ERVWZ4V3UBYH4NQH",
            "effectiveDate": "2017-12-01T00:00:00Z",
            "offerTermCode": "JRTCKXETXF",
            "termAttributes": {}
          }
        },
        "Reserved": {
          "ERVWZ4V3UBYH4NQH.NQ3QZPMQV9": {
            "priceDimensions": {
              "ERVWZ4V3UBYH4NQH.NQ3QZPMQV9.2TG2D8R56U": {
                "unit": "Quantity",
                "description": "Upfront Fee",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.NQ3QZPMQV9.2TG2D8R56U",
                "pricePerUnit": {
                  "USD": "316"
                }
              },
              "ERVWZ4V3UBYH4NQH.NQ3QZPMQV9.6YS6EN2CT7": {
                "unit": "Hrs",
                "endRange": "Inf",
                "description": "USD 0.0 per Linux/UNIX (Amazon VPC), t1.micro reserved instance applied",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.NQ3QZPMQV9.6YS6EN2CT7",
                "beginRange": "0",
                "pricePerUnit": {
                  "USD": "0.0000000000"
                }
              }
            },
            "sku": "ERVWZ4V3UBYH4NQH",
            "effectiveDate": "2015-04-30T23:59:59Z",
            "offerTermCode": "NQ3QZPMQV9",
            "termAttributes": {
              "LeaseContractLength": "3yr",
              "OfferingClass": "standard",
              "PurchaseOption": "All Upfront"
            }
          },
          "ERVWZ4V3UBYH4NQH.6QCMYABX3D": {
            "priceDimensions": {
              "ERVWZ4V3UBYH4NQH.6QCMYABX3D.2TG2D8R56U": {
                "unit": "Quantity",
                "description": "Upfront Fee",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.6QCMYABX3D.2TG2D8R56U",
                "pricePerUnit": {
                  "USD": "138"
                }
              },
              "ERVWZ4V3UBYH4NQH.6QCMYABX3D.6YS6EN2CT7": {
                "unit": "Hrs",
                "endRange": "Inf",
                "description": "USD 0.0 per Linux/UNIX (Amazon VPC), t1.micro reserved instance applied",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.6QCMYABX3D.6YS6EN2CT7",
                "beginRange": "0",
                "pricePerUnit": {
                  "USD": "0.0000000000"
                }
              }
            },
            "sku": "ERVWZ4V3UBYH4NQH",
            "effectiveDate": "2015-04-30T23:59:59Z",
            "offerTermCode": "6QCMYABX3D",
            "termAttributes": {
              "LeaseContractLength": "1yr",
              "OfferingClass": "standard",
              "PurchaseOption": "All Upfront"
            }
          },
          "ERVWZ4V3UBYH4NQH.38NPMPTW36": {
            "priceDimensions": {
              "ERVWZ4V3UBYH4NQH.38NPMPTW36.2TG2D8R56U": {
                "unit": "Quantity",
                "description": "Upfront Fee",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.38NPMPTW36.2TG2D8R56U",
                "pricePerUnit": {
                  "USD": "100"
                }
              },
              "ERVWZ4V3UBYH4NQH.38NPMPTW36.6YS6EN2CT7": {
                "unit": "Hrs",
                "endRange": "Inf",
                "description": "Linux/UNIX (Amazon VPC), t1.micro reserved instance applied",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.38NPMPTW36.6YS6EN2CT7",
                "beginRange": "0",
                "pricePerUnit": {
                  "USD": "0.0090000000"
                }
              }
            },
            "sku": "ERVWZ4V3UBYH4NQH",
            "effectiveDate": "2016-10-31T23:59:59Z",
            "offerTermCode": "38NPMPTW36",
            "termAttributes": {
              "LeaseContractLength": "3yr",
              "OfferingClass": "standard",
              "PurchaseOption": "Partial Upfront"
            }
          },
          "ERVWZ4V3UBYH4NQH.HU7G6KETJZ": {
            "priceDimensions": {
              "ERVWZ4V3UBYH4NQH.HU7G6KETJZ.2TG2D8R56U": {
                "unit": "Quantity",
                "description": "Upfront Fee",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.HU7G6KETJZ.2TG2D8R56U",
                "pricePerUnit": {
                  "USD": "62"
                }
              },
              "ERVWZ4V3UBYH4NQH.HU7G6KETJZ.6YS6EN2CT7": {
                "unit": "Hrs",
                "endRange": "Inf",
                "description": "Linux/UNIX (Amazon VPC), t1.micro reserved instance applied",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.HU7G6KETJZ.6YS6EN2CT7",
                "beginRange": "0",
                "pricePerUnit": {
                  "USD": "0.0090000000"
                }
              }
            },
            "sku": "ERVWZ4V3UBYH4NQH",
            "effectiveDate": "2015-04-30T23:59:59Z",
            "offerTermCode": "HU7G6KETJZ",
            "termAttributes": {
              "LeaseContractLength": "1yr",
              "OfferingClass": "standard",
              "PurchaseOption": "Partial Upfront"
            }
          },
          "ERVWZ4V3UBYH4NQH.4NA7Y494T4": {
            "priceDimensions": {
              "ERVWZ4V3UBYH4NQH.4NA7Y494T4.6YS6EN2CT7": {
                "unit": "Hrs",
                "endRange": "Inf",
                "description": "Linux/UNIX (Amazon VPC), t1.micro reserved instance applied",
                "appliesTo": [],
                "rateCode": "ERVWZ4V3UBYH4NQH.4NA7Y494T4.6YS6EN2CT7",
                "beginRange": "0",
                "pricePerUnit": {
                  "USD": "0.0180000000"
                }
              }
            },
            "sku": "ERVWZ4V3UBYH4NQH",
            "effectiveDate": "2016-08-31T23:59:59Z",
            "offerTermCode": "4NA7Y494T4",
            "termAttributes": {
              "LeaseContractLength": "1yr",
              "OfferingClass": "standard",
              "PurchaseOption": "No Upfront"
            }
          }
        }
      },
      "version": "20180131042456",
      "publicationDate": "2018-01-31T04:24:56Z"
    }
  ],
  "FormatVersion": "aws_v1"
}

返されるデータの構造がわかったので、後は必要な項目だけを抜き出して CSV で出力してやれば、Google スプレッドシートにインポートで取り込めそうです。ということで、今日はここまでです。


っと、ここまで書いてから投稿しようとしたら、AWS Price List API の使用 とかいうよさげなページがあるのに気づいたのでこれから読みます。。。

(参照)

続きを読む

より美しくWP Offload S3 Liteに既存のメディアを登録する方法

解決したい課題

すでに運用中のwordpressサイトがあります。コンテンツが増加し動画などのメディアファイルが多くなってきて、apacheで配信しているとサイトがどんどん重くなってしまいます。

WP Offload S3 Liteプラグインを導入して新しいメディアファイルはS3から配信されるようになりましたが、既存の大量のメディアファイルをS3から配信するにはどうすればいいのか?

という問題に遭遇したので対応方法を残しておきます。

方法2:SQLでなんとかする方法(NG)

同じようなことでお困りの方が結構いらっしゃるのでググると対応方法が出てきます。その多くはSQLでpost, post_metaを直接書き換える方法がほとんどです。
ですが、レコード内の設定内容はPHPでシリアライズされており、文字数などの記録されています。
正しく書き込むには新しい内容を正しくシリアライズしなくてはいけません。
そのためにプログラムを書かなければいけないのがデメリットです。

方法2: wp_cliでなんとかする。(better)

wb_cliはwordpressの為のコマンドラインインターフェースです。オプション豊富で様々な操作が可能です。これを使ってやってみました。

wp_contents/uploadsの内容ををAWSに転送

aws cliで一発です。

aws s3 sync wp-content/uploads/ s3://awesome-site.hoge.com/wp-content/uploads/

お決まりですが、S3バケットを作成してIAMを作成して、必要な権限だけ与えてください。
{{ bucket_name }}の部分を実際のバケット名に置き換えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:CreateBucket",
                "s3:DeleteObject",
                "s3:Put*",
                "s3:Get*",
                "s3:List*"
            ],
            "Resource": [
                "arn:aws:s3:::{{ bucket_name }}",
                "arn:aws:s3:::{{ bucket_name }}/*"
            ]
        }
    ]
}

もう少し権限絞りたい気もしますが、Get/PutのみだとPluginから権限がないと怒られます。

post_metaにS3のパスを正しく記録する

プラグインを入れてからメディアファイルをアップロードすると
post_metaにS3のパスが記録されます。

そこで正しいpost_metaを書き込んであげることで、プラグインに正しくファイルの位置を教えることができます。

メディアファイルのIDを取得する

ここで処理したいのはすべてのmediaファイルです。
メディアファイルのPOST_IDをすべて取得するには、以下のコマンドを使えばいいみたいです。

メディアファイルはpost_typeがattachmentです。

./bin/wp-cli.phar post list --post_type=attachment --format=ids

これを配列にいれてLOOPで処理すれば良さそうです。

メディアファイルのパスを取得する。

postテーブルのレコードのなかでguidというフィールドにhttp://から始まるURIが書かれています。

このような形式で記録されています。

./bin/wp-cli.phar post get 6802
+-----------------------+-----------------------------------------------------------------------------+
| Field                 | Value                                                                       |
+-----------------------+-----------------------------------------------------------------------------+
| ID                    | 6802                                                                        |
| post_author           | 1                                                                           |
| post_date             | 2018-01-30 18:20:09                                                         |
| post_date_gmt         | 2018-01-30 09:20:09                                                         |
| post_content          |                                                                             |
| post_title            | 20130705170431                                                              |
| post_excerpt          |                                                                             |
| post_status           | inherit                                                                     |
| comment_status        | open                                                                        |
| ping_status           | open                                                                        |
| post_password         |                                                                             |
| post_name             | 20130705170431                                                              |
| to_ping               |                                                                             |
| pinged                |                                                                             |
| post_modified         | 2018-01-30 18:20:09                                                         |
| post_modified_gmt     | 2018-01-30 09:20:09                                                         |
| post_content_filtered |                                                                             |
| post_parent           | 0                                                                           |
| guid                  | http://awsome-site.hoge.com/wp-content/uploads/2018/01/20130705170431.png |
| menu_order            | 0                                                                           |
| post_type             | attachment                                                                  |
| post_mime_type        | image/png                                                                   |
| comment_count         | 0                                                                           |
+-----------------------+-----------------------------------------------------------------------------+

必要なのはguidの中にあるcontent/uploads/内のパスですので、正規表現を使って
http://xxx.hoge.com/ みたいなプロトコル・ドメイン名を外します。

./bin/wp-cli.phar post get $ID --field=guid | sed -e 's/(http:.*)/(wp-content.*)/2/'

jqを使ってpost_metaを生成する

プラグインを入れた後にメディアファイルを追加すると以下のようなレコードがpost_metaに生成されます。KEYはamazonS3_infoです。

./bin/wp-cli.phar post meta get 6802 amazonS3_info

3つの情報が記録されています。これを新たに生成してあげればよいわけです。

array (
  'bucket' => 'site.hoge.com',
  'key' => 'wp-content/uploads/2018/01/30182009/20130705170431.png',
  'region' => 'ap-northeast-1',
)

正しいJSONを生成したいので、みんなが大好きなjqを使ってゼロからJSONを構築します。
jqはAWS CLIの結果をJSONでもらって、いろんなフィルタしたりするのに使うことが多いですが、シェルスクリプトからJSONを生成するにも便利です。

実際に実行すると、このような結果が得られます。

jq -Mncr ".bucket = "bucket-name" | .key = "GUID" | .region = "ap-northeast-1""
{"bucket":"bucket-name","key":"GUID","region":"ap-northeast-1"}

こうして生成したJSONを変数にいれて、wp-cliを使ってpost_metaを追加します。

JSON=`jq -Mncr ".bucket = "$BUCKET" | .key = "$GUID" | .region = "ap-northeast-1""`
echo $JSON | ./bin/wp-cli.phar post meta add $ID amazonS3_info --format=json

ちなみにpost_metaを削除することも可能です。

./bin/wp-cli.phar post meta delete $ID amazonS3_info

出来上がり

でき当たったシェルスクリプトがこれです。
すべてのメディアファイルをWP Offload S3 LiteをつかってS3で配信することができるようになりました。

#!/bin/bash

ARRAY=(`./bin/wp-cli.phar post list --post_type=attachment --format=ids`)
for ID in "${ARRAY[@]}"; do
    GUID=`./bin/wp-cli.phar post get $ID --field=guid | sed -e 's/(http:.*)/(wp-content.*)/2/'`
    BUCKET='bucket-name.hoge.com'
    JSON=`jq -Mncr ".bucket = "$BUCKET" | .key = "$GUID" | .region = "ap-northeast-1""`
    echo $JSON | ./bin/wp-cli.phar post meta add $ID amazonS3_info --format=json
done

続きを読む

オートヒーリングでゆるーくEIPもヒーリングしてみた

region=$(curl -s http: //169 .254.169.254 /latest/meta-data/placement/availability-zone | sed -e ‘s/.$//’ ). export AWS_DEFAULT_REGION=${region}. allocation_id=$(aws ec2 describe-instances –instance- id ${instance_id} | jq -r ‘.Reservations[].Instances[].Tags[] | select(.Key == “allocation-id”).Value’ ). 続きを読む

LocalStack導入から実行まで

はじめに

最近、AWS API GateWay + Lambdaを使用して、サーバレスでWeb APIを作成する機会がありました。
AWS コンソールから関数の作成、コードの編集を行う、といった体制で進めていましたが、一つの問題が出ました。

それが「コード書き換えてしまう問題」です

具体的に説明しますと、作業者AがとあるLambda関数を編集中に、作業者Bが同じLambda関数を更新しました。
その後、作業者Aが編集内容を更新すると、作業者Bの編集した内容が消えてしまうという問題です。
当時はバージョンの発行を行なっておらず、消えてしまったコードを遡ることができませんでした(このことを機にバージョン発行を覚えました)

元々、Lambdaで作成したソースコードをGitで管理したい、ローカル環境でテストしてから反映したい、等の意見を頂いていたので、ローカルによるLambda関数の実行、バージョンの管理をテーマに調べました、

そこで出会ったのがLocalStackでした。

LocalStackとは

LocalStackは、ローカルにAWSのモック/テスト環境を作ってくれてるツールです。

イメージとしては、ローカルに擬似的なAWS環境を作ってくれて、料金など気にせずにテストや
動作確認を可能にしてくれます。
他にもツールはありましたが、LocalStackの導入が比較的簡単そうであったため、今回はこれを使用します。

要件

  • ローカルにてLambda関数の実行
  • ソースコードをGitにて管理
  • AWS Lambda反映時に自動でバージョンを作成

環境

  • OS: macOS High Sierra(10.13.2)
  • Lambda ランタイム: Python3.6

事前準備

docker, docker-composeのインストール

公式サイトはコチラ

aws-cliのインストール

$ brew install awscli

aws アクセスキーの発行

公式サイトにて方法が記載されているので、各自発行してください。

aws プロファイルの設定

$ aws configure --profile localstack

AWS Access Key ID [None]: アクセスキー
AWS Secret Access Key [None]: シークレットキー
Default region name [None]: ap-northeast-1
Default output format [None]: text

jqコマンドのインストール

$ brew install jq

LocalStack導入手順

LocalStackインストール&起動

$ git clone https://github.com/localstack/localstack
$ cd localstack
$ TMPDIR=/private$TMPDIR docker-compose up -d

上記実行後、http://localhost:8080にてlocalstackのダッシュボート表示

スクリーンショット 2018-01-23 17.35.16.png

Lambda関数作成

$ mkdir 任意の作業ディレクトリ名
$ cd 任意の作業ディレクトリ名
$ vi lambda.py
lambda.py
# サンプルコードです
def lambda_handler(event, context):
    return 'Hello from Lambda'

localstack上にLambda関数を作成

  1. zipで固める
$ zip lambda.zip lambda.py
  1. localstack上に作成
$ aws --endpoint-url=http://localhost:4574 --region ap-northeast-1 --profile localstack lambda create-function --function-name="作成する関数名" --runtime=python3.6 --role="実行ロール名" --handler=lambda.lambda_handler --zip-file fileb://lambda.zip

スクリーンショット 2018-01-24 11.01.33.png

lambda関数実行

$ aws lambda --endpoint-url=http://localhost:4574 invoke --function-name "実行する関数名" --payload '{"key1":"value1", "key2":"value2", "key3":"value3"}' result.log
200

200が返って来たら成功

レスポンスの確認

$ cat result.log | jq

result.logをjqコマンドでJSON形式に変換して出力することで確認できます。


localstack上のlambda関数の一覧取得

$ aws --endpoint-url=http://localhost:4574 --region ap-northeast-1 --profile localstack lambda list-functions

localstack上のlambda関数の削除

$ aws --endpoint-url=http://localhost:4574 --region ap-northeast-1 --profile localstack lambda delete-function --function-name "削除する関数名"

localstack上のlambda関数の更新

$ aws --endpoint-url=http://localhost:4574 --region ap-northeast-1 --profile localstack lambda update-function-code --profile localstack --function-name "更新する関数名" --zip-file fileb://lambda.zip --publish

感想

dockerでローカルにawsテスト用のエンドポイントを作成し、awscliでlambda関数をLocalStackにあげて実行するような形です。
コマンドが少し長くて大変ですが、そこはエイリアスを設定したりでカバーできたらと思っています。
lambda関数の実行/作成/削除/更新のコマンドオプションに--endpoint-url=http://localhost:4574という指定があります。
これがLocalStackでの実行を指定しており、このオプションをつけないことによって、運用しているAWSに対して直接lambda関数の作成や更新など行うことができるため、localでテストしてすぐ本番に反映できるような感じです。
lambda関数更新時は、Lambda側が勝手にバージョンを作成してくれるため、Lambda側のバージョン管理も簡単にできるように感じました。

落とし穴

Lambdaにて関数を作成した場合、ファイル名はlambda_function.拡張子で固定されています。
ローカルでファイル名をhogehoge.pyのように適当につけて、既存lambda関数に対して更新をかけると、ファイルがないと怒られて動かなくなるため、注意が必要です。(テスト環境が一時停止して結構焦りました。)


参考サイト

続きを読む

[AWS] ELBとEC2の tag:Name の仕様の違いについて

細かい点ですが、EC2とELBの所謂「Nameタグ」の仕様に違いがあったのでまとめておきます。

EC2の場合

コンソールでEC2を確認すると、一覧と「タグ」タブ内に2つ”Name”が確認できます。EC2では、この2つは同じものです。

image.png

これは実体としてはタグで、describe-instancesコマンドで確認できます。

$ aws ec2 describe-instances --region eu-west-3 | jq -r '.Reservations[] .Instances[] .Tags[]'
{
  "Value": "bastion-ec2",
  "Key": "Name"
}

更新したい場合は、create-tagsコマンドで(少々違和感がありますが)上書きできます。

$ aws ec2 create-tags --region eu-west-3 --resources i-02axxxxxxxxxxxxx --tags Key=Name,Value=name-update-test

コンソールでは両方更新されることが確認できます。

image.png

ELBの場合

コンソールでELBを確認すると、一覧の方に”名前”が、「タグ」タブの方に”Name”が表示されています。EC2と似た表示ですが、ELBではこれらは別物です。

image.png

“名前”の方はロードバランサ名(LoadBalancerName)でdescribe-load-balancersコマンドで確認できます。”Name”の方がタグで、describe-tagsコマンドで確認できます。

$ aws elb describe-load-balancers --region eu-west-3 | jq -r '.LoadBalancerDescriptions[] .LoadBalancerName'
internal-elb
$ aws elb describe-tags --region eu-west-3 --load-balancer-names internal-elb
{
    "TagDescriptions": [
        {
            "Tags": [
                {
                    "Value": "this-is-name-tag",
                    "Key": "Name"
                }
            ],
            "LoadBalancerName": "internal-elb"
        }
    ]
}

ロードバランサ名(LoadBalancerName)の方は、作成後には更新できません。タグの方はEC2と同様にadd-tagsコマンドで上書きできます。

$ aws elb add-tags --region eu-west-3 --load-balancer-names internal-elb --tags Key=Name,Value=tag-update-test
$ aws elb describe-tags --region eu-west-3 --load-balancer-names internal-elb
{
    "TagDescriptions": [
        {
            "Tags": [
                {
                    "Value": "tag-update-test",
                    "Key": "Name"
                }
            ],
            "LoadBalancerName": "internal-elb"
        }
    ]
}

コンソールでもタグの方だけ更新されたことが確認できます。

image.png

ALBまたはNLBの場合

ALB(Application Load Balancer)またはNLB(Network Load Balancer)の場合も同様の動作となります。awscliはelbv2コマンドで確認できます。

名前とタグの確認:

$ aws elbv2 describe-load-balancers --region ap-northeast-1 | jq -r '.LoadBalancers[] .LoadBalancerName'
internal-elb
$ aws elbv2 describe-tags --region ap-northeast-1 --resource-arns arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:loadbalancer/net/internal-elb/xxxxxxxxxx
{
    "TagDescriptions": [
        {
            "ResourceArn": "arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:loadbalancer/net/internal-elb/xxxxxxxxxx",
            "Tags": [
                {
                    "Value": "this-is-name-tag",
                    "Key": "Name"
                }
            ]
        }
    ]
}

タグの変更:

$ aws elbv2 add-tags --region ap-northeast-1 --tags Key=Name,Value=tag-update-test --resource-arns arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:loadbalancer/net/internal-elb/xxxxxxxxxx
$ aws elbv2 describe-tags --region ap-northeast-1 --resource-arns arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:loadbalancer/net/internal-elb/xxxxxxxxxx
{
    "TagDescriptions": [
        {
            "ResourceArn": "arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:loadbalancer/net/internal-elb/xxxxxxxxxx",
            "Tags": [
                {
                    "Value": "tag-update-test",
                    "Key": "Name"
                }
            ]
        }
    ]
}

まとめ

EC2の場合は特に意識せず「Nameタグ」を使っていましたが、ELBの場合は名前(LoadBalancerName)とタグ(tag:Name)が別々に設定できるので混同しないよう注意が必要です。違う値を設定した場合はどちらを意図しているのかきちんと確認するようにしましょう。

続きを読む

[AWS]ACMの期限確認を自動にしてSlackに飛ばすようにしてみた

AWSからACMの更新メールが来ないときあるので、念のため作ってみた
速攻作ったので、クレーム一切受け付けません

jq不使用
複数アカウント対応

#!/bin/bash

now=`date "+%s"`
_lockfile_dir=/***/***/.acm-tmp-lock
_lockfile=${_lockfile_dir}/acm_check.lock

[ -d ${_lockfile_dir} ] || mkdir ${_lockfile_dir}

if [ -f ${_lockfile} ]; then
    echo "LOCKED"
    exit 1
fi

#Create LockFile
touch ${_lockfile}

_conf=/***/***/conf/
_date=`date`

for _confgile in `ls -1 ${_conf}|grep knife.rb`
do
    _title=`echo ${_confgile} | sed 's/-knife.rb//g'`
    _region=`grep region ${_conf}${_confgile} | cut -d '=' -f 2 | cut -d '"' -f 2`
    chanel_name='#chanel-name'

    all_region=(us-east-2 us-east-1 us-west-1 us-west-2 ap-south-1 ap-northeast-2 ap-southeast-1 ap-southeast-2 ap-northeast-1 ca-central-1 eu-central-1 eu-west-1 eu-west-2 sa-east-1)
    for region_all in ${all_region[@]}
    do
      for acmid in `aws --profile ${_title} --region ${region_all} acm list-certificates --output text | awk -F " " '{print $2}'`
      do

        certificate_name=`aws --profile ${_title} --region ${region_all} acm describe-certificate --certificate-arn ${acmid} --query Certificate.[DomainName] --output text`
        certificate_use=`aws --profile ${_title} --region ${region_all} acm describe-certificate --certificate-arn ${acmid} --query Certificate.[InUseBy] --output text`
        certificate_unix_time=`aws --profile ${_title} --region ${region_all} acm describe-certificate --certificate-arn ${acmid} --query Certificate.[NotAfter] --output text | awk -F "." '{print $1}'`
        if [ "${certificate_unix_time}" != "None" ]; then
         jst_time=`date -d @$certificate_unix_time`
         rema_check=`expr $certificate_unix_time - $now`
         rema_time=`expr $rema_check / 86400 + 1`
        fi

         if [ $rema_time -lt 40 ] && [ -n "${certificate_use}" ]; then
           curl -X POST --data-urlencode "payload={"channel": "$chanel_name", "username": "ACM-Alert", "text": " Warning remaining $rema_time day $certificate_name $region_all", "icon_emoji": ":warning:"}" https://hooks.slack.com/
         elif [ $rema_time -lt 40 ]; then
           curl -X POST --data-urlencode "payload={"channel": "$chanel_name", "username": "ACM-Alert", "text": " Info remaining $rema_time day $certificate_name but Not Use ACM $region_all", "icon_emoji": ":warning:"}" https://hooks.slack.com/
         else
          echo "[Remaining] ${rema_time}day" #ファイルに吐いてもいいと思う なんでもいい
         fi

      done
   done
done

rm -f ${_lockfile}

動かなくても何も言わないでね!!
そのうちDocker化してみる

続きを読む

Glueの使い方的な⑥(監視モニタリング)

監視モニタリング概要

現状整っているとは言い難いので、他のサービスも含めた監視について考えてみる

全体の流れ

  • CloudWatchメトリクス
  • CloudWatchイベント
  • CloudWatchログ
  • APIで状態取得
  • 何を監視すべきか

CloudWatchメトリクス

ない

今後は追加されてくるかと思います

CloudWatchイベント

バージニアには以下のイベントがあります。
現状、”Job State change”と”Crawler state change”の2つ

スクリーンショット 0030-01-04 18.12.26.png

Job State changeはステートの全ての変化のみ

スクリーンショット 0030-01-04 18.12.39.png

Crawler state changeは任意のステートの変化
Failed
Started
Succeeded

スクリーンショット 0030-01-04 18.12.54.png

これらのイベントをフックに、Lambda動かしたり、SNSで通知したり、StepFunction動かしたり、SQSにキューしたりできます。

ただ、東京とアイルランドにはこのGlueのイベントがありません↓

スクリーンショット 0030-01-04 18.28.00.png

CloudWatchログ

GlueはCloudWatchに出力します。ログに関してはCloudWatchのログ監視が使えます
出力ログは主にSparkのログを”error”と”output”で2種類です

一般的なCloudWatchのログに対してのアラーム設定手順になります

CloudWatchのログの画面に行く。
“aws-glue/jobs/error”のロググループにチェックを入れ、”メトリクスフィルタ”ボタンをクリック

スクリーンショット 0030-01-06 10.18.42.png

ログメトリクスフィルタの定義の画面です
フィルタする文字列パターンに文字列を入れ”パターンのテスト”をクリックすればフィルタの確認が出来ます。結果は一番下に出力されます。
テストするログデータも変更できます。
画面はテストなので”INFO”でフィルタしています。サンプルから1件フィルタできてることがわかります。
エラーログなのにINFOが出てます。。
確認ができたら右下の”メトリクスの割り当て”をクリックします。

スクリーンショット 0030-01-06 10.20.48.png

フィルタの名前、メトリクス名などを任意の名前で入力して、右下の”フィルタの作成”をクリックします

スクリーンショット 0030-01-06 10.20.55.png

フィルタが作成されました

スクリーンショット 0030-01-06 10.21.43.png

最初のCloudWatchログの画面に行くと、”aws-glue/jobs/error”ロググループの右側に”1フィルタ”となっています
ここをクリックします

スクリーンショット 0030-01-06 10.22.19.png

右上の”アラームの作成”をクリックします

スクリーンショット 0030-01-06 10.22.44.png

いつものアラーム設定画面となります

スクリーンショット 0030-01-06 10.23.03.png

APIで状態取得

Glueの使い方的な③(CLIでジョブ作成)“(以後③と書きます)でも触れたようにAPIにアクセスして状態の取得ができます。他のAWSリソースももちろん同様のことができます。

get系のAPIが使えます

クローラーの状態取得

例えばクローラーのgetで取得できる情報は

$ aws glue get-crawler --name se2_in0
{
    "Crawler": {
        "CrawlElapsedTime": 0, 
        "Name": "se2_in0", 
        "CreationTime": 1514874041.0, 
        "LastUpdated": 1514874041.0, 
        "Targets": {
            "JdbcTargets": [], 
            "S3Targets": [
                {
                    "Path": "s3://test-glue00/se2/in0", 
                    "Exclusions": []
                }
            ]
        }, 
        "LastCrawl": {
            "Status": "SUCCEEDED", 
            "LogStream": "se2_in0", 
            "MessagePrefix": "903fa0e1-2874-4b50-a686-660d2da54004", 
            "StartTime": 1515146760.0, 
            "LogGroup": "/aws-glue/crawlers"
        }, 
        "State": "READY", 
        "Version": 1, 
        "Role": "test-glue", 
        "DatabaseName": "se2", 
        "SchemaChangePolicy": {
            "DeleteBehavior": "DEPRECATE_IN_DATABASE", 
            "UpdateBehavior": "UPDATE_IN_DATABASE"
        }, 
        "TablePrefix": "se2_", 
        "Classifiers": []
    }
}

ここから状態だけを取ってきたければ以下のような感じでステータスを見て成否判定することは出来ます。

$ aws glue get-crawler --name se2_in0 | jq -r .Crawler.LastCrawl.Status
SUCCEEDED

ジョブの状態取得

ジョブも同じ要領ですが、ステータスを得るためにget-job-runにRunIDを渡して上げる必要があります

RunIDはstart-jobでリターン値で得られます

$ aws glue start-job-run --job-name se2_job0
{
    "JobRunId": "jr_711b8b157e3b36a1dc1a48c87c5e8b00c509150cc4f9d7d7106009e57f2cac9b"
}

また、get-job-runsでジョブの履歴から取ることもできます

$ aws glue get-job-runs --job-name se2_jobx 
{
    "JobRuns": [
        {
            "LastModifiedOn": 1514440793.923, 
            "StartedOn": 1514440639.623, 
            "PredecessorRuns": [], 
            "Attempt": 0, 
            "JobRunState": "SUCCEEDED", 
            "JobName": "se2_jobx", 
            "Arguments": {
                "--job-bookmark-option": "job-bookmark-disable"
            }, 
            "AllocatedCapacity": 10, 
            "CompletedOn": 1514440793.923, 
            "Id": "jr_1b3c00146b02e36e4682f352a084a2fd37931967346b44a7c39ad182347957d3"
        }, 
        {
            "LastModifiedOn": 1514440548.4, 
            "StartedOn": 1514440394.07, 
            "PredecessorRuns": [], 
            "Attempt": 0, 
            "JobRunState": "FAILED", 
            "ErrorMessage": "An error occurred while calling o54.getCatalogSource. No such table: se4.se04_in", 
            "JobName": "se2_jobx", 
            "Arguments": {
                "--job-bookmark-option": "job-bookmark-disable"
            }, 
            "AllocatedCapacity": 10, 
            "CompletedOn": 1514440548.4, 
            "Id": "jr_aca8a8c587b0986d040aba7eadc7b216e83db409f842cb9d2912c400b181c907"
        }
    ]
}

取得できたRunIDを使ってステータスを取ります

$ aws glue get-job-run --job-name se2_job0 --run-id jr_dff0ac334e5c5bf3043acc5158f9c3bc1f9c8eae048e053536581278ec34a063 
{
    "JobRun": {
        "LastModifiedOn": 1514875561.077, 
        "StartedOn": 1514875046.406, 
        "PredecessorRuns": [], 
        "Attempt": 0, 
        "JobRunState": "SUCCEEDED", 
        "JobName": "se2_job0", 
        "Arguments": {
            "--job-bookmark-option": "job-bookmark-disable"
        }, 
        "AllocatedCapacity": 10, 
        "CompletedOn": 1514875561.077, 
        "Id": "jr_dff0ac334e5c5bf3043acc5158f9c3bc1f9c8eae048e053536581278ec34a063"
    }
}
$ aws glue get-job-run --job-name se2_job0 --run-id jr_dff0ac334e5c5bf3043acc5158f9c3bc1f9c8eae048e053536581278ec34a063 | jq .JobRun.JobRunState -r
SUCCEEDED

ポーリング型とはなりますが、定期的に状態を確認するというやり方もあります

何を監視すべきか

Glueはバッチ処理で多くの場合ジョブフローを形成する中の一部として使われると思います

何を見るべきかはいろんな意見や視点がありますし業務のサービスレベルでも違うので一概に言えないですが、お勧めとして

まずはジョブの成否を見ます。

“CloudWatchイベント”でも述べたように現状Glueジョブのイベントタイプは”全てのステータスチェンジ”の1つしかないので、ジョブが失敗したらアラートを飛ばすということがCloudWatchだけだと出来ません。本記事の”APIで状態取得”で述べたような方法を使って、ジョブ実行した後非同期でステータスを確認する。またはCloudWatchイベントでJobStateChangeのイベントでLambdaを起動してGlueのAPIを叩いてステータス確認するとよりタイムラグない監視になると思います。

多くの場合ジョブフローを形成し、1つのジョブステップを処理の最小単位とするので、その成否の確認はロールバックし易さや業務影響などの考慮が入っています。その単位でジョブの失敗に気づき、そしてより詳細な調査が必要な場合にログを確認していくのが1つのパターンと思います
このジョブフローをサーバーレスで行うサービスにAWSのStepFunctionというサービスがあります。Glueとも相性がいいのでまた今度書いてゆきます

エラーは吐かないが実行時間が長い

これも気づきたいポイントになると思います。
ジョブのスタート時間は取れるので、そこから経過した時間を算出し、通常1時間のジョブが2時間を超えたらアラートを上げる
などもいいと思います。

$ aws glue get-job-run --job-name se2_job0 --run-id jr_dff0ac334e5c5bf3043acc5158f9c3bc1f9c8eae048e053536581278ec34a063 | jq .JobRun.StartedOn -r
1514875046.406

ログ監視

上記の監視運用を繰り返していくと、ジョブが失敗してログ調査をしてこのログは検知したいといった知見が溜まっていきます。
その場合は本記事の”CloudWatchログ”でも述べたやり方でログ監視してください。ジョブが失敗することには変わらないですがログのアラートも飛んでくることで既知の問題であることが即座にわかりトラブルシュートが格段に早まります

To Be Continue

  • StepFunctionでGlueのジョブフローを作るを書く

こちらも是非

CloudWatchログ監視
http://www.d2c-smile.com/201609137826#a1

続きを読む

【AWS】Aurora(MySQL)のCluster/Instanceを自動的に削除/リストアする【Aurora】

はじめに

Auroraがリリースされてから後、AWS上で稼働するサービスでAuroraを利用している諸兄諸姉が多いのではないかと思います。

RDS全般にまつわる悩み事ですが、開発環境の費用削減のため、RDSを利用しない間は停止したいと思っても、
RDSは容易に停止できない、削除することしかできないという制約があります。

そこで、Aurora Cluster/Instanceのsnapshotを作成して削除するスクリプトと、
削除時と同じ構成のAurora Cluster/Instanceをリストアするスクリプトを作ってみましたので、参考までに公開します。

このスクリプトを実際に利用したことにより発生した損害等には責任を負いません。
また、利用に関するサポートは致しません。
飽くまで参考程度にしてください。

基本的な考え方

Auroraはその構成上、単一インスタンス構成であろうとも、必ずClusterとして構成されます。

Auroraインスタンスの作成/削除時の流れは概ね下記のようになります。
簡単に言うと、Clusterというガワがまず存在して、ここにInstanceを追加/削除していく、というイメージになります。

  • インスタンス削除

    • メンバーInstanceを削除する
    • Instanceの削除完了を待つ(対象Instanceが存在しなくなる、ClusterのDBClusterMembersが存在しなくなる)
    • Clusterを削除する。併せて、snapshotを作成する。
    • Clusterの削除完了を待つ
  • インスタンス作成
    • Aurora Clusterを作成する
    • Clusterの作成完了を待つ(Statusがavailableになる)
    • 作成したClusterのメンバーとなるInstanceを作成する
    • Instanceの作成完了を待つ(Statusがavailableになる)

スクリプトの解説

前提条件

  • 既に存在しているAurora Cluster/Instanceを削除し、これを任意のタイミングでリストアする、という利用方法を想定しています。
  • このスクリプトはAWS CLIがインストールされたLinuxで動作します。
  • AWS CLIに環境ごとのprofileが設定されていることとします。
  • 対象はAurora(MySQL)エンジンのみとし、1Cluster・1Instance構成のみに対応しています。
  • Cluster名はInstance名に「-cluster」サフィックスが付与された名称とします。

2インスタンス以上のClusterは対象としていません。
こまめな削除・リストアを必要とするのは検証や開発で利用する環境のみであること、
それらの環境は通常はシングルインスタンスの最小構成であることから、このような前提となっています。

検証した環境

OS: CentOS Linux release 7.2.1511
Bash: GNU bash, version 4.2.46(1)-release (x86_64-redhat-linux-gnu)
AWS CLI: aws-cli/1.11.63 Python/2.7.5 Linux/3.10.0-327.22.2.el7.x86_64 botocore/1.5.26

Cluster/Instance削除

第1引数にAWS CLIのプロファイルを、第2引数には削除対象のAurora Instanceを改行(LFのみ)で羅列したファイルを指定します。
また、空行をスキップする処理は入れていないので、空行は含めないようにしてください。

具体的には、下記のようなファイルとなります。

rds-aurora-instance1
rds-aurora-instance2

Aurora Clusterは、Aurora Instance名に「-cluster」サフィックスが付与されたものと想定します。

これを実行すると、カレントディレクトリ配下に「result-delete-yyyymmdd」というディレクトリが作成され、ログ等が出力されます。
削除したCluster/Instanceに関する情報がJSONとして出力されますが、これはリストア時に必要となります。
# 上記JSONは、rds-describe-db-clusters及びrds-describe-db-instancesの実行結果となります。

RDS snapshotとして、「Instance名-yyyymmdd」と「Instance名-final」の2つが作成されます。
「Instance名-final」は次回削除時に手動で削除する必要があるため、誤操作に対する備えとして、「Instance名-yyyymmdd」を作成するようにしています。

#!/bin/sh


if [ $# -lt 2 ] ;then
    echo "usage: $0 <AWS CLI profile> <Listfile of RDS instances to be deleted>"
    exit 1;
fi


AWS_CLI_PROFILE=$1
RDS_LIST=$2

CURRENT_DATE=`TZ=Asia/Tokyo date +%Y%m%d`
CURRENT_DATETIME=`TZ=Asia/Tokyo date +%Y%m%d-%H%M%S`

OUTPUT_DIR="./result-delete-${CURRENT_DATE}"
LOG_STDOUT_FILE=${OUTPUT_DIR}/rds_delete_instance_${CURRENT_DATETIME}_stdout.log
LOG_STDERR_FILE=${OUTPUT_DIR}/rds_delete_instance_${CURRENT_DATETIME}_stderr.log
TMP_FILE=`mktemp`

alias aws="aws --profile ${AWS_CLI_PROFILE}"

mkdir -p ${OUTPUT_DIR}

#
# Utility Functions
#

function log_both () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDOUT_FILE}
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDERR_FILE}
    return
}

function log_std () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDOUT_FILE}
    return
}

function log_err () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDERR_FILE}
    return
}

function _describe_current_information (){
    local _rds_instance=$1
    local _rds_cluster=$2
    local _cmd

    # fetch current information of the RDS instance
    log_both "creating snapshots of \"${_rds_instance}\""
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-instances --filters Name=db-instance-id,Values=${_rds_instance}" >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd > ${TMP_FILE} 2>> ${LOG_STDERR_FILE}
    cat ${TMP_FILE} >> ${LOG_STDOUT_FILE}
    cp ${TMP_FILE} ${OUTPUT_DIR}/${_rds_instance}_instance_information_${CURRENT_DATETIME}.json

    # fetch current information of the RDS cluster
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd > ${TMP_FILE} 2>> ${LOG_STDERR_FILE}
    cat ${TMP_FILE} >> ${LOG_STDOUT_FILE}
    cp ${TMP_FILE} ${OUTPUT_DIR}/${_rds_instance}_cluster_information_${CURRENT_DATETIME}.json

}

function _create_snapshot (){
    local _rds_cluster=$1
    local _rds_snapshot=$2
    local _cmd

    # create an explicit snapshot
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds create-db-cluster-snapshot --db-cluster-identifier ${_rds_cluster} --db-cluster-snapshot-identifier ${_rds_snapshot}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

}

function _delete_instance (){
    local _rds_instance=$1
    local _cmd
    local _instance_num

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds delete-db-instance --db-instance-identifier ${_rds_instance}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-instances --filters Name=db-instance-id,Values=${_rds_instance}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}


    # wait to finish deliting the RDS instance
    log_both "waiting to finish deleting the RDS instance \"${_rds_instance}\"."
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-instances --filters Name=db-instance-id,Values=${_rds_instance}"
    log_both "checking the RDS instance sutatus with the following AWS API: \"${_cmd}\"."

    while true
    do
        sleep 10

        _instance_num=`${_cmd} | jq '.DBInstances | length'`
        if [ ${_instance_num} -eq 1 ];then
            continue
        fi

        break
    done

    log_both "deleting the RDS instance \"${_rds_instance}\" has been finished."
}

function _delete_cluster (){
    local _rds_cluster=$1
    local _rds_snapshot=$2

    # wait until the members of its cluster is 0.
    log_both "waiting to change the members of the cluster \"${_rds_cluster}\" 0."
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --db-cluster-identifier ${_rds_cluster}"
    log_both "checking whether the members of the cluster is 0 with the following AWS API: \"${_cmd}\"."

    while true
    do
        sleep 5

        _instance_num=`${_cmd} | jq '.DBClusters[0].DBClusterMembers | length'`
        if [ ${_instance_num} -ne 0 ];then
            continue
        fi

        break
    done

    log_both "current cluster status: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    # start deleting the cluster
    _cmd="aws --profile ${AWS_CLI_PROFILE} rds delete-db-cluster --db-cluster-identifier ${_rds_cluster} --final-db-snapshot-identifier ${_rds_snapshot}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    _cmd="aws --profile ${AWS_CLI_PROFILE} rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

}


#
# Main Function
#

    #
    # Description:
    #   This program deletes an Aurora RDS cluster and its instance and creates its snapshots automatically.
    #   It enables you to treat an RDS as if it can stop/start instances.
    #   It deal with each RDS instances one by one.
    #   It assumes that the cluster name is a combination with its instance name and a "-cluster" suffix.
    #
    # How it works in detail:
    #   1. Fetch current information. It will be saved into the ${OUTPUT_DIR} directory.
    #   2. Create its snapshot. it will be named what consists its instance name and a datetime suffix.
    #   3. Delete the RDS instance with creating a snapshot named what is the same as its instance name.
    #
    # License:
    #   Copyright (c) 2017 Taichi Miki
    #   Released under the MIT license
    #


    log_both "a temporary file is ${TMP_FILE}"

    for _rds_instance in `grep -v "^#.*$" ${RDS_LIST}`
    do

        _rds_cluster="${_rds_instance}-cluster"

        log_both "Start deleting the instance \"${_rds_instance}\", cluster \"${_rds_cluster}\" with creating snapshots."

        # fetch current information
        _describe_current_information ${_rds_instance} ${_rds_cluster}

        # create an explicit snapshot
        _create_snapshot ${_rds_cluster} "${_rds_instance}-${CURRENT_DATE}"

        # delete the RDS instance
        log_both "deleting the instance \"${_rds_instance}\""
        _delete_instance ${_rds_instance}


        # delete the RDS cluster with creating a snapshot
        log_both "deleting the cluster \"${_rds_cluster}\""
        _delete_cluster ${_rds_cluster} "${_rds_instance}-final"

        log_both "the deleting process for the instance \"${_rds_instance}\", cluster \"${_rds_cluster}\" has been finished."
    done

    log_both "delete the temporary file \"${TMP_FILE}\"."
    rm ${TMP_FILE}

    log_both "the deleting process for all instances has been finished."

Cluster/Instanceリストア

第1引数にAWS CLIのプロファイルを指定します。
第2引数には下記項目を「,」で区切って並べたファイルを指定します。項目の間には空白を含めないようにしてください。
空白を含めると確実に動かなくなります。

  • Aurora Instance名
  • リストアさせたいSnapshot名
  • 対象Aurora Clusterに対して、削除前に取得したrds-describe-db-clustersの実行結果(JSON形式)
  • 対象Aurora Instanceに対して、削除前に取得したrds-describe-db-instancesの実行結果(JSON形式)

Aurora Clusterは、Aurora Instance名に「-cluster」サフィックスが付与されたものと想定します。

上記項目のうち、3つ目と4つ目のファイルは、Cluster/Instance削除時に出力されたファイルを指定する想定です。
対象を複数指定する場合は、改行(LFのみ)で羅列したファイルを指定します。
また、空行をスキップする処理は入れていないので、空行は含めないようにしてください。

具体的には、下記のようなファイルとなります。

rds-aurora-instance1,rds-aurora-instance1-final,rds-aurora-instance1_cluster_information.json,rds-aurora-instance1_instance_information.json
rds-aurora-instance2,rds-aurora-instance2-final,rds-aurora-instance2_cluster_information.json,rds-aurora-instance2_instance_information.json

これを実行すると、カレントディレクトリ配下に「result-restore-yyyymmdd」というディレクトリが作成され、ログ等が出力されます。
VPCやDB Subnet Group, Security Groupなど、Aurora Cluster/Instance構築に必要な情報は、
削除前に実行したrds-describe-db-clusters/rds-describe-db-instancesの実行結果から取得します。

#!/bin/sh


if [ $# -lt 2 ] ;then
    echo "usage: $0 <AWS CLI profile> <List file consists an RDS restoration definition.>"
    echo "       The list file is like a CSV file consists 4 parameters those are"
    echo "       \"instance name\", \"snapshot name\", \"Information of the cluster\", \"Information of the instance\"."
    echo '       The 3rd and 4th parameters in the argument file are the result of AWS APIs "describe-db-cluster" and "describe-db-instances"'
    echo '       when the cluster and the instance were still alive.'
    exit 1;
fi


AWS_CLI_PROFILE=$1
RDS_LIST=$2

CURRENT_DATE=`TZ=Asia/Tokyo date +%Y%m%d`
CURRENT_DATETIME=`TZ=Asia/Tokyo date +%Y%m%d-%H%M%S`

OUTPUT_DIR="./result-restore-${CURRENT_DATE}"
LOG_STDOUT_FILE=${OUTPUT_DIR}/rds_delete_instance_${CURRENT_DATETIME}_stdout.log
LOG_STDERR_FILE=${OUTPUT_DIR}/rds_delete_instance_${CURRENT_DATETIME}_stderr.log
TMP_FILE=`mktemp`

alias aws="aws --profile ${AWS_CLI_PROFILE}"

mkdir -p ${OUTPUT_DIR}

#
# Utility Functions
#

function log_both () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDOUT_FILE}
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDERR_FILE}
    return
}

function log_std () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDOUT_FILE}
    return
}

function log_err () {
    echo "[`date +%Y-%m-%dT%H:%M:%S%z`] " $1 >> ${LOG_STDERR_FILE}
    return
}

function _describe_current_information (){
    local _rds_instance=$1
    local _rds_cluster=$2
    local _cmd

    # fetch current information of the RDS instance
    log_both "creating snapshots of \"${_rds_instance}\""
    _cmd="aws rds describe-db-instances --filters Name=db-instance-id,Values=${_rds_instance}" >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd > ${TMP_FILE} 2>> ${LOG_STDERR_FILE}
    cat ${TMP_FILE} >> ${LOG_STDOUT_FILE}
    cp ${TMP_FILE} ${OUTPUT_DIR}/${_rds_instance}_instance_information_${CURRENT_DATETIME}.json

    # fetch current information of the RDS cluster
    _cmd="aws rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd > ${TMP_FILE} 2>> ${LOG_STDERR_FILE}
    cat ${TMP_FILE} >> ${LOG_STDOUT_FILE}
    cp ${TMP_FILE} ${OUTPUT_DIR}/${_rds_instance}_cluster_information_${CURRENT_DATETIME}.json

}

function _create_cluster (){
    local _rds_cluster=$1
    local _snapshot=$2
    local _cluster_json_file=$3

    local _db_subnet_group_name=`cat ${_cluster_json_file} | jq -r '.DBClusters[0].DBSubnetGroup'`
    local _vpc_security_group_ids=`cat ${_cluster_json_file} | jq -r '.DBClusters[0].VpcSecurityGroups[0].VpcSecurityGroupId'`

    local _cmd


    _cmd="aws rds restore-db-cluster-from-snapshot --engine aurora --db-cluster-identifier ${_rds_cluster} --snapshot-identifier ${_snapshot} --db-subnet-group-name ${_db_subnet_group_name} --vpc-security-group-ids ${_vpc_security_group_ids}"
    log_both "invoking an AWS API: \"${_cmd}\"."

    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}


    _cmd="aws rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    # wait until the cluster's status changed into available.
    log_both "waiting to finish creating the RDS cluster \"${_rds_cluster}\"."

    while true
    do
        sleep 15

        _rds_status=`${_cmd} | jq -r '.DBClusters[0].Status'`
        if [ ${_rds_status} == "available" ];then
            break
        fi

    done
}

function _modify_cluster (){
    local _rds_cluster=$1
    local _cluster_json_file=$2

    local _db_cluster_parameter_group_name=`cat ${_cluster_json_file} | jq -r '.DBClusters[0].DBClusterParameterGroup'`

    local _cmd

    _cmd="aws rds modify-db-cluster --db-cluster-identifier ${_rds_cluster} --db-cluster-parameter-group-name ${_db_cluster_parameter_group_name}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

    _cmd="aws rds describe-db-clusters --filters Name=db-cluster-id,Values=${_rds_cluster}"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

}


function _create_instance (){
    local _rds_instance=$1
    local _rds_cluster=$2
    local _instance_json_file=$3

    local _db_instance_class=`cat ${_instance_json_file} | jq -r '.DBInstances[0].DBInstanceClass'`
    local _db_parameter_group_name=`cat ${_instance_json_file} | jq -r '.DBInstances[0].DBParameterGroups[0].DBParameterGroupName'`

    local _cmd

    _cmd="aws rds create-db-instance --engine aurora --db-cluster-identifier ${_rds_cluster} --db-instance-identifier ${_rds_instance} --db-instance-class ${_db_instance_class} --db-parameter-group-name ${_db_parameter_group_name} --no-multi-az --no-publicly-accessible"
    log_both "invoking an AWS API: \"${_cmd}\"."
    $_cmd >> ${LOG_STDOUT_FILE} 2>> ${LOG_STDERR_FILE}

}


#
# Main Function
#

    #
    # Description:
    #   This program restores an Aurora(MySQL) cluster with one instance from a snapshot.
    #   It assumes that the cluster name is a combination with its instance name and a "-cluster" suffix.
    #
    #   The 3rd and 4th parameter in the argument file
    #   are the result of AWS APIs "describe-db-cluster" and "describe-db-instances"
    #   when the cluster and the instance are still alive.
    #
    # How it works in detail:
    #   1. read argument file and separate each line by one comma ",".
    #   2. get information of the cluster and instance to check they don't exist yet.
    #   3. create an Aurora(MySQL) cluster with parameters indicated by the information json file..
    #   4. modify the cluster's parameter group, because it cannot be set at creating.
    #   5. create an RDS instance as a member of the cluster.
    #
    # License:
    #   Copyright (c) 2017 Taichi Miki
    #   Released under the MIT license
    #

    log_both "a temporary file is ${TMP_FILE}"

    for _restore_definition in `grep -v "^#.*$" ${RDS_LIST}`
    do

        _rds_instance=`echo ${_restore_definition} | awk -F, '{print $1};'`
        _rds_cluster=${_rds_instance}-cluster
        _snapshot=`echo ${_restore_definition} | awk -F, '{print $2};'`
        _cluster_info_json=`echo ${_restore_definition} | awk -F, '{print $3};'`
        _instance_info_json=`echo ${_restore_definition} | awk -F, '{print $4};'`

        log_both "Start restoring the instance \"${_rds_instance}\", cluster \"${_rds_cluster}\" from the snapshot \"${_snapshot}\"."

        # fetch current information
        _describe_current_information ${_rds_instance} ${_rds_cluster}

        # create the RDS cluster
        log_both "creating a cluster \"${_rds_cluster}\""
        _create_cluster ${_rds_cluster} ${_snapshot} ${_cluster_info_json}

        # modify the cluster's parameter group
        log_both "modifying the cluster \"${_rds_cluster}\" to set its parameter group appropriately."
        _modify_cluster ${_rds_cluster} ${_cluster_info_json}

        # create an RDS instance
        log_both "creating an instance \"${_rds_instance}\" into the cluster \"${_rds_cluster}\"."
        _create_instance ${_rds_instance} ${_rds_cluster} ${_instance_info_json}


        log_both "the restoring process for the instance \"${_rds_instance}\", cluster \"${_rds_cluster}\" has been finished."
    done

    log_both "delete the temporary file \"${TMP_FILE}\"."
    rm ${TMP_FILE}

    log_both "the restoring process for all instances has been finished."

おわりに

Githubは準備中…

続きを読む