first commit
This commit is contained in:
651
recipes/ecs.mk
Normal file
651
recipes/ecs.mk
Normal file
@@ -0,0 +1,651 @@
|
||||
# AWS ECS Deployment Recipe
|
||||
# Include this in your main Makefile with: include recipes/ecs.mk
|
||||
|
||||
# Configuration variables
|
||||
ECS_CLUSTER_NAME ?= bifrost-cluster
|
||||
ECS_SERVICE_NAME ?= bifrost-service
|
||||
ECS_TASK_FAMILY ?= bifrost-task
|
||||
IMAGE_TAG ?= latest
|
||||
LAUNCH_TYPE ?= FARGATE
|
||||
SECRET_BACKEND ?= secretsmanager
|
||||
AWS_REGION ?= us-east-1
|
||||
VPC_ID ?=
|
||||
SUBNET_IDS ?=
|
||||
SECURITY_GROUP_IDS ?=
|
||||
TARGET_GROUP_ARN ?=
|
||||
CONTAINER_PORT ?= 8080
|
||||
SECRET_NAME ?= bifrost/config
|
||||
SECRET_ARN ?=
|
||||
EXECUTION_ROLE_ARN ?=
|
||||
TASK_ROLE_ARN ?=
|
||||
|
||||
# Configuration JSON file path (optional - provide path to your config.json file)
|
||||
# Example: CONFIG_JSON_FILE=/path/to/config.json
|
||||
CONFIG_JSON_FILE ?=
|
||||
|
||||
.PHONY: deploy-ecs list-ecs-network-resources check-ecs-prerequisites create-ecs-secret register-ecs-task-definition create-ecs-service update-ecs-service tail-ecs-logs ecs-status get-ecs-url cleanup-ecs
|
||||
|
||||
deploy-ecs: ## Deploy Bifrost to ECS (Usage: make deploy-ecs SUBNET_IDS='...' SECURITY_GROUP_IDS='...' [CONFIG_JSON_FILE='...'])
|
||||
@echo "$(BLUE)Starting ECS deployment...$(NC)"
|
||||
@echo ""
|
||||
@$(MAKE) check-ecs-prerequisites
|
||||
@if [ -n "$(CONFIG_JSON_FILE)" ]; then \
|
||||
$(MAKE) create-ecs-secret; \
|
||||
else \
|
||||
echo "$(YELLOW)No CONFIG_JSON_FILE provided, skipping secret creation$(NC)"; \
|
||||
fi
|
||||
@$(MAKE) register-ecs-task-definition
|
||||
@$(MAKE) create-ecs-service
|
||||
@echo ""
|
||||
@echo "$(GREEN)✓ ECS deployment complete!$(NC)"
|
||||
|
||||
list-ecs-network-resources: ## List available VPCs, subnets and security groups for ECS deployment
|
||||
@echo "$(BLUE)Listing available network resources in region $(AWS_REGION)...$(NC)"
|
||||
@echo ""
|
||||
@# Check if AWS CLI is configured
|
||||
@aws sts get-caller-identity > /dev/null 2>&1 || \
|
||||
(echo "$(RED)Error: AWS CLI is not configured.$(NC)" && \
|
||||
echo "$(YELLOW)Run: aws configure$(NC)" && exit 1)
|
||||
@echo "$(CYAN)Available VPCs:$(NC)"
|
||||
@aws ec2 describe-vpcs \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'Vpcs[*].[VpcId,CidrBlock,IsDefault,Tags[?Key==`Name`].Value|[0]]' \
|
||||
--output table
|
||||
@echo ""
|
||||
@echo "$(CYAN)Available Subnets:$(NC)"
|
||||
@aws ec2 describe-subnets \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'Subnets[*].[SubnetId,AvailabilityZone,VpcId,CidrBlock,Tags[?Key==`Name`].Value|[0]]' \
|
||||
--output table
|
||||
@echo ""
|
||||
@echo "$(CYAN)Available Security Groups:$(NC)"
|
||||
@aws ec2 describe-security-groups \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'SecurityGroups[*].[GroupId,GroupName,VpcId,Description]' \
|
||||
--output table
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Usage (Option 1 - Recommended):$(NC)"
|
||||
@echo " Use VPC ID to auto-fetch all subnets:"
|
||||
@echo " $(GREEN)make deploy-ecs VPC_ID='vpc-xxx' SECURITY_GROUP_IDS='sg-xxx'$(NC)"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Usage (Option 2):$(NC)"
|
||||
@echo " Specify subnet IDs manually:"
|
||||
@echo " $(GREEN)make deploy-ecs SUBNET_IDS='subnet-xxx,subnet-yyy' SECURITY_GROUP_IDS='sg-xxx'$(NC)"
|
||||
|
||||
check-ecs-prerequisites: ## Check ECS deployment prerequisites
|
||||
@echo "$(YELLOW)Checking prerequisites...$(NC)"
|
||||
@# Check if AWS CLI is installed
|
||||
@which aws > /dev/null || (echo "$(RED)Error: AWS CLI is not installed.$(NC)" && \
|
||||
echo "$(YELLOW)Please install AWS CLI first.$(NC)" && \
|
||||
echo "$(CYAN)Documentation: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html$(NC)" && \
|
||||
exit 1)
|
||||
@echo "$(GREEN)✓ AWS CLI is installed$(NC)"
|
||||
@# Check if AWS CLI is configured
|
||||
@aws sts get-caller-identity > /dev/null 2>&1 || \
|
||||
(echo "$(RED)Error: AWS CLI is not configured.$(NC)" && \
|
||||
echo "$(YELLOW)Please configure AWS CLI with your credentials.$(NC)" && \
|
||||
echo "$(CYAN)Run: aws configure$(NC)" && \
|
||||
echo "$(CYAN)Documentation: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html$(NC)" && \
|
||||
exit 1)
|
||||
@echo "$(GREEN)✓ AWS CLI is configured$(NC)"
|
||||
@# Check if cluster exists
|
||||
@aws ecs describe-clusters --clusters $(ECS_CLUSTER_NAME) --region $(AWS_REGION) > /dev/null 2>&1 || \
|
||||
(echo "$(RED)Error: ECS cluster '$(ECS_CLUSTER_NAME)' not found$(NC)" && exit 1)
|
||||
@echo "$(GREEN)✓ ECS cluster '$(ECS_CLUSTER_NAME)' exists$(NC)"
|
||||
@# Check/fetch execution role ARN if not provided and using Fargate
|
||||
@if [ -z "$(EXECUTION_ROLE_ARN)" ]; then \
|
||||
echo "$(CYAN)No EXECUTION_ROLE_ARN provided, checking for default role...$(NC)"; \
|
||||
ACCOUNT_ID=$$(aws sts get-caller-identity --query Account --output text 2>/dev/null); \
|
||||
DEFAULT_ROLE_ARN="arn:aws:iam::$$ACCOUNT_ID:role/ecsTaskExecutionRole"; \
|
||||
if aws iam get-role --role-name ecsTaskExecutionRole --region $(AWS_REGION) > /dev/null 2>&1; then \
|
||||
echo "$$DEFAULT_ROLE_ARN" > /tmp/ecs-execution-role.tmp; \
|
||||
echo "$(GREEN)✓ Found default execution role: ecsTaskExecutionRole$(NC)"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "$(RED)Error: No execution role found$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(YELLOW)ECS tasks require an execution role for CloudWatch logs and pulling images.$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(CYAN)Option 1 - Create the default role:$(NC)"; \
|
||||
echo " $(GREEN)aws iam create-role --role-name ecsTaskExecutionRole \\"; \
|
||||
echo " --assume-role-policy-document '{"; \
|
||||
echo " \"Version\": \"2012-10-17\","; \
|
||||
echo " \"Statement\": [{"; \
|
||||
echo " \"Effect\": \"Allow\","; \
|
||||
echo " \"Principal\": {\"Service\": \"ecs-tasks.amazonaws.com\"},"; \
|
||||
echo " \"Action\": \"sts:AssumeRole\""; \
|
||||
echo " }]"; \
|
||||
echo " }'$(NC)"; \
|
||||
echo ""; \
|
||||
echo " $(GREEN)aws iam attach-role-policy --role-name ecsTaskExecutionRole \\"; \
|
||||
echo " --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(CYAN)Option 2 - Specify an existing role:$(NC)"; \
|
||||
echo " $(GREEN)make deploy-ecs EXECUTION_ROLE_ARN='arn:aws:iam::ACCOUNT:role/YOUR_ROLE' ...$(NC)"; \
|
||||
echo ""; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
else \
|
||||
echo "$(GREEN)✓ Using provided execution role$(NC)"; \
|
||||
fi
|
||||
@# Fetch subnets from VPC if VPC_ID is provided but SUBNET_IDS is not
|
||||
@if [ -n "$(VPC_ID)" ] && [ -z "$(SUBNET_IDS)" ]; then \
|
||||
echo "$(CYAN)Fetching subnets from VPC $(VPC_ID)...$(NC)"; \
|
||||
FETCHED_SUBNETS=$$(aws ec2 describe-subnets \
|
||||
--region $(AWS_REGION) \
|
||||
--filters "Name=vpc-id,Values=$(VPC_ID)" \
|
||||
--query 'Subnets[*].SubnetId' \
|
||||
--output text 2>/dev/null | tr '\t' ','); \
|
||||
if [ -z "$$FETCHED_SUBNETS" ]; then \
|
||||
echo "$(RED)Error: No subnets found in VPC $(VPC_ID)$(NC)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "$$FETCHED_SUBNETS" > /tmp/ecs-subnets.tmp; \
|
||||
echo "$(GREEN)✓ Found subnets: $$FETCHED_SUBNETS$(NC)"; \
|
||||
fi
|
||||
@# Check if required network parameters are provided
|
||||
@if [ -z "$(SUBNET_IDS)" ] && [ ! -f /tmp/ecs-subnets.tmp ] && [ -z "$(VPC_ID)" ]; then \
|
||||
echo ""; \
|
||||
echo "$(RED)Error: Network configuration is required$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(YELLOW)You must provide either:$(NC)"; \
|
||||
echo " - VPC_ID (will auto-fetch all subnets in VPC)"; \
|
||||
echo " - SUBNET_IDS (specific subnet IDs)"; \
|
||||
echo ""; \
|
||||
echo "$(CYAN)To list available VPCs and network resources, run:$(NC)"; \
|
||||
echo " $(GREEN)make list-ecs-network-resources$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(CYAN)Then deploy with VPC ID (recommended):$(NC)"; \
|
||||
echo " $(GREEN)make deploy-ecs VPC_ID='vpc-xxx' SECURITY_GROUP_IDS='sg-xxx'$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(CYAN)Or deploy with specific subnet IDs:$(NC)"; \
|
||||
echo " $(GREEN)make deploy-ecs SUBNET_IDS='subnet-xxx,subnet-yyy' SECURITY_GROUP_IDS='sg-xxx'$(NC)"; \
|
||||
echo ""; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ -z "$(SECURITY_GROUP_IDS)" ]; then \
|
||||
echo ""; \
|
||||
echo "$(RED)Error: SECURITY_GROUP_IDS is required$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(CYAN)To list available security groups, run:$(NC)"; \
|
||||
echo " $(GREEN)make list-ecs-network-resources$(NC)"; \
|
||||
echo ""; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(GREEN)✓ Network configuration ready$(NC)"
|
||||
|
||||
create-ecs-secret: ## Create configuration secret in AWS (Secrets Manager or SSM)
|
||||
@if [ -z "$(CONFIG_JSON_FILE)" ]; then \
|
||||
echo "$(RED)Error: CONFIG_JSON_FILE is required for secret creation$(NC)"; \
|
||||
echo "$(YELLOW)Provide CONFIG_JSON_FILE as path to your Bifrost config.json file$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f "$(CONFIG_JSON_FILE)" ]; then \
|
||||
echo "$(RED)Error: Config file not found: $(CONFIG_JSON_FILE)$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(YELLOW)Creating configuration secret...$(NC)"
|
||||
@echo "$(CYAN)Reading config from: $(CONFIG_JSON_FILE)$(NC)"
|
||||
@if [ "$(SECRET_BACKEND)" = "secretsmanager" ]; then \
|
||||
echo "$(CYAN)Using AWS Secrets Manager...$(NC)"; \
|
||||
aws secretsmanager describe-secret --secret-id $(SECRET_NAME) --region $(AWS_REGION) > /dev/null 2>&1 && \
|
||||
(echo "$(YELLOW)Secret already exists, updating...$(NC)" && \
|
||||
aws secretsmanager update-secret \
|
||||
--secret-id $(SECRET_NAME) \
|
||||
--secret-string file://$(CONFIG_JSON_FILE) \
|
||||
--region $(AWS_REGION) > /dev/null) || \
|
||||
(echo "$(YELLOW)Creating new secret...$(NC)" && \
|
||||
aws secretsmanager create-secret \
|
||||
--name $(SECRET_NAME) \
|
||||
--secret-string file://$(CONFIG_JSON_FILE) \
|
||||
--region $(AWS_REGION) > /dev/null); \
|
||||
echo "$(GREEN)✓ Secret created/updated in Secrets Manager: $(SECRET_NAME)$(NC)"; \
|
||||
elif [ "$(SECRET_BACKEND)" = "ssm" ]; then \
|
||||
echo "$(CYAN)Using AWS Systems Manager Parameter Store...$(NC)"; \
|
||||
aws ssm put-parameter \
|
||||
--name $(SECRET_NAME) \
|
||||
--value file://$(CONFIG_JSON_FILE) \
|
||||
--type SecureString \
|
||||
--overwrite \
|
||||
--region $(AWS_REGION) > /dev/null; \
|
||||
echo "$(GREEN)✓ Parameter created/updated in SSM: $(SECRET_NAME)$(NC)"; \
|
||||
else \
|
||||
echo "$(RED)Error: SECRET_BACKEND must be 'secretsmanager' or 'ssm'$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
register-ecs-task-definition: ## Register ECS task definition
|
||||
@echo "$(YELLOW)Registering ECS task definition...$(NC)"
|
||||
@echo "$(CYAN)Launch type: $(LAUNCH_TYPE)$(NC)"
|
||||
@# Create CloudWatch log group if it doesn't exist
|
||||
@echo "$(CYAN)Ensuring CloudWatch log group exists...$(NC)"
|
||||
@aws logs create-log-group \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--region $(AWS_REGION) 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ CloudWatch log group ready: /ecs/$(ECS_TASK_FAMILY)$(NC)"
|
||||
@# Get secret ARN if CONFIG_JSON_FILE was provided
|
||||
@if [ -n "$(CONFIG_JSON_FILE)" ]; then \
|
||||
echo "$(CYAN)Secret backend: $(SECRET_BACKEND)$(NC)"; \
|
||||
else \
|
||||
echo "$(YELLOW)No CONFIG_JSON_FILE provided, deploying without secret$(NC)"; \
|
||||
fi
|
||||
$(eval SECRET_VALUE_ARN := $(shell \
|
||||
if [ -n "$(CONFIG_JSON_FILE)" ]; then \
|
||||
if [ "$(SECRET_BACKEND)" = "secretsmanager" ]; then \
|
||||
if [ -z "$(SECRET_ARN)" ]; then \
|
||||
aws secretsmanager describe-secret --secret-id $(SECRET_NAME) --region $(AWS_REGION) --query 'ARN' --output text 2>/dev/null; \
|
||||
else \
|
||||
echo "$(SECRET_ARN)"; \
|
||||
fi; \
|
||||
elif [ "$(SECRET_BACKEND)" = "ssm" ]; then \
|
||||
if [ -z "$(SECRET_ARN)" ]; then \
|
||||
aws ssm get-parameter --name $(SECRET_NAME) --region $(AWS_REGION) --query 'Parameter.ARN' --output text 2>/dev/null; \
|
||||
else \
|
||||
echo "$(SECRET_ARN)"; \
|
||||
fi; \
|
||||
fi; \
|
||||
fi))
|
||||
@if [ -n "$(CONFIG_JSON_FILE)" ] && [ -z "$(SECRET_VALUE_ARN)" ]; then \
|
||||
echo "$(RED)Error: Could not retrieve secret ARN$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ -n "$(SECRET_VALUE_ARN)" ]; then \
|
||||
echo "$(GREEN)✓ Secret ARN: $(SECRET_VALUE_ARN)$(NC)"; \
|
||||
fi
|
||||
@# Create task definition JSON using shell script for proper JSON formatting
|
||||
@TASK_CPU="256"; \
|
||||
TASK_MEMORY="512"; \
|
||||
if [ "$(LAUNCH_TYPE)" = "FARGATE" ]; then \
|
||||
TASK_CPU="512"; \
|
||||
TASK_MEMORY="1024"; \
|
||||
fi; \
|
||||
EXEC_ROLE="$(EXECUTION_ROLE_ARN)"; \
|
||||
if [ -z "$$EXEC_ROLE" ] && [ -f /tmp/ecs-execution-role.tmp ]; then \
|
||||
EXEC_ROLE=$$(cat /tmp/ecs-execution-role.tmp); \
|
||||
fi; \
|
||||
{ \
|
||||
echo '{'; \
|
||||
echo ' "family": "$(ECS_TASK_FAMILY)",'; \
|
||||
echo ' "networkMode": "awsvpc",'; \
|
||||
echo ' "requiresCompatibilities": ["$(LAUNCH_TYPE)"],'; \
|
||||
if [ "$(LAUNCH_TYPE)" = "FARGATE" ]; then \
|
||||
echo ' "cpu": "'$$TASK_CPU'",'; \
|
||||
echo ' "memory": "'$$TASK_MEMORY'",'; \
|
||||
fi; \
|
||||
if [ -n "$$EXEC_ROLE" ]; then \
|
||||
echo ' "executionRoleArn": "'$$EXEC_ROLE'",'; \
|
||||
fi; \
|
||||
if [ -n "$(TASK_ROLE_ARN)" ]; then \
|
||||
echo ' "taskRoleArn": "$(TASK_ROLE_ARN)",'; \
|
||||
fi; \
|
||||
echo ' "containerDefinitions": [{'; \
|
||||
echo ' "name": "bifrost",'; \
|
||||
echo ' "image": "maximhq/bifrost:$(IMAGE_TAG)",'; \
|
||||
echo ' "essential": true,'; \
|
||||
if [ -n "$(SECRET_VALUE_ARN)" ]; then \
|
||||
echo ' "entryPoint": ["/bin/sh", "-c"],'; \
|
||||
echo ' "command": ["if [ -n \"$$BIFROST_CONFIG\" ]; then echo \"$$BIFROST_CONFIG\" > /app/data/config.json; else echo \"ERROR: BIFROST_CONFIG not set\" >&2 && exit 1; fi && exec /app/docker-entrypoint.sh /app/main"],'; \
|
||||
fi; \
|
||||
echo ' "portMappings": [{'; \
|
||||
echo ' "containerPort": $(CONTAINER_PORT),'; \
|
||||
echo ' "protocol": "tcp"'; \
|
||||
echo ' }],'; \
|
||||
echo ' "environment": [],'; \
|
||||
if [ -n "$(SECRET_VALUE_ARN)" ]; then \
|
||||
echo ' "secrets": [{'; \
|
||||
echo ' "name": "BIFROST_CONFIG",'; \
|
||||
echo ' "valueFrom": "$(SECRET_VALUE_ARN)"'; \
|
||||
echo ' }],'; \
|
||||
fi; \
|
||||
echo ' "healthCheck": {'; \
|
||||
echo ' "command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:$(CONTAINER_PORT)/health || exit 1"],'; \
|
||||
echo ' "interval": 30,'; \
|
||||
echo ' "timeout": 5,'; \
|
||||
echo ' "retries": 3,'; \
|
||||
echo ' "startPeriod": 60'; \
|
||||
echo ' },'; \
|
||||
echo ' "logConfiguration": {'; \
|
||||
echo ' "logDriver": "awslogs",'; \
|
||||
echo ' "options": {'; \
|
||||
echo ' "awslogs-group": "/ecs/$(ECS_TASK_FAMILY)",'; \
|
||||
echo ' "awslogs-region": "$(AWS_REGION)",'; \
|
||||
echo ' "awslogs-stream-prefix": "bifrost"'; \
|
||||
echo ' }'; \
|
||||
echo ' }'; \
|
||||
echo ' }]'; \
|
||||
echo '}'; \
|
||||
} > /tmp/ecs-task-def.json
|
||||
@# Register task definition
|
||||
@aws ecs register-task-definition \
|
||||
--cli-input-json file:///tmp/ecs-task-def.json \
|
||||
--region $(AWS_REGION) > /dev/null
|
||||
@rm -f /tmp/ecs-task-def.json
|
||||
@echo "$(GREEN)✓ Task definition registered: $(ECS_TASK_FAMILY)$(NC)"
|
||||
|
||||
create-ecs-service: ## Create or update ECS service
|
||||
@echo "$(YELLOW)Creating/updating ECS service...$(NC)"
|
||||
@# Get subnet IDs from parameter or temp file
|
||||
@SUBNETS="$(SUBNET_IDS)"; \
|
||||
if [ -z "$$SUBNETS" ] && [ -f /tmp/ecs-subnets.tmp ]; then \
|
||||
SUBNETS=$$(cat /tmp/ecs-subnets.tmp); \
|
||||
fi; \
|
||||
if aws ecs describe-services --cluster $(ECS_CLUSTER_NAME) --services $(ECS_SERVICE_NAME) --region $(AWS_REGION) 2>/dev/null | grep -q "ACTIVE"; then \
|
||||
echo "$(YELLOW)Service exists, updating...$(NC)"; \
|
||||
$(MAKE) update-ecs-service; \
|
||||
else \
|
||||
echo "$(YELLOW)Creating new service...$(NC)"; \
|
||||
if [ -z "$(TARGET_GROUP_ARN)" ]; then \
|
||||
echo "$(CYAN)Creating service without load balancer...$(NC)"; \
|
||||
aws ecs create-service \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--service-name $(ECS_SERVICE_NAME) \
|
||||
--task-definition $(ECS_TASK_FAMILY) \
|
||||
--desired-count 1 \
|
||||
--launch-type $(LAUNCH_TYPE) \
|
||||
--network-configuration "awsvpcConfiguration={subnets=[$$SUBNETS],securityGroups=[$(SECURITY_GROUP_IDS)],assignPublicIp=ENABLED}" \
|
||||
--region $(AWS_REGION) > /dev/null; \
|
||||
else \
|
||||
echo "$(CYAN)Creating service with load balancer...$(NC)"; \
|
||||
aws ecs create-service \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--service-name $(ECS_SERVICE_NAME) \
|
||||
--task-definition $(ECS_TASK_FAMILY) \
|
||||
--desired-count 1 \
|
||||
--launch-type $(LAUNCH_TYPE) \
|
||||
--network-configuration "awsvpcConfiguration={subnets=[$$SUBNETS],securityGroups=[$(SECURITY_GROUP_IDS)],assignPublicIp=ENABLED}" \
|
||||
--load-balancers "targetGroupArn=$(TARGET_GROUP_ARN),containerName=bifrost,containerPort=$(CONTAINER_PORT)" \
|
||||
--health-check-grace-period-seconds 60 \
|
||||
--region $(AWS_REGION) > /dev/null; \
|
||||
fi; \
|
||||
echo "$(GREEN)✓ Service created: $(ECS_SERVICE_NAME)$(NC)"; \
|
||||
rm -f /tmp/ecs-subnets.tmp /tmp/ecs-execution-role.tmp; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Waiting for service to stabilize...$(NC)"
|
||||
@echo "$(CYAN)This may take a few minutes...$(NC)"
|
||||
@aws ecs wait services-stable \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--services $(ECS_SERVICE_NAME) \
|
||||
--region $(AWS_REGION) && \
|
||||
echo "$(GREEN)✓ Service is stable and running!$(NC)" || \
|
||||
(echo "$(RED)✗ Service failed to stabilize$(NC)" && exit 1)
|
||||
@echo ""
|
||||
@echo "$(CYAN)Deployment Status:$(NC)"
|
||||
@aws ecs describe-services \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--services $(ECS_SERVICE_NAME) \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount,Pending:pendingCount}' \
|
||||
--output table
|
||||
@echo ""
|
||||
@echo "$(CYAN)Task Details:$(NC)"
|
||||
@TASK_ARN=$$(aws ecs list-tasks \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--service-name $(ECS_SERVICE_NAME) \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'taskArns[0]' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$TASK_ARN" ] && [ "$$TASK_ARN" != "None" ]; then \
|
||||
aws ecs describe-tasks \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--tasks $$TASK_ARN \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'tasks[0].{TaskARN:taskArn,LastStatus:lastStatus,HealthStatus:healthStatus,StartedAt:startedAt}' \
|
||||
--output table; \
|
||||
echo ""; \
|
||||
if [ -z "$(TARGET_GROUP_ARN)" ]; then \
|
||||
echo "$(CYAN)Public IP Address:$(NC)"; \
|
||||
ENI_ID=$$(aws ecs describe-tasks \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--tasks $$TASK_ARN \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$ENI_ID" ] && [ "$$ENI_ID" != "None" ]; then \
|
||||
PUBLIC_IP=$$(aws ec2 describe-network-interfaces \
|
||||
--network-interface-ids $$ENI_ID \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'NetworkInterfaces[0].Association.PublicIp' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$PUBLIC_IP" ] && [ "$$PUBLIC_IP" != "None" ]; then \
|
||||
echo " $$PUBLIC_IP"; \
|
||||
echo ""; \
|
||||
echo "$(GREEN)✓ Service is accessible at: http://$$PUBLIC_IP:$(CONTAINER_PORT)$(NC)"; \
|
||||
echo "$(CYAN) Health check: http://$$PUBLIC_IP:$(CONTAINER_PORT)/health$(NC)"; \
|
||||
else \
|
||||
echo " $(YELLOW)Public IP not yet assigned$(NC)"; \
|
||||
fi; \
|
||||
else \
|
||||
echo " $(YELLOW)Network interface not found$(NC)"; \
|
||||
fi; \
|
||||
echo ""; \
|
||||
fi; \
|
||||
echo "$(CYAN)Recent logs (last 20 events):$(NC)"; \
|
||||
LOG_STREAM=$$(aws logs describe-log-streams \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--order-by LastEventTime \
|
||||
--descending \
|
||||
--max-items 1 \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'logStreams[0].logStreamName' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$LOG_STREAM" ] && [ "$$LOG_STREAM" != "None" ]; then \
|
||||
aws logs get-log-events \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--log-stream-name $$LOG_STREAM \
|
||||
--limit 20 \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'events[*].message' \
|
||||
--output text 2>/dev/null || echo "$(YELLOW)No logs available yet$(NC)"; \
|
||||
else \
|
||||
echo "$(YELLOW)No log stream found yet$(NC)"; \
|
||||
fi; \
|
||||
else \
|
||||
echo "$(YELLOW)No tasks running yet$(NC)"; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "$(GREEN)To tail logs continuously, run:$(NC)"
|
||||
@echo " $(CYAN)make tail-ecs-logs$(NC)"
|
||||
|
||||
update-ecs-service: ## Force new deployment of ECS service
|
||||
@echo "$(YELLOW)Forcing new deployment...$(NC)"
|
||||
@aws ecs update-service \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--service $(ECS_SERVICE_NAME) \
|
||||
--force-new-deployment \
|
||||
--region $(AWS_REGION) > /dev/null
|
||||
@echo "$(GREEN)✓ Service updated with new deployment$(NC)"
|
||||
|
||||
tail-ecs-logs: ## Tail CloudWatch logs for the ECS service (Ctrl+C to exit)
|
||||
@echo "$(YELLOW)Tailing logs from /ecs/$(ECS_TASK_FAMILY)...$(NC)"
|
||||
@echo "$(CYAN)Press Ctrl+C to stop$(NC)"
|
||||
@echo ""
|
||||
@# Use aws logs tail command (requires AWS CLI v2)
|
||||
@if aws logs tail --help > /dev/null 2>&1; then \
|
||||
aws logs tail /ecs/$(ECS_TASK_FAMILY) \
|
||||
--follow \
|
||||
--format short \
|
||||
--region $(AWS_REGION); \
|
||||
else \
|
||||
echo "$(YELLOW)AWS CLI v2 'tail' command not available, falling back to polling...$(NC)"; \
|
||||
echo ""; \
|
||||
LAST_TIMESTAMP=0; \
|
||||
while true; do \
|
||||
LOG_STREAM=$$(aws logs describe-log-streams \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--order-by LastEventTime \
|
||||
--descending \
|
||||
--max-items 1 \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'logStreams[0].logStreamName' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$LOG_STREAM" ] && [ "$$LOG_STREAM" != "None" ]; then \
|
||||
if [ $$LAST_TIMESTAMP -eq 0 ]; then \
|
||||
EVENTS=$$(aws logs get-log-events \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--log-stream-name $$LOG_STREAM \
|
||||
--limit 10 \
|
||||
--region $(AWS_REGION) 2>/dev/null); \
|
||||
else \
|
||||
EVENTS=$$(aws logs get-log-events \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--log-stream-name $$LOG_STREAM \
|
||||
--start-time $$LAST_TIMESTAMP \
|
||||
--region $(AWS_REGION) 2>/dev/null); \
|
||||
fi; \
|
||||
if [ -n "$$EVENTS" ]; then \
|
||||
echo "$$EVENTS" | jq -r '.events[] | "\(.timestamp | todate) \(.message)"' 2>/dev/null || \
|
||||
echo "$$EVENTS" | grep -o '"message":"[^"]*"' | sed 's/"message":"//;s/"$$//'; \
|
||||
NEW_TIMESTAMP=$$(echo "$$EVENTS" | jq -r '.events[-1].timestamp // 0' 2>/dev/null); \
|
||||
if [ -n "$$NEW_TIMESTAMP" ] && [ "$$NEW_TIMESTAMP" != "0" ] && [ "$$NEW_TIMESTAMP" != "null" ]; then \
|
||||
LAST_TIMESTAMP=$$(($$NEW_TIMESTAMP + 1)); \
|
||||
fi; \
|
||||
fi; \
|
||||
fi; \
|
||||
sleep 2; \
|
||||
done; \
|
||||
fi
|
||||
|
||||
ecs-status: ## Show current ECS service status and recent logs
|
||||
@echo "$(CYAN)Service Status:$(NC)"
|
||||
@aws ecs describe-services \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--services $(ECS_SERVICE_NAME) \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount,Pending:pendingCount,Events:events[0:3]}' \
|
||||
--output table
|
||||
@echo ""
|
||||
@echo "$(CYAN)Running Tasks:$(NC)"
|
||||
@aws ecs list-tasks \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--service-name $(ECS_SERVICE_NAME) \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'taskArns[]' \
|
||||
--output table
|
||||
@echo ""
|
||||
@echo "$(CYAN)Recent Logs:$(NC)"
|
||||
@LOG_STREAM=$$(aws logs describe-log-streams \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--order-by LastEventTime \
|
||||
--descending \
|
||||
--max-items 1 \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'logStreams[0].logStreamName' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$LOG_STREAM" ] && [ "$$LOG_STREAM" != "None" ]; then \
|
||||
aws logs get-log-events \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--log-stream-name $$LOG_STREAM \
|
||||
--limit 20 \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'events[*].message' \
|
||||
--output text 2>/dev/null; \
|
||||
else \
|
||||
echo "$(YELLOW)No log stream found$(NC)"; \
|
||||
fi
|
||||
|
||||
get-ecs-url: ## Get the public URL/IP of the ECS service
|
||||
@echo "$(YELLOW)Fetching service URL...$(NC)"
|
||||
@echo ""
|
||||
@# Check if service uses load balancer
|
||||
@LB_ARN=$$(aws ecs describe-services \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--services $(ECS_SERVICE_NAME) \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'services[0].loadBalancers[0].targetGroupArn' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$LB_ARN" ] && [ "$$LB_ARN" != "None" ]; then \
|
||||
echo "$(CYAN)Service is using Application Load Balancer$(NC)"; \
|
||||
LB_ARN=$$(aws elbv2 describe-target-groups \
|
||||
--target-group-arns $$LB_ARN \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'TargetGroups[0].LoadBalancerArns[0]' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$LB_ARN" ] && [ "$$LB_ARN" != "None" ]; then \
|
||||
LB_DNS=$$(aws elbv2 describe-load-balancers \
|
||||
--load-balancer-arns $$LB_ARN \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'LoadBalancers[0].DNSName' \
|
||||
--output text 2>/dev/null); \
|
||||
echo ""; \
|
||||
echo "$(GREEN)✓ Load Balancer URL: http://$$LB_DNS$(NC)"; \
|
||||
echo "$(CYAN) Health check: http://$$LB_DNS/health$(NC)"; \
|
||||
fi; \
|
||||
else \
|
||||
echo "$(CYAN)Service is using direct public IP (no load balancer)$(NC)"; \
|
||||
TASK_ARN=$$(aws ecs list-tasks \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--service-name $(ECS_SERVICE_NAME) \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'taskArns[0]' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$TASK_ARN" ] && [ "$$TASK_ARN" != "None" ]; then \
|
||||
ENI_ID=$$(aws ecs describe-tasks \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--tasks $$TASK_ARN \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$ENI_ID" ] && [ "$$ENI_ID" != "None" ]; then \
|
||||
PUBLIC_IP=$$(aws ec2 describe-network-interfaces \
|
||||
--network-interface-ids $$ENI_ID \
|
||||
--region $(AWS_REGION) \
|
||||
--query 'NetworkInterfaces[0].Association.PublicIp' \
|
||||
--output text 2>/dev/null); \
|
||||
if [ -n "$$PUBLIC_IP" ] && [ "$$PUBLIC_IP" != "None" ]; then \
|
||||
echo ""; \
|
||||
echo "$(GREEN)✓ Service URL: http://$$PUBLIC_IP:$(CONTAINER_PORT)$(NC)"; \
|
||||
echo "$(CYAN) Health check: http://$$PUBLIC_IP:$(CONTAINER_PORT)/health$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(YELLOW)Note: Public IP may change if task is restarted. Consider using a load balancer for production.$(NC)"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "$(RED)✗ Public IP not assigned$(NC)"; \
|
||||
echo "$(YELLOW)The task may still be starting or the service is not in a VPC with public subnets.$(NC)"; \
|
||||
fi; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "$(RED)✗ Network interface not found$(NC)"; \
|
||||
fi; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "$(RED)✗ No running tasks found$(NC)"; \
|
||||
echo "$(YELLOW)Check service status with: make ecs-status$(NC)"; \
|
||||
fi; \
|
||||
fi
|
||||
@echo ""
|
||||
|
||||
cleanup-ecs: ## Remove ECS service and task definitions
|
||||
@echo "$(YELLOW)Cleaning up ECS resources...$(NC)"
|
||||
@# Delete service
|
||||
@if aws ecs describe-services --cluster $(ECS_CLUSTER_NAME) --services $(ECS_SERVICE_NAME) --region $(AWS_REGION) 2>/dev/null | grep -q "ACTIVE"; then \
|
||||
echo "$(YELLOW)Deleting service...$(NC)"; \
|
||||
aws ecs update-service \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--service $(ECS_SERVICE_NAME) \
|
||||
--desired-count 0 \
|
||||
--region $(AWS_REGION) > /dev/null; \
|
||||
aws ecs delete-service \
|
||||
--cluster $(ECS_CLUSTER_NAME) \
|
||||
--service $(ECS_SERVICE_NAME) \
|
||||
--region $(AWS_REGION) > /dev/null; \
|
||||
echo "$(GREEN)✓ Service deleted$(NC)"; \
|
||||
else \
|
||||
echo "$(YELLOW)Service does not exist$(NC)"; \
|
||||
fi
|
||||
@# Deregister all task definition revisions
|
||||
@echo "$(YELLOW)Deregistering task definitions...$(NC)"
|
||||
@for arn in $$(aws ecs list-task-definitions --family-prefix $(ECS_TASK_FAMILY) --region $(AWS_REGION) --query 'taskDefinitionArns[]' --output text); do \
|
||||
aws ecs deregister-task-definition --task-definition $$arn --region $(AWS_REGION) > /dev/null; \
|
||||
echo "$(GREEN)✓ Deregistered: $$arn$(NC)"; \
|
||||
done
|
||||
@# Delete CloudWatch log group
|
||||
@echo "$(YELLOW)Deleting CloudWatch log group...$(NC)"
|
||||
@aws logs delete-log-group \
|
||||
--log-group-name /ecs/$(ECS_TASK_FAMILY) \
|
||||
--region $(AWS_REGION) 2>/dev/null || echo "$(YELLOW)Log group does not exist$(NC)"
|
||||
@echo "$(GREEN)✓ Log group deleted$(NC)"
|
||||
@# Clean up temp files
|
||||
@rm -f /tmp/ecs-subnets.tmp /tmp/ecs-execution-role.tmp /tmp/ecs-task-def.json
|
||||
@echo "$(GREEN)✓ Cleanup complete$(NC)"
|
||||
|
||||
96
recipes/fly.mk
Normal file
96
recipes/fly.mk
Normal file
@@ -0,0 +1,96 @@
|
||||
# Fly.io Deployment Recipe
|
||||
# Include this in your main Makefile with: include recipes/fly.mk
|
||||
|
||||
.PHONY: deploy-to-fly-io
|
||||
|
||||
deploy-to-fly-io: ## Deploy to Fly.io (Usage: make deploy-to-fly-io APP_NAME=your-app-name)
|
||||
@echo "$(BLUE)Starting Fly.io deployment...$(NC)"
|
||||
@echo ""
|
||||
@# Check if APP_NAME is provided
|
||||
@if [ -z "$(APP_NAME)" ]; then \
|
||||
echo "$(RED)Error: APP_NAME is required$(NC)"; \
|
||||
echo "$(YELLOW)Usage: make deploy-to-fly-io APP_NAME=your-app-name$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(YELLOW)Checking prerequisites...$(NC)"
|
||||
@# Check if docker is installed
|
||||
@which docker > /dev/null || (echo "$(RED)Error: Docker is not installed. Please install Docker first.$(NC)" && exit 1)
|
||||
@echo "$(GREEN)✓ Docker is installed$(NC)"
|
||||
@# Check if flyctl is installed
|
||||
@which flyctl > /dev/null || (echo "$(RED)Error: flyctl is not installed. Please install flyctl first.$(NC)" && exit 1)
|
||||
@echo "$(GREEN)✓ flyctl is installed$(NC)"
|
||||
@# Check if app exists on Fly.io
|
||||
@flyctl status -a $(APP_NAME) > /dev/null 2>&1 || (echo "$(RED)Error: App '$(APP_NAME)' not found on Fly.io$(NC)" && echo "$(YELLOW)Create the app first with: flyctl launch --name $(APP_NAME)$(NC)" && exit 1)
|
||||
@echo "$(GREEN)✓ App '$(APP_NAME)' exists on Fly.io$(NC)"
|
||||
@echo ""
|
||||
@# Check if fly.toml exists, create temp if needed
|
||||
@if [ -f "fly.toml" ]; then \
|
||||
echo "$(GREEN)✓ Using existing fly.toml$(NC)"; \
|
||||
else \
|
||||
echo "$(YELLOW)fly.toml not found in current directory$(NC)"; \
|
||||
echo "$(CYAN)Would you like to create a temporary fly.toml with 2 vCPU configuration?$(NC)"; \
|
||||
echo "$(CYAN)(It will be removed after deployment)$(NC)"; \
|
||||
printf "Create temporary fly.toml? [y/N]: "; read response; \
|
||||
case "$$response" in \
|
||||
[yY][eE][sS]|[yY]) \
|
||||
echo "$(YELLOW)Creating temporary fly.toml with 2 vCPU configuration...$(NC)"; \
|
||||
echo "app = '$(APP_NAME)'" > fly.toml; \
|
||||
echo "primary_region = 'iad'" >> fly.toml; \
|
||||
echo "" >> fly.toml; \
|
||||
echo "[build]" >> fly.toml; \
|
||||
echo " image = 'registry.fly.io/$(APP_NAME):latest'" >> fly.toml; \
|
||||
echo "" >> fly.toml; \
|
||||
echo "[http_service]" >> fly.toml; \
|
||||
echo " internal_port = 8080" >> fly.toml; \
|
||||
echo " force_https = true" >> fly.toml; \
|
||||
echo " auto_stop_machines = true" >> fly.toml; \
|
||||
echo " auto_start_machines = true" >> fly.toml; \
|
||||
echo " min_machines_running = 0" >> fly.toml; \
|
||||
echo "" >> fly.toml; \
|
||||
echo "[[vm]]" >> fly.toml; \
|
||||
echo " memory = '2gb'" >> fly.toml; \
|
||||
echo " cpu_kind = 'shared'" >> fly.toml; \
|
||||
echo " cpus = 2" >> fly.toml; \
|
||||
echo "$(GREEN)✓ Created temporary fly.toml with 2 vCPU configuration$(NC)"; \
|
||||
touch .fly.toml.tmp.marker; \
|
||||
;; \
|
||||
*) \
|
||||
echo "$(RED)Deployment cancelled. Please create a fly.toml file or run 'flyctl launch' first.$(NC)"; \
|
||||
exit 1; \
|
||||
;; \
|
||||
esac; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Building Docker image...$(NC)"
|
||||
@$(MAKE) build-docker-image
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Tagging image for Fly.io registry...$(NC)"
|
||||
@docker tag bifrost:latest registry.fly.io/$(APP_NAME):latest
|
||||
$(eval GIT_SHA=$(shell git rev-parse --short HEAD))
|
||||
@docker tag bifrost:$(GIT_SHA) registry.fly.io/$(APP_NAME):$(GIT_SHA)
|
||||
@echo "$(GREEN)✓ Tagged: registry.fly.io/$(APP_NAME):latest$(NC)"
|
||||
@echo "$(GREEN)✓ Tagged: registry.fly.io/$(APP_NAME):$(GIT_SHA)$(NC)"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Pushing to Fly.io registry...$(NC)"
|
||||
@echo "$(YELLOW)Authenticating with Fly.io...$(NC)"
|
||||
@flyctl auth docker
|
||||
@echo "$(GREEN)✓ Authenticated with Fly.io$(NC)"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Pushing image to Fly.io registry...$(NC)"
|
||||
@docker push registry.fly.io/$(APP_NAME):latest
|
||||
@docker push registry.fly.io/$(APP_NAME):$(GIT_SHA)
|
||||
@echo "$(GREEN)✓ Image pushed to registry$(NC)"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Deploying to Fly.io...$(NC)"
|
||||
@flyctl deploy -a $(APP_NAME)
|
||||
@echo ""
|
||||
@echo "$(GREEN)✓ Deployment complete!$(NC)"
|
||||
@echo "$(CYAN)App URL: https://$(APP_NAME).fly.dev$(NC)"
|
||||
@echo ""
|
||||
@# Clean up temporary fly.toml if we created it
|
||||
@if [ -f ".fly.toml.tmp.marker" ]; then \
|
||||
echo "$(YELLOW)Cleaning up temporary fly.toml...$(NC)"; \
|
||||
rm -f fly.toml .fly.toml.tmp.marker; \
|
||||
echo "$(GREEN)✓ Temporary fly.toml removed$(NC)"; \
|
||||
fi
|
||||
|
||||
411
recipes/local-k8s.mk
Normal file
411
recipes/local-k8s.mk
Normal file
@@ -0,0 +1,411 @@
|
||||
# Local Kubernetes Deployment Recipe
|
||||
# Include this in your main Makefile with: include recipes/local-k8s.mk
|
||||
|
||||
# Configuration variables
|
||||
K8S_NAMESPACE ?= bifrost
|
||||
K8S_RELEASE_NAME ?= bifrost
|
||||
K8S_HOST ?= bifrost.local
|
||||
BIFROST_IMAGE_TAG ?= v1.3.38
|
||||
|
||||
# Storage configuration (set via prompts or override)
|
||||
# Options: no, sqlite, postgres
|
||||
K8S_CONFIG_STORE ?=
|
||||
K8S_LOG_STORE ?=
|
||||
|
||||
# Spinner characters for loading animation
|
||||
SPINNER = ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
|
||||
|
||||
# Helper function to run command with spinner
|
||||
# Usage: $(call run_with_spinner,command,message)
|
||||
define run_with_spinner
|
||||
@( \
|
||||
$(1) > /tmp/k8s-cmd-output.log 2>&1 & \
|
||||
CMD_PID=$$!; \
|
||||
SPINNER_CHARS="⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏"; \
|
||||
i=0; \
|
||||
while kill -0 $$CMD_PID 2>/dev/null; do \
|
||||
CHAR=$$(echo $$SPINNER_CHARS | cut -d' ' -f$$((i % 10 + 1))); \
|
||||
printf "\r $(CYAN)$$CHAR $(2)$(NC)"; \
|
||||
i=$$((i + 1)); \
|
||||
sleep 0.1; \
|
||||
done; \
|
||||
wait $$CMD_PID; \
|
||||
EXIT_CODE=$$?; \
|
||||
printf "\r"; \
|
||||
if [ $$EXIT_CODE -eq 0 ]; then \
|
||||
echo "$(GREEN)✓ $(2) - done$(NC)"; \
|
||||
else \
|
||||
echo "$(RED)✗ $(2) - failed$(NC)"; \
|
||||
echo "$(YELLOW)Error output:$(NC)"; \
|
||||
cat /tmp/k8s-cmd-output.log; \
|
||||
exit 1; \
|
||||
fi \
|
||||
)
|
||||
endef
|
||||
|
||||
.PHONY: deploy-local-k8s setup-local-k8s-prerequisites setup-local-k8s-ingress setup-local-k8s-tls setup-local-k8s-secrets deploy-local-k8s-helm local-k8s-update-key local-k8s-status local-k8s-logs local-k8s-port-forward cleanup-local-k8s
|
||||
|
||||
deploy-local-k8s: ## Deploy Bifrost to local Kubernetes with storage options + HTTPS (Usage: make deploy-local-k8s [OPENAI_API_KEY='...'])
|
||||
@echo ""
|
||||
@echo "$(BLUE)╔════════════════════════════════════════════════════════════════╗$(NC)"
|
||||
@echo "$(BLUE)║ Bifrost Local K8s - Configurable Storage + Secrets + HTTPS ║$(NC)"
|
||||
@echo "$(BLUE)╚════════════════════════════════════════════════════════════════╝$(NC)"
|
||||
@echo ""
|
||||
@# Prompt for storage options
|
||||
@echo "$(CYAN)Storage Configuration$(NC)"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Config Store:$(NC)"
|
||||
@echo " 1) no (disabled)"
|
||||
@echo " 2) sqlite"
|
||||
@echo " 3) postgres"
|
||||
@printf " Select [2]: "; \
|
||||
read CONFIG_CHOICE; \
|
||||
case "$$CONFIG_CHOICE" in \
|
||||
1) echo "no" > /tmp/bifrost-config-store.tmp ;; \
|
||||
3) echo "postgres" > /tmp/bifrost-config-store.tmp ;; \
|
||||
*) echo "sqlite" > /tmp/bifrost-config-store.tmp ;; \
|
||||
esac
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Log Store:$(NC)"
|
||||
@echo " 1) no (disabled)"
|
||||
@echo " 2) sqlite"
|
||||
@echo " 3) postgres"
|
||||
@printf " Select [2]: "; \
|
||||
read LOG_CHOICE; \
|
||||
case "$$LOG_CHOICE" in \
|
||||
1) echo "no" > /tmp/bifrost-log-store.tmp ;; \
|
||||
3) echo "postgres" > /tmp/bifrost-log-store.tmp ;; \
|
||||
*) echo "sqlite" > /tmp/bifrost-log-store.tmp ;; \
|
||||
esac
|
||||
@echo ""
|
||||
@echo "$(CYAN)Step 1/5: Checking prerequisites$(NC)"
|
||||
@$(MAKE) setup-local-k8s-prerequisites
|
||||
@echo "$(CYAN)Step 2/5: Setting up ingress controller$(NC)"
|
||||
@$(MAKE) setup-local-k8s-ingress
|
||||
@echo "$(CYAN)Step 3/5: Generating TLS certificate$(NC)"
|
||||
@$(MAKE) setup-local-k8s-tls
|
||||
@echo "$(CYAN)Step 4/5: Creating secrets$(NC)"
|
||||
@$(MAKE) setup-local-k8s-secrets
|
||||
@echo "$(CYAN)Step 5/5: Deploying Bifrost$(NC)"
|
||||
@CONFIG_STORE=$$(cat /tmp/bifrost-config-store.tmp 2>/dev/null || echo "sqlite"); \
|
||||
LOG_STORE=$$(cat /tmp/bifrost-log-store.tmp 2>/dev/null || echo "sqlite"); \
|
||||
$(MAKE) deploy-local-k8s-helm K8S_CONFIG_STORE="$$CONFIG_STORE" K8S_LOG_STORE="$$LOG_STORE" K8S_SKIP_PROMPTS=true
|
||||
@rm -f /tmp/bifrost-config-store.tmp /tmp/bifrost-log-store.tmp
|
||||
@echo ""
|
||||
@echo "$(GREEN)╔════════════════════════════════════════════════════════════════╗$(NC)"
|
||||
@echo "$(GREEN)║ DEPLOYMENT COMPLETE! ║$(NC)"
|
||||
@echo "$(GREEN)╚════════════════════════════════════════════════════════════════╝$(NC)"
|
||||
@echo ""
|
||||
@INGRESS_IP=$$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "127.0.0.1"); \
|
||||
echo "$(CYAN)Next Steps:$(NC)"; \
|
||||
echo " 1. Add to /etc/hosts: $(YELLOW)echo '$$INGRESS_IP $(K8S_HOST)' | sudo tee -a /etc/hosts$(NC)"; \
|
||||
echo " 2. Access: $(YELLOW)https://$(K8S_HOST)$(NC)"; \
|
||||
echo " 3. Or port-forward: $(YELLOW)make local-k8s-port-forward$(NC)"; \
|
||||
echo ""
|
||||
|
||||
setup-local-k8s-prerequisites: ## Check local K8s prerequisites
|
||||
@which kubectl > /dev/null || (echo "$(RED)✗ kubectl not installed$(NC)" && exit 1)
|
||||
@echo " $(GREEN)✓ kubectl$(NC)"
|
||||
@which helm > /dev/null || (echo "$(RED)✗ helm not installed$(NC)" && exit 1)
|
||||
@echo " $(GREEN)✓ helm$(NC)"
|
||||
@which openssl > /dev/null || (echo "$(RED)✗ openssl not installed$(NC)" && exit 1)
|
||||
@echo " $(GREEN)✓ openssl$(NC)"
|
||||
@kubectl cluster-info > /dev/null 2>&1 || (echo "$(RED)✗ Cannot connect to K8s cluster$(NC)" && exit 1)
|
||||
@echo " $(GREEN)✓ Kubernetes cluster$(NC)"
|
||||
@kubectl create namespace $(K8S_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1
|
||||
@echo " $(GREEN)✓ Namespace '$(K8S_NAMESPACE)'$(NC)"
|
||||
@echo ""
|
||||
|
||||
setup-local-k8s-ingress: ## Install nginx-ingress controller
|
||||
@if kubectl get pods -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx 2>/dev/null | grep -q Running; then \
|
||||
echo " $(GREEN)✓ nginx-ingress already running$(NC)"; \
|
||||
else \
|
||||
echo " $(YELLOW)Installing nginx-ingress (this may take 2-3 minutes)...$(NC)"; \
|
||||
( \
|
||||
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx >/dev/null 2>&1 || true; \
|
||||
helm repo update >/dev/null 2>&1; \
|
||||
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
|
||||
--namespace ingress-nginx \
|
||||
--create-namespace \
|
||||
--set controller.service.type=LoadBalancer \
|
||||
--wait --timeout 300s >/dev/null 2>&1 & \
|
||||
CMD_PID=$$!; \
|
||||
ELAPSED=0; \
|
||||
while kill -0 $$CMD_PID 2>/dev/null; do \
|
||||
PODS_READY=$$(kubectl get pods -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx -o jsonpath='{.items[*].status.phase}' 2>/dev/null | grep -c Running || echo 0); \
|
||||
SPINNER_CHARS="⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏"; \
|
||||
CHAR=$$(echo $$SPINNER_CHARS | cut -d' ' -f$$((ELAPSED % 10 + 1))); \
|
||||
printf "\r $(CYAN)$$CHAR Installing nginx-ingress... [%ds] pods ready: %s$(NC) " "$$ELAPSED" "$$PODS_READY"; \
|
||||
ELAPSED=$$((ELAPSED + 1)); \
|
||||
sleep 1; \
|
||||
done; \
|
||||
wait $$CMD_PID; \
|
||||
EXIT_CODE=$$?; \
|
||||
printf "\r%-70s\r" " "; \
|
||||
if [ $$EXIT_CODE -eq 0 ]; then \
|
||||
echo " $(GREEN)✓ nginx-ingress installed [$$ELAPSED seconds]$(NC)"; \
|
||||
else \
|
||||
echo " $(RED)✗ nginx-ingress installation failed$(NC)"; \
|
||||
echo " $(YELLOW)Run 'kubectl get pods -n ingress-nginx' to debug$(NC)"; \
|
||||
exit 1; \
|
||||
fi \
|
||||
); \
|
||||
fi
|
||||
@echo ""
|
||||
|
||||
setup-local-k8s-tls: ## Generate self-signed TLS certificate
|
||||
@mkdir -p /tmp/bifrost-certs
|
||||
@openssl genrsa -out /tmp/bifrost-certs/tls.key 2048 2>/dev/null
|
||||
@openssl req -new -x509 -key /tmp/bifrost-certs/tls.key -out /tmp/bifrost-certs/tls.crt \
|
||||
-days 365 -subj "/CN=$(K8S_HOST)" \
|
||||
-addext "subjectAltName=DNS:$(K8S_HOST),DNS:localhost,IP:127.0.0.1" 2>/dev/null
|
||||
@kubectl create secret tls bifrost-tls-secret \
|
||||
--cert=/tmp/bifrost-certs/tls.crt --key=/tmp/bifrost-certs/tls.key \
|
||||
-n $(K8S_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1
|
||||
@rm -rf /tmp/bifrost-certs
|
||||
@echo " $(GREEN)✓ TLS certificate for $(K8S_HOST)$(NC)"
|
||||
@echo ""
|
||||
|
||||
setup-local-k8s-secrets: ## Create Kubernetes secrets for Bifrost
|
||||
@# Ensure namespace exists first
|
||||
@kubectl create namespace $(K8S_NAMESPACE) --dry-run=client -o yaml 2>/dev/null | kubectl apply -f - >/dev/null 2>&1 || true
|
||||
$(eval BIFROST_ENCRYPTION_KEY ?= $(shell openssl rand -base64 32))
|
||||
$(eval OPENAI_KEY := $(if $(OPENAI_API_KEY),$(OPENAI_API_KEY),sk-placeholder-replace-me))
|
||||
@kubectl create secret generic bifrost-provider-keys \
|
||||
--from-literal=openai-api-key='$(OPENAI_KEY)' \
|
||||
-n $(K8S_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
|
||||
@echo " $(GREEN)✓ bifrost-provider-keys$(NC)"
|
||||
@kubectl create secret generic bifrost-encryption-key \
|
||||
--from-literal=encryption-key='$(BIFROST_ENCRYPTION_KEY)' \
|
||||
-n $(K8S_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
|
||||
@echo " $(GREEN)✓ bifrost-encryption-key$(NC)"
|
||||
@if [ "$(OPENAI_KEY)" = "sk-placeholder-replace-me" ]; then \
|
||||
echo " $(YELLOW)⚠ Using placeholder OpenAI key$(NC)"; \
|
||||
echo " Update later: make local-k8s-update-key OPENAI_API_KEY='sk-...'"; \
|
||||
fi
|
||||
@echo ""
|
||||
|
||||
deploy-local-k8s-helm: ## Deploy Bifrost using Helm (Usage: make deploy-local-k8s-helm [K8S_CONFIG_STORE=sqlite] [K8S_LOG_STORE=postgres] [BIFROST_IMAGE_TAG=v1.3.38])
|
||||
@# Show prompts if not called from deploy-local-k8s (K8S_SKIP_PROMPTS not set)
|
||||
@if [ -z "$(K8S_SKIP_PROMPTS)" ]; then \
|
||||
echo ""; \
|
||||
echo "$(BLUE)╔════════════════════════════════════════════════════════════════╗$(NC)"; \
|
||||
echo "$(BLUE)║ Bifrost Helm Deployment ║$(NC)"; \
|
||||
echo "$(BLUE)╚════════════════════════════════════════════════════════════════╝$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(CYAN)Deployment Configuration$(NC)"; \
|
||||
echo ""; \
|
||||
echo "$(YELLOW)Image Tag$(NC) [$(BIFROST_IMAGE_TAG)]:"; \
|
||||
printf " Enter tag: "; \
|
||||
read IMAGE_TAG_INPUT; \
|
||||
if [ -n "$$IMAGE_TAG_INPUT" ]; then \
|
||||
echo "$$IMAGE_TAG_INPUT" > /tmp/bifrost-image-tag.tmp; \
|
||||
else \
|
||||
echo "$(BIFROST_IMAGE_TAG)" > /tmp/bifrost-image-tag.tmp; \
|
||||
fi; \
|
||||
echo ""; \
|
||||
echo "$(YELLOW)Helm Chart Version$(NC) (leave empty for local chart):"; \
|
||||
printf " Enter version: "; \
|
||||
read HELM_VERSION_INPUT; \
|
||||
echo "$$HELM_VERSION_INPUT" > /tmp/bifrost-helm-version.tmp; \
|
||||
echo ""; \
|
||||
echo "$(YELLOW)Config Store:$(NC)"; \
|
||||
echo " 1) no (disabled)"; \
|
||||
echo " 2) sqlite"; \
|
||||
echo " 3) postgres"; \
|
||||
printf " Select [2]: "; \
|
||||
read CONFIG_CHOICE; \
|
||||
case "$$CONFIG_CHOICE" in \
|
||||
1) echo "no" > /tmp/bifrost-config-store.tmp ;; \
|
||||
3) echo "postgres" > /tmp/bifrost-config-store.tmp ;; \
|
||||
*) echo "sqlite" > /tmp/bifrost-config-store.tmp ;; \
|
||||
esac; \
|
||||
echo ""; \
|
||||
echo "$(YELLOW)Log Store:$(NC)"; \
|
||||
echo " 1) no (disabled)"; \
|
||||
echo " 2) sqlite"; \
|
||||
echo " 3) postgres"; \
|
||||
printf " Select [2]: "; \
|
||||
read LOG_CHOICE; \
|
||||
case "$$LOG_CHOICE" in \
|
||||
1) echo "no" > /tmp/bifrost-log-store.tmp ;; \
|
||||
3) echo "postgres" > /tmp/bifrost-log-store.tmp ;; \
|
||||
*) echo "sqlite" > /tmp/bifrost-log-store.tmp ;; \
|
||||
esac; \
|
||||
echo ""; \
|
||||
fi
|
||||
@# Read configuration from temp files or use provided values
|
||||
@CONFIG_STORE="$(K8S_CONFIG_STORE)"; \
|
||||
if [ -z "$$CONFIG_STORE" ] && [ -f /tmp/bifrost-config-store.tmp ]; then \
|
||||
CONFIG_STORE=$$(cat /tmp/bifrost-config-store.tmp); \
|
||||
fi; \
|
||||
if [ -z "$$CONFIG_STORE" ]; then \
|
||||
CONFIG_STORE="sqlite"; \
|
||||
fi; \
|
||||
LOG_STORE="$(K8S_LOG_STORE)"; \
|
||||
if [ -z "$$LOG_STORE" ] && [ -f /tmp/bifrost-log-store.tmp ]; then \
|
||||
LOG_STORE=$$(cat /tmp/bifrost-log-store.tmp); \
|
||||
fi; \
|
||||
if [ -z "$$LOG_STORE" ]; then \
|
||||
LOG_STORE="sqlite"; \
|
||||
fi; \
|
||||
IMAGE_TAG="$(BIFROST_IMAGE_TAG)"; \
|
||||
if [ -f /tmp/bifrost-image-tag.tmp ]; then \
|
||||
IMAGE_TAG=$$(cat /tmp/bifrost-image-tag.tmp); \
|
||||
fi; \
|
||||
HELM_VERSION=""; \
|
||||
if [ -f /tmp/bifrost-helm-version.tmp ]; then \
|
||||
HELM_VERSION=$$(cat /tmp/bifrost-helm-version.tmp); \
|
||||
fi; \
|
||||
echo " $(CYAN)Configuration:$(NC)"; \
|
||||
echo " Image Tag: $$IMAGE_TAG"; \
|
||||
if [ -n "$$HELM_VERSION" ]; then \
|
||||
echo " Chart: bifrost/bifrost v$$HELM_VERSION (remote)"; \
|
||||
else \
|
||||
echo " Chart: ./helm-charts/bifrost (local)"; \
|
||||
fi; \
|
||||
echo " Config Store: $$CONFIG_STORE"; \
|
||||
echo " Log Store: $$LOG_STORE"; \
|
||||
echo ""; \
|
||||
echo " $(YELLOW)Deploying Bifrost $$IMAGE_TAG (this may take 1-2 minutes)...$(NC)"; \
|
||||
NEEDS_POSTGRES="false"; \
|
||||
if [ "$$CONFIG_STORE" = "postgres" ] || [ "$$LOG_STORE" = "postgres" ]; then \
|
||||
NEEDS_POSTGRES="true"; \
|
||||
fi; \
|
||||
POSTGRES_PASSWORD=""; \
|
||||
if [ "$$NEEDS_POSTGRES" = "true" ]; then \
|
||||
EXISTING_PG_PWD=$$(kubectl get secret $(K8S_RELEASE_NAME)-postgresql -n $(K8S_NAMESPACE) -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true); \
|
||||
if [ -n "$$EXISTING_PG_PWD" ]; then \
|
||||
POSTGRES_PASSWORD="$$EXISTING_PG_PWD"; \
|
||||
echo " $(GREEN)✓ Using existing PostgreSQL password from helm secret$(NC)"; \
|
||||
else \
|
||||
EXISTING_PG_PWD=$$(kubectl get secret bifrost-postgresql-password -n $(K8S_NAMESPACE) -o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null || true); \
|
||||
if [ -n "$$EXISTING_PG_PWD" ]; then \
|
||||
POSTGRES_PASSWORD="$$EXISTING_PG_PWD"; \
|
||||
echo " $(GREEN)✓ Using existing PostgreSQL password$(NC)"; \
|
||||
else \
|
||||
POSTGRES_PASSWORD=$$(openssl rand -base64 24 | tr -d '/+=' | head -c 24); \
|
||||
kubectl create secret generic bifrost-postgresql-password \
|
||||
--from-literal=password="$$POSTGRES_PASSWORD" \
|
||||
-n $(K8S_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1; \
|
||||
echo " $(GREEN)✓ Created new PostgreSQL password secret$(NC)"; \
|
||||
fi; \
|
||||
fi; \
|
||||
fi; \
|
||||
HELM_EXTRA_ARGS=""; \
|
||||
if [ "$$CONFIG_STORE" = "no" ]; then \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set storage.configStore.enabled=false"; \
|
||||
elif [ "$$CONFIG_STORE" = "sqlite" ]; then \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set storage.configStore.enabled=true"; \
|
||||
elif [ "$$CONFIG_STORE" = "postgres" ]; then \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set storage.configStore.enabled=true"; \
|
||||
fi; \
|
||||
if [ "$$LOG_STORE" = "no" ]; then \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set storage.logsStore.enabled=false"; \
|
||||
elif [ "$$LOG_STORE" = "sqlite" ]; then \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set storage.logsStore.enabled=true"; \
|
||||
elif [ "$$LOG_STORE" = "postgres" ]; then \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set storage.logsStore.enabled=true"; \
|
||||
fi; \
|
||||
if [ "$$NEEDS_POSTGRES" = "true" ]; then \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set storage.mode=postgres --set postgresql.enabled=true --set postgresql.auth.password=$$POSTGRES_PASSWORD"; \
|
||||
elif [ "$$CONFIG_STORE" = "no" ] && [ "$$LOG_STORE" = "no" ]; then \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set postgresql.enabled=false"; \
|
||||
else \
|
||||
HELM_EXTRA_ARGS="$$HELM_EXTRA_ARGS --set storage.mode=sqlite --set postgresql.enabled=false"; \
|
||||
fi; \
|
||||
CHART_REF="./helm-charts/bifrost"; \
|
||||
if [ -n "$$HELM_VERSION" ]; then \
|
||||
echo " $(CYAN)Setting up Helm repository...$(NC)"; \
|
||||
helm repo add bifrost https://maximhq.github.io/bifrost/helm-charts >/dev/null 2>&1 || true; \
|
||||
helm repo update >/dev/null 2>&1; \
|
||||
CHART_REF="bifrost/bifrost --version $$HELM_VERSION"; \
|
||||
echo " $(GREEN)✓ Using remote chart v$$HELM_VERSION$(NC)"; \
|
||||
fi; \
|
||||
( \
|
||||
helm upgrade --install $(K8S_RELEASE_NAME) $$CHART_REF \
|
||||
-n $(K8S_NAMESPACE) \
|
||||
--create-namespace \
|
||||
-f ./recipes/values-local-k8s.yaml \
|
||||
--set image.tag=$$IMAGE_TAG \
|
||||
--set 'ingress.hosts[0].host=$(K8S_HOST)' \
|
||||
--set 'ingress.tls[0].hosts[0]=$(K8S_HOST)' \
|
||||
$$HELM_EXTRA_ARGS \
|
||||
--wait --timeout 300s > /tmp/bifrost-helm-output.log 2>&1 & \
|
||||
CMD_PID=$$!; \
|
||||
ELAPSED=0; \
|
||||
while kill -0 $$CMD_PID 2>/dev/null; do \
|
||||
BIFROST_STATUS=$$(kubectl get pods -n $(K8S_NAMESPACE) -l app.kubernetes.io/name=bifrost -o jsonpath='{.items[0].status.phase}' 2>/dev/null || echo "Pending"); \
|
||||
if [ "$$NEEDS_POSTGRES" = "true" ]; then \
|
||||
PG_STATUS=$$(kubectl get pods -n $(K8S_NAMESPACE) -l app.kubernetes.io/component=database -o jsonpath='{.items[0].status.phase}' 2>/dev/null || echo "Pending"); \
|
||||
STATUS_MSG="postgres: $$PG_STATUS | bifrost: $$BIFROST_STATUS"; \
|
||||
else \
|
||||
STATUS_MSG="bifrost: $$BIFROST_STATUS"; \
|
||||
fi; \
|
||||
SPINNER_CHARS="⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏"; \
|
||||
CHAR=$$(echo $$SPINNER_CHARS | cut -d' ' -f$$((ELAPSED % 10 + 1))); \
|
||||
printf "\r $(CYAN)$$CHAR Deploying... [%ds] %s$(NC) " "$$ELAPSED" "$$STATUS_MSG"; \
|
||||
ELAPSED=$$((ELAPSED + 1)); \
|
||||
sleep 1; \
|
||||
done; \
|
||||
wait $$CMD_PID; \
|
||||
EXIT_CODE=$$?; \
|
||||
printf "\r%-80s\r" " "; \
|
||||
if [ $$EXIT_CODE -eq 0 ]; then \
|
||||
echo " $(GREEN)✓ Bifrost deployed [$$ELAPSED seconds]$(NC)"; \
|
||||
else \
|
||||
echo " $(RED)✗ Deployment failed$(NC)"; \
|
||||
echo ""; \
|
||||
echo " $(YELLOW)Helm output:$(NC)"; \
|
||||
cat /tmp/bifrost-helm-output.log | sed 's/^/ /'; \
|
||||
echo ""; \
|
||||
echo " $(YELLOW)Debug commands:$(NC)"; \
|
||||
echo " kubectl get pods -n $(K8S_NAMESPACE)"; \
|
||||
echo " kubectl logs -l app.kubernetes.io/name=bifrost -n $(K8S_NAMESPACE)"; \
|
||||
rm -f /tmp/bifrost-helm-output.log; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
rm -f /tmp/bifrost-helm-output.log \
|
||||
); \
|
||||
rm -f /tmp/bifrost-config-store.tmp /tmp/bifrost-log-store.tmp /tmp/bifrost-image-tag.tmp /tmp/bifrost-helm-version.tmp
|
||||
|
||||
local-k8s-update-key: ## Update OpenAI API key (Usage: make local-k8s-update-key OPENAI_API_KEY='sk-...')
|
||||
@if [ -z "$(OPENAI_API_KEY)" ]; then \
|
||||
echo "$(RED)Error: OPENAI_API_KEY is required$(NC)"; \
|
||||
echo "$(YELLOW)Usage: make local-k8s-update-key OPENAI_API_KEY='sk-your-key'$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@kubectl create secret generic bifrost-provider-keys \
|
||||
--from-literal=openai-api-key='$(OPENAI_API_KEY)' \
|
||||
-n $(K8S_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1
|
||||
@echo "$(GREEN)✓ Updated bifrost-provider-keys$(NC)"
|
||||
@echo "$(YELLOW)Restarting Bifrost to pick up new key...$(NC)"
|
||||
@kubectl rollout restart deployment/$(K8S_RELEASE_NAME) -n $(K8S_NAMESPACE) >/dev/null 2>&1
|
||||
@echo "$(GREEN)✓ Bifrost restarted$(NC)"
|
||||
|
||||
local-k8s-status: ## Check local K8s deployment status
|
||||
@echo "$(BLUE)Bifrost Local K8s Status$(NC)"
|
||||
@echo ""
|
||||
@echo "$(CYAN)Pods:$(NC)"
|
||||
@kubectl get pods -n $(K8S_NAMESPACE) -o wide
|
||||
@echo ""
|
||||
@echo "$(CYAN)Services:$(NC)"
|
||||
@kubectl get svc -n $(K8S_NAMESPACE)
|
||||
@echo ""
|
||||
@echo "$(CYAN)Ingress:$(NC)"
|
||||
@kubectl get ingress -n $(K8S_NAMESPACE)
|
||||
@echo ""
|
||||
|
||||
local-k8s-logs: ## View Bifrost logs
|
||||
@kubectl logs -l app.kubernetes.io/name=bifrost -n $(K8S_NAMESPACE) -f
|
||||
|
||||
local-k8s-port-forward: ## Port-forward Bifrost service to localhost:8080
|
||||
@echo "$(CYAN)Port-forwarding to http://localhost:8080$(NC)"
|
||||
@echo "$(YELLOW)Press Ctrl+C to stop$(NC)"
|
||||
@kubectl port-forward svc/$(K8S_RELEASE_NAME) 8080:8080 -n $(K8S_NAMESPACE)
|
||||
|
||||
cleanup-local-k8s: ## Remove Bifrost from local K8s
|
||||
@echo "$(YELLOW)Cleaning up Bifrost deployment...$(NC)"
|
||||
@helm uninstall $(K8S_RELEASE_NAME) -n $(K8S_NAMESPACE) 2>/dev/null || true
|
||||
@kubectl delete namespace $(K8S_NAMESPACE) 2>/dev/null || true
|
||||
@echo "$(GREEN)✓ Cleanup complete$(NC)"
|
||||
158
recipes/values-local-k8s.yaml
Normal file
158
recipes/values-local-k8s.yaml
Normal file
@@ -0,0 +1,158 @@
|
||||
# Bifrost Local Kubernetes Values
|
||||
# Configurable Storage + Secrets + HTTPS configuration
|
||||
#
|
||||
# Usage:
|
||||
# make deploy-local-k8s # Interactive prompts for storage options
|
||||
# make deploy-local-k8s-helm # Direct helm deployment with prompts
|
||||
#
|
||||
# Storage options are prompted at runtime:
|
||||
# - Config Store: no / sqlite / postgres
|
||||
# - Log Store: no / sqlite / postgres
|
||||
|
||||
# Image configuration
|
||||
image:
|
||||
repository: docker.io/maximhq/bifrost
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "v1.3.38" # Override with --set image.tag=vX.X.X
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
# Service
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
|
||||
# Ingress with TLS
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
|
||||
hosts:
|
||||
- host: bifrost.local # Override with --set ingress.hosts[0].host=your.host
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: bifrost-tls-secret
|
||||
hosts:
|
||||
- bifrost.local # Override with --set ingress.tls[0].hosts[0]=your.host
|
||||
|
||||
# Storage configuration (defaults to sqlite, can be overridden to postgres via --set flags)
|
||||
storage:
|
||||
mode: sqlite # Override with --set storage.mode=postgres
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
configStore:
|
||||
enabled: true
|
||||
logsStore:
|
||||
enabled: true
|
||||
|
||||
# PostgreSQL (deployed when storage.mode=postgres and postgresql.enabled=true)
|
||||
postgresql:
|
||||
enabled: false # Override with --set postgresql.enabled=true
|
||||
external:
|
||||
enabled: false
|
||||
auth:
|
||||
username: bifrost
|
||||
database: bifrost
|
||||
# password: set via --set postgresql.auth.password=xxx (persisted in bifrost-postgresql-password secret)
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
# No vector store for basic setup
|
||||
vectorStore:
|
||||
enabled: false
|
||||
type: none
|
||||
|
||||
# Bifrost configuration
|
||||
bifrost:
|
||||
appDir: /app/data
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
logLevel: info
|
||||
logStyle: json
|
||||
|
||||
# Encryption key - optional, uncomment if you have the secret
|
||||
# encryptionKeySecret:
|
||||
# name: "bifrost-encryption-key"
|
||||
# key: "encryption-key"
|
||||
|
||||
client:
|
||||
dropExcessRequests: false
|
||||
initialPoolSize: 100
|
||||
allowedOrigins:
|
||||
- "*"
|
||||
enableLogging: true
|
||||
enforceGovernanceHeader: false
|
||||
allowDirectKeys: true
|
||||
maxRequestBodySizeMb: 100
|
||||
|
||||
# Provider configuration - empty by default, configure via UI or uncomment below
|
||||
# To use secrets, first create them:
|
||||
# kubectl create secret generic bifrost-provider-keys --from-literal=openai-api-key='sk-...' -n bifrost
|
||||
# Then uncomment:
|
||||
# providers:
|
||||
# openai:
|
||||
# keys:
|
||||
# - value: "env.OPENAI_API_KEY"
|
||||
# weight: 1
|
||||
# providerSecrets:
|
||||
# openai:
|
||||
# existingSecret: "bifrost-provider-keys"
|
||||
# key: "openai-api-key"
|
||||
# envVar: "OPENAI_API_KEY"
|
||||
|
||||
providers: {}
|
||||
providerSecrets: {}
|
||||
|
||||
plugins:
|
||||
telemetry:
|
||||
enabled: false
|
||||
logging:
|
||||
enabled: true
|
||||
governance:
|
||||
enabled: false
|
||||
|
||||
# Resource limits for local environment
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
|
||||
# Probes
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
|
||||
Reference in New Issue
Block a user