このインスタンスまだ必要?一定期間停止状態である EC2 インスタンスを作成時刻とともに一覧出力する

EC2 インスタンスの停止時刻は一部条件付きですが StateTransitionReason に記録されています。--query オプションでのフィルタにもそのまま使えるので助かります。

コンバンハ、千葉(幸)です。

数百台規模の EC2 インスタンスを利用されている環境を支援する機会がありました。複数システムが混在しており、かつ環境の歴史も長くなっていたため、不要なインスタンスの見直しをするアプローチに少し苦労します。

ひとまず以下の情報を AWS CLI でまとめて取得したいと考えました。

  • いつから停止しているか
  • 作成されたのはいつか
  • 最後に起動されたのはいつか

全量が出力されるとボリュームが大きいので、「365日以上停止状態にあるインスタンス」といった形でフィルタリングしたいです。

バチっと要件に合うフィルタリング方法はなかったのですが、それに近しいところまで実現できたのでその内容をご紹介します。

まとめ

  • OS からでなく API によって停止されたインスタンスはStateTransitionReasonに停止時刻が記録されている
    • --queryでのフィルタに使える
  • インスタンスの作成時刻そのものを取得できる項目はないが、以下がそれに近しい値になる
    • プライマリ ENI のアタッチ時刻
    • ルートボリュームのアタッチ時刻
  • LaunchTimeはインスタンスのローンチ(新規作成)だけでなく停止→起動の際にも値が更新される

今回用いたコマンド例は以下の通り。

# 事前に遡る日数を環境変数に格納:今回は365日前を指定
## GNU dateの場合
TIME=$(date -u -d '365 days ago' +"%Y-%m-%d %H:%M:%S %Z")
## BSD dateの場合
TIME=$(date -ju -v-365d +"%Y-%m-%d %H:%M:%S %Z")


# コマンドを実行
aws ec2 describe-instances \
  --query "Reservations[].Instances[?State.Name=='stopped' && StateTransitionReason < 'User initiated ($TIME)'].{
      InstanceId: InstanceId,
      Name: Tags[?Key=='Name']|[0].Value,
      StateTransitionReason: StateTransitionReason,
      LaunchTime: LaunchTime,
      ENIAttachTime: NetworkInterfaces[0].Attachment.AttachTime,
      EBSAttachTime: BlockDeviceMappings[0].Ebs.AttachTime
  }|[]" --output table

実行結果のイメージは以下です。

describe-instances_image

(テーブル形式で出力した場合、列が昇順に並べ替えられてしまうのが少し残念です。)

インスタンスがいつから停止しているかでフィルタリングする

EC2 インスタンスが停止状態にある場合、そのトリガーは大きく2種類あります。

  • AWS API で停止(マネジメントコンソールや AWS CLI による停止など)
  • OS から停止(OS 上でのshutdownコマンドの実行など)

このうち、前者により停止された場合、StateTransitionReasonにはUser initiated (2022-06-02 06:05:59 GMT)のように記録されます。

このメッセージ内のタイムスタンプを利用し、--queryオプションでフィルタリングが可能です。

# 遡る日数を指定
## GNU dateの場合
TIME=$(date -u -d '365 days ago' +"%Y-%m-%d %H:%M:%S %Z")
## BSD dateの場合
TIME=$(date -ju -v-365d +"%Y-%m-%d %H:%M:%S %Z")

$ aws ec2 describe-instances\
  --query "Reservations[].Instances[?State.Name=='stopped' && StateTransitionReason < 'User initiated ($TIME)']|[]"

環境に存在するインスタンスが OS 起因で停止されていた場合、今回の手法は残念ながらマッチしません。

インスタンスの作成時刻を推測する

一定期間停止状態にあるインスタンスが洗い出されたら、それがいつ作成されたものなのかを調べたくなります。

LaunchTimeというそれらしきパラメータがあるのでそれを見ればいいじゃないか、と思いますが、この値は「停止→起動」のタイミングでも更新されます。

Launch Time

そのため、「作成後に停止→起動したことがない」インスタンスを除いて、LaunchTimeを見ても作成時刻は確認できません。

代わりに、以下の値を確認することで作成時刻を推測します。

  • プライマリ ENI のアタッチ時刻
  • ルートボリュームのアタッチ時刻

これらは新規作成時刻とほぼ同じ値を持ち、更新される機会もほぼありません。(少なくともルートボリュームは置き換えができるのでゼロではない。。)

両方の値を見れば精度が高い情報が取れるだろう、ということでふたつとも取得しています。

AWS CLI リファレンスから引用した Output 例で言うと、以下のハイライト部が該当します。

{
    "Reservations": [
        {
            "Groups": [],
            "Instances": [
                {
                    "AmiLaunchIndex": 0,
                    "ImageId": "ami-0abcdef1234567890",
                    "InstanceId": "i-1234567890abcdef0",
                    "InstanceType": "t3.nano",
                    "KeyName": "my-key-pair",
                    "LaunchTime": "2022-11-15T10:48:59+00:00",
                    "Monitoring": {
                        "State": "disabled"
                    },
                    "Placement": {
                        "AvailabilityZone": "us-east-2a",
                        "GroupName": "",
                        "Tenancy": "default"
                    },
                    "PrivateDnsName": "ip-10-0-0-157.us-east-2.compute.internal",
                    "PrivateIpAddress": "10-0-0-157",
                    "ProductCodes": [],
                    "PublicDnsName": "ec2-34-253-223-13.us-east-2.compute.amazonaws.com",
                    "PublicIpAddress": "34.253.223.13",
                    "State": {
                        "Code": 16,
                        "Name": "running"
                    },
                    "StateTransitionReason": "",
                    "SubnetId": "subnet-04a636d18e83cfacb",
                    "VpcId": "vpc-1234567890abcdef0",
                    "Architecture": "x86_64",
                    "BlockDeviceMappings": [
                        {
                            "DeviceName": "/dev/xvda",
                            "Ebs": {
                                "AttachTime": "2022-11-15T10:49:00+00:00",
                                "DeleteOnTermination": true,
                                "Status": "attached",
                                "VolumeId": "vol-02e6ccdca7de29cf2"
                            }
                        }
                    ],
                    "ClientToken": "1234abcd-1234-abcd-1234-d46a8903e9bc",
                    "EbsOptimized": true,
                    "EnaSupport": true,
                    "Hypervisor": "xen",
                    "IamInstanceProfile": {
                        "Arn": "arn:aws:iam::111111111111:instance-profile/AmazonSSMRoleForInstancesQuickSetup",
                        "Id": "111111111111111111111"
                    },
                    "NetworkInterfaces": [
                        {
                            "Association": {
                                "IpOwnerId": "amazon",
                                "PublicDnsName": "ec2-34-253-223-13.us-east-2.compute.amazonaws.com",
                                "PublicIp": "34.253.223.13"
                            },
                            "Attachment": {
                                "AttachTime": "2022-11-15T10:48:59+00:00",
                                "AttachmentId": "eni-attach-1234567890abcdefg",
                                "DeleteOnTermination": true,
                                "DeviceIndex": 0,
                                "Status": "attached",
                                "NetworkCardIndex": 0
                            },
                            "Description": "",
                            "Groups": [
                                {
                                    "GroupName": "launch-wizard-146",
                                    "GroupId": "sg-1234567890abcdefg"
                                }
                            ],
                            "Ipv6Addresses": [],
                            "MacAddress": "00:11:22:33:44:55",
                            "NetworkInterfaceId": "eni-1234567890abcdefg",
                            "OwnerId": "104024344472",
                            "PrivateDnsName": "ip-10-0-0-157.us-east-2.compute.internal",
                            "PrivateIpAddress": "10-0-0-157",
                            "PrivateIpAddresses": [
                                {
                                    "Association": {
                                        "IpOwnerId": "amazon",
                                        "PublicDnsName": "ec2-34-253-223-13.us-east-2.compute.amazonaws.com",
                                        "PublicIp": "34.253.223.13"
                                    },
                                    "Primary": true,
                                    "PrivateDnsName": "ip-10-0-0-157.us-east-2.compute.internal",
                                    "PrivateIpAddress": "10-0-0-157"
                                }
                            ],
                            "SourceDestCheck": true,
                            "Status": "in-use",
                            "SubnetId": "subnet-1234567890abcdefg",
                            "VpcId": "vpc-1234567890abcdefg",
                            "InterfaceType": "interface"
                        }
                    ],
                    "RootDeviceName": "/dev/xvda",
                    "RootDeviceType": "ebs",
                    "SecurityGroups": [
                        {
                            "GroupName": "launch-wizard-146",
                            "GroupId": "sg-1234567890abcdefg"
                        }
                    ],
                    "SourceDestCheck": true,
                    "Tags": [
                        {
                            "Key": "Name",
                            "Value": "my-instance"
                        }
                    ],
                    "VirtualizationType": "hvm",
                    "CpuOptions": {
                        "CoreCount": 1,
                        "ThreadsPerCore": 2
                    },
                    "CapacityReservationSpecification": {
                        "CapacityReservationPreference": "open"
                    },
                    "HibernationOptions": {
                        "Configured": false
                    },
                    "MetadataOptions": {
                        "State": "applied",
                        "HttpTokens": "optional",
                        "HttpPutResponseHopLimit": 1,
                        "HttpEndpoint": "enabled",
                        "HttpProtocolIpv6": "disabled",
                        "InstanceMetadataTags": "enabled"
                    },
                    "EnclaveOptions": {
                        "Enabled": false
                    },
                    "PlatformDetails": "Linux/UNIX",
                    "UsageOperation": "RunInstances",
                    "UsageOperationUpdateTime": "2022-11-15T10:48:59+00:00",
                    "PrivateDnsNameOptions": {
                        "HostnameType": "ip-name",
                        "EnableResourceNameDnsARecord": true,
                        "EnableResourceNameDnsAAAARecord": false
                    },
                    "MaintenanceOptions": {
                        "AutoRecovery": "default"
                    }
                }
            ],
            "OwnerId": "111111111111",
            "ReservationId": "r-1234567890abcdefg"
        }
    ]
}

おまけ:細部のカスタマイズ

今回はテーブル形式で出力しましたが、量が多い場合はテキスト形式のほうが取り回しやすい場合もあるでしょう。--output textに変更するなどしてお好みの形で取り扱ってください。

また、User initiated (2022-06-02 06:05:59 GMT)のうちタイムスタンプだけが欲しい場合もあると思います。その場合は以下のようにパイプを挟んで sed で置換するなどで比較的簡単に対応できます。(テーブル形式の場合は枠が崩れてしまいますが。。)

$ echo "User initiated (2022-06-02 06:05:59 GMT)" | sed -E 's/User initiated \(([^)]+)\)/\1/'
2022-06-02 06:05:59 GMT

# GMTもいらない場合
$ echo "User initiated (2022-06-02 06:05:59 GMT)" | sed -E 's/User initiated \(([^)]+)\GMT)/\1/'
2022-06-02 06:05:59

終わりに

EC2 インスタンスを停止している期間によってフィルタリングしたい、その際には「いつから停止しているか」「いつ作成されたのか」「最後に起動したのはいつか」もあわせて取得したい、という話でした。

「EC2 インスタンスの停止時刻」は完全な形では取得できないのが少しネックではあります。OS からの操作で停止した場合にも AWS Config ではステータスの変化が記録されるので、そちらもあわせればより広いケースにマッチするかと思います。が、そこまでは手間をかけたくないな……ということで割り切りました。

棚卸しの際にご活用ください。

以上、 チバユキ (@batchicchi) がお送りしました。

参考