Spaces:
Runtime error
Runtime error
| """Deploy SoM to AWS EC2 via Github action. | |
| Usage: | |
| 1. Create and populate the .env file: | |
| cat > .env <<EOF | |
| AWS_ACCESS_KEY_ID=<your aws access key id> | |
| AWS_SECRET_ACCESS_KEY=<your aws secret access key (required)> | |
| AWS_REGION=<your aws region (required)> | |
| GITHUB_OWNER=<your github owner (required)> # e.g. microsoft | |
| GITHUB_REPO=<your github repo (required)> # e.g. SoM | |
| GITHUB_TOKEN=<your github token (required)> | |
| PROJECT_NAME=<your project name (required)> # for tagging AWS resources | |
| OPENAI_API_KEY=<your openai api key (optional)> | |
| EOF | |
| 2. Create a virtual environment for deployment: | |
| python3.10 -m venv venv | |
| source venv/bin/activate | |
| pip install -r deploy_requirements.txt | |
| 3. Run the deployment script: | |
| python deploy.py start | |
| 4. Wait for the build to succeed in Github actions (see console output for URL) | |
| 5. Open the gradio interface (see console output for URL) and test it out. | |
| Note that it may take a minute for the interface to become available. | |
| You can also interact with the server programmatically: | |
| python client.py "http://<server_ip>:6092" | |
| 6. Terminate the EC2 instance and stop incurring charges: | |
| python deploy.py stop | |
| Or, to shut it down without removing it: | |
| python deploy.py pause | |
| (This can later be re-started with the `start` command.) | |
| 7. (optional) List all tagged instances with their respective statuses: | |
| python deploy.py status | |
| Troubleshooting Token Scope Error: | |
| If you encounter an error similar to the following when pushing changes to | |
| GitHub Actions workflow files: | |
| ! [remote rejected] feat/docker -> feat/docker (refusing to allow a | |
| Personal Access Token to create or update workflow | |
| `.github/workflows/docker-build-ec2.yml` without `workflow` scope) | |
| This indicates that the Personal Access Token (PAT) being used does not | |
| have the necessary permissions ('workflow' scope) to create or update GitHub | |
| Actions workflows. To resolve this issue, you will need to create or update | |
| your PAT with the appropriate scope. | |
| Creating or Updating a Classic PAT with 'workflow' Scope: | |
| 1. Go to GitHub and sign in to your account. | |
| 2. Click on your profile picture in the top right corner, and then click 'Settings'. | |
| 3. In the sidebar, click 'Developer settings'. | |
| 4. Click 'Personal access tokens', then 'Classic tokens'. | |
| 5. To update an existing token: | |
| a. Find the token you wish to update in the list and click on it. | |
| b. Scroll down to the 'Select scopes' section. | |
| c. Make sure the 'workflow' scope is checked. This scope allows for | |
| managing GitHub Actions workflows. | |
| d. Click 'Update token' at the bottom of the page. | |
| 6. To create a new token: | |
| a. Click 'Generate new token'. | |
| b. Give your token a descriptive name under 'Note'. | |
| c. Scroll down to the 'Select scopes' section. | |
| d. Check the 'workflow' scope to allow managing GitHub Actions workflows. | |
| e. Optionally, select any other scopes needed for your project. | |
| f. Click 'Generate token' at the bottom of the page. | |
| 7. Copy the generated token. Make sure to save it securely, as you will not | |
| be able to see it again. | |
| After creating or updating your PAT with the 'workflow' scope, update the | |
| Git remote configuration to use the new token, and try pushing your changes | |
| again. | |
| Note: Always keep your tokens secure and never share them publicly. | |
| """ | |
| import base64 | |
| import json | |
| import os | |
| import subprocess | |
| import time | |
| from botocore.exceptions import ClientError | |
| from jinja2 import Environment, FileSystemLoader | |
| from loguru import logger | |
| from nacl import encoding, public | |
| from pydantic_settings import BaseSettings | |
| import boto3 | |
| import fire | |
| import git | |
| import paramiko | |
| import requests | |
| class Config(BaseSettings): | |
| AWS_ACCESS_KEY_ID: str | |
| AWS_SECRET_ACCESS_KEY: str | |
| AWS_REGION: str | |
| GITHUB_OWNER: str | |
| GITHUB_REPO: str | |
| GITHUB_TOKEN: str | |
| OPENAI_API_KEY: str | None = None | |
| PROJECT_NAME: str | |
| AWS_EC2_AMI: str = "ami-0f9c346cdcac09fb5" # Deep Learning AMI GPU PyTorch 2.0.1 (Ubuntu 20.04) 20230827 | |
| AWS_EC2_DISK_SIZE: int = 100 # GB | |
| #AWS_EC2_INSTANCE_TYPE: str = "p3.2xlarge" # (V100 16GB $3.06/hr x86_64) | |
| AWS_EC2_INSTANCE_TYPE: str = "g4dn.xlarge" # (T4 16GB $0.526/hr x86_64) | |
| AWS_EC2_USER: str = "ubuntu" | |
| class Config: | |
| env_file = ".env" | |
| env_file_encoding = 'utf-8' | |
| def AWS_EC2_KEY_NAME(self) -> str: | |
| return f"{self.PROJECT_NAME}-key" | |
| def AWS_EC2_KEY_PATH(self) -> str: | |
| return f"./{self.AWS_EC2_KEY_NAME}.pem" | |
| def AWS_EC2_SECURITY_GROUP(self) -> str: | |
| return f"{self.PROJECT_NAME}-SecurityGroup" | |
| def AWS_SSM_ROLE_NAME(self) -> str: | |
| return f"{self.PROJECT_NAME}-SSMRole" | |
| def AWS_SSM_PROFILE_NAME(self) -> str: | |
| return f"{self.PROJECT_NAME}-SSMInstanceProfile" | |
| def GITHUB_PATH(self) -> str: | |
| return f"{self.GITHUB_OWNER}/{self.GITHUB_REPO}" | |
| config = Config() | |
| def encrypt(public_key: str, secret_value: str) -> str: | |
| """ | |
| Encrypts a Unicode string using the provided public key. | |
| Args: | |
| public_key (str): The public key for encryption, encoded in Base64. | |
| secret_value (str): The Unicode string to be encrypted. | |
| Returns: | |
| str: The encrypted value, encoded in Base64. | |
| """ | |
| public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder()) | |
| sealed_box = public.SealedBox(public_key) | |
| encrypted = sealed_box.encrypt(secret_value.encode("utf-8")) | |
| return base64.b64encode(encrypted).decode("utf-8") | |
| def set_github_secret(token: str, repo: str, secret_name: str, secret_value: str) -> None: | |
| """ | |
| Sets a secret in the specified GitHub repository. | |
| Args: | |
| token (str): GitHub token with permissions to set secrets. | |
| repo (str): Repository path in the format "owner/repo". | |
| secret_name (str): The name of the secret to set. | |
| secret_value (str): The value of the secret. | |
| Returns: | |
| None | |
| """ | |
| secret_value = secret_value or "" | |
| headers = { | |
| "Authorization": f"token {token}", | |
| "Accept": "application/vnd.github.v3+json" | |
| } | |
| response = requests.get(f"https://api.github.com/repos/{repo}/actions/secrets/public-key", headers=headers) | |
| response.raise_for_status() | |
| key = response.json()['key'] | |
| key_id = response.json()['key_id'] | |
| encrypted_value = encrypt(key, secret_value) | |
| secret_url = f"https://api.github.com/repos/{repo}/actions/secrets/{secret_name}" | |
| data = {"encrypted_value": encrypted_value, "key_id": key_id} | |
| response = requests.put(secret_url, headers=headers, json=data) | |
| response.raise_for_status() | |
| logger.info(f"set {secret_name=}") | |
| def set_github_secrets() -> None: | |
| """ | |
| Sets required AWS credentials and SSH private key as GitHub Secrets. | |
| Returns: | |
| None | |
| """ | |
| # Set AWS secrets | |
| set_github_secret(config.GITHUB_TOKEN, config.GITHUB_PATH, 'AWS_ACCESS_KEY_ID', config.AWS_ACCESS_KEY_ID) | |
| set_github_secret(config.GITHUB_TOKEN, config.GITHUB_PATH, 'AWS_SECRET_ACCESS_KEY', config.AWS_SECRET_ACCESS_KEY) | |
| set_github_secret(config.GITHUB_TOKEN, config.GITHUB_PATH, 'OPENAI_API_KEY', config.OPENAI_API_KEY) | |
| # Read the SSH private key from the file | |
| try: | |
| with open(config.AWS_EC2_KEY_PATH, 'r') as key_file: | |
| ssh_private_key = key_file.read() | |
| set_github_secret(config.GITHUB_TOKEN, config.GITHUB_PATH, 'SSH_PRIVATE_KEY', ssh_private_key) | |
| except IOError as e: | |
| logger.error(f"Error reading SSH private key file: {e}") | |
| def create_key_pair(key_name: str = config.AWS_EC2_KEY_NAME, key_path: str = config.AWS_EC2_KEY_PATH) -> str | None: | |
| """ | |
| Creates a new EC2 key pair and saves it to a file. | |
| Args: | |
| key_name (str): The name of the key pair to create. Defaults to config.AWS_EC2_KEY_NAME. | |
| key_path (str): The path where the key file should be saved. Defaults to config.AWS_EC2_KEY_PATH. | |
| Returns: | |
| str | None: The name of the created key pair or None if an error occurred. | |
| """ | |
| ec2_client = boto3.client('ec2', region_name=config.AWS_REGION) | |
| try: | |
| key_pair = ec2_client.create_key_pair(KeyName=key_name) | |
| private_key = key_pair['KeyMaterial'] | |
| # Save the private key to a file | |
| with open(key_path, "w") as key_file: | |
| key_file.write(private_key) | |
| os.chmod(key_path, 0o400) # Set read-only permissions | |
| logger.info(f"Key pair {key_name} created and saved to {key_path}") | |
| return key_name | |
| except ClientError as e: | |
| logger.error(f"Error creating key pair: {e}") | |
| return None | |
| def get_or_create_security_group_id(ports: list[int] = [22, 6092]) -> str | None: | |
| """ | |
| Retrieves or creates a security group with the specified ports opened. | |
| Args: | |
| ports (list[int]): A list of ports to open in the security group. Defaults to [22, 6092]. | |
| Returns: | |
| str | None: The ID of the security group, or None if an error occurred. | |
| """ | |
| ec2 = boto3.client('ec2', region_name=config.AWS_REGION) | |
| # Construct ip_permissions list | |
| ip_permissions = [{ | |
| 'IpProtocol': 'tcp', | |
| 'FromPort': port, | |
| 'ToPort': port, | |
| 'IpRanges': [{'CidrIp': '0.0.0.0/0'}] | |
| } for port in ports] | |
| try: | |
| response = ec2.describe_security_groups(GroupNames=[config.AWS_EC2_SECURITY_GROUP]) | |
| security_group_id = response['SecurityGroups'][0]['GroupId'] | |
| logger.info(f"Security group '{config.AWS_EC2_SECURITY_GROUP}' already exists: {security_group_id}") | |
| for ip_permission in ip_permissions: | |
| try: | |
| ec2.authorize_security_group_ingress( | |
| GroupId=security_group_id, | |
| IpPermissions=[ip_permission] | |
| ) | |
| logger.info(f"Added inbound rule to allow TCP traffic on port {ip_permission['FromPort']} from any IP") | |
| except ClientError as e: | |
| if e.response['Error']['Code'] == 'InvalidPermission.Duplicate': | |
| logger.info(f"Rule for port {ip_permission['FromPort']} already exists") | |
| else: | |
| logger.error(f"Error adding rule for port {ip_permission['FromPort']}: {e}") | |
| return security_group_id | |
| except ClientError as e: | |
| if e.response['Error']['Code'] == 'InvalidGroup.NotFound': | |
| try: | |
| # Create the security group | |
| response = ec2.create_security_group( | |
| GroupName=config.AWS_EC2_SECURITY_GROUP, | |
| Description='Security group for specified port access', | |
| TagSpecifications=[ | |
| { | |
| 'ResourceType': 'security-group', | |
| 'Tags': [{'Key': 'Name', 'Value': config.PROJECT_NAME}] | |
| } | |
| ] | |
| ) | |
| security_group_id = response['GroupId'] | |
| logger.info(f"Created security group '{config.AWS_EC2_SECURITY_GROUP}' with ID: {security_group_id}") | |
| # Add rules for the given ports | |
| ec2.authorize_security_group_ingress(GroupId=security_group_id, IpPermissions=ip_permissions) | |
| logger.info(f"Added inbound rules to allow access on {ports=}") | |
| return security_group_id | |
| except ClientError as e: | |
| logger.error(f"Error creating security group: {e}") | |
| return None | |
| else: | |
| logger.error(f"Error describing security groups: {e}") | |
| return None | |
| def deploy_ec2_instance( | |
| ami: str = config.AWS_EC2_AMI, | |
| instance_type: str = config.AWS_EC2_INSTANCE_TYPE, | |
| project_name: str = config.PROJECT_NAME, | |
| key_name: str = config.AWS_EC2_KEY_NAME, | |
| disk_size: int = config.AWS_EC2_DISK_SIZE, | |
| ) -> tuple[str | None, str | None]: | |
| """ | |
| Deploys an EC2 instance with the specified parameters. | |
| Args: | |
| ami (str): The Amazon Machine Image ID to use for the instance. Defaults to config.AWS_EC2_AMI. | |
| instance_type (str): The type of instance to deploy. Defaults to config.AWS_EC2_INSTANCE_TYPE. | |
| project_name (str): The project name, used for tagging the instance. Defaults to config.PROJECT_NAME. | |
| key_name (str): The name of the key pair to use for the instance. Defaults to config.AWS_EC2_KEY_NAME. | |
| disk_size (int): The size of the disk in GB. Defaults to config.AWS_EC2_DISK_SIZE. | |
| Returns: | |
| tuple[str | None, str | None]: A tuple containing the instance ID and IP address, or None, None if deployment fails. | |
| """ | |
| ec2 = boto3.resource('ec2') | |
| ec2_client = boto3.client('ec2') | |
| # Check if key pair exists, if not create one | |
| try: | |
| ec2_client.describe_key_pairs(KeyNames=[key_name]) | |
| except ClientError as e: | |
| create_key_pair(key_name) | |
| # Fetch the security group ID | |
| security_group_id = get_or_create_security_group_id() | |
| if not security_group_id: | |
| logger.error("Unable to retrieve security group ID. Instance deployment aborted.") | |
| return None, None | |
| # Check for existing instances | |
| instances = ec2.instances.filter( | |
| Filters=[ | |
| {'Name': 'tag:Name', 'Values': [config.PROJECT_NAME]}, | |
| {'Name': 'instance-state-name', 'Values': ['running', 'pending', 'stopped']} | |
| ] | |
| ) | |
| for instance in instances: | |
| if instance.state['Name'] == 'running': | |
| logger.info(f"Instance already running: ID - {instance.id}, IP - {instance.public_ip_address}") | |
| return instance.id, instance.public_ip_address | |
| elif instance.state['Name'] == 'stopped': | |
| logger.info(f"Starting existing stopped instance: ID - {instance.id}") | |
| ec2_client.start_instances(InstanceIds=[instance.id]) | |
| instance.wait_until_running() | |
| instance.reload() | |
| logger.info(f"Instance started: ID - {instance.id}, IP - {instance.public_ip_address}") | |
| return instance.id, instance.public_ip_address | |
| elif state == 'pending': | |
| logger.info(f"Instance is pending: ID - {instance.id}. Waiting for 'running' state.") | |
| try: | |
| instance.wait_until_running() # Wait for the instance to be in 'running' state | |
| instance.reload() # Reload the instance attributes | |
| logger.info(f"Instance is now running: ID - {instance.id}, IP - {instance.public_ip_address}") | |
| return instance.id, instance.public_ip_address | |
| except botocore.exceptions.WaiterError as e: | |
| logger.error(f"Error waiting for instance to run: {e}") | |
| return None, None | |
| # Define EBS volume configuration | |
| ebs_config = { | |
| 'DeviceName': '/dev/sda1', # You may need to change this depending on the instance type and AMI | |
| 'Ebs': { | |
| 'VolumeSize': disk_size, | |
| 'VolumeType': 'gp3', # Or other volume types like gp2, io1, etc. | |
| 'DeleteOnTermination': True # Set to False if you want to keep the volume after instance termination | |
| }, | |
| } | |
| # Create a new instance if none exist | |
| new_instance = ec2.create_instances( | |
| ImageId=ami, | |
| MinCount=1, | |
| MaxCount=1, | |
| InstanceType=instance_type, | |
| KeyName=key_name, | |
| SecurityGroupIds=[security_group_id], | |
| BlockDeviceMappings=[ebs_config], | |
| TagSpecifications=[ | |
| { | |
| 'ResourceType': 'instance', | |
| 'Tags': [{'Key': 'Name', 'Value': project_name}] | |
| }, | |
| ] | |
| )[0] | |
| new_instance.wait_until_running() | |
| new_instance.reload() | |
| logger.info(f"New instance created: ID - {new_instance.id}, IP - {new_instance.public_ip_address}") | |
| return new_instance.id, new_instance.public_ip_address | |
| def configure_ec2_instance( | |
| instance_id: str | None = None, | |
| instance_ip: str | None = None, | |
| max_ssh_retries: int = 10, | |
| ssh_retry_delay: int = 10, | |
| max_cmd_retries: int = 10, | |
| cmd_retry_delay: int = 30, | |
| ) -> tuple[str | None, str | None]: | |
| """ | |
| Configures the specified EC2 instance for Docker builds. | |
| Args: | |
| instance_id (str | None): The ID of the instance to configure. If None, a new instance will be deployed. Defaults to None. | |
| instance_ip (str | None): The IP address of the instance. Must be provided if instance_id is manually passed. Defaults to None. | |
| max_ssh_retries (int): Maximum number of SSH connection retries. Defaults to 10. | |
| ssh_retry_delay (int): Delay between SSH connection retries in seconds. Defaults to 10. | |
| max_cmd_retries (int): Maximum number of command execution retries. Defaults to 10. | |
| cmd_retry_delay (int): Delay between command execution retries in seconds. Defaults to 30. | |
| Returns: | |
| tuple[str | None, str | None]: A tuple containing the instance ID and IP address, or None, None if configuration fails. | |
| """ | |
| if not instance_id: | |
| ec2_instance_id, ec2_instance_ip = deploy_ec2_instance() | |
| else: | |
| ec2_instance_id = instance_id | |
| ec2_instance_ip = instance_ip # Ensure instance IP is provided if instance_id is manually passed | |
| key = paramiko.RSAKey.from_private_key_file(config.AWS_EC2_KEY_PATH) | |
| ssh_client = paramiko.SSHClient() | |
| ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
| ssh_retries = 0 | |
| while ssh_retries < max_ssh_retries: | |
| try: | |
| ssh_client.connect(hostname=ec2_instance_ip, username='ubuntu', pkey=key) | |
| break # Successful SSH connection, break out of the loop | |
| except Exception as e: | |
| ssh_retries += 1 | |
| logger.error(f"SSH connection attempt {ssh_retries} failed: {e}") | |
| if ssh_retries < max_ssh_retries: | |
| logger.info(f"Retrying SSH connection in {ssh_retry_delay} seconds...") | |
| time.sleep(ssh_retry_delay) | |
| else: | |
| logger.error("Maximum SSH connection attempts reached. Aborting.") | |
| return | |
| # Commands to set up the EC2 instance for Docker builds | |
| commands = [ | |
| "sudo apt-get update", | |
| "sudo apt-get install -y docker.io", | |
| "sudo systemctl start docker", | |
| "sudo systemctl enable docker", | |
| "sudo usermod -a -G docker ${USER}", | |
| "sudo curl -L \"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose", | |
| "sudo chmod +x /usr/local/bin/docker-compose", | |
| "sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose", | |
| ] | |
| for command in commands: | |
| logger.info(f"Executing command: {command}") | |
| cmd_retries = 0 | |
| while cmd_retries < max_cmd_retries: | |
| stdin, stdout, stderr = ssh_client.exec_command(command) | |
| exit_status = stdout.channel.recv_exit_status() # Blocking call | |
| if exit_status == 0: | |
| logger.info(f"Command executed successfully") | |
| break | |
| else: | |
| error_message = stderr.read() | |
| if "Could not get lock" in str(error_message): | |
| cmd_retries += 1 | |
| logger.warning(f"dpkg is locked, retrying command in {cmd_retry_delay} seconds... Attempt {cmd_retries}/{max_cmd_retries}") | |
| time.sleep(cmd_retry_delay) | |
| else: | |
| logger.error(f"Error in command: {command}, Exit Status: {exit_status}, Error: {error_message}") | |
| break # Non-dpkg lock error, break out of the loop | |
| ssh_client.close() | |
| return ec2_instance_id, ec2_instance_ip | |
| def generate_github_actions_workflow() -> None: | |
| """ | |
| Generates and writes the GitHub Actions workflow file for Docker build on EC2. | |
| Returns: | |
| None | |
| """ | |
| current_branch = get_current_git_branch() | |
| _, host = deploy_ec2_instance() | |
| # Set up Jinja2 environment | |
| env = Environment(loader=FileSystemLoader('.')) | |
| template = env.get_template('docker-build-ec2.yml.j2') | |
| # Render the template with the current branch | |
| rendered_workflow = template.render( | |
| branch_name=current_branch, | |
| host=host, | |
| username=config.AWS_EC2_USER, | |
| project_name=config.PROJECT_NAME, | |
| github_path=config.GITHUB_PATH, | |
| github_repo=config.GITHUB_REPO, | |
| ) | |
| # Write the rendered workflow to a file | |
| workflows_dir = '.github/workflows' | |
| os.makedirs(workflows_dir, exist_ok=True) | |
| with open(os.path.join(workflows_dir, 'docker-build-ec2.yml'), 'w') as file: | |
| file.write("# Autogenerated via deploy.py, do not edit!\n\n") | |
| file.write(rendered_workflow) | |
| logger.info("GitHub Actions EC2 workflow file generated successfully.") | |
| def get_current_git_branch() -> str: | |
| """ | |
| Retrieves the current active git branch name. | |
| Returns: | |
| str: The name of the current git branch. | |
| """ | |
| repo = git.Repo(search_parent_directories=True) | |
| branch = repo.active_branch.name | |
| return branch | |
| def get_github_actions_url() -> str: | |
| """ | |
| Get the GitHub Actions URL for the user's repository. | |
| Returns: | |
| str: The Github Actions URL | |
| """ | |
| url = f"https://github.com/{config.GITHUB_OWNER}/{config.GITHUB_REPO}/actions" | |
| return url | |
| def get_gradio_server_url(ip_address: str) -> str: | |
| """ | |
| Get the Gradio server URL using the provided IP address. | |
| Args: | |
| ip_address (str): The IP address of the EC2 instance running the Gradio server. | |
| Returns: | |
| str: The Gradio server URL | |
| """ | |
| url = f"http://{ip_address}:6092" # TODO: make port configurable | |
| return url | |
| def git_push_set_upstream(branch_name: str): | |
| """ | |
| Pushes the current branch to the remote 'origin' and sets it to track the upstream branch. | |
| Args: | |
| branch_name (str): The name of the current branch to push. | |
| """ | |
| try: | |
| # Push the current branch and set the remote 'origin' as upstream | |
| subprocess.run(["git", "push", "--set-upstream", "origin", branch_name], check=True) | |
| logger.info(f"Branch '{branch_name}' pushed and set up to track 'origin/{branch_name}'.") | |
| except subprocess.CalledProcessError as e: | |
| logger.error(f"Failed to push branch '{branch_name}' to 'origin': {e}") | |
| def update_git_remote_with_pat(github_owner: str, repo_name: str, pat: str): | |
| """ | |
| Updates the git remote 'origin' to include the Personal Access Token in the URL. | |
| Args: | |
| github_owner (str): GitHub repository owner. | |
| repo_name (str): GitHub repository name. | |
| pat (str): Personal Access Token with the necessary scopes. | |
| """ | |
| new_origin_url = f"https://{github_owner}:{pat}@github.com/{github_owner}/{repo_name}.git" | |
| try: | |
| # Remove the existing 'origin' remote | |
| subprocess.run(["git", "remote", "remove", "origin"], check=True) | |
| # Add the new 'origin' with the PAT in the URL | |
| subprocess.run(["git", "remote", "add", "origin", new_origin_url], check=True) | |
| logger.info("Git remote 'origin' updated successfully.") | |
| except subprocess.CalledProcessError as e: | |
| logger.error(f"Failed to update git remote 'origin': {e}") | |
| class Deploy: | |
| def start() -> None: | |
| """ | |
| Main method to execute the deployment process. | |
| Returns: | |
| None | |
| """ | |
| set_github_secrets() | |
| instance_id, instance_ip = configure_ec2_instance() | |
| assert instance_ip, f"invalid {instance_ip=}" | |
| generate_github_actions_workflow() | |
| # Update the Git remote configuration to include the PAT | |
| update_git_remote_with_pat( | |
| config.GITHUB_OWNER, config.GITHUB_REPO, config.GITHUB_TOKEN, | |
| ) | |
| # Add, commit, and push the workflow file changes, setting the upstream branch | |
| try: | |
| subprocess.run( | |
| ["git", "add", ".github/workflows/docker-build-ec2.yml"], check=True, | |
| ) | |
| subprocess.run( | |
| ["git", "commit", "-m", "'add workflow file'"], check=True, | |
| ) | |
| current_branch = get_current_git_branch() | |
| git_push_set_upstream(current_branch) | |
| except subprocess.CalledProcessError as e: | |
| logger.error(f"Failed to commit or push changes: {e}") | |
| github_actions_url = get_github_actions_url() | |
| gradio_server_url = get_gradio_server_url(instance_ip) | |
| logger.info("Deployment process completed.") | |
| logger.info(f"Check the GitHub Actions at {github_actions_url}.") | |
| logger.info("Once the action is complete, run:") | |
| logger.info(f" python client.py {gradio_server_url}") | |
| def pause(project_name: str = config.PROJECT_NAME) -> None: | |
| """ | |
| Shuts down the EC2 instance associated with the specified project name. | |
| Args: | |
| project_name (str): The project name used to tag the instance. Defaults to config.PROJECT_NAME. | |
| Returns: | |
| None | |
| """ | |
| ec2 = boto3.resource('ec2') | |
| instances = ec2.instances.filter( | |
| Filters=[ | |
| {'Name': 'tag:Name', 'Values': [project_name]}, | |
| {'Name': 'instance-state-name', 'Values': ['running']} | |
| ] | |
| ) | |
| for instance in instances: | |
| logger.info(f"Shutting down instance: ID - {instance.id}") | |
| instance.stop() | |
| def stop( | |
| project_name: str = config.PROJECT_NAME, | |
| security_group_name: str = config.AWS_EC2_SECURITY_GROUP, | |
| ) -> None: | |
| """ | |
| Terminates the EC2 instance and deletes the associated security group. | |
| Args: | |
| project_name (str): The project name used to tag the instance. Defaults to config.PROJECT_NAME. | |
| security_group_name (str): The name of the security group to delete. Defaults to config.AWS_EC2_SECURITY_GROUP. | |
| Returns: | |
| None | |
| """ | |
| ec2_resource = boto3.resource('ec2') | |
| ec2_client = boto3.client('ec2') | |
| # Terminate EC2 instances | |
| instances = ec2_resource.instances.filter( | |
| Filters=[ | |
| {'Name': 'tag:Name', 'Values': [project_name]}, | |
| {'Name': 'instance-state-name', 'Values': ['pending', 'running', 'shutting-down', 'stopped', 'stopping']} | |
| ] | |
| ) | |
| for instance in instances: | |
| logger.info(f"Terminating instance: ID - {instance.id}") | |
| instance.terminate() | |
| instance.wait_until_terminated() | |
| logger.info(f"Instance {instance.id} terminated successfully.") | |
| # Delete security group | |
| try: | |
| ec2_client.delete_security_group(GroupName=security_group_name) | |
| logger.info(f"Deleted security group: {security_group_name}") | |
| except ClientError as e: | |
| if e.response['Error']['Code'] == 'InvalidGroup.NotFound': | |
| logger.info(f"Security group {security_group_name} does not exist or already deleted.") | |
| else: | |
| logger.error(f"Error deleting security group: {e}") | |
| def status() -> None: | |
| """ | |
| Lists all EC2 instances tagged with the project name. | |
| Returns: | |
| None | |
| """ | |
| ec2 = boto3.resource('ec2') | |
| instances = ec2.instances.filter( | |
| Filters=[{'Name': 'tag:Name', 'Values': [config.PROJECT_NAME]}] | |
| ) | |
| for instance in instances: | |
| logger.info(f"Instance ID: {instance.id}, State: {instance.state['Name']}") | |
| if __name__ == "__main__": | |
| fire.Fire(Deploy) | |