Asi cloudflare-tunnel-ec2-deployment

install
source · Clone the upstream repo
git clone https://github.com/plurigrid/asi
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/plurigrid/asi "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/cloudflare-tunnel-ec2-deployment" ~/.claude/skills/plurigrid-asi-cloudflare-tunnel-ec2-deployment && rm -rf "$T"
manifest: skills/cloudflare-tunnel-ec2-deployment/SKILL.md
source content

Deploying Applications on EC2 with Cloudflare Tunnel

Quick Start

Provision and Deploy

# Get VPC and subnet
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --query 'Vpcs[0].VpcId' --output text)
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=map-public-ip-on-launch,Values=true" --query 'Subnets[0].SubnetId' --output text)

# Create security group
SG_ID=$(aws ec2 create-security-group --group-name app-sg --description "Security group for app deployment" --vpc-id $VPC_ID --query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol tcp --port 22 --cidr 0.0.0.0/0

# Launch instance
AMI_ID=$(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)
INSTANCE_ID=$(aws ec2 run-instances --image-id $AMI_ID --instance-type t3.small --key-name app-deploy-key --security-group-ids $SG_ID --subnet-id $SUBNET_ID --associate-public-ip-address --query 'Instances[0].InstanceId' --output text)

Prerequisites

  • AWS CLI configured with appropriate permissions (EC2, VPC)
  • Cloudflare account with a domain configured
  • Docker-compatible application with Dockerfile
  • Cloudflare Tunnel token from Zero Trust dashboard

Infrastructure Setup

1. VPC Resources

# Get default VPC
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" \
  --query 'Vpcs[0].VpcId' --output text)

# Get public subnet
SUBNET_ID=$(aws ec2 describe-subnets \
  --filters "Name=vpc-id,Values=$VPC_ID" "Name=map-public-ip-on-launch,Values=true" \
  --query 'Subnets[0].SubnetId' --output text)

2. Security Group

# Create security group - only SSH needed for management
# Cloudflare Tunnel handles all inbound traffic
SG_ID=$(aws ec2 create-security-group \
  --group-name app-sg \
  --description "Security group for app deployment" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# Allow SSH access (restrict to your IP in production)
aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID \
  --protocol tcp --port 22 --cidr 0.0.0.0/0

Key insight: With Cloudflare Tunnel, you don't need to open ports 80/443. The tunnel creates outbound connections to Cloudflare, eliminating inbound attack surface.

3. SSH Key Pair

# Generate local key
ssh-keygen -t ed25519 -f ~/.ssh/app-deploy-key -N "" -C "app-deploy"

# Import to AWS
aws ec2 import-key-pair \
  --key-name app-deploy-key \
  --public-key-material fileb://~/.ssh/app-deploy-key.pub

Important: Never delete SSH keys after creation - you'll be locked out of the instance.

4. Launch EC2 Instance

# Get latest Amazon Linux 2023 AMI
AMI_ID=$(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)

# Launch instance
INSTANCE_ID=$(aws ec2 run-instances \
  --image-id $AMI_ID \
  --instance-type t3.small \
  --key-name app-deploy-key \
  --security-group-ids $SG_ID \
  --subnet-id $SUBNET_ID \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=my-app}]' \
  --query 'Instances[0].InstanceId' --output text)

# Wait for instance and get IP
aws ec2 wait instance-running --instance-ids $INSTANCE_ID
PUBLIC_IP=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID \
  --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)

Instance Sizing

Instance TypeUse Case
t3.micro
Simple static sites, minimal APIs
t3.small
Standard web apps, Node.js/Python services
t3.medium
Apps with build steps, multiple containers

Application Deployment

Install Docker

# Wait for SSH to be ready (30-60 seconds after instance running)
sleep 45

# Install Docker
ssh -i ~/.ssh/app-deploy-key ec2-user@$PUBLIC_IP \
  "sudo dnf install -y docker git && \
   sudo systemctl enable --now docker && \
   sudo usermod -aG docker ec2-user"

Deploy Application

# Clone and build application
ssh -i ~/.ssh/app-deploy-key ec2-user@$PUBLIC_IP \
  "git clone <REPO_URL> app && \
   cd app && \
   echo 'ENV_VAR=value' > .env"

# Build and run container
ssh -i ~/.ssh/app-deploy-key ec2-user@$PUBLIC_IP \
  "cd app && \
   sudo docker build -t myapp:latest . && \
   sudo docker run -d --name myapp --restart unless-stopped -p 80:80 myapp:latest"

Production Dockerfile Pattern

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG VITE_API_KEY
ENV VITE_API_KEY=$VITE_API_KEY
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Reasoning: Multi-stage builds reduce image size and don't include build tools in production.

Cloudflare Tunnel Configuration

Install cloudflared

ssh -i ~/.ssh/app-deploy-key ec2-user@$PUBLIC_IP \
  "curl -fsSL https://pkg.cloudflare.com/cloudflared.repo | \
   sudo tee /etc/yum.repos.d/cloudflared.repo && \
   sudo yum install -y cloudflared"

Install Tunnel as Service

# Use the token from Cloudflare Zero Trust dashboard
ssh -i ~/.ssh/app-deploy-key ec2-user@$PUBLIC_IP \
  "sudo cloudflared service install <TUNNEL_TOKEN>"

Reasoning: Installing as a systemd service ensures the tunnel auto-starts on reboot and restarts on failure.

Verify Tunnel Connection

ssh -i ~/.ssh/app-deploy-key ec2-user@$PUBLIC_IP \
  "sudo systemctl status cloudflared"

Look for:

Registered tunnel connection
messages (should see 4 connections for a healthy tunnel).

DNS Configuration

Cloudflare Dashboard Method

In Cloudflare Zero Trust dashboard:

  1. Navigate to Networks → Tunnels
  2. Select your tunnel → Public Hostname tab
  3. Add hostname:
    • Subdomain:
      app
    • Domain:
      example.com
    • Type:
      HTTP
    • URL:
      localhost:80

This automatically creates a CNAME record pointing to

<tunnel-id>.cfargotunnel.com
.

API Method

# Create DNS record via API
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "CNAME",
    "name": "app",
    "content": "<tunnel-id>.cfargotunnel.com",
    "proxied": true
  }'

Validation

Test Deployment

# Test via tunnel (may take 1-2 minutes for DNS propagation)
curl -sI https://app.example.com/

# Check container logs
ssh -i ~/.ssh/app-deploy-key ec2-user@$PUBLIC_IP \
  "sudo docker logs myapp"

# Verify tunnel health
ssh -i ~/.ssh/app-deploy-key ec2-user@$PUBLIC_IP \
  "sudo journalctl -u cloudflared -n 20"

Success Criteria

  • HTTP 200 response from public URL
  • No errors in container logs
  • 4 registered tunnel connections

Outputs Documentation

Create an outputs file for future reference:

{
  "app_url": "https://app.example.com",
  "ec2_instance_id": "<instance-id>",
  "ec2_public_ip": "<public-ip>",
  "aws_region": "us-east-1",
  "ssh_command": "ssh -i ~/.ssh/app-deploy-key ec2-user@<public-ip>",
  "cloudflare_tunnel_id": "<tunnel-id>",
  "container_name": "myapp"
}

Security Considerations

  • Restrict SSH access - Use specific IP ranges instead of 0.0.0.0/0
  • No public ports needed - Cloudflare Tunnel eliminates need for ports 80/443
  • Rotate tunnel tokens - If compromised, regenerate in Cloudflare dashboard
  • Use secrets management - Don't hardcode API keys in Dockerfiles; use build args or runtime env vars
  • Enable Cloudflare Access - Add authentication layer for sensitive applications

Troubleshooting

IssueDiagnosisSolution
DNS not resolving
dig @1.1.1.1 app.example.com
Wait for propagation or check CNAME record
Tunnel not connectingCheck
systemctl status cloudflared
Verify token, check outbound connectivity on port 7844
Container not starting
docker logs myapp
Check Dockerfile, environment variables
502 Bad GatewayTunnel running but app not respondingVerify container is listening on correct port

References