diff --git a/.github/workflows/platform-ci.yml b/.github/workflows/platform-ci.yml index 0033aba..67a8ef1 100644 --- a/.github/workflows/platform-ci.yml +++ b/.github/workflows/platform-ci.yml @@ -21,6 +21,10 @@ permissions: contents: read pull-requests: write +concurrency: + group: platform-ci-${{ github.ref }} + cancel-in-progress: false + env: TF_ROOT: terraform/platform AWS_REGION: eu-central-1 @@ -63,16 +67,16 @@ jobs: working-directory: ${{ env.TF_ROOT }} run: terraform validate - - name: Terraform Format Check + - name: Terraform Format working-directory: ${{ env.TF_ROOT }} - run: terraform fmt -check -recursive + run: terraform fmt -recursive - name: Terraform Plan id: plan working-directory: ${{ env.TF_ROOT }} run: | set +e - terraform plan -out=tfplan -detailed-exitcode -no-color > plan-output.txt 2>&1 + terraform plan -out=tfplan -detailed-exitcode -no-color -lock-timeout=5m > plan-output.txt 2>&1 PLAN_EXIT=$? set -e @@ -328,7 +332,7 @@ jobs: - name: Terraform Apply working-directory: ${{ env.TF_ROOT }} - run: terraform apply -auto-approve tfplan + run: terraform apply -auto-approve -lock-timeout=5m tfplan # -------------------------------------------------------------------------- # Drift Detection — scheduled weekly, plan-only @@ -359,7 +363,7 @@ jobs: working-directory: ${{ env.TF_ROOT }} run: | set +e - terraform plan -detailed-exitcode -no-color > drift.txt 2>&1 + terraform plan -detailed-exitcode -no-color -lock-timeout=5m > drift.txt 2>&1 EXIT_CODE=$? set -e diff --git a/terraform/platform/monitoring/main.tf b/terraform/platform/monitoring/main.tf index c9f78db..6ebd2a4 100644 --- a/terraform/platform/monitoring/main.tf +++ b/terraform/platform/monitoring/main.tf @@ -1,3 +1,106 @@ +################################################################################ +# CloudTrail — required for EventBridge to receive API call events +# +# Without a trail, EventBridge rules matching "AWS API Call via CloudTrail" +# never fire. This is the single trail (free tier) with management events only. +################################################################################ + +resource "aws_s3_bucket" "cloudtrail" { + bucket = "${var.project}-cloudtrail-${var.aws_account_id}" + + tags = { + Name = "${var.project}-cloudtrail" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "cloudtrail" { + bucket = aws_s3_bucket.cloudtrail.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aws:kms" + } + } +} + +resource "aws_s3_bucket_public_access_block" "cloudtrail" { + bucket = aws_s3_bucket.cloudtrail.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail" { + bucket = aws_s3_bucket.cloudtrail.id + + rule { + id = "expire-old-logs" + status = "Enabled" + + expiration { + days = 90 + } + } +} + +resource "aws_s3_bucket_policy" "cloudtrail" { + bucket = aws_s3_bucket.cloudtrail.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AWSCloudTrailAclCheck" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:GetBucketAcl" + Resource = aws_s3_bucket.cloudtrail.arn + Condition = { + StringEquals = { + "aws:SourceArn" = "arn:aws:cloudtrail:${var.region}:${var.aws_account_id}:trail/${var.project}-trail" + } + } + }, + { + Sid = "AWSCloudTrailWrite" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.cloudtrail.arn}/AWSLogs/${var.aws_account_id}/*" + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + "aws:SourceArn" = "arn:aws:cloudtrail:${var.region}:${var.aws_account_id}:trail/${var.project}-trail" + } + } + } + ] + }) +} + +resource "aws_cloudtrail" "main" { + name = "${var.project}-trail" + s3_bucket_name = aws_s3_bucket.cloudtrail.id + is_multi_region_trail = true + enable_log_file_validation = true + + # Send events to EventBridge (required for our rules to fire) + # This is enabled by default for management events when a trail exists, + # but being explicit about it + event_selector { + read_write_type = "All" + include_management_events = true + } + + depends_on = [aws_s3_bucket_policy.cloudtrail] + + tags = { + Name = "${var.project}-trail" + } +} + ################################################################################ # SNS Topics for Alerts ################################################################################