目次
2025年現在、AWSマネジメントコンソールおよびaws-cliにてAMI登録をする際。
AMIおよび紐づくSnapshotsに一括でタグ付けする事ができるようになっています。
(昔はAMI登録時に紐づくSnapshotにタグ付けできなかった)
AMI登録時にSnapshotも同時にタグ付けすることで、AMIとSnapshotのタグが乖離する場面は少なくなったように思いますが。
そうは言ってもあとから手作業でタグを調整した場合などでは、AMI側のタグを調整して、Snapshot側のタグ調整を忘れてしまうといったケースなどはあり得ます。
本記事では、AMIと紐づくSnapshotのタグが乖離していないかaws-cliでデータ取得して、そのデータをExcel(PowerQuery)に読み込んで加工しレポートを出力し確認してみます。
aws-cliでデータ取得
まずはExcelで加工するデータを取得します。
下記コマンドで2種類のファイルを取得します。
ec2_images_info.json:AMIのID(ImageId)と紐づくSnapshotIdとタグ情報の一覧(json)
ec2_snapshots_info.json:SnapshotIdとタグ情報の一覧(json)
# AMIと紐づくSnapshotIdとTagsの一覧(json)
# ec2_images_info.json を出力
aws ec2 describe-images --owners self --query 'Images[*].{ImageId:ImageId,SnapshotId:BlockDeviceMappings[*].Ebs.SnapshotId,Tags:Tags}' --output json > ec2_images_info.json
# Snapshotとタグの一覧(json)
# ec2_snapshots_info.json を出力
aws ec2 describe-snapshots --owner-ids self --query 'Snapshots[*].{SnapshotId:SnapshotId, Tags:Tags}' --output json > ec2_snapshots_info.json
今回は下記のように1台のEC2インスタンスに4台のEBSをアタッチしている状態でAMIを取得し、タグ情報をちぐはぐに登録して確認してみます。
Snapshot:1行目はtest-tag002の登録がない
Snapshot:2行目はtest-tag001の登録がない
Snapshot:3行目はAMIとタグ登録一致
Snapshot:4行目はtest-tag001とtest-tag002の登録はあるが、値が違う
今回、取得したjsonファイルは下記。
[
{
"ImageId": "ami-0ef2527e3570a9ae8",
"SnapshotId": [
"snap-085ea3f6e9e49767c",
"snap-02e222d282f5717f3",
"snap-09fb9a4754886e367",
"snap-03a3a89a30c8abb0c"
],
"Tags": [
{
"Key": "Name",
"Value": "bxtest001"
},
{
"Key": "test-tag001",
"Value": "hello"
},
{
"Key": "test-tag002",
"Value": "world"
}
]
}
]
[
{
"SnapshotId": "snap-03a3a89a30c8abb0c",
"Tags": [
{
"Key": "test-tag002",
"Value": "world"
},
{
"Key": "Name",
"Value": "bxtest001"
}
]
},
{
"SnapshotId": "snap-09fb9a4754886e367",
"Tags": [
{
"Key": "test-tag001",
"Value": "hello"
},
{
"Key": "test-tag002",
"Value": "world"
},
{
"Key": "Name",
"Value": "bxtest001"
}
]
},
{
"SnapshotId": "snap-085ea3f6e9e49767c",
"Tags": [
{
"Key": "Name",
"Value": "bxtest001"
},
{
"Key": "test-tag001",
"Value": "hello"
}
]
},
{
"SnapshotId": "snap-02e222d282f5717f3",
"Tags": [
{
"Key": "Name",
"Value": "bxtest001"
},
{
"Key": "test-tag001",
"Value": "foo"
},
{
"Key": "test-tag002",
"Value": "bar"
}
]
}
]
作成したレポートについて
今回作成したレポートでは下記のような結果が出力されます。
このレポートからは1行目と2行目ではSnapshotId:snap-02e222d282f5717f3 では Snapshots側には{test-tag001,foo} , {test-tag002,bar}が登録されているがAMI側には存在しない。
3行目と4行目ではSnapshotId:snap-02e222d282f5717f3 では AMI側には{test-tag001,hello} , {test-tag002,world}が登録されているがSnapshot側には存在しない。
5行目ではSnapshotId:snap-03a3a89a30c8abb0cでは AMI側には{test-tag001,hello} が登録されているがSnapshot側には存在しない。
6行目ではSnapshotId:snap-085ea3f6e9e49767cでは AMI側には {test-tag002,world}が登録されているがSnapshot側には存在しない。
といった事を読み取る事ができます。
PowerQuery
下記のような感じでPowerQuery作成しています。
// ec2_images_info_path
"ec2_images_info.json格納先ファイルパスに置き換え" meta [IsParameterQuery=true, Type="Any", IsParameterQueryRequired=true]
// ec2_images_info
let
Source = Json.Document(File.Contents(ec2_images_info_path)),
#"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Renamed Columns" = Table.RenameColumns(#"Converted to Table",{{"Column1", "images"}}),
#"Expanded images" = Table.ExpandRecordColumn(#"Renamed Columns", "images", {"ImageId", "SnapshotId", "Tags"}, {"images.ImageId", "images.SnapshotId", "images.Tags"}),
#"Expanded SnapshotId" = Table.ExpandListColumn(#"Expanded images", "images.SnapshotId"),
#"Expanded Tags" = Table.ExpandListColumn(#"Expanded SnapshotId", "images.Tags"),
#"Expanded KeyValue" = Table.ExpandRecordColumn(#"Expanded Tags", "images.Tags", {"Key", "Value"}, {"images.Tags.Key", "images.Tags.Value"}),
#"Changed Type" = Table.TransformColumnTypes(#"Expanded KeyValue",{{"images.ImageId", type text}, {"images.SnapshotId", type text}, {"images.Tags.Key", type text}, {"images.Tags.Value", type text}})
in
#"Changed Type"
// check_report
let
Source = ec2_images_info,
#"Merged Queries" = Table.NestedJoin(Source, {"images.SnapshotId", "images.Tags.Key", "images.Tags.Value"}, ec2_snapshots_info, {"snapshots.SnapshotId", "snapshots.Tags.Key", "snapshots.Tags.Value"}, "ec2_snapshots_info", JoinKind.FullOuter),
#"Expanded snapshots" = Table.ExpandTableColumn(#"Merged Queries", "ec2_snapshots_info", {"snapshots.SnapshotId", "snapshots.Tags.Key", "snapshots.Tags.Value"}, {"snapshots.SnapshotId", "snapshots.Tags.Key", "snapshots.Tags.Value"}),
#"Added Conditional Column" = Table.AddColumn(#"Expanded snapshots", "判定", each if [images.Tags.Key] <> [snapshots.Tags.Key] then "NG" else if [images.Tags.Value] <> [snapshots.Tags.Value] then "NG" else "OK"),
#"Filtered Rows" = Table.SelectRows(#"Added Conditional Column", each ([判定] = "NG")),
#"Add Column result.Image" = Table.AddColumn(#"Filtered Rows", "result.ImageId", each if [images.ImageId] = null then FxGetImageId([snapshots.SnapshotId]) else [images.ImageId]),
#"Add Column result.SnapshotId" = Table.AddColumn(#"Add Column result.Image", "result.SnapshotId", each if [images.SnapshotId] = null then [snapshots.SnapshotId] else [images.SnapshotId]),
#"Removed Other Columns" = Table.SelectColumns(#"Add Column result.SnapshotId",{"images.Tags.Key", "images.Tags.Value", "snapshots.Tags.Key", "snapshots.Tags.Value", "判定", "result.ImageId", "result.SnapshotId"}),
#"Reordered Columns" = Table.ReorderColumns(#"Removed Other Columns",{"判定", "result.ImageId", "result.SnapshotId", "images.Tags.Key", "images.Tags.Value", "snapshots.Tags.Key", "snapshots.Tags.Value"}),
#"Sorted Rows" = Table.Sort(#"Reordered Columns",{{"result.ImageId", Order.Ascending}, {"result.SnapshotId", Order.Ascending}, {"snapshots.Tags.Key", Order.Ascending}})
in
#"Sorted Rows"
// ec2_snapshots_info_path
"換えec2_snapshots_info.json格納先ファイルパスに置き換え" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true]
// ec2_snapshots_info
let
Source = Json.Document(File.Contents(ec2_snapshots_info_path)),
#"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"Renamed Columns" = Table.RenameColumns(#"Converted to Table",{{"Column1", "snapshots"}}),
#"Expanded snapshots" = Table.ExpandRecordColumn(#"Renamed Columns", "snapshots", {"SnapshotId", "Tags"}, {"snapshots.SnapshotId", "snapshots.Tags"}),
#"Expanded Tags" = Table.ExpandListColumn(#"Expanded snapshots", "snapshots.Tags"),
#"Expanded KeyValue" = Table.ExpandRecordColumn(#"Expanded Tags", "snapshots.Tags", {"Key", "Value"}, {"snapshots.Tags.Key", "snapshots.Tags.Value"}),
#"Changed Type" = Table.TransformColumnTypes(#"Expanded KeyValue",{{"snapshots.SnapshotId", type text}, {"snapshots.Tags.Key", type text}, {"snapshots.Tags.Value", type text}})
in
#"Changed Type"
// FxGetImageId
let
ソース = (SnapshotId as text) => let
Source = SnapshotId,
SelectRows = Table.SelectRows(ec2_images_info, each [images.SnapshotId] = Source),
ImageId = try SelectRows[images.ImageId]{0} otherwise null
in
ImageId
in
ソース
チェックレポートの仕組みについてメモ書き
- 取得したec2_images_info.jsonとec2_snapshots_info.jsonを項目(SnapshotsId、Tags.Key、tags.Value)で完全外部でマージ。
- ec2_snapshots_info.json側のタグ情報とec2_images_info.json側のタグ情報が一致していたらOK 、一致していなかったら判定列にNG。
- NG行のみフィルタ
- ec2_snapshots_info.json側にしかないタグレコードではImageIdがもともとないので、ec2_images_info.json側にImageIdがあればこれを採用、なければ作成した関数FxGetImageIdでImageIdを取得してresult.ImageId項目に結果出力
- AMI側とSnapshot側でどちらか片方にしか登録されていないタグの場合があるので、SnapshotIdは存在する方から取得してresult.SnapshotId項目に結果出力
odcファイル
odcファイルとしてレポートをエクスポートした物は下記。
こちらを読み込み、適宜ファイル格納パスを変更すれば利用できる。
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta http-equiv=Content-Type content="text/x-ms-odc; charset=utf-8">
<meta name=ProgId content=ODC.Database>
<meta name=SourceType content=OLEDB>
<title>クエリ - check_report</title>
<xml id=docprops><o:DocumentProperties
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns="http://www.w3.org/TR/REC-html40">
<o:Description>ブック内の 'check_report' クエリへの接続です。</o:Description>
<o:Name>クエリ - check_report</o:Name>
</o:DocumentProperties>
</xml><xml id=msodc><odc:OfficeDataConnection
xmlns:odc="urn:schemas-microsoft-com:office:odc"
xmlns="http://www.w3.org/TR/REC-html40">
<odc:PowerQueryConnection odc:Type="OLEDB">
<odc:ConnectionString>Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=check_report;Extended Properties=""</odc:ConnectionString>
<odc:CommandType>SQL</odc:CommandType>
<odc:CommandText>SELECT * FROM [check_report]</odc:CommandText>
</odc:PowerQueryConnection>
<odc:PowerQueryMashupData><Mashup xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/DataMashup"><Client>EXCEL</Client><Version>2.140.405.0</Version><MinVersion>2.21.0.0</MinVersion><Culture>ja-JP</Culture><SafeCombine>false</SafeCombine><Items><Query Name="ec2_images_info_path"><Formula><![CDATA["ec2_images_info.jsonファイルパス" meta [IsParameterQuery=true, Type="Any", IsParameterQueryRequired=true]]]></Formula><IsParameterQuery xsi:nil="true" /><IsDirectQuery xsi:nil="true" /></Query><Query Name="ec2_images_info"><Formula><![CDATA[let Source = Json.Document(File.Contents(ec2_images_info_path)), #"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), #"Renamed Columns" = Table.RenameColumns(#"Converted to Table",{{"Column1", "images"}}), #"Expanded images" = Table.ExpandRecordColumn(#"Renamed Columns", "images", {"ImageId", "SnapshotId", "Tags"}, {"images.ImageId", "images.SnapshotId", "images.Tags"}), #"Expanded SnapshotId" = Table.ExpandListColumn(#"Expanded images", "images.SnapshotId"), #"Expanded Tags" = Table.ExpandListColumn(#"Expanded SnapshotId", "images.Tags"), #"Expanded KeyValue" = Table.ExpandRecordColumn(#"Expanded Tags", "images.Tags", {"Key", "Value"}, {"images.Tags.Key", "images.Tags.Value"}), #"Changed Type" = Table.TransformColumnTypes(#"Expanded KeyValue",{{"images.ImageId", type text}, {"images.SnapshotId", type text}, {"images.Tags.Key", type text}, {"images.Tags.Value", type text}}) in #"Changed Type"]]></Formula><IsParameterQuery xsi:nil="true" /><IsDirectQuery xsi:nil="true" /></Query><Query Name="check_report"><Formula><![CDATA[let Source = ec2_images_info, #"Merged Queries" = Table.NestedJoin(Source, {"images.SnapshotId", "images.Tags.Key", "images.Tags.Value"}, ec2_snapshots_info, {"snapshots.SnapshotId", "snapshots.Tags.Key", "snapshots.Tags.Value"}, "ec2_snapshots_info", JoinKind.FullOuter), #"Expanded snapshots" = Table.ExpandTableColumn(#"Merged Queries", "ec2_snapshots_info", {"snapshots.SnapshotId", "snapshots.Tags.Key", "snapshots.Tags.Value"}, {"snapshots.SnapshotId", "snapshots.Tags.Key", "snapshots.Tags.Value"}), #"Added Conditional Column" = Table.AddColumn(#"Expanded snapshots", "判定", each if [images.Tags.Key] <> [snapshots.Tags.Key] then "NG" else if [images.Tags.Value] <> [snapshots.Tags.Value] then "NG" else "OK"), #"Filtered Rows" = Table.SelectRows(#"Added Conditional Column", each ([判定] = "NG")), #"Add Column result.Image" = Table.AddColumn(#"Filtered Rows", "result.ImageId", each if [images.ImageId] = null then FxGetImageId([snapshots.SnapshotId]) else [images.ImageId]), #"Add Column result.SnapshotId" = Table.AddColumn(#"Add Column result.Image", "result.SnapshotId", each if [images.SnapshotId] = null then [snapshots.SnapshotId] else [images.SnapshotId]), #"Removed Other Columns" = Table.SelectColumns(#"Add Column result.SnapshotId",{"images.Tags.Key", "images.Tags.Value", "snapshots.Tags.Key", "snapshots.Tags.Value", "判定", "result.ImageId", "result.SnapshotId"}), #"Reordered Columns" = Table.ReorderColumns(#"Removed Other Columns",{"判定", "result.ImageId", "result.SnapshotId", "images.Tags.Key", "images.Tags.Value", "snapshots.Tags.Key", "snapshots.Tags.Value"}), #"Sorted Rows" = Table.Sort(#"Reordered Columns",{{"result.ImageId", Order.Ascending}, {"result.SnapshotId", Order.Ascending}, {"snapshots.Tags.Key", Order.Ascending}}) in #"Sorted Rows"]]></Formula><IsParameterQuery xsi:nil="true" /><IsDirectQuery xsi:nil="true" /></Query><Query Name="ec2_snapshots_info_path"><Formula><![CDATA["ec2_snapshots_info.jsonファイルパス" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true]]]></Formula><IsParameterQuery xsi:nil="true" /><IsDirectQuery xsi:nil="true" /></Query><Query Name="ec2_snapshots_info"><Formula><![CDATA[let Source = Json.Document(File.Contents(ec2_snapshots_info_path)), #"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), #"Renamed Columns" = Table.RenameColumns(#"Converted to Table",{{"Column1", "snapshots"}}), #"Expanded snapshots" = Table.ExpandRecordColumn(#"Renamed Columns", "snapshots", {"SnapshotId", "Tags"}, {"snapshots.SnapshotId", "snapshots.Tags"}), #"Expanded Tags" = Table.ExpandListColumn(#"Expanded snapshots", "snapshots.Tags"), #"Expanded KeyValue" = Table.ExpandRecordColumn(#"Expanded Tags", "snapshots.Tags", {"Key", "Value"}, {"snapshots.Tags.Key", "snapshots.Tags.Value"}), #"Changed Type" = Table.TransformColumnTypes(#"Expanded KeyValue",{{"snapshots.SnapshotId", type text}, {"snapshots.Tags.Key", type text}, {"snapshots.Tags.Value", type text}}) in #"Changed Type"]]></Formula><IsParameterQuery xsi:nil="true" /><IsDirectQuery xsi:nil="true" /></Query><Query Name="FxGetImageId"><Formula><![CDATA[let ソース = (SnapshotId as text) => let Source = SnapshotId, SelectRows = Table.SelectRows(ec2_images_info, each [images.SnapshotId] = Source), ImageId = try SelectRows[images.ImageId]{0} otherwise null in ImageId in ソース]]></Formula><IsParameterQuery xsi:nil="true" /><IsDirectQuery xsi:nil="true" /></Query></Items></Mashup></odc:PowerQueryMashupData>
</odc:OfficeDataConnection>
</xml>
<style>
<!--
.ODCDataSource
{
behavior: url(dataconn.htc);
}
-->
</style>
</head>
<body onload='init()' scroll=no leftmargin=0 topmargin=0 rightmargin=0 style=''>
<table style='' cellpadding=0 cellspacing=0 width='100%'>
<tr>
<td id=tdName style=''>
</td>
<td id=tdTableDropdown style=''>
</td>
</tr>
<tr>
<td id=tdDesc colspan='2' style=''>
</td>
</tr>
<tr>
<td colspan='2' style=''>
<div id='pt' style='' class=''></div>
</td>
</tr>
</table>
<script language='javascript'>
function init() {
var sName, sDescription;
var i, j;
try {
sName = unescape(location.href)
i = sName.lastIndexOf(".")
if (i>=0) { sName = sName.substring(1, i); }
i = sName.lastIndexOf("/")
if (i>=0) { sName = sName.substring(i+1, sName.length); }
document.title = sName;
document.getElementById("tdName").innerText = sName;
sDescription = document.getElementById("docprops").innerHTML;
i = sDescription.indexOf("escription>")
if (i>=0) { j = sDescription.indexOf("escription>", i + 11); }
if (i>=0 && j >= 0) {
j = sDescription.lastIndexOf("</", j);
if (j>=0) {
sDescription = sDescription.substring(i+11, j);
if (sDescription != "") {
document.getElementById("tdDesc").style.fontSize="x-small";
document.getElementById("tdDesc").innerHTML = sDescription;
}
}
}
}
catch(e) {
}
}
</script>
</body>
</html>
総評
AMIとSnapshotに同様のタグを付与して管理しているケースでは、今回作成したレポートで泣き別れてしまった状況を確認できます。
ただしAMI側には登録しないが、Snapshotだけには追加でタグ(デバイス毎の用途やドライブ名やetcetc)を追加している場合は、こういった部分も今回作成したレポートには出てきてしまうので注意は必要です。
作成したレポートがタグ情報の調整に活用できれば幸いです。