timelooker_stack.py•13.7 kB
from aws_cdk import (
Stack,
aws_ec2 as ec2,
aws_rds as rds,
aws_s3 as s3,
aws_ses as ses,
aws_secretsmanager as secretsmanager,
aws_iam as iam,
CfnOutput,
RemovalPolicy,
Duration
)
from constructs import Construct
class TimeLookerStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# VPC with public subnets only (no NAT Gateway needed)
self.vpc = ec2.Vpc(
self,
"TimeLookerVPC",
max_azs=2,
nat_gateways=0, # No NAT Gateway to save costs
subnet_configuration=[
ec2.SubnetConfiguration(
subnet_type=ec2.SubnetType.PUBLIC,
name="PublicSubnet",
cidr_mask=24
)
]
)
# Security group for RDS
self.db_security_group = ec2.SecurityGroup(
self,
"TimeLookerDBSecurityGroup",
vpc=self.vpc,
description="Security group for TimeLooker RDS database",
allow_all_outbound=False
)
# Allow PostgreSQL access from Lambda (and development)
self.db_security_group.add_ingress_rule(
peer=ec2.Peer.any_ipv4(), # Public access with security group
connection=ec2.Port.tcp(5432),
description="PostgreSQL access"
)
# RDS PostgreSQL Database
self.database = rds.DatabaseInstance(
self,
"TimeLookerDatabase",
engine=rds.DatabaseInstanceEngine.postgres(
version=rds.PostgresEngineVersion.VER_15_13
),
instance_type=ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE3,
ec2.InstanceSize.MICRO # db.t3.micro - cheapest option
),
database_name="timelooker",
credentials=rds.Credentials.from_generated_secret(
"timelookeradmin", # No hyphens allowed in RDS username
secret_name="timelooker/database/credentials"
),
vpc=self.vpc,
vpc_subnets=ec2.SubnetSelection(
subnet_type=ec2.SubnetType.PUBLIC
),
security_groups=[self.db_security_group],
allocated_storage=20, # 20 GB minimum
storage_type=rds.StorageType.GP2,
backup_retention=Duration.days(1), # Minimal backup for dev
deletion_protection=False, # Allow deletion for dev environment
publicly_accessible=True, # Public access via security group
removal_policy=RemovalPolicy.DESTROY # Allow CDK to delete
)
# S3 Bucket for email templates
self.email_templates_bucket = s3.Bucket(
self,
"TimeLookerEmailTemplates",
bucket_name=f"timelooker-email-templates-{self.account}-{self.region}",
versioned=False,
public_read_access=False,
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True # Clean up on stack deletion
)
# Note: Email template will be uploaded separately after deployment
# The template is defined in _get_default_email_template() method below
# Secrets Manager for API keys
self.openai_secret = secretsmanager.Secret(
self,
"TimeLookerOpenAISecret",
secret_name="timelooker/openai/api-key",
description="OpenAI API key for TimeLooker search comparison",
generate_secret_string=secretsmanager.SecretStringGenerator(
secret_string_template='{"api_key": ""}',
generate_string_key="api_key",
exclude_characters=' "\\/@'
)
)
self.anthropic_secret = secretsmanager.Secret(
self,
"TimeLookerAnthropicSecret",
secret_name="timelooker/anthropic/api-key",
description="Anthropic API key for TimeLooker alternative search engine",
generate_secret_string=secretsmanager.SecretStringGenerator(
secret_string_template='{"api_key": ""}',
generate_string_key="api_key",
exclude_characters=' "\\/@'
)
)
self.x402_secret = secretsmanager.Secret(
self,
"TimeLookerX402Secret",
secret_name="timelooker/x402/private-key",
description="X402 private key for payment processing",
generate_secret_string=secretsmanager.SecretStringGenerator(
secret_string_template='{"private_key": ""}',
generate_string_key="private_key",
exclude_characters=' "\\/@'
)
)
# IAM role for Lambda functions
self.lambda_execution_role = iam.Role(
self,
"TimeLookerLambdaExecutionRole",
assumed_by=iam.CompositePrincipal(
iam.ServicePrincipal("lambda.amazonaws.com"),
iam.ServicePrincipal("scheduler.amazonaws.com") # Allow EventBridge Scheduler
),
managed_policies=[
iam.ManagedPolicy.from_aws_managed_policy_name(
"service-role/AWSLambdaBasicExecutionRole"
)
]
)
# Add permissions for Lambda to access resources
self.lambda_execution_role.add_to_policy(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"secretsmanager:GetSecretValue"
],
resources=[
self.openai_secret.secret_arn,
self.anthropic_secret.secret_arn,
self.x402_secret.secret_arn,
self.database.secret.secret_arn
]
)
)
self.lambda_execution_role.add_to_policy(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"s3:GetObject"
],
resources=[
f"{self.email_templates_bucket.bucket_arn}/*"
]
)
)
self.lambda_execution_role.add_to_policy(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"ses:SendEmail",
"ses:SendRawEmail"
],
resources=["*"] # SES requires wildcard for email sending
)
)
# SES Email Identity (will need manual verification)
# Note: This creates the identity but email verification must be done manually
self.ses_email_identity = ses.CfnEmailIdentity(
self,
"TimeLookerSESIdentity",
email_identity="fortnightlydevs@gmail.com" # Replace with your domain
)
# IAM role for the main application to manage Lambda functions
self.app_execution_role = iam.Role(
self,
"TimeLookerAppExecutionRole",
assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
managed_policies=[
iam.ManagedPolicy.from_aws_managed_policy_name(
"service-role/AWSLambdaBasicExecutionRole"
)
]
)
# Add permissions for the app to manage Lambda functions and EventBridge
self.app_execution_role.add_to_policy(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"lambda:CreateFunction",
"lambda:DeleteFunction",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:GetFunction",
"lambda:InvokeFunction",
"lambda:AddPermission",
"lambda:RemovePermission"
],
resources=["*"]
)
)
self.app_execution_role.add_to_policy(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"events:PutRule",
"events:DeleteRule",
"events:PutTargets",
"events:RemoveTargets",
"events:DescribeRule",
"scheduler:CreateSchedule",
"scheduler:DeleteSchedule",
"scheduler:UpdateSchedule",
"scheduler:GetSchedule"
],
resources=["*"]
)
)
self.app_execution_role.add_to_policy(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"iam:PassRole"
],
resources=[self.lambda_execution_role.role_arn]
)
)
# Outputs for use in the application
CfnOutput(
self,
"DatabaseEndpoint",
value=self.database.instance_endpoint.hostname,
description="RDS PostgreSQL endpoint"
)
CfnOutput(
self,
"DatabasePort",
value=str(self.database.instance_endpoint.port),
description="RDS PostgreSQL port"
)
CfnOutput(
self,
"DatabaseName",
value="timelooker",
description="Database name"
)
CfnOutput(
self,
"DatabaseSecretArn",
value=self.database.secret.secret_arn,
description="ARN of the database credentials secret"
)
CfnOutput(
self,
"EmailTemplatesBucket",
value=self.email_templates_bucket.bucket_name,
description="S3 bucket for email templates"
)
CfnOutput(
self,
"OpenAISecretArn",
value=self.openai_secret.secret_arn,
description="ARN of the OpenAI API key secret"
)
CfnOutput(
self,
"AnthropicSecretArn",
value=self.anthropic_secret.secret_arn,
description="ARN of the Anthropic API key secret"
)
CfnOutput(
self,
"X402SecretArn",
value=self.x402_secret.secret_arn,
description="ARN of the X402 private key secret"
)
CfnOutput(
self,
"LambdaExecutionRoleArn",
value=self.lambda_execution_role.role_arn,
description="ARN of the Lambda execution role"
)
CfnOutput(
self,
"AppExecutionRoleArn",
value=self.app_execution_role.role_arn,
description="ARN of the application execution role"
)
CfnOutput(
self,
"SESEmailIdentity",
value=self.ses_email_identity.email_identity,
description="SES email identity (requires manual verification)"
)
def _get_default_email_template(self) -> str:
"""Default HTML email template for notifications."""
return """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TimeLooker Notification</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #2c3e50; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; }
.content { background: #f8f9fa; padding: 20px; border: 1px solid #dee2e6; }
.footer { background: #6c757d; color: white; padding: 15px; text-align: center; border-radius: 0 0 5px 5px; font-size: 0.9em; }
.item { background: white; margin: 10px 0; padding: 15px; border-left: 4px solid #007bff; border-radius: 3px; }
.item h3 { margin: 0 0 10px 0; color: #007bff; }
.item .meta { color: #6c757d; font-size: 0.9em; margin: 5px 0; }
.item .description { margin: 10px 0; }
.item .url { word-break: break-all; }
</style>
</head>
<body>
<div class="header">
<h1>🔍 TimeLooker Notification</h1>
<p>New items found for your monitoring task</p>
</div>
<div class="content">
<h2>Task: {{task_description}}</h2>
<p><strong>Time:</strong> {{timestamp}}</p>
<p><strong>New items found:</strong> {{new_items_count}}</p>
<h3>New Items:</h3>
{{#new_items}}
<div class="item">
<h3>{{name}}</h3>
<div class="meta">
<strong>Source:</strong> {{source}} |
<strong>Location:</strong> {{location}}
</div>
{{#description}}
<div class="description">{{description}}</div>
{{/description}}
{{#url}}
<div class="meta">
<strong>URL:</strong> <a href="{{url}}" target="_blank">{{url}}</a>
</div>
{{/url}}
{{#additional_info}}
<div class="meta">
<strong>Additional Info:</strong> {{additional_info}}
</div>
{{/additional_info}}
</div>
{{/new_items}}
</div>
<div class="footer">
<p>This is an automated message from TimeLooker MCP Server</p>
<p>Powered by AWS Lambda, RDS, and SES</p>
</div>
</body>
</html>"""