Upgrade to Pro — share decks privately, control downloads, hide ads and more …

不要なリソースを自動で定期的に整理する方法 ~Sandboxアカウントのコストを削減しよう!~

amixedcolor
October 12, 2024

不要なリソースを自動で定期的に整理する方法 ~Sandboxアカウントのコストを削減しよう!~

JAWS FESTA 2024 in 広島 にて登壇した内容です!
実際のソースコードをゆっくりご確認いただけます。

◼︎登壇情報
Track:C
13:35 ~ 13:55

◼︎セッション概要
本セッションでは、Sandboxアカウントに残ってしまいがちな不要なリソースを、定期的に自動で削除するソリューションを説明します。
具体的には、(検索には引っかからないが使うならこれしかないというとても便利な)「Resource Exproler API」、タグ付けを用いたリソースを整理する仕組み、ソリューションの組織内における運用方法を紹介します。
本セッションを聴講することにより、アカウントにおけるコスト削減を自動化できるようになります。
想定する聴衆は、アカウントのコスト削減に取り組みたい方、今は手動で取り組んでいて自動化を試みたい方が中心です。

◼︎登壇者
保 龍児 (amixedcolor/エイミ)
株式会社Relic所属、インフラエンジニア。 新卒2年目、本年よりプラットフォームエンジニアリングに従事。 普段は主にインフラ面での技術相談を受けたり、各種脆弱性への対応を推進したり、共通基盤となるインフラを作成したりしています!

◼︎セッションカテゴリ
セキュリティ以外のガバナンス系

amixedcolor

October 12, 2024
Tweet

More Decks by amixedcolor

Other Decks in Technology

Transcript

  1. 4 • アカウントの 不要リソースを削除 したい • お試しのリソースがたくさん作成されるようなアカウント ex) Sandboxアカウント・Dev環境アカウント •

    (複数アカウントにまたがる不要リソースを削除したい) • 不要なリソースの削除で コストを削減 したい • 不要なリソースを 自動で 削除する方法を知りたい • 不要なリソースの削除を できる ようになりたい • 具体的なソースコード・ロジック・API • 具体的な組織内での運用方法 想定する聴衆
  2. 6 • X(旧Twitter)のDM • Xにて@メンション • 私の発表時間帯付近の #jawsfesta2024 #jawsfesta2024_c を付けた投稿

    • できる限り全部追います!! • この後の休憩時間・会場で見かけたとき • ぜひ話しかけてください!! • 懇親会などなど全部参加します! • もちろんこちらでもぜひ!! ご質問・ご意見お待ちしております!! @amixedcolor エイミ
  3. 9 • AWS SAM (Serverless Application Model) • EventBridge •

    Lambda • Javascript • Node.js • Jest • IAM • Slack App • Incoming Webhook 使用技術は?
  4. 10 • AWSコンソールほぼ触ったことない • SAMと言われてピンとこない • Lambdaと言われてピンとこない • AWS SDK使ったコード書いたことない

    • 新卒2年目 • Laravelアプリケーション開発PJに配属直後から従事 • AWSとプラットフォームエンジニアリングに興味を持ち異動 • Python・C・JS・PHPを中心に触ったことがある 実装開始時点のスキルは?
  5. 13 • 開発者が自由に使える環境、サンドボックス環境 • リソースがどんどん作られる • 放置するとコストがどんどん増えていく • 開発者で管理して削除して欲しい •

    ルールを作る • 「1日で削除 Must!」 • 「タグ付けの徹底 Must!」 • 「 2日以上に渡って何らかのリソースを利用する場合、予算を出して 上長に確認するのをお忘れなく」 開発の経緯
  6. 15 • 何日も削除されない 削除Must! • タグ付けがされたリソースはほとんどない タグ付けMust! • 管理コストは日に日に増えていく •

    全てのエンジニアがサンドボックスを利用可能 • 会社が大きくなってエンジニアも増加 • 適切に削除してくれる人は少ない ルールを作った結果……機能しない!
  7. 19 ポイント • 都度消して良いか許可を取るのではなく、消すなと言われていなけれ ば消す →自動で消せるように • 消す場合でも事前に消す対象を通知する →納得感を持ってもらう 具体的には?

    • Deletion Policyタグを設定していなければ消す • Deletion Policyタグで「YYYYMM」を設定した月までは残す • YYYY = 年、MM = 月(ゼロ埋め) • フォーマットが沿っていない場合は見直すようARNを通知する • 消す2週間前時点で、消す予定のリソースのARNを通知する リソースを消せるようにする
  8. 39 • Resource Explorer APIの概要と利用方法 • タグ付けを用いたリソースの判別ロジック • 削除対象のものを判別 •

    書式が沿っていないものを判別 • 削除関数の実装方法と使い方 • リソースタイプごとに実装すること • 自分を削除しないこと • テストの方法 • 自動テスト • 手動テスト 自分で実現できるようになるために(1/2)
  9. 41 • Resource Explorer APIの概要と利用方法 • タグ付けを用いたリソースの判別ロジック • 削除対象のものを判別 •

    書式が沿っていないものを判別 • 削除関数の実装方法と使い方 • リソースタイプごとに実装すること • 自分を削除しないこと • テストの方法 • 自動テスト • 手動テスト 自分で実現できるようになるために(1/2)
  10. 42 • そもそもResource Explorerとは? • リソース検索および検出サービス • 下記の項目などでフィルタできる • アカウントID

    • リージョン • リソースタイプ • サービス • リージョンを跨いだ検索 ができる • 「aws リソース一覧 api」で調べると? • Resource Groups Tagging APIが出てくるが、一度以上タグがついた リソースしか取得できない Resource Explorer APIの概要(1/2)
  11. 43 • Resourceの一覧を取得するならResource Explorer • APIはあるのか?→ある!メリットが大きい • 複数のサービスを 1つのAPIで 扱える

    • 簡単に 全リージョンのリソースを 一覧で取得できる • 自動整理する上で特定のリソースタイプに絞る • 料金がかかりがちなリソースタイプは絞られる • リソースタイプについて • Resource Explorer で検索できるリソースタイプ(基本的にほぼ全て検索可能) • https://docs.aws.amazon.com/ja_jp/resource- explorer/latest/userguide/supported-resource-types.html • 他のリソースタイプとして表示されるリソースタイプもある Resource Explorer APIについて(2/2)
  12. 46 Resource Explorer API: Searchの返すレスポンス(2/3) … "Resources": [ { "Arn":

    "string", "LastReportedAt": "string", "OwningAccountId": "string", "Properties": [ 次のページで説明 ], "Region": "string", "ResourceType": "string", "Service": "string" } ], "ViewArn": "string" } Resourcesが主に使用する部分
  13. 47 Resource Explorer API: Searchの返すレスポンス(3/3) … "Properties": [ { "Data":

    [ { "Key": ”string", "Value": "string" } ], "LastReportedAt": "string", "Name": "string" } ] … この部分はリソースタイプによって違うが、 今回利用したリソースタイプでは全て、タグ のKeyとValueをもつ辞書のリストだった
  14. 50 ソースコードを読み解いてできるようになる 使用技術 • JavaScript • Node.js • Jest 注意

    • 完全に一致したコードではなく抜粋しています • 読みやすさのための改変があります • 説明はAWSから離れてしまうためほとんどしません • もちろんスライドは公開するので、ご覧いただくか、お気軽にご質問ください! export const HelloJawsFesta2024InHiroshimaHandler = async (event) => { return { 'statusCode': 200, 'body': 'Hello JAWS FESTA 2024 in Hiroshima!' }; }
  15. 51 リソースタイプを絞って変数定義する const targetResources = [ 'apigateway:restapis', 'ec2:elastic-ip', 'ec2:natgateway', 'ec2:instance',

    'ec2:volume', // EBS volume 'ec2:vpc-endpoint', 'elasticache:cluster', 'elasticache:replicationgroup', 'elasticloadbalancing:loadbalancer', 'elasticloadbalancing:loadbalancer/app', 'elasticloadbalancing:loadbalancer/net', 'lambda:function', 'rds:cluster', // RDS DB Cluster 'rds:db', // RDS DB Instance ];
  16. 52 Resource Explorerを使う import { ResourceExplorer2Client, SearchCommand } from "@aws-sdk/client-resource-explorer-2";

    const params = { QueryString: "resourcetype:" + targetResource, } const command = new SearchCommand(params); let resources = []; do { const response = await client.send(command); resources = resources.concat(response.Resources); params.NextToken = response.NextToken; await new Promise(resolve => setTimeout(resolve, 400)); } while (params.NextToken);
  17. 53 • Resource Explorer APIの概要と利用方法 • タグ付けを用いたリソースの判別ロジック • 削除対象のものを判別 •

    書式が沿っていないものを判別 • 削除関数の実装方法と使い方 • リソースタイプごとに実装すること • 自分を削除しないこと • テストの方法 • 自動テスト • 手動テスト 自分で実現できるようになるために(1/2)
  18. 54 削除対象のものを判別するロジック 削除対象 である タグを 持っていない 年月が過去 対象のリソース1つにおける、 全てのKeyが「Deletion Policy」と異なること

    タグを 持っている 書式が正しい 指定した月の 翌月頭が 今以前 対象のリソース1つにおける、 いずれかのKeyが「Deletion Policy」と一致すること YYYYMM 6文字 number型 である 01 - 12 の間 OR YYYYMM その月いっぱいは保持 翌月頭 now() <= AND
  19. 56 タグを持っていない const resourcesWithoutDeletionPolicy = resources.filter((resource) => { return resource.Properties.every((jsonElement)

    => { return jsonElement.Data.every((element) => { return element.Key !== "Deletion Policy"; }); }); });
  20. 58 年月が過去: タグを持っている const resourcesWithDeletionPolicy = resources.filter((resource) => { return

    resource.Properties.some((jsonElement) => { return jsonElement.Data.some((element) => { return element.Key === "Deletion Policy"; }); }); });
  21. 59 年月が過去: 書式が正しい export const isValidDate = (value) => {

    const is6digit = value.length === 6; if (!is6digit) { return false; } const isNumber = typeof parseInt(value) === "number"; if (!isNumber) { return false; } const MM = value.slice(4, 6); const Month = parseInt(MM); const isValidMonth = 1 <= Month && Month <= 12; if (!isValidMonth) { return false; } return true; }
  22. 60 年月が過去: 指定した月いっぱい <=now() const YYYY = dateFormattedByYYYYMM.slice(0, 4); const

    MM = dateFormattedByYYYYMM.slice(4, 6); // 日本時間のその月の初日の0時0分とする const dateFormattedByISO8601 = YYYY + "-" + MM + "-01T00:00:00.001+09:00"; const parsedDateAsBeginOfMonth = new Date(Date.parse(dateFormattedByISO8601)); const parsedDateAsBeginOfNextMonth = new Date(parsedDateAsBeginOfMonth.setMonth( parsedDateAsBeginOfMonth.getMonth() + 1 )); return parsedDateAsBeginOfNextMonth <= now;
  23. 61 • Resource Explorer APIの概要と利用方法 • タグ付けを用いたリソースの判別ロジック • 削除対象のものを判別 •

    書式が沿っていないものを判別 • 削除関数の実装方法と使い方 • リソースタイプごとに実装すること • 自分を削除しないこと • テストの方法 • 自動テスト • 手動テスト 自分で実現できるようになるために(1/2)
  24. 62 書式が沿っていないものを判別するロジック 書式が 沿って いない タグを 持っている 書式が 正しくない 書式が正しい

    対象のリソース1つにおける、 いずれかのKeyが「Deletion Policy」と一致すること YYYYMM 6文字 number型 である 01 - 12 の間 AND NOT
  25. 64 書式が沿っていない: タグを持っている const resourcesWithDeletionPolicy = resources.filter((resource) => { return

    resource.Properties.some((jsonElement) => { return jsonElement.Data.some((element) => { return element.Key === "Deletion Policy"; }); }); });
  26. 65 書式が沿っていない: NOT (書式が正しい) 書式が正しい export const isValidDate = (value)

    => { const is6digit = value.length === 6; if (!is6digit) { return false; } const isNumber = typeof parseInt(value) === "number"; if (!isNumber) { return false; } const MM = value.slice(4, 6); const Month = parseInt(MM); const isValidMonth = 1 <= Month && Month <= 12; if (!isValidMonth) { return false; } return true; } NOT (書式が正しい) return !isValidDate(element.Value)
  27. 66 • Resource Explorer APIの概要と利用方法 • タグ付けを用いたリソースの判別ロジック • 削除対象のものを判別 •

    書式が沿っていないものを判別 • 削除関数の実装方法と使い方 • リソースタイプごとに実装すること • 自分を削除しないこと • テストの方法 • 自動テスト • 手動テスト 自分で実現できるようになるために(1/2)
  28. 67 1. リソースタイプごとに実装する • AWSにはARNなどでリソースを一元的に削除できるAPIはない (現時点で) • 各リソースのAPIには削除メソッドが用意されているのでそれを使う 2. 削除ハンドラから使い分ける

    • サービスとリソースタイプで対応する関数を変数で切り替えて使う 3. エラーハンドリングする • どれかで失敗しても処理を続ける • エラーが出ても、削除対象のリソース全ての削除を試みる • エラー時に一定の時間をおいて繰り返す • 削除順やレート制限など、一時的なエラーであることがある リソースタイプごとに実装すること
  29. 69 リソースタイプごとに実装する: EC2インスタンス削除の例 const extractInstanceId = (arn) => { const

    parts = arn.split('/'); const instanceId = parts[parts.length - 1]; return instanceId; } export const deleteEc2Instance = async (resource) => { const client = new EC2Client({ region: resource.Region }); const command = new TerminateInstancesCommand({ InstanceIds: [extractInstanceId(resource.Arn)] }); try { const response = await client.send(command); return response; } catch (error) { throw error; } };
  30. 71 削除ハンドラから使い分ける: 実装した関数を辞書の値にする const implementedFunctions = { "apigateway": { "restapis":

    deleteApigatewayRestapis, }, "ec2": { "elastic-ip": deleteEc2ElasticIp, "natgateway": deleteEc2NatGateway, "instance": deleteEc2Instance, "volume": deleteEc2EbsVolume, "vpc-endpoint": deleteVpcEndpoint, }, "elasticache": { "cluster": deleteElasticacheCluster, // Memcached cluster "replicationgroup": deleteElasticacheReplicationGroup, // Redis cluster }, … }
  31. 72 削除ハンドラから使い分ける: 対応する関数を変数に入れる Object.keys(uniqueResourceTypesByService).forEach( (service) => { uniqueResourceTypesByService[service].forEach( (resourceType) =>

    { if (isDeletionImplemented(service, resourceType)) { const deletionFunction = implementedFunctions[targetService][targetResourceType]; groupedResources[service][resourceType].forEach( (resource) => { …(後述:どれかで失敗しても処理を続ける) } ) } } ); } );
  32. 74 エラーハンドリング: どれかで失敗しても処理を続ける const promises = []; …(先述: 対応する関数を変数に入れる) promises.push(

    …(後述:エラー時に一定の時間をおいて繰り返す) ); const responses = await Promise.allSettled(promises); const fulfilledResponses = responses.filter((response) => {return response.status === "fulfilled";}); console.log(fulfilledResponses); const rejectedResponses = responses.filter((response) => {return response.status === "rejected";}); if (rejectedResponses.length > 0) { console.error(rejectedResponses); throw new Error(…); }
  33. 75 エラーハンドリング: エラー時に一定の時間をおいて繰り返す …(先述:どれかで失敗しても処理を続ける) promises.push(retry(deletionFunction, resource, 5)); … const retry

    = async (deletionFunction, resource, maxRetryAttempts) => { let attempt = 0; while (attempt < maxRetryAttempts) { try { return await deletionFunction(resource); } catch (error) { attempt++; if (attempt >= maxRetryAttempts) { throw error; } await new Promise(resolve => setTimeout(resolve, 1000)); } } };
  34. 76 • Resource Explorer APIの概要と利用方法 • タグ付けを用いたリソースの判別ロジック • 削除対象のものを判別 •

    書式が沿っていないものを判別 • 削除関数の実装方法と使い方 • リソースタイプごとに実装すること • 自分を削除しないこと • テストの方法 • 自動テスト • 手動テスト 自分で実現できるようになるために(1/2)
  35. 77 • Deletion Policyタグをつけておく • 削除されないかつ不正フォーマットで通知されないように • 例えば? • YYYYMMを999912にする

    • Retainを入れておいてそれは不正フォーマットではないことにする 自分を削除しない
  36. 78 • Resource Explorer APIの概要と利用方法 • タグ付けを用いたリソースの判別ロジック • 削除対象のものを判別 •

    書式が沿っていないものを判別 • 削除関数の実装方法と使い方 • リソースタイプごとに実装すること • 自分を削除しないこと • テストの方法 • 自動テスト • 手動テスト 自分で実現できるようになるために(1/2)
  37. 79 テスト対象 • 削除対象のものの判別関数 • 書式が沿っていないものの判別関数 • それらに必要な個別で切り出した関数 テスト方法 •

    Resource Explorer APIの返すJSON形式をもとに、入力と期待出力の JSONを作成 • ユニットテストでそれぞれの関数の返り値が期待出力に合っているか をassert 自動テスト
  38. 80 自動テストの例(用意するJSON: タグがないものの例) { "resources": [ { "Arn": "arn:example-not-having-any-properties-because-there-is-not-any-tags", "LastReportedAt":

    "2024-01-01T00:00:00.000Z", "OwningAccountId": ”012345678901", "Properties": [], "Region": "ap-northeast-1", "ResourceType": "example:example", "Service": "example" }, { "Arn": "arn:example-has-other-resource-data-but-no-deletion-policy-tag", … ] }
  39. 81 自動テストの例(実際のユニットテスト) describe('Test for separate-by-deletion-policy-havingness', function () { it('Deletion Policyキーの有無で判別し2つに分割できること',

    () => { const [ resourcesWithDeletionPolicyKey, resourcesWithoutDeletionPolicyKey ] = separateByDeletionPolicyHavingness( resourcesWithAllExamples.resources ); expect(resourcesWithDeletionPolicyKey).toEqual(expectedResourcesWithDeletionPolicyKe y.resources); expect(resourcesWithoutDeletionPolicyKey).toEqual(expectedResourcesWithoutDeletionP olicyKey.resources); }); });
  40. 82 • Resource Explorer APIの概要と利用方法 • タグ付けを用いたリソースの判別ロジック • 削除対象のものを判別 •

    書式が沿っていないものを判別 • 削除関数の実装方法と使い方 • リソースタイプごとに実装すること • 自分を削除しないこと • テストの方法 • 自動テスト • 手動テスト 自分で実現できるようになるために(1/2)
  41. 83 テスト対象 • それぞれの削除関数全て テスト方法 • テストしたい関数で消す予定のリソースを作成 • if文でそのリソースのARN以外は即時returnし、該当リソースだけ削除 注意点

    • 本番のAWSアカウントとは分けておくと事故がない • モックして自動テストすることも可能と思われるが今回は手動テスト の方が早くて確実だと判断した • 実装コストの観点 • モックではなく実際のAPIを使うことによる削除順依存・レート制限の再現 手動テスト
  42. 84 if文でそのリソースのARN以外は即時returnする例 export const deleteEc2Instance = async (resource) => {

    //追加 if (resource.Arn !== "arn:of-manually-created-resource") { return; } …(削除のコード) };
  43. 86 タグ付けの依頼 • アカウントの利用時に読んでもらうドキュメントを作成 • そのドキュメントの中で依頼する • アカウントへの権限付与時にドキュメントを共有する 「何もしていない」は「削除して良い」とする •

    削除して良い条件を書かせるのではなく、削除してはいけない場合に 必要事項を書かせる • そういうルールとしてドキュメントに記載する 削除する前に通知をする • 上2つに加えて通知することで、気づかなかったと言わせない • こちらの義務を果たした上で削除することで、納得を得られる 組織内で定める運用ルール
  44. 92 • X(旧Twitter)のDM • Xにて@メンション • 私の発表時間帯付近の #jawsfesta2024 #jawsfesta2024_c を付けた投稿

    • できる限り全部追います!! • この後の休憩時間・会場で見かけたとき • ぜひ話しかけてください!! • 懇親会などなど全部参加します! • もちろんこちらでもぜひ!! ご質問・ご意見お待ちしております!! @amixedcolor エイミ