| |
| """ |
| AWS Deployment Script for FRED ML |
| Deploys Lambda function, S3 bucket, and EventBridge rule |
| """ |
|
|
| import boto3 |
| import json |
| import os |
| import zipfile |
| import tempfile |
| import shutil |
| from pathlib import Path |
| import argparse |
| import logging |
|
|
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| class FredMLDeployer: |
| def __init__(self, region='us-east-1'): |
| """Initialize the deployer with AWS clients""" |
| self.region = region |
| self.cloudformation = boto3.client('cloudformation', region_name=region) |
| self.s3 = boto3.client('s3', region_name=region) |
| self.lambda_client = boto3.client('lambda', region_name=region) |
| self.ssm = boto3.client('ssm', region_name=region) |
| |
| def create_lambda_package(self, source_dir: str, output_path: str): |
| """Create Lambda deployment package""" |
| logger.info("Creating Lambda deployment package...") |
| |
| with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: |
| |
| for root, dirs, files in os.walk(source_dir): |
| for file in files: |
| if file.endswith('.py'): |
| file_path = os.path.join(root, file) |
| arcname = os.path.relpath(file_path, source_dir) |
| zipf.write(file_path, arcname) |
| |
| |
| requirements_path = os.path.join(source_dir, 'requirements.txt') |
| if os.path.exists(requirements_path): |
| zipf.write(requirements_path, 'requirements.txt') |
| |
| def deploy_s3_bucket(self, stack_name: str, bucket_name: str): |
| """Deploy S3 bucket using CloudFormation""" |
| logger.info(f"Deploying S3 bucket: {bucket_name}") |
| |
| template_path = Path(__file__).parent.parent / 'infrastructure' / 's3' / 'bucket.yaml' |
| |
| with open(template_path, 'r') as f: |
| template_body = f.read() |
| |
| try: |
| response = self.cloudformation.create_stack( |
| StackName=stack_name, |
| TemplateBody=template_body, |
| Parameters=[ |
| { |
| 'ParameterKey': 'BucketName', |
| 'ParameterValue': bucket_name |
| } |
| ], |
| Capabilities=['CAPABILITY_NAMED_IAM'] |
| ) |
| |
| logger.info(f"Stack creation initiated: {response['StackId']}") |
| return response['StackId'] |
| |
| except self.cloudformation.exceptions.AlreadyExistsException: |
| logger.info(f"Stack {stack_name} already exists, updating...") |
| |
| response = self.cloudformation.update_stack( |
| StackName=stack_name, |
| TemplateBody=template_body, |
| Parameters=[ |
| { |
| 'ParameterKey': 'BucketName', |
| 'ParameterValue': bucket_name |
| } |
| ], |
| Capabilities=['CAPABILITY_NAMED_IAM'] |
| ) |
| |
| logger.info(f"Stack update initiated: {response['StackId']}") |
| return response['StackId'] |
| |
| def deploy_lambda_function(self, function_name: str, s3_bucket: str, api_key: str): |
| """Deploy Lambda function""" |
| logger.info(f"Deploying Lambda function: {function_name}") |
| |
| |
| lambda_dir = Path(__file__).parent.parent / 'lambda' |
| package_path = tempfile.mktemp(suffix='.zip') |
| |
| try: |
| self.create_lambda_package(str(lambda_dir), package_path) |
| |
| |
| try: |
| self.ssm.put_parameter( |
| Name='/fred-ml/api-key', |
| Value=api_key, |
| Type='SecureString', |
| Overwrite=True |
| ) |
| logger.info("Updated FRED API key in SSM") |
| except Exception as e: |
| logger.error(f"Failed to update API key: {e}") |
| |
| |
| with open(package_path, 'rb') as f: |
| code = f.read() |
| |
| try: |
| |
| self.lambda_client.update_function_code( |
| FunctionName=function_name, |
| ZipFile=code |
| ) |
| logger.info(f"Updated Lambda function: {function_name}") |
| |
| except self.lambda_client.exceptions.ResourceNotFoundException: |
| |
| template_path = Path(__file__).parent.parent / 'infrastructure' / 'lambda' / 'function.yaml' |
| |
| with open(template_path, 'r') as f: |
| template_body = f.read() |
| |
| |
| template_body = template_body.replace( |
| 'import json\ndef lambda_handler(event, context):\n return {\n \'statusCode\': 200,\n \'body\': json.dumps(\'Hello from Lambda!\')\n }', |
| 'import json\ndef lambda_handler(event, context):\n return {\n \'statusCode\': 200,\n \'body\': json.dumps(\'FRED ML Lambda Function\')\n }' |
| ) |
| |
| stack_name = f"{function_name}-stack" |
| |
| response = self.cloudformation.create_stack( |
| StackName=stack_name, |
| TemplateBody=template_body, |
| Parameters=[ |
| { |
| 'ParameterKey': 'FunctionName', |
| 'ParameterValue': function_name |
| }, |
| { |
| 'ParameterKey': 'S3BucketName', |
| 'ParameterValue': s3_bucket |
| } |
| ], |
| Capabilities=['CAPABILITY_NAMED_IAM'] |
| ) |
| |
| logger.info(f"Lambda stack creation initiated: {response['StackId']}") |
| |
| finally: |
| |
| if os.path.exists(package_path): |
| os.remove(package_path) |
| |
| def deploy_eventbridge_rule(self, stack_name: str, lambda_function: str, s3_bucket: str): |
| """Deploy EventBridge rule for quarterly scheduling""" |
| logger.info(f"Deploying EventBridge rule: {stack_name}") |
| |
| template_path = Path(__file__).parent.parent / 'infrastructure' / 'eventbridge' / 'quarterly-rule.yaml' |
| |
| with open(template_path, 'r') as f: |
| template_body = f.read() |
| |
| try: |
| response = self.cloudformation.create_stack( |
| StackName=stack_name, |
| TemplateBody=template_body, |
| Parameters=[ |
| { |
| 'ParameterKey': 'LambdaFunctionName', |
| 'ParameterValue': lambda_function |
| }, |
| { |
| 'ParameterKey': 'S3BucketName', |
| 'ParameterValue': s3_bucket |
| } |
| ], |
| Capabilities=['CAPABILITY_NAMED_IAM'] |
| ) |
| |
| logger.info(f"EventBridge stack creation initiated: {response['StackId']}") |
| return response['StackId'] |
| |
| except self.cloudformation.exceptions.AlreadyExistsException: |
| logger.info(f"Stack {stack_name} already exists, updating...") |
| |
| response = self.cloudformation.update_stack( |
| StackName=stack_name, |
| TemplateBody=template_body, |
| Parameters=[ |
| { |
| 'ParameterKey': 'LambdaFunctionName', |
| 'ParameterValue': lambda_function |
| }, |
| { |
| 'ParameterKey': 'S3BucketName', |
| 'ParameterValue': s3_bucket |
| } |
| ], |
| Capabilities=['CAPABILITY_NAMED_IAM'] |
| ) |
| |
| logger.info(f"EventBridge stack update initiated: {response['StackId']}") |
| return response['StackId'] |
| |
| def wait_for_stack_completion(self, stack_name: str): |
| """Wait for CloudFormation stack to complete""" |
| logger.info(f"Waiting for stack {stack_name} to complete...") |
| |
| waiter = self.cloudformation.get_waiter('stack_create_complete') |
| try: |
| waiter.wait(StackName=stack_name) |
| logger.info(f"Stack {stack_name} completed successfully") |
| except Exception as e: |
| logger.error(f"Stack {stack_name} failed: {e}") |
| raise |
| |
| def deploy_all(self, bucket_name: str, function_name: str, api_key: str): |
| """Deploy all components""" |
| logger.info("Starting FRED ML deployment...") |
| |
| try: |
| |
| s3_stack_name = f"{bucket_name}-stack" |
| self.deploy_s3_bucket(s3_stack_name, bucket_name) |
| self.wait_for_stack_completion(s3_stack_name) |
| |
| |
| self.deploy_lambda_function(function_name, bucket_name, api_key) |
| |
| |
| eventbridge_stack_name = f"{function_name}-eventbridge-stack" |
| self.deploy_eventbridge_rule(eventbridge_stack_name, function_name, bucket_name) |
| self.wait_for_stack_completion(eventbridge_stack_name) |
| |
| logger.info("FRED ML deployment completed successfully!") |
| |
| except Exception as e: |
| logger.error(f"Deployment failed: {e}") |
| raise |
|
|
| def main(): |
| parser = argparse.ArgumentParser(description='Deploy FRED ML to AWS') |
| parser.add_argument('--region', default='us-west-2', help='AWS region') |
| parser.add_argument('--bucket', default='fredmlv1', help='S3 bucket name') |
| parser.add_argument('--function', default='fred-ml-processor', help='Lambda function name') |
| parser.add_argument('--api-key', required=True, help='FRED API key') |
| |
| args = parser.parse_args() |
| |
| deployer = FredMLDeployer(region=args.region) |
| deployer.deploy_all(args.bucket, args.function, args.api_key) |
|
|
| if __name__ == "__main__": |
| main() |