エンタープライズクラウド部の山下(祐)です。
今回は、AWS Config Rules(以下、Configルール)で一定期間利用の無いIAMユーザーを検知し、修復アクションでAWSDenyAllポリシーをアタッチ&管理者へのメール通知を行ってみたいと思います。
また、CloudFormation StackSets(以下、StackSets)とAWS Config Conformance Packs(以下、適合パック)を使用し、AWS Organizations(以下、Organizations)の組織全体で利用の無いIAMユーザーを管理できるようにしたいと思います。
- 適合パックとStackSetsの配布イメージ
- 修復の流れ
- StackSetsの委任
- 修復アクション用IAMロール
- SNSトピック
- Configルール
- 修復アクション
- 適合パック
- 動作確認
- 終わりに
- オマケ(参考リンク集)
適合パックとStackSetsの配布イメージ
Organizationsの組織全体に、適合パックを配布します。修復アクション用のSSMドキュメントは独自のものを作成するため、修復アクションを行うための権限を付与するIAMロールとともに、StackSetsで配布します。配布はセキュリティアカウントから行うこととし、ConfigとStackSetsを管理アカウントから委任します。
修復の流れ
一定期間利用の無いIAMユーザーを、Configルールで検知します。修復アクションで、対象のIAMユーザーにAWSマネージドポリシーのAWSDenyAllポリシーをアタッチします。また、SNSで管理者へのEメール通知も行います。SNSはセキュリティアカウントにデプロイし、各アカウントからのメッセージを受け付けます。
AWSマネージドの修復アクションにはIAMユーザーを削除するものがありますが、いきなり削除は少々やり過ぎだと思うので、今回は独自の修復アクションを作成します。
StackSetsの委任
まずは、管理アカウントのStackSetsの画面から委任を行います。
詳細は、以下の公式ドキュメントもご参照ください。
委任された管理者の登録 - AWS CloudFormation
修復アクション用IAMロール
修復アクション用のIAMロールには、対象IAMユーザーの情報取得・ポリシーアタッチの権限と、SNSトピックにメッセージを送付する権限が必要です。 今回は検証なので、横着してマネージドポリシーの「IAMFullAccess」と「AmazonSNSFullAccess」を付与してしまいます。 実際の構築の際は、必要最小限のポリシーを付与したロールを作成いただければ幸いです。
信頼ポリシーでは、SSMへのAssumeRoleを許可します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "ssm.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
以下のテンプレートをStackSetsで各アカウントに配布します。
クリックすると展開されます
AWSTemplateFormatVersion: "2010-09-09" Description: "For SSM Automation" Resources: role: Type: "AWS::IAM::Role" DeletionPolicy: "Delete" Properties: ManagedPolicyArns: - "arn:aws:iam::aws:policy/AmazonSNSFullAccess" - "arn:aws:iam::aws:policy/IAMFullAccess" RoleName: "SSM-Automation-Set-Deny-Role" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Sid: "" Effect: "Allow" Principal: Service: - "ssm.amazonaws.com" Action: - "sts:AssumeRole" Path: "/"
SNSトピック
SNSトピックはセキュリティアカウントのみに作成し、各アカウントからのメッセージを集約します。 アクセスポリシーで、Organizationsの組織IDを指定することで、組織内のアカウントからのメッセージを受け取ることが可能です。
クリックすると展開されます
{ "Version": "2008-10-17", "Id": "__default_policy_ID", "Statement": [ { "Sid": "__default_statement_ID", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": [ "SNS:RemovePermission", "SNS:SetTopicAttributes", "SNS:DeleteTopic", "SNS:ListSubscriptionsByTopic", "SNS:GetTopicAttributes", "SNS:AddPermission", "SNS:Subscribe" ], "Resource": "arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:Unused-IAMUser-Notification", "Condition": { "StringEquals": { "AWS:SourceOwner": "XXXXXXXXXXXX" } } }, { "Sid": "Allow_Publish_from_Organzations", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": "SNS:Publish", "Resource": "arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:Unused-IAMUser-Notification", "Condition": { "StringEquals": { "aws:PrincipalOrgID": "o-xxxxxxxxxx" } } } ] }
サブスクリプションについては、今回はEメールを設定します。
Configルール
Configルールは、AWSマネージドルールの「iam-user-unused-credentials-check」を使用します。このルールは、一定期間利用のないコンソールパスワード、アクセスキーを持つIAMユーザーを検知することが可能です。期間のデフォルト値は90日ですが、今回は検証のためにすぐ検知したいので、1日に設定します。ルールの詳細については、以下の公式ドキュメントも参照してください。
iam-user-unused-credentials-チェック - AWS Config
注意点として、AWS::IAM::Userリソースタイプはグローバルリソースであり、2022年2月以前に利用可能だった全てのリージョンでリソース記録が可能です。不要なリソース記録を回避するために、一つのリージョンのみにルールを適用することが推奨されています。
上述の公式ドキュメントの注記を引用します。
AWS::IAM::Userリソースタイプは、2022 AWS Config 年 2 AWS Config 月以前に利用可能だった地域でのみ記録できます。 AWS::IAM::User2022 年 2 AWS Config 月以降にサポートされるリージョンでは記録できません。
さらに、少なくとも 1 つのリージョンで AWS::IAM::User を記録することを選択した場合、AWS::IAM::User のコンプライアンスを報告するこのルールなどの定期ルールは、定期ルールが追加されたリージョンで AWS::IAM::User の記録を有効にしていなくても、定期ルールが追加されたすべてのリージョンの AWS::IAM::User で評価を実行します。
不必要な評価や API スロットリングを回避するため、このルールはサポートされているリージョンの 1 つにのみデプロイできます。別のリージョンのグローバル IAM リソースタイプの記録を有効にしている場合、グローバル IAM リソースタイプの記録を有効にしていなければ、このルールは評価の実行を回避しません。不必要な評価を回避するため、このルールのデプロイを 1 つのリージョンに制限する必要があります。
以下の公式ドキュメントにも同様の注記があります。合わせてご参照ください。
AWS Config どのリソースを記録するかを選択する - AWS Config
今回は、東京リージョンに本ルールをデプロイします。
修復アクション
Configルールの修復アクションには、AWS Systems Manager(以下、SSM)のAutomationを利用します。Automationの利用時には、Runbookという処理を記載したドキュメントを指定します。予め用意されたRunbookも存在しますが、今回実施したいアクションに合致するものが無いため、自前で作成します。RunbookはYAML・JSONで記述することも可能ですが、AWS Step Functions同様に、ビジュアルツールを用いて作成することも可能です。本ブログでは、双方の設定を記載して説明します。
ビジュアルツール
今回作成した修復アクションをビジュアルツールで表示すると下図のようになります。
それぞれのアクションについて説明します。
ランブック属性
ランブックのパラメータとして、IAMUserIdとAutomationAssumeRole を設定します。パラメータの値は適合パックに記載します。 IAMUserId はConfigルールが検知したIDなので動的な値となります。AutomationAssumeRole は、修復アクション用IAMロールのARNを静的に記載します。 適合パックの記載は後述しますので、ここでは、ランブック側の記載方法のみ紹介します。
① GetUsername
最初のステップでは、boto3を使用して、以下のアクションを行います。
- Configルールから受け取ったIAMユーザーIDから、IAMユーザー名を取得。
- 取得したIAMユーザーに、すでに AWSDenyAll ポリシーがアタッチされていないか判定。
- AWSDenyAll の判定結果に応じたメッセージを作成。
2について補足します。今回の修復アクションは AWSDenyAll ポリシーをアタッチするだけなので、Configルールの非準拠状態(IAMが使われていない)が解消されるわけではありません。 よって、修復アクション後もIAMユーザーが引き続き利用されない場合、再度Configルールで検知される可能性があります。 そのため、AWSDenyAll のアタッチ有無を判定するロジックを入れています。
本ステップでは、インプット情報として、ランブック属性で指定したパラメータ「IAMUserId」を定義します。
また、上記1~3で取得した情報については、後続のステップで参照させるため、アウトプットとしても定義します。
② Branch
上述通り、AWSDenyAll がすでにアタッチされている可能性があるため、このステップで処理を分岐させます。 AWSDenyAll がアタッチされていない場合は ③以降のステップに進みます。 すでにアタッチされている場合は⑤のステップに進みます。 インプット情報として、「① GetUsername」での、AWSDenyAll のアタッチ有無の判定結果を定義します。
③ AttachUserPolicy
このステップでは、対象のIAMユーザーに AWSDenyAll をアタッチします。 Runbookに予め用意されているAWS APIの「IAM AttachUserPolicy」を使用します。 インプット情報として、「① GetUsername」で取得したIAMユーザー名と、AWSDenyAll のARNを定義します。
④ Publish
このステップでは、管理者にメール送信を行うために、SNSトピックにメッセージを送付します。 インプット情報として、「① GetUsername」で作成したメッセージと、SNSトピックのARNを定義します。
⑤ Sleep
このステップでは、5秒間の待機を行います。Runbookで「何もせずに終了」という定義が出来なかったため、Sleepで代替しました。 インプット情報として、待機時間(Duration)を定義します。ISO 8601の表記に従う必要があるため、「PT5S」と記載しています。
YAML
上記ビジュアルツールでの設定の、YAMLでの記述内容は以下です。
クリックすると展開されます
schemaVersion: '0.3' description: This runbook attaches an All Deny policy to IAM users that have been inactive for a period of time. parameters: IAMUserId: type: String description: (Required) The ID of the IAM user you want to delete. allowedPattern: ^AIDA[A-Z0-9]+$ AutomationAssumeRole: type: String description: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. assumeRole: '{{ AutomationAssumeRole }}' mainSteps: - description: Gathers the user name of the IAM user you specify in the IAMUserId parameter. name: GetUsername action: aws:executeScript nextStep: Branch isEnd: false inputs: Runtime: python3.11 Handler: script_handler InputPayload: IAMUserId: '{{ IAMUserId }}' Script: |- from time import sleep import boto3 THROTTLE_PERIOD = 0.05 def get_username(iam_client, iam_user_id): paginator = iam_client.get_paginator("list_users") page_iterator = paginator.paginate() for page in page_iterator: for user in page["Users"]: if user["UserId"] == iam_user_id: return user["UserName"] sleep(THROTTLE_PERIOD) def check_deny_all_policy(iam_client, iam_username): paginator = iam_client.get_paginator('list_attached_user_policies') for response in paginator.paginate(UserName=iam_username): for policy in response['AttachedPolicies']: if policy['PolicyArn'] == 'arn:aws:iam::aws:policy/AWSDenyAll': return True break return False def get_account_number(iam_client, iam_username): user_info = iam_client.get_user(UserName=iam_username) arn = user_info['User']['Arn'] account_number = arn.split(':')[4] return account_number def script_handler(event, context): iam_client = boto3.client("iam") iam_user_id = event["IAMUserId"] iam_username = get_username(iam_client, iam_user_id) iam_account_number = get_account_number(iam_client, iam_username) if iam_username is not None: iam_user_deny_policy = check_deny_all_policy(iam_client, iam_username) if iam_user_deny_policy is True: result_message = f"AWSDenyAll is already attached to AWS IAM USER, {iam_username}. AWS Account is {iam_account_number}." return {"UserName": iam_username, "Messages": result_message, "AWSDenyAll": iam_user_deny_policy, "AWSAccount": iam_account_number} else: result_message = f"AWS IAM USER, {iam_username} has been unused for a period of time, so AWSDenyAll policy is attached. AWS Account is {iam_account_number}." return {"UserName": iam_username, "Messages": result_message, "AWSDenyAll": iam_user_deny_policy, "AWSAccount": iam_account_number} else: result_message = f"AWS IAM USER ID, {iam_user_id} DOES NOT EXIST." raise Exception(result_message) outputs: - Type: String Name: UserName Selector: $.Payload.UserName - Type: String Name: Messages Selector: $.Payload.Messages - Type: Boolean Name: AWSDenyAll Selector: $.Payload.AWSDenyAll - name: Branch action: aws:branch inputs: Choices: - NextStep: Sleep Variable: '{{ GetUsername.AWSDenyAll }}' BooleanEquals: true Default: AttachUserPolicy - name: Sleep action: aws:sleep isEnd: true inputs: Duration: PT5S - name: AttachUserPolicy action: aws:executeAwsApi nextStep: Publish isEnd: false inputs: Service: iam Api: AttachUserPolicy UserName: '{{ GetUsername.UserName }}' PolicyArn: arn:aws:iam::aws:policy/AWSDenyAll - name: Publish action: aws:executeAwsApi isEnd: true inputs: Message: '{{ GetUsername.Messages }}' TopicArn: arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:Unused-IAMUser-Notification Service: sns Api: Publish
CloudFormationテンプレート
RunbookもStackSetsで各アカウントにデプロイします。デプロイ用のテンプレートは以下です。
クリックすると展開されます
AWSTemplateFormatVersion: 2010-09-09 Description: This runbook attaches an All Deny policy to IAM users that have been inactive for a period of time. Resources: AttachAWSAllDeny: Type: AWS::SSM::Document Properties: Content: schemaVersion: '0.3' description: This runbook attaches an All Deny policy to IAM users that have been inactive for a period of time. parameters: IAMUserId: type: String description: (Required) The ID of the IAM user you want to delete. allowedPattern: ^AIDA[A-Z0-9]+$ AutomationAssumeRole: type: String description: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf. assumeRole: '{{ AutomationAssumeRole }}' mainSteps: - description: Gathers the user name of the IAM user you specify in the IAMUserId parameter. name: GetUsername action: aws:executeScript nextStep: Branch isEnd: false inputs: Runtime: python3.11 Handler: script_handler InputPayload: IAMUserId: '{{ IAMUserId }}' Script: |- from time import sleep import boto3 THROTTLE_PERIOD = 0.05 def get_username(iam_client, iam_user_id): paginator = iam_client.get_paginator("list_users") page_iterator = paginator.paginate() for page in page_iterator: for user in page["Users"]: if user["UserId"] == iam_user_id: return user["UserName"] sleep(THROTTLE_PERIOD) def check_deny_all_policy(iam_client, iam_username): paginator = iam_client.get_paginator('list_attached_user_policies') for response in paginator.paginate(UserName=iam_username): for policy in response['AttachedPolicies']: if policy['PolicyArn'] == 'arn:aws:iam::aws:policy/AWSDenyAll': return True break return False def get_account_number(iam_client, iam_username): user_info = iam_client.get_user(UserName=iam_username) arn = user_info['User']['Arn'] account_number = arn.split(':')[4] return account_number def script_handler(event, context): iam_client = boto3.client("iam") iam_user_id = event["IAMUserId"] iam_username = get_username(iam_client, iam_user_id) iam_account_number = get_account_number(iam_client, iam_username) if iam_username is not None: iam_user_deny_policy = check_deny_all_policy(iam_client, iam_username) if iam_user_deny_policy is True: result_message = f"AWSDenyAll is already attached to AWS IAM USER, {iam_username}. AWS Account is {iam_account_number}." return {"UserName": iam_username, "Messages": result_message, "AWSDenyAll": iam_user_deny_policy, "AWSAccount": iam_account_number} else: result_message = f"AWS IAM USER, {iam_username} has been unused for a period of time, so AWSDenyAll policy is attached. AWS Account is {iam_account_number}." return {"UserName": iam_username, "Messages": result_message, "AWSDenyAll": iam_user_deny_policy, "AWSAccount": iam_account_number} else: result_message = f"AWS IAM USER ID, {iam_user_id} DOES NOT EXIST." raise Exception(result_message) outputs: - Type: String Name: UserName Selector: $.Payload.UserName - Type: String Name: Messages Selector: $.Payload.Messages - Type: Boolean Name: AWSDenyAll Selector: $.Payload.AWSDenyAll - name: Branch action: aws:branch inputs: Choices: - NextStep: Sleep Variable: '{{ GetUsername.AWSDenyAll }}' BooleanEquals: true Default: AttachUserPolicy - name: Sleep action: aws:sleep isEnd: true inputs: Duration: PT5S - name: AttachUserPolicy action: aws:executeAwsApi nextStep: Publish isEnd: false inputs: Service: iam Api: AttachUserPolicy UserName: '{{ GetUsername.UserName }}' PolicyArn: arn:aws:iam::aws:policy/AWSDenyAll - name: Publish action: aws:executeAwsApi isEnd: true inputs: Message: '{{ GetUsername.Messages }}' TopicArn: arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:Unused-IAMUser-Notification Service: sns Api: Publish DocumentFormat: YAML DocumentType: Automation Name: AttachAWSAllDeny UpdateMethod: NewVersion
「content」の中身は修復アクションのYAMLそのものです。それ以外の設定項目については、以下公式ドキュメントをご参照ください。
AWS::SSM::Document - AWS CloudFormation
適合パック
適合パック用YAMLテンプレート
今回作成した適合パック用のYAMLテンプレートは以下です。
クリックすると展開されます
AWSTemplateFormatVersion: 2010-09-09 Description: Attaches an All Deny policy to IAM users that have been inactive for a period of time. Resources: IamUserUnusedCredentialsCheck: Type: AWS::Config::ConfigRule Properties: ConfigRuleName: iam-user-unused-credentials-check InputParameters: maxCredentialUsageAge: 1 Source: Owner: AWS SourceIdentifier: IAM_USER_UNUSED_CREDENTIALS_CHECK AttachAWSAllDeny: DependsOn: IamUserUnusedCredentialsCheck Type: 'AWS::Config::RemediationConfiguration' Properties: ConfigRuleName: iam-user-unused-credentials-check TargetId: "AttachAWSAllDeny" TargetType: "SSM_DOCUMENT" TargetVersion: "1" Parameters: AutomationAssumeRole: StaticValue: Values: - arn:aws:iam::xxxxxxxxxxxx:role/SSM-Automation-Set-Deny-Role IAMUserId: ResourceValue: Value: "RESOURCE_ID" Automatic: True MaximumAutomaticAttempts: 1 RetryAttemptSeconds: 60
Configルールにはマネージドルールの「iam-user-unused-credentials-check」を指定し、修復アクションにはStackSetsで各アカウントにデプロイ済みの「AttachAWSAllDeny」を指定しています。「AutomationAssumeRole」には、セキュリティアカウントの修復アクション用ロールのARNを指定します。
Configルールや修復アクションに関する記述方法の詳細は、以下公式ドキュメントをご参照ください。
AWS::Config::ConfigRule - AWS CloudFormation
AWS::Config::RemediationConfiguration - AWS CloudFormation
サービス有効化と委任
適合パックを委任アカウントからOrganizationsの組織に一括デプロイするためには、以下の事前準備が必要です。
- AWS ConfigサービスがAWS Organizationsへアクセスできるようにする。(管理アカウントで実施)
- 委任管理者アカウントを登録する。(管理アカウントで実施)
2024年5月現在、マネジメントコンソールから上記作業が出来ないようなので、CloudShellにて実施します。 管理アカウントで以下のコマンドを実行します。
クリックすると展開されます
#1. AWS ConfigサービスがAWS Organizationsへアクセスできるようにする。 [cloudshell-user@ip-10-132-78-25 ~]$ aws organizations enable-aws-service-access --service-principal=config-multiaccountsetup.amazonaws.com [cloudshell-user@ip-10-132-78-25 ~]$ - #2. 委任管理者アカウントを登録する。 [cloudshell-user@ip-10-132-78-25 ~]$ aws organizations register-delegated-administrator --service-principal=config-multiaccountsetup.amazonaws.com --account-id="{委任先アカウント}" [cloudshell-user@ip-10-132-78-25 ~]$ [cloudshell-user@ip-10-132-78-25 ~]$ - #正常に委任されたことを確認する。 [cloudshell-user@ip-10-132-78-25 ~]$ aws organizations list-delegated-administrators --service-principal=config-multiaccountsetup.amazonaws.com { "DelegatedAdministrators": [ { "Id": "xxxxxxxxxxxx", "Arn": "arn:aws:organizations::xxxxxxxxxxxx:account/o-xxxxxxxxxx/xxxxxxxxxxxx", "Email": "xxxxx@serverworks.co.jp", "Name": "Security", "Status": "ACTIVE", "JoinedMethod": "CREATED", "JoinedTimestamp": "2024-04-24T06:56:52.804000+00:00", "DelegationEnabledDate": "2024-05-09T14:54:54.180000+00:00" } ] } [cloudshell-user@ip-10-132-78-25 ~]$
適合パックのデプロイ
サービス有効化と委任が完了したら、今度は委任アカウントのCloudShellで作業を行います。まず、CloudShellに先ほどの適合パック用YAMLテンプレートをアップロードします。
次に、CloudShell上で以下のコマンドを実行し、適合パックをデプロイします。
クリックすると展開されます
# 適合パック用YAMLファイルが存在することを確認する [cloudshell-user@ip-10-130-58-254 ~]$ ls -l total 4 -rw-r--r--. 1 cloudshell-user cloudshell-user 1077 May 9 16:30 AttachAWSAllDeny_ConformancePack.yml [cloudshell-user@ip-10-130-58-254 ~]$ - # 適合パックをデプロイする [cloudshell-user@ip-10-130-58-254 ~]$ aws configservice put-organization-conformance-pack \ > --organization-conformance-pack-name="AttachAWSAllDeny" \ > --template-body="file://AttachAWSAllDeny_ConformancePack.yml" { "OrganizationConformancePackArn": "arn:aws:config:ap-northeast-1:xxxxxxxxxxxx:organization-conformance-pack/AttachAWSAllDeny-f19zroz2" } [cloudshell-user@ip-10-130-58-254 ~]$ - # 適合パックのデプロイ状況を確認する [cloudshell-user@ip-10-130-58-254 ~]$ aws configservice get-organization-conformance-pack-detailed-status \ > --organization-conformance-pack-name=AttachAWSAllDeny { "OrganizationConformancePackDetailedStatuses": [ { "AccountId": "xxxxxxxxxxxx", "ConformancePackName": "OrgConformsPack-AttachAWSAllDeny-f19zroz2", "Status": "CREATE_SUCCESSFUL", "LastUpdateTime": "2024-05-09T16:33:09.697000+00:00" }, { "AccountId": "xxxxxxxxxxxx", "ConformancePackName": "OrgConformsPack-AttachAWSAllDeny-f19zroz2", "Status": "CREATE_SUCCESSFUL", "LastUpdateTime": "2024-05-09T16:33:07.726000+00:00" }, { "AccountId": "xxxxxxxxxxxx", "ConformancePackName": "OrgConformsPack-AttachAWSAllDeny-f19zroz2", "Status": "CREATE_SUCCESSFUL", "LastUpdateTime": "2024-05-09T16:33:11.591000+00:00" }, { "AccountId": "xxxxxxxxxxxx", "ConformancePackName": "OrgConformsPack-AttachAWSAllDeny-f19zroz2", "Status": "CREATE_SUCCESSFUL", "LastUpdateTime": "2024-05-09T16:33:06.739000+00:00" }, { "AccountId": "xxxxxxxxxxxx", "ConformancePackName": "OrgConformsPack-AttachAWSAllDeny-f19zroz2", "Status": "CREATE_SUCCESSFUL", "LastUpdateTime": "2024-05-09T16:33:08.620000+00:00" } ] } (END)
サービス有効化と委任、および、適合パックのデプロイについては、以下のAWS公式ブログでも詳しく紹介されていますので、合わせてご参照ください。
AWS Config 適合パックの紹介 | Amazon Web Services ブログ
動作確認
適合パックがデプロイできたようなので、各アカウントのConfigルールを見てみます。
Configルールがデプロイされて、自動修復アクションも正常に実行されているようです。
AWSDenyAllも想定通りアタッチされています。
通知メールも想定通り来ていました。
記載は省略しますが、他アカウントでも想定通りConfigルールがデプロイされ、修復アクションが実行されていました。
終わりに
今回、独自のSSM Runbookを作成し、適合パックも独自のものを作成しましたが、これらはCloudFormationテンプレート等と比べると参考情報が少なく、作成に少々苦労しました。
また、適合パックのデプロイについても、マネジメントコンソールでは出来ない等、ややクセがある印象です。
学習コスト・運用コストも考慮のうえ、これらのサービスを利用した実装をご検討いただければ幸いです。
オマケ(参考リンク集)
最後に、本ブログの作成にあたり参考にした弊社ブログのリンクを記載します。
適合パックやStackSetsについて詳しく説明しておりますので、よろしければ合わせてご参照ください。
本ブログが少しでもお役にたてば幸いです。
山下 祐樹(執筆記事の一覧)
2021年11月中途入社。前職では情シスとして社内ネットワークの更改や運用に携わっていました。 2023 Japan AWS All Certifications Engineers。