Quickstart: AWS (EC2)
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 | Network Operations Guide |
| 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