From 29536fa3a0096ae0894e84757e6598828cea314d Mon Sep 17 00:00:00 2001 From: BalaSriharsha <> Date: Fri, 25 Apr 2025 03:21:02 +0530 Subject: [PATCH 1/3] Added migrations in helm-charts --- .../sql/postgres/0003_violet_lifeguard.sql | 1 + .../flotorch/sql/postgres/0004_kind_sersi.sql | 1 + docs/charts/flotorch-0.1.0.tgz | Bin 13882 -> 13978 bytes docs/charts/index.yaml | 6 +++--- 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 charts/flotorch/sql/postgres/0003_violet_lifeguard.sql create mode 100644 charts/flotorch/sql/postgres/0004_kind_sersi.sql diff --git a/charts/flotorch/sql/postgres/0003_violet_lifeguard.sql b/charts/flotorch/sql/postgres/0003_violet_lifeguard.sql new file mode 100644 index 0000000..b7d37cb --- /dev/null +++ b/charts/flotorch/sql/postgres/0003_violet_lifeguard.sql @@ -0,0 +1 @@ +ALTER TABLE "models" DROP CONSTRAINT "models_name_unique"; \ No newline at end of file diff --git a/charts/flotorch/sql/postgres/0004_kind_sersi.sql b/charts/flotorch/sql/postgres/0004_kind_sersi.sql new file mode 100644 index 0000000..313542a --- /dev/null +++ b/charts/flotorch/sql/postgres/0004_kind_sersi.sql @@ -0,0 +1 @@ +ALTER TABLE "ragendpoints" ALTER COLUMN "vectorStorageId" DROP NOT NULL; \ No newline at end of file diff --git a/docs/charts/flotorch-0.1.0.tgz b/docs/charts/flotorch-0.1.0.tgz index b7cb95b36d363583f7017e750af7b3b594be89b2..651ad439ab2f9a90a30b7e45955f0256f5f7f382 100644 GIT binary patch delta 13699 zcmV-}HGImtY?^J5Nq?DSHxS5mCf)S=GK6Gk6Osa%>AbspIKmd7#@HTNPKN3BfBzmy zzDTxg3;~j9zU3{I`@R{hj!wc63a0spsFt0n)f z?bY_bs~^^Cdx!h=TK(|5T7B>M_~1KGTX{<~kMB`PzNy zzgL2;8~vrGM4mygOZlq?c#o_pFc?a&dVsSqUGP&$Rsw(*-k!oS0tAh*N6D-N0A??`!S54rbbP#b zd{jGrbG(00HSb6j8Kmla6$Cw6saN3?esyg4;PEm`-+zNHGO@=RI>KY02&P2XyOXeN z_6Gn2xsHda!|LGQ`CAIdC*ZhxP~EFQ%f&X@ukO`L0Jw~;c@)`3kR91@Xd&|iP~s#0 z#=~FH38-IS){3Tujl1`b@1aCf4EABdKIF6~P82t4@36X8-75hAeL4|Yd=HTXLuRjC z=y?wgF@H}0G!Ng_j}C{&`)`kG=)ib8+((D-@C_WoAv9~px{jHU!k*|#?E>>xmNWLm zt3A=X8^@z#f_lGPCVJlf*r%A6E}D`lTd}`{f^XFPo#ww~e3PeY(FJy^9(? zgq})`!sB)rjGF*-a?r%voL+@hR)VB*@7{2n_kSnIL*Ya;pbdryIQNJUY?{ahrcbav z1}1VXXEsGPEdgN8n`R#J*kXGV5oWS)-eB{mUs|2BZuhWDzFxlRU3G6RE+9HLTTRvy zWE*70m;9g6>;zo=`b*>R%a`7#>HZsI|M)9Bvs(w_rup{j-A|Lz*{_U-5VN-Nsm9qbHy|q15$Gc6X^e zJ*LNdh}iN}e(<2{65qBFVLjZ1E-ohkQHJIe+vNlv${|zx-}>IZAPl+3;gGuyp(kL! zR;vZ0LdJ;R#4j256AUUb>;c;;{EZdCoW&5Pwy}&uS*mvnq^n5@fKU z9YM=O<+-^=4lz(AD=&jd&_Zd>#Q@Sxgdl+n>BI|D15n{FPQY&TMkMKTiS>q{5q}n4 zlmTrj#G#Ji zSSO(0yb-bo?+TMMr9$57bm z{^vO)1aa%0cLM4uT1+7|CV!VwyYwxaQxgCog9=KO=)m<)rGl&~=2ikg*rhG?`|3R4*{6QGgBu>h|R%5+@tr&SJDI9}8{wS-Z-}shw<5<|3oq+bJ}kIXNXQDd6OD$Ⓢ0m0yA+_Jg`?Ofj_1kOgNwWPdoe>79T>S$IVR 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 diff --git a/docs/charts/index.yaml b/docs/charts/index.yaml index 2e7a4d5..a5f756f 100644 --- a/docs/charts/index.yaml +++ b/docs/charts/index.yaml @@ -3,12 +3,12 @@ entries: flotorch: - apiVersion: v2 appVersion: 1.0.0 - created: "2025-04-21T14:02:20.262881+05:30" + created: "2025-04-25T03:20:11.653804+05:30" description: A Helm chart for Flotorch Enterprise - digest: a84a847a6fa2381f2912d59941385fc440e14a3d4a9b018b7db9a9af4ebfd7dd + digest: daf17cb81b6a6bbe9119919fdde66eaf8a81c021e775e42b45feea62bfa48563 name: flotorch type: application urls: - https://balasriharsha-ch.github.io/FloTorch/charts/flotorch-0.1.0.tgz version: 0.1.0 -generated: "2025-04-21T14:02:20.262026+05:30" +generated: "2025-04-25T03:20:11.653159+05:30" From e5cc60312425be8c897a95d1d3aad7bc3aea36bb Mon Sep 17 00:00:00 2001 From: BalaSriharsha <> Date: Fri, 25 Apr 2025 03:37:25 +0530 Subject: [PATCH 2/3] Changed install.md --- cfn/kubernetes-setup-template.yaml | 37 ++++++++++++++++++++++++++- install.md | 40 ++++++++++++++++++++---------- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/cfn/kubernetes-setup-template.yaml b/cfn/kubernetes-setup-template.yaml index c0def18..8c9e1b8 100644 --- a/cfn/kubernetes-setup-template.yaml +++ b/cfn/kubernetes-setup-template.yaml @@ -612,6 +612,25 @@ Resources: ConsoleRepositoryUri: !Ref ConsoleRepositoryUri GatewayRepositoryUri: !Ref GatewayRepositoryUri + # SSM Parameters for superuser credentials + SuperUserUsernameParameter: + Type: AWS::SSM::Parameter + DependsOn: KubernetesSetupResource + Properties: + Name: !Sub /flotorch/${TableSuffix}/console/superuser/username + Type: String + Value: superadmin@flotorch.local + Description: FloTorch console superuser username + + SuperUserPasswordParameter: + Type: AWS::SSM::Parameter + DependsOn: KubernetesSetupResource + Properties: + Name: !Sub /flotorch/${TableSuffix}/console/superuser/password + Type: String + Value: Password2025! + Description: FloTorch console superuser password + Conditions: IsUsEast1Region: !Equals - !Ref AWS::Region @@ -639,4 +658,20 @@ Outputs: EC2PublicIP: Description: Public IP address of the EC2 instance - Value: !GetAtt EC2Instance.PublicIp \ No newline at end of file + Value: !GetAtt EC2Instance.PublicIp + + ConsoleSubdomain: + Description: Subdomain for the console + Value: !Sub ${TableSuffix}.flotorch.cloud + + SuperUserUsername: + Description: Superuser username for console login stored in SSM Parameter + Value: !Ref SuperUserUsernameParameter + + SuperUserUsernameParameter: + Description: SSM Parameter path for superuser username + Value: !Sub /flotorch/${TableSuffix}/console/superuser/username + + SuperUserPasswordParameter: + Description: SSM Parameter path for superuser password + Value: !Sub /flotorch/${TableSuffix}/console/superuser/password \ No newline at end of file diff --git a/install.md b/install.md index c2f8d8b..94b2903 100644 --- a/install.md +++ b/install.md @@ -61,35 +61,49 @@ Complete the required parameters: ### 3. Configure DNS for Application Access -After deployment completes, get the EC2 instance's public IP: - -```bash -aws cloudformation describe-stacks \ - --stack-name YOUR_PROJECT_NAME-KubernetesSetupStack \ - --query "Stacks[0].Outputs[?OutputKey=='InstancePublicIp'].OutputValue" \ - --output text -``` +After deployment completes, get the EC2 instance's public IP from the CloudFormation stack outputs of the `-KubernetesSetupStack`. Map the FloTorch domain to this IP in your hosts file: - **Linux users**: ```bash - echo " console.flotorch.com" | sudo tee -a /etc/resolv.conf + echo " .flotorch.cloud" | sudo tee -a /etc/resolv.conf ``` - **macOS users**: ```bash sudo vi /private/etc/hosts - # Add: console.flotorch.com + # Add: .flotorch.cloud sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder ``` ### 4. Access Your FloTorch Console Once DNS is configured, access your FloTorch console at: -- https://console.flotorch.com +- https://.flotorch.cloud:30443/ + +### 5. Login Credentials + +#### Superuser Login + +A default superuser account is created automatically and stored in AWS Systems Manager Parameter Store: + +- **Username**: `superadmin@flotorch.local` (stored at `/flotorch//console/superuser/username`) +- **Password**: `Password2025!` (stored at `/flotorch//console/superuser/password`) + +You can retrieve these credentials using AWS CLI: + +```bash +# Get username +aws ssm get-parameter --name /flotorch//console/superuser/username --query Parameter.Value --output text + +# Get password +aws ssm get-parameter --name /flotorch//console/superuser/password --query Parameter.Value --output text +``` + +Or view them in the CloudFormation stack outputs for your `-KubernetesSetupStack`. -Log in with the credentials you specified in the CloudFormation parameters. +> **Important**: Change the default password after your first login for security purposes. ## What's New @@ -116,7 +130,7 @@ aws cloudformation describe-stack-events --stack-name YOUR_PROJECT_NAME After successful deployment, you'll have access to: -1. **FloTorch Console**: https://console.flotorch.com +1. **FloTorch Console**: https://.flotorch.cloud 2. **AppRunner Service**: For API requests (URL in CloudFormation outputs) 3. **OpenSearch Dashboard**: For data exploration (if enabled) From e0907f47fe6c9fbc390118c6e8488edb2062858e Mon Sep 17 00:00:00 2001 From: BalaSriharsha <> Date: Fri, 25 Apr 2025 20:16:54 +0530 Subject: [PATCH 3/3] Added Docker Template --- cfn/docker-enterprise-setup.yaml | 707 +++++++++++++++++++++++++++++++ cfn/master-template.yaml | 70 +-- 2 files changed, 751 insertions(+), 26 deletions(-) create mode 100644 cfn/docker-enterprise-setup.yaml diff --git a/cfn/docker-enterprise-setup.yaml b/cfn/docker-enterprise-setup.yaml new file mode 100644 index 0000000..90836d1 --- /dev/null +++ b/cfn/docker-enterprise-setup.yaml @@ -0,0 +1,707 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Docker Setup Template for Flotorch' + +Parameters: + TableSuffix: + Type: String + Description: Suffix to append to resource names + VpcId: + Type: String + Description: ID of the VPC where the instance will be created + SubnetId: + Type: String + Description: ID of the subnet where the instance will be created + # KeyName parameter removed as we'll create a new key pair for each deployment + InstanceType: + Type: String + Default: t2.large + Description: EC2 instance type + ExperimentationHost: + Type: String + Description: URL for the experimentation API (AppRunner service URL) + ExperimentationUsername: + Type: String + Default: admin + Description: Username for experimentation API + ExperimentationPassword: + Type: String + Description: Password for experimentation API + ConsoleImageTag: + Type: String + Description: Tag for console image + GatewayImageTag: + Type: String + Description: Tag for gateway image + ConsoleRepositoryUri: + Type: String + Description: URI for the console repository + GatewayRepositoryUri: + Type: String + Description: URI for the gateway repository + +Resources: + EC2SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for FloTorch Docker EC2 instance + VpcId: !Ref VpcId + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 + Description: SSH access + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: 0.0.0.0/0 + Description: HTTP NodePort access + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + Description: HTTPS NodePort access + - IpProtocol: tcp + FromPort: 8000 + ToPort: 8000 + CidrIp: 0.0.0.0/0 + Description: Console NodePort access + - IpProtocol: tcp + FromPort: 3000 + ToPort: 3000 + CidrIp: 0.0.0.0/0 + Description: Gateway NodePort access + - IpProtocol: tcp + FromPort: 6739 + ToPort: 6739 + CidrIp: 0.0.0.0/0 + Description: Redis NodePort access + - IpProtocol: tcp + FromPort: 5432 + ToPort: 5432 + CidrIp: 0.0.0.0/0 + Description: Postgres NodePort access + + EC2InstanceRole: + 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/AmazonSSMFullAccess + - arn:aws:iam::aws:policy/AmazonEC2FullAccess + - arn:aws:iam::aws:policy/AmazonS3FullAccess + - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess + + EC2InstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: + Roles: + - !Ref EC2InstanceRole + + # Create a unique key name using a timestamp to avoid name conflicts + EC2KeyPairName: + Type: AWS::SSM::Parameter + Properties: + Type: String + Value: !Sub 'flotorch-k8s-${AWS::StackName}-${AWS::Region}-${AWS::AccountId}-${!Timestamp}' + Description: 'Parameter store for the EC2 key pair name' + + EC2KeyPair: + Type: AWS::EC2::KeyPair + DependsOn: EC2KeyPairName + Properties: + KeyName: !GetAtt EC2KeyPairName.Value + + EC2Instance: + Type: AWS::EC2::Instance + DependsOn: EC2KeyPair + Properties: + InstanceType: !Ref InstanceType + ImageId: !If + - IsUsEast1Region + - ami-01816d07b1128cd2d # Amazon Linux 2023 AMI for us-east-1 + - ami-093a4ad9a8cc370f4 # Amazon Linux 2023 AMI for other regions + KeyName: !Ref EC2KeyPair + IamInstanceProfile: !Ref EC2InstanceProfile + SubnetId: !Ref SubnetId + SecurityGroupIds: + - !Ref EC2SecurityGroup + BlockDeviceMappings: + - DeviceName: /dev/xvda + Ebs: + VolumeSize: 48 + VolumeType: gp3 + DeleteOnTermination: true + Tags: + - Key: Name + Value: !Sub FloTorch-K8S-${AWS::StackName} + UserData: + Fn::Base64: !Sub | + #!/bin/bash -xe + yum update -y + sudo su ec2-user + cd /home/ec2-user + # Install docker + if ! command -v docker &> /dev/null; then + echo "Installing docker..." + sudo yum install -y docker + sudo systemctl start docker + sudo systemctl enable docker + sudo usermod -aG docker ec2-user + newgrp docker + else + echo "docker is already installed" + fi + + # Install docker compose + if ! command -v docker-compose &> /dev/null; then + echo "Installing docker compose..." + sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + docker-compose version + else + echo "docker compose is already installed" + fi + + # Install git + if ! command -v git &> /dev/null; then + echo "Installing Git..." + sudo yum install -y git + else + echo "Git is already installed" + fi + + DockerSetupRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + - arn:aws:iam::aws:policy/AmazonSSMFullAccess + - arn:aws:iam::aws:policy/AmazonEC2FullAccess + - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess + - arn:aws:iam::aws:policy/AmazonS3FullAccess + + DockerSetupFunction: + Type: AWS::Lambda::Function + DependsOn: EC2Instance + Properties: + Handler: index.handler + Role: !GetAtt DockerSetupRole.Arn + Code: + ZipFile: | + import boto3 + import cfnresponse + import time + import json + import re + import botocore + + def handler(event, context): + try: + if event['RequestType'] in ['Create', 'Update']: + ssm = boto3.client('ssm') + ec2 = boto3.client('ec2') + + # Get the instance ID from the EC2 instance created in this stack + ec2_instance_logical_id = 'EC2Instance' + stack_id = event['StackId'] + stack_name = stack_id.split('/')[1] + print(f"Getting EC2 instance ID from stack {stack_name}") + + # Describe the stack resources to get the physical ID of the EC2 instance + cfn = boto3.client('cloudformation') + response = cfn.describe_stack_resource( + StackName=stack_name, + LogicalResourceId=ec2_instance_logical_id + ) + + raw_instance_id = response['StackResourceDetail']['PhysicalResourceId'] + print(f"Using EC2 instance ID: {raw_instance_id}") + + # Get experimentation parameters + experimentation_host = event['ResourceProperties'].get('ExperimentationHost', '') + experimentation_username = event['ResourceProperties'].get('ExperimentationUsername', 'admin') + experimentation_password = event['ResourceProperties'].get('ExperimentationPassword', '') + console_image_tag = event['ResourceProperties'].get('ConsoleImageTag', '') + gateway_image_tag = event['ResourceProperties'].get('GatewayImageTag', '') + console_repository_uri = event['ResourceProperties'].get('ConsoleRepositoryUri', '') + gateway_repository_uri = event['ResourceProperties'].get('GatewayRepositoryUri', '') + table_suffix = event['ResourceProperties'].get('TableSuffix', '') + + print("Experimentation Host: " + experimentation_host) + print("Experimentation Username: " + experimentation_username) + + # Extract just the instance ID if there's extra text + instance_id_match = re.search(r'(i-[a-z0-9]+)', raw_instance_id) + if instance_id_match: + instance_id = instance_id_match.group(1) + print("Extracted instance ID: " + instance_id + " from " + raw_instance_id) + else: + instance_id = raw_instance_id + print("Using instance ID as provided: " + instance_id) + + # Wait for the instance to be in running state + print("Waiting for instance to reach running state...") + waiter = ec2.get_waiter('instance_running') + waiter.wait(InstanceIds=[instance_id]) + print("Instance is now running. Waiting for status checks to pass...") + + # Wait for the instance status checks to pass + waiter = ec2.get_waiter('instance_status_ok') + waiter.wait(InstanceIds=[instance_id]) + print("Instance status checks passed. Waiting for SSM agent to come online...") + + # Wait for the SSM agent to be online + ssm_online = False + max_retries = 30 # Try for about 5 minutes (10 seconds between retries) + retry_count = 0 + + while not ssm_online and retry_count < max_retries: + try: + # Check if the instance is SSM-managed (connected) + response = ssm.describe_instance_information( + Filters=[{ + 'Key': 'InstanceIds', + 'Values': [instance_id] + }] + ) + + if response['InstanceInformationList']: + ssm_online = True + print("SSM agent is online and instance is ready for commands.") + else: + print(f"Waiting for SSM agent to come online... (Attempt {retry_count + 1}/{max_retries})") + time.sleep(10) # Wait 10 seconds before trying again + retry_count += 1 + except Exception as e: + print(f"Error checking SSM status: {str(e)}") + time.sleep(10) + retry_count += 1 + + if not ssm_online: + raise Exception("Timed out waiting for SSM agent to come online") + + # Get the instance details after it's fully initialized + response = ec2.describe_instances(InstanceIds=[instance_id]) + private_ip = response['Reservations'][0]['Instances'][0]['PrivateIpAddress'] + # Get the public IP address + public_ip = response['Reservations'][0]['Instances'][0]['PublicIpAddress'] + availability_zone = response['Reservations'][0]['Instances'][0]['Placement']['AvailabilityZone'] + # Extract region from AZ (remove the last character) + region = availability_zone[:-1] + print("Private IP of the instance: " + private_ip) + print("Public IP of the instance: " + public_ip) + print("Region of the instance: " + region) + + # Create a base script template + script_template = '''#!/bin/bash + experimentation_host="{}" + experimentation_username="{}" + experimentation_password="{}" + console_image_tag="{}" + gateway_image_tag="{}" + console_repository_uri="{}" + gateway_repository_uri="{}" + table_suffix="{}" + public_ip="{}" + set -e''' + + # Format the template with the actual values + install_script = script_template.format(experimentation_host, experimentation_username, experimentation_password, console_image_tag, gateway_image_tag, console_repository_uri, gateway_repository_uri, table_suffix, public_ip) + ''' + + + echo "Starting Docker tools installation and setup" + + # Add /usr/local/bin to PATH + export PATH=$PATH:/usr/local/bin/ + + if command -v docker &> /dev/null; then + echo "docker is already installed" + else + echo "Installing docker..." + sudo yum install -y docker + sudo systemctl start docker + sudo systemctl enable docker + sudo usermod -aG docker ec2-user + newgrp docker + fi + + # Install docker compose + if command -v docker-compose &> /dev/null; then + echo "docker compose is already installed" + else + echo "Installing docker compose..." + sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + docker-compose version + fi + + # Install git + if ! command -v git &> /dev/null; then + echo "Git is already installed" + else + echo "Installing Git..." + sudo yum install -y git + fi + + if command -v aws &> /dev/null; then + echo "aws is already installed" + else + # Install awscli + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + sudo yum install -y unzip + unzip awscliv2.zip + sudo ./aws/install + rm -rf aws awscliv2.zip + fi + + private_ip=$(hostname -I | awk '{print $1}') + region=$(aws ec2 describe-instances --instance-ids $(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) --query 'Reservations[0].Instances[0].Placement.AvailabilityZone' --output text | sed 's/.$//') + echo "Region: $region" + echo "Private IP: $private_ip" + + aws s3 cp s3://flotorch-public/docker-compose.yaml /home/ec2-user/docker-compose.yaml + + # Create a values override file to avoid escaping issues + rm -rf /home/ec2-user/.env + + cat > /home/ec2-user/.env << EOF + CLICKHOUSE_USER=flotorch + CLICKHOUSE_PASSWORD=ad59165b73960e4c9b3e5a58ababad07 + CONSOLE_DB_USER=postgres + CONSOLE_DB_PASSWORD=u6RLCfL9AJBYvkfB + CONSOLE_DB_NAME=flotorch + CONSOLE_DOMAIN=$table_suffix-console.flotorch.cloud + GATEWAY_DOMAIN=$table_suffix-gateway.flotorch.cloud + CONSOLE_SESSION_PASSWORD=h3dPidJQENDRR5RrzL8SMRPGGaeFdEC8 + CONSOLE_ENCRYPTION_KEY=GXQA5xxSYm38c37zaDnE4gCd9MBJhfDX + EXPERIMENTATION_HOST=$experimentation_host + EXPERIMENTATION_USERNAME=$experimentation_username + EXPERIMENTATION_PASSWORD=$experimentation_password + PAIRING_KEY=iazRs8LdRkYS7nmxRrqqbrtXShrDdFEe + GATEWAY_NAME=$table_suffix-gateway + EOF + + # Make sure ec2-user owns the file + chown ec2-user:ec2-user /home/ec2-user/.env + + # Get AWS account ID + AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + + # Update image names in docker-compose.yaml + echo "Updating image names in docker-compose.yaml..." + # Replace console image + sed -i "s|image: .*flotorch\/console.*$|image: $AWS_ACCOUNT_ID.dkr.ecr.$region.amazonaws.com/flotorch-console-$table_suffix:test-cfn|g" /home/ec2-user/docker-compose.yaml + + # Replace gateway image + sed -i "s|image: .*flotorch\/gateway.*$|image: $AWS_ACCOUNT_ID.dkr.ecr.$region.amazonaws.com/flotorch-gateway-$table_suffix:test-cfn|g" /home/ec2-user/docker-compose.yaml + + # Print updated image names for verification + echo "Updated image names in docker-compose.yaml:" + grep -E 'image:.*flotorch-(console|gateway)' /home/ec2-user/docker-compose.yaml + + aws ecr get-login-password --region $region | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$region.amazonaws.com + + # Verify docker-compose.yaml exists and is valid + echo "Checking if docker-compose.yaml exists..." + if [ -f /home/ec2-user/docker-compose.yaml ]; then + echo "docker-compose.yaml found, proceeding with deployment..." + cd /home/ec2-user + export PATH=$PATH:/usr/local/bin + # Add debugging info + echo "Current directory: $(pwd)" + echo "Docker compose file contents (first 10 lines):" + head -10 docker-compose.yaml + # Add domain entries to /etc/hosts for proper service discovery + echo "127.0.0.1 $table_suffix-console.flotorch.cloud" | sudo tee -a /etc/hosts + echo "127.0.0.1 $table_suffix-gateway.flotorch.cloud" | sudo tee -a /etc/hosts + + # Make docker socket accessible + sudo chmod 666 /var/run/docker.sock + + # Verify the docker-compose file format + echo "Verifying docker-compose.yaml format..." + cat /home/ec2-user/docker-compose.yaml | grep -q "^version:" || { + echo "WARNING: No version found in docker-compose.yaml, adding version header..." + echo 'version: "3"' | cat - /home/ec2-user/docker-compose.yaml > /home/ec2-user/temp && mv /home/ec2-user/temp /home/ec2-user/docker-compose.yaml + } + + # Make docker socket accessible and ensure we're using version 1 of compose command + sudo chmod 666 /var/run/docker.sock + echo "Starting services with docker-compose..." + + # First try with sudo + if sudo docker-compose -f /home/ec2-user/docker-compose.yaml up -d; then + echo "Docker Compose started successfully with sudo docker-compose" + else + echo "Trying alternate docker compose command..." + # If that fails, try with docker compose (V2 syntax) + if sudo docker compose -f /home/ec2-user/docker-compose.yaml up -d; then + echo "Docker Compose started successfully with sudo docker compose" + else + echo "ERROR: Failed to start with docker compose, trying one last approach" + # Last resort, use full path + sudo -E /usr/local/bin/docker-compose -f /home/ec2-user/docker-compose.yaml up -d + fi + fi + else + echo "ERROR: docker-compose.yaml not found at /home/ec2-user/docker-compose.yaml" + ls -la /home/ec2-user/ + exit 1 + fi + + # Clone repository to get the SQL migration files + echo "Cloning FloTorch repository for migration files..." + if [ ! -d "FloTorch" ]; then + rm -rf FloTorch + git clone https://github.com/FissionAI/FloTorch.git + cd FloTorch + git config --global --add safe.directory /home/ec2-user/FloTorch + git checkout v3.0.1 + if [ $? -ne 0 ]; then + echo "Failed to clone repository" + exit 1 + fi + cd .. + fi + + # Wait for containers to start and be ready + echo "Waiting for services to be ready..." + sleep 30 + + # Run Clickhouse migrations + echo "Running Clickhouse migrations..." + CLICKHOUSE_CONTAINER_ID=$(docker ps -f name=clickhouse -q) + if [ -n "$CLICKHOUSE_CONTAINER_ID" ]; then + # Check if ClickHouse is accepting connections + RETRY_COUNT=0 + MAX_RETRIES=10 + CLICKHOUSE_READY=false + + while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$CLICKHOUSE_READY" = "false" ]; do + if docker exec -i clickhouse clickhouse-client --user flotorch --password ad59165b73960e4c9b3e5a58ababad07 --query "SELECT 1" > /dev/null 2>&1; then + CLICKHOUSE_READY=true + echo "ClickHouse is ready for connections" + else + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Waiting for ClickHouse to be ready... Attempt $RETRY_COUNT of $MAX_RETRIES" + sleep 5 + fi + done + + if [ "$CLICKHOUSE_READY" = "false" ]; then + echo "ERROR: ClickHouse is not ready after waiting" + docker logs clickhouse + else + # Run migrations + CLICKHOUSE_MIGRATION_FILES=$(find /home/ec2-user/FloTorch/charts/flotorch/sql/clickhouse -type f -name "*.sql" | sort) + for file in $CLICKHOUSE_MIGRATION_FILES; do + echo "Applying ClickHouse migration: $file" + docker exec -i clickhouse clickhouse-client --user flotorch --password ad59165b73960e4c9b3e5a58ababad07 --database flotorch < "$file" + if [ $? -ne 0 ]; then + echo "WARNING: Error applying migration: $file" + fi + done + fi + else + echo "ERROR: ClickHouse container not found" + fi + + # Run Postgres migrations + echo "Running Postgres migrations..." + POSTGRES_CONTAINER_ID=$(docker ps -f name=postgres -q) + if [ -n "$POSTGRES_CONTAINER_ID" ]; then + # Check if Postgres is accepting connections + RETRY_COUNT=0 + MAX_RETRIES=10 + POSTGRES_READY=false + + while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$POSTGRES_READY" = "false" ]; do + if docker exec -i postgres pg_isready -U postgres > /dev/null 2>&1; then + POSTGRES_READY=true + echo "PostgreSQL is ready for connections" + else + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Waiting for PostgreSQL to be ready... Attempt $RETRY_COUNT of $MAX_RETRIES" + sleep 5 + fi + done + + if [ "$POSTGRES_READY" = "false" ]; then + echo "ERROR: PostgreSQL is not ready after waiting" + docker logs postgres + else + # Run migrations + POSTGRES_MIGRATION_FILES=$(find /home/ec2-user/FloTorch/charts/flotorch/sql/postgres -type f -name "*.sql" | sort) + for file in $POSTGRES_MIGRATION_FILES; do + echo "Applying PostgreSQL migration: $file" + docker exec -i postgres psql -U postgres -d flotorch < "$file" || echo "WARNING: Error applying migration: $file" + done + fi + else + echo "ERROR: PostgreSQL container not found" + fi + + # Final check to ensure all containers are still running + echo "Performing final check on container status..." + RUNNING_CONTAINERS=$(docker ps --format "{{.Names}}" | grep -E 'postgres|redis|clickhouse|console|gateway|reverse-proxy' | wc -l) + echo "Found $RUNNING_CONTAINERS running containers" + + if [ "$RUNNING_CONTAINERS" -ge "6" ]; then + echo "All containers are running successfully!" + + # Test connectivity between console and gateway + echo "Testing connectivity between console and gateway..." + # Try to connect from console to gateway + docker exec console curl -s -I http://gateway:3000 || echo "WARNING: Console cannot directly connect to gateway" + else + echo "WARNING: Not all containers are running. Current status:" + docker ps + fi + + # Create Cloudflare DNS record for subdomain + echo "Creating Cloudflare DNS record for ${table_suffix}.flotorch.cloud..." + # Use the public IP passed from the Lambda function + PUBLIC_IP="${public_ip}" + CLOUDFLARE_API_TOKEN="gXtnJiUVhXk76gRzR6NDjiVRWOww8HJFR8hanMqY" + CLOUDFLARE_ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=flotorch.cloud" \ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ + -H "Content-Type: application/json" | jq -r '.result[0].id') + + # Create the properly escaped JSON payload directly + echo "Creating Cloudflare DNS record with type A, name $table_suffix, pointing to $PUBLIC_IP" + curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/dns_records" \ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"type":"A","name":"'"$table_suffix"-console"'","content":"'"$PUBLIC_IP"'","ttl":120,"proxied":true}' + + echo "DNS record for $table_suffix-console.flotorch.cloud created and pointing to $PUBLIC_IP" + + curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/dns_records" \ + -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"type":"A","name":"'"$table_suffix"-gateway"'","content":"'"$PUBLIC_IP"'","ttl":120,"proxied":true}' + + echo "DNS record for $table_suffix-gateway.flotorch.cloud created and pointing to $PUBLIC_IP" + + echo "Docker setup completed successfully!" + ''' + + # Run script using SSM + command = ssm.send_command( + InstanceIds=[instance_id], + DocumentName='AWS-RunShellScript', + Parameters={ + 'commands': [install_script] + } + ) + + command_id = command['Command']['CommandId'] + print("Command ID: " + command_id) + print("Waiting for Docker setup to complete...") + + while True: + try: + result = ssm.get_command_invocation( + CommandId=command_id, + InstanceId=instance_id + ) + if result['Status'] in ['Success', 'Failed', 'Cancelled', 'TimedOut']: + if result['Status'] != 'Success': + error_message = result.get('StandardErrorContent', 'No error content available') + raise Exception(f"Command failed with status {result['Status']}: {error_message}") + break + time.sleep(30) # Check every 30 seconds + except botocore.exceptions.ClientError as e: + if "InvocationDoesNotExist" in str(e): + time.sleep(30) + continue + else: + raise + + print("Docker setup completed successfully!") + # Only include minimal response data to avoid oversize errors + # Never return large amounts of data from Lambda custom resources + response_data = {'Message': 'Docker setup completed successfully'} + cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) + else: + # For Delete or other events, just return success + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + except Exception as e: + print(f"Error: {str(e)}") + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)}) + + Runtime: python3.9 + Timeout: 900 + MemorySize: 256 + + DockerSetupResource: + Type: Custom::DockerSetup + Properties: + ServiceToken: !GetAtt DockerSetupFunction.Arn + ExperimentationHost: !Ref ExperimentationHost + ExperimentationUsername: !Ref ExperimentationUsername + ExperimentationPassword: !Ref ExperimentationPassword + TableSuffix: !Ref TableSuffix + ConsoleImageTag: !Ref ConsoleImageTag + GatewayImageTag: !Ref GatewayImageTag + ConsoleRepositoryUri: !Ref ConsoleRepositoryUri + GatewayRepositoryUri: !Ref GatewayRepositoryUri + + # SSM Parameters for superuser credentials + SuperUserUsernameParameter: + Type: AWS::SSM::Parameter + DependsOn: DockerSetupResource + Properties: + Name: !Sub /flotorch/${TableSuffix}/console/superuser/username + Type: String + Value: superadmin@flotorch.local + Description: FloTorch console superuser username + + SuperUserPasswordParameter: + Type: AWS::SSM::Parameter + DependsOn: DockerSetupResource + Properties: + Name: !Sub /flotorch/${TableSuffix}/console/superuser/password + Type: String + Value: Password2025! + Description: FloTorch console superuser password + +Conditions: + IsUsEast1Region: !Equals + - !Ref AWS::Region + - us-east-1 + +Outputs: + EC2PublicIP: + Description: Public IP address of the EC2 instance + Value: !GetAtt EC2Instance.PublicIp + + ConsoleSubdomain: + Description: Subdomain for the console + Value: !Sub ${TableSuffix}.flotorch.cloud + + SuperUserUsername: + Description: Superuser username for console login stored in SSM Parameter + Value: !Ref SuperUserUsernameParameter + + SuperUserUsernameParameter: + Description: SSM Parameter path for superuser username + Value: !Sub /flotorch/${TableSuffix}/console/superuser/username + + SuperUserPasswordParameter: + Description: SSM Parameter path for superuser password + Value: !Sub /flotorch/${TableSuffix}/console/superuser/password \ No newline at end of file diff --git a/cfn/master-template.yaml b/cfn/master-template.yaml index 5e41b67..7abd40f 100644 --- a/cfn/master-template.yaml +++ b/cfn/master-template.yaml @@ -393,13 +393,51 @@ Resources: AppImageTag: "3.0.0.1" NeedOpensearch: !Ref NeedOpensearch - KubernetesSetupStackWithOpenSearch: + # KubernetesSetupStackWithOpenSearch: + # Type: AWS::CloudFormation::Stack + # Condition: CreateOpenSearchStack + # DependsOn: [AppRunnerStack] + # DeletionPolicy: Delete + # Properties: + # TemplateURL: !Sub https://flotorch-public.s3.us-east-1.amazonaws.com/test-cfn/templates/kubernetes-setup-template.yaml + # Parameters: + # TableSuffix: !Ref TableSuffix + # VpcId: !GetAtt VPCStack.Outputs.VpcId + # SubnetId: !GetAtt VPCStack.Outputs.PublicSubnet1 + # InstanceType: t2.large + # ExperimentationHost: !GetAtt AppRunnerStack.Outputs.ServiceUrl + # ExperimentationUsername: !Ref OpenSearchAdminUser + # ExperimentationPassword: !Ref NginxAuthPassword + # ConsoleImageTag: "test-cfn" + # GatewayImageTag: "test-cfn" + # ConsoleRepositoryUri: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/flotorch-console-${TableSuffix}" + # GatewayRepositoryUri: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/flotorch-gateway-${TableSuffix}" + + # KubernetesSetupStackNoOpenSearch: + # Type: AWS::CloudFormation::Stack + # Condition: CreateNoOpenSearchStack + # DependsOn: [AppRunnerStackNoOpenSearch] + # Properties: + # TemplateURL: !Sub https://flotorch-public.s3.us-east-1.amazonaws.com/test-cfn/templates/kubernetes-setup-template.yaml + # Parameters: + # TableSuffix: !Ref TableSuffix + # VpcId: !GetAtt VPCStack.Outputs.VpcId + # SubnetId: !GetAtt VPCStack.Outputs.PublicSubnet1 + # InstanceType: t2.large + # ExperimentationHost: !GetAtt AppRunnerStackNoOpenSearch.Outputs.ServiceUrl + # ExperimentationUsername: !Ref OpenSearchAdminUser + # ExperimentationPassword: !Ref NginxAuthPassword + # ConsoleImageTag: "test-cfn" + # GatewayImageTag: "test-cfn" + # ConsoleRepositoryUri: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/flotorch-console-${TableSuffix}" + # GatewayRepositoryUri: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/flotorch-gateway-${TableSuffix}" + + DockerEnterpriseSetupStackWithOpenSearch: Type: AWS::CloudFormation::Stack Condition: CreateOpenSearchStack DependsOn: [AppRunnerStack] - DeletionPolicy: Delete Properties: - TemplateURL: !Sub https://flotorch-public.s3.us-east-1.amazonaws.com/test-cfn/templates/kubernetes-setup-template.yaml + TemplateURL: !Sub https://flotorch-public.s3.us-east-1.amazonaws.com/test-enterprise/templates/docker-enterprise-setup.yaml Parameters: TableSuffix: !Ref TableSuffix VpcId: !GetAtt VPCStack.Outputs.VpcId @@ -412,13 +450,13 @@ Resources: GatewayImageTag: "test-cfn" ConsoleRepositoryUri: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/flotorch-console-${TableSuffix}" GatewayRepositoryUri: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/flotorch-gateway-${TableSuffix}" - - KubernetesSetupStackNoOpenSearch: + + DockerEnterpriseSetupStackNoOpenSearch: Type: AWS::CloudFormation::Stack Condition: CreateNoOpenSearchStack DependsOn: [AppRunnerStackNoOpenSearch] Properties: - TemplateURL: !Sub https://flotorch-public.s3.us-east-1.amazonaws.com/test-cfn/templates/kubernetes-setup-template.yaml + TemplateURL: !Sub https://flotorch-public.s3.us-east-1.amazonaws.com/test-enterprise/templates/docker-enterprise-setup.yaml Parameters: TableSuffix: !Ref TableSuffix VpcId: !GetAtt VPCStack.Outputs.VpcId @@ -443,26 +481,6 @@ Outputs: Condition: CreateNoOpenSearchStack Value: !GetAtt AppRunnerStackNoOpenSearch.Outputs.ServiceUrl - KubernetesConsoleUrl: - Description: URL for Kubernetes Console access - Condition: CreateOpenSearchStack - Value: !GetAtt KubernetesSetupStackWithOpenSearch.Outputs.HTTPSNodePort - - KubernetesConsoleUrlNoOpenSearch: - Description: URL for Kubernetes Console access - Condition: CreateNoOpenSearchStack - Value: !GetAtt KubernetesSetupStackNoOpenSearch.Outputs.HTTPSNodePort - - KubernetesEC2KeyPairWithOpenSearch: - Description: Name of the key pair for the Kubernetes EC2 instance with OpenSearch - Condition: CreateOpenSearchStack - Value: !GetAtt KubernetesSetupStackWithOpenSearch.Outputs.EC2KeyPairName - - KubernetesEC2KeyPairNoOpenSearch: - Description: Name of the key pair for the Kubernetes EC2 instance without OpenSearch - Condition: CreateNoOpenSearchStack - Value: !GetAtt KubernetesSetupStackNoOpenSearch.Outputs.EC2KeyPairName - OpenSearchDashboardsUrl: Description: URL for OpenSearch Dashboards Condition: CreateOpenSearchStack