ElasticBeanstalk, RDSを自動停止・自動起動する

hatappi.hateblo.jp

最近個人で運営しているサービスをGCPからAWSへと少しづつ移行している
今回はElasticBeanstalk(以降はEB)の自動停止・自動起動をする
個人で使用しているアプリケーションなので、お金は出来るだけ安くしたい
そこで深夜とか使わない時間は止めて朝方とかに立ち上げることにした

EB, RDSとそれぞれの自動停止・自動起動の仕方を探る

まずEBの自動停止・起動だがEBにはCLIがあり今バージョン3で2の時はeb stop, eb startがあったので停止と起動がコマンド一発で出来た
しかしバージョン3では廃止された
公式のdocによるとeb stopeb terminateして環境を終了して
起動する時はeb startではなくeb createで環境を作成してくれとのこと

RDSに関しては今までだと自動停止・自動起動というと停止する時にスナップショットをとってインスタンスを削除して、起動する時にスナップショットから復元するような方法だった
それはさすがに面倒だなと思ったら最近リリースされましたね!!

https://forums.aws.amazon.com/ann.jspa?annID=4670

dev.classmethod.jp

RDSのコマンド一発で停止・起動できるようになりました
ただ現状だといくつかの制約事項があるようです ※ 2017/07/05時点

今回はMultiAZではなくシングルでかつMySQLでたててたので停止が出来そう

実際の作業

どこかのサーバーでcronでコマンドを実行しても良いですが、ここはLambdaの出番です
LambdaでCloudWatch Eventsでスケージューリングして自動起動・停止を行います

Lambdaのコンソール上でコードを書いても良かったのですが、Pythonを使う際のawssdkのbotoがまだRDSのstop,startに対応しているバージョンでなかったので自前で最新版のものを用意してあげる必要がありました
それには以前も使用したServerless Frameworkを使用します

hatappi.hateblo.jp

serverless用のyamlファイルは下記のようなものを定義しました

service: eb-rds-auto-service
frameworkVersion: ">=1.14.0 <2.0.0"

provider:
  name: aws
  runtime: python2.7
  stage: prod
  region: ap-northeast-1
  role: arn:aws:iam::111111:role/hoge

plugins:
  - serverless-python-requirements

functions:
  start:
    handler: handler.start
    events:
      - schedule: cron(0 1 ? * * *) # UTC
  stop:
    handler: handler.stop
    events:
      - schedule: cron(0 13 ? * * *) # UTC

Pythonのhandlerは下記のようなものを使用します

# -*- coding: utf-8 -*-
from __future__ import print_function

import json
import boto3
from datetime import datetime

eb = boto3.client('elasticbeanstalk')
rds = boto3.client('rds')

rds_instance_identifier = "test-mysql"
application_name = "app_name"
template_name = "template_name"
environment_name = "test-env"
app_version_label = 'app-eeee-111111_2222222'

def stop(event, context):
    try:
        rds.stop_db_instance(
            DBInstanceIdentifier=rds_instance_identifier
        )

        eb.describe_environments(
            ApplicationName=application_name,
            EnvironmentNames=[
                environment_name
            ]
        )

        env = [ env for env in response['Environments'] if env['EnvironmentName'] == environment_name ][0]
        env_id = env['EnvironmentId']

        eb.delete_configuration_template(
            ApplicationName=application_name,
            TemplateName=template_name
        )

        eb.create_configuration_template(
            ApplicationName=application_name,
            EnvironmentId=env_id,
            TemplateName=template_name
        )

        eb.terminate_environment(
            EnvironmentId=env_id
        )

        return { "status": "ok" }
    except Exception as e:
        print(e)
        message = 'STOP ERROR'
        raise Exception(message)

def start(event, context):
    try:
        rds.start_db_instance(
            DBInstanceIdentifier=rds_instance_identifier
        )

        eb.create_environment(
            ApplicationName=application_name,
            EnvironmentName=environment_name,
            TemplateName=template_name,
            CNAMEPrefix=environment_name,
            VersionLabel=app_version_label
        )

        return { "status": "ok" }
    except Exception as e:
        print(e)
        message = 'START ERROR'
        raise Exception(message)

後はrequirements.txt

boto3>=1.4.4

handler内では停止の場合はRDSの停止とEBの環境の設定をS3に保存した上で環境の削除をしています
環境の設定を保存することでstartする時にその設定をロードすることで同じ環境を再現できる
ただ保存された環境に何のアプリケーションを動かすかまでは入っておらずcreate_environmentする時にはVersionLabelを指定する必要がある
でないとサンプルアプリケーションが立ち上がる

serverless deployでLambdaに停止、起動とそれぞれの関数が定義されて指定時間になると自動停止、自動起動を行ってくれる
これで節約しながらサービスを運用できそう!!