エンジニアBLOG

2024/03/21

【AWS】EBSスナップショットの自動化

始めに

エンジニアブログを閲覧頂きありがとうございます。
アルティウスリンクのミチガミです。
今回は現場で構築したEBSスナップショットの自動化を備忘録として掲載します。

自動化対象の環境(ざっくり)

-24H稼働している
-基本的にデータの流入が24H行われている
-マルチAZにて冗長化
-スナップショットの対象のデータ領域インスタンスが複数台存在する
-集計等の処理が頻繁に実行されている

やりたいこと

・EBSのスナップショットを1日1回取得したい
・プロセス停止、インスタンス停止を行い完全な状態でスナップショットを取得
・リージョン毎に取得し、24H稼働を続ける
・開始時刻を指定したい(処理実行タイミングを避けたい)

Amazon Data Lifecycle Manager

今回はAWS側から提供されているライフサイクルマネージャーは使用しません。
リンクから分かるように様々な設定が容易に設定でき便利ですが、問題点もあります。

・問題点
①インスタンスの停止等を行わないので完全な状態でのスナップショットを取得できない。
②開始予定時間に実行される訳ではない(予定時刻から、1時間以内のうちに実行される)
③②に関連してマルチAZ構成を考慮して取得できない
④プロセス停止等の考慮が出来ない

利用するマネージドサービス

・Amazon EventBridge
・AWS Lambda
・AWS Step Functions

Amazon EventBridge

・StepFunctionsのキックをおこなう。
・イベントスケジュールにスナップショットを起動したい時刻を設定。
・ターゲットに起動したいStepFunctionsのを設定。

AWS Lambda

StepFunctionsから呼び出す関数を作成。
今回用意した関数は以下
・ジョブステータスチェック
 ジョブが実行中がをチェックする関数

・管理系コンソールの設定変更
 スナップショット取得対象を管理するコンソール。
 安全にプロセス停止を行うのに必要なモード切替をおこなう関数。
 また、後段処理の為にここでループ処理によるスナップショットの順序把握をするために、
 Map配列を作成しておく。順序はインスタンスに付けられたタグを元に処理される。



  processingorder = []
  ec2_resp = ec2.describe_instances(Filters=[{'Name':'tag-key','Values':['SnapshotOrderNumber']}])
  for ec2_reservation in ec2_resp['Reservations']:
    for ec2_instance in ec2_reservation['Instances']:
      ec2_tags = dict([(tag['Key'], tag['Value']) for tag in ec2_instance['Tags']])
      processingorder.append(ec2_tags['SnapshotOrderNumber'])
    processingorder = sorted(set(processingorder), key=int)

  ProcessOrder = ([dict(zip(["processingorder"],item)) for item in processingorder])

  return ProcessOrder

・プロセスコントロール
 対象のプロセス停止とインスタンス停止をおこなう関数。
・スナップショット作成
 スナップショットを作成する関数。
スナップショットを取得する。また、EBSのタグで保存世代数を指定することも可能。



TAGKEY = 'Backup-Generation'
ORDER = 'SnapshotOrderNumber'

import boto3
import collections
import time
from botocore.client import ClientError
import os

client = boto3.client('ec2', os.environ['AWS_REGION'])

def lambda_handler(event, context):

  # 処理順を取得
  global ordernumber
  ordernumber = int(event['processingorder'])

  descriptions = create_snapshots()
  delete_old_snapshots(descriptions)

def create_snapshots():
  # タグ付きのボリュームを取得
  volumes = get_volumes([TAGKEY])
  number = get_volumes([ORDER])

  descriptions = {}

  for (v, n) in zip(volumes, number):
    tags = { t['Key']: t['Value'] for t in v['Tags'] }
    generation = int( tags.get(TAGKEY, 0) )
    ordertags = { r['Key']: r['Value'] for r in n['Tags'] }
    ordernum = int( ordertags.get(ORDER, 0) )

    if generation < 1:
      continue

    #処理順序判定
    if ordernum == ordernumber:
      volume_id = v['VolumeId']
      description = volume_id if tags.get('Name') is '' else '%s(%s)' % (volume_id, tags['Name'])
      description = 'Auto Snapshot ' + description
      name = tags.get('Name')

      snapshot = _create_snapshot(volume_id, description, name)
      print('create snapshot %s(%s)' % (snapshot['SnapshotId'], description))

      descriptions[description] = generation

  return descriptions

def get_volumes(tag_names):
  volumes = client.describe_volumes(
    Filters=[
      {
        'Name': 'tag-key',
        'Values': tag_names
      }
    ]
  )['Volumes']

  return volumes

def delete_old_snapshots(descriptions):
  snapshots_descriptions = get_snapshots_descriptions(list(descriptions.keys()))

  for description, snapshots in snapshots_descriptions.items():
    delete_count = len(snapshots) - descriptions[description]

    if delete_count <= 0:
      continue

    snapshots.sort(key=lambda x:x['StartTime'])

    old_snapshots = snapshots[0:delete_count]

    for s in old_snapshots:
      _delete_snapshot(s['SnapshotId'])
      print('delete snapshot %s(%s)' % (s['SnapshotId'], s['Description']))

def get_snapshots_descriptions(descriptions):
  snapshots = client.describe_snapshots(
    Filters=[
      {
        'Name': 'description',
        'Values': descriptions,
      }
    ]
  )['Snapshots']

  groups = collections.defaultdict(lambda: [])
  { groups[ s['Description'] ].append(s) for s in snapshots }

  return groups

def _create_snapshot(id, description, name):
  for i in range(1, 3):
    try:
      return client.create_snapshot(VolumeId=id,Description=description,TagSpecifications=[{'ResourceType':'snapshot', 'Tags': [{'Key': 'Name', 'Value': name},]},])
    except ClientError as e:
      print(str(e))
    time.sleep(1)
  raise Exception('cannot create snapshot ' + description)

def _delete_snapshot(id):
  for i in range(1, 3):
    try:
      return client.delete_snapshot(SnapshotId=id)
    except ClientError as e:
      print(str(e))
      if e.response['Error']['Code'] == 'InvalidSnapshot.InUse':
        return;
    time.sleep(1)
  raise Exception('cannot delete snapshot ' + id)

# EOF

AWS Step Functions

Step Functionsは以下のように作成

image

ループ処理において、配列項目へのパスを指定するオプションを用いProcessOrderを設定。

image

ループ内の処理では、値をペイロードし処理順序を担保

image

まとめ

かなりざっくりではありますが、EBSスナップショットの自動化についての備忘録でした。
取り敢えず作ってみたみたいな部分が多く、改善点は多くみられますがAWS Lambdaと
AWS Step Functionsの知識が少し増えました。

今後はJP1等のジョブ管理システムを上手く活用しAWSの
マネージドサービスに置き換えていけたらと考えています。
最後までご覧いただきありがとうございました。