AWSの運用サーバーをなる安で管理するコツ(Spot Instance,userDataによる構成管理)


はじめに

こんにちわ。Wano株式会社でエンジニアをやっているnariと申します。
今回は、Spot Instanceを活用して本番サーバー管理の費用を抑えるtipsを紹介したいと思います。

Spot Instanceとは

スポットインスタンスはオンデマンドインスタンスに比べ最大 90% 低い価格で購入できます。さらに、EC2 Auto Scaling を使用すればスポット、オンデマンド、RI のすべてを使ってキャパシティーをプロビジョニングし、ワークロードのコストとパフォーマンスを最適化できます
(Amazon EC2 スポットインスタンス | AWSより引用)

めちゃくちゃ破格でインスタンスを利用できるSpot Instanceですが、もちろんデメリットもあリます。
供給が安定してきたと言っても供給が中断されることがあり、その中断通知が2分前とかなりシビアなので、しっかりとそれ用のしくみ作りをする必要があります。(ここはオンデマンド、リザーブドインスタンスとの大きな違い)

Spot Instanceを使いこなすための準備

1.EC2スポットサービスによる供給の中断 と向き合う

今回は運用サーバーであり、少々のダウンタイムは問題ない(最近のスポットインスタンス供給はかなり安定してきている)ので、ここの話はしません。
ダウンタイムが許容されないサービスでは、2分前の中断通知を受け取って行うべきこととして、

  • ALBを使用している場合、中断対象のインスタンスをTargetGroupから外す
  • ECSを使用している場合、コンテナインスタンス自体のドレイニングを行う

などを行う必要があります(ここら辺の説明はECS(on EC2)でAutoScaling(Spot Instance運用)を実現する - Qiitaで扱っています)

2.インスタンスをステートレスに保ち(状態を持たない)、使い捨てにする

インスタンスが状態を持ってしまうと、停止起動の度にその状態への復元の暖かい作業が必要となります。(当然一貫性も担保できない)

そのため、AMIとブートストラッピングを組み合わせて、いつでもリソースを作成/復元できるように準備しておくことが大事です。
今回は、UserDataを用いた起動テンプレートによる簡易なブートストラッピングの例を載せておきます(複雑になってくるとchefなり使った方が楽なのかもですが、最近はコンテナ設定に構成管理の部分まで寄せれたりするので中々学習できていない)

以下の例は、ssm-userでsshレスオペレーションすること前提でのuserDataとなります
(参考:AWS Session Managerを用いたsshレスオペレーションを、ユーザーデータでのブートストラッピングで実現する - Qiita)

userData.sh
#!/bin/sh
# タイムゾーンの変更
sed -ie 's/ZONE=\"UTC\"/ZONE=\"Asia\/Tokyo\"/g' /etc/sysconfig/clock
sed -ie 's/UTC=true/UTC=false/g' /etc/sysconfig/clock
ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
# ロケールの変更
sed -ie 's/en_US\.UTF-8/ja_JP\.UTF-8/g' /etc/sysconfig/i18n
# rpmを最新化
yum -y update

# 必要なパッケージを入れる
yum -y install wget python curl groff  gcc make cmake g++ openssl git tree  ca-certificates vim jq

# customize ~/.bashrc
chmod 755 /home/ssm-user/.bashrc
cat <<EOT  >> /home/ssm-user/.bashrc
export HISTSIZE=2000
//お好みの設定を
EOT
source /home/ssm-user/.bashrc

# customize ~/.vimrc
cat <<EOT >> /home/ssm-user/.vimrc
//お好みの設定を
EOT

##### mysql
yum localinstall https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm -y
yum-config-manager --disable mysql80-community
yum-config-manager --enable mysql57-community
yum install mysql-community-server -y


##### redis
amazon-linux-extras install redis4.0

##### Install awscli
curl "https://bootstrap.pypa.io/get-pip.py" | python && \
pip install awscli

#### make .bash_history
touch /home/ssm-user/.bash_history
chmod 755 /home/ssm-user/.bash_history
chown ssm-user:ssm-user /home/ssm-user/.bash_history

#### Install docker
amazon-linux-extras install -y docker
systemctl start docker
systemctl enable docker

#### git clone source
export ssm_id_name=$(aws ssm get-parameters --names /hoge/id --with-decryption --region=ap-northeast-1 | jq '.Parameters[] | .Value' |sed 's/\"//g')
export ssm_pass_name=$(aws ssm get-parameters --names /hoge/pass --with-decryption --region=ap-northeast-1 | jq '.Parameters[] | .Value' |sed 's/\"//g')
cd /home/ssm-user
git clone https://$ssm_id_name:$ssm_pass_name@gitlab.hoge.co.jp/hoge/hoge.git

3.AutoScalingGroupでサーバー供給が中断しても、自動復旧する仕組み作り

AutoScalingでは、 ミックスインスタンスグループ(オンデマンドとスポットを一つのASG)が使用可能となっているので、こちらを利用するとあるプールのインスタンス供給が中断してもすぐに別のプールのインスタンスまたはオンデマンドインスタンスがDesiredな数まで立ち上がるように設定することができます。
こちらTerraformを利用すると非常に簡単にできますので、サンプルソースを載せておきます。

main.tf
resource "aws_iam_instance_profile" "default" {
  name = var.instance_profile_name
  role = var.iam_role_name
}


resource "aws_key_pair" "default" {
  key_name   = "key_pair_of_${var.name}"
  public_key = file("${path.module}/dummy.pub")
}

resource "aws_launch_template" "default" {
  name          = var.name
  image_id      = var.ami
  instance_type = var.instancetypes[0]
  key_name      = aws_key_pair.default.key_name

  iam_instance_profile {
    name = aws_iam_instance_profile.default.name
  }
  user_data = base64encode(var.user_data == null ? local.user_data : var.user_data)

  block_device_mappings {
    device_name = "/dev/xvda" # root device name of amazon linux2

    ebs {
      volume_size           = "${var.root_block_device_size}"
      volume_type           = "${var.root_block_device_type}"
      delete_on_termination = true
    }
  }
}

resource "aws_autoscaling_group" "default" {
  name                = aws_launch_template.default.name
  vpc_zone_identifier = [var.subnet_id]
  min_size            = var.min_size
  max_size            = var.max_size
  desired_capacity    = var.desired_size

  lifecycle {
    ignore_changes = ["desired_capacity"]
  }
  mixed_instances_policy {
    launch_template {
      launch_template_specification {
        launch_template_id = "${aws_launch_template.default.id}"
        version            = "${aws_launch_template.default.latest_version}"
      }

      dynamic override {
        for_each = var.instancetypes
        content {
          instance_type = override.value
        }
      }
    }

    instances_distribution {
      on_demand_percentage_above_base_capacity = 0
      spot_allocation_strategy                 = "lowest-price"
      spot_instance_pools                      = "2"
    }
  }

  tag {
    key                 = "Name"
    value               = var.name
    propagate_at_launch = true
  }
}

終わりに

ダウンタイムが許容される部分に関しては、このようにかなり簡単にSpotInstanceを導入できるようになっていました。
自分の想像では供給も安定しておらず入札形式でオークションみたいになっている(昔はそうだったみたい)んだろうな、、と勝手に敷居を高く設定していたので、非常に拍子抜けしました。(いい意味で)
こんなお手軽にコストを抑えられる(70~90%off!!)のであれば、どんどん活用していきたいですね。

参考文献