This lab demonstrates how to use Crossplane to manage AWS infrastructure declaratively using Kubernetes APIs. We'll use KIND (Kubernetes-in-Docker) to run a local Kubernetes cluster, while Crossplane manages real AWS resources in your cloud account.
⚠️ IMPORTANT: While the Kubernetes control plane runs locally via KIND, Crossplane will create real AWS resources (VPCs, subnets, NAT gateways, etc.) that incur real costs. Monitor your AWS billing and clean up resources after the lab!
- Prerequisites
- Understanding the Architecture
- Repository Structure
- Lab Exercises
- Exercise 0: Set Up KIND Cluster
- Exercise 1: Install Crossplane
- Exercise 2: Install AWS Provider
- Exercise 3: Configure AWS Provider
- Exercise 4: Create VPC and Subnets
- Exercise 5: Add Internet Gateway and NAT
- Exercise 6: Configure Security Groups and NACLs
- Exercise 7: VPC Endpoint and Routing
- Exercise 8: CloudFront Distribution
- Exercise 9: Advanced - Compositions
- Cleanup
- Troubleshooting
- Additional Resources
Before starting this lab, ensure you have:
- Docker installed and running (required for KIND)
- KIND installed (Installation Guide)
- kubectl installed and configured
- Helm v3.x installed
- Git for cloning the repository
- AWS Account with appropriate permissions
- AWS CLI configured with credentials (optional, for verification)
- IAM User with permissions for:
- EC2 (VPC, subnets, security groups, NAT gateways, etc.)
- CloudFront (if using CloudFront)
- S3 (if using VPC endpoints for S3)
Your IAM user needs the following permissions (or use AmazonEC2FullAccess and CloudFrontFullAccess for simplicity):
ec2:*(VPC, subnets, security groups, NAT gateways, route tables, etc.)cloudfront:*(if using CloudFront)elasticloadbalancing:*(if needed)iam:CreateServiceLinkedRole(for NAT Gateway)
┌─────────────────────────────────────────┐
│ Your Local Machine │
│ │
│ ┌───────────────────────────────────┐ │
│ │ KIND Cluster (Docker) │ │
│ │ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Kubernetes │ │ │
│ │ │ Control │ │ │
│ │ │ Plane │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
│ │ ┌──────▼───────┐ │ │
│ │ │ Crossplane │ │ │
│ │ │ Controller │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
│ │ ┌──────▼───────┐ │ │
│ │ │ AWS Provider │ │ │
│ │ │ Controller │ │ │
│ │ └──────┬───────┘ │ │
│ └─────────┼─────────────────────────┘ │
└────────────┼─────────────────────────────┘
│ HTTPS API Calls
│ (AWS Credentials)
▼
┌─────────────────────────────────────────┐
│ AWS Cloud (us-east-1) │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ VPC │ │ CloudFront│ │
│ │ Subnets │ │ Distribution│ │
│ │ NAT │ │ │ │
│ │ Security │ └──────────┘ │
│ │ Groups │ │
│ └──────────┘ │
│ │
│ REAL AWS RESOURCES = REAL COSTS │
└─────────────────────────────────────────┘
Key Points:
- KIND cluster runs locally in Docker containers
- Crossplane runs inside the KIND cluster
- Crossplane makes real API calls to AWS
- Resources created are real and billable
- Use AWS Free Tier where possible, but NAT Gateways are not free
crossplane-aws-vpc-lab/
├── README.md # This file
├── docs/
│ ├── aws-concepts.md # AWS networking concepts
│ └── crossplane-fundamentals.md # Crossplane concepts
├── kind/
│ └── kind-config.yaml # KIND cluster configuration
├── manifests/
│ ├── provider-aws.yaml # AWS provider installation
│ ├── provider-config.yaml # AWS provider configuration
│ ├── vpc.yaml # VPC resource
│ ├── subnet.yaml # Subnet resources
│ ├── elastic-ip.yaml # Elastic IP for NAT
│ ├── internet-gateway.yaml # Internet Gateway
│ ├── nat-gateway.yaml # NAT Gateway
│ ├── security-group.yaml # Security Groups
│ ├── nacl.yaml # Network ACLs
│ ├── route-table.yaml # Route tables and routes
│ ├── vpc-endpoint.yaml # VPC Endpoint (S3)
│ ├── cloudfront.yaml # CloudFront distribution
│ └── composition/
│ ├── xrd.yaml # Composite Resource Definition
│ └── composition.yaml # Composition template
└── scripts/
└── setup.sh # Automated setup script
Follow these exercises in sequence. Each exercise builds on the previous one.
Objective: Create a local Kubernetes cluster using KIND.
-
Create the KIND cluster:
kind create cluster --config kind/kind-config.yaml --name crossplane-lab
-
Verify kubectl context:
kubectl cluster-info --context kind-crossplane-lab
-
Check cluster nodes:
kubectl get nodes
You should see one control-plane node running.
Alternatively, use the setup script:
chmod +x scripts/setup.sh
./scripts/setup.shThis script will:
- Check prerequisites
- Create the KIND cluster
- Install Crossplane via Helm
- Verify the installation
Discussion Points:
- KIND creates a Kubernetes cluster using Docker containers
- The control plane runs in a Docker container
- This cluster is completely local but uses standard Kubernetes APIs
Objective: Install Crossplane in the KIND cluster.
If you used the automated setup script, Crossplane is already installed. Otherwise:
-
Add Crossplane Helm repository:
helm repo add crossplane-stable https://charts.crossplane.io/stable helm repo update
-
Install Crossplane:
helm install crossplane \ --namespace crossplane-system \ --create-namespace \ crossplane-stable/crossplane \ --wait
-
Verify installation:
kubectl get pods -n crossplane-system
You should see Crossplane pods running. Wait until all pods are
READY. -
Check Crossplane components:
kubectl get all -n crossplane-system
Expected Output:
NAME READY STATUS RESTARTS AGE
pod/crossplane-7d8b9c5c6f-abc123 1/1 Running 0 2m
pod/crossplane-rbac-manager-... 1/1 Running 0 2m
Discussion Points:
- Crossplane runs as Kubernetes controllers
- It watches for Custom Resources (CRDs) that represent cloud resources
- The reconciliation loop continuously ensures desired state matches actual state
Objective: Install the AWS provider for Crossplane to manage AWS resources.
-
Check available provider packages: Visit Upbound Marketplace to see the latest versions.
-
Review the provider manifest:
cat manifests/provider-aws.yaml
This installs two providers:
provider-aws-ec2: For VPC, subnets, security groups, NAT gateways, etc.provider-aws-cloudfront: For CloudFront distributions
-
Install the providers:
kubectl apply -f manifests/provider-aws.yaml
-
Wait for providers to be ready:
kubectl get providers
Watch until both show
INSTALLEDandHEALTHY:kubectl wait --for=condition=Healthy provider/provider-aws-ec2 --timeout=300s kubectl wait --for=condition=Healthy provider/provider-aws-cloudfront --timeout=300s
-
Verify provider pods:
kubectl get pods -n crossplane-system | grep provider -
Check available CRDs:
kubectl get crds | grep aws.upbound.ioYou should see CRDs like:
vpcs.ec2.aws.upbound.iosubnets.ec2.aws.upbound.iosecuritygroups.ec2.aws.upbound.iodistributions.cloudfront.aws.upbound.io- etc.
Discussion Points:
- Providers install CRDs that extend Kubernetes API
- Each provider handles a subset of AWS services
- Family providers (like
provider-aws-ec2) are modular and focused
Objective: Configure AWS credentials so Crossplane can manage AWS resources.
-
Create AWS credentials secret:
kubectl create secret generic aws-credentials \ --namespace crossplane-system \ --from-literal=aws_access_key_id=YOUR_ACCESS_KEY \ --from-literal=aws_secret_access_key=YOUR_SECRET_KEY
Replace
YOUR_ACCESS_KEYandYOUR_SECRET_KEYwith your actual AWS credentials.Alternative: If you have AWS CLI configured, you can extract credentials:
# Get your access key ID aws configure get aws_access_key_id # Get your secret access key aws configure get aws_secret_access_key
-
Create AWS credentials file format: For the provider config, we need credentials in AWS config file format. Update the secret:
# Create a temporary credentials file cat > /tmp/aws-credentials <<EOF [default] aws_access_key_id = YOUR_ACCESS_KEY aws_secret_access_key = YOUR_SECRET_KEY EOF # Create secret from file kubectl create secret generic aws-credentials \ --namespace crossplane-system \ --from-file=credentials=/tmp/aws-credentials # Clean up rm /tmp/aws-credentials
Or if the secret already exists, delete and recreate it.
-
Review provider configuration:
cat manifests/provider-config.yaml
-
Apply provider configuration:
kubectl apply -f manifests/provider-config.yaml
-
Verify provider config:
kubectl get providerconfig
You should see
defaultprovider config.
Security Best Practices:
- Never commit AWS credentials to Git
- Use IAM roles in production (not access keys)
- Rotate credentials regularly
- Use least-privilege IAM policies
Discussion Points:
- ProviderConfig tells providers how to authenticate
- Secrets store credentials securely in Kubernetes
- One ProviderConfig can be used by multiple resources
Objective: Create a VPC and subnets using Crossplane.
-
Review VPC manifest:
cat manifests/vpc.yaml
Key fields:
cidrBlock: IP address range for the VPCenableDnsHostnamesandenableDnsSupport: DNS configurationproviderConfigRef: References the provider config from Exercise 3
-
Apply VPC manifest:
kubectl apply -f manifests/vpc.yaml
-
Monitor VPC creation:
kubectl get vpc lab-vpc -w
Watch for
SYNCEDandREADYto becomeTrue:NAME SYNCED READY EXTERNAL-NAME AGE lab-vpc True True vpc-xxxxx 1m -
Get VPC details:
kubectl describe vpc lab-vpc
Note the
EXTERNAL-NAME(the actual AWS VPC ID). -
Verify in AWS (optional):
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=lab-vpc"
-
Review subnet manifest:
cat manifests/subnet.yaml
This creates two subnets:
- Public subnet:
10.0.1.0/24withmapPublicIpOnLaunch: true - Private subnet:
10.0.2.0/24withmapPublicIpOnLaunch: false
Notice the
vpcIdReffield that references the VPC created earlier. - Public subnet:
-
Apply subnet manifest:
kubectl apply -f manifests/subnet.yaml
-
Monitor subnet creation:
kubectl get subnet -w
-
Check subnet details:
kubectl describe subnet lab-public-subnet kubectl describe subnet lab-private-subnet
Discussion Points:
vpcIdRefcreates a dependency - Crossplane waits for VPC to be ready- Subnets inherit VPC settings
- Public vs. private subnets differ by IP mapping and routing
Objective: Enable internet connectivity for public and private subnets.
-
Apply Elastic IP manifest:
kubectl apply -f manifests/elastic-ip.yaml
-
Wait for EIP to be ready:
kubectl get eip lab-eip -w
-
Review Internet Gateway manifest:
cat manifests/internet-gateway.yaml
-
Apply Internet Gateway:
kubectl apply -f manifests/internet-gateway.yaml
-
Monitor creation:
kubectl get internetgateway lab-igw -w
-
Review NAT Gateway manifest:
cat manifests/nat-gateway.yaml
Notice:
subnetIdRef: References the public subnet (NAT must be in public subnet)allocationIdRef: References the Elastic IP
-
Apply NAT Gateway:
kubectl apply -f manifests/nat-gateway.yaml
-
Monitor creation (this takes a few minutes):
kubectl get natgateway lab-nat -w
⚠️ Note: NAT Gateways incur hourly charges (~$0.045/hour) plus data transfer costs.
-
Review route table manifest:
cat manifests/route-table.yaml
This creates:
- Public route table with route to Internet Gateway
- Private route table with route to NAT Gateway
- Associations between subnets and route tables
-
Apply route tables:
kubectl apply -f manifests/route-table.yaml
-
Verify routes:
kubectl get routetable kubectl get route kubectl get routetableassociation
Discussion Points:
- Internet Gateway enables internet access for public subnets
- NAT Gateway allows private subnets outbound internet access
- Route tables control traffic direction
- NAT Gateway is a managed service (not free tier)
Objective: Set up firewall rules for network security.
-
Review security group manifest:
cat manifests/security-group.yaml
This creates two security groups:
lab-web-sg: Allows HTTP/HTTPS from internetlab-db-sg: Allows MySQL (3306) only from web security group
-
Apply security groups:
kubectl apply -f manifests/security-group.yaml
-
Monitor creation:
kubectl get securitygroup
-
Check security group rules:
kubectl describe securitygroup lab-web-sg kubectl describe securitygroup lab-db-sg
Notice how
lab-db-sgreferenceslab-web-sgusingsecurityGroupIdsRefs.
-
Review NACL manifest:
cat manifests/nacl.yaml
This modifies the default NACL (allows all traffic in both directions).
-
Apply NACL:
kubectl apply -f manifests/nacl.yaml
Discussion Points:
- Security groups are stateful and instance-level
- NACLs are stateless and subnet-level
- Security groups can reference other security groups
- See PDF slides Image ID 5-7, 10 for visual comparisons
Objective: Create a VPC endpoint for private S3 access.
-
Review VPC endpoint manifest:
cat manifests/vpc-endpoint.yaml
This creates an S3 Gateway Endpoint:
- Type:
Gateway(free, only for S3 and DynamoDB) - Associated with both route tables
- Allows private access to S3 without internet
- Type:
-
Apply VPC endpoint:
kubectl apply -f manifests/vpc-endpoint.yaml
-
Monitor creation:
kubectl get vpcendpoint lab-s3-endpoint -w
-
Verify endpoint:
kubectl describe vpcendpoint lab-s3-endpoint
Discussion Points:
- VPC endpoints keep traffic within AWS network
- Gateway endpoints are free (S3, DynamoDB only)
- Interface endpoints use PrivateLink (charges apply)
- Reduces NAT Gateway data transfer costs for S3 access
Objective: Create a CloudFront CDN distribution.
-
Update CloudFront manifest: Edit
manifests/cloudfront.yaml:- Replace
mybucket.s3.amazonaws.comwith your S3 bucket domain - Replace
origin-access-identity/cloudfront/ABCDEFG1234567with your OAI/OAC
- Replace
-
Review CloudFront manifest:
cat manifests/cloudfront.yaml
-
Apply CloudFront distribution:
kubectl apply -f manifests/cloudfront.yaml
-
Monitor creation (this takes several minutes):
kubectl get distribution lab-cloudfront -w
-
Get distribution details:
kubectl describe distribution lab-cloudfront
Note the
domainNamein the status - this is your CloudFront URL.
Discussion Points:
- CloudFront is a global CDN
- Reduces latency by caching at edge locations
- Can use S3, ALB, or custom origins
- Supports custom SSL certificates
Objective: Create a reusable composition that packages multiple resources.
-
Apply Composite Resource Definition (XRD):
kubectl apply -f manifests/composition/xrd.yaml
This defines a new
VPCNetworkresource type. -
Verify XRD:
kubectl get xrd
-
Apply Composition:
kubectl apply -f manifests/composition/composition.yaml
This defines what resources are created when a
VPCNetworkis created. -
Verify Composition:
kubectl get composition
-
Create a VPCNetwork resource:
cat <<EOF | kubectl apply -f - apiVersion: aws.example.org/v1alpha1 kind: VPCNetwork metadata: name: my-vpc-network spec: parameters: region: us-east-1 cidrBlock: "10.1.0.0/16" publicSubnetCidr: "10.1.1.0/24" privateSubnetCidr: "10.1.2.0/24" availabilityZone: us-east-1a EOF
-
Monitor composition:
kubectl get vpcnetwork my-vpc-network -w kubectl get managed
You should see all the composed resources being created (VPC, subnets, IGW, NAT, routes, etc.).
-
Check individual resources:
kubectl get vpc,subnet,internetgateway,natgateway
Discussion Points:
- Compositions create higher-level abstractions
- Users don't need to know about individual resources
- Enforces consistent patterns
- Reusable across teams
-
Delete all manifests (in reverse dependency order):
# Delete CloudFront first (takes time to delete) kubectl delete -f manifests/cloudfront.yaml # Delete VPC endpoint kubectl delete -f manifests/vpc-endpoint.yaml # Delete route tables and associations kubectl delete -f manifests/route-table.yaml # Delete NAT Gateway (charges stop when deleted) kubectl delete -f manifests/nat-gateway.yaml # Delete security groups and NACLs kubectl delete -f manifests/security-group.yaml kubectl delete -f manifests/nacl.yaml # Delete Internet Gateway kubectl delete -f manifests/internet-gateway.yaml # Delete Elastic IP kubectl delete -f manifests/elastic-ip.yaml # Delete subnets kubectl delete -f manifests/subnet.yaml # Delete VPC (deletes default route table, default NACL, etc.) kubectl delete -f manifests/vpc.yaml # Delete compositions kubectl delete -f manifests/composition/ # Delete provider config kubectl delete -f manifests/provider-config.yaml # Delete providers kubectl delete -f manifests/provider-aws.yaml # Delete secrets kubectl delete secret aws-credentials -n crossplane-system
Or delete all at once (may require retries due to dependencies):
kubectl delete -f manifests/ --recursive
-
Verify resources are deleted:
kubectl get managed
Should return empty or only resources being deleted.
-
Check AWS console to ensure resources are deleted (especially NAT Gateways and Elastic IPs).
kind delete cluster --name crossplane-labhelm uninstall crossplane -n crossplane-systemSymptoms: Provider shows INSTALLING or UNHEALTHY
Solutions:
# Check provider logs
kubectl logs -n crossplane-system -l pkg.crossplane.io/provider=provider-aws-ec2
# Check provider status
kubectl describe provider provider-aws-ec2
# Reinstall provider
kubectl delete -f manifests/provider-aws.yaml
kubectl apply -f manifests/provider-aws.yamlSymptoms: Resource shows SYNCED=False or stays in SYNCING
Solutions:
# Check resource status
kubectl describe vpc lab-vpc
# Check events
kubectl get events --sort-by='.lastTimestamp'
# Check provider logs
kubectl logs -n crossplane-system -l pkg.crossplane.io/provider=provider-aws-ec2 --tail=100Symptoms: InvalidClientTokenId or UnauthorizedOperation
Solutions:
- Verify AWS credentials secret exists:
kubectl get secret aws-credentials -n crossplane-system
- Check credentials format in secret
- Verify IAM user has required permissions
- Test credentials with AWS CLI:
aws sts get-caller-identity
Symptoms: Resource stuck waiting for dependency
Solutions:
- Check if referenced resource exists and is ready:
kubectl get vpc lab-vpc
- Verify
*Reffields point to correct resource names - Check resource names match exactly (case-sensitive)
Symptoms: NAT Gateway takes 5+ minutes to create
This is normal - NAT Gateway provisioning takes time. Monitor with:
kubectl get natgateway lab-nat -w- See
docs/aws-concepts.mdfor detailed AWS networking concepts - See
docs/crossplane-fundamentals.mdfor Crossplane concepts - Crossplane Examples
- Upbound Blog
This lab references PDF slides with images:
- Image ID 5: Security Group hierarchy
- Image ID 6: Security Group rules
- Image ID 7: NACL diagram
- Image ID 10: Security Group vs. NACL comparison
This lab is provided for educational purposes. Use at your own risk regarding AWS costs.
Found an issue or want to improve the lab? Please open an issue or submit a pull request!
Happy Learning! 🚀
Remember: This lab creates real AWS resources. Always clean up after completing the exercises to avoid unexpected charges.