"""Pipeline stack: S3, classifier + slack-post Lambdas, Glue, Athena, IAM. Scaffold — the exports bucket (Phase 1 of docs/BUILD.md) is included so the stack synthesizes to something real. The classifier Lambda (Phase 2), Glue database + Athena workgroup with partition projection (Phase 3), and the slack-post Lambda + IAM (Phase 4) are added in their respective phases. Lambda defaults when added: Python 3.12, ARM64, explicit LogGroup with 60-day retention. No DynamoDB — this is an S3 + Athena analytics workload (see CLAUDE.md). """ from aws_cdk import ( Duration, RemovalPolicy, Stack, ) from aws_cdk import ( aws_iam as iam, ) from aws_cdk import ( aws_s3 as s3, ) from constructs import Construct class PipelineStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # Phase 1 — single exports bucket. # Prefixes: raw/ (incoming), analytics/ (per-WO snapshots), athena-results/. self.exports_bucket = s3.Bucket( self, "Exports", bucket_name=f"apm-wo-analysis-exports-{self.account}", encryption=s3.BucketEncryption.S3_MANAGED, block_public_access=s3.BlockPublicAccess.BLOCK_ALL, enforce_ssl=True, removal_policy=RemovalPolicy.RETAIN, lifecycle_rules=[ s3.LifecycleRule( id="expire-raw-exports", prefix="raw/", expiration=Duration.days(90), ) ], ) # Phase 1 — least-privilege identity for the local drop-folder uploader. # Scoped to s3:PutObject on raw/* only. The access key is created # out-of-band (aws iam create-access-key) and stored in the local # ~/.aws/credentials profile `apm-wo-drop` — never in CloudFormation. self.drop_uploader = iam.User( self, "DropUploader", user_name="apm-wo-drop-uploader" ) self.drop_uploader.add_to_policy( iam.PolicyStatement( sid="PutRawExportsOnly", actions=["s3:PutObject"], resources=[self.exports_bucket.arn_for_objects("raw/*")], ) ) # Phase 2 — classifier Lambda, S3-triggered on the raw/ prefix. TODO # Phase 3 — Glue database `apm_wo_analysis` + Athena workgroup # (partition projection on dt; no crawler). TODO # Phase 4 — slack-post Lambda + scoped IAM. TODO