はじめに
AWS Configは、アカウント運用上のコンプライアンス要件をルールに落とし込み、違反しているリソースを検知してアクションを行うところまでを自動化することができる。
ルールについても、多岐にわたるAWSのマネージドなルールと、それに対する自動修復アクションが容易されているため、アカウント開始後すぐにでも利用開始できるようになっている。
チームがある程度の大きさの規模になり、アカウントを複数人で運用することになった際、ガバナンスのためのトイルを減らしてくれる便利なサービスなので、是非活用できるようにしておきたい。
※AWSに不慣れなメンバがJOINした際の事故防止にも役に立つ。
本記事に必要な前提知識は以下の通り。
- AWS+Terraformの基本的な知識
- AWS SDK for Python (Boto3)の基本的な知識
IAMの準備
AWS Configはサービスリンクロールがあるので、基本はこれを使えば良い。
サービスリンクロールを使わない場合は、以下のようにAWS Configに対して権限を付与しておこう。
AWS Configのチェックは多岐に渡るため、一つ一つ設定していくとそれだけで大きな手間になってしまうため、AWS_ConfigRole
のマネージドポリシーを使用する。
AWS ConfigはAmazon S3に情報を記録するため、カスタムポリシーで権限を付与しておく。
オブジェクトの出力先が決まっているので、最小権限構成にするためにresources
で形式を合わせておく。
resource "aws_iam_role" "config" {
name = local.iam_config_role_name
assume_role_policy = data.aws_iam_policy_document.config_assume.json
}
data "aws_iam_policy_document" "config_assume" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["config.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
data "aws_iam_policy" "config_managed_policy" {
name = "AWS_ConfigRole"
}
resource "aws_iam_role_policy_attachment" "config_managed_policy" {
role = aws_iam_role.config.id
policy_arn = data.aws_iam_policy.config_managed_policy.arn
}
resource "aws_iam_role_policy" "config" {
name = local.iam_config_policy_name
role = aws_iam_role.config.id
policy = data.aws_iam_policy_document.config_custom.json
}
data "aws_iam_policy_document" "config_custom" {
statement {
effect = "Allow"
actions = [
"s3:PutObject",
"s3:PutObjectAcl"
]
resources = [
"${aws_s3_bucket.example.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
]
condition {
test = "StringLike"
variable = "s3:x-amz-acl"
values = [
"bucket-owner-full-control",
]
}
}
statement {
effect = "Allow"
actions = [
"s3:GetBucketAcl",
]
resources = [
"${aws_s3_bucket.example.arn}",
]
}
}
レコーダーの設定
まず最初に、AWS Configの情報を記録するためにレコーダーを設定する。
レコーダーの情報を格納するAmazon S3バケットの設定
特に難しいことはないので、以下のようにAmazon S3バケットを作っておく。
バージョニングはあってもなくても良いが、この後のAWS Configのルールでバージョニング有無をチェックするようにするので、そこでコンプライアンス違反とならないよう設定をしておく。
resource "aws_s3_bucket" "example" {
bucket = local.s3_bucket_name
force_destroy = true
}
resource "aws_s3_bucket_ownership_controls" "example" {
bucket = aws_s3_bucket.example.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_versioning" "example" {
bucket = aws_s3_bucket.example.id
versioning_configuration {
status = "Enabled"
}
}
レコーダーを設定する
レコーダーはTerraformのaws_config_configuration_recorder
のリソースを扱う。
role_arn
には、最初に作成したIAMロールを設定する。
また、aws_config_delivery_channel
のリソースで、上記で作成したAmazon S3に接続を行い、aws_config_configuration_recorder_status
でis_enabled = true
して有効化することでAWS Configの利用が開始できる。
レコーダーはリージョンに1つあれば良い(というか、2つ以上作ろうとしてもエラーになる)ので、既に存在する場合は定義しなくても良い。
aws_config_delivery_channel
とaws_config_configuration_recorder_status
には、インプットとなるパラメータに良い感じの依存関係がないため、depends_on
で明示して新規作成時のエラーを防止する。
resource "aws_config_configuration_recorder" "example" {
name = local.config_recorder_name
role_arn = aws_iam_role.config.arn
}
resource "aws_config_delivery_channel" "example" {
depends_on = [aws_config_configuration_recorder.example]
name = local.config_delivery_channel_name
s3_bucket_name = aws_s3_bucket.example.bucket
}
resource "aws_config_configuration_recorder_status" "example" {
depends_on = [aws_config_delivery_channel.example]
name = aws_config_configuration_recorder.example.name
is_enabled = true
}
ルールの設定
ルールの設定には、Terraformのaws_config_config_rule
のリソースを用いる。
今回は、設定が簡単なAmazon S3のバージョニング設定を行っていない場合に、コンプライアンス違反として検知するマネージドなルールを適用する。scope
およびinput_parameters
のisMfaDeleteEnabled
は省略可能だが、練習のためにちゃんと書いておく。
resource "aws_config_config_rule" "example1" {
depends_on = [aws_config_configuration_recorder.example]
name = local.config_rule_name1
scope {
compliance_resource_types = [
"AWS::S3::Bucket",
]
}
source {
owner = "AWS"
source_identifier = "S3_BUCKET_VERSIONING_ENABLED"
}
input_parameters = jsonencode(
{
isMfaDeleteEnabled = "false",
}
)
}
これでterraform apply
すると、マネコンからもルールが参照できるようになる。
上記のリンクを踏んでいくと、詳細の画面で設定を確認できる。
御覧の通り、非準拠のリソースは存在しない。
せっかくなので、ここで、あえて非準拠のAmazon S3のリソースを作成してみる。
Terraformのaws_s3_bucket_versioning
のリソースのversioning_configuration
をstatus = "Disabled"
にすることで、今回のコンプライアンス要件を違反することになる。
resource "aws_s3_bucket" "example_test" {
bucket = local.s3_bucket_for_test_name
}
resource "aws_s3_bucket_ownership_controls" "example_test" {
bucket = aws_s3_bucket.example_test.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_public_access_block" "example_test" {
bucket = aws_s3_bucket.example_test.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_versioning" "example_test" {
bucket = aws_s3_bucket.example_test.id
versioning_configuration {
status = "Disabled"
}
}
これをterraform apply
してしばらく待つと、ルールの画面下部、「対象範囲内のリソース」が以下のようになる。なお、分かりやすくするために、リソースは「すべて」を表示している。config-example-bucket
はレコーダー記録用のバケットで、最初にバージョニングを有効にしているため、コンプライアンス準拠となっているが、新規に作ったバケットはコンプライアンス非準拠の結果になっている。
これで、ルールに違反したリソースを自動で検知できるようになった。
修復アクションの設定
さて、せっかくルール違反を検知できるようになったのだから、違反したものを検知したら可能な限り速やかに正しい状態にしておきたい。AWS Configには修復アクションという機能があるので、設定をしていこう。
修正アクションはTerraformのaws_config_remediation_configuration
のリソースを使用する。
config_rule_name
でルールとの紐づけを行う。
修正アクションそのものは、AWS Systems ManagerのAutomation機能を呼び出すことになるが、aws_config_remediation_configuration
が呼び出しを行ってくれるため、自分でのAmazon EventBridgeの設定は不要だ。
Amazon S3のバージョニング設定は、予めAWSが用意したAWS Systems Manager Automationのドキュメントがあるため、それを指定すれば良い。
maximum_automatic_attempts
とretry_attempt_seconds
のパラメータは、エラーになった際のリトライ回数と間隔、execution_controls
はAWS Systems Managerの同時実行数の制御のために用いるパラメータであるため、必要に応じて修正する。
resource "aws_config_remediation_configuration" "example1" {
config_rule_name = aws_config_config_rule.example1.name
resource_type = "AWS::S3::Bucket"
target_type = "SSM_DOCUMENT"
target_id = "AWS-ConfigureS3BucketVersioning"
target_version = "1"
parameter {
name = "AutomationAssumeRole"
static_value = aws_iam_role.ssm_remediation.arn
}
parameter {
name = "BucketName"
resource_value = "RESOURCE_ID"
}
parameter {
name = "VersioningState"
static_value = "Enabled"
}
automatic = true
maximum_automatic_attempts = 5
retry_attempt_seconds = 60
execution_controls {
ssm_controls {
concurrent_execution_rate_percentage = 25
error_percentage = 35
}
}
}
ここで注意しなければいけないのが、AWS Systems Managerに修正のための権限が必要であるということだ。
以下のように、IAMロールを作成して、今回必要になるAmazon S3のバージョニング設定の変更の権限を不要しよう。
なお、修正アクションはアカウント内のすべてのリソースに適用する可能性があるため、resources
に関しては最小権限で絞ることはできず*
を設定している。
resource "aws_iam_role" "ssm_remediation" {
name = local.iam_ssm_remediation_role_name
assume_role_policy = data.aws_iam_policy_document.ssm_remediation_assume.json
}
data "aws_iam_policy_document" "ssm_remediation_assume" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["ssm.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
resource "aws_iam_role_policy" "ssm_remediation" {
name = local.iam_ssm_remediation_policy_name
role = aws_iam_role.ssm_remediation.id
policy = data.aws_iam_policy_document.ssm_remediation_custom.json
}
data "aws_iam_policy_document" "ssm_remediation_custom" {
statement {
effect = "Allow"
actions = [
"s3:GetBucketVersioning",
"s3:PutBucketVersioning",
]
resources = [
"*",
]
}
}
上記をApplyしてしばらく待つと、修復アクションが自動実行され、以下の通り、コンプライアンスが準拠した状態になる。
Amazon S3側でも、バージョニング設定が変わっていることを確認できる。
バージョニングの設定は一度Enabledにすると、Terraformでは一度aws_s3_bucket_versioning
のリソースを削除しないと、Disabledに戻すことはできない。更新時に誤って戻してしまうことはないので、期待する状態を維持しやすくなったと言える。
カスタム修復アクションの設定
マネージドルールに対して、AWSがプレフィックスしている修復アクションが無いものもある。
例えば、S3_LIFECYCLE_POLICY_CHECK
(その名の通りAmazon S3のライフサイクルポリシーの設定有無や、設定内容を細かくチェックする定義ができるルール)だ。
本項では、これを自動で修復するアクションを作っていく。
AWS Systems Manager Automationドキュメントの作成
カスタム修復アクションもAWS Systems Manager Automationのドキュメントで制御する。
以下のようにカスタム修復アクションを作成しよう。
内容の詳細は本記事の趣旨からは外れるため割愛する。
Amazon S3のライフサイクルポリシーを取得し、
- 重複するルール名がないこと
- 1000件を超えないこと
を確認したうえで、既存のルールに修復用のルールを追加してライフサイクルポリシーを更新している。
今回は練習用であまり深く気にせずこの設定を行っているが、ライフサイクルポリシーは複数設定ができるため、実際は競合時の動作が問題ないかをよく検証したうえで設定を導入した方が良い。
競合時の考え方はAWS公式のユーザーガイドを参照。
resource "aws_ssm_document" "example" {
name = local.ssm_document_name
document_type = "Automation"
document_format = "YAML"
content = file("${path.module}/ssmdoument.yaml")
}
schemaVersion: "0.3"
description: |
Remediation Action for AWS Config managed rule S3_LIFECYCLE_POLICY_CHECK.
assumeRole: "{{ AutomationAssumeRole }}"
parameters:
AutomationAssumeRole:
type: AWS::IAM::Role::Arn
default: ""
BucketName:
type: AWS::S3::Bucket::Name
DaysUntilTransit:
type: Integer
allowedPattern: ^[1-9][0-9]{0,1023}$
RuleId:
type: String
allowedPattern: ^[a-zA-Z0-9_-]{1,1024}$
outputs:
- VerifyTransitionRule.VerifyTransitionRuleResponse
- VerifyTransitionRule.LifecycleConfigurationRule
mainSteps:
- name: PutTransitionRule
action: aws:executeScript
timeoutSeconds: 120
inputs:
Runtime: python3.8
Handler: handler
InputPayload:
BucketName: "{{ BucketName }}"
DaysUntilTransit: "{{ DaysUntilTransit }}"
RuleId: "{{ RuleId }}"
Script: |
import time
import boto3
import botocore
def get_bucket_rules(s3_client, bucket_name):
try:
response = s3_client.get_bucket_lifecycle_configuration(Bucket=bucket_name)
return response["Rules"]
except botocore.exceptions.ClientError as error:
response = error.response
error_code = response["Error"]["Code"]
error_message = response["Error"]["Message"]
if error_code == "NoSuchLifecycleConfiguration" and "does not exist" in error_message:
return []
raise Exception from error
def handler(event, context):
bucket_name = event["BucketName"]
days_until_transit = event["DaysUntilTransit"]
rule_id = event["RuleId"]
s3_client = boto3.client("s3")
bucket_rules = get_bucket_rules(s3_client=s3_client, bucket_name=bucket_name)
if bucket_rules and len(bucket_rules) == 1000:
raise Exception(f"S3 BUCKET {bucket_name} CANNOT HAVE MORE THAN 1,000 LIFECYCLE CONFIGURATION RULES")
if bucket_rules:
bucket_rule_ids = []
for rule in bucket_rules:
bucket_rule_ids.append(rule["ID"])
if rule_id in bucket_rule_ids:
raise Exception(
f"S3 BUCKET {bucket_name} ALREADY CONTAINS A LIFECYCLE CONFIGURATION WITH ID {rule_id}"
)
transitions = {
"ID": rule_id,
"Filter": {"Prefix": ""},
"Status": "Enabled",
"Transitions": [{"Days": days_until_transit, "StorageClass": "INTELLIGENT_TIERING"}],
}
bucket_rules.append(transitions)
print(bucket_rules)
try:
s3_client.put_bucket_lifecycle_configuration(
Bucket=bucket_name, LifecycleConfiguration={"Rules": bucket_rules}
)
time.sleep(5)
except botocore.exceptions.ClientError as error:
raise Exception from error
- name: VerifyTransitionRule
action: aws:executeScript
timeoutSeconds: 120
maxAttempts: 5
inputs:
Runtime: python3.8
Handler: handler
InputPayload:
BucketName: "{{ BucketName }}"
RuleId: "{{ RuleId }}"
Script: |
import boto3
import botocore
def handler(event, context):
bucket_name = event["BucketName"]
rule_id = event["RuleId"]
s3_client = boto3.client("s3")
try:
response = s3_client.get_bucket_lifecycle_configuration(Bucket=bucket_name)
rules = response["Rules"]
for rule in rules:
if rule_id == rule.get("ID"):
success_message = (
"Verification of transit configured for Amazon S3 Bucket is successful."
)
return {"VerifyTransitionRule": success_message, "Rule": rule}
raise Exception(
f"FAILED TO VERIFY LIFECYCLE CONFIGURATION RULE WITH ID {rule_id} ON S3 BUCKET {bucket_name}"
)
except botocore.exceptions.ClientError as error:
raise Exception from error
outputs:
- Name: VerifyTransitionRuleResponse
Type: String
Selector: $.Payload.VerifyTransitionRule
- Name: LifecycleConfigurationRule
Type: StringMap
Selector: $.Payload.Rule
AWS Systems ManagerのIAM権限の付与
上記の通り、スクリプト内でGet/PutLifecycleConfigurationのAPIを呼び出しているため。IAM権限の更新が必要だ。もともと作っていたポリシーに以下の権限を追加しよう。
data "aws_iam_policy_document" "ssm_remediation_custom" {
statement {
effect = "Allow"
actions = [
"s3:GetBucketVersioning",
+ "s3:GetLifecycleConfiguration",
"s3:PutBucketVersioning",
+ "s3:PutLifecycleConfiguration",
]
resources = [
"*",
]
}
}
AWS Configルールと修復アクションの設定
上記で作成したAWS Systems Manager Automationドキュメントを以下のように呼び出す。
今回は、バケットの移行設定が40日になっていないものを、40日に補正する修復アクションを実行する。
基本的にプレフィックスの修復アクションの呼び出しと変わらないが、特筆すべき点は、target_id
とtarget_version
を、上記で作成したリソースへの参照にしているところだ。特に、target_version
は固定にしていると、AWS Systems Manager Automationに更新されず思わぬ動作をする可能性があるので注意が必要だ。
resource "aws_config_config_rule" "example2" {
depends_on = [aws_config_configuration_recorder.example]
name = local.config_rule_name2
scope {
compliance_resource_types = [
"AWS::S3::Bucket",
]
}
source {
owner = "AWS"
source_identifier = "S3_LIFECYCLE_POLICY_CHECK"
}
input_parameters = jsonencode(
{
targetTransitionDays = "40",
}
)
}
resource "aws_config_remediation_configuration" "example2" {
config_rule_name = aws_config_config_rule.example2.name
resource_type = "AWS::S3::Bucket"
target_type = "SSM_DOCUMENT"
target_id = aws_ssm_document.example.name
target_version = aws_ssm_document.example.latest_version
parameter {
name = "AutomationAssumeRole"
static_value = aws_iam_role.ssm_remediation.arn
}
parameter {
name = "BucketName"
resource_value = "RESOURCE_ID"
}
parameter {
name = "DaysUntilTransit"
static_value = "40"
}
parameter {
name = "RuleId"
static_value = "AWSConfigRemedation_S3Lifecycle_Transition"
}
automatic = true
maximum_automatic_attempts = 5
retry_attempt_seconds = 60
execution_controls {
ssm_controls {
concurrent_execution_rate_percentage = 25
error_percentage = 35
}
}
}
上記をApplyした後に、
resource "aws_s3_bucket_lifecycle_configuration" "example_test" {
bucket = aws_s3_bucket.example_test.id
rule {
id = "rule"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
}
}
と、ライフサイクルポリシーの移行までの期間が30日の設定を行うと、追加されたルールの詳細画面で、
といったかたちで検知したい違反が見つかり、しばらく待つとカスタム修復アクションによる修復で以下のようにステータスが変わる。
Amazon S3のコンソールでも、修復アクション呼び出しのパラメータで指定したAWSConfigRemedation_S3Lifecycle_Transition
でライフサイクルルールが作成されているのが確認できる。
これで、修復ルールを自分でも作成できるようになった!
参考:エラー発生時のトラブルシュート方法まとめ
記事を書くにあたり参考にしていた情報。
- ルール設定が上手く動かない場合: AWS re:Post「AWS Config ルールが機能しません。その理由は?」
- 修正アクションが上手く動かない場合: AWS re:Post「AWS Config で失敗した修復アクションをトラブルシューティングする方法を教えてください。」
また、カスタム修復アクション内のスクリプトのエラーについては、AWS Systems Manager Automationのコンソールで確認ができる。デバッグ時の参考になれば。