AWS cloudformation quicklaunch (#5650)

This commit is contained in:
Alexander Petric
2025-04-21 10:26:28 -04:00
committed by GitHub
parent 5bca8f60e9
commit 2620300ce9
2 changed files with 734 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
# windmill-cloudformation
Cloudformation Template for Windmill on AWS EKS
## Overview
This CloudFormation template automatically deploys Windmill on AWS EKS. The deployment includes:
- An EKS cluster with configurable node types and sizes
- An RDS PostgreSQL database for Windmill data
- AWS Load Balancer Controller for handling ingress traffic
- Proper network configuration with VPC, subnets, and security groups
- A fully automated installation of Windmill via Helm
## Parameters
The template accepts various parameters to customize your deployment:
- **NodeInstanceType**: EC2 instance type for EKS worker nodes (t3.small to r5.2xlarge)
- **NodeGroupSize**: Number of EKS worker nodes
- **RdsInstanceClass**: RDS instance class for the PostgreSQL database (db.t3.micro to db.r5.2xlarge)
- **DBPassword**: Password for the PostgreSQL database
- **WorkerReplicas**: Number of Windmill worker replicas
- **NativeWorkerReplicas**: Number of Windmill native worker replicas
- **Enterprise**: Enable Windmill [Enterprise features](https://www.windmill.dev/docs/misc/plans_details#upgrading-to-enterprise-edition) (requires license key)
## Customization
To modify the Helm chart configuration or update the template, refer to the official Windmill Helm chart repository:
[https://github.com/windmill-labs/windmill-helm-charts](https://github.com/windmill-labs/windmill-helm-charts)
## Documentation
For more information about Windmill's Helm chart deployment options, see:
[https://www.windmill.dev/docs/advanced/self_host#helm-chart](https://www.windmill.dev/docs/advanced/self_host#helm-chart)
For detailed information about setting up RDS for Windmill on AWS:
[https://www.windmill.dev/docs/advanced/self_host/aws_ecs#create-a-rds-database](https://www.windmill.dev/docs/advanced/self_host/aws_ecs#create-a-rds-database)
## Deployment
1. Upload the CloudFormation template to your AWS account
2. Fill in the required parameters
3. Deploy the stack
4. Access Windmill using the URL provided in the Outputs section of the stack
After deployment, you can access Windmill via the LoadBalancer URL shown in the CloudFormation stack outputs.

View File

@@ -0,0 +1,688 @@
AWSTemplateFormatVersion: "2010-09-09"
Description: Deploy Windmill on EKS with Helm
Parameters:
NodeInstanceType:
Type: String
Default: t3.medium
AllowedValues:
- t3.small
- t3.medium
- t3.large
- t3.xlarge
- t3.2xlarge
- m5.large
- m5.xlarge
- m5.2xlarge
- m5.4xlarge
- c5.large
- c5.xlarge
- c5.2xlarge
- r5.large
- r5.xlarge
- r5.2xlarge
Description: EC2 instance type for the EKS worker nodes
NodeGroupSize:
Type: Number
Default: 2
RdsInstanceClass:
Type: String
Default: db.t3.small
AllowedValues:
- db.t3.micro
- db.t3.small
- db.t3.medium
- db.t3.large
- db.t3.xlarge
- db.m5.large
- db.m5.xlarge
- db.m5.2xlarge
- db.r5.large
- db.r5.xlarge
- db.r5.2xlarge
Description: RDS instance class for the PostgreSQL database
DBPassword:
Type: String
NoEcho: true
WorkerReplicas:
Type: Number
Default: 2
NativeWorkerReplicas:
Type: Number
Default: 1
Enterprise:
Type: String
Default: false
AllowedValues:
- true
- false
Description: Enable Windmill Enterprise features (requires license key)
Mappings:
RegionMap:
us-east-1:
AMI: ami-0cff7528ff583bf9a
us-east-2:
AMI: ami-0cd3c7f72edd5b06d
us-west-1:
AMI: ami-0d9858aa3c6322f73
us-west-2:
AMI: ami-098e42ae54c764c35
ca-central-1:
AMI: ami-00f881f027a6d74a0
eu-west-1:
AMI: ami-04dd4500af104442f
eu-west-2:
AMI: ami-0eb260c4d5475b901
eu-west-3:
AMI: ami-05e8e20cef0eaa9d0
eu-central-1:
AMI: ami-0bad4a5e987bdebde
ap-northeast-1:
AMI: ami-0b7546e839d7ace12
ap-northeast-2:
AMI: ami-0fd0765afb77bcca7
ap-southeast-1:
AMI: ami-0c802847a7dd848c0
ap-southeast-2:
AMI: ami-07620139298af599e
ap-south-1:
AMI: ami-0851b76e8b1bce90b
sa-east-1:
AMI: ami-054a31f1b3bf90920
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-vpc"
- Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
Value: shared
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags:
- Key: kubernetes.io/role/elb
Value: "1"
- Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
Value: shared
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet-1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags:
- Key: kubernetes.io/role/elb
Value: "1"
- Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
Value: shared
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet-2
RouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref RouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
SubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
DependsOn: PublicRoute
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref RouteTable
SubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
DependsOn: PublicRoute
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref RouteTable
EKSClusterRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- eks.amazonaws.com
- ec2.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEKSClusterPolicy
- arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
Policies:
- PolicyName: EKSAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- eks:*
- ec2:DescribeInstances
- ec2:DescribeRouteTables
- ec2:DescribeSecurityGroups
- ec2:DescribeSubnets
- ec2:DescribeVpcs
- iam:GetRole
- iam:ListRoles
Resource: "*"
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:PutParameter
Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*"
- PolicyName: KubernetesAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- eks:DescribeCluster
- eks:ListClusters
- eks:AccessKubernetesApi
Resource: !Sub "arn:aws:eks:${AWS::Region}:${AWS::AccountId}:cluster/${AWS::StackName}-cluster"
EKSNodeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
- arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
EKSCluster:
Type: AWS::EKS::Cluster
Properties:
Name: !Sub "${AWS::StackName}-cluster"
RoleArn: !GetAtt EKSClusterRole.Arn
ResourcesVpcConfig:
SubnetIds:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
EndpointPublicAccess: true
AccessConfig:
AuthenticationMode: API_AND_CONFIG_MAP
BootstrapClusterCreatorAdminPermissions: true
DependsOn:
- PublicRoute
- VPCCleanup
EKSClusterAccess:
Type: AWS::EKS::AccessEntry
Properties:
ClusterName: !Ref EKSCluster
PrincipalArn: !GetAtt EKSClusterRole.Arn
Type: STANDARD
Username: admin
AccessPolicies:
- PolicyArn: arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy
AccessScope:
Type: cluster
EKSNodeGroup:
Type: AWS::EKS::Nodegroup
DependsOn:
- WindmillDB
- EKSCluster
Properties:
ClusterName: !Ref EKSCluster
NodeRole: !GetAtt EKSNodeRole.Arn
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
ScalingConfig:
MinSize: 1
DesiredSize: !Ref NodeGroupSize
MaxSize: 4
InstanceTypes:
- !Ref NodeInstanceType
WindmillDBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS instance
SubnetIds:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
WindmillDBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow PostgreSQL access from EKS nodes
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 10.0.0.0/16
WindmillDB:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: !Sub "${AWS::StackName}-db"
AllocatedStorage: 20
DBInstanceClass: !Ref RdsInstanceClass
Engine: postgres
EngineVersion: 17.2
MasterUsername: postgres
MasterUserPassword: !Ref DBPassword
DBName: windmill
PubliclyAccessible: false
DBSubnetGroupName: !Ref WindmillDBSubnetGroup
VPCSecurityGroups:
- !Ref WindmillDBSecurityGroup
DependsOn:
- WindmillDBSubnetGroup
WindmillInstallerInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref EKSClusterRole
WindmillInstallerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Windmill installer instance
VpcId: !Ref VPC
SecurityGroupEgress:
- IpProtocol: -1
FromPort: -1
ToPort: -1
CidrIp: 0.0.0.0/0
WindmillInstaller:
Type: AWS::EC2::Instance
CreationPolicy:
ResourceSignal:
Timeout: PT30M # Gives 30 minutes for the installation to complete
DependsOn:
- EKSNodeGroup
- WindmillDB
Properties:
ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
InstanceType: t3.micro
IamInstanceProfile: !Ref WindmillInstallerInstanceProfile
SubnetId: !Ref PublicSubnet1
SecurityGroupIds:
- !Ref WindmillInstallerSecurityGroup
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -e # Exit on any error
# Install required tools
yum update -y
yum install -y aws-cli jq postgresql15 aws-cfn-bootstrap
# Set up logging directory with correct permissions
mkdir -p /var/log/windmill-installer
touch /var/log/windmill-installer/install.log
# Create installation directory
mkdir -p /opt/windmill-installer
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/
# Install helm
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod +x get_helm.sh
./get_helm.sh
# Create the installation script
cat << 'EOF' > /opt/windmill-installer/install.sh
#!/bin/bash
set -e
# Configure kubectl
aws sts get-caller-identity > /dev/null
export AWS_SDK_LOAD_CONFIG=1
export KUBECONFIG=/root/.kube/config
aws eks update-kubeconfig --name ${AWS::StackName}-cluster --region ${AWS::Region}
# Add debugging for each kubectl attempt
echo "HOME: $HOME"
echo "KUBECONFIG: $KUBECONFIG"
echo "User: $(whoami)"
echo "AWS Identity: $(aws sts get-caller-identity)"
echo "Trying kubectl command..."
kubectl get nodes || echo "Command failed with status $?"
echo "Waiting for EKS nodes to be ready..."
while true; do
# Force credential refresh on each attempt
aws sts get-caller-identity > /dev/null
aws eks update-kubeconfig --name ${AWS::StackName}-cluster --region ${AWS::Region}
if kubectl get nodes &>/dev/null; then
READY_NODES=$(kubectl get nodes -o json | jq -r '.items[] | select(.status.conditions[] | select(.type=="Ready" and .status=="True")) | .metadata.name' | wc -l)
DESIRED_NODES=${NodeGroupSize}
if [ "$READY_NODES" -eq "$DESIRED_NODES" ]; then
echo "All nodes are ready"
break
fi
echo "Found $READY_NODES ready nodes out of $DESIRED_NODES desired nodes"
else
echo "Waiting for cluster access..."
fi
sleep 30
done
echo "Installing AWS Load Balancer Controller..."
# Install AWS Load Balancer Controller
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=${AWS::StackName}-cluster \
--set region=${AWS::Region} \
--set vpcId=${VPC}
echo "Waiting for AWS Load Balancer Controller to be ready..."
kubectl wait --namespace kube-system \
--for=condition=ready pod \
--selector=app.kubernetes.io/name=aws-load-balancer-controller \
--timeout=300s
echo "Waiting for RDS to be available..."
while true; do
if pg_isready -h ${WindmillDB.Endpoint.Address} -p 5432 -U postgres 2>/dev/null; then
echo "Database is ready"
break
fi
echo "Database not ready yet..."
sleep 30
done
echo "Creating namespace and installing Windmill..."
kubectl create namespace windmill
# Add helm repo and install Windmill
helm repo add windmill https://windmill-labs.github.io/windmill-helm-charts
helm repo update
helm install ${AWS::StackName} windmill/windmill \
--namespace windmill \
--set windmill.databaseUrl="postgres://postgres:${DBPassword}@${WindmillDB.Endpoint.Address}/windmill?sslmode=require" \
--set windmill.baseDomain=windmill.local \
--set windmill.baseProtocol=http \
--set windmill.appReplicas=${WorkerReplicas} \
--set windmill.lspReplicas=2 \
--set windmill.workerGroups[0].name=default \
--set windmill.workerGroups[0].mode=worker \
--set windmill.workerGroups[0].replicas=${WorkerReplicas} \
--set windmill.workerGroups[1].name=native \
--set windmill.workerGroups[1].mode=worker \
--set windmill.workerGroups[1].replicas=${NativeWorkerReplicas} \
--set windmill.app.service.spec.type=LoadBalancer \
--set windmill.app.service.spec.sessionAffinity=None \
--set windmill.app.service.port=8000 \
--set windmill.app.service.ports[0].port=8000 \
--set windmill.app.service.ports[0].targetPort=8000 \
--set windmill.app.service.ports[0].protocol=TCP \
--set postgresql.enabled=false \
--set enterprise.enabled=${Enterprise}
# Change service to LoadBalancer
echo "Changing service to LoadBalancer"
kubectl patch service windmill-app -n windmill -p '{"spec":{"type":"LoadBalancer","sessionAffinity":"None"}}'
kubectl patch service windmill-app -n windmill -p '{"spec":{"ports":[{"name":"api","port":8000,"targetPort":8000,"protocol":"TCP"},{"name":"http","port":80,"targetPort":8000,"protocol":"TCP"}]}}'
# Wait for LoadBalancer to get an address
echo "Waiting for LoadBalancer address..."
while true; do
LB_HOSTNAME=$(kubectl get svc -n windmill windmill-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null)
if [ ! -z "$LB_HOSTNAME" ]; then
break
fi
echo "Waiting for LoadBalancer hostname..."
sleep 30
done
# Store the LoadBalancer hostname in SSM
aws ssm put-parameter \
--region ${AWS::Region} \
--name "/${AWS::StackName}/loadbalancer-hostname" \
--value "$LB_HOSTNAME" \
--type "String" \
--overwrite
# Signal CloudFormation that installation is complete
echo "Signal CloudFormation that installation is complete"
/opt/aws/bin/cfn-signal -e $? \
--stack ${AWS::StackName} \
--resource WindmillInstaller \
--region ${AWS::Region}
# Self-terminate this instance
echo "Self-terminating instance"
aws ec2 terminate-instances --instance-ids $(curl -s http://169.254.169.254/latest/meta-data/instance-id) --region ${AWS::Region}
EOF
# Set permissions and run the installation script
chmod +x /opt/windmill-installer/install.sh
# Run the installation script directly (not as ec2-user)
cd /opt/windmill-installer && ./install.sh > /var/log/windmill-installer/install.log 2>&1
LoadBalancerHostnameLookup:
Type: Custom::SSMParameterLookup
DependsOn: WindmillInstaller
Properties:
ServiceToken: !GetAtt LookupSSMParameterFunction.Arn
ParameterName: !Sub "/${AWS::StackName}/loadbalancer-hostname"
LookupSSMParameterFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Timeout: 300
Runtime: nodejs18.x
Code:
ZipFile: !Sub |
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');
const response = require('cfn-response');
exports.handler = async (event, context) => {
if (event.RequestType === 'Delete') {
return response.send(event, context, response.SUCCESS);
}
try {
const ssmClient = new SSMClient();
// Loop to check for the parameter until it's not "pending"
let tries = 0;
let paramValue = "pending";
while (paramValue === "pending" && tries < 20) {
const params = {
Name: event.ResourceProperties.ParameterName,
WithDecryption: false
};
const result = await ssmClient.send(new GetParameterCommand(params));
paramValue = result.Parameter.Value;
if (paramValue === "pending") {
await new Promise(resolve => setTimeout(resolve, 15000)); // wait 15 seconds
tries++;
}
}
return response.send(event, context, response.SUCCESS, {
HostnameValue: paramValue
});
} catch (error) {
console.error(error);
return response.send(event, context, response.FAILED, { error: error.message });
}
};
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: SSMParameterAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ssm:GetParameter
Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*"
VPCCleanupFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt VPCCleanupRole.Arn
Timeout: 300
Runtime: nodejs18.x
Code:
ZipFile: |
const { ElasticLoadBalancingClient, DescribeLoadBalancersCommand,
DeleteLoadBalancerCommand } = require('@aws-sdk/client-elastic-load-balancing');
const response = require('cfn-response');
exports.handler = async (event, context) => {
if (event.RequestType !== 'Delete') {
return response.send(event, context, response.SUCCESS);
}
try {
const elb = new ElasticLoadBalancingClient();
const vpcId = event.ResourceProperties.VpcId;
// Find and delete Classic Load Balancers in the VPC
const lbResponse = await elb.send(new DescribeLoadBalancersCommand({}));
let deleted = false;
for (const lb of lbResponse.LoadBalancerDescriptions || []) {
if (lb.VPCId === vpcId) {
console.log(`Deleting Classic Load Balancer: ${lb.LoadBalancerName}`);
await elb.send(new DeleteLoadBalancerCommand({
LoadBalancerName: lb.LoadBalancerName
}));
deleted = true;
}
}
if (deleted) {
// Wait for deletion to complete
console.log('Waiting 30 seconds for load balancer deletion to complete...');
await new Promise(r => setTimeout(r, 30000));
}
return response.send(event, context, response.SUCCESS);
} catch (error) {
console.error('Error deleting load balancers:', error);
return response.send(event, context, response.FAILED, {error: error.message});
}
};
VPCCleanupRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: VPCCleanupPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ec2:DescribeAddresses
- ec2:DisassociateAddress
- ec2:DescribeNetworkInterfaces
- elasticloadbalancing:DescribeLoadBalancers
- elasticloadbalancing:DeleteLoadBalancer
- elasticloadbalancingv2:DescribeLoadBalancers
- elasticloadbalancingv2:DeleteLoadBalancer
Resource: "*"
VPCCleanup:
Type: Custom::VPCCleanup
Properties:
ServiceToken: !GetAtt VPCCleanupFunction.Arn
VpcId: !Ref VPC
Outputs:
ClusterName:
Description: EKS cluster name
Value: !Sub "${AWS::StackName}-cluster"
DatabaseEndpoint:
Description: RDS instance endpoint
Value: !GetAtt WindmillDB.Endpoint.Address
LoadBalancerHostname:
Description: Windmill LoadBalancer hostname
Value: !Sub "http://${LoadBalancerHostnameLookup.HostnameValue}"