DynamoDBのお手軽バックアップ

アプリの状態だけ保持しているようなあまり大きくないDynamoDBのテーブルを
バックアップするのにDataPipeline使うのもどうかと思うのでツール作ろうかと考えていたら
既にあった。

github.com

使ってみたら結構便利だったので使い方をまとめます。

AmazonLinuxへインストール

このツールAWSpython向けSDKであるbotoを利用しますが、
AmazonLinuxにはpython + botoが初めから入っています。
dynamodumpをgitからチェックアウトするだけで使う準備は完了です。

$ yum -y install git
$ git clone https://github.com/bchew/dynamodump.git
$ cd dynamodump

バックアップ

バックアップ対象のDynamoDBテーブルへの読み込み権限があれば、
以下の3つのオプションのみでバックアップが取得できます。

パラメータ名
-m (Mode) backup
-r (Region) ap-northeast-1
-s (SrcTable) 対象のテーブル名
$ python dynamodump.py -m backup -r ap-northeast-1 -s my-table
INFO:root:Starting backup for my-table..
INFO:root:Dumping table schema for my-table
INFO:root:Dumping table items for my-table
INFO:root:Backup for my-table table completed. Time taken: 0:00:00

$ tree
.
|-- LICENSE
|-- README.md
|-- dump
|   `-- my-table
|       |-- data
|       |   `-- 0001.json
|       `-- schema.json
|-- dynamodump.py
`-- requirements.txt

リストア

リストアはバックアップ時と同じパラメータでも実行可能です。
しかしそのまま実行すると前処理でテーブルを削除してしまうので、
データだけを戻したい場合は--dataOnlyオプションを指定します。

パラメータ名
-m (Mode) restore
-r (Region) ap-northeast-1
-s (SrcTable) 対象のテーブル名
--dataOnly スキーマの作成/削除を行わない
$ python dynamodump.py -m restore -r ap-northeast-1 -s my-table
INFO:root:my-table table is being deleted..
INFO:root:my-table table deleted!
INFO:root:Starting restore for my-table to my-table..
INFO:root:Creating my-table table with temp write capacity of 25
INFO:root:Waiting for my-table table to be created.. [CREATING]
INFO:root:my-table created.
INFO:root:Restoring data for my-table table..
INFO:root:Processing 0001.json of my-table
INFO:root:Updating my-table table read capacity to: 1, write capacity to: 1
INFO:root:Restore for my-table to my-table table completed. Time taken: 0:00:21
$ python dynamodump.py -m restore -r ap-northeast-1 -s my-table --dataOnly
INFO:root:Starting restore for my-table to my-table..
INFO:root:Restoring data for my-table table..
INFO:root:Processing 0001.json of my-table
INFO:root:Restore for my-table to my-table table completed. Time taken: 0:00:01

S3へ定期バックアップするように設定する

cronでも良いと思いますが今回はJenkinsを使用してジョブを定義しました。
Git:RepositoryURLhttps://github.com/bchew/dynamodump.gitを指定して、
シェルの実行で以下のように定義するのがオススメです。
対象のバケット名やテーブルはお好みで変更してください。

BUCKET_NAME=yustam-dynamo-backup
ARCHIVE_NAME=dump_`date +%Y%m%d%H%M`.tgz

python dynamodump.py -m backup -r ap-northeast-1 -s my-table

tar czf $ARCHIVE_NAME ./dump
rm -rf ./dump

aws s3 cp $ARCHIVE_NAME s3://$BUCKET_NAME/dynamo/$ARCHIVE_NAME
rm -f $ARCHIVE_NAME

exit 0;

ビルドトリガ定期的に実行にして好きな時間に動かしたらいいと思います。

AWS CLIを使用してEC2のバックアップを定期的に取得する

AWS CLIjqを使用した実装です。
設定した世代数分保持して古いバックアップから消していきます。

設定値 説明
MAX_IMAGE_NUM バックアップのイメージを保持する世代数
INSTANCE_ID バックアップを取得する対象のEC2インスタンスID
AMI_NAME 取得するAMIの名前。実行毎に重複しないようにする
DESCRIPTION バックアップの種類を識別するために使用します

cronやJenkinsのジョブに設定して定期的に実行します。
再起動を許容する場合は--no-rebootオプションを外します。
EBS付きのインスタンスは追加のオプションが必要なので注意。

#!/bin/sh
MAX_IMAGE_NUM=2
REGION=ap-northeast-1
INSTANCE_ID=i-00000000
AMI_NAME=ec2-backup-$(date +%m%d%H%M)
DESCRIPTION="daily_backup"

IMAGES=$(aws ec2 describe-images\
  --region ap-northeast-1\
  --owners self\
  --filters Name=description,Values=$DESCRIPTION\
  | jq .Images)

# Delete oldest image
if [[ $(echo $IMAGES | jq length) -ge $MAX_IMAGE_NUM ]]; then
  OLDEST_IMAGE_ID=$(echo $IMAGES | jq 'min_by(.CreationDate)' | jq -r .ImageId)
  echo "Delete: $OLDEST_IMAGE_ID"
  aws ec2 deregister-image\
    --region $REGION\
    --image-id $OLDEST_IMAGE_ID
fi

# Create image
echo "Create: $AMI_NAME"
aws ec2 create-image\
  --region ap-northeast-1\
  --instance-id $INSTANCE_ID\
  --name $AMI_NAME\
  --no-reboot\
  --description $DESCRIPTION

exit 0

Packer + Jenkins(on EC2)で自動AMI作成

Packerを使用してAMIの作成を自動化が出来たのでJenkinsでCIしてみようと思います。
Packerの使い方やJenkinsの使い方は割愛して簡単にポイントのみまとめます。

Jenkinsの設定

Gitリポジトリを指定

こちらのようなリポジトリを作成します。 template.jsonがPackerのテンプレートファイルです。

ジョブの設定

シェルの実行で以下のようにシェルを設定します。
packerをインストールしてしまえば毎回ダウンロードする必要はありません。

mkdir packer
cd packer
wget -o - https://dl.bintray.com/mitchellh/packer/packer_0.8.0_linux_amd64.zip
unzip packer_0.8.0_linux_amd64.zip
cd ../

./packer/packer build template.json

rm -rf ./packer
exit 0;

設定は以上です。あとは好きなタイミングでジョブが動くように設定します。

注意する点

Jenkinsからpackerを使用してsshするとsudoが使用できない

packerを使用するとEC2にsshで接続してコマンドを実行しようとしますが、
スクリプト内でsudoを使用するとエラーが出て失敗してしまいます。

sudo: sorry, you must have a tty to run sudo

解決法

そのため、ここではEC2のUserDataにて/etc/sudoersを編集するようにしました。
packerではAMI作成用のEC2にUserDataを指定できるため、そこでsudoの設定を変更します。

#!/bin/bash -ex
sed -i 's/Defaults    requiretty/Defaults    !requiretty/g' /etc/sudoers

その他、sudoで実行したいコマンドがあればここで実行しておくと良いです。
ビルダーのuser_data_fileにファイル名を指定すると起動時にUserDataへ設定されます。

{
  "builders": [{
    "type": "amazon-ebs",
    "region": "ap-northeast-1",
    "source_ami": "ami-cbf90ecb",
    "instance_type": "t2.micro",
    "ssh_username": "ec2-user",
    "ami_name": "packer-ami-{{timestamp}}",
    "user_data_file": "files/user-data.txt"
  }],
  "provisioners": [{
    "type": "shell",
    "scripts": [
      "scripts/create-user-dirs.sh",
      "scripts/install-packages.sh"
    ]
  }]
}

AmazonSNS通知の内容を電話で通知する

Twilioという電話通知を行えるAPIサービスを使用して、

SNS通知 -> Lambda -> Twilio -> 携帯電話

という流れで電話通知を行う仕組みを作ってみます。

使用するサービス

Twilioのアカウントを作成する

  • サインアップからアカウントを作成します。
  • アカウントを作成したらこちらでTwilioの電話番号を取得します。
  • Show API CredentialsをクリックしAccountSidAuthTokenをメモします。

Lambdaアプリの作成

アプリはこちらからダウンロードします。 gitnpmを使用しますので入ってない場合はインストールしてください。

git clone https://github.com/y-matsuki/sns-phone-call.git
cd sns-phone-call/

設定ファイルconfig/default.yamlを編集します。

twilio:
  accountSid: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
  authToken: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
  language: 'ja-jp'
  voice: 'woman'
  toNumber: '+81xxxxxxxxxx'
  fromNumber: '+81xxxxxxxxx'
  • accountSid, authTokenは先ほどメモした値を設定します。
  • fromNumberには先ほど取得したTwilioの電話番号を設定します。
  • toNumberには認証済みの電話番号(アカウント登録時の電話番号)を設定します。

設定ファイルを修正したら以下のコマンドでzipパッケージを作成します。

npm install
npm run build

SnsPhoneCall.zipが出来たら成功です。

Lambdaアプリのデプロイ

AWS Management ConsoleにログインしAWS Lambdaのページへ移動します。 現在(2015/6)はまだ東京リージョンに作成できませんが夏には来るそうです。

項目名 設定値
Name sns-phone-call(任意)
Runtime NodeJS
Code entry type Upload a .ZIP file
Handler SnsPhoneCall.handler
Role Basic execution role

Uploadボタンから先ほど作成したSnsPhoneCall.zipをアップロードして、 Create Lambda Functionボタンをクリックします。

SNS通知の作成

Amazon SNSのページへ移動してCreate new topicからトピックを作成します。 作成したらActions > Suscribe to topicから配信先を登録します。 ProtocolAWS Lambdaを選択すると先ほど作成したLambdaアプリが表示されます。

Create Subscriptionをクリックしたら準備完了です。

動作確認

先ほど作成したSNSトピックを選択しPublish to toipcをクリックします。 Messageに喋らせたい内容を記述してPublish messageをクリックします。

設定ファイルのtoNumberに設定した電話番号宛に電話が届いたら成功です。

まとめ

Lambdaを使用することでEC2なしでTwilioAPIをコールする仕組みができました。 Lambdaアプリの処理はnode.jsで記述しているためSNS通知の内容を読んで キーワードでフィルタしたり宛先を振り分けたりカスタマイズすると面白いかなと思います。

他のアカウントへS3バケットへのアクセスを許可する

クロスアカウントと外部ID

このあたりに書いてあることを実践してみます。
外部 ID について - AWS Security Token Service
一時的なセキュリティ認証情報を使用して AWS リソースをリクエストする - AWS Security Token Service

準備

アクセス元のアカウントIDを調べます。
ここではアクセス元を999999999999としS3バケットがある方を111111111111とします。

S3バケットを持つアカウントの方にIAMにロールを作成

  • ロール名を入力

ここではcross-accountとしました。

  • アクセス元のアカウントIDを入力

f:id:yustam:20150207133008p:plain

  • ポリシーを選択

f:id:yustam:20150207133022p:plain

ここではSelectPolicyTemplateからAmazon S3 Read Only Accessを選択し、
そのまま進んでロールを作成しました。

アクセス元のアカウントからアクセスしてみる

  • アクセス元のアカウントにEC2を立ててそこからアクセスしてみます。

sshログインしたら試しにS3のバケット一覧を取得してみます。

$ ssh ec2-user@ec2-54-65-xxx-xxx.ap-northeast-1.compute.amazonaws.com -i key.pem
[ec2-user@ip-172-31-24-163 ~]$ aws s3 ls
2014-12-23 07:25:37 cf-templates-xxxxxxxxxxxxx-ap-northeast-1
2015-01-11 03:32:18 elasticbeanstalk-ap-northeast-1-999999999999
2015-01-05 14:42:54 yustam-artifact-repository
2014-11-07 15:21:07 yustam-docker-registry
2014-12-11 09:52:14 yustam-lambda-sample
2014-11-14 12:26:45 yustam-private

ロールを付与しておくとここではアクセス元アカウントのバケットが確認できます。

  • STSのAssumeRoleでセッションを作成

ここではaws-cliを使用してクロスアカウントアクセスを利用します。
sts assume-roleを使用してS3バケットアクセス用のセッションを作成します。

[ec2-user@ip-172-31-24-163 ~]$ aws sts assume-role --role-arn arn:aws:iam::111111111111:role/cross-account --role-session-name hoge
{
    "AssumedRoleUser": {
        "AssumedRoleId": "AROAJWSTZ4KKB2EXAMPLE:hoge",
        "Arn": "arn:aws:sts::111111111111:assumed-role/cross-account/hoge"
    },
    "Credentials": {
        "SecretAccessKey": "EmlBUyGcOKPI/Sdsf4RCE56sHnbo6XfJkEXAMPLE",
        "SessionToken": "AQoDYXdzECUagAKpFP2OknLKXsPLl/801n...(省略)",
        "Expiration": "2015-02-07T05:19:53Z",
        "AccessKeyId": "ASIAJ2GNOUN4ZEXAMPLE"
    }
}
  • 作成したセッションを利用してS3バケットにアクセスしてみる

先ほどのレスポンスに記載されたアクセスキー/シークレットキー/トークンを使用します。

[ec2-user@ip-172-31-24-163 ~]$ export AWS_ACCESS_KEY_ID=ASIAJ2GNOUN4ZEXAMPLE
[ec2-user@ip-172-31-24-163 ~]$ export AWS_SECRET_ACCESS_KEY=EmlBUyGcOKPI/Sdsf4RCE56sHnbo6XfJkEXAMPLE
[ec2-user@ip-172-31-24-163 ~]$ export AWS_SECURITY_TOKEN=AQoDYXdzECUagAKpFP2OknLKXsPLl/801n...(省略)
[ec2-user@ip-172-31-24-163 ~]$ aws s3 ls
2013-02-15 08:04:27 private.yustam.jp
2013-01-21 05:53:07 www.robotjohn.jp
2013-07-10 01:08:47 www.yustam.jp
2013-10-27 02:28:07 yustam.convert

S3バケットの一覧を取得するとアクセス先アカウントのバケット一覧が取得できます。


Javaから利用する場合は以下のようにAssumeRoleしてcom.amazonaws.auth.BasicSessionCredentialsを作成すればいいみたいです。

package jp.yustam.s3.ca;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicSessionCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.Bucket;
import com.amazonaws.services.securitytoken.AWSSecurityTokenService;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient;
import com.amazonaws.services.securitytoken.model.AssumeRoleRequest;
import com.amazonaws.services.securitytoken.model.AssumeRoleResult;
import com.amazonaws.services.securitytoken.model.Credentials;
import org.junit.Test;

public class CrossAccountTest {

  static final String ROLE_ARN = "arn:aws:iam::111111111111:role/cross-account";
  static final String ROLE_SESSION_NAME = "hoge";

  @Test
  public void testAccessS3BucketOnAnotherAccount() {
    AWSSecurityTokenService sts = new AWSSecurityTokenServiceClient();
    AssumeRoleResult result = sts.assumeRole(new AssumeRoleRequest()
        .withRoleArn(ROLE_ARN)
        .withRoleSessionName(ROLE_SESSION_NAME));

    Credentials credentials = result.getCredentials();
    AWSCredentials sessionCredentials
        = new BasicSessionCredentials(credentials.getAccessKeyId(),
        credentials.getSecretAccessKey(),
        credentials.getSessionToken());
    AmazonS3 s3 = new AmazonS3Client(sessionCredentials);
    for (Bucket bucket : s3.listBuckets()) {
      System.out.println(bucket);
    }
  }

}

Javaからもっと簡単に使う(3/5更新)

Credentialsを毎回生成するのもイケてないし有効期限があるから管理めんどうだなと
思ってたらSTSAssumeRoleSessionCredentialsProviderという便利なものが既に用意されていた。

AWSCredentialsProvider cp
    = new STSAssumeRoleSessionCredentialsProvider(ROLE_ARN, ROLE_SESSION_NAME);
AmazonS3 s3 = new AmazonS3Client(cp);

re:Invent 2014スライドのメモ

re:Invent 2014のスライドが少しずつ登録されているようなので気になるものをメモ。

サービス別

Amazon Key Management Service

(SEC301) Encryption and Key Management in AWS | AWS re:Invent 2014

(SEC406) NEW LAUNCH: Building Secure Applications with AWS Key Manage…

Amazon Aurora

(SDD415) NEW LAUNCH: Amazon Aurora: Amazon’s New Relational Database …

CloudFormation

(APP306) Using AWS CloudFormation for Deployment and Management at Sc…

EC2

(SDD406) Amazon EC2 Instances Deep Dive | AWS re:Invent 2014

IAM

(SEC305) IAM Best Practices | AWS re:Invent 2014

Netflix

(DEV309) From Asgard to Zuul: How Netflix’s Proven Open Source Tools …

(BDT403) Netflix's Next Generation Big Data Platform | AWS re:Invent …

Hybrid

(ARC203) Expanding Your Data Center with Hybrid Infrastructure | AWS …

(ENT401) Hybrid Infrastructure Integration | AWS re:Invent 2014

その他

(DEV302) Tips, Tricks, and Best Practices for the AWS SDK for Java | …

(BAC304) Deploying a Disaster Recovery Site on AWS: Minimal Cost with…

(ARC306) IoT: Small Things and the Cloud | AWS re:Invent 2014

(ARC307) Infrastructure as Code | AWS re:Invent 2014

(SDD409) Amazon RDS for PostgreSQL Deep Dive | AWS re:Invent 2014

整理中...

(SDD408) Amazon Route 53 Deep Dive: Delivering Resiliency, Minimizing…

(PFC403) Maximizing Amazon S3 Performance | AWS re:Invent 2014

(PFC402) Bigger, Faster: Performance Tips for High Speed and High Vol…

(PFC306) Performance Tuning Amazon EC2 Instances | AWS re:Invent 2014

(PFC303) Milliseconds Matter: Design, Deploy, and Operate Your Applic…

(MED303) Secure Media Streaming and Delivery | AWS re:Invent 2014

(MBL202) NEW LAUNCH: Getting Started with AWS Lambda | AWS re:Invent …

(DEV308) Automating Your Software Delivery Pipeline | AWS re:Invent 2…

(ARC318) Continuous Delivery at a Rate of 500 Deployments a Day! | AW…

(ARC402) Deployment Automation: From Developers' Keyboards to End Use…

(ARC311) Extreme Availability for Mission-Critical Applications | AWS…

(ARC304) Designing for SaaS: Next-Generation Software Delivery Models…

(APP310) Scheduling Using Apache Mesos in the Cloud | AWS re:Invent 2…

(ARC203) Expanding Your Data Center with Hybrid Infrastructure | AWS …

(APP313) NEW LAUNCH: Amazon EC2 Container Service in Action | AWS re:…

(APP303) Lightning Fast Deploys with Docker Containers and AWS | AWS …

(APP308) Chef on AWS: Deep Dive | AWS re:Invent 2014

(SPOT303) Building Mission Critical Database Applications: A Conversa…

(WEB301) Operational Web Log Analysis | AWS re:Invent 2014

Enterprise Security Considerations

[(ARC311) Extreme Availability for Mission-Critical Applications | AWS…

(APP204) NEW LAUNCH: Introduction to AWS Service Catalog | AWS re:Inv…

(APP304) AWS CloudFormation Best Practices | AWS re:Invent 2014

(APP307) Leverage the Cloud with a Blue/Green Deployment Architecture…

(ARC312) Processing Money in the Cloud | AWS re:Invent 2014

(ARC202) Real-World Real-Time Analytics | AWS re:Invent 2014

(ENT209) Netflix Cloud Migration, DevOps and Distributed Systems | AW…

(SDD407) Amazon DynamoDB: Data Modeling and Scaling Best Practices | …

(SDD405) Amazon Kinesis Deep Dive | AWS re:Invent 2014

(SDD401) Amazon Elastic MapReduce Deep Dive and Best Practices | AWS …

(SDD424) Simplifying Scalable Distributed Applications Using DynamoDB…

(SDD416) Amazon EBS Deep Dive | AWS re:Invent 2014

(SDD413) Amazon S3 Deep Dive and Best Practices | AWS re:Invent 2014

(SPOT305) Event-Driven Computing on Change Logs in AWS | AWS re:Inven…

(SDD418) Amazon CloudWatch Deep Dive | AWS re:Invent 2014

(SDD423) Elastic Load Balancing Deep Dive and Best Practices | AWS re…

[http://www.slideshare.net/AmazonWebServices/sec405-enterprise-cloud-security-via-devsecops-aws-reinvent-2014

CloudFormationのテンプレートをVelocityで生成する

CloudFormationのテンプレートは変数を定義したり他のコンポーネントを参照させたり結構いろいろなことが出来ますが汎用的に書こうとすると複雑になってしまい慣れてない人だと読みづらいテンプレートになってしまうため、 テンプレートのテンプレートを作成してApache VelocityでAMIやスナップショットIDなど 注入する方がメンテナンスも楽かなと思い試してみました。
要するにMappingsとParametersを使用せずにVelocityのテンプレートを書いてJavaから変数を渡します。
Velocityに限らずのテンプレートエンジンだとコメントも書き放題なのでメンテナンスを考えると何かしらテンプレートエンジンを使用する方がいい気がします。 他にCloudFormationのテンプレートに向いているテンプレートエンジンがあれば教えて欲しいです。

テンプレート

セキュリティグループにEC2インスタンス×1+EBSボリューム×1(ファイル用)+EBSボリューム×N(DB用)みたいな簡単な構成です。

velocity-cfn-template-sample.template.vm

Javaコード

VelocityContext context = new VelocityContext();
context.put("description", new String("this is my sample template."));
context.put("availability_zone", new String("ap-notrheast-1"));
context.put("instance", new Instance("ami-00000000", "SampleInstance", "t2.micro"));
context.put("volume", new Snapshot(null, "File", "standard", "/dev/sdf", "/mnt/file", 10));

List<Ingress> ingress = new ArrayList<>();
ingress.add(new Ingress("tcp", 22, 22, "XXX.XXX.XXX.XXX"));
ingress.add(new Ingress("tcp", 5432, 5432, "XXX.XXX.XXX.XXX"));
context.put("security_group", new SecurityGroup("SampleSG", "this is a sample.", ingress));

List<Snapshot> snapshots = new ArrayList<>();
snapshots.add(new Snapshot("snap-00000001", "DB01", "standard", "/dev/sdh", "/var/lib/pgsql9/data1", 10));
snapshots.add(new Snapshot("snap-00000002", "DB02", "standard", "/dev/sdi", "/var/lib/pgsql9/data2", 10));
snapshots.add(new Snapshot("snap-00000003", "DB03", "standard", "/dev/sdj", "/var/lib/pgsql9/data3", 10));
context.put("snapshots", snapshots);

Velocity.addProperty("input.encoding", "UTF-8");
Velocity.addProperty("output.encoding", "UTF-8");
Velocity.addProperty("resource.loader", "class");
Velocity.addProperty("class.resource.loader.class",
    "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
Velocity.init();

Template template = Velocity.getTemplate(name);

StringWriter sw = new StringWriter();
template.merge(context, sw);
System.out.println(sw.toString());

出力

velocity-cfn-template-sample.template