AWSのEBSボリュームにタグをつけるLambdaを作った記録

EBSボリュームは管理しないと無駄にお金がかかります。

何気なくEC2を作るとEBSボリュームが作られます(既存のボリュームを使った場合を除く)。
EC2を削除(終了)してもEBSボリュームはデフォルトで削除されません。
結果、気が付いたら使ってないEBSボリュームが残っていることがあります。
aws.amazon.com

EC2インスタンスを削除するときはEBSボリュームも削除します。

f:id:ponsuke_tarou:20200106105351p:plain
EC2を削除するときのダイアログ

EC2インスタンスを作成するときは「合わせて削除」オプションを設定することもできます。

後で設定するのは大変なようです。
qiita.com

それでもアタッチされていないEBSボリュームが残ることはあります。

複数人で長期間使っていればうっかりアタッチされていないEBSボリュームが残ることはあります。
とはいえ、本当にアタッチされていなければ削除していいのか?誰かが何かの目的で残しているのかも?
となった時に何に使われていたがわかる情報があると助かります。

やりたいこと

前提 : EC2にはNameタグをつけておきます。

EC2インスタンスには、Nameというタグをつけてインスタンスを使っているプロジェクトやサーバの情報をValueに入れておきます。
例えば、ponsukeプロジェクトのMySQLデータベースサーバにしているインスタンスなら「ponsuke-MySQL」をNameタグのValueに入れておく、みたいな。

アタッチされているEC2のNameタグと同じValueをEBSボリュームのNameタグに設定します。

例えば、「ponsuke-MySQL」というNameタグのついているEC2インスタンスにアタッチされているEBSボリュームに「ponsuke-MySQL」というNameタグをつける、みたいな。
そうすれば、EC2インスタンスを削除したあとでも「ponsuke-MySQL」で使っていたんだな、じゃ削除していいね、ってなります。

同じようなことをやっている先人の知恵を流用します。

www.simpline.co.jp
core.cohalz.co

Lambdaを作る記録

Lambdaの実行権限を作成します。

IAMのポリシーを作成します。
  1. AWSマネジメントコンソールにある[IAM] > サイドメニューの[ポリシー] > [ポリシーの作成]ボタンで作成画面を開きます。
    • f:id:ponsuke_tarou:20200109100533p:plain
      [ポリシーの作成]ボタンで作成画面を開きます。
  2. [JSON]タブを開いて下にある内容を入力します。
  3. [ポリシーの確認]ボタンで確認画面へ遷移して[名前(必須)]と[説明(任意)]を入力します。
  4. [ポリシーの作成]ボタンでポリシーを作成して一覧画面に戻ります。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:CreateTags",
        "logs:CreateLogStream",
        "logs:CreateLogGroup",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
IAMのロールを作成します。
  1. AWSマネジメントコンソールにある[IAM] > サイドメニューの[ロール] > [ロールの作成]ボタンで作成画面を開きます。
    • f:id:ponsuke_tarou:20200109101230p:plain
      [ロールの作成]ボタンで作成画面を開きます。
  2. [AWSサービス] > [Lambda]を選択後に[次のステップ: アクセス権限]ボタンで次の画面を開きます。
  3. [Attach アクセス権限ポリシー]の[ポリシーのフィルタ]で作成したポリシーを検索して選択後に[次のステップ: タグ]ボタンで次の画面を開きます。
  4. [タグの追加 (オプション)]は任意なので設定せずに[次のステップ: 確認]ボタンで確認画面を開きます。
  5. [ロール名]を入力して[ロールの作成]ボタンでロールを作成して一覧画面に戻ります。

Lambda関数を作成します。

  1. AWSのコンソールにある[Lambda] > [関数の作成]ボタンで画面を開きます。
    • f:id:ponsuke_tarou:20200109094021p:plain
      [関数の作成]ボタンで画面を開きます。
  2. 必要な項目を入力後に[関数の作成]ボタンで関数を作成します。
    • オプション : [一から作成]
    • 関数名 : attatch_name_tag_for_ebs_volume(任意の関数名でOK)
    • ランタイム : Python3.8
    • 実行ロール : 既存のロールを使用する
    • 既存のロール : 作成したロールを選択します。
Lambda関数を実行するトリガーを作成します。
  1. [Designer]にある[トリガーを追加]ボタンでトリガーの設定画面を開きます。
    • f:id:ponsuke_tarou:20200115095939p:plain
      [Designer]にある[トリガーを追加]ボタン
  2. プルダウンから[CloudWatch Events]を選択します。
  3. 各入力欄を記載します
    • ルール : [新規ルールの作成]
    • ルール名(必須) : attatch_name_tag_for_ebs_volume(任意の関数名でOK)
    • ルールタイプ : [スケジュール式]
    • f:id:ponsuke_tarou:20200115101713p:plain
  4. [追加]ボタンでトリガーを作成します。
    • f:id:ponsuke_tarou:20200115101819p:plain
関数の内容を実装します。
import boto3

ec2 = boto3.client('ec2')

print('Loading function')

def get_name_tag_value(tags):
    """
    タグリストからNameタグの値を取得する.
    Parameters
    ----------
    tags
        辞書形式のタグリスト
    """
    name_tag_value = ''
    for tag in tags:
        if tag['Key'] == 'Name':
            name_tag_value = tag['Value']
            break
    return name_tag_value

def get_volumes_no_name_tag_and_attached():
    """
    [アタッチされていて][Nameタグが設定されていない]EBSボリュームを取得する.
    """
    volumes = ec2.describe_volumes(
        Filters=[
            {
                'Name': 'attachment.status',
                'Values': ['attached']
            }
        ]
    )['Volumes']

    volumes_no_name_tag = []
    for volume in volumes:
        if 'Tags' not in volume:
            # タグが設定されていない場合:Nameタグのないボリュームとする
            volumes_no_name_tag.append(volume)
        else:
            # タグが設定されている場合
            name_tag_value = get_name_tag_value(volume['Tags'])
            if name_tag_value == '':
                # Nameタグの値を取得できない場合:Nameタグのないボリュームとする
                volumes_no_name_tag.append(volume)
    return volumes_no_name_tag

def get_ec2_instance_name_tag_value(volume):
    """
    EBSボリュームにアタッチされているEC2インスタンスに設定されているNameタグの値を取得する.
    Parameters
    ----------
    volume
        対象となるEBSボリューム
    """
    # アタッチされているEC2インスタンスのIDを取得する
    instance_id = volume['Attachments'][0]['InstanceId']
    # EC2インスタンスのIDからインスタンスに設定されているタグ群を取得する
    tags = ec2.describe_instances(
        Filters=[
            {
                'Name': 'instance-id',
                'Values': [instance_id]
            }
        ]
    )['Reservations'][0]['Instances'][0]['Tags']
    # EC2インスタンスについているNameタグの値を取得する
    name_tag_value = get_name_tag_value(tags)
    return name_tag_value

def set_name_tag_for_volume(volume_id, name_tag_value):
    """
    EBSボリュームにNameタグを設定する.
    Parameters
    ----------
    volume_id
        NameタグをつけるEBSボリュームID
    name_tag_value
        EC2インスタンスについているNameタグの値
    """
    response = ec2.create_tags(
        Resources=[volume_id],
        Tags=[{'Key': 'Name', 'Value': name_tag_value}]
    )
    return 0

def lambda_handler(event, context):
    volumes = get_volumes_no_name_tag_and_attached()
    for volume in volumes:
        # EBSボリュームのIDを取得する.
        volume_id = volume['VolumeId']
        print(str(volume_id) + 'にはNameタグが付いていません')
        name_tag_value = get_ec2_instance_name_tag_value(volume)
        # EC2インスタンスにNameタグが設定されている場合に処理を実行する.
        if name_tag_value != '':
            set_name_tag_for_volume(volume_id, name_tag_value)
            print(str(volume_id) + 'のNameタグに「' + name_tag_value + '」とつけました')
    return 0

失敗したこと

describe_volumes()の実行権限がIAMロールになかった

[ERROR] ClientError: An error occurred (UnauthorizedOperation) when calling the DescribeVolumes operation: You are not authorized to perform this operation.
  • 事象 : describe_volumes()を実行したらエラーになった
  • 原因 : describe_volumes()を実行する権限がないから
    • エラーの時のIAMロールの権限設定 > 「ec2:DescribeInstances」しかない
...省略...
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:CreateTags",
        "logs:CreateLogStream",
        "logs:CreateLogGroup",
        "logs:PutLogEvents"
      ],
...省略...
...省略...
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeVolumes", <<< 追加
        "ec2:DescribeInstances",
        "ec2:CreateTags",
        "logs:CreateLogStream",
        "logs:CreateLogGroup",
        "logs:PutLogEvents"
      ],
...省略...

describe_volumesの引数の型が不正だった

[ERROR] ParamValidationError: Parameter validation failed:
Invalid type for parameter Filters[0].Values, value: Name, type: <class 'str'>, valid types: <class 'list'>, <class 'tuple'>
  • 事象 : Filtersに指定したValuesに文字(str)を指定して実行したらエラーになった
    # Nameタグが設定されていないEBSボリュームを取得する
    response = ec2.describe_volumes(
        Filters=[
            {
                'Name': 'tag:Name',
                'Values': ''
            }
        ]
    )['Volumes']
  • 原因 : Valuesにlistかtupleのシーケンス型を使っていないから
    • [ドキュメントにもValues (list) と書いてあります。]
  • 対応 : Valuesをlistで指定する
            {
                'Name': 'tag:Name',
                'Values': [''] <<<<<<<<<< 修正
            }