Quickstart: AWS (EC2)
For: Platform engineers deploying Axemere Gateway on AWS. Detail page for Cloud Deployment — see also GCP and Azure.
Axemere Gateway sits between your applications and AI providers (OpenAI, Anthropic, etc.), enforcing policies, tracking attribution, and recording every request. This guide deploys the gateway as a binary on an AWS EC2 instance with RDS PostgreSQL.
Table of Contents
Prerequisites
- AWS CLI v2 configured with credentials that can create VPCs, EC2 instances, RDS instances, IAM roles, and Secrets Manager secrets
- An AWS account with billing enabled in your chosen region
curlandjqfor testing (installed on the EC2 instance in Step 5)- An OpenAI or Anthropic API key to store as a credential. You can skip this and test with a policy denial instead.
Architecture
The gateway runs as a systemd service on the EC2 instance. RDS is in a private subnet and accessible only from within the VPC.
Steps
Step 1: Create the VPC and Security Groups
REGION="us-east-1" # Create VPC VPC_ID=$(aws ec2 create-vpc \ --cidr-block 10.0.0.0/16 \ --region $REGION \ --query 'Vpc.VpcId' --output text) aws ec2 create-tags --resources $VPC_ID \ --tags Key=Name,Value=axemere-vpc --region $REGION # Create subnets (one public for EC2, two private for RDS multi-AZ) SUBNET_PUBLIC=$(aws ec2 create-subnet \ --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 \ --availability-zone ${REGION}a \ --query 'Subnet.SubnetId' --output text --region $REGION) SUBNET_PRIVATE_A=$(aws ec2 create-subnet \ --vpc-id $VPC_ID --cidr-block 10.0.2.0/24 \ --availability-zone ${REGION}a \ --query 'Subnet.SubnetId' --output text --region $REGION) SUBNET_PRIVATE_B=$(aws ec2 create-subnet \ --vpc-id $VPC_ID --cidr-block 10.0.3.0/24 \ --availability-zone ${REGION}b \ --query 'Subnet.SubnetId' --output text --region $REGION) # Create and attach internet gateway (for outbound from EC2 to AI providers) IGW=$(aws ec2 create-internet-gateway \ --query 'InternetGateway.InternetGatewayId' --output text --region $REGION) aws ec2 attach-internet-gateway --internet-gateway-id $IGW \ --vpc-id $VPC_ID --region $REGION # Route table for public subnet RTB=$(aws ec2 create-route-table --vpc-id $VPC_ID \ --query 'RouteTable.RouteTableId' --output text --region $REGION) aws ec2 create-route --route-table-id $RTB \ --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW --region $REGION aws ec2 associate-route-table --route-table-id $RTB \ --subnet-id $SUBNET_PUBLIC --region $REGION # Security group for EC2 (allow 7080 from within VPC; 22 for SSH if needed) SG_EC2=$(aws ec2 create-security-group \ --group-name axemere-gateway-sg \ --description "Axemere gateway" \ --vpc-id $VPC_ID \ --query 'GroupId' --output text --region $REGION) aws ec2 authorize-security-group-ingress \ --group-id $SG_EC2 --protocol tcp --port 7080 \ --cidr 10.0.0.0/16 --region $REGION # Security group for RDS (allow 5432 from EC2 SG only) SG_RDS=$(aws ec2 create-security-group \ --group-name axemere-rds-sg \ --description "Axemere RDS" \ --vpc-id $VPC_ID \ --query 'GroupId' --output text --region $REGION) aws ec2 authorize-security-group-ingress \ --group-id $SG_RDS --protocol tcp --port 5432 \ --source-group $SG_EC2 --region $REGION
Step 2: Provision the RDS Instance
DB_PASSWORD=$(openssl rand -hex 24) echo "RDS password: $DB_PASSWORD" # save this # Subnet group for RDS (requires two AZs) aws rds create-db-subnet-group \ --db-subnet-group-name axemere-db-subnet \ --db-subnet-group-description "Axemere RDS subnets" \ --subnet-ids $SUBNET_PRIVATE_A $SUBNET_PRIVATE_B \ --region $REGION # Create the RDS instance aws rds create-db-instance \ --db-instance-identifier axemere-db \ --db-instance-class db.t3.micro \ --engine postgres \ --engine-version 15.7 \ --master-username postgres \ --master-user-password "$DB_PASSWORD" \ --db-name mvgc_gateway \ --db-subnet-group-name axemere-db-subnet \ --vpc-security-group-ids $SG_RDS \ --no-publicly-accessible \ --storage-type gp3 \ --allocated-storage 20 \ --region $REGION # Wait for the instance to become available (~5 minutes) aws rds wait db-instance-available \ --db-instance-identifier axemere-db --region $REGION # Get the endpoint DB_HOST=$(aws rds describe-db-instances \ --db-instance-identifier axemere-db --region $REGION \ --query 'DBInstances[0].Endpoint.Address' --output text) echo "RDS endpoint: $DB_HOST"
Step 3: Launch the EC2 Instance
Create an IAM role so the instance can read secrets:
# Trust policy cat > trust-policy.json << 'EOF' {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]} EOF aws iam create-role \ --role-name axemere-gateway-role \ --assume-role-policy-document file://trust-policy.json aws iam attach-role-policy \ --role-name axemere-gateway-role \ --policy-arn arn:aws:iam::aws:policy/SecretsManagerReadWrite aws iam create-instance-profile \ --instance-profile-name axemere-gateway-profile aws iam add-role-to-instance-profile \ --instance-profile-name axemere-gateway-profile \ --role-name axemere-gateway-role
Launch the instance (Amazon Linux 2023):
# Find the latest Amazon Linux 2023 AMI AMI=$(aws ec2 describe-images \ --owners amazon \ --filters Name=name,Values='al2023-ami-2023*-x86_64' \ Name=state,Values=available \ --query 'sort_by(Images, &CreationDate)[-1].ImageId' \ --output text --region $REGION) INSTANCE_ID=$(aws ec2 run-instances \ --image-id $AMI \ --instance-type t3.medium \ --subnet-id $SUBNET_PUBLIC \ --security-group-ids $SG_EC2 \ --iam-instance-profile Name=axemere-gateway-profile \ --associate-public-ip-address \ --region $REGION \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=axemere-gateway}]' \ --query 'Instances[0].InstanceId' --output text) aws ec2 wait instance-running --instance-ids $INSTANCE_ID --region $REGION INSTANCE_IP=$(aws ec2 describe-instances \ --instance-ids $INSTANCE_ID --region $REGION \ --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) echo "Instance IP: $INSTANCE_IP"
For production, use AWS Systems Manager Session Manager instead of a public IP for SSH. Add
AmazonSSMManagedInstanceCoreto the IAM role and remove the public IP.
Step 4: Store Secrets in AWS Secrets Manager
ADMIN_TOKEN=$(openssl rand -hex 32) aws secretsmanager create-secret \ --name axemere/admin-token \ --secret-string "$ADMIN_TOKEN" \ --region $REGION aws secretsmanager create-secret \ --name axemere/db-url \ --secret-string "postgres://postgres:${DB_PASSWORD}@${DB_HOST}:5432/mvgc_gateway?sslmode=require" \ --region $REGION # Optional: store your AI provider key aws secretsmanager create-secret \ --name axemere/openai-key \ --secret-string "sk-..." \ --region $REGION
Step 5: Install and Configure the Gateway
SSH into the instance:
ssh ec2-user@$INSTANCE_IP # or use SSM Session Manager
On the instance:
# Install the gateway (Amazon Linux 2023 / RHEL) sudo rpm --import https://raw.githubusercontent.com/Axemere-LLC/mvgc-rpm/main/gpg.key sudo tee /etc/yum.repos.d/mvgc.repo << 'EOF' [mvgc] name=Axemere Gateway baseurl=https://raw.githubusercontent.com/Axemere-LLC/mvgc-rpm/main/stable enabled=1 gpgcheck=1 gpgkey=https://raw.githubusercontent.com/Axemere-LLC/mvgc-rpm/main/gpg.key EOF sudo dnf install -y mvgc-gateway jq mvgc-gateway --version
Create a startup script to pull secrets from Secrets Manager:
sudo mkdir -p /etc/mvgc sudo tee /etc/mvgc/env-from-secrets.sh << 'SCRIPT' #!/bin/bash REGION="us-east-1" export_secret() { local name="$1" var="$2" local val val=$(aws secretsmanager get-secret-value \ --secret-id "$name" --region "$REGION" \ --query SecretString --output text 2>/dev/null) [ -n "$val" ] && export "$var"="$val" } export_secret axemere/db-url DATABASE_URL export_secret axemere/admin-token MVGC_ADMIN_TOKEN export_secret axemere/openai-key OPENAI_API_KEY SCRIPT sudo chmod 700 /etc/mvgc/env-from-secrets.sh
Create the systemd unit:
sudo tee /etc/systemd/system/mvgc-gateway.service << 'EOF' [Unit] Description=Axemere Gateway After=network-online.target Wants=network-online.target [Service] Type=simple ExecStartPre=/bin/bash /etc/mvgc/env-from-secrets.sh ExecStart=/usr/bin/mvgc-gateway Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now mvgc-gateway sudo systemctl status mvgc-gateway
Step 6: Verify the Deployment
curl -s http://localhost:7080/healthz | jq .
Expected:
{"status":"ok", "version":"...", "node_id":"...", ...}
Step 7: Register a Workload and Send Your First Request
export MVGC_ADMIN_TOKEN=$(aws secretsmanager get-secret-value \ --secret-id axemere/admin-token --region $REGION \ --query SecretString --output text) GATEWAY_URL="http://localhost:7080" # Register a workload curl -s -X PUT "${GATEWAY_URL}/v1/admin/workloads" \ -H "MVGC-Admin-Token: $MVGC_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "workload_id": "wl-quickstart", "org_id": "org-quickstart", "name": "Quickstart Workload", "default_attribution": { "project_id": "proj-quickstart" }, "allowed_connection_types": ["direct_api"] }' | jq . # Test a policy denial (no API key needed) curl -s -X POST "${GATEWAY_URL}/v1/actions:execute" \ -H "Content-Type: application/json" \ -d '{ "schema": "mvgc.action_request.v2", "org_id": "org-quickstart", "workload_id": "wl-quickstart", "action": { "type": "ai.infer", "method": "POST", "target_host": "api.example-blocked.com", "params": {"model": "test"} }, "attribution": { "project_id": "proj-quickstart" } }' | jq .
Expected (HTTP 403):
{"decision": "deny", "reason": "...", "request_id": "..."}
Production Considerations
- RDS sizing.
db.t3.microis for evaluation. Usedb.t3.mediumor larger for production. Enable Multi-AZ for high availability. - EC2 sizing.
t3.mediumis a starting point. Usem6i.largeor larger under sustained load. Consider an Auto Scaling Group behind an ALB for horizontal scaling. - TLS. Put an AWS Application Load Balancer with an ACM certificate in front of the gateway for HTTPS termination. The gateway itself listens on plain HTTP internally.
- NAT Gateway. For EC2 in a private subnet (no public IP), add a NAT Gateway to the public subnet so the gateway can reach AI provider APIs.
- Secret rotation. Use Secrets Manager automatic rotation for the database password. The gateway reads secrets at startup; restart the service after rotation.
- Systems Manager. Use SSM Session Manager instead of SSH for shell access. Requires no open port 22 and no key pair.
- Connecting to the control plane. To manage this gateway from
console.axemere.ai, see the Self-Hosted + CP Connected guide.
Next Steps
| Task | Where to look |
|---|---|
| Configure credentials and policies | Configuration Reference |
| Connect to the Axemere control plane | CP Connected Onboarding |
| Set up monitoring and observability | Telemetry and Observability |
| Integrate your application | Developer Integration Guide |
| Use the managed gateway service | Managed Gateway Guide |
Cleanup
# On the instance sudo systemctl stop mvgc-gateway # From your local machine aws ec2 terminate-instances --instance-ids $INSTANCE_ID --region $REGION aws rds delete-db-instance \ --db-instance-identifier axemere-db \ --skip-final-snapshot --region $REGION aws secretsmanager delete-secret --secret-id axemere/admin-token --region $REGION aws secretsmanager delete-secret --secret-id axemere/db-url --region $REGION aws secretsmanager delete-secret --secret-id axemere/openai-key --region $REGION # Wait for instance and DB to terminate before removing VPC resources aws ec2 wait instance-terminated --instance-ids $INSTANCE_ID --region $REGION aws rds wait db-instance-deleted --db-instance-identifier axemere-db --region $REGION aws ec2 delete-security-group --group-id $SG_EC2 --region $REGION aws ec2 delete-security-group --group-id $SG_RDS --region $REGION aws ec2 detach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC_ID --region $REGION aws ec2 delete-internet-gateway --internet-gateway-id $IGW --region $REGION aws ec2 delete-subnet --subnet-id $SUBNET_PUBLIC --region $REGION aws ec2 delete-subnet --subnet-id $SUBNET_PRIVATE_A --region $REGION aws ec2 delete-subnet --subnet-id $SUBNET_PRIVATE_B --region $REGION aws ec2 delete-vpc --vpc-id $VPC_ID --region $REGION