diff --git a/.dockerignore b/.dockerignore index f6e1422..3a359cf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,3 @@ -secrets \ No newline at end of file +secrets +secrets.json +.vscode \ No newline at end of file diff --git a/.github/workflows/github_cd.yaml b/.github/workflows/github_cd.yaml index 0de73da..3c98749 100644 --- a/.github/workflows/github_cd.yaml +++ b/.github/workflows/github_cd.yaml @@ -1,4 +1,4 @@ -name: Upload Python Package +name: Continuous Deployment Workflow on: release: @@ -28,6 +28,7 @@ jobs: with: name: release-dists path: dist/ + pypi-publish: runs-on: ubuntu-latest needs: @@ -45,3 +46,65 @@ jobs: path: dist/ - name: Publish release distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + upload-docker-images: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v3 + with: + python-version: "3.12" + - name: Install Hatch + run: | + python -m pip install --upgrade pip + pip install Hatch + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4.1.0 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + - name: Build Docker images + run: | + # Get the latest commit hash + GIT_COMMIT=$(git rev-parse --short HEAD) + # Define public ECR repository URL + PUBLIC_ECR_URL=public.ecr.aws/e7b5q5z5/nam685 + # LangGraph API server + langgraph build -t chatbot -c langgraph.json + docker tag chatbot:latest $PUBLIC_ECR_URL/chatbot:$GIT_COMMIT + # API + docker build -t chatbot-api -f api/Dockerfile . + docker tag chatbot-api:latest $PUBLIC_ECR_URL/chatbot-api:$GIT_COMMIT + # UI + docker build -t chatbot-ui -f ui/Dockerfile ./ui + docker tag chatbot-ui:latest $PUBLIC_ECR_URL/chatbot-ui:$GIT_COMMIT + - name: Log in to Amazon ECR + run: | + aws ecr-public get-login-password --region us-east-1 | \ + docker login --username AWS --password-stdin ublic.ecr.aws + - name: Push Docker images to ECR + run: | + PUBLIC_ECR_URL=public.ecr.aws/e7b5q5z5/nam685 + docker push $PUBLIC_ECR_URL/chatbot:$GIT_COMMIT + docker push $PUBLIC_ECR_URL/chatbot-api:$GIT_COMMIT + docker push $PUBLIC_ECR_URL/chatbot-ui:$GIT_COMMIT + + deploy-ecs-by-cloudformation: + runs-on: ubuntu-latest + needs: + - upload-docker-images + steps: + - uses: actions/checkout@v4 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4.1.0 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + - name: Deploy CloudFormation stack + uses: aws-actions/aws-cloudformation-github-deploy@v1 + with: + name: chatbot-ecs + template: ecs-infrastructure.yaml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 66df350..e3f2adf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ __pycache__/ .ruff_cache .langgraph_api dist -ui/node_modules \ No newline at end of file +ui/node_modules +secrets.json +deployment-steps.md \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index 0ea30a6..8f86fd1 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12 +FROM python:3.12-alpine WORKDIR /code COPY ./pyproject.toml /code/pyproject.toml diff --git a/api/healthcheck.py b/api/healthcheck.py new file mode 100644 index 0000000..9a04f19 --- /dev/null +++ b/api/healthcheck.py @@ -0,0 +1,20 @@ +import os +import urllib.request + + +def health_check(): + port = os.getenv("CONTAINER_PORT", "8080") + url = f"http://localhost:{port}/health" + try: + with urllib.request.urlopen(url) as response: + status_code = response.getcode() + body = response.read().decode() + print(f"Status code: {status_code}") + print(f"Response body: {body}") + except Exception as e: + print(f"Health check failed: {e}") + exit(1) + + +if __name__ == "__main__": + health_check() diff --git a/api/main.py b/api/main.py index b90d8ac..96e1f02 100644 --- a/api/main.py +++ b/api/main.py @@ -1,3 +1,5 @@ +import os + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -6,9 +8,7 @@ app = FastAPI() origins = [ - "http://127.0.0.1:3000", - "http://localhost:3000", - "http://ui:3000", + os.getenv("UI_URL", "http://127.0.0.1:3000"), ] app.add_middleware( diff --git a/api/routers.py b/api/routers.py index 2e0d9a0..da2c5d2 100644 --- a/api/routers.py +++ b/api/routers.py @@ -1,3 +1,4 @@ +import os from typing import Any from fastapi import APIRouter @@ -6,7 +7,9 @@ from .models import ChatMessage, HumanReview, Thread router = APIRouter() -langgraph_client = get_client(url="http://langgraph-api:8000") +langgraph_client = get_client( + url=os.getenv("LANGGRAPH_API_URI", "http://langgraph-api:8000") +) def parse_ai_response( @@ -34,6 +37,11 @@ async def read_main() -> dict: return {"msg": "Hello! Welcome to the LangGraph Chat API"} +@router.get("/health") +async def health_check() -> dict: + return {"status": "ok"} + + @router.get("/chat") async def list_chat_threads() -> list[Thread]: threads_data = await langgraph_client.threads.search( diff --git a/compose.yaml b/compose.yaml index a17be38..34a7271 100644 --- a/compose.yaml +++ b/compose.yaml @@ -37,15 +37,46 @@ services: env_file: - .env environment: + CONTAINER_PORT: 8000 REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable + healthcheck: + test: [ "CMD", "python", "src/chatbot/healthcheck.py" ] + interval: 10s + timeout: 1s + retries: 5 + start_period: 10s api: image: chatbot-api:latest ports: - "8080:8080" + environment: + CONTAINER_PORT: 8080 + UI_URL: http://localhost:3000 + LANGGRAPH_API_URI: http://langgraph-api:8000 + depends_on: + langgraph-api: + condition: service_healthy + healthcheck: + test: [ "CMD", "python", "api/healthcheck.py" ] + start_period: 3s + timeout: 1s + retries: 5 + interval: 5s ui: image: chatbot-ui:latest ports: - "3000:3000" environment: - - HOSTNAME=0.0.0.0 + HOSTNAME: 0.0.0.0 + CONTAINER_PORT: 3000 + API_URL: http://localhost:8080 + depends_on: + api: + condition: service_healthy + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:3000/health" ] + start_period: 3s + timeout: 1s + retries: 5 + interval: 5s diff --git a/ecs-infrastructure.yaml b/ecs-infrastructure.yaml new file mode 100644 index 0000000..ba0ca40 --- /dev/null +++ b/ecs-infrastructure.yaml @@ -0,0 +1,736 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'ECS Infrastructure for Chatbot Application' + +Parameters: + VpcCidr: + Type: String + Default: '10.0.0.0/16' + + ECRRepositoryURI: + Type: String + Description: 'Base ECR repository URI (without image name)' + Default: 'public.ecr.aws/e7b5q5z5/nam685' + + SecretARN: + Type: String + Description: Secret ARN for chatbot application + Default: 'arn:aws:secretsmanager:eu-central-1:417744795771:secret:chatbot-secrets-TXOVcr' + +Resources: + # VPC and Networking + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: !Ref VpcCidr + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: chatbot-vpc + + InternetGateway: + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Name + Value: chatbot-igw + + AttachGateway: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: !Ref VPC + InternetGatewayId: !Ref InternetGateway + + PublicSubnet: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: '10.0.1.0/24' + AvailabilityZone: !Select [0, !GetAZs ''] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: chatbot-public-subnet + + PrivateSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: '10.0.2.0/24' + AvailabilityZone: !Select [0, !GetAZs ''] + Tags: + - Key: Name + Value: chatbot-private-subnet-1 + + PrivateSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: '10.0.3.0/24' + AvailabilityZone: !Select [1, !GetAZs ''] + Tags: + - Key: Name + Value: chatbot-private-subnet-2 + + NATGateway: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt EIPForNAT.AllocationId + SubnetId: !Ref PublicSubnet + Tags: + - Key: Name + Value: chatbot-nat + + EIPForNAT: + Type: AWS::EC2::EIP + DependsOn: AttachGateway + Properties: + Domain: vpc + + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: chatbot-public-rt + + PrivateRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: chatbot-private-rt + + PublicRoute: + Type: AWS::EC2::Route + DependsOn: AttachGateway + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: '0.0.0.0/0' + GatewayId: !Ref InternetGateway + + PrivateRoute: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTable + DestinationCidrBlock: '0.0.0.0/0' + NatGatewayId: !Ref NATGateway + + PublicSubnetRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet + RouteTableId: !Ref PublicRouteTable + + PrivateSubnet1RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PrivateSubnet1 + RouteTableId: !Ref PrivateRouteTable + + PrivateSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PrivateSubnet2 + RouteTableId: !Ref PrivateRouteTable + + # Security Groups + ALBSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for Application Load Balancer + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: '0.0.0.0/0' + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: '0.0.0.0/0' + + ECSSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for ECS tasks + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 3000 + ToPort: 3000 + SourceSecurityGroupId: !Ref ALBSecurityGroup + - IpProtocol: tcp + FromPort: 8080 + ToPort: 8080 + SourceSecurityGroupId: !Ref ECSSecurityGroup + - IpProtocol: tcp + FromPort: 8000 + ToPort: 8000 + SourceSecurityGroupId: !Ref ECSSecurityGroup + - IpProtocol: tcp + FromPort: 5432 + ToPort: 5432 + SourceSecurityGroupId: !Ref ECSSecurityGroup + - IpProtocol: tcp + FromPort: 6379 + ToPort: 6379 + SourceSecurityGroupId: !Ref ECSSecurityGroup + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: '0.0.0.0/0' + + # EFS for PostgreSQL data persistence + EFSFileSystem: + Type: AWS::EFS::FileSystem + Properties: + CreationToken: chatbot-postgres-data + FileSystemTags: + - Key: Name + Value: chatbot-postgres-data + + EFSMountTarget1: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref EFSFileSystem + SubnetId: !Ref PrivateSubnet1 + SecurityGroups: + - !Ref EFSSecurityGroup + + EFSMountTarget2: + Type: AWS::EFS::MountTarget + Properties: + FileSystemId: !Ref EFSFileSystem + SubnetId: !Ref PrivateSubnet2 + SecurityGroups: + - !Ref EFSSecurityGroup + + EFSSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for EFS + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 2049 + ToPort: 2049 + SourceSecurityGroupId: !Ref ECSSecurityGroup + + # IAM Roles + ECSTaskExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: SecretsManagerAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref SecretARN + + # ECS Cluster + ECSCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: chatbot-cluster + CapacityProviders: + - EC2 + DefaultCapacityProviderStrategy: + - CapacityProvider: EC2 + Weight: 1 + + # Auto Scaling Group for EC2 instances + LaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Properties: + LaunchTemplateName: chatbot-ecs-lt + LaunchTemplateData: + ImageId: ami-0c02fb55956c7d316 # Amazon Linux 2 ECS-optimized + InstanceType: t2.micro + IamInstanceProfile: + Arn: !GetAtt EC2InstanceProfile.Arn + SecurityGroupIds: + - !Ref ECSSecurityGroup + UserData: + Fn::Base64: !Sub | + #!/bin/bash + echo ECS_CLUSTER=${ECSCluster} >> /etc/ecs/ecs.config + + AutoScalingGroup: + Type: AWS::AutoScaling::AutoScalingGroup + Properties: + VPCZoneIdentifier: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + LaunchTemplate: + LaunchTemplateId: !Ref LaunchTemplate + Version: !GetAtt LaunchTemplate.LatestVersionNumber + MinSize: 1 + MaxSize: 3 + DesiredCapacity: 1 + Tags: + - Key: Name + Value: chatbot-ecs-instance + PropagateAtLaunch: true + + EC2Role: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: ec2.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role + + EC2InstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: + Roles: + - !Ref EC2Role + + # Service Discovery + ServiceDiscoveryNamespace: + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Properties: + Name: chatbot.local + Vpc: !Ref VPC + + # Task Definitions + PostgresTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: chatbot-postgres + NetworkMode: awsvpc + RequiresCompatibilities: + - EC2 + ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn + ContainerDefinitions: + - Name: postgres + Image: postgres:16 + Memory: 512 + Essential: true + Environment: + - Name: POSTGRES_DB + Value: postgres + - Name: POSTGRES_USER + Value: postgres + Secrets: + - Name: POSTGRES_PASSWORD + ValueFrom: !Sub "${SecretARN}:POSTGRES_PASSWORD::" + MountPoints: + - SourceVolume: postgres-data + ContainerPath: /var/lib/postgresql/data + HealthCheck: + Command: + - CMD-SHELL + - pg_isready -U postgres + Interval: 30 + Timeout: 5 + Retries: 3 + StartPeriod: 60 + PortMappings: + - ContainerPort: 5432 + Protocol: tcp + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: postgres + Volumes: + - Name: postgres-data + EFSVolumeConfiguration: + FileSystemId: !Ref EFSFileSystem + + RedisTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: chatbot-redis + NetworkMode: awsvpc + RequiresCompatibilities: + - EC2 + ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn + ContainerDefinitions: + - Name: redis + Image: redis:6 + Memory: 256 + Essential: true + HealthCheck: + Command: + - CMD-SHELL + - redis-cli ping + Interval: 30 + Timeout: 5 + Retries: 3 + PortMappings: + - ContainerPort: 6379 + Protocol: tcp + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: redis + + LangGraphTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: chatbot-langgraph + NetworkMode: awsvpc + RequiresCompatibilities: + - EC2 + ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn + ContainerDefinitions: + - Name: langgraph-api + Image: !Sub "${ECRRepositoryURI}/chatbot:latest" + Memory: 1024 + Essential: true + Environment: + - Name: CONTAINER_PORT + Value: "8000" + - Name: REDIS_URI + Value: redis://redis.chatbot.local:6379 + - Name: POSTGRES_URI + Value: postgres://postgres:password@postgres.chatbot.local:5432/postgres?sslmode=disable + - Name: LANGSMITH_TRACING + Value: "true" + - Name: LANGSMITH_PROJECT + Value: chatbot + - Name: LANGSMITH_ENDPOINT + Value: https://eu.api.smith.langchain.com + Secrets: + - Name: LANGSMITH_API_KEY + ValueFrom: !Sub "${SecretARN}:LANGSMITH_API_KEY::" + - Name: OPENAI_API_KEY + ValueFrom: !Sub "${SecretARN}:OPENAI_API_KEY::" + - Name: POSTGRES_PASSWORD + ValueFrom: !Sub "${SecretARN}:POSTGRES_PASSWORD::" + HealthCheck: + Command: + - CMD-SHELL + - python src/chatbot/healthcheck.py + Interval: 30 + Timeout: 5 + Retries: 3 + StartPeriod: 60 + PortMappings: + - ContainerPort: 8000 + Protocol: tcp + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: langgraph + + FastAPITaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: chatbot-fastapi + NetworkMode: awsvpc + RequiresCompatibilities: + - EC2 + ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn + ContainerDefinitions: + - Name: fastapi + Image: !Sub "${ECRRepositoryURI}/chatbot-api:latest" + Memory: 512 + Essential: true + Environment: + - Name: CONTAINER_PORT + Value: "8080" + - Name: UI_URL + Value: http://ui.chatbot.local:3000 + - Name: LANGGRAPH_API_URI + Value: http://langgraph-api.chatbot.local:8000 + HealthCheck: + Command: + - CMD-SHELL + - python api/healthcheck.py + Interval: 30 + Timeout: 5 + Retries: 3 + StartPeriod: 30 + PortMappings: + - ContainerPort: 8080 + Protocol: tcp + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: fastapi + + NextJSTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: chatbot-nextjs + NetworkMode: awsvpc + RequiresCompatibilities: + - EC2 + ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn + ContainerDefinitions: + - Name: nextjs + Image: !Sub "${ECRRepositoryURI}/chatbot-ui:latest" + Memory: 512 + Essential: true + Environment: + - Name: HOSTNAME + Value: 0.0.0.0 + - Name: CONTAINER_PORT + Value: "3000" + - Name: API_URL + Value: http://fastapi.chatbot.local:8080 + HealthCheck: + Command: + - CMD-SHELL + - curl -f http://localhost:3000/health + Interval: 30 + Timeout: 5 + Retries: 3 + StartPeriod: 30 + PortMappings: + - ContainerPort: 3000 + Protocol: tcp + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: nextjs + + # CloudWatch Log Group + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: /ecs/chatbot + RetentionInDays: 3 + + # ECS Services + PostgresService: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref ECSCluster + TaskDefinition: !Ref PostgresTaskDefinition + DesiredCount: 1 + LaunchType: EC2 + NetworkConfiguration: + AwsvpcConfiguration: + SecurityGroups: + - !Ref ECSSecurityGroup + Subnets: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + ServiceRegistries: + - RegistryArn: !GetAtt PostgresServiceDiscovery.Arn + + PostgresServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: postgres + DnsConfig: + NamespaceId: !Ref ServiceDiscoveryNamespace + DnsRecords: + - Type: A + TTL: 60 + HealthCheckCustomConfig: + FailureThreshold: 1 + + RedisService: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref ECSCluster + TaskDefinition: !Ref RedisTaskDefinition + DesiredCount: 1 + LaunchType: EC2 + NetworkConfiguration: + AwsvpcConfiguration: + SecurityGroups: + - !Ref ECSSecurityGroup + Subnets: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + ServiceRegistries: + - RegistryArn: !GetAtt RedisServiceDiscovery.Arn + + RedisServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: redis + DnsConfig: + NamespaceId: !Ref ServiceDiscoveryNamespace + DnsRecords: + - Type: A + TTL: 60 + HealthCheckCustomConfig: + FailureThreshold: 1 + + LangGraphService: + Type: AWS::ECS::Service + DependsOn: + - PostgresService + - RedisService + Properties: + Cluster: !Ref ECSCluster + TaskDefinition: !Ref LangGraphTaskDefinition + DesiredCount: 1 + LaunchType: EC2 + NetworkConfiguration: + AwsvpcConfiguration: + SecurityGroups: + - !Ref ECSSecurityGroup + Subnets: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + ServiceRegistries: + - RegistryArn: !GetAtt LangGraphServiceDiscovery.Arn + + LangGraphServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: langgraph-api + DnsConfig: + NamespaceId: !Ref ServiceDiscoveryNamespace + DnsRecords: + - Type: A + TTL: 60 + HealthCheckCustomConfig: + FailureThreshold: 1 + + FastAPIService: + Type: AWS::ECS::Service + DependsOn: LangGraphService + Properties: + Cluster: !Ref ECSCluster + TaskDefinition: !Ref FastAPITaskDefinition + DesiredCount: 1 + LaunchType: EC2 + NetworkConfiguration: + AwsvpcConfiguration: + SecurityGroups: + - !Ref ECSSecurityGroup + Subnets: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + ServiceRegistries: + - RegistryArn: !GetAtt FastAPIServiceDiscovery.Arn + + FastAPIServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: fastapi + DnsConfig: + NamespaceId: !Ref ServiceDiscoveryNamespace + DnsRecords: + - Type: A + TTL: 60 + HealthCheckCustomConfig: + FailureThreshold: 1 + + NextJSService: + Type: AWS::ECS::Service + DependsOn: FastAPIService + Properties: + Cluster: !Ref ECSCluster + TaskDefinition: !Ref NextJSTaskDefinition + DesiredCount: 1 + LaunchType: EC2 + NetworkConfiguration: + AwsvpcConfiguration: + SecurityGroups: + - !Ref ECSSecurityGroup + Subnets: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + LoadBalancers: + - ContainerName: nextjs + ContainerPort: 3000 + TargetGroupArn: !Ref ALBTargetGroup + ServiceRegistries: + - RegistryArn: !GetAtt NextJSServiceDiscovery.Arn + + NextJSServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: ui + DnsConfig: + NamespaceId: !Ref ServiceDiscoveryNamespace + DnsRecords: + - Type: A + TTL: 60 + HealthCheckCustomConfig: + FailureThreshold: 1 + + # Application Load Balancer + ApplicationLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Name: chatbot-alb + Scheme: internet-facing + Type: application + Subnets: + - !Ref PublicSubnet + - !Ref PrivateSubnet2 # Need second public subnet for ALB + SecurityGroups: + - !Ref ALBSecurityGroup + + ALBTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: chatbot-nextjs-tg + Port: 3000 + Protocol: HTTP + VpcId: !Ref VPC + TargetType: ip + HealthCheckPath: /health + HealthCheckProtocol: HTTP + HealthCheckIntervalSeconds: 30 + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 2 + UnhealthyThresholdCount: 3 + + ALBListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + DefaultActions: + - Type: forward + TargetGroupArn: !Ref ALBTargetGroup + LoadBalancerArn: !Ref ApplicationLoadBalancer + Port: 80 + Protocol: HTTP + +Outputs: + LoadBalancerDNS: + Description: DNS name of the load balancer + Value: !GetAtt ApplicationLoadBalancer.DNSName + Export: + Name: !Sub "${AWS::StackName}-LoadBalancerDNS" + + VPCId: + Description: VPC ID + Value: !Ref VPC + Export: + Name: !Sub "${AWS::StackName}-VPC" + + ECSClusterName: + Description: ECS Cluster Name + Value: !Ref ECSCluster + Export: + Name: !Sub "${AWS::StackName}-ECSCluster" \ No newline at end of file diff --git a/src/chatbot/__about__.py b/src/chatbot/__about__.py index e581cd2..b2cc0f9 100644 --- a/src/chatbot/__about__.py +++ b/src/chatbot/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2025-present Nam Le # # SPDX-License-Identifier: MIT -__version__ = "0.0.8" +__version__ = "0.0.9" diff --git a/src/chatbot/healthcheck.py b/src/chatbot/healthcheck.py new file mode 100644 index 0000000..8f8cf6a --- /dev/null +++ b/src/chatbot/healthcheck.py @@ -0,0 +1,20 @@ +import os +import urllib.request + + +def health_check(): + port = os.getenv("CONTAINER_PORT", "8000") + url = f"http://localhost:{port}/ok" + try: + with urllib.request.urlopen(url) as response: + status_code = response.getcode() + body = response.read().decode() + print(f"Status code: {status_code}") + print(f"Response body: {body}") + except Exception as e: + print(f"Health check failed: {e}") + exit(1) + + +if __name__ == "__main__": + health_check() diff --git a/ui/Dockerfile b/ui/Dockerfile index 10bef73..618e1b5 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -60,6 +60,11 @@ EXPOSE 3000 ENV PORT=3000 +# curl for healthcheck# curl for healthcheck +USER root +RUN apk add --no-cache curl +USER nextjs + # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/config/next-config-js/output ENV HOSTNAME="0.0.0.0" diff --git a/ui/app/health/route.ts b/ui/app/health/route.ts new file mode 100644 index 0000000..f7fba3b --- /dev/null +++ b/ui/app/health/route.ts @@ -0,0 +1,5 @@ +export async function GET() { + return new Response(JSON.stringify({ status: 'ok' }), { + headers: { 'Content-Type': 'application/json' }, + }); +} \ No newline at end of file diff --git a/ui/app/utils/chat.tsx b/ui/app/utils/chat.ts similarity index 94% rename from ui/app/utils/chat.tsx rename to ui/app/utils/chat.ts index e3a3c0d..438a897 100644 --- a/ui/app/utils/chat.tsx +++ b/ui/app/utils/chat.ts @@ -1,4 +1,4 @@ -export const apiUrl = 'http://localhost:8080'; +export const apiUrl = process.env.API_URL || 'http://localhost:8080'; export async function fetchThreads() { const res = await fetch(`${apiUrl}/chat`);