[AWS] Terraform で EFS 作って、EC2 起動時にマウントさせておく

Terraform を使うと、EFS を作成して EC2 にマウントさせておくなんてことが簡単にできます。
Autoscaling 環境で Web ドキュメントルートを共有したい時とかに便利なんで、みんな使えばいいと思うよ。
なお、この記事の想定読者は AWS はダッシュボードからポチポチしてインスタンス立てたりしてるけど、そろそろインフラをコードで管理したいな。Terraform とか便利そうだねー使ってみたいねーって人です。
てわけで、単純に EC2 立ち上げても面白くないので EFS をマウントさせてみました。

そもそも、Terraform ってなんだ?って人は、以下のページとか参考になると思います。
Terraform簡易チュートリアル on AWS

実際の設定

Terraform は、特定のディレクトリ下にある拡張子が .tf なファイルを全部読み込んでいい感じにリソースを起動してくれます。なので、機能別に .tf 作成していってみましょう。

メイン設定

まず、メインの設定を作成。
プロバイダーとか、設定ファイル内で使用する変数とか設定していってみましょうか。

main.tf
# 今回のプロジェクト名
variable "project" {}
variable "domain" {}

# AWS リソースを起動するリージョンとかの情報
variable "region" { default = "us-west-2" }
variable "azs" {
    default {
        "a" = "us-west-2a"
        "b" = "us-west-2b"
        "c" = "us-west-2c"
    }
}

# AMI ID (Amazon Linux)
variable "ami" { 
    default {
        "us-west-2" = "ami-8ca83fec"
    }
}

# EC2 接続用の SSH 鍵の公開鍵
variable "ssh_public_key" {}

provider "aws" {
    region = "${var.region}"
}

variable で設定した値は tf ファイル内で ${var.region} のようにして参照可能です。
また、terraform の各種コマンドを実行する際に以下のようにパラメータとして変数を渡して上書きすることもできます。

$ terraform plan \
  -var 'project=example' \
  -var 'domain=example.com'

同じディレクトリ内に terraform.tfvars というファイルがあれば、それを読み込んで値が上書きされたりします。この辺の詳細は以下を参照してください。
Input Variables – Terraform by HashiCorp

provider "aws" は、aws を使いますよって宣言です。
以下のように アクセスキーを書いておくこともできますが、それやるとうっかり github とかに公開した時とかに切ない目にあうのでやめたほうが吉でしょう。

provider "aws" {
    access_key = "__ACCESS_KEY__"
    secret_key = "__SECRET_KEY__"
    region = "us-west-2"
}

環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY を読み込んでいい感じでやってくれるので、僕は direnv 使って作業ディレクトリ内で環境変数を変更することで対応してます。
(もちろん、この場合でも .gitignore.envrc を含めておいて間違って公開しないようにしないと切ない目にあうので注意)

VPC の作成

こんな感じの .tf ファイルで VPC と subnet が作成できます。

vpc.tf
## VPC
resource "aws_vpc" "app" {
    cidr_block           = "172.31.0.0/16"
    enable_dns_hostnames = true
    enable_dns_support   = true
    instance_tenancy     = "default"

    tags {
        "Name" = "${var.project}"
    }
}

## Subnet
resource "aws_subnet" "a" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.0.0/20"
    availability_zone       = "${lookup(var.azs,"a")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-a"
    }
}

resource "aws_subnet" "b" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.16.0/20"
    availability_zone       = "${lookup(var.azs,"b")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-b"
    }
}

resource "aws_subnet" "c" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.32.0/20"
    availability_zone       = "${lookup(var.azs,"c")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-c"
    }
}

resource "aws_subnet" の中に ${aws_vpc.app.id} ってのが出てきましたね。
Terraform の中では、管理下にあるリソースの情報を他のリソースの設定でも参照することが可能です。
各リソースで使用できる値が異なってくるので、その辺は公式ドキュメント読みましょう。
例えば aws_vpc で使用できる値は aws_vpc を参照すればわかります。

また、${lookup(var.azs,"a")} ってのも出てきましたね。
これは Terraform の組み込み関数です、lookup は配列の中からキーをもとに値を探す関数です。
詳しくは Built-in Functions を読んでください。

ついでに Internet Gateway と Route Table も設定しておきましょう。

route-table.tf
## Internet Gateway
resource "aws_internet_gateway" "igw" {
    vpc_id = "${aws_vpc.app.id}"
}

## Route Table
resource "aws_route_table" "rtb" {
    vpc_id     = "${aws_vpc.app.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.igw.id}"
    }
}

resource "aws_route_table_association" "route_a" {
    subnet_id = "${aws_subnet.a.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_b" {
    subnet_id = "${aws_subnet.b.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_c" {
    subnet_id = "${aws_subnet.c.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

IAM ロールの作成

次に EC2 に割り当てるための IAM ロールを作ってみましょう。
ポリシーは、AWS が用意している AmazonEC2RoleforDataPipelineRole と、EC2 から CloudwatchLogs にログを送信するためのカスタムポリシーを作ってアタッチしてみます。

iam-role.tf
## For EC2 instance Role
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

## AmazonEC2RoleforDataPipelineRole
resource "aws_iam_role_policy_attachment" "data-pipeline" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforDataPipelineRole"
}

## PutCloudwatchLogs
resource "aws_iam_policy" "put-cloudwatch-logs" {
    name        = "AmazonEC2PutCloudwatchLogs"
    description = ""
    policy      = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "put-cloudwatch-logs" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "${aws_iam_policy.put-cloudwatch-logs.arn}"
}

aws_iam_roleassume_role_policy のところと、aws_iam_policypolicy のところでヒアドキュメントが出てきましたね。
こんな風に複数行にわたるインラインポリシーはヒアドキュメントで記述することが可能です。
また、以下のように別ファイルにしておいて読み込ませることも可能です。
管理しやすい方でやってください。

iam-role.tf
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = "${file("data/instance_role_assume_policy.json")}"
}
data/instance_role_assume_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

セキュリティグループの作成

EC2 から EFS へのアクセスは 2049 番ポートを介して行われるので、EFS が所属するセキュリティグループに穴を開けないといけません。
EC2 は 80, 443, 22 を解放してみます。

security-group.tf
## For EC2
resource "aws_security_group" "ec2" {
    name        = "${var.project}-EC2"
    description = "for ${var.project} EC2"
    vpc_id      = "${aws_vpc.app.id}"

    ingress = [
        {
            from_port       = 80
            to_port         = 80
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 443
            to_port         = 443
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 22
            to_port         = 22
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        }
    ]

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

## For EFS
resource "aws_security_group" "efs" {
    name        = "${var.project}-EFS"
    description = "for ${var.project} EFS"
    vpc_id      = "${aws_vpc.app.id}"

    ingress {
        from_port       = 2049
        to_port         = 2049
        protocol        = "tcp"
        security_groups = ["${aws_security_group.ec2.id}"]
    }

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

EFS の作成

こんな感じで EFS が作成できます。
各サブネットごとにマウントターゲットを作成して、そいつをセキュリティグループに所属させる形ですね。

efs.tf
resource "aws_efs_file_system" "app" {
  tags {
        "Name" = "${var.domain}"
  }
}

resource "aws_efs_mount_target" "app-a" {
  file_system_id  = "${aws_efs_file_system.app.id}"
  subnet_id       = "${aws_subnet.a.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-b" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.b.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-c" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.c.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

EC2 の作成

さて、いよいよ EC2 です。
ここでは、user-data を使って、初回ローンチ時に EFS をマウントさせてしまいます。
さらにマウントした EFS 内に html/ ってディレクトリを作成して、そいつを /var/www/html にシンボリックリンクしてみましょうか。
と言っても、こんな感じで大丈夫です。

ec2.tf
## IAM Instance Profile
resource "aws_iam_instance_profile" "instance_role" {
    name = "instance_role"
    role = "${aws_iam_role.instance_role.name}"
}

## SSH Key
resource "aws_key_pair" "deployer" {
  key_name   = "${var.project}"
  public_key = "${var.ssh_public_key}"
}

## EC2
resource "aws_instance" "app" {
    ami                         = "${lookup(var.ami,var.region)}"
    availability_zone           = "${aws_subnet.a.availability_zone}"
    ebs_optimized               = false
    instance_type               = "t2.micro"
    monitoring                  = true
    key_name                    = "${aws_key_pair.deployer.key_name}"
    subnet_id                   = "${aws_subnet.a.id}"
    vpc_security_group_ids      = ["${aws_security_group.ec2.id}"]
    associate_public_ip_address = true
    source_dest_check           = true
    iam_instance_profile        = "${aws_iam_instance_profile.instance_role.id}"
    disable_api_termination     = false

    user_data                   = <<USERDATA
#!/bin/bash
az="${aws_subnet.a.availability_zone}"
efs_region="${var.region}"
efs_id="${aws_efs_file_system.app.id}"
efs_mount_target="${aws_efs_mount_target.app-a.dns_name}:/"
efs_mount_point="/mnt/efs/$${efs_id}/$${az}"
web_doc_root="/var/www/html"

# EFS Mount
/usr/bin/yum -y install nfs-utils || /usr/bin/yum -y update nfs-utils
if [ ! -d $${efs_mount_point} ]; then
  mkdir -p $${efs_mount_point}
fi
cp -pi /etc/fstab /etc/fstab.$(date "+%Y%m%d")
echo "$${efs_mount_target}    $${efs_mount_point}   nfs4    defaults" | tee -a /etc/fstab
mount $${efs_mount_point}

# create Web document root
if [ -d $${web_doc_root} ]; then
  rm -rf $${web_doc_root}
fi
if [ ! -d $${efs_mount_point}/html ]; then
  mkdir $${efs_mount_point}/html
  chown ec2-user:ec2-user $${efs_mount_point}/html
fi
ln -s $${efs_mount_point}/html $${web_doc_root}
chown -h ec2-user:ec2-user $${web_doc_root}
USERDATA

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name"          = "${var.domain}"
    }
}

user_data は長めのシェルスクリプトなので、可読性が悪いから ${file("data/user_data.sh")} とかってやって別ファイルで管理したいですよね。
でも待ってください、ヒアドキュメントでやってるのは理由があるのです。

ヒアドキュメントで書くと、user_data 用のシェルスクリプトの中で Terraform の変数が使えます。
マウントするには EFS の ID とか、マウントターゲットの dns_name とか必要になってきますが、それを作成前に知らなくてもこのように書いておけるのです。便利ですね。
その代わり、user_data 用のシェルスクリプト内でローカルな環境変数を使いたい場合は $${efs_mount_point} のように書いてあげてくださいね。

ざっと、こんな感じです。
慣れちゃえば、tf ファイルを使い回しできるので便利ですよ。
また、すでに作成済みの AWS リソースを Terraform 管理下に置きたい場合は

$ terraform import aws_instance.app ${instance_id}

のようにして管理下に置くことができます。
管理されているリソースは terraform.tfstate というファイルに書き込まれます。
さらに別プロダクトになるのですが Terraforming と言うツールを使用すると、既存の AWS リソースから Terraform 用の tf ファイルを作成したり、terraform.tfstate を作成したりもできるので便利です。
Terraforming については、Terraforming で既存のインフラを Terraform 管理下におく を参考にしてください。

実際にリソース作成してみる

tf ファイル書いたら

$ terraform plan

で、設定ファイルに誤りがないか?既存のリソースへの影響はどの程度あるのかが確認できます。
実際に反映させたい時は

$ terraform apply

で、おっけ。

では、良い Terraform を!

続きを読む

全力AWSでMastodonサーバー立てました chitose.moe

https://chitose.moe/

スポンサーが1社付いてるのですぐに消えたりはしないはず。
メール送信数制限の都合で1日のユーザー登録数には上限がある。
メールが届かない時は暫く待つか24時間後に再送信。
とはいえ5万なので上限になることはないか。

環境

  • AWS
  • EC2 Container Service
  • ecs-cli
  • RDS(PostgreSQL)
  • ElastiCache(Redis)
  • S3
  • CloudFront
  • ALB
  • SES

やれるだけやって分散した。
いくらでもサーバー増やせるけどたぶんそこまで必要にはならない。

最後SESのsandbox解除待ちで時間かかった…。

途中段階の役に立たないメモ

ローカルで動かすだけならVagrantのほうが早い。
最初から管理者アカウントも作られる。

docker-composeはproduction用。

git clone https://github.com/tootsuite/mastodon
cd mastodon
sudo curl -o /usr/local/bin/ecs-cli https://s3.amazonaws.com/amazon-ecs-cli/ecs-cli-darwin-amd64-latest
sudo chmod +x /usr/local/bin/ecs-cli
ecs-cli help
ecs-cli configure -p default --cluster mastodon
ecs-cli up ...
aws ecr get-login
docker login ...
aws ecr create-repository --repository-name mastodon
docker build -t mastodon .
docker tag mastodon:latest ...
docker push ...

docker-compose.ymlを編集
– buildの代わりにimage: mastodon
– DB永続化するためコメントを消す

ecs-cli compose service up

ここで上手く動かなくなった。
Elastic Beanstalkでやろうとしたけどこっちもだめ。
いろいろやってるうちにオリジナルのdocker-compose.ymlが事前に用意したDocker image使うようになってた。
ECS使う方法に戻る…。

assetsを削除して再生成してS3にアップ。
precompileを繰り返すとファイルが増える…?

rm -rf ./public/assets
docker-compose run --rm web rails assets:precompile

aws s3 cp ./public/assets s3://{S3バケット}/assets --recursive --acl public-read --cache-control "max-age=604800"

S3に置くと/public/assets以下がないのでprecompile済みファイルが使われない。
sprockets-manifestが必要。
S3に置くのはやめた。
image: gargron/mastodonは使わず自分でビルドする方法に戻る…。

.dockerignoreを変更してimageにassetsも含まれるように。

#public/assets

docker-compose.ymlをコピーしてaws.ymlを作りこっちを書き換えて行く。
docker-compose.ymlはローカル用に元のまま。
あ、当然gitのブランチは分けてる。

AWSのECSでは

  • buildが使えない
  • volumesで相対パスが使えない

という仕様なのでそれに合わせる。
volumesはよくわからないのでほぼ使わないように…。

ここからも色々苦労したけど細かすぎてもう忘れたのでメモ程度。
AWSのサービスで使えるものは使う。
db,redisはもちろん分離。
S3も当たり前に使う。
nginxは不要だった。nginx使おうとして無駄に混乱…。
ポートマッピングはELB。
httpsへのリダイレクトはCF。

LOCAL_HTTPS=falseでも問題なくhttpsで動くけどメール内のURLだけhttpになる。
リダイレクトされるので妥協。

ecs-cli compose serviceで--load-balancer-name付きで起動だけどうしてもできなかったので諦めた。

/api/v1/streamingはサブドメインにした。
STREAMING_API_BASE_URLで指定すればそれが使われる。
LOCAL_HTTPS=falseだとwebsocketでエラー出てたので無理矢理な対応。

S3_BUCKETS3_HOSTNAMEはS3のドメインそのまま使う用で
自分のサブドメイン使う場合はS3_CLOUDFRONT_HOSTも設定する。
CF使ってるかに関係なく静的ホスティングなら。

nginx使わなくても動いてるけど何か問題あれば後で修正していく。

docker-compose.ymlあるから簡単に動かせるかと思ったけどそんなことはなかった。
EC2上でdocker-compose upすれば簡単だけどそれじゃAWSの意味がない。

ALBの使用

その後調べて分かったので追記。
--load-balancer-nameはClassic Load Balancer用なので違う。
ALBは--target-group-arnを使う。
ただしサービスごとに一つしか設定できないのでサービスを複数作るしかない?

ecs-cli compose -f aws.yml --project-name mastodon-web service create --target-group-arn {web用のターゲットグループ} --container-name web --container-port 3000 --role ecsServiceRole

ecs-cli compose -f aws.yml --project-name mastodon-api service create --target-group-arn {streaming用のターゲットグループ} --container-name streaming --container-port 4000 --role ecsServiceRole

--project-nameの指定も必要になった。

ecs-cli compose -f aws.yml --project-name mastodon-web service up

AutoScalingでEC2インスタンス数を増減。
一つのEC2内で複数のタスクが動くし、タスクもAutoScalingできるようになった。

サービスを分けるならaws.ymlも分割したほうがメモリの無駄もない。

最終版

ごちゃごちゃしたのでまとめ。

aws-web.yml

mem_limitは分からないので仮。
portsの0が動的ポートのために必要。
もしくは0:部分なしでいいかも。こっちは未検証。

version: '2'
services:

  web:
    restart: always
    image: {自分のimage}
    env_file: .env.production
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    mem_limit: 536870912
    ports:
      - "0:3000"

aws-api.yml

version: '2'
services:
  streaming:
    restart: always
    image: {自分のimage}
    env_file: .env.production
    command: npm run start
    mem_limit: 268435456
    ports:
      - "0:4000"

  sidekiq:
    restart: always
    image: {自分のimage}
    env_file: .env.production
    command: bundle exec sidekiq -q default -q mailers -q pull -q push
    mem_limit: 268435456

ALB

ターゲットグループ

  • port3000のweb用
  • port4000のstreaming用

を作る。

リスナーHTTPS 443
ヘルスチェックのポートをトラフィックポートにする。

  • HostがAPI用のサブドメインならstreaming
  • Pathが/ならweb

というルールを設定する。

CloudFrontはこのALBをOriginにする。
/assets以下はキャッシュ強く。他は短め。

Route53はCloudFrontを指定。

今後のためのアップデート手順

masterブランチを最新にしてからマージ。

DB_HOSTはRDSを指定してるのでdb:migrateはローカルから直接行う。
これはどうなんだろうとは思うけど他の方法が分からなかった。

docker-compose build
docker-compose run --rm web rails db:migrate
docker-compose run --rm web rails assets:precompile

docker imageの更新はECRの手順通りに。

aws ecr get-login --region ap-northeast-1 | bash

docker build ...
docker tag ...
docker push ...

ecs-cli使ったほうが少し短いかも。

docker build ...
docker tag ...

ecs-cli push ...

後はup。upはymlが変更されてないと以前のタスクのままなのでimageだけ更新した場合は更新されない。
CIで動かしてdocker tagを設定してymlの書き換えまで自動化かな。

ecs-cli compose -f aws-web.yml --project-name mastodon-web service up
ecs-cli compose -f aws-api.yml --project-name mastodon-api service up

動きさえすれば運用段階では楽になる。

続きを読む

jqでちょっぴり複雑な検索をする

 jqコマンドで配列の要素かつ複雑な構造をもつ要素(ここではaws ec2 describe-instancesの結果)に対して検索を行ってみました。

はじめに

 株式会社アイリッジのCommon Lisp大好きサーバサイドエンジニア、tanaka.lispです。

 AWS EC2を利用していると、特定のインスタンスの情報(インスタンスIDやインスタンスタイプ、などなど…)を知りたくなることがあります。ただ、そのためだけに管理コンソールをぽちぽちして取得していると、なんだか負けた気がするし(これ大事)、シェルスクリプトなどで結果を利用するときに不便です。

 awscliaws ec2 describe-instancesで取得した結果をjqに食わせ、インスタンスにつけている名前からイイかんじに検索・整形したいところですが、awscliはけっこう複雑な返り値を返してきますよね…:

{
  "Reservations": [
    "Instances": [
      {
        "InstanceId": "i-commonlisp",
        "Tags": [
          {
            "Key": "Name",
            "Value": "app.commonlisp"
          },
          {
            "Key": "aws.cloudformation:stack-name",
            "Value": "stack.commonlisp"
          }
        ],
        ...
      },
      ...
    ],
    ...
  ]
}

 そこで、名前(TagsKeyNameの値)によるインスタンスの検索方法を試行錯誤しました。

よういするもの

  • jq: jq-1.5-1-a5b5cbe
  • awscli: aws-cli/1.11.58 Python/3.6.0 Linux/4.8.0-41-generic botocore/1.5.21

jqでの検索

 jqでは入力のJSONに対して、以下のようなことができます:

  • 入力JSONの整形 ({"id": .InstanceId, "tags": .Tags})
  • 入力JSONからのデータ抽出 (select)

 今回はこれらを組み合わせて、aws ec2 describe-instancesの結果から探しているインスタンスのインスタンスID(InstanceId)を取得してみます。

入力の整形

 たとえばこんなJSONオブジェクトがあって、

[
  {
    "name": "Lisp",
    "developer": "John McCarthy"
  },
  {
    "name": "Common Lisp",
    "developer": "ANSI X3J13 committee",
    "spec": "ANSI INCITS 226-1994 (R2004)"
  },
  {
    "name": "Arc",
    "developer": "Paul Graham",
    "books": [
      "On Lisp",
      "Hackers and Painters"
    ]
  },
  {
    "name": "Clojure",
    "developer": "Rich Hicky"
  }
]

言語名(lang)と開発者名(developer)だけがほしいとき、入力をprintf的に整形できます。

$ echo '...上のJSON...' \
   | jq '.[] | {"lang": .name, "developer": .developer}'
{
  "lang": "Lisp",
  "developer": "John McCarthy"
}
{
  "lang": "Common Lisp",
  "developer": "ANSI X3J13 committee"
}
{
  "lang": "Arc",
  "developer": "Paul Graham"
}
{
  "lang": "Clojure",
  "developer": "Rich Hicky"
}

要素の抽出のようにも思えますが、別にもっと抽出っぽいオペレータがあるので、こちらは整形と呼びました。

入力からのデータ抽出

 条件を満たすもののみをpass throughするというオペレータもあって、それがselectです。こちらを抽出とここでは呼んでいます。

 manにあることがすべてなのですが、

   select(boolean_expression)
       The function select(foo) produces its input unchanged if foo returns true for that input, and produces no output otherwise.

       It´s useful for filtering lists: [1,2,3] | map(select(. >= 2)) will give you [2,3].

正規表現を組み合わせたりできるので強力です;

$ echo '...上のJSON...' \
  | jq '.[] | .name |select( .| test("Lisp"))'
"Lisp"
"Common Lisp"

実際に抽出

Exactlyな名前でインスタンスの検索

 では実際に検索クエリをつくってみます。

 まずはインスタンスリストを得て、

$ aws ec2 describe-instances | jq '.Reservations[].Instances[]'
{
  "ImageId": "ami-commonlisp",
  "State": {
    "Code": 16,
    "Name": "running"
  }
}
{
...  # ずらずら出るので省略

そこにTagsKeyNameValueがExactlyapp.commonlispの要素だけ真になる条件を加えます(ルシのファルシがパージでコクーン感ある)。

$ aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select(.Tags[].Key == "Name" and .Tags[].Value == "app.commonlisp")
{
  "InstanceId": "i-commonlisp",
  ...  # ずらずら出るので省略
}

 ついでに、不要な要素は出ないようにしましょう。

$ aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select(.Tags[].Key == "Name" and .Tags[].Value == "app.commonlisp")
  | {"instance-id": .InstanceId, "tags": .Tags}'
{
  "instance-id": "i-commonlisp",
  "tags": [
    {
      "Key": "Name",
      "Value": "app.commonlisp"
    },
    {
      "Key": "aws:autoscaling:groupName",
      "Value": "commonlisp-ApplicationFleet"
    },
    ...  # タグが出るので省略
  ]
}

名前うろおぼえインスタンスの検索

 なんかappって名前をつけたような気がするけどなー、まったく思い出せない。俺たちは雰囲気でサーバを立てている。そんなアナタに捧ぐ。主にぼく自身に捧ぐ :angel:

$ aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select( .Tags[].Key == "Name" and (.Tags[].Value | test("^app")))
  | {"instance-id": .InstanceId, "tags": .Tags}'
{
  "instance-id": "i-commonlisp",
  "tags": [
    {
      "Key": "Name",
      "Value": "app.commonlisp"
    },
    {
      "Key": "aws:autoscaling:groupName",
      "Value": "commonlisp-ApplicationFleet"
    },
    ...  # タグが出るので省略
  ]
}
{
  "instance-id": "i-clojure",
  "tags": [
    {
      "Key": "Name",
      "Value": "app.clojure"
    },
    {
      "Key": "aws:autoscaling:groupName",
      "Value": "clojure-ApplicationFleet"
    },
    ...  # タグが出るので省略
  ]
}

おわりに

 jqでの検索について、以下のことを述べました:

  • 要素を検索する基本的な方法
  • 対象要素のさらに中の辞書も検索条件にする方法

 jqってよくできたDSLですね。jqはチューリング完全でもあるらしく、まさしくJSON時代のawk感があります。


 ところでjqで検索するだけなのになぜ記事が長くなるのか :sob:

続きを読む

AWS Batch とは何か

AWS re:Invent 2016 で発表された AWS Batch。
語感から、誤解されるサービス No.1 な気がします。
定時バッチなどとは何がどう違うのかをメモ。

機能概要

以下公式資料とドキュメント、実際さわってみた所感を合わせて。

結局何なのか

科学技術計算・ハイパフォーマンスコンピューティング用途で真価を発揮する、
大規模なスケール、ジョブの依存定義 が可能なマネージド 並列分散 処理基盤。

主な機能、ポイント

  • クラスタ管理、ジョブキュー、ジョブスケジューラを AWS にお任せできる
  • 処理すべきジョブの数に応じ、適切に 自動伸縮1 するクラスタ
  • ジョブに 依存関係 が定義できる(B は A に依存した処理である2、など)
  • 優先度 を持ったキューを複数定義できる
  • 処理能力ごとにクラスタを分割管理することもできる
  • リソース調達が EC2 より直感的、かつ Spot Fleet も統合済み
  • クラスタは ECS 上に構築される
  • サードパーティのデータ分析ワークフローエンジンのサポートあり

これがマネージドされると僕らは何がうれしいのか

  • 本来やりたい、ジョブの実行依頼(submit)と実処理だけを考えればよくなる
  • クラスタ全体で利用可能なリソースの把握、過不足に応じたその調整が不要3
  • 前処理、集約処理、後処理といった流れのある処理も基盤側に制御を移譲できる
  • 依頼者や状況に応じた優先的リソース配分が容易に実現できる
  • CPU / GPU でそれぞれクラスタを作り、前処理 CPU、本処理 GPU なども簡単
  • クラスタごとに可用性、パフォーマンス、コストのバランスが定義しやすい
  • Docker イメージにさえしてしまえばどんな処理も AWS Batch に乗せられる
  • すでにデータ分析パイプラインの定義があれば移行しやすい(かもしれない)

逆に現在4サポートしていないこと

以下 API を駆使してプログラムは書けるものの、標準機能にはありません。

  • cron のように事前指定した時間での起動など、定期タスク管理
  • ジョブの処理そのものや待機状態のタイムアウト
  • 処理が失敗した場合のリトライ
  • リソース不足時、どのリソースがどれだけ足りないかの把握

定時バッチのマネージドサービスではないよという話

再掲:「AWS Black Belt Online Seminar 2017 AWS Batch」10 ページ目
https://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-online-seminar-2017-aws-batch/10

ストリーミングではなく、(本来の意味の)バッチ的に渡ってくるタスクを
スケーラブルに並列処理させるための仕組みとのことです。

ユースケース

公式

以下からも明らかに主なターゲットはシミュレーションやデータ解析のための処理。
変数を変えながら大量に流す処理細切れにして並列に流せる処理 が向いている。

公式(番外編)

膨大なパラメタを探索したり、予め大量の画像に推論したラベルを貼るといった
昨今話題となっている深層学習分野にも有用、今後利用は広がりそう。

個人的印象

上の深層学習の例では API 消費にレートリミットをかけるという位置付けで
AWS Batch を使っていますが、この使い方はとても汎用的だと思います。
ということで、以下あたりも向いていそう。

  • レートリミットをかけたい処理の実行

例えば同時に流してよい処理がクラスタ全体で一つだけであるとか、
3 つ程度にしたいといった時にクラスタとジョブの定義だけで調整可能。

  • ECS で RunTask していた非同期タスク

科学技術計算といった高度なものではなく、もっとシンプルなタスク。
Web / スマホアプリでも定期的に必要になる小さなジョブなども
予め Docker イメージになっているようであればすぐ使えて便利。

リソースの管理とジョブのスケジューリング

業界によって意味の異なる言葉の整理。

HPC 界隈、コンテナ(Web)界隈、業務システム界隈それぞれで
少しずつ意味が異なるようなので、比較しながら併記します。
(AWS Batch での意味 = HPC 分野での意味)

クラスタ、リソース管理

AWS Batch のいうクラスタは、将来的には HPC 方向に機能強化されるはず・・

  • HPC: 施設・研究者で共有される 巨大なリソース をクラスタとして管理。
    ジョブの要求するハードウェア性能は一般にとてもシビアであり、
    どこでどんな性能のノードが使えるかといった情報の収集も厳密。
  • コンテナ: 複数のサービスでシェアされる特定サーバ群のことで
    一般には可用性やスケーラビリティが重視された構成が取られる。
    どのホストで稼働するかは重要ではなく、むしろどこでも動くよう設計される。
  • 業務: データセンタ内のリソースを管理。あまり包括的には扱われない。
    稼働するアプリケーションは頻繁に変わるものではなく
    とにかく安定的・効率的にサービスが稼働することが大切。

スケジューラ

  • HPC: 実行したいジョブはキューに投げる。リソースが空いたら次の処理が始まる。
    ジョブ同士の依存関係が強い。順序や処理間のデータ移動も重要。
    優先的に処理したいジョブは割り込める必要がある。
    多くは試行錯誤のためのジョブ、FAIL してもいいが処理時間はとにかく長い。
  • コンテナ: 各サービスの要求するリソースを見つけ次第どこかに投入される。
    サービスの理想状態を別途管理する必要があり、異常終了時などの再起動や
    すでに起動しているサービスの状態を考慮した配置も必要になる。
  • 業務: 業務やその時間、ワークフローに応じてジョブを起動・停止する。
    ジョブ同士の依存関係も強く、処理に失敗した場合のワークフローは最も複雑。
    ユーザごとにそのあたりを設定変更できる柔軟さが必要。

AWS Batch の概念・機能

コンピューティング環境

AWS Batch には「コンピューティング環境」という概念がありますが
いわゆる「クラスタ」のようなもので、複数インスタンスの集合です。

そしてそれは、用途に応じて 2 種類あります。

Managed 環境

Unmanaged でなければいけない理由がなければ Managed 環境がオススメ。
Managed 環境の特徴は以下の通り。

  • 待機ジョブの深さに応じてクラスタがオートスケールアウト / イン
  • ECS クラスタを裏で作成・管理してくれる
  • Spot Fleet が簡単に使えて便利

Unmanaged 環境

ちょっと変わったクラスタにカスタマイズする必要がある場合はこちら。
AWS Batch の生成する ECS クラスタに「関連づけるインスタンス」を
自由にカスタマイズ可能。例えばこんなとき。

  • 特定の AMI を使いたい
  • あれこれ調整した AutoScaling グループを使いたい
  • EFS を使いたい
  • GPU を使いたい

Unmanaged 環境の場合はキューの深さによるオートスケールが働かないため
このようなアーキテクチャ で別途用意する必要があります。

ジョブ定義・実行

ジョブの定義は JSON で事前に宣言 することになります。
ジョブの処理ロジックについては、さらにそれ以前に Docker イメージにして
DockerHub や ECR などに push しておく必要があります。

実行時パラメタ

ジョブの基本的な起動パラメタは事前に JSON に定義するものの、
実行時に挙動を変える手段が AWS Batch には 2 つのあります

  • Ref:XXX & –parameters : 事前に XXX と定義したパラメタを実行時に指定
  • 環境変数 : コンテナへ渡す環境変数を実行時に指定

依存関係のあるジョブ

ジョブ投入時に --depends-on オプションで、すでに投入済みの
依存するジョブの ID を渡すことで依存関係を定義できます。

ジョブ間のデータ連携

HPC 系バッチコンピューティングを支援するマネージドサービスということで
そのデータの連携には以下サービス群の利用が想定されているようです。

  • EFS: まだ東京リージョンに来てないけども・・
  • EBS: 前処理したタイミングでスナップショットを取り展開、など
  • S3: AWS といえば S3 の活用ですが、もちろん大活躍
  • RDS / DynamoDB: 使いどころはたくさんありそう

他サービスとの連携

AWS を使い倒せば、よりセキュアで安定したサービスにも

  • CloudWatch Logs: ジョブの標準出力は全てここに連携されます
  • IAM: ECS ベースなこともあり、ジョブごとの権限管理にはロールが使える
  • KMS: 秘密情報の管理には KMS + IAM ロールがほんとに便利
  • Lambda + CloudWatch Events: Unmanaged 環境の自前オートスケールなど

他サービスとの使い分け

Lambda や EMR、ECS とどう使い分けるの?という。Batch を選択する意味。

なぜ Batch がでたのか

Azure には AWS より以前に、同名の Azure Batch というサービスがありましたが
こちらも狙いは、汎用的な 科学技術計算や HPC 基盤のマネージド提供でした。

AWS にも EMR や MachineLearning といったある種の問題解決に特化した
マネージドサービスはありましたが、汎用的なバッチ環境としては
AWS Batch が最も適切なサービスとなりそうです。

使い分け

上記のメリットに合致したジョブなら AWS Batch。
悩ましいとしたら、例えば

  • Lambda では難しい(運用・開発効率、サーバ性能、タイムアウト、言語)
  • EMR 向けに作りこまれたものではない(既存の処理を使いたい)

のならば AWS Batch を使う、など。

使ってみよう

実際にジョブを流してみると感覚がつかめると思います。

ハンズオン


  1. Managed 環境だった場合の挙動 

  2. A が正常終了(SUCCEEDED)すると B が開始され、A が異常終了(FAILED)した場合は B も FAILED になる 

  3. 厳密には待機ジョブの数で調整がかかるので、CPU やメモリなどに応じたリソースの調達・解放がしたい場合は API を利用したプログラムが別途必要 

  4. 2017/03/01 時点 

続きを読む

Terraform で AWS環境を実運用する上で困ったことと、その対処

忘備録的なもの。
2017年2月時点、Terraformは0.8.6.

操作用AWSアカウントの認証情報の扱い

困ったこと

ネットの参考情報だと、awsの認証情報(credentials)を直接書くサンプルが非常に多かった。
しかし、tfファイルを書き換える運用だと、いつか間違えてcommit & pushしてインシデントになりそう。

Terraformを最初に動かすためのユーザーの認証情報は、その性質上大きな権限を持っていることが多いと思うので、慎重になりたい。

解決策

AWS-CLIのNamed Profile使おう。

$ aws configure --profile my-profile-name
AWS Access Key ID [None]: xxxxxxxxxx
AWS Secret Access Key [None]: xxxxxxxxxx
Default region name [None]: ap-northeast-1
Default output format [None]: 

事前にこうして設定しておけば、認証情報は$home/.aws/credentialsに名前付きで入る。

スクリプトからはこう参照する。これならばcommitしてしまっても問題ない。

example1.tf
provider "aws" {
  profile = "my-profile-name"
}

あとは、上記の設定手順をREADME.md なんかに書いて、KEYIDとSECRET自体はいつも通り正しく管理してあげればいい。

.tfstateファイルの扱い

困ったこと

.tfstateファイルを紛失してしまうと、作成したインスタンスを、Terraformから管理できなくなってしまうので、最重要ファイルだ。
かといって、gitにcommitするルールだと、commit忘れが怖いし、その際pullし忘れるとつらい。
一方、手動であちこち引き回しても同じことで、別の開発者が古いstateに基づいて、重複したインスタンスを立ててしまうかもしれない…

解決策

Backendの、remote state機能使おう。

ちゃんと公式ドキュメントある。 https://www.terraform.io/docs/state/remote/s3.html

こんな感じ。profileも指定できるので、そちらにregionを書いておけば省略できる。

$ terraform remote config 
    -backend=s3 
    -backend-config="bucket=my-tfstate-store-name-at-s3" 
    -backend-config="key=hogehoge/terraform.tfstate" 
    -backend-config="profile=my-profile-name"

これを実行した時点で、存在していたterraform.tfstateは、./.terraform/terraform.tfstate に移動されるようだ。

あとは自動でtfstateファイルをアップロード、ダウンロードしてくれる。
指定するバケットはacl=privateにしておくこと!!
あと、上記リンクでは、S3のversioningとかもつけておくことを勧めている。(私はやらなかった)

S3以外にも、いろいろ手段は用意されているようだ。

環境ごとのvariableの扱い

困ったこと

ステージングと本番、みたいに分けようと思って、環境ごとにvariableで設定値を与えられるようにしたけど、
-var オプションで引数に全部指定するの辛い

解決策

共通の設定ならば、terraform.tfvars という名前のファイルに書いておけば、指定しないでも勝手に読み込んでくれるとのこと。
https://www.terraform.io/intro/getting-started/variables.html#from-a-file

環境ごとに違う変数は、-var-fileオプションを使ってスイッチするのがよさそうだ。

$ terraform plan -var-file staging.tfvars
$ terraform plan -var-file production.tfvars

的な。

varは後ろに指定したものが有効(上書きされる)とのことなので、上手に使えば強いはず。

Packerで作ったAMIでEC2インスタンスを生成したい

困ったこと

auto-scalingを考えたときに、元となるAMIをちゃんと運用したい。
(注意、私はAWSのAutoScaling をよくわかっていないかも。)

そこで、Packerでイミュータブルなイメージ作ったらすごーいはず。
イミュータブルということは、イメージにDB接続情報がないといけない気がする。
よって、Terraformで先にRDS立てないといけないけど、そのTerraform側でAMIを使いたいからこういう話になっているわけで…
循環参照してしまう。

そもそも、AutoScaling配下のインスタンスを入れ替える際に全インスタンスを落とさないようにする、というのがなかなか厳しいようだ。
AutoScalingとの相性は、改善まち、という感じか。

参考: http://qiita.com/minamijoyo/items/e32eaeebc906b7e77ef8

準解決策1 null_resource

最終的にはうまくいかなかったが、最初に試した方法

null_resourceというものがある。
https://www.terraform.io/docs/provisioners/null_resource.html

何もしない代わりに、自身の状態(=変更が起きるトリガー)を定義するtriggers属性と、provisionerを設定できるリソースだ。
これを使って、

example2.tf
variable "ami_name_prefix" {}

resource "null_resource" "packer" {
  triggers {
    db_host = "${aws_db_instance.hogehoge.address}"
  }

  provisioner "local-exec" {
    command = <<EOS
packer build 
  -var 'DB_HOST=${aws_db_instance.hogehoge.address}' 
  -var 'AMI_NAME_PREFIX=${ami_name_prefix}' 
  packer.json"
EOS
  }
}

data "aws_ami" "packer_ami" {
  most_recent = true
  name_regex = "^${var.ami_name_prefix}.*"
  owners = ["self"]
  depends_on = ["null_resource.packer"]
}

resource "aws_instance" "hoge" {
  ami_id = "${data.aws_ami.packer_ami.id}"
  ...
}

とこんな感じ。

しかし、Terraform 0.8.6だと、triggersの中にinterpolationを混ぜると、問答無用でcomputed扱いになってしまうようで、
この記述では毎回AMIが作成されてしまって、差分のみ実行というTerraformの観点からは、使えなかった。

準解決策2 イミュータブルを諦める

オートスケーリング自体はこのパターンでは不可能か。AMI化を再度行う必要があると思う。

ほぼ完成品のAMIを組み立てておいて、aws_instanceのprovisionerで最後の仕上げをする。
Dockerなんかは、環境変数で外部情報を読み込ませる、なんてことをするらしいので、この手法に踏み切った。

// .envが欠けた状態でAMIをつくる
$ packer packer.json
example3.tf
data "aws_ami" "packer_ami" {
  most_recent = true
  name_regex = "^packer-ami-.*$"
  owners = ["self"]
  depends_on = ["null_resource.packer"]
}

data "template_file" "envfile" {
  # template = "${file(./env.tpl)}" などとした方が望ましいが例示のため。
  template = <<EOS
export DB_HOST="${db_host}"
export DB_PORT="${db_port}"
....
EOS

  vars {
    db_host = "${aws_db_instance.main.address}"
    db_port = "${aws_db_instance.main.port}"
  }
}

resource "aws_instance" "ec2" {
  ami_id = "${data.aws_ami.packer_ami.id}"
  ...

  # envファイルをuploadする。envファイルはdirenvとかdotenvとかで読み込まれるようにしておく。
  provisioner "file" {
    content = "${data.template_file.envfile.rendered}"
    destination = "/home/ec2-user/.env"
  }

  # 上で入れた環境変数を使ってサービスを立ち上げる。
  provisioner "remote-exec" {
    inline = ["sudo service unicorn start"]
  }

  # provisionerは、実行した環境からssh(のgolang実装。sshコマンドではない)で接続することになる。
  # 当然security_groupとvpc_internet_gatewayが適切に設定されている必要がある。
  connection {
    type = "ssh"
    ...
  }
}

tfファイルを構造化したい

困ったこと

コピペや反復は悪だし、再利用性も下がる。

COUNT

COUNT = nで配列のように同じタイプのリソースを複数作れる。
それぞれ微妙に違う値を書きたいなら、

COUNT = 2
ATTR = "${element(list("a", "b", "c", "d"), count.index)}"

などとできる。
listは変数化するとなおよい。

module

複数のリソースにまたがっての反復パターンなら、module化してしまうとよい。
module自体の書き方はここでは説明しないが、

./modules/my_module/variables.tf
./modules/my_module/main.tf
./modules/my_module/output.tf

を作り、

example4.tf
module "use_of_my_module" {
  source = "./my_module"
  var1 = ...
  var2 = ...
}

と書くことで使用。

$ terraform get

で、モジュールをterraformに読み込ませる準備をさせる。(./.terraform/modules/ にsym-linkが作成されるようだ)

様々に公開されているmoduleもあるようなので、むしろ自分でresourceを書かないほうが良いのかもしれない。

その他

また何かあれば書く。

続きを読む

CloudWatch エージェントは SSM エージェントと呼ばれる別のエージェントに移行されましたとさ

ログは外に出しますよね

EC2 の Windows インスタンスでイベントログやアプリケーションログを CloudWatch Logs に転送して運用しています。
ログを見るためにいちいちインスタンスに入る必要もないし、必要なログがインスタンス外にあることで AutoScaling でインスタンスが勝手に破棄されても大丈夫という設計です。

これまで

ログ転送設定を C:Program FilesAmazonEc2ConfigServiceSettingAWS.EC2.Windows.CloudWatch.json に書いて EC2Config サービスを再起動することでインスタンスごとにログ転送の仕方を設定していました。
これを CodeDeploy の Hook スクリプトで行い、アプリケーションの配置と同時にログが指定した(アプリケーションごとの)ロググループにロストすることなく集めることができていました。
ちなみに Windows Server 2012 R2 です。

EC2Config ログ転送やめるってよ

あるとき、こんな記事を見つけました。
Windows ServerのCloudWatch LogsをSSMで行う | Developers.IO

従来のEC2 Configの設定を行っても、CloudWatch Logsへログの転送が行われないので注意してください。

え?まじで?
試しに新しい AMI で試してみましたがやっぱりログは転送されません。
今後は SSM で設定しなくちゃいけないのかと調べましたが、どうもやりたいことができません。
やりたいことって何よ?を書くと長くなりそうなんで書きませんが、とにかく、インスタンスに置いたローカルファイルからログ転送設定をしたいんです。

どうも SSMAgent です

EC2Config バージョン履歴的なドキュメント を確認したらなんだかローカル設定ファイルを使えそうな雰囲気が・・・

ローカル設定ファイル (AWS.EC2.Windows.CloudWatch.json) を使用して、インスタンスで CloudWatch の統合を有効にしている場合、SSM エージェントと連携して動作するようファイルを設定する必要があります。詳細については、「Windows Server 2016 インスタンスで CloudWatch 統合用のローカル設定ファイルを使用する」を参照してください。

お?・・・え? Windows Server 2016 だけなん?

結局、サポートに問い合わせて回答をいただきました。

CloudWatch Logsの設定方法

これまでと同様にローカルのファイルをお使い頂けますが、CloudWatchもしくはCloudwath Logsの設定変更が必要な場合には、下記の手順を実施頂くようお願いいたします。

  1. Amazonssmagent サービスを停止します。
    コマンドプロンプトで net stop amazonssmagent を実行することで停止できます。
  2. C:Program FilesAmazonSSMPluginsawsCloudWatchAWS.EC2.Windows.CloudWatch.json を削除いたします。
  3. C:Program FilesAmazonEc2ConfigServiceSettingAWS.EC2.Windows.CloudWatch.json の内容を編集いたします。
  4. Amazomssmagent サービスを開始します。
    コマンドプロンプトで net start amazonssmagent を実行することで開始できます。

新しい設定が反映されますと、AWS.EC2.Windows.CloudWatch.json の内容が C:Program FilesAmazonSSMPluginsawsCloudWatchAWS.EC2.Windows.CloudWatch.json に出力されます。
テキストファイルの改行コードが変わり、Headerが追加されますが、影響はございませんので、無視頂くようお願いいたします。

・・・ふむ。
これ通りに試したところ、無事にログ転送がされました。

極端なことを言ってしまえば僕のケースでは、再起動するサービスを EC2Config から SSMAgent に変えるだけで良かったと言う話です。(担当部署が変わったからそっちに聞いて的な)
でもあくまで設定ファイルの大本は今まで通り EC2Config の下にあるファイルで、それを読み込んで SSM の下のワークスペース的なところに保持するって感じなんですね。

まとめ

  • 新しい Windows AMI でも CloudWatch Logs のログ転送設定をローカルファイルで行える
  • ただし扱うサービスは SSMAgent
  • ファイルの置き場に注意
  • AWS のドキュメントはやっぱり追いつくのが遅い
  • AWS のサポートは丁寧で助かる

続きを読む

CloudFormationでのAutoScalingGroup配下のインスタンス入れ替え方法

AWS::AutoScaling::AutoScalingGroupの更新時にはインスタンスの入れ替えが発生する場合がある。
LaunchConfigurationに変更があった場合、VPCの設定が変わった場合など。

この時、どのようにインスタンスを入れ替えるかをUpdatePolicy属性で指定することができる。
指定できる方式は2種類。

  • AutoScalingRollingUpdate
    既存のAutoScalingGroup配下のインスタンスを数台ずつ入れ替えていく方法。

  • AutoScalingReplacingUpdate
    新しいAutoScalingGroupを作成して、新旧のグループを入れ替える方法。

どちらも指定しないとインスタンスの入れ替えが発生せず変更が反映されないので注意。
両方指定した場合はAutoScalingReplacingUpdateWillReplaceの値によってどちらの方式が優先されるか決まる。
trueならAutoScalingReplacingUpdateが優先、falseならAutoScalingRollingUpdateが優先、となる。

AutoScalingRollingUpdate

下記の例では、最低1台は稼働させつつ、1台ずつ入れ替える。
SignalResourceの受信を持って起動完了とみなす。
シグナル受信のタイムアウトは10分。

AutoScalingRollingUpdate
Resources:
  ASG:
    Type: "AWS::AutoScaling::AutoScalingGroup"
    UpdatePolicy:
      AutoScalingRollingUpdate:
        MaxBatchSize: 1
        MinInstancesInService: 1
        PauseTime: PT10M
        WaitOnResourceSignals: true
          :
    Properties:
      :

その他の設定はドキュメントを参照。
AutoScalingRollingUpdate Policy

実行時のCloudFormationのイベントはこんな感じ。下に行くほど古いイベント。
新旧インスタンスの起動と停止は同時に行われる。

Received SUCCESS signal with UniqueId i-555...
New instance(s) added to autoscaling group - Waiting on 1 resource signal(s) with a timeout of PT10M.
Successfully terminated instance(s) [i-222...] (Progress 100%).
Terminating instance(s) [i-222...]; replacing with 1 new instance(s).

Received SUCCESS signal with UniqueId i-444...
New instance(s) added to autoscaling group - Waiting on 1 resource signal(s) with a timeout of PT10M.
Successfully terminated instance(s) [i-111...] (Progress 67%).
Terminating instance(s) [i-111...]; replacing with 1 new instance(s).

Received SUCCESS signal with UniqueId i-333...
New instance(s) added to autoscaling group - Waiting on 1 resource signal(s) with a timeout of PT10M.
Successfully terminated instance(s) [i-000...] (Progress 33%).
Terminating instance(s) [i-000...]; replacing with 1 new instance(s).

Rolling update initiated. Terminating 3 obsolete instance(s) in batches of 1, while keeping at least 1 instance(s) in service. Waiting on resource signals with a timeout of PT10M when new instances are added to the autoscaling group.

ロールバック時の挙動

スタック更新失敗時のロールバックの挙動について。
デフォルトの設定では1台でも入れ替えに失敗するとロールバックされる。
UpdatePolicyMinSuccessfulInstancesPercentが100%の場合(デフォルトで100%)。

MinSuccessfulInstancesPercent
UpdatePolicy:
  AutoScalingRollingUpdate:
    MinSuccessfulInstancesPercent: 100

それまでに起動に成功したインスタンスがあれば数台ずつ停止され、前回の設定を使って起動したインスタンスに入れ替わっていく。
ロールバック時もUpdatePolicyの設定に従って入れ替えられる。

前回の設定でインスタンスを起動できるようにしておかないとロールバックに失敗するので注意。
参考:CloudFormationのロールバック時に設定ファイルも元に戻す

メリット

  • 余分なインスタンスの起動がない
    数台ずつ入れ替えるのでインスタンスの同時起動数がさほど増えない。
    なのでEC2のリミットに引っかかりにくい。

  • 暖機運転が不要
    これは作りと設定次第。
    徐々に入れ替わるので、キャッシュなどが消えても結果的に影響が少なくなる。

デメリット

  • 入れ替え完了まで時間がかかる
    徐々に入れ替わるので当然時間がかかる。
    MaxBatchSizeを最大にしてMinInstancesInServiceを0にすれば全台一斉に入れ替えることもできる。
    サービスは死ぬ。すでに死んでいる時などに有効。

  • 入れ替え中は新旧インスタンスが混在する
    アプリケーションの作りによってはデータの整合性が取れなくなったりする。

  • 途中で起動失敗するとロールバックにも時間がかかる
    起動時と逆に数台ずつ古いインスタンスに入れ替わっていくため。

  • ロールバック時に旧設定で起動できるよう工夫する必要がある
    「ロールバック時の挙動」に書いた通り。

AutoScalingReplacingUpdate

下記の例では、新規AutoScalingGroupを作成し、3台分のSignalResourceの受信をもって作成完了とする。
シグナル受信のタイムアウトは10分。
CreationPolicyを指定しないとSignalResourceFAILUREを送信してもロールバックが行われず入れ替えが完了してしまうので注意。

AutoScalingReplacingUpdate
Resources:
  ASG:
    Type: "AWS::AutoScaling::AutoScalingGroup"
    CreationPolicy:
      ResourceSignal:
        Count: 3
        Timeout: PT10M
    UpdatePolicy:
      AutoScalingReplacingUpdate:
        WillReplace: true
    Properties:
      :

その他の設定はドキュメントを参照。
AutoScalingReplacingUpdate Policy

実行時のCloudFormationのイベントはこんな感じ。下に行くほど古いイベント。

Received SUCCESS signal with UniqueId i-555...
Received SUCCESS signal with UniqueId i-444...
Received SUCCESS signal with UniqueId i-333...

Resource creation Initiated
Requested update requires the creation of a new physical resource; hence creating one.

ロールバック時の挙動

スタック更新失敗時のロールバックの挙動について。
デフォルトの設定では1台でも入れ替えに失敗するとロールバックされる。
CreationPolicyMinSuccessfulInstancesPercentが100%の場合(デフォルトで100%)。
UpdatePolicyではないので注意。

MinSuccessfulInstancesPercent
CreationPolicy:
  AutoScalingCreationPolicy:
    MinSuccessfulInstancesPercent: 100

新規のAutoScalingGroupと配下の新規インスタンスは削除される。
インスタンス起動時にELBには登録されるが、InServiceになる前に外される。

メリット

  • 入れ替え完了まで時間がかからない
    全台同時に入れ替えるので完了までの時間が短い。

  • 入れ替え中に新旧インスタンスが混在しない
    新旧インスタンスが共存している時間が短いので、混在することによるリスクが少ない。

  • ロールバック時に現行システムに影響がない
    スタックの更新に失敗しても、現行のインスタンスは影響を受けない。

デメリット

  • 一時的なインスタンス同時起動数が2倍になる
    全台同時に入れ替えるので一時的に2倍のインスタンス数が必要になる。
    なのでEC2のリミットに引っかかりやすい。

  • 暖機運転が必要
    これは作りと設定次第。
    全台同時に入れ替わるので、例えば全サーバでキャッシュが消えてサービスに影響がでたりする。
    その場合、暖機運転を行ったあとSignalResourceを送信するなどの対応が必要。

続きを読む