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.mdsource 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 Type | Use Case |
|---|---|
| Simple static sites, minimal APIs |
| Standard web apps, Node.js/Python services |
| 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:
- Navigate to Networks → Tunnels
- Select your tunnel → Public Hostname tab
- Add hostname:
- Subdomain:
app - Domain:
example.com - Type:
HTTP - URL:
localhost:80
- Subdomain:
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
| Issue | Diagnosis | Solution |
|---|---|---|
| DNS not resolving | | Wait for propagation or check CNAME record |
| Tunnel not connecting | Check | Verify token, check outbound connectivity on port 7844 |
| Container not starting | | Check Dockerfile, environment variables |
| 502 Bad Gateway | Tunnel running but app not responding | Verify container is listening on correct port |