729 lines
30 KiB
Python
Executable File
729 lines
30 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import json
|
|
import yaml
|
|
import requests
|
|
import os
|
|
|
|
# Steps for deployment
|
|
STEPS = [
|
|
"refresh_aws_auth",
|
|
"fetch_or_create_vpc_and_subnets",
|
|
"create_ecr_repositories",
|
|
"create_iam_role",
|
|
"create_security_groups",
|
|
"request_acm_certificate",
|
|
"build_and_push_docker",
|
|
"create_task_definition",
|
|
"setup_alb",
|
|
"deploy_ecs_service",
|
|
"configure_custom_domain",
|
|
"test_endpoints"
|
|
]
|
|
|
|
# Utility function to prompt user for confirmation
|
|
def confirm_step(step_name):
|
|
while True:
|
|
response = input(f"Proceed with {step_name}? (yes/no): ").strip().lower()
|
|
if response in ["yes", "no"]:
|
|
return response == "yes"
|
|
print("Please enter 'yes' or 'no'.")
|
|
|
|
# Utility function to run AWS CLI or shell commands and handle errors
|
|
def run_command(command, error_message, additional_diagnostics=None, cwd="."):
|
|
try:
|
|
result = subprocess.run(command, capture_output=True, text=True, check=True, cwd=cwd)
|
|
return result
|
|
except subprocess.CalledProcessError as e:
|
|
with open("error_context.md", "w") as f:
|
|
f.write(f"{error_message}:\n")
|
|
f.write(f"Command: {' '.join(command)}\n")
|
|
f.write(f"Exit Code: {e.returncode}\n")
|
|
f.write(f"Stdout: {e.stdout}\n")
|
|
f.write(f"Stderr: {e.stderr}\n")
|
|
if additional_diagnostics:
|
|
for diag_cmd in additional_diagnostics:
|
|
diag_result = subprocess.run(diag_cmd, capture_output=True, text=True)
|
|
f.write(f"\nDiagnostic command: {' '.join(diag_cmd)}\n")
|
|
f.write(f"Stdout: {diag_result.stdout}\n")
|
|
f.write(f"Stderr: {diag_result.stderr}\n")
|
|
raise Exception(f"{error_message}: {e.stderr}")
|
|
|
|
# Utility function to load or initialize state
|
|
def load_state(project_name):
|
|
state_file = f"{project_name}-state.json"
|
|
if os.path.exists(state_file):
|
|
with open(state_file, "r") as f:
|
|
return json.load(f)
|
|
return {"last_step": -1}
|
|
|
|
# Utility function to save state
|
|
def save_state(project_name, state):
|
|
state_file = f"{project_name}-state.json"
|
|
with open(state_file, "w") as f:
|
|
json.dump(state, f, indent=4)
|
|
|
|
# DNS Check Function
|
|
def check_dns_propagation(domain, alb_dns):
|
|
try:
|
|
result = subprocess.run(["dig", "+short", domain], capture_output=True, text=True)
|
|
if alb_dns in result.stdout:
|
|
return True
|
|
return False
|
|
except Exception as e:
|
|
print(f"Failed to check DNS: {e}")
|
|
return False
|
|
|
|
# Step Functions
|
|
def refresh_aws_auth(project_name, state, config):
|
|
if state["last_step"] >= 0:
|
|
print("Skipping refresh_aws_auth (already completed)")
|
|
return
|
|
if not confirm_step("Refresh AWS authentication"):
|
|
sys.exit("User aborted.")
|
|
run_command(
|
|
["aws", "sts", "get-caller-identity"],
|
|
"Failed to verify AWS credentials"
|
|
)
|
|
print("AWS authentication verified.")
|
|
state["last_step"] = 0
|
|
save_state(project_name, state)
|
|
|
|
def fetch_or_create_vpc_and_subnets(project_name, state, config):
|
|
if state["last_step"] >= 1:
|
|
print("Skipping fetch_or_create_vpc_and_subnets (already completed)")
|
|
return state["vpc_id"], state["public_subnets"]
|
|
if not confirm_step("Fetch or Create VPC and Subnets"):
|
|
sys.exit("User aborted.")
|
|
|
|
# Fetch AWS account ID
|
|
result = run_command(
|
|
["aws", "sts", "get-caller-identity"],
|
|
"Failed to get AWS account ID"
|
|
)
|
|
account_id = json.loads(result.stdout)["Account"]
|
|
|
|
# Fetch default VPC
|
|
result = run_command(
|
|
["aws", "ec2", "describe-vpcs", "--filters", "Name=isDefault,Values=true", "--region", config["aws_region"]],
|
|
"Failed to describe VPCs"
|
|
)
|
|
vpcs = json.loads(result.stdout).get("Vpcs", [])
|
|
if not vpcs:
|
|
result = run_command(
|
|
["aws", "ec2", "create-vpc", "--cidr-block", "10.0.0.0/16", "--region", config["aws_region"]],
|
|
"Failed to create VPC"
|
|
)
|
|
vpc_id = json.loads(result.stdout)["Vpc"]["VpcId"]
|
|
run_command(
|
|
["aws", "ec2", "modify-vpc-attribute", "--vpc-id", vpc_id, "--enable-dns-hostnames", "--region", config["aws_region"]],
|
|
"Failed to enable DNS hostnames"
|
|
)
|
|
else:
|
|
vpc_id = vpcs[0]["VpcId"]
|
|
|
|
# Fetch or create subnets
|
|
result = run_command(
|
|
["aws", "ec2", "describe-subnets", "--filters", f"Name=vpc-id,Values={vpc_id}", "--region", config["aws_region"]],
|
|
"Failed to describe subnets"
|
|
)
|
|
subnets = json.loads(result.stdout).get("Subnets", [])
|
|
if len(subnets) < 2:
|
|
azs = json.loads(run_command(
|
|
["aws", "ec2", "describe-availability-zones", "--region", config["aws_region"]],
|
|
"Failed to describe availability zones"
|
|
).stdout)["AvailabilityZones"][:2]
|
|
subnet_ids = []
|
|
for i, az in enumerate(azs):
|
|
az_name = az["ZoneName"]
|
|
result = run_command(
|
|
["aws", "ec2", "create-subnet", "--vpc-id", vpc_id, "--cidr-block", f"10.0.{i}.0/24", "--availability-zone", az_name, "--region", config["aws_region"]],
|
|
f"Failed to create subnet in {az_name}"
|
|
)
|
|
subnet_id = json.loads(result.stdout)["Subnet"]["SubnetId"]
|
|
subnet_ids.append(subnet_id)
|
|
run_command(
|
|
["aws", "ec2", "modify-subnet-attribute", "--subnet-id", subnet_id, "--map-public-ip-on-launch", "--region", config["aws_region"]],
|
|
f"Failed to make subnet {subnet_id} public"
|
|
)
|
|
else:
|
|
subnet_ids = [s["SubnetId"] for s in subnets[:2]]
|
|
|
|
# Ensure internet gateway
|
|
result = run_command(
|
|
["aws", "ec2", "describe-internet-gateways", "--filters", f"Name=attachment.vpc-id,Values={vpc_id}", "--region", config["aws_region"]],
|
|
"Failed to describe internet gateways"
|
|
)
|
|
igws = json.loads(result.stdout).get("InternetGateways", [])
|
|
if not igws:
|
|
result = run_command(
|
|
["aws", "ec2", "create-internet-gateway", "--region", config["aws_region"]],
|
|
"Failed to create internet gateway"
|
|
)
|
|
igw_id = json.loads(result.stdout)["InternetGateway"]["InternetGatewayId"]
|
|
run_command(
|
|
["aws", "ec2", "attach-internet-gateway", "--vpc-id", vpc_id, "--internet-gateway-id", igw_id, "--region", config["aws_region"]],
|
|
"Failed to attach internet gateway"
|
|
)
|
|
|
|
state["vpc_id"] = vpc_id
|
|
state["public_subnets"] = subnet_ids
|
|
state["last_step"] = 1
|
|
save_state(project_name, state)
|
|
print(f"VPC ID: {vpc_id}, Subnets: {subnet_ids}")
|
|
return vpc_id, subnet_ids
|
|
|
|
def create_ecr_repositories(project_name, state, config):
|
|
if state["last_step"] >= 2:
|
|
print("Skipping create_ecr_repositories (already completed)")
|
|
return
|
|
if not confirm_step("Create ECR Repositories"):
|
|
sys.exit("User aborted.")
|
|
|
|
account_id = json.loads(run_command(
|
|
["aws", "sts", "get-caller-identity"],
|
|
"Failed to get AWS account ID"
|
|
).stdout)["Account"]
|
|
repos = [project_name, f"{project_name}-nginx"]
|
|
for repo in repos:
|
|
result = subprocess.run(
|
|
["aws", "ecr", "describe-repositories", "--repository-names", repo, "--region", config["aws_region"]],
|
|
capture_output=True, text=True
|
|
)
|
|
if result.returncode != 0:
|
|
run_command(
|
|
["aws", "ecr", "create-repository", "--repository-name", repo, "--region", config["aws_region"]],
|
|
f"Failed to create ECR repository {repo}"
|
|
)
|
|
print(f"ECR repository {repo} is ready.")
|
|
state["last_step"] = 2
|
|
save_state(project_name, state)
|
|
|
|
def create_iam_role(project_name, state, config):
|
|
if state["last_step"] >= 3:
|
|
print("Skipping create_iam_role (already completed)")
|
|
return
|
|
if not confirm_step("Create IAM Role"):
|
|
sys.exit("User aborted.")
|
|
|
|
account_id = json.loads(run_command(
|
|
["aws", "sts", "get-caller-identity"],
|
|
"Failed to get AWS account ID"
|
|
).stdout)["Account"]
|
|
role_name = "ecsTaskExecutionRole"
|
|
trust_policy = {
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
|
|
"Action": "sts:AssumeRole"
|
|
}
|
|
]
|
|
}
|
|
with open("trust_policy.json", "w") as f:
|
|
json.dump(trust_policy, f)
|
|
|
|
result = subprocess.run(
|
|
["aws", "iam", "get-role", "--role-name", role_name],
|
|
capture_output=True, text=True
|
|
)
|
|
if result.returncode != 0:
|
|
run_command(
|
|
["aws", "iam", "create-role", "--role-name", role_name, "--assume-role-policy-document", "file://trust_policy.json"],
|
|
f"Failed to create IAM role {role_name}"
|
|
)
|
|
run_command(
|
|
["aws", "iam", "attach-role-policy", "--role-name", role_name, "--policy-arn", "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"],
|
|
"Failed to attach ECS task execution policy"
|
|
)
|
|
os.remove("trust_policy.json")
|
|
state["execution_role_arn"] = f"arn:aws:iam::{account_id}:role/{role_name}"
|
|
state["last_step"] = 3
|
|
save_state(project_name, state)
|
|
print(f"IAM role {role_name} configured.")
|
|
|
|
def create_security_groups(project_name, state, config):
|
|
if state["last_step"] >= 4:
|
|
print("Skipping create_security_groups (already completed)")
|
|
return state["alb_sg_id"], state["ecs_sg_id"]
|
|
if not confirm_step("Create Security Groups"):
|
|
sys.exit("User aborted.")
|
|
|
|
vpc_id = state["vpc_id"]
|
|
alb_sg_name = f"{project_name}-alb-sg"
|
|
result = run_command(
|
|
["aws", "ec2", "describe-security-groups", "--filters", f"Name=vpc-id,Values={vpc_id}", f"Name=group-name,Values={alb_sg_name}", "--region", config["aws_region"]],
|
|
"Failed to describe ALB security group"
|
|
)
|
|
if not json.loads(result.stdout).get("SecurityGroups"):
|
|
result = run_command(
|
|
["aws", "ec2", "create-security-group", "--group-name", alb_sg_name, "--description", "Security group for ALB", "--vpc-id", vpc_id, "--region", config["aws_region"]],
|
|
"Failed to create ALB security group"
|
|
)
|
|
alb_sg_id = json.loads(result.stdout)["GroupId"]
|
|
run_command(
|
|
["aws", "ec2", "authorize-security-group-ingress", "--group-id", alb_sg_id, "--protocol", "tcp", "--port", "80", "--cidr", "0.0.0.0/0", "--region", config["aws_region"]],
|
|
"Failed to authorize HTTP ingress"
|
|
)
|
|
run_command(
|
|
["aws", "ec2", "authorize-security-group-ingress", "--group-id", alb_sg_id, "--protocol", "tcp", "--port", "443", "--cidr", "0.0.0.0/0", "--region", config["aws_region"]],
|
|
"Failed to authorize HTTPS ingress"
|
|
)
|
|
else:
|
|
alb_sg_id = json.loads(result.stdout)["SecurityGroups"][0]["GroupId"]
|
|
|
|
ecs_sg_name = f"{project_name}-ecs-sg"
|
|
result = run_command(
|
|
["aws", "ec2", "describe-security-groups", "--filters", f"Name=vpc-id,Values={vpc_id}", f"Name=group-name,Values={ecs_sg_name}", "--region", config["aws_region"]],
|
|
"Failed to describe ECS security group"
|
|
)
|
|
if not json.loads(result.stdout).get("SecurityGroups"):
|
|
result = run_command(
|
|
["aws", "ec2", "create-security-group", "--group-name", ecs_sg_name, "--description", "Security group for ECS tasks", "--vpc-id", vpc_id, "--region", config["aws_region"]],
|
|
"Failed to create ECS security group"
|
|
)
|
|
ecs_sg_id = json.loads(result.stdout)["GroupId"]
|
|
run_command(
|
|
["aws", "ec2", "authorize-security-group-ingress", "--group-id", ecs_sg_id, "--protocol", "tcp", "--port", "80", "--source-group", alb_sg_id, "--region", config["aws_region"]],
|
|
"Failed to authorize ECS ingress"
|
|
)
|
|
else:
|
|
ecs_sg_id = json.loads(result.stdout)["SecurityGroups"][0]["GroupId"]
|
|
|
|
state["alb_sg_id"] = alb_sg_id
|
|
state["ecs_sg_id"] = ecs_sg_id
|
|
state["last_step"] = 4
|
|
save_state(project_name, state)
|
|
print("Security groups configured.")
|
|
return alb_sg_id, ecs_sg_id
|
|
|
|
def request_acm_certificate(project_name, state, config):
|
|
if state["last_step"] >= 5:
|
|
print("Skipping request_acm_certificate (already completed)")
|
|
return state["cert_arn"]
|
|
if not confirm_step("Request ACM Certificate"):
|
|
sys.exit("User aborted.")
|
|
|
|
domain_name = config["domain_name"]
|
|
result = run_command(
|
|
["aws", "acm", "describe-certificates", "--certificate-statuses", "ISSUED", "--region", config["aws_region"]],
|
|
"Failed to describe certificates"
|
|
)
|
|
certificates = json.loads(result.stdout).get("CertificateSummaryList", [])
|
|
cert_arn = next((c["CertificateArn"] for c in certificates if c["DomainName"] == domain_name), None)
|
|
|
|
if not cert_arn:
|
|
result = run_command(
|
|
["aws", "acm", "request-certificate", "--domain-name", domain_name, "--validation-method", "DNS", "--region", config["aws_region"]],
|
|
"Failed to request ACM certificate"
|
|
)
|
|
cert_arn = json.loads(result.stdout)["CertificateArn"]
|
|
|
|
time.sleep(10)
|
|
result = run_command(
|
|
["aws", "acm", "describe-certificate", "--certificate-arn", cert_arn, "--region", config["aws_region"]],
|
|
"Failed to describe certificate"
|
|
)
|
|
cert_details = json.loads(result.stdout)["Certificate"]
|
|
dns_validations = cert_details.get("DomainValidationOptions", [])
|
|
for validation in dns_validations:
|
|
if validation["ValidationMethod"] == "DNS" and "ResourceRecord" in validation:
|
|
record = validation["ResourceRecord"]
|
|
print(f"Please add this DNS record to validate the certificate for {domain_name}:")
|
|
print(f"Name: {record['Name']}")
|
|
print(f"Type: {record['Type']}")
|
|
print(f"Value: {record['Value']}")
|
|
print("Press Enter after adding the DNS record...")
|
|
input()
|
|
|
|
while True:
|
|
result = run_command(
|
|
["aws", "acm", "describe-certificate", "--certificate-arn", cert_arn, "--region", config["aws_region"]],
|
|
"Failed to check certificate status"
|
|
)
|
|
status = json.loads(result.stdout)["Certificate"]["Status"]
|
|
if status == "ISSUED":
|
|
break
|
|
elif status in ["FAILED", "REVOKED", "INACTIVE"]:
|
|
print("Certificate issuance failed.")
|
|
sys.exit(1)
|
|
time.sleep(10)
|
|
|
|
state["cert_arn"] = cert_arn
|
|
state["last_step"] = 5
|
|
save_state(project_name, state)
|
|
print(f"Certificate ARN: {cert_arn}")
|
|
return cert_arn
|
|
|
|
def build_and_push_docker(project_name, state, config):
|
|
if state["last_step"] >= 6:
|
|
print("Skipping build_and_push_docker (already completed)")
|
|
return state["fastapi_image"], state["nginx_image"]
|
|
if not confirm_step("Build and Push Docker Images"):
|
|
sys.exit("User aborted.")
|
|
|
|
with open("./version.txt", "r") as f:
|
|
version = f.read().strip()
|
|
|
|
account_id = json.loads(run_command(
|
|
["aws", "sts", "get-caller-identity"],
|
|
"Failed to get AWS account ID"
|
|
).stdout)["Account"]
|
|
region = config["aws_region"]
|
|
|
|
login_password = run_command(
|
|
["aws", "ecr", "get-login-password", "--region", region],
|
|
"Failed to get ECR login password"
|
|
).stdout.strip()
|
|
run_command(
|
|
["docker", "login", "--username", "AWS", "--password", login_password, f"{account_id}.dkr.ecr.{region}.amazonaws.com"],
|
|
"Failed to authenticate Docker to ECR"
|
|
)
|
|
|
|
fastapi_image = f"{account_id}.dkr.ecr.{region}.amazonaws.com/{project_name}:{version}"
|
|
run_command(
|
|
["docker", "build", "-f", "Dockerfile", "-t", fastapi_image, "."],
|
|
"Failed to build FastAPI Docker image"
|
|
)
|
|
run_command(
|
|
["docker", "push", fastapi_image],
|
|
"Failed to push FastAPI image"
|
|
)
|
|
|
|
nginx_image = f"{account_id}.dkr.ecr.{region}.amazonaws.com/{project_name}-nginx:{version}"
|
|
run_command(
|
|
["docker", "build", "-f", "Dockerfile", "-t", nginx_image, "."],
|
|
"Failed to build Nginx Docker image",
|
|
cwd="./nginx"
|
|
)
|
|
run_command(
|
|
["docker", "push", nginx_image],
|
|
"Failed to push Nginx image"
|
|
)
|
|
|
|
state["fastapi_image"] = fastapi_image
|
|
state["nginx_image"] = nginx_image
|
|
state["last_step"] = 6
|
|
save_state(project_name, state)
|
|
print("Docker images built and pushed.")
|
|
return fastapi_image, nginx_image
|
|
|
|
def create_task_definition(project_name, state, config):
|
|
if state["last_step"] >= 7:
|
|
print("Skipping create_task_definition (already completed)")
|
|
return state["task_def_arn"]
|
|
if not confirm_step("Create Task Definition"):
|
|
sys.exit("User aborted.")
|
|
|
|
log_group = f"/ecs/{project_name}-logs"
|
|
result = run_command(
|
|
["aws", "logs", "describe-log-groups", "--log-group-name-prefix", log_group, "--region", config["aws_region"]],
|
|
"Failed to describe log groups"
|
|
)
|
|
if not any(lg["logGroupName"] == log_group for lg in json.loads(result.stdout).get("logGroups", [])):
|
|
run_command(
|
|
["aws", "logs", "create-log-group", "--log-group-name", log_group, "--region", config["aws_region"]],
|
|
f"Failed to create log group {log_group}"
|
|
)
|
|
|
|
task_definition = {
|
|
"family": f"{project_name}-taskdef",
|
|
"networkMode": "awsvpc",
|
|
"requiresCompatibilities": ["FARGATE"],
|
|
"cpu": "512",
|
|
"memory": "2048",
|
|
"executionRoleArn": state["execution_role_arn"],
|
|
"containerDefinitions": [
|
|
{
|
|
"name": "fastapi",
|
|
"image": state["fastapi_image"],
|
|
"portMappings": [{"containerPort": 8000, "hostPort": 8000, "protocol": "tcp"}],
|
|
"logConfiguration": {
|
|
"logDriver": "awslogs",
|
|
"options": {
|
|
"awslogs-group": log_group,
|
|
"awslogs-region": config["aws_region"],
|
|
"awslogs-stream-prefix": "fastapi"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "nginx",
|
|
"image": state["nginx_image"],
|
|
"portMappings": [{"containerPort": 80, "hostPort": 80, "protocol": "tcp"}],
|
|
"logConfiguration": {
|
|
"logDriver": "awslogs",
|
|
"options": {
|
|
"awslogs-group": log_group,
|
|
"awslogs-region": config["aws_region"],
|
|
"awslogs-stream-prefix": "nginx"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
with open("task_def.json", "w") as f:
|
|
json.dump(task_definition, f)
|
|
result = run_command(
|
|
["aws", "ecs", "register-task-definition", "--cli-input-json", "file://task_def.json", "--region", config["aws_region"]],
|
|
"Failed to register task definition"
|
|
)
|
|
task_def_arn = json.loads(result.stdout)["taskDefinition"]["taskDefinitionArn"]
|
|
os.remove("task_def.json")
|
|
|
|
state["task_def_arn"] = task_def_arn
|
|
state["last_step"] = 7
|
|
save_state(project_name, state)
|
|
print("Task definition created.")
|
|
return task_def_arn
|
|
|
|
def setup_alb(project_name, state, config):
|
|
if state["last_step"] >= 8:
|
|
print("Skipping setup_alb (already completed)")
|
|
return state["alb_arn"], state["tg_arn"], state["alb_dns"]
|
|
if not confirm_step("Set Up ALB"):
|
|
sys.exit("User aborted.")
|
|
|
|
vpc_id = state["vpc_id"]
|
|
public_subnets = state["public_subnets"]
|
|
alb_name = f"{project_name}-alb"
|
|
|
|
result = subprocess.run(
|
|
["aws", "elbv2", "describe-load-balancers", "--names", alb_name, "--region", config["aws_region"]],
|
|
capture_output=True, text=True
|
|
)
|
|
if result.returncode != 0:
|
|
run_command(
|
|
["aws", "elbv2", "create-load-balancer", "--name", alb_name, "--subnets"] + public_subnets + ["--security-groups", state["alb_sg_id"], "--region", config["aws_region"]],
|
|
"Failed to create ALB"
|
|
)
|
|
alb_arn = json.loads(run_command(
|
|
["aws", "elbv2", "describe-load-balancers", "--names", alb_name, "--region", config["aws_region"]],
|
|
"Failed to describe ALB"
|
|
).stdout)["LoadBalancers"][0]["LoadBalancerArn"]
|
|
alb_dns = json.loads(run_command(
|
|
["aws", "elbv2", "describe-load-balancers", "--names", alb_name, "--region", config["aws_region"]],
|
|
"Failed to get ALB DNS name"
|
|
).stdout)["LoadBalancers"][0]["DNSName"]
|
|
|
|
tg_name = f"{project_name}-tg"
|
|
result = subprocess.run(
|
|
["aws", "elbv2", "describe-target-groups", "--names", tg_name, "--region", config["aws_region"]],
|
|
capture_output=True, text=True
|
|
)
|
|
if result.returncode != 0:
|
|
run_command(
|
|
["aws", "elbv2", "create-target-group", "--name", tg_name, "--protocol", "HTTP", "--port", "80", "--vpc-id", vpc_id, "--region", config["aws_region"]],
|
|
"Failed to create target group"
|
|
)
|
|
tg_arn = json.loads(run_command(
|
|
["aws", "elbv2", "describe-target-groups", "--names", tg_name, "--region", config["aws_region"]],
|
|
"Failed to describe target group"
|
|
).stdout)["TargetGroups"][0]["TargetGroupArn"]
|
|
|
|
result = run_command(
|
|
["aws", "elbv2", "describe-listeners", "--load-balancer-arn", alb_arn, "--region", config["aws_region"]],
|
|
"Failed to describe listeners"
|
|
)
|
|
listeners = json.loads(result.stdout).get("Listeners", [])
|
|
if not any(l["Port"] == 80 for l in listeners):
|
|
run_command(
|
|
["aws", "elbv2", "create-listener", "--load-balancer-arn", alb_arn, "--protocol", "HTTP", "--port", "80", "--default-actions", "Type=redirect,RedirectConfig={Protocol=HTTPS,Port=443,StatusCode=HTTP_301}", "--region", config["aws_region"]],
|
|
"Failed to create HTTP listener"
|
|
)
|
|
if not any(l["Port"] == 443 for l in listeners):
|
|
run_command(
|
|
["aws", "elbv2", "create-listener", "--load-balancer-arn", alb_arn, "--protocol", "HTTPS", "--port", "443", "--certificates", f"CertificateArn={state['cert_arn']}", "--default-actions", f"Type=forward,TargetGroupArn={tg_arn}", "--region", config["aws_region"]],
|
|
"Failed to create HTTPS listener"
|
|
)
|
|
|
|
state["alb_arn"] = alb_arn
|
|
state["tg_arn"] = tg_arn
|
|
state["alb_dns"] = alb_dns
|
|
state["last_step"] = 8
|
|
save_state(project_name, state)
|
|
print("ALB configured.")
|
|
return alb_arn, tg_arn, alb_dns
|
|
|
|
def deploy_ecs_service(project_name, state, config):
|
|
if state["last_step"] >= 9:
|
|
print("Skipping deploy_ecs_service (already completed)")
|
|
return
|
|
if not confirm_step("Deploy ECS Service"):
|
|
sys.exit("User aborted.")
|
|
|
|
cluster_name = f"{project_name}-cluster"
|
|
result = run_command(
|
|
["aws", "ecs", "describe-clusters", "--clusters", cluster_name, "--region", config["aws_region"]],
|
|
"Failed to describe clusters"
|
|
)
|
|
if not json.loads(result.stdout).get("clusters"):
|
|
run_command(
|
|
["aws", "ecs", "create-cluster", "--cluster-name", cluster_name, "--region", config["aws_region"]],
|
|
"Failed to create ECS cluster"
|
|
)
|
|
|
|
service_name = f"{project_name}-service"
|
|
result = run_command(
|
|
["aws", "ecs", "describe-services", "--cluster", cluster_name, "--services", service_name, "--region", config["aws_region"]],
|
|
"Failed to describe services",
|
|
additional_diagnostics=[["aws", "ecs", "list-tasks", "--cluster", cluster_name, "--service-name", service_name, "--region", config["aws_region"]]]
|
|
)
|
|
services = json.loads(result.stdout).get("services", [])
|
|
if not services or services[0]["status"] == "INACTIVE":
|
|
run_command(
|
|
["aws", "ecs", "create-service", "--cluster", cluster_name, "--service-name", service_name, "--task-definition", state["task_def_arn"], "--desired-count", "1", "--launch-type", "FARGATE", "--network-configuration", f"awsvpcConfiguration={{subnets={json.dumps(state['public_subnets'])},securityGroups=[{state['ecs_sg_id']}],assignPublicIp=ENABLED}}", "--load-balancers", f"targetGroupArn={state['tg_arn']},containerName=nginx,containerPort=80", "--region", config["aws_region"]],
|
|
"Failed to create ECS service"
|
|
)
|
|
else:
|
|
run_command(
|
|
["aws", "ecs", "update-service", "--cluster", cluster_name, "--service", service_name, "--task-definition", state["task_def_arn"], "--region", config["aws_region"]],
|
|
"Failed to update ECS service"
|
|
)
|
|
|
|
state["last_step"] = 9
|
|
save_state(project_name, state)
|
|
print("ECS service deployed.")
|
|
|
|
def configure_custom_domain(project_name, state, config):
|
|
if state["last_step"] >= 10:
|
|
print("Skipping configure_custom_domain (already completed)")
|
|
return
|
|
if not confirm_step("Configure Custom Domain"):
|
|
sys.exit("User aborted.")
|
|
|
|
domain_name = config["domain_name"]
|
|
alb_dns = state["alb_dns"]
|
|
print(f"Please add a CNAME record for {domain_name} pointing to {alb_dns} in your DNS provider.")
|
|
print("Press Enter after updating the DNS record...")
|
|
input()
|
|
|
|
while not check_dns_propagation(domain_name, alb_dns):
|
|
print("DNS propagation not complete. Waiting 30 seconds before retrying...")
|
|
time.sleep(30)
|
|
print("DNS propagation confirmed.")
|
|
|
|
state["last_step"] = 10
|
|
save_state(project_name, state)
|
|
print("Custom domain configured.")
|
|
|
|
def test_endpoints(project_name, state, config):
|
|
if state["last_step"] >= 11:
|
|
print("Skipping test_endpoints (already completed)")
|
|
return
|
|
if not confirm_step("Test Endpoints"):
|
|
sys.exit("User aborted.")
|
|
|
|
domain = config["domain_name"]
|
|
time.sleep(30) # Wait for service to stabilize
|
|
|
|
response = requests.get(f"https://{domain}/health", verify=False)
|
|
if response.status_code != 200:
|
|
with open("error_context.md", "w") as f:
|
|
f.write("Health endpoint test failed:\n")
|
|
f.write(f"Status Code: {response.status_code}\n")
|
|
f.write(f"Response: {response.text}\n")
|
|
sys.exit(1)
|
|
print("Health endpoint test passed.")
|
|
|
|
payload = {
|
|
"urls": ["https://example.com"],
|
|
"browser_config": {"headless": True},
|
|
"crawler_config": {"stream": False}
|
|
}
|
|
response = requests.post(f"https://{domain}/crawl", json=payload, verify=False)
|
|
if response.status_code != 200:
|
|
with open("error_context.md", "w") as f:
|
|
f.write("Crawl endpoint test failed:\n")
|
|
f.write(f"Status Code: {response.status_code}\n")
|
|
f.write(f"Response: {response.text}\n")
|
|
sys.exit(1)
|
|
print("Crawl endpoint test passed.")
|
|
|
|
state["last_step"] = 11
|
|
save_state(project_name, state)
|
|
print("Endpoints tested successfully.")
|
|
|
|
# Main Deployment Function
|
|
def deploy(project_name, force=False):
|
|
config_file = f"{project_name}-config.yml"
|
|
if not os.path.exists(config_file):
|
|
print(f"Configuration file {config_file} not found. Run 'init' first.")
|
|
sys.exit(1)
|
|
|
|
with open(config_file, "r") as f:
|
|
config = yaml.safe_load(f)
|
|
|
|
state = load_state(project_name)
|
|
if force:
|
|
state = {"last_step": -1}
|
|
|
|
last_step = state.get("last_step", -1)
|
|
|
|
for step_idx, step_name in enumerate(STEPS):
|
|
if step_idx <= last_step:
|
|
print(f"Skipping {step_name} (already completed)")
|
|
continue
|
|
print(f"Executing step: {step_name}")
|
|
func = globals()[step_name]
|
|
if step_name == "fetch_or_create_vpc_and_subnets":
|
|
vpc_id, public_subnets = func(project_name, state, config)
|
|
elif step_name == "create_security_groups":
|
|
alb_sg_id, ecs_sg_id = func(project_name, state, config)
|
|
elif step_name == "request_acm_certificate":
|
|
cert_arn = func(project_name, state, config)
|
|
elif step_name == "build_and_push_docker":
|
|
fastapi_image, nginx_image = func(project_name, state, config)
|
|
elif step_name == "create_task_definition":
|
|
task_def_arn = func(project_name, state, config)
|
|
elif step_name == "setup_alb":
|
|
alb_arn, tg_arn, alb_dns = func(project_name, state, config)
|
|
elif step_name == "deploy_ecs_service":
|
|
func(project_name, state, config)
|
|
elif step_name == "configure_custom_domain":
|
|
func(project_name, state, config)
|
|
elif step_name == "test_endpoints":
|
|
func(project_name, state, config)
|
|
else:
|
|
func(project_name, state, config)
|
|
|
|
# Init Command
|
|
def init(project_name, domain_name, aws_region):
|
|
config = {
|
|
"project_name": project_name,
|
|
"domain_name": domain_name,
|
|
"aws_region": aws_region
|
|
}
|
|
config_file = f"{project_name}-config.yml"
|
|
with open(config_file, "w") as f:
|
|
yaml.dump(config, f)
|
|
print(f"Configuration file {config_file} created.")
|
|
|
|
# Argument Parser
|
|
parser = argparse.ArgumentParser(description="Crawl4AI Deployment Script")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
# Init Parser
|
|
init_parser = subparsers.add_parser("init", help="Initialize configuration")
|
|
init_parser.add_argument("--project", required=True, help="Project name")
|
|
init_parser.add_argument("--domain", required=True, help="Domain name")
|
|
init_parser.add_argument("--region", required=True, help="AWS region")
|
|
|
|
# Deploy Parser
|
|
deploy_parser = subparsers.add_parser("deploy", help="Deploy the project")
|
|
deploy_parser.add_argument("--project", required=True, help="Project name")
|
|
deploy_parser.add_argument("--force", action="store_true", help="Force redeployment from start")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "init":
|
|
init(args.project, args.domain, args.region)
|
|
elif args.command == "deploy":
|
|
deploy(args.project, args.force)
|
|
else:
|
|
parser.print_help() |