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
  • curl and jq for 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

HTTP :7080

proxied calls

optional

Your Application

Axemere Gateway
(EC2 Instance)

RDS PostgreSQL 15
(private subnet)

AI Provider
(OpenAI / Anthropic)

Axemere Control Plane
(CP-connected mode)

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 AmazonSSMManagedInstanceCore to 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.micro is for evaluation. Use db.t3.medium or larger for production. Enable Multi-AZ for high availability.
  • EC2 sizing. t3.medium is a starting point. Use m6i.large or 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

TaskWhere to look
Configure credentials and policiesConfiguration Reference
Connect to the Axemere control planeCP Connected Onboarding
Set up monitoring and observabilityNetwork Operations Guide
Integrate your applicationDeveloper Integration Guide
Use the managed gateway serviceManaged 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