diff --git a/terraform/QUICK-START.md b/terraform/QUICK-START.md new file mode 100644 index 000000000..17c4cced8 --- /dev/null +++ b/terraform/QUICK-START.md @@ -0,0 +1,233 @@ +# Code-Server AWS Deployment - Quick Start + +This is a condensed guide to get code-server running on AWS quickly. For detailed documentation, see [README.md](README.md). + +## Prerequisites + +- AWS Account with credentials configured +- Terraform >= 1.0 +- AWS CLI +- kubectl and Helm (for EKS deployment) +- SAML/OIDC Provider configured (Okta, Azure AD, Google, etc.) + +## 5-Minute EC2 Setup + +### 1. Configure Variables + +```bash +cd deployments/ec2 +cp terraform.tfvars.example terraform.tfvars +``` + +Edit `terraform.tfvars` with minimum required values: + +```hcl +aws_region = "us-east-1" + +# OAuth2/SAML Configuration +oauth2_client_id = "your-client-id" +oauth2_client_secret = "your-client-secret" +oauth2_issuer_url = "https://your-idp.com/.well-known/openid-configuration" +oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" +oauth2_cookie_secret = "run: python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())'" +``` + +### 2. Deploy + +```bash +../../scripts/deploy-ec2.sh +``` + +### 3. Get Access URL + +```bash +terraform output alb_url +``` + +### 4. Get Password + +```bash +aws secretsmanager get-secret-value \ + --secret-id $(terraform output -raw code_server_password_secret_arn) \ + --query SecretString --output text +``` + +## 10-Minute EKS Setup + +### 1. Configure Variables + +```bash +cd deployments/eks +cp terraform.tfvars.example terraform.tfvars +``` + +Edit `terraform.tfvars`: + +```hcl +aws_region = "us-east-1" + +# OAuth2/SAML Configuration +oauth2_client_id = "your-client-id" +oauth2_client_secret = "your-client-secret" +oauth2_cookie_secret = "generate-random-secret" +``` + +Edit `k8s/code-server-values.yaml`: + +```yaml +ingress: + hosts: + - host: code-server.example.com +``` + +Edit `k8s/oauth2-proxy.yaml`: + +```yaml +data: + oauth2_proxy.cfg: | + redirect_url = "https://code-server.example.com/oauth2/callback" + oidc_issuer_url = "https://your-idp.com" +``` + +### 2. Deploy + +```bash +../../scripts/deploy-eks.sh +``` + +### 3. Get Load Balancer URL + +```bash +kubectl get ingress -n code-server +``` + +## Common Commands + +### EC2 + +```bash +# View logs +aws logs tail /aws/ec2/code-server-dev-code-server --follow + +# Scale instances +terraform apply -var="desired_instances=3" + +# Destroy +../../scripts/destroy-ec2.sh +``` + +### EKS + +```bash +# View pods +kubectl get pods -n code-server + +# View logs +kubectl logs -n code-server -l app.kubernetes.io/name=code-server -f + +# Scale pods +kubectl scale deployment code-server -n code-server --replicas=3 + +# Destroy +../../scripts/destroy-eks.sh +``` + +## Generate Cookie Secret + +```bash +python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())' +``` + +Or: + +```bash +openssl rand -base64 32 +``` + +## SAML/OIDC Provider Quick Links + +### Okta + +```hcl +oauth2_issuer_url = "https://.okta.com/.well-known/openid-configuration" +``` + +### Azure AD + +```hcl +oauth2_issuer_url = "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration" +``` + +### Google + +```hcl +oauth2_issuer_url = "https://accounts.google.com/.well-known/openid-configuration" +``` + +## Troubleshooting + +### Can't access code-server + +1. Check security group allows your IP +2. Verify ALB is healthy: `aws elbv2 describe-target-health --target-group-arn ` +3. Check logs for errors + +### Authentication fails + +1. Verify redirect URL matches IdP configuration exactly +2. Check client ID and secret are correct +3. View OAuth2 Proxy logs for detailed error messages + +### Pods not starting (EKS) + +1. Check events: `kubectl get events -n code-server --sort-by='.lastTimestamp'` +2. Check pod status: `kubectl describe pod -n code-server` +3. Verify nodes have capacity: `kubectl top nodes` + +## Cost Estimate + +### EC2 (t3.medium, 1 instance) + +- EC2: ~$30/month +- ALB: ~$20/month +- NAT Gateway: ~$32/month +- EBS: ~$5/month +- **Total: ~$87/month** + +### EKS (t3.medium, 2 nodes) + +- EKS Control Plane: ~$73/month +- EC2 Nodes: ~$60/month +- ALB: ~$20/month +- NAT Gateway: ~$32/month +- EBS: ~$10/month +- **Total: ~$195/month** + +### Cost Optimization + +- Use single NAT gateway: Save ~$32-64/month +- Use SPOT instances (EKS): Save up to 90% on compute +- Scale to zero during off-hours: Save on compute costs +- Use GP3 instead of GP2: Save ~20% on storage + +## Next Steps + +1. Configure DNS (CNAME to ALB DNS) +2. Set up ACM certificate for HTTPS +3. Configure auto-scaling policies +4. Set up CloudWatch alarms +5. Review and adjust security groups +6. Configure backup/snapshot policies + +## Full Documentation + +- [Complete README](README.md) - Detailed deployment guide +- [SAML Setup Guide](SAML-SETUP-GUIDE.md) - IdP configuration +- [Code-Server Docs](https://coder.com/docs/code-server) - Code-Server features + +## Support + +For issues: +1. Check [Troubleshooting](README.md#troubleshooting) in README +2. Review CloudWatch logs +3. Check AWS service health dashboard diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 000000000..107406a7d --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,758 @@ +# Code-Server AWS Deployment with Terraform + +This repository contains Terraform code to deploy [code-server](https://github.com/coder/code-server) on AWS using either **EC2** or **EKS**, with private networking, security hardening, and SAML authentication. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Features](#features) +- [Prerequisites](#prerequisites) +- [Quick Start](#quick-start) + - [EC2 Deployment](#ec2-deployment) + - [EKS Deployment](#eks-deployment) +- [Configuration](#configuration) +- [SAML/OIDC Authentication](#samloidc-authentication) +- [Security Features](#security-features) +- [Deployment Procedures](#deployment-procedures) +- [Rollout and Updates](#rollout-and-updates) +- [Monitoring and Logging](#monitoring-and-logging) +- [Troubleshooting](#troubleshooting) +- [Cost Optimization](#cost-optimization) +- [Cleanup](#cleanup) + +## Overview + +This Terraform configuration provides two deployment options for code-server: + +1. **EC2 Deployment**: Code-server running on Auto Scaling EC2 instances behind an Application Load Balancer +2. **EKS Deployment**: Code-server running on Amazon EKS (Kubernetes) with Helm charts + +Both deployments include: +- Private networking with VPC, subnets, and NAT gateways +- SAML/OIDC authentication via OAuth2 Proxy +- HTTPS support with ACM certificates +- Encryption at rest using AWS KMS +- Auto-scaling capabilities +- CloudWatch logging and monitoring +- Security hardening following AWS best practices + +## Architecture + +### EC2 Architecture + +``` +Internet → ALB (HTTPS) → OAuth2 Proxy → Code-Server (EC2 Auto Scaling) + ↓ ↓ + Private Subnets Private Subnets + ↓ ↓ + NAT Gateway VPC Endpoints + ↓ + IGW +``` + +### EKS Architecture + +``` +Internet → ALB Ingress → OAuth2 Proxy Pod → Code-Server Pods + ↓ ↓ + EKS Cluster EKS Nodes + ↓ ↓ + Private Subnets Private Subnets + ↓ ↓ + NAT Gateway VPC Endpoints + ↓ + IGW +``` + +## Features + +- **Private Network Setup**: All compute resources in private subnets +- **SAML/OIDC Authentication**: OAuth2 Proxy for enterprise SSO integration +- **High Availability**: Multi-AZ deployment with auto-scaling +- **Security**: + - Encryption at rest (KMS) + - Encryption in transit (TLS) + - IAM roles with least privilege + - Security groups with minimal ingress + - VPC Flow Logs + - IMDSv2 required +- **Monitoring**: CloudWatch Logs and Metrics +- **Infrastructure as Code**: Full Terraform automation +- **Cost Optimized**: Options for single NAT gateway and SPOT instances + +## Prerequisites + +Before deploying, ensure you have: + +1. **AWS Account** with appropriate permissions +2. **AWS CLI** configured with credentials + ```bash + aws configure + ``` +3. **Terraform** >= 1.0 installed + ```bash + # Install via brew (macOS) + brew install terraform + + # Or download from https://www.terraform.io/downloads + ``` +4. **kubectl** (for EKS deployment) + ```bash + brew install kubectl + ``` +5. **Helm** (for EKS deployment) + ```bash + brew install helm + ``` +6. **ACM Certificate** (optional, for HTTPS) + - Request a certificate in AWS Certificate Manager + - Validate domain ownership + - Note the certificate ARN + +7. **SAML/OIDC Provider** configured (e.g., Okta, Azure AD, Google Workspace) + +## Quick Start + +### EC2 Deployment + +1. **Navigate to EC2 deployment directory:** + ```bash + cd deployments/ec2 + ``` + +2. **Copy and configure variables:** + ```bash + cp terraform.tfvars.example terraform.tfvars + vim terraform.tfvars # Edit with your values + ``` + +3. **Deploy using the automated script:** + ```bash + ../../scripts/deploy-ec2.sh + ``` + + Or manually: + ```bash + terraform init + terraform plan + terraform apply + ``` + +4. **Get the code-server password:** + ```bash + aws secretsmanager get-secret-value \ + --secret-id $(terraform output -raw code_server_password_secret_arn) \ + --query SecretString \ + --output text + ``` + +5. **Access code-server:** + ```bash + echo $(terraform output -raw alb_url) + # Navigate to this URL in your browser + ``` + +### EKS Deployment + +1. **Navigate to EKS deployment directory:** + ```bash + cd deployments/eks + ``` + +2. **Copy and configure variables:** + ```bash + cp terraform.tfvars.example terraform.tfvars + vim terraform.tfvars # Edit with your values + ``` + +3. **Deploy using the automated script:** + ```bash + ../../scripts/deploy-eks.sh + ``` + + This script will: + - Deploy EKS infrastructure + - Configure kubectl + - Install AWS Load Balancer Controller + - Deploy code-server (optional) + - Deploy OAuth2 Proxy (optional) + +4. **Manual deployment alternative:** + ```bash + # Deploy infrastructure + terraform init + terraform plan + terraform apply + + # Configure kubectl + aws eks update-kubeconfig --region --name + + # Deploy code-server + helm upgrade --install code-server ../../ci/helm-chart \ + --namespace code-server \ + --create-namespace \ + --values k8s/code-server-values.yaml + + # Deploy OAuth2 Proxy + kubectl apply -f k8s/oauth2-proxy.yaml + ``` + +5. **Get the Load Balancer URL:** + ```bash + kubectl get ingress -n code-server + # Wait for ADDRESS field to be populated + ``` + +## Configuration + +### Key Configuration Files + +#### EC2 Deployment + +- `deployments/ec2/terraform.tfvars` - Main configuration +- `modules/ec2/user-data.sh` - EC2 initialization script + +Important variables: + +```hcl +# Network +vpc_cidr = "10.0.0.0/16" +private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] + +# Security +allowed_cidr_blocks = ["10.0.0.0/8"] # Restrict access +internal_alb = true # Private load balancer + +# OAuth2/SAML +oauth2_issuer_url = "https://your-idp.com/.well-known/openid-configuration" +oauth2_client_id = "your-client-id" +oauth2_client_secret = "your-client-secret" +oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" + +# Generate cookie secret +oauth2_cookie_secret = "run: python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())'" +``` + +#### EKS Deployment + +- `deployments/eks/terraform.tfvars` - Main configuration +- `deployments/eks/k8s/code-server-values.yaml` - Helm values +- `deployments/eks/k8s/oauth2-proxy.yaml` - OAuth2 Proxy manifest + +Important Helm values: + +```yaml +# k8s/code-server-values.yaml +ingress: + enabled: true + ingressClassName: "alb" + annotations: + alb.ingress.kubernetes.io/scheme: internal + alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:... + +persistence: + enabled: true + storageClass: "gp3" + size: 20Gi + +resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 2000m + memory: 4Gi +``` + +## SAML/OIDC Authentication + +This deployment uses OAuth2 Proxy to provide SAML/OIDC authentication. + +### Supported Providers + +- Okta +- Azure Active Directory +- Google Workspace +- AWS SSO (IAM Identity Center) +- Any OIDC-compliant provider + +### Configuration Steps + +1. **Configure your Identity Provider:** + + **For Okta:** + ``` + Application Type: Web + Sign-in redirect URIs: https://code-server.example.com/oauth2/callback + Sign-out redirect URIs: https://code-server.example.com + Grant types: Authorization Code, Refresh Token + ``` + + **For Azure AD:** + ``` + Platform: Web + Redirect URI: https://code-server.example.com/oauth2/callback + Supported account types: Single tenant or Multi-tenant + ``` + +2. **Get OIDC Discovery URL:** + + Usually in format: + - Okta: `https://.okta.com/.well-known/openid-configuration` + - Azure AD: `https://login.microsoftonline.com//v2.0/.well-known/openid-configuration` + - Google: `https://accounts.google.com/.well-known/openid-configuration` + +3. **Configure Terraform variables:** + ```hcl + oauth2_issuer_url = "" + oauth2_client_id = "" + oauth2_client_secret = "" + oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" + ``` + +4. **Restrict access by email (optional):** + ```hcl + oauth2_allowed_emails = [ + "user1@company.com", + "user2@company.com" + ] + ``` + +### Testing Authentication + +1. Access your code-server URL +2. You should be redirected to your IdP login page +3. After successful authentication, you'll be redirected back to code-server +4. OAuth2 Proxy validates the session and proxies requests to code-server + +## Security Features + +### Network Security + +- **Private Subnets**: All compute resources in private subnets +- **NAT Gateway**: Outbound internet access without public IPs +- **Security Groups**: Minimal ingress rules +- **VPC Flow Logs**: Network traffic monitoring +- **Internal ALB Option**: Keep load balancer private + +### Encryption + +- **At Rest**: KMS encryption for EBS volumes and EKS secrets +- **In Transit**: TLS for all external connections +- **Secrets**: AWS Secrets Manager for code-server password + +### IAM + +- **Least Privilege**: Minimal IAM permissions +- **IRSA** (EKS): IAM Roles for Service Accounts +- **Instance Profiles**: Role-based access for EC2 + +### Compliance + +- **IMDSv2 Required**: Enhanced EC2 metadata security +- **Encrypted Storage**: All data encrypted at rest +- **Audit Logs**: CloudWatch and VPC Flow Logs + +## Deployment Procedures + +### Initial Deployment + +1. **Prepare Configuration:** + ```bash + cd deployments/ + cp terraform.tfvars.example terraform.tfvars + # Edit terraform.tfvars with your values + ``` + +2. **Review Plan:** + ```bash + terraform init + terraform plan + # Review all resources to be created + ``` + +3. **Apply Configuration:** + ```bash + terraform apply + # Type 'yes' to confirm + ``` + +4. **Verify Deployment:** + ```bash + # EC2 + aws autoscaling describe-auto-scaling-groups \ + --auto-scaling-group-names + + # EKS + kubectl get pods -n code-server + kubectl get ingress -n code-server + ``` + +### DNS Configuration + +1. **Get Load Balancer DNS:** + ```bash + # EC2 + terraform output alb_dns_name + + # EKS + kubectl get ingress -n code-server -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}' + ``` + +2. **Create DNS Record:** + ``` + Type: CNAME + Name: code-server.example.com + Value: + TTL: 300 + ``` + +3. **Update Configuration:** + ```hcl + # Update oauth2_redirect_url with actual domain + oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" + ``` + +4. **Reapply:** + ```bash + terraform apply + ``` + +## Rollout and Updates + +### Update Code-Server Version + +**EC2:** +```bash +# Update version in terraform.tfvars +code_server_version = "4.19.0" + +# Apply changes (will trigger rolling update) +terraform apply + +# Monitor Auto Scaling Group +aws autoscaling describe-auto-scaling-instances +``` + +**EKS:** +```bash +# Update version in k8s/code-server-values.yaml +image: + tag: "4.19.0" + +# Perform rolling update +helm upgrade code-server ../../ci/helm-chart \ + --namespace code-server \ + --values k8s/code-server-values.yaml \ + --wait + +# Monitor rollout +kubectl rollout status deployment/code-server -n code-server +``` + +### Blue-Green Deployment (EKS) + +```bash +# Create new deployment with different name +helm install code-server-blue ../../ci/helm-chart \ + --namespace code-server \ + --values k8s/code-server-values-blue.yaml + +# Test the new version +kubectl port-forward -n code-server svc/code-server-blue 8081:8080 + +# Switch traffic by updating ingress +kubectl apply -f k8s/ingress-blue.yaml + +# Delete old deployment +helm uninstall code-server-green -n code-server +``` + +### Scaling + +**EC2:** +```bash +# Update desired capacity +terraform apply -var="desired_instances=3" + +# Or use AWS CLI +aws autoscaling set-desired-capacity \ + --auto-scaling-group-name \ + --desired-capacity 3 +``` + +**EKS:** +```bash +# Scale deployment +kubectl scale deployment code-server -n code-server --replicas=3 + +# Or update Helm values +helm upgrade code-server ../../ci/helm-chart \ + --namespace code-server \ + --set replicaCount=3 +``` + +### Rollback + +**EC2:** +```bash +# Terraform doesn't have built-in rollback +# Revert changes in git and reapply +git revert +terraform apply +``` + +**EKS:** +```bash +# Helm rollback +helm rollback code-server -n code-server + +# Kubernetes rollback +kubectl rollout undo deployment/code-server -n code-server +``` + +## Monitoring and Logging + +### CloudWatch Logs + +**EC2:** +```bash +# View logs +aws logs tail /aws/ec2/-code-server --follow + +# Filter logs +aws logs filter-log-events \ + --log-group-name /aws/ec2/-code-server \ + --filter-pattern "ERROR" +``` + +**EKS:** +```bash +# View pod logs +kubectl logs -n code-server -l app.kubernetes.io/name=code-server --tail=100 -f + +# View previous pod logs +kubectl logs -n code-server --previous + +# View OAuth2 Proxy logs +kubectl logs -n code-server -l app=oauth2-proxy --tail=100 -f +``` + +### Metrics + +**EC2:** +```bash +# Auto Scaling Group metrics +aws cloudwatch get-metric-statistics \ + --namespace AWS/EC2 \ + --metric-name CPUUtilization \ + --dimensions Name=AutoScalingGroupName,Value= \ + --start-time 2024-01-01T00:00:00Z \ + --end-time 2024-01-01T23:59:59Z \ + --period 3600 \ + --statistics Average +``` + +**EKS:** +```bash +# Pod metrics (requires metrics-server) +kubectl top pods -n code-server + +# Node metrics +kubectl top nodes + +# View HPA status +kubectl get hpa -n code-server +``` + +### Health Checks + +**EC2:** +```bash +# Check target group health +aws elbv2 describe-target-health \ + --target-group-arn +``` + +**EKS:** +```bash +# Check pod health +kubectl get pods -n code-server +kubectl describe pod -n code-server + +# Check ingress status +kubectl describe ingress -n code-server +``` + +## Troubleshooting + +### Common Issues + +#### EC2: Instances Not Healthy + +```bash +# Check Auto Scaling Group +aws autoscaling describe-auto-scaling-groups \ + --auto-scaling-group-names + +# Check instance logs (via SSM) +aws ssm start-session --target + +# View user-data logs +sudo cat /var/log/cloud-init-output.log + +# Check Docker containers +sudo docker ps +sudo docker logs code-server +sudo docker logs oauth2-proxy +``` + +#### EKS: Pods Not Starting + +```bash +# Check pod status +kubectl describe pod -n code-server + +# Check events +kubectl get events -n code-server --sort-by='.lastTimestamp' + +# Check storage +kubectl get pvc -n code-server +kubectl describe pvc -n code-server + +# Check node resources +kubectl describe node +``` + +#### Authentication Not Working + +```bash +# EC2: Check OAuth2 Proxy logs +aws logs tail /aws/ec2/-code-server \ + --filter-pattern "oauth2-proxy" \ + --follow + +# EKS: Check OAuth2 Proxy logs +kubectl logs -n code-server -l app=oauth2-proxy + +# Verify configuration +# - Redirect URL matches IdP configuration +# - Client ID and secret are correct +# - Issuer URL is accessible +``` + +#### Load Balancer Not Accessible + +```bash +# Check security groups +aws ec2 describe-security-groups --group-ids + +# Check ALB status +aws elbv2 describe-load-balancers + +# Check target health +aws elbv2 describe-target-health --target-group-arn + +# EKS: Check ingress +kubectl describe ingress -n code-server +``` + +## Cost Optimization + +### Single NAT Gateway + +Reduce costs by using a single NAT gateway (not recommended for production): + +```hcl +single_nat_gateway = true +``` + +Savings: ~$32-96/month (depending on region) + +### SPOT Instances (EKS) + +Use SPOT instances for EKS nodes: + +```hcl +capacity_type = "SPOT" +``` + +Savings: Up to 90% on compute costs (with interruption risk) + +### Auto Scaling + +Configure aggressive scale-down policies: + +```hcl +# EC2 +min_instances = 0 # Scale to zero during off-hours + +# EKS +min_nodes = 0 # Requires cluster autoscaler +``` + +### Storage Optimization + +Use GP3 instead of GP2: + +```hcl +ebs_volume_type = "gp3" # EC2 + +# EKS +storageClass: "gp3" # Helm values +``` + +Savings: ~20% on storage costs + +## Cleanup + +### EC2 + +```bash +# Using script +../scripts/destroy-ec2.sh + +# Or manually +cd deployments/ec2 +terraform destroy +``` + +### EKS + +```bash +# Using script +../scripts/destroy-eks.sh + +# Or manually +helm uninstall code-server -n code-server +kubectl delete namespace code-server +cd deployments/eks +terraform destroy +``` + +**Important**: Terraform destroy will remove all resources including: +- EC2 instances / EKS cluster +- Load balancers +- VPC and networking +- KMS keys (after 7-30 day waiting period) +- CloudWatch logs (based on retention settings) + +## Support and Contributing + +For issues or questions: +1. Check the [troubleshooting](#troubleshooting) section +2. Review [code-server documentation](https://coder.com/docs/code-server) +3. Check AWS service health dashboard +4. Review CloudWatch logs + +## License + +This Terraform configuration is provided as-is under the MIT License. + +## Additional Resources + +- [Code-Server Documentation](https://coder.com/docs/code-server) +- [OAuth2 Proxy Documentation](https://oauth2-proxy.github.io/oauth2-proxy/) +- [AWS EKS Best Practices](https://aws.github.io/aws-eks-best-practices/) +- [Terraform AWS Provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) diff --git a/terraform/SAML-SETUP-GUIDE.md b/terraform/SAML-SETUP-GUIDE.md new file mode 100644 index 000000000..cd244faa0 --- /dev/null +++ b/terraform/SAML-SETUP-GUIDE.md @@ -0,0 +1,494 @@ +# SAML/OIDC Authentication Setup Guide + +This guide provides step-by-step instructions for configuring various identity providers with code-server deployment. + +## Table of Contents + +- [Overview](#overview) +- [Okta Setup](#okta-setup) +- [Azure Active Directory Setup](#azure-active-directory-setup) +- [Google Workspace Setup](#google-workspace-setup) +- [AWS IAM Identity Center (SSO) Setup](#aws-iam-identity-center-sso-setup) +- [Generic OIDC Provider](#generic-oidc-provider) +- [Testing Authentication](#testing-authentication) +- [Troubleshooting](#troubleshooting) + +## Overview + +The code-server deployment uses OAuth2 Proxy to provide authentication via SAML/OIDC. This acts as a reverse proxy that handles authentication before requests reach code-server. + +### Key Concepts + +- **OIDC Discovery URL**: Endpoint that provides IdP configuration +- **Client ID**: Unique identifier for your application +- **Client Secret**: Secret key for authentication +- **Redirect URI**: URL where users return after authentication +- **Cookie Secret**: Secret for encrypting session cookies + +## Okta Setup + +### 1. Create Application in Okta + +1. Log in to your Okta admin console +2. Navigate to **Applications** → **Applications** +3. Click **Create App Integration** +4. Select: + - **Sign-in method**: OIDC - OpenID Connect + - **Application type**: Web Application +5. Click **Next** + +### 2. Configure Application + +**General Settings:** +- **App integration name**: Code-Server +- **Logo**: (optional) Upload code-server logo + +**Sign-in redirect URIs:** +``` +https://code-server.example.com/oauth2/callback +``` + +**Sign-out redirect URIs:** +``` +https://code-server.example.com +``` + +**Assignments:** +- **Controlled access**: Choose who can access (Everyone, specific groups, etc.) + +Click **Save** + +### 3. Get Configuration Values + +After creating the application: + +1. Copy **Client ID** +2. Copy **Client Secret** (click "Show" if hidden) +3. Note your Okta domain (e.g., `dev-12345.okta.com`) + +### 4. Configure Terraform Variables + +```hcl +# terraform.tfvars + +oauth2_issuer_url = "https://dev-12345.okta.com/.well-known/openid-configuration" +oauth2_client_id = "" +oauth2_client_secret = "" +oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" + +# Generate cookie secret +# python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())' +oauth2_cookie_secret = "" + +# Optional: Restrict to specific users +oauth2_allowed_emails = [ + "user1@company.com", + "user2@company.com" +] +``` + +### 5. Assign Users + +In Okta admin console: +1. Go to **Applications** → **Code-Server** +2. Click **Assignments** tab +3. Click **Assign** → **Assign to People** or **Assign to Groups** +4. Add users/groups who should have access + +## Azure Active Directory Setup + +### 1. Register Application + +1. Log in to [Azure Portal](https://portal.azure.com) +2. Navigate to **Azure Active Directory** → **App registrations** +3. Click **New registration** + +### 2. Configure Application + +**Name:** Code-Server +**Supported account types:** +- Single tenant (most common) +- Or Multi-tenant if needed + +**Redirect URI:** +- Platform: **Web** +- URL: `https://code-server.example.com/oauth2/callback` + +Click **Register** + +### 3. Configure Authentication + +1. Go to **Authentication** in left menu +2. Under **Implicit grant and hybrid flows**, check: + - ✅ ID tokens (used for implicit and hybrid flows) +3. Under **Advanced settings**: + - Allow public client flows: **No** +4. Click **Save** + +### 4. Create Client Secret + +1. Go to **Certificates & secrets** in left menu +2. Click **New client secret** +3. Description: Code-Server +4. Expires: Choose duration (24 months recommended) +5. Click **Add** +6. **Copy the secret value immediately** (it won't be shown again) + +### 5. API Permissions + +1. Go to **API permissions** in left menu +2. Verify these permissions exist: + - Microsoft Graph → `openid` + - Microsoft Graph → `profile` + - Microsoft Graph → `email` +3. Click **Grant admin consent** (if you have admin rights) + +### 6. Get Configuration Values + +From **Overview** page: +- **Application (client) ID**: Copy this +- **Directory (tenant) ID**: Copy this + +### 7. Configure Terraform Variables + +```hcl +# terraform.tfvars + +oauth2_issuer_url = "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration" +oauth2_client_id = "" +oauth2_client_secret = "" +oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" +oauth2_cookie_secret = "" + +# Optional: Restrict by email +oauth2_allowed_emails = [ + "user1@company.com", + "user2@company.com" +] +``` + +### 8. Restrict Access (Optional) + +To limit access to specific users/groups: + +1. Go to **Enterprise applications** +2. Find your **Code-Server** application +3. Go to **Properties** +4. Set **User assignment required?** to **Yes** +5. Go to **Users and groups** +6. Click **Add user/group** +7. Select users or groups + +## Google Workspace Setup + +### 1. Create OAuth Client + +1. Go to [Google Cloud Console](https://console.cloud.google.com) +2. Select or create a project +3. Navigate to **APIs & Services** → **Credentials** +4. Click **Create Credentials** → **OAuth client ID** + +### 2. Configure OAuth Consent Screen + +If prompted: +1. Click **Configure Consent Screen** +2. User Type: **Internal** (for Google Workspace) or **External** +3. Fill in application information: + - App name: Code-Server + - User support email: Your email + - Developer contact: Your email +4. Scopes: Add `openid`, `email`, `profile` +5. Click **Save and Continue** + +### 3. Create OAuth Client ID + +**Application type:** Web application + +**Name:** Code-Server + +**Authorized redirect URIs:** +``` +https://code-server.example.com/oauth2/callback +``` + +Click **Create** + +### 4. Get Configuration Values + +After creation: +- Copy **Client ID** +- Copy **Client Secret** + +### 5. Configure Terraform Variables + +```hcl +# terraform.tfvars + +oauth2_issuer_url = "https://accounts.google.com/.well-known/openid-configuration" +oauth2_client_id = ".apps.googleusercontent.com" +oauth2_client_secret = "" +oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" +oauth2_cookie_secret = "" + +# Restrict to your domain +oauth2_allowed_emails = [ + "user1@company.com", + "user2@company.com" +] +``` + +### 6. Domain Restriction (Google Workspace) + +To restrict to your entire domain: + +```yaml +# For EKS: k8s/oauth2-proxy.yaml +# Add to ConfigMap: +email_domains = ["company.com"] +``` + +## AWS IAM Identity Center (SSO) Setup + +### 1. Enable IAM Identity Center + +1. Go to [IAM Identity Center](https://console.aws.amazon.com/singlesignon) +2. Enable IAM Identity Center if not already enabled +3. Note your **AWS access portal URL** + +### 2. Register Application + +1. In IAM Identity Center, go to **Applications** +2. Click **Add application** +3. Select **I have an application I want to set up** +4. Click **Next** + +### 3. Configure Application + +**Display name:** Code-Server + +**Description:** Code-Server IDE + +**Application start URL:** `https://code-server.example.com` + +**Application metadata:** +- Choose **Manual entry** +- **Application ACS URL**: `https://code-server.example.com/oauth2/callback` +- **Application SAML audience**: `https://code-server.example.com` + +Click **Submit** + +### 4. Get Configuration Values + +1. Download the **IAM Identity Center SAML metadata file** +2. Note the **Client ID** (from application details) +3. Create a **Client Secret** (in application settings) + +### 5. Configure Terraform Variables + +```hcl +# terraform.tfvars + +# Use OIDC endpoint for your region +oauth2_issuer_url = "https://portal.sso..amazonaws.com/.well-known/openid-configuration" +oauth2_client_id = "" +oauth2_client_secret = "" +oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" +oauth2_cookie_secret = "" +``` + +### 6. Assign Users + +1. Go to **Assigned users** tab +2. Click **Assign users** +3. Select users or groups +4. Click **Assign users** + +## Generic OIDC Provider + +For any OIDC-compliant provider: + +### 1. Required Information + +Obtain from your IdP: +- OIDC Discovery URL (usually `https://idp.example.com/.well-known/openid-configuration`) +- Client ID +- Client Secret +- Supported scopes (typically `openid`, `profile`, `email`) + +### 2. Register Redirect URI + +In your IdP, register: +``` +https://code-server.example.com/oauth2/callback +``` + +### 3. Configure Terraform Variables + +```hcl +# terraform.tfvars + +oauth2_issuer_url = "" +oauth2_client_id = "" +oauth2_client_secret = "" +oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" +oauth2_cookie_secret = "" +``` + +## Testing Authentication + +### 1. Deploy Application + +```bash +# EC2 +cd deployments/ec2 +terraform apply + +# EKS +cd deployments/eks +terraform apply +kubectl apply -f k8s/oauth2-proxy.yaml +``` + +### 2. Access Application + +Navigate to your code-server URL (e.g., `https://code-server.example.com`) + +### 3. Expected Flow + +1. Browser redirects to IdP login page +2. Enter credentials and authenticate +3. IdP redirects back to code-server with authorization code +4. OAuth2 Proxy exchanges code for tokens +5. Session cookie is set +6. Request is proxied to code-server +7. Code-server interface loads + +### 4. Verify Session + +After successful login: +- Check browser cookies for `_oauth2_proxy` cookie +- Cookie should be HttpOnly, Secure, and SameSite + +### 5. Test Logout + +Navigate to: `https://code-server.example.com/oauth2/sign_out` + +You should be logged out and redirected to IdP + +## Troubleshooting + +### Common Issues + +#### Redirect URI Mismatch + +**Error:** `redirect_uri_mismatch` or similar + +**Solution:** +1. Verify redirect URI in IdP exactly matches Terraform configuration +2. Check for trailing slashes (should not have them) +3. Ensure HTTPS (not HTTP) + +#### Invalid Client + +**Error:** `invalid_client` + +**Solution:** +1. Verify Client ID is correct +2. Verify Client Secret is correct and not expired +3. Check that client is enabled in IdP + +#### Cookie Errors + +**Error:** Authentication succeeds but session is not maintained + +**Solution:** +1. Ensure `oauth2_cookie_secret` is set and is 32 bytes (base64 encoded) +2. Verify domain in cookie matches your URL +3. Check browser is accepting cookies +4. Ensure HTTPS is configured (cookies may not work over HTTP) + +#### Access Denied + +**Error:** User authenticates but gets "Access Denied" + +**Solution:** +1. Check `oauth2_allowed_emails` list +2. Verify user is assigned to application in IdP +3. Check OAuth2 Proxy logs: + ```bash + # EC2 + aws logs tail /aws/ec2/-code-server --filter-pattern oauth2-proxy + + # EKS + kubectl logs -n code-server -l app=oauth2-proxy + ``` + +#### Issuer URL Not Accessible + +**Error:** `error fetching OIDC discovery` + +**Solution:** +1. Verify issuer URL is accessible from your network +2. Check security groups allow outbound HTTPS +3. Verify URL is correct (test in browser) +4. Check for typos in URL + +### Debug Mode + +Enable debug logging: + +**EC2:** + +Edit `modules/ec2/user-data.sh` and add to oauth2-proxy args: +```yaml +- --log-level=debug +``` + +**EKS:** + +Edit `deployments/eks/k8s/oauth2-proxy.yaml`: +```yaml +args: + - --log-level=debug +``` + +### Testing Connectivity + +**Test OIDC Discovery:** +```bash +curl -s https://your-idp.com/.well-known/openid-configuration | jq . +``` + +**Test Redirect:** +```bash +# Should show OAuth2 login page +curl -I https://code-server.example.com +``` + +**Check OAuth2 Proxy Health:** +```bash +# EC2 +curl http://:4180/ping + +# EKS +kubectl port-forward -n code-server svc/oauth2-proxy 4180:4180 +curl http://localhost:4180/ping +``` + +## Security Best Practices + +1. **Use HTTPS**: Always use HTTPS for production +2. **Rotate Secrets**: Regularly rotate client secrets and cookie secrets +3. **Limit Scope**: Request only necessary OIDC scopes +4. **Session Timeout**: Configure appropriate session expiry +5. **Restrict Emails**: Use `oauth2_allowed_emails` to limit access +6. **Monitor Logs**: Regularly review authentication logs +7. **Use Groups**: Manage access via IdP groups rather than individual users + +## Additional Resources + +- [OAuth2 Proxy Documentation](https://oauth2-proxy.github.io/oauth2-proxy/) +- [OIDC Specification](https://openid.net/connect/) +- [Okta OIDC Guide](https://developer.okta.com/docs/concepts/oauth-openid/) +- [Azure AD OIDC Guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) +- [Google OIDC Guide](https://developers.google.com/identity/protocols/oauth2/openid-connect) diff --git a/terraform/deployments/ec2/main.tf b/terraform/deployments/ec2/main.tf new file mode 100644 index 000000000..5e7ad96c4 --- /dev/null +++ b/terraform/deployments/ec2/main.tf @@ -0,0 +1,117 @@ +# EC2 Deployment Configuration for Code-Server +# This file creates all necessary infrastructure to deploy code-server on EC2 + +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.5" + } + } + + # Uncomment and configure for remote state storage + # backend "s3" { + # bucket = "your-terraform-state-bucket" + # key = "code-server/ec2/terraform.tfstate" + # region = "us-east-1" + # encrypt = true + # dynamodb_table = "terraform-state-lock" + # } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "code-server" + Environment = var.environment + ManagedBy = "Terraform" + Deployment = "EC2" + } + } +} + +locals { + name_prefix = "${var.project_name}-${var.environment}" + cluster_name = "${local.name_prefix}-eks" # Used for VPC subnet tagging + + common_tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + Deployment = "EC2" + } +} + +# VPC Module +module "vpc" { + source = "../../modules/vpc" + + name_prefix = local.name_prefix + vpc_cidr = var.vpc_cidr + public_subnet_cidrs = var.public_subnet_cidrs + private_subnet_cidrs = var.private_subnet_cidrs + aws_region = var.aws_region + cluster_name = local.cluster_name + enable_nat_gateway = true + single_nat_gateway = var.single_nat_gateway + enable_vpc_endpoints = true + enable_flow_logs = true + flow_logs_retention_days = 30 + + tags = local.common_tags +} + +# Security Module +module "security" { + source = "../../modules/security" + + name_prefix = local.name_prefix + vpc_id = module.vpc.vpc_id + allowed_cidr_blocks = var.allowed_cidr_blocks + ssh_allowed_cidr_blocks = var.ssh_allowed_cidr_blocks + + tags = local.common_tags +} + +# EC2 Module for Code-Server +module "code_server_ec2" { + source = "../../modules/ec2" + + name_prefix = local.name_prefix + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnet_ids + alb_subnet_ids = var.internal_alb ? module.vpc.private_subnet_ids : module.vpc.public_subnet_ids + security_group_id = module.security.code_server_ec2_security_group_id + alb_security_group_id = module.security.alb_security_group_id + iam_instance_profile_name = module.security.code_server_ec2_instance_profile_name + kms_key_arn = module.security.kms_key_arn + aws_region = var.aws_region + + instance_type = var.instance_type + ebs_volume_size = var.ebs_volume_size + min_instances = var.min_instances + max_instances = var.max_instances + desired_instances = var.desired_instances + code_server_version = var.code_server_version + certificate_arn = var.certificate_arn + internal_alb = var.internal_alb + enable_autoscaling = var.enable_autoscaling + + # OAuth2 Proxy Configuration + oauth2_client_id = var.oauth2_client_id + oauth2_client_secret = var.oauth2_client_secret + oauth2_issuer_url = var.oauth2_issuer_url + oauth2_redirect_url = var.oauth2_redirect_url + oauth2_cookie_secret = var.oauth2_cookie_secret + oauth2_allowed_emails = var.oauth2_allowed_emails + + tags = local.common_tags +} diff --git a/terraform/deployments/ec2/outputs.tf b/terraform/deployments/ec2/outputs.tf new file mode 100644 index 000000000..4fdfa9902 --- /dev/null +++ b/terraform/deployments/ec2/outputs.tf @@ -0,0 +1,77 @@ +# EC2 Deployment Outputs + +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +output "private_subnet_ids" { + description = "IDs of private subnets" + value = module.vpc.private_subnet_ids +} + +output "public_subnet_ids" { + description = "IDs of public subnets" + value = module.vpc.public_subnet_ids +} + +output "alb_dns_name" { + description = "DNS name of the Application Load Balancer" + value = module.code_server_ec2.alb_dns_name +} + +output "alb_url" { + description = "URL to access Code-Server" + value = var.certificate_arn != "" ? "https://${module.code_server_ec2.alb_dns_name}" : "http://${module.code_server_ec2.alb_dns_name}" +} + +output "code_server_password_secret_arn" { + description = "ARN of the Secrets Manager secret containing code-server password" + value = module.code_server_ec2.code_server_password_secret_arn +} + +output "autoscaling_group_name" { + description = "Name of the Auto Scaling Group" + value = module.code_server_ec2.autoscaling_group_name +} + +output "kms_key_arn" { + description = "ARN of the KMS key for encryption" + value = module.security.kms_key_arn +} + +output "next_steps" { + description = "Next steps to complete the setup" + value = <<-EOT + + Code-Server EC2 Deployment Complete! + + Next Steps: + 1. Access Code-Server at: ${var.certificate_arn != "" ? "https" : "http"}://${module.code_server_ec2.alb_dns_name} + + 2. Get the code-server password: + aws secretsmanager get-secret-value \ + --secret-id ${module.code_server_ec2.code_server_password_secret_arn} \ + --region ${var.aws_region} \ + --query SecretString \ + --output text + + 3. Configure DNS (if using custom domain): + - Create a CNAME record pointing to: ${module.code_server_ec2.alb_dns_name} + - Update oauth2_redirect_url with your domain + + 4. Monitor the deployment: + - CloudWatch Logs: /aws/ec2/${local.name_prefix}-code-server + - Auto Scaling Group: ${module.code_server_ec2.autoscaling_group_name} + + 5. For SAML/OIDC authentication: + - Ensure your IdP is configured with the redirect URL: ${var.oauth2_redirect_url} + - Verify allowed email addresses are configured + + Security Notes: + - All instances are in private subnets + - ALB is ${var.internal_alb ? "internal (private network only)" : "public"} + - Data is encrypted at rest using KMS + - VPC Flow Logs are enabled for monitoring + EOT +} diff --git a/terraform/deployments/ec2/terraform.tfvars.example b/terraform/deployments/ec2/terraform.tfvars.example new file mode 100644 index 000000000..88af8886a --- /dev/null +++ b/terraform/deployments/ec2/terraform.tfvars.example @@ -0,0 +1,47 @@ +# Example Terraform Variables for EC2 Deployment +# Copy this file to terraform.tfvars and fill in your values + +aws_region = "us-east-1" +project_name = "code-server" +environment = "dev" + +# VPC Configuration +vpc_cidr = "10.0.0.0/16" +public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] +single_nat_gateway = false # Set to true for cost optimization (single NAT gateway) + +# Security Configuration +allowed_cidr_blocks = ["10.0.0.0/8"] # Restrict to private network +ssh_allowed_cidr_blocks = [] # No SSH access (use SSM instead) + +# EC2 Configuration +instance_type = "t3.medium" +ebs_volume_size = 50 +min_instances = 1 +max_instances = 3 +desired_instances = 1 +code_server_version = "latest" +enable_autoscaling = true + +# Load Balancer Configuration +# Get certificate ARN from ACM or leave empty for HTTP +certificate_arn = "" # Example: "arn:aws:acm:us-east-1:123456789012:certificate/xxxxx" +internal_alb = true # Set to false for internet-facing ALB + +# OAuth2 / SAML Configuration +# Configure these values based on your IdP (Okta, Azure AD, etc.) +oauth2_client_id = "your-client-id-from-idp" +oauth2_client_secret = "your-client-secret-from-idp" +oauth2_issuer_url = "https://your-idp.com/.well-known/openid-configuration" +oauth2_redirect_url = "https://code-server.example.com/oauth2/callback" + +# Generate cookie secret with: +# python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())' +oauth2_cookie_secret = "generate-random-secret-here" + +# Allowed email addresses (leave empty to allow all authenticated users) +oauth2_allowed_emails = [ + # "user1@example.com", + # "user2@example.com" +] diff --git a/terraform/deployments/ec2/variables.tf b/terraform/deployments/ec2/variables.tf new file mode 100644 index 000000000..cddf70ce9 --- /dev/null +++ b/terraform/deployments/ec2/variables.tf @@ -0,0 +1,147 @@ +# EC2 Deployment Variables + +variable "aws_region" { + description = "AWS region for deployment" + type = string + default = "us-east-1" +} + +variable "project_name" { + description = "Project name" + type = string + default = "code-server" +} + +variable "environment" { + description = "Environment name (dev, staging, prod)" + type = string + default = "dev" +} + +# VPC Configuration +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "public_subnet_cidrs" { + description = "CIDR blocks for public subnets" + type = list(string) + default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +} + +variable "private_subnet_cidrs" { + description = "CIDR blocks for private subnets" + type = list(string) + default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] +} + +variable "single_nat_gateway" { + description = "Use a single NAT gateway (cost optimization)" + type = bool + default = false +} + +# Security Configuration +variable "allowed_cidr_blocks" { + description = "CIDR blocks allowed to access the ALB" + type = list(string) + default = ["10.0.0.0/8"] # Restrict to private network +} + +variable "ssh_allowed_cidr_blocks" { + description = "CIDR blocks allowed to SSH into instances" + type = list(string) + default = [] # No SSH access by default +} + +# EC2 Configuration +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.medium" +} + +variable "ebs_volume_size" { + description = "Size of EBS volume in GB" + type = number + default = 50 +} + +variable "min_instances" { + description = "Minimum number of instances" + type = number + default = 1 +} + +variable "max_instances" { + description = "Maximum number of instances" + type = number + default = 3 +} + +variable "desired_instances" { + description = "Desired number of instances" + type = number + default = 1 +} + +variable "code_server_version" { + description = "Version of code-server Docker image" + type = string + default = "latest" +} + +variable "enable_autoscaling" { + description = "Enable auto scaling" + type = bool + default = true +} + +# Load Balancer Configuration +variable "certificate_arn" { + description = "ARN of ACM certificate for HTTPS (leave empty to use HTTP)" + type = string + default = "" +} + +variable "internal_alb" { + description = "Whether the ALB should be internal (private network only)" + type = bool + default = true +} + +# OAuth2 / SAML Configuration +variable "oauth2_client_id" { + description = "OAuth2 client ID from your SAML/OIDC provider" + type = string +} + +variable "oauth2_client_secret" { + description = "OAuth2 client secret from your SAML/OIDC provider" + type = string + sensitive = true +} + +variable "oauth2_issuer_url" { + description = "OAuth2 issuer URL (OIDC discovery endpoint)" + type = string +} + +variable "oauth2_redirect_url" { + description = "OAuth2 redirect URL (https://your-domain.com/oauth2/callback)" + type = string +} + +variable "oauth2_cookie_secret" { + description = "OAuth2 cookie secret (generate with: python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')" + type = string + sensitive = true +} + +variable "oauth2_allowed_emails" { + description = "List of allowed email addresses (leave empty to allow all)" + type = list(string) + default = [] +} diff --git a/terraform/deployments/eks/k8s/code-server-values.yaml b/terraform/deployments/eks/k8s/code-server-values.yaml new file mode 100644 index 000000000..128f7ae5e --- /dev/null +++ b/terraform/deployments/eks/k8s/code-server-values.yaml @@ -0,0 +1,144 @@ +# Helm values for Code-Server deployment on EKS +# This file should be customized before deployment + +replicaCount: 1 + +image: + repository: codercom/code-server + tag: "4.18.0" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +nameOverride: "" +fullnameOverride: "code-server" + +serviceAccount: + create: true + annotations: {} + name: "code-server" + +podAnnotations: {} + +podSecurityContext: + fsGroup: 1000 + runAsUser: 1000 + runAsNonRoot: true + +securityContext: + enabled: true + fsGroup: 1000 + runAsUser: 1000 + runAsNonRoot: true + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false + allowPrivilegeEscalation: false + +service: + type: ClusterIP + port: 8080 + +# Ingress configuration with AWS Load Balancer Controller +ingress: + enabled: true + ingressClassName: "alb" + annotations: + alb.ingress.kubernetes.io/scheme: internal + alb.ingress.kubernetes.io/target-type: ip + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' + alb.ingress.kubernetes.io/ssl-redirect: '443' + alb.ingress.kubernetes.io/healthcheck-path: /healthz + alb.ingress.kubernetes.io/healthcheck-protocol: HTTP + alb.ingress.kubernetes.io/healthcheck-interval-seconds: '30' + alb.ingress.kubernetes.io/healthcheck-timeout-seconds: '5' + alb.ingress.kubernetes.io/healthy-threshold-count: '2' + alb.ingress.kubernetes.io/unhealthy-threshold-count: '2' + # Uncomment and set your certificate ARN + # alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:region:account:certificate/xxxxx + # Uncomment to set custom subnets + # alb.ingress.kubernetes.io/subnets: subnet-xxxxx,subnet-yyyyy + # Uncomment to set custom security groups + # alb.ingress.kubernetes.io/security-groups: sg-xxxxx + hosts: + - host: code-server.example.com + paths: + - path: / + pathType: Prefix + tls: [] + +resources: + limits: + cpu: 2000m + memory: 4Gi + requests: + cpu: 500m + memory: 1Gi + +# Persistent storage configuration +persistence: + enabled: true + storageClass: "gp3" + accessMode: ReadWriteOnce + size: 20Gi + annotations: {} + +# Volume permissions init container +volumePermissions: + enabled: true + securityContext: + runAsUser: 0 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Extra environment variables +extraVars: + - name: DISABLE_TELEMETRY + value: "true" + +# Extra arguments for code-server +extraArgs: [] + +# Lifecycle hooks +lifecycle: + enabled: false + +# Extra init containers (e.g., for installing extensions) +extraInitContainers: | + # - name: install-extensions + # image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + # imagePullPolicy: IfNotPresent + # env: + # - name: SERVICE_URL + # value: https://open-vsx.org/vscode/gallery + # - name: ITEM_URL + # value: https://open-vsx.org/vscode/item + # command: + # - sh + # - -c + # - | + # code-server --install-extension ms-python.python + # code-server --install-extension golang.Go + # volumeMounts: + # - name: data + # mountPath: /home/coder + +# Extra containers (e.g., OAuth2 Proxy sidecar) +extraContainers: "" + +# Extra secret mounts +extraSecretMounts: [] + +# Extra volume mounts +extraVolumeMounts: [] + +# Extra configmap mounts +extraConfigmapMounts: [] + +# Extra ports +extraPorts: [] diff --git a/terraform/deployments/eks/k8s/oauth2-proxy.yaml b/terraform/deployments/eks/k8s/oauth2-proxy.yaml new file mode 100644 index 000000000..0da9233dd --- /dev/null +++ b/terraform/deployments/eks/k8s/oauth2-proxy.yaml @@ -0,0 +1,224 @@ +# OAuth2 Proxy deployment for SAML authentication +# This provides authentication layer for Code-Server on EKS +--- +apiVersion: v1 +kind: Namespace +metadata: + name: code-server +--- +apiVersion: v1 +kind: Secret +metadata: + name: oauth2-proxy-secrets + namespace: code-server +type: Opaque +stringData: + client-id: "YOUR_SAML_CLIENT_ID" + client-secret: "YOUR_SAML_CLIENT_SECRET" + cookie-secret: "GENERATE_WITH_python_-c_import_os_base64_print_base64_urlsafe_b64encode_os_urandom_32_decode" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: oauth2-proxy-config + namespace: code-server +data: + oauth2_proxy.cfg: | + provider = "oidc" + provider_display_name = "SSO" + redirect_url = "https://code-server.example.com/oauth2/callback" + oidc_issuer_url = "https://your-saml-idp.com" + upstreams = "http://code-server:8080" + email_domains = ["*"] + cookie_secure = true + cookie_httponly = true + cookie_samesite = "lax" + cookie_refresh = "1h" + cookie_expire = "24h" + set_xauthrequest = true + pass_access_token = true + pass_authorization_header = true + set_authorization_header = true + skip_provider_button = false + whitelist_domains = [".example.com"] +--- +apiVersion: v1 +kind: Service +metadata: + name: oauth2-proxy + namespace: code-server + labels: + app: oauth2-proxy +spec: + type: ClusterIP + ports: + - name: http + port: 4180 + targetPort: 4180 + protocol: TCP + selector: + app: oauth2-proxy +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oauth2-proxy + namespace: code-server + labels: + app: oauth2-proxy +spec: + replicas: 2 + selector: + matchLabels: + app: oauth2-proxy + template: + metadata: + labels: + app: oauth2-proxy + spec: + serviceAccountName: oauth2-proxy + securityContext: + runAsNonRoot: true + runAsUser: 2000 + fsGroup: 2000 + seccompProfile: + type: RuntimeDefault + containers: + - name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v7.5.1 + imagePullPolicy: IfNotPresent + args: + - --config=/etc/oauth2-proxy/oauth2_proxy.cfg + - --http-address=0.0.0.0:4180 + env: + - name: OAUTH2_PROXY_CLIENT_ID + valueFrom: + secretKeyRef: + name: oauth2-proxy-secrets + key: client-id + - name: OAUTH2_PROXY_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oauth2-proxy-secrets + key: client-secret + - name: OAUTH2_PROXY_COOKIE_SECRET + valueFrom: + secretKeyRef: + name: oauth2-proxy-secrets + key: cookie-secret + ports: + - name: http + containerPort: 4180 + protocol: TCP + livenessProbe: + httpGet: + path: /ping + port: http + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 1 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /ping + port: http + scheme: HTTP + initialDelaySeconds: 5 + timeoutSeconds: 1 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - name: config + mountPath: /etc/oauth2-proxy + readOnly: true + volumes: + - name: config + configMap: + name: oauth2-proxy-config +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: oauth2-proxy + namespace: code-server +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: oauth2-proxy + namespace: code-server + annotations: + alb.ingress.kubernetes.io/scheme: internal + alb.ingress.kubernetes.io/target-type: ip + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' + alb.ingress.kubernetes.io/ssl-redirect: '443' + alb.ingress.kubernetes.io/healthcheck-path: /ping + alb.ingress.kubernetes.io/healthcheck-protocol: HTTP + # Uncomment and set your certificate ARN + # alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:region:account:certificate/xxxxx +spec: + ingressClassName: alb + rules: + - host: code-server.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: oauth2-proxy + port: + number: 4180 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: oauth2-proxy + namespace: code-server +spec: + minAvailable: 1 + selector: + matchLabels: + app: oauth2-proxy +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: oauth2-proxy + namespace: code-server +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: oauth2-proxy + minReplicas: 2 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 75 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 diff --git a/terraform/deployments/eks/main.tf b/terraform/deployments/eks/main.tf new file mode 100644 index 000000000..9e4fb3a17 --- /dev/null +++ b/terraform/deployments/eks/main.tf @@ -0,0 +1,245 @@ +# EKS Deployment Configuration for Code-Server +# This file creates all necessary infrastructure to deploy code-server on EKS + +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.23" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.11" + } + } + + # Uncomment and configure for remote state storage + # backend "s3" { + # bucket = "your-terraform-state-bucket" + # key = "code-server/eks/terraform.tfstate" + # region = "us-east-1" + # encrypt = true + # dynamodb_table = "terraform-state-lock" + # } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "code-server" + Environment = var.environment + ManagedBy = "Terraform" + Deployment = "EKS" + } + } +} + +locals { + name_prefix = "${var.project_name}-${var.environment}" + cluster_name = "${local.name_prefix}-eks" + + common_tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + Deployment = "EKS" + } +} + +# VPC Module +module "vpc" { + source = "../../modules/vpc" + + name_prefix = local.name_prefix + vpc_cidr = var.vpc_cidr + public_subnet_cidrs = var.public_subnet_cidrs + private_subnet_cidrs = var.private_subnet_cidrs + aws_region = var.aws_region + cluster_name = local.cluster_name + enable_nat_gateway = true + single_nat_gateway = var.single_nat_gateway + enable_vpc_endpoints = true + enable_flow_logs = true + flow_logs_retention_days = 30 + + tags = local.common_tags +} + +# Security Module +module "security" { + source = "../../modules/security" + + name_prefix = local.name_prefix + vpc_id = module.vpc.vpc_id + allowed_cidr_blocks = var.allowed_cidr_blocks + ssh_allowed_cidr_blocks = var.ssh_allowed_cidr_blocks + + tags = local.common_tags +} + +# EKS Module +module "eks" { + source = "../../modules/eks" + + cluster_name = local.cluster_name + cluster_role_arn = module.security.eks_cluster_iam_role_arn + node_role_arn = module.security.eks_nodes_iam_role_arn + private_subnet_ids = module.vpc.private_subnet_ids + public_subnet_ids = module.vpc.public_subnet_ids + cluster_security_group_id = module.security.eks_cluster_security_group_id + kms_key_arn = module.security.kms_key_arn + + kubernetes_version = var.kubernetes_version + endpoint_public_access = var.endpoint_public_access + public_access_cidrs = var.public_access_cidrs + + node_instance_types = var.node_instance_types + capacity_type = var.capacity_type + node_disk_size = var.node_disk_size + min_nodes = var.min_nodes + max_nodes = var.max_nodes + desired_nodes = var.desired_nodes + + enable_ebs_csi_driver = true + enable_irsa = true + enable_aws_load_balancer_controller = true + + tags = local.common_tags +} + +# Configure Kubernetes provider +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = [ + "eks", + "get-token", + "--cluster-name", + module.eks.cluster_id, + "--region", + var.aws_region + ] + } +} + +# Configure Helm provider +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = [ + "eks", + "get-token", + "--cluster-name", + module.eks.cluster_id, + "--region", + var.aws_region + ] + } + } +} + +# Install AWS Load Balancer Controller +resource "helm_release" "aws_load_balancer_controller" { + name = "aws-load-balancer-controller" + repository = "https://aws.github.io/eks-charts" + chart = "aws-load-balancer-controller" + namespace = "kube-system" + version = "1.6.2" + + set { + name = "clusterName" + value = module.eks.cluster_id + } + + set { + name = "serviceAccount.create" + value = "true" + } + + set { + name = "serviceAccount.name" + value = "aws-load-balancer-controller" + } + + set { + name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" + value = module.eks.aws_load_balancer_controller_role_arn + } + + set { + name = "region" + value = var.aws_region + } + + set { + name = "vpcId" + value = module.vpc.vpc_id + } + + depends_on = [module.eks] +} + +# Create namespace for code-server +resource "kubernetes_namespace" "code_server" { + metadata { + name = "code-server" + labels = { + name = "code-server" + } + } + + depends_on = [module.eks] +} + +# Create secret for OAuth2 Proxy +resource "kubernetes_secret" "oauth2_proxy" { + count = var.deploy_oauth2_proxy ? 1 : 0 + + metadata { + name = "oauth2-proxy-secrets" + namespace = kubernetes_namespace.code_server.metadata[0].name + } + + data = { + client-id = var.oauth2_client_id + client-secret = var.oauth2_client_secret + cookie-secret = var.oauth2_cookie_secret + } + + type = "Opaque" +} + +# Storage Class for EBS GP3 +resource "kubernetes_storage_class" "gp3" { + metadata { + name = "gp3" + } + + storage_provisioner = "ebs.csi.aws.com" + volume_binding_mode = "WaitForFirstConsumer" + + parameters = { + type = "gp3" + encrypted = "true" + kmsKeyId = module.security.kms_key_arn + } + + depends_on = [module.eks] +} diff --git a/terraform/deployments/eks/outputs.tf b/terraform/deployments/eks/outputs.tf new file mode 100644 index 000000000..92e4f93af --- /dev/null +++ b/terraform/deployments/eks/outputs.tf @@ -0,0 +1,110 @@ +# EKS Deployment Outputs + +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +output "private_subnet_ids" { + description = "IDs of private subnets" + value = module.vpc.private_subnet_ids +} + +output "public_subnet_ids" { + description = "IDs of public subnets" + value = module.vpc.public_subnet_ids +} + +output "eks_cluster_id" { + description = "ID of the EKS cluster" + value = module.eks.cluster_id +} + +output "eks_cluster_endpoint" { + description = "Endpoint of the EKS cluster" + value = module.eks.cluster_endpoint +} + +output "eks_cluster_arn" { + description = "ARN of the EKS cluster" + value = module.eks.cluster_arn +} + +output "eks_cluster_oidc_issuer_url" { + description = "OIDC issuer URL of the EKS cluster" + value = module.eks.cluster_oidc_issuer_url +} + +output "kms_key_arn" { + description = "ARN of the KMS key for encryption" + value = module.security.kms_key_arn +} + +output "configure_kubectl" { + description = "Command to configure kubectl" + value = "aws eks update-kubeconfig --region ${var.aws_region} --name ${module.eks.cluster_id}" +} + +output "next_steps" { + description = "Next steps to complete the setup" + value = <<-EOT + + Code-Server EKS Deployment Complete! + + Next Steps: + + 1. Configure kubectl to access the cluster: + ${join("\n ", [ + "aws eks update-kubeconfig --region ${var.aws_region} --name ${module.eks.cluster_id}", + "kubectl get nodes # Verify nodes are ready" + ])} + + 2. Deploy Code-Server using Helm: + ${join("\n ", [ + "cd k8s", + "# Edit code-server-values.yaml with your configuration", + "helm upgrade --install code-server ../../ci/helm-chart \\", + " --namespace code-server \\", + " --create-namespace \\", + " --values code-server-values.yaml" + ])} + + 3. (Optional) Deploy OAuth2 Proxy for SAML authentication: + ${join("\n ", [ + "# Edit k8s/oauth2-proxy.yaml with your SAML/OIDC configuration", + "kubectl apply -f k8s/oauth2-proxy.yaml" + ])} + + 4. Get the Load Balancer URL: + ${join("\n ", [ + "kubectl get ingress -n code-server", + "# Wait for ADDRESS to be populated", + "# The URL will be in the format: xxxxx.region.elb.amazonaws.com" + ])} + + 5. Configure DNS (if using custom domain): + ${join("\n ", [ + "# Create a CNAME record pointing to the ALB DNS name", + "# Update the ingress configuration with your domain" + ])} + + 6. Monitor the deployment: + ${join("\n ", [ + "kubectl get pods -n code-server", + "kubectl logs -n code-server -l app.kubernetes.io/name=code-server", + "kubectl describe ingress -n code-server" + ])} + + Security Notes: + - All worker nodes are in private subnets + - EKS API endpoint is ${var.endpoint_public_access ? "public" : "private"} + - Data is encrypted at rest using KMS + - VPC Flow Logs are enabled for monitoring + - IRSA (IAM Roles for Service Accounts) is enabled + + Useful Commands: + - Scale nodes: kubectl scale deployment code-server -n code-server --replicas=3 + - View logs: kubectl logs -n code-server -f deployment/code-server + - Port forward (testing): kubectl port-forward -n code-server svc/code-server 8080:8080 + EOT +} diff --git a/terraform/deployments/eks/terraform.tfvars.example b/terraform/deployments/eks/terraform.tfvars.example new file mode 100644 index 000000000..80273bec8 --- /dev/null +++ b/terraform/deployments/eks/terraform.tfvars.example @@ -0,0 +1,38 @@ +# Example Terraform Variables for EKS Deployment +# Copy this file to terraform.tfvars and fill in your values + +aws_region = "us-east-1" +project_name = "code-server" +environment = "dev" + +# VPC Configuration +vpc_cidr = "10.0.0.0/16" +public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] +single_nat_gateway = false # Set to true for cost optimization + +# Security Configuration +allowed_cidr_blocks = ["10.0.0.0/8"] # Restrict to private network +ssh_allowed_cidr_blocks = [] # No SSH access (use SSM instead) + +# EKS Configuration +kubernetes_version = "1.28" +endpoint_public_access = false # Set to true if you need public API access +public_access_cidrs = ["0.0.0.0/0"] # Restrict this if endpoint_public_access is true + +# Node Group Configuration +node_instance_types = ["t3.medium"] +capacity_type = "ON_DEMAND" # or "SPOT" for cost optimization +node_disk_size = 50 +min_nodes = 1 +max_nodes = 3 +desired_nodes = 2 + +# OAuth2 / SAML Configuration +deploy_oauth2_proxy = true +oauth2_client_id = "your-client-id-from-idp" +oauth2_client_secret = "your-client-secret-from-idp" + +# Generate cookie secret with: +# python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())' +oauth2_cookie_secret = "generate-random-secret-here" diff --git a/terraform/deployments/eks/variables.tf b/terraform/deployments/eks/variables.tf new file mode 100644 index 000000000..6adc6a5ce --- /dev/null +++ b/terraform/deployments/eks/variables.tf @@ -0,0 +1,139 @@ +# EKS Deployment Variables + +variable "aws_region" { + description = "AWS region for deployment" + type = string + default = "us-east-1" +} + +variable "project_name" { + description = "Project name" + type = string + default = "code-server" +} + +variable "environment" { + description = "Environment name (dev, staging, prod)" + type = string + default = "dev" +} + +# VPC Configuration +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "public_subnet_cidrs" { + description = "CIDR blocks for public subnets" + type = list(string) + default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +} + +variable "private_subnet_cidrs" { + description = "CIDR blocks for private subnets" + type = list(string) + default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] +} + +variable "single_nat_gateway" { + description = "Use a single NAT gateway (cost optimization)" + type = bool + default = false +} + +# Security Configuration +variable "allowed_cidr_blocks" { + description = "CIDR blocks allowed to access the ALB" + type = list(string) + default = ["10.0.0.0/8"] # Restrict to private network +} + +variable "ssh_allowed_cidr_blocks" { + description = "CIDR blocks allowed to SSH into nodes" + type = list(string) + default = [] # No SSH access by default +} + +# EKS Configuration +variable "kubernetes_version" { + description = "Kubernetes version for EKS cluster" + type = string + default = "1.28" +} + +variable "endpoint_public_access" { + description = "Enable public access to EKS API endpoint" + type = bool + default = false +} + +variable "public_access_cidrs" { + description = "CIDR blocks allowed to access EKS API endpoint" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "node_instance_types" { + description = "Instance types for EKS nodes" + type = list(string) + default = ["t3.medium"] +} + +variable "capacity_type" { + description = "Capacity type for EKS nodes (ON_DEMAND or SPOT)" + type = string + default = "ON_DEMAND" +} + +variable "node_disk_size" { + description = "Disk size for EKS nodes in GB" + type = number + default = 50 +} + +variable "min_nodes" { + description = "Minimum number of EKS nodes" + type = number + default = 1 +} + +variable "max_nodes" { + description = "Maximum number of EKS nodes" + type = number + default = 3 +} + +variable "desired_nodes" { + description = "Desired number of EKS nodes" + type = number + default = 2 +} + +# OAuth2 / SAML Configuration +variable "deploy_oauth2_proxy" { + description = "Deploy OAuth2 Proxy for authentication" + type = bool + default = true +} + +variable "oauth2_client_id" { + description = "OAuth2 client ID from your SAML/OIDC provider" + type = string + default = "" +} + +variable "oauth2_client_secret" { + description = "OAuth2 client secret from your SAML/OIDC provider" + type = string + sensitive = true + default = "" +} + +variable "oauth2_cookie_secret" { + description = "OAuth2 cookie secret (generate with: python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')" + type = string + sensitive = true + default = "" +} diff --git a/terraform/modules/ec2/main.tf b/terraform/modules/ec2/main.tf new file mode 100644 index 000000000..99e639a7e --- /dev/null +++ b/terraform/modules/ec2/main.tf @@ -0,0 +1,346 @@ +# EC2 Module for Code-Server Deployment +# Deploys code-server on EC2 instances with Auto Scaling, ALB, and OAuth2 Proxy + +# Get latest Amazon Linux 2023 AMI +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["al2023-ami-*-x86_64"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +# Generate random password for code-server if not provided +resource "random_password" "code_server" { + count = var.code_server_password == "" ? 1 : 0 + length = 32 + special = true +} + +# Store password in AWS Secrets Manager +resource "aws_secretsmanager_secret" "code_server_password" { + name = "${var.name_prefix}-code-server-password" + description = "Code-Server password" + recovery_window_in_days = 7 + + tags = var.tags +} + +resource "aws_secretsmanager_secret_version" "code_server_password" { + secret_id = aws_secretsmanager_secret.code_server_password.id + secret_string = var.code_server_password != "" ? var.code_server_password : random_password.code_server[0].result +} + +# User data script for EC2 instances +locals { + user_data = templatefile("${path.module}/user-data.sh", { + code_server_version = var.code_server_version + region = var.aws_region + secret_name = aws_secretsmanager_secret.code_server_password.name + oauth2_client_id = var.oauth2_client_id + oauth2_client_secret = var.oauth2_client_secret + oauth2_issuer_url = var.oauth2_issuer_url + oauth2_redirect_url = var.oauth2_redirect_url + cookie_secret = var.oauth2_cookie_secret + allowed_emails = join(",", var.oauth2_allowed_emails) + }) +} + +# Launch Template +resource "aws_launch_template" "code_server" { + name_prefix = "${var.name_prefix}-code-server-" + image_id = var.ami_id != "" ? var.ami_id : data.aws_ami.amazon_linux.id + instance_type = var.instance_type + + iam_instance_profile { + name = var.iam_instance_profile_name + } + + vpc_security_group_ids = [var.security_group_id] + + user_data = base64encode(local.user_data) + + block_device_mappings { + device_name = "/dev/xvda" + + ebs { + volume_size = var.ebs_volume_size + volume_type = var.ebs_volume_type + encrypted = true + kms_key_id = var.kms_key_arn + delete_on_termination = true + } + } + + metadata_options { + http_endpoint = "enabled" + http_tokens = "required" + http_put_response_hop_limit = 1 + instance_metadata_tags = "enabled" + } + + monitoring { + enabled = true + } + + tag_specifications { + resource_type = "instance" + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-code-server" + } + ) + } + + tag_specifications { + resource_type = "volume" + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-code-server-volume" + } + ) + } + + lifecycle { + create_before_destroy = true + } + + tags = var.tags +} + +# Auto Scaling Group +resource "aws_autoscaling_group" "code_server" { + name = "${var.name_prefix}-code-server-asg" + vpc_zone_identifier = var.subnet_ids + target_group_arns = [aws_lb_target_group.code_server.arn, aws_lb_target_group.oauth2_proxy.arn] + health_check_type = "ELB" + health_check_grace_period = 300 + min_size = var.min_instances + max_size = var.max_instances + desired_capacity = var.desired_instances + + launch_template { + id = aws_launch_template.code_server.id + version = "$Latest" + } + + tag { + key = "Name" + value = "${var.name_prefix}-code-server" + propagate_at_launch = true + } + + dynamic "tag" { + for_each = var.tags + content { + key = tag.key + value = tag.value + propagate_at_launch = true + } + } + + lifecycle { + create_before_destroy = true + ignore_changes = [desired_capacity] + } +} + +# Application Load Balancer +resource "aws_lb" "code_server" { + name = "${var.name_prefix}-code-server-alb" + internal = var.internal_alb + load_balancer_type = "application" + security_groups = [var.alb_security_group_id] + subnets = var.alb_subnet_ids + + enable_deletion_protection = var.enable_deletion_protection + enable_http2 = true + enable_cross_zone_load_balancing = true + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-code-server-alb" + } + ) +} + +# Target Group for OAuth2 Proxy +resource "aws_lb_target_group" "oauth2_proxy" { + name = "${var.name_prefix}-oauth2-tg" + port = 4180 + protocol = "HTTP" + vpc_id = var.vpc_id + + health_check { + enabled = true + healthy_threshold = 2 + unhealthy_threshold = 2 + timeout = 5 + interval = 30 + path = "/ping" + matcher = "200" + } + + deregistration_delay = 30 + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-oauth2-tg" + } + ) +} + +# Target Group for Code-Server +resource "aws_lb_target_group" "code_server" { + name = "${var.name_prefix}-code-server-tg" + port = 8080 + protocol = "HTTP" + vpc_id = var.vpc_id + + health_check { + enabled = true + healthy_threshold = 2 + unhealthy_threshold = 2 + timeout = 5 + interval = 30 + path = "/healthz" + matcher = "200" + } + + deregistration_delay = 30 + + stickiness { + type = "lb_cookie" + cookie_duration = 86400 + enabled = true + } + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-code-server-tg" + } + ) +} + +# HTTPS Listener (primary) +resource "aws_lb_listener" "https" { + count = var.certificate_arn != "" ? 1 : 0 + load_balancer_arn = aws_lb.code_server.arn + port = "443" + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" + certificate_arn = var.certificate_arn + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.oauth2_proxy.arn + } + + tags = var.tags +} + +# HTTP Listener (redirect to HTTPS) +resource "aws_lb_listener" "http" { + load_balancer_arn = aws_lb.code_server.arn + port = "80" + protocol = "HTTP" + + default_action { + type = var.certificate_arn != "" ? "redirect" : "forward" + + dynamic "redirect" { + for_each = var.certificate_arn != "" ? [1] : [] + content { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } + + target_group_arn = var.certificate_arn == "" ? aws_lb_target_group.oauth2_proxy.arn : null + } + + tags = var.tags +} + +# CloudWatch Log Group for Code-Server +resource "aws_cloudwatch_log_group" "code_server" { + name = "/aws/ec2/${var.name_prefix}-code-server" + retention_in_days = var.log_retention_days + + tags = var.tags +} + +# Auto Scaling Policies +resource "aws_autoscaling_policy" "scale_up" { + count = var.enable_autoscaling ? 1 : 0 + name = "${var.name_prefix}-code-server-scale-up" + autoscaling_group_name = aws_autoscaling_group.code_server.name + adjustment_type = "ChangeInCapacity" + scaling_adjustment = 1 + cooldown = 300 +} + +resource "aws_autoscaling_policy" "scale_down" { + count = var.enable_autoscaling ? 1 : 0 + name = "${var.name_prefix}-code-server-scale-down" + autoscaling_group_name = aws_autoscaling_group.code_server.name + adjustment_type = "ChangeInCapacity" + scaling_adjustment = -1 + cooldown = 300 +} + +# CloudWatch Alarms for Auto Scaling +resource "aws_cloudwatch_metric_alarm" "cpu_high" { + count = var.enable_autoscaling ? 1 : 0 + alarm_name = "${var.name_prefix}-code-server-cpu-high" + comparison_operator = "GreaterThanThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "300" + statistic = "Average" + threshold = "80" + + dimensions = { + AutoScalingGroupName = aws_autoscaling_group.code_server.name + } + + alarm_description = "This metric monitors ec2 cpu utilization" + alarm_actions = [aws_autoscaling_policy.scale_up[0].arn] + + tags = var.tags +} + +resource "aws_cloudwatch_metric_alarm" "cpu_low" { + count = var.enable_autoscaling ? 1 : 0 + alarm_name = "${var.name_prefix}-code-server-cpu-low" + comparison_operator = "LessThanThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "300" + statistic = "Average" + threshold = "20" + + dimensions = { + AutoScalingGroupName = aws_autoscaling_group.code_server.name + } + + alarm_description = "This metric monitors ec2 cpu utilization" + alarm_actions = [aws_autoscaling_policy.scale_down[0].arn] + + tags = var.tags +} diff --git a/terraform/modules/ec2/outputs.tf b/terraform/modules/ec2/outputs.tf new file mode 100644 index 000000000..a4857caf9 --- /dev/null +++ b/terraform/modules/ec2/outputs.tf @@ -0,0 +1,41 @@ +# EC2 Module Outputs + +output "alb_dns_name" { + description = "DNS name of the Application Load Balancer" + value = aws_lb.code_server.dns_name +} + +output "alb_arn" { + description = "ARN of the Application Load Balancer" + value = aws_lb.code_server.arn +} + +output "alb_zone_id" { + description = "Zone ID of the Application Load Balancer" + value = aws_lb.code_server.zone_id +} + +output "autoscaling_group_name" { + description = "Name of the Auto Scaling Group" + value = aws_autoscaling_group.code_server.name +} + +output "autoscaling_group_arn" { + description = "ARN of the Auto Scaling Group" + value = aws_autoscaling_group.code_server.arn +} + +output "launch_template_id" { + description = "ID of the Launch Template" + value = aws_launch_template.code_server.id +} + +output "code_server_password_secret_arn" { + description = "ARN of the Secrets Manager secret containing code-server password" + value = aws_secretsmanager_secret.code_server_password.arn +} + +output "cloudwatch_log_group_name" { + description = "Name of the CloudWatch Log Group" + value = aws_cloudwatch_log_group.code_server.name +} diff --git a/terraform/modules/ec2/user-data.sh b/terraform/modules/ec2/user-data.sh new file mode 100644 index 000000000..4d0e0390e --- /dev/null +++ b/terraform/modules/ec2/user-data.sh @@ -0,0 +1,212 @@ +#!/bin/bash +# User data script for Code-Server EC2 instances +# This script installs code-server, oauth2-proxy, and configures them + +set -e + +# Update system +yum update -y + +# Install dependencies +yum install -y docker git wget curl jq + +# Start and enable Docker +systemctl start docker +systemctl enable docker + +# Add ec2-user to docker group +usermod -aG docker ec2-user + +# Install CloudWatch Agent +wget https://s3.amazonaws.com/amazoncloudwatch-agent/amazon_linux/amd64/latest/amazon-cloudwatch-agent.rpm +rpm -U ./amazon-cloudwatch-agent.rpm +rm -f ./amazon-cloudwatch-agent.rpm + +# Configure CloudWatch Agent +cat > /opt/aws/amazon-cloudwatch-agent/etc/config.json < /opt/code-server/docker-compose.yml < /opt/code-server/allowed-emails.txt < /etc/systemd/system/code-server.service < /etc/cron.d/code-server-logs <> /var/log/code-server.log 2>&1 +* * * * * root docker logs oauth2-proxy --tail 100 >> /var/log/oauth2-proxy.log 2>&1 +EOF + +echo "Code-Server installation completed successfully!" diff --git a/terraform/modules/ec2/variables.tf b/terraform/modules/ec2/variables.tf new file mode 100644 index 000000000..aa5568dad --- /dev/null +++ b/terraform/modules/ec2/variables.tf @@ -0,0 +1,171 @@ +# EC2 Module Variables + +variable "name_prefix" { + description = "Prefix for resource names" + type = string +} + +variable "vpc_id" { + description = "ID of the VPC" + type = string +} + +variable "subnet_ids" { + description = "List of subnet IDs for EC2 instances" + type = list(string) +} + +variable "alb_subnet_ids" { + description = "List of subnet IDs for ALB" + type = list(string) +} + +variable "security_group_id" { + description = "Security group ID for EC2 instances" + type = string +} + +variable "alb_security_group_id" { + description = "Security group ID for ALB" + type = string +} + +variable "iam_instance_profile_name" { + description = "IAM instance profile name for EC2 instances" + type = string +} + +variable "kms_key_arn" { + description = "ARN of KMS key for EBS encryption" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.medium" +} + +variable "ami_id" { + description = "AMI ID for EC2 instances (leave empty for latest Amazon Linux 2023)" + type = string + default = "" +} + +variable "ebs_volume_size" { + description = "Size of EBS volume in GB" + type = number + default = 50 +} + +variable "ebs_volume_type" { + description = "Type of EBS volume" + type = string + default = "gp3" +} + +variable "min_instances" { + description = "Minimum number of instances in ASG" + type = number + default = 1 +} + +variable "max_instances" { + description = "Maximum number of instances in ASG" + type = number + default = 3 +} + +variable "desired_instances" { + description = "Desired number of instances in ASG" + type = number + default = 1 +} + +variable "code_server_version" { + description = "Version of code-server to install" + type = string + default = "latest" +} + +variable "code_server_password" { + description = "Password for code-server (leave empty for auto-generated)" + type = string + default = "" + sensitive = true +} + +variable "certificate_arn" { + description = "ARN of ACM certificate for HTTPS" + type = string + default = "" +} + +variable "internal_alb" { + description = "Whether the ALB should be internal" + type = bool + default = true +} + +variable "enable_deletion_protection" { + description = "Enable deletion protection for ALB" + type = bool + default = false +} + +variable "log_retention_days" { + description = "Number of days to retain CloudWatch logs" + type = number + default = 30 +} + +variable "enable_autoscaling" { + description = "Enable auto scaling based on CPU metrics" + type = bool + default = true +} + +# OAuth2 Proxy variables +variable "oauth2_client_id" { + description = "OAuth2 client ID" + type = string +} + +variable "oauth2_client_secret" { + description = "OAuth2 client secret" + type = string + sensitive = true +} + +variable "oauth2_issuer_url" { + description = "OAuth2 issuer URL (OIDC/SAML endpoint)" + type = string +} + +variable "oauth2_redirect_url" { + description = "OAuth2 redirect URL" + type = string +} + +variable "oauth2_cookie_secret" { + description = "OAuth2 cookie secret (generate with: python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())')" + type = string + sensitive = true +} + +variable "oauth2_allowed_emails" { + description = "List of allowed email addresses for OAuth2" + type = list(string) + default = [] +} + +variable "tags" { + description = "Tags to apply to resources" + type = map(string) + default = {} +} diff --git a/terraform/modules/eks/iam-policy-aws-load-balancer-controller.json b/terraform/modules/eks/iam-policy-aws-load-balancer-controller.json new file mode 100644 index 000000000..f0386794b --- /dev/null +++ b/terraform/modules/eks/iam-policy-aws-load-balancer-controller.json @@ -0,0 +1,241 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:CreateServiceLinkedRole" + ], + "Resource": "*", + "Condition": { + "StringEquals": { + "iam:AWSServiceName": "elasticloadbalancing.amazonaws.com" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:DescribeAccountAttributes", + "ec2:DescribeAddresses", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInternetGateways", + "ec2:DescribeVpcs", + "ec2:DescribeVpcPeeringConnections", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeInstances", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeTags", + "ec2:GetCoipPoolUsage", + "ec2:DescribeCoipPools", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeListenerCertificates", + "elasticloadbalancing:DescribeSSLPolicies", + "elasticloadbalancing:DescribeRules", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeTargetGroupAttributes", + "elasticloadbalancing:DescribeTargetHealth", + "elasticloadbalancing:DescribeTags" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "cognito-idp:DescribeUserPoolClient", + "acm:ListCertificates", + "acm:DescribeCertificate", + "iam:ListServerCertificates", + "iam:GetServerCertificate", + "waf-regional:GetWebACL", + "waf-regional:GetWebACLForResource", + "waf-regional:AssociateWebACL", + "waf-regional:DisassociateWebACL", + "wafv2:GetWebACL", + "wafv2:GetWebACLForResource", + "wafv2:AssociateWebACL", + "wafv2:DisassociateWebACL", + "shield:GetSubscriptionState", + "shield:DescribeProtection", + "shield:CreateProtection", + "shield:DeleteProtection" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateSecurityGroup" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateTags" + ], + "Resource": "arn:aws:ec2:*:*:security-group/*", + "Condition": { + "StringEquals": { + "ec2:CreateAction": "CreateSecurityGroup" + }, + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateTags", + "ec2:DeleteTags" + ], + "Resource": "arn:aws:ec2:*:*:security-group/*", + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "true", + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress", + "ec2:DeleteSecurityGroup" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:CreateLoadBalancer", + "elasticloadbalancing:CreateTargetGroup" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:CreateListener", + "elasticloadbalancing:DeleteListener", + "elasticloadbalancing:CreateRule", + "elasticloadbalancing:DeleteRule" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:AddTags", + "elasticloadbalancing:RemoveTags" + ], + "Resource": [ + "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" + ], + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "true", + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:AddTags", + "elasticloadbalancing:RemoveTags" + ], + "Resource": [ + "arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:ModifyLoadBalancerAttributes", + "elasticloadbalancing:SetIpAddressType", + "elasticloadbalancing:SetSecurityGroups", + "elasticloadbalancing:SetSubnets", + "elasticloadbalancing:DeleteLoadBalancer", + "elasticloadbalancing:ModifyTargetGroup", + "elasticloadbalancing:ModifyTargetGroupAttributes", + "elasticloadbalancing:DeleteTargetGroup" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:AddTags" + ], + "Resource": [ + "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" + ], + "Condition": { + "StringEquals": { + "elasticloadbalancing:CreateAction": [ + "CreateTargetGroup", + "CreateLoadBalancer" + ] + }, + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:RegisterTargets", + "elasticloadbalancing:DeregisterTargets" + ], + "Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*" + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:SetWebAcl", + "elasticloadbalancing:ModifyListener", + "elasticloadbalancing:AddListenerCertificates", + "elasticloadbalancing:RemoveListenerCertificates", + "elasticloadbalancing:ModifyRule" + ], + "Resource": "*" + } + ] +} diff --git a/terraform/modules/eks/main.tf b/terraform/modules/eks/main.tf new file mode 100644 index 000000000..2d364afdc --- /dev/null +++ b/terraform/modules/eks/main.tf @@ -0,0 +1,232 @@ +# EKS Module for Code-Server Deployment +# Creates an EKS cluster with managed node groups in private subnets + +# EKS Cluster +resource "aws_eks_cluster" "main" { + name = var.cluster_name + role_arn = var.cluster_role_arn + version = var.kubernetes_version + + vpc_config { + subnet_ids = concat(var.private_subnet_ids, var.public_subnet_ids) + endpoint_private_access = true + endpoint_public_access = var.endpoint_public_access + public_access_cidrs = var.endpoint_public_access ? var.public_access_cidrs : [] + security_group_ids = [var.cluster_security_group_id] + } + + encryption_config { + provider { + key_arn = var.kms_key_arn + } + resources = ["secrets"] + } + + enabled_cluster_log_types = var.cluster_log_types + + tags = merge( + var.tags, + { + Name = var.cluster_name + } + ) + + depends_on = [ + var.cluster_role_arn + ] +} + +# EKS Cluster Addons +resource "aws_eks_addon" "vpc_cni" { + cluster_name = aws_eks_cluster.main.name + addon_name = "vpc-cni" + addon_version = var.vpc_cni_version + resolve_conflicts_on_create = "OVERWRITE" + resolve_conflicts_on_update = "PRESERVE" + + tags = var.tags +} + +resource "aws_eks_addon" "kube_proxy" { + cluster_name = aws_eks_cluster.main.name + addon_name = "kube-proxy" + addon_version = var.kube_proxy_version + resolve_conflicts_on_create = "OVERWRITE" + resolve_conflicts_on_update = "PRESERVE" + + tags = var.tags +} + +resource "aws_eks_addon" "coredns" { + cluster_name = aws_eks_cluster.main.name + addon_name = "coredns" + addon_version = var.coredns_version + resolve_conflicts_on_create = "OVERWRITE" + resolve_conflicts_on_update = "PRESERVE" + + tags = var.tags + + depends_on = [ + aws_eks_node_group.main + ] +} + +resource "aws_eks_addon" "ebs_csi_driver" { + count = var.enable_ebs_csi_driver ? 1 : 0 + cluster_name = aws_eks_cluster.main.name + addon_name = "aws-ebs-csi-driver" + addon_version = var.ebs_csi_driver_version + resolve_conflicts_on_create = "OVERWRITE" + resolve_conflicts_on_update = "PRESERVE" + service_account_role_arn = aws_iam_role.ebs_csi_driver[0].arn + + tags = var.tags +} + +# IAM Role for EBS CSI Driver +resource "aws_iam_role" "ebs_csi_driver" { + count = var.enable_ebs_csi_driver ? 1 : 0 + name = "${var.cluster_name}-ebs-csi-driver-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Federated = aws_iam_openid_connect_provider.eks[0].arn + } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + StringEquals = { + "${replace(aws_iam_openid_connect_provider.eks[0].url, "https://", "")}:sub" = "system:serviceaccount:kube-system:ebs-csi-controller-sa" + "${replace(aws_iam_openid_connect_provider.eks[0].url, "https://", "")}:aud" = "sts.amazonaws.com" + } + } + } + ] + }) + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "ebs_csi_driver" { + count = var.enable_ebs_csi_driver ? 1 : 0 + role = aws_iam_role.ebs_csi_driver[0].name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy" +} + +# OIDC Provider for EKS +data "tls_certificate" "eks" { + url = aws_eks_cluster.main.identity[0].oidc[0].issuer +} + +resource "aws_iam_openid_connect_provider" "eks" { + count = var.enable_irsa ? 1 : 0 + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint] + url = aws_eks_cluster.main.identity[0].oidc[0].issuer + + tags = var.tags +} + +# EKS Node Group +resource "aws_eks_node_group" "main" { + cluster_name = aws_eks_cluster.main.name + node_group_name = "${var.cluster_name}-node-group" + node_role_arn = var.node_role_arn + subnet_ids = var.private_subnet_ids + version = var.kubernetes_version + + scaling_config { + desired_size = var.desired_nodes + max_size = var.max_nodes + min_size = var.min_nodes + } + + update_config { + max_unavailable = 1 + } + + instance_types = var.node_instance_types + capacity_type = var.capacity_type + disk_size = var.node_disk_size + + labels = var.node_labels + + dynamic "taint" { + for_each = var.node_taints + content { + key = taint.value.key + value = taint.value.value + effect = taint.value.effect + } + } + + tags = merge( + var.tags, + { + Name = "${var.cluster_name}-node-group" + } + ) + + lifecycle { + create_before_destroy = true + ignore_changes = [scaling_config[0].desired_size] + } + + depends_on = [ + var.node_role_arn + ] +} + +# CloudWatch Log Group for EKS +resource "aws_cloudwatch_log_group" "eks_cluster" { + name = "/aws/eks/${var.cluster_name}/cluster" + retention_in_days = var.log_retention_days + + tags = var.tags +} + +# IAM Role for AWS Load Balancer Controller +resource "aws_iam_role" "aws_load_balancer_controller" { + count = var.enable_aws_load_balancer_controller ? 1 : 0 + name = "${var.cluster_name}-aws-load-balancer-controller" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Federated = var.enable_irsa ? aws_iam_openid_connect_provider.eks[0].arn : null + } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + StringEquals = var.enable_irsa ? { + "${replace(aws_iam_openid_connect_provider.eks[0].url, "https://", "")}:sub" = "system:serviceaccount:kube-system:aws-load-balancer-controller" + "${replace(aws_iam_openid_connect_provider.eks[0].url, "https://", "")}:aud" = "sts.amazonaws.com" + } : {} + } + } + ] + }) + + tags = var.tags +} + +resource "aws_iam_policy" "aws_load_balancer_controller" { + count = var.enable_aws_load_balancer_controller ? 1 : 0 + name = "${var.cluster_name}-AWSLoadBalancerControllerIAMPolicy" + description = "IAM policy for AWS Load Balancer Controller" + + policy = file("${path.module}/iam-policy-aws-load-balancer-controller.json") + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "aws_load_balancer_controller" { + count = var.enable_aws_load_balancer_controller ? 1 : 0 + role = aws_iam_role.aws_load_balancer_controller[0].name + policy_arn = aws_iam_policy.aws_load_balancer_controller[0].arn +} diff --git a/terraform/modules/eks/outputs.tf b/terraform/modules/eks/outputs.tf new file mode 100644 index 000000000..8eb96263f --- /dev/null +++ b/terraform/modules/eks/outputs.tf @@ -0,0 +1,67 @@ +# EKS Module Outputs + +output "cluster_id" { + description = "ID of the EKS cluster" + value = aws_eks_cluster.main.id +} + +output "cluster_arn" { + description = "ARN of the EKS cluster" + value = aws_eks_cluster.main.arn +} + +output "cluster_endpoint" { + description = "Endpoint of the EKS cluster" + value = aws_eks_cluster.main.endpoint +} + +output "cluster_version" { + description = "Kubernetes version of the EKS cluster" + value = aws_eks_cluster.main.version +} + +output "cluster_security_group_id" { + description = "Security group ID of the EKS cluster" + value = aws_eks_cluster.main.vpc_config[0].cluster_security_group_id +} + +output "cluster_certificate_authority_data" { + description = "Certificate authority data for the EKS cluster" + value = aws_eks_cluster.main.certificate_authority[0].data + sensitive = true +} + +output "cluster_oidc_issuer_url" { + description = "OIDC issuer URL of the EKS cluster" + value = aws_eks_cluster.main.identity[0].oidc[0].issuer +} + +output "oidc_provider_arn" { + description = "ARN of the OIDC provider for EKS" + value = var.enable_irsa ? aws_iam_openid_connect_provider.eks[0].arn : null +} + +output "node_group_id" { + description = "ID of the EKS node group" + value = aws_eks_node_group.main.id +} + +output "node_group_arn" { + description = "ARN of the EKS node group" + value = aws_eks_node_group.main.arn +} + +output "node_group_status" { + description = "Status of the EKS node group" + value = aws_eks_node_group.main.status +} + +output "aws_load_balancer_controller_role_arn" { + description = "ARN of the AWS Load Balancer Controller IAM role" + value = var.enable_aws_load_balancer_controller ? aws_iam_role.aws_load_balancer_controller[0].arn : null +} + +output "cloudwatch_log_group_name" { + description = "Name of the CloudWatch log group for EKS" + value = aws_cloudwatch_log_group.eks_cluster.name +} diff --git a/terraform/modules/eks/variables.tf b/terraform/modules/eks/variables.tf new file mode 100644 index 000000000..f2d8ee109 --- /dev/null +++ b/terraform/modules/eks/variables.tf @@ -0,0 +1,166 @@ +# EKS Module Variables + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string +} + +variable "cluster_role_arn" { + description = "ARN of the IAM role for the EKS cluster" + type = string +} + +variable "node_role_arn" { + description = "ARN of the IAM role for EKS nodes" + type = string +} + +variable "private_subnet_ids" { + description = "List of private subnet IDs for EKS nodes" + type = list(string) +} + +variable "public_subnet_ids" { + description = "List of public subnet IDs for EKS control plane" + type = list(string) +} + +variable "cluster_security_group_id" { + description = "Security group ID for EKS cluster" + type = string +} + +variable "kms_key_arn" { + description = "ARN of KMS key for EKS encryption" + type = string +} + +variable "kubernetes_version" { + description = "Kubernetes version for EKS cluster" + type = string + default = "1.28" +} + +variable "endpoint_public_access" { + description = "Enable public access to EKS cluster endpoint" + type = bool + default = false +} + +variable "public_access_cidrs" { + description = "CIDR blocks allowed to access EKS cluster endpoint" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "cluster_log_types" { + description = "List of cluster log types to enable" + type = list(string) + default = ["api", "audit", "authenticator", "controllerManager", "scheduler"] +} + +variable "log_retention_days" { + description = "Number of days to retain CloudWatch logs" + type = number + default = 30 +} + +variable "vpc_cni_version" { + description = "Version of VPC CNI addon" + type = string + default = "v1.14.0-eksbuild.3" +} + +variable "kube_proxy_version" { + description = "Version of kube-proxy addon" + type = string + default = "v1.28.1-eksbuild.1" +} + +variable "coredns_version" { + description = "Version of CoreDNS addon" + type = string + default = "v1.10.1-eksbuild.2" +} + +variable "ebs_csi_driver_version" { + description = "Version of EBS CSI driver addon" + type = string + default = "v1.24.0-eksbuild.1" +} + +variable "enable_ebs_csi_driver" { + description = "Enable EBS CSI driver addon" + type = bool + default = true +} + +variable "enable_irsa" { + description = "Enable IAM Roles for Service Accounts (IRSA)" + type = bool + default = true +} + +variable "enable_aws_load_balancer_controller" { + description = "Enable AWS Load Balancer Controller IAM role" + type = bool + default = true +} + +variable "node_instance_types" { + description = "List of instance types for EKS nodes" + type = list(string) + default = ["t3.medium"] +} + +variable "capacity_type" { + description = "Capacity type for EKS nodes (ON_DEMAND or SPOT)" + type = string + default = "ON_DEMAND" +} + +variable "node_disk_size" { + description = "Disk size for EKS nodes in GB" + type = number + default = 50 +} + +variable "min_nodes" { + description = "Minimum number of EKS nodes" + type = number + default = 1 +} + +variable "max_nodes" { + description = "Maximum number of EKS nodes" + type = number + default = 3 +} + +variable "desired_nodes" { + description = "Desired number of EKS nodes" + type = number + default = 2 +} + +variable "node_labels" { + description = "Labels to apply to EKS nodes" + type = map(string) + default = {} +} + +variable "node_taints" { + description = "Taints to apply to EKS nodes" + type = list(object({ + key = string + value = string + effect = string + })) + default = [] +} + +variable "tags" { + description = "Tags to apply to resources" + type = map(string) + default = {} +} diff --git a/terraform/modules/security/main.tf b/terraform/modules/security/main.tf new file mode 100644 index 000000000..4a66fbde7 --- /dev/null +++ b/terraform/modules/security/main.tf @@ -0,0 +1,334 @@ +# Security Module for Code-Server +# Creates security groups, IAM roles, and ACM certificates + +# Security Group for ALB +resource "aws_security_group" "alb" { + name_prefix = "${var.name_prefix}-alb-" + description = "Security group for Application Load Balancer" + vpc_id = var.vpc_id + + ingress { + description = "HTTPS from anywhere" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = var.allowed_cidr_blocks + } + + ingress { + description = "HTTP from anywhere (redirect to HTTPS)" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = var.allowed_cidr_blocks + } + + egress { + description = "Allow all outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-alb-sg" + } + ) + + lifecycle { + create_before_destroy = true + } +} + +# Security Group for EC2 Code-Server instances +resource "aws_security_group" "code_server_ec2" { + name_prefix = "${var.name_prefix}-code-server-ec2-" + description = "Security group for Code-Server EC2 instances" + vpc_id = var.vpc_id + + ingress { + description = "Code-Server port from ALB" + from_port = 8080 + to_port = 8080 + protocol = "tcp" + security_groups = [aws_security_group.alb.id] + } + + ingress { + description = "OAuth2 Proxy from ALB" + from_port = 4180 + to_port = 4180 + protocol = "tcp" + security_groups = [aws_security_group.alb.id] + } + + ingress { + description = "SSH from bastion (if needed)" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = var.ssh_allowed_cidr_blocks + } + + egress { + description = "Allow all outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-code-server-ec2-sg" + } + ) + + lifecycle { + create_before_destroy = true + } +} + +# Security Group for EKS Cluster +resource "aws_security_group" "eks_cluster" { + name_prefix = "${var.name_prefix}-eks-cluster-" + description = "Security group for EKS cluster control plane" + vpc_id = var.vpc_id + + egress { + description = "Allow all outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-eks-cluster-sg" + } + ) + + lifecycle { + create_before_destroy = true + } +} + +# Security Group for EKS Nodes +resource "aws_security_group" "eks_nodes" { + name_prefix = "${var.name_prefix}-eks-nodes-" + description = "Security group for EKS worker nodes" + vpc_id = var.vpc_id + + ingress { + description = "Allow nodes to communicate with each other" + from_port = 0 + to_port = 65535 + protocol = "tcp" + self = true + } + + ingress { + description = "Allow pods to communicate with the cluster API Server" + from_port = 443 + to_port = 443 + protocol = "tcp" + security_groups = [aws_security_group.eks_cluster.id] + } + + ingress { + description = "Allow ALB to reach pods" + from_port = 0 + to_port = 65535 + protocol = "tcp" + security_groups = [aws_security_group.alb.id] + } + + egress { + description = "Allow all outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-eks-nodes-sg" + } + ) + + lifecycle { + create_before_destroy = true + } +} + +# Allow EKS control plane to communicate with nodes +resource "aws_security_group_rule" "cluster_to_nodes" { + description = "Allow control plane to communicate with worker nodes" + from_port = 1025 + to_port = 65535 + protocol = "tcp" + security_group_id = aws_security_group.eks_nodes.id + source_security_group_id = aws_security_group.eks_cluster.id + type = "ingress" +} + +# IAM Role for EC2 Code-Server instances +resource "aws_iam_role" "code_server_ec2" { + name = "${var.name_prefix}-code-server-ec2-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + } + ] + }) + + tags = var.tags +} + +# IAM Instance Profile for EC2 +resource "aws_iam_instance_profile" "code_server_ec2" { + name = "${var.name_prefix}-code-server-ec2-profile" + role = aws_iam_role.code_server_ec2.name + + tags = var.tags +} + +# Attach SSM policy for Systems Manager access +resource "aws_iam_role_policy_attachment" "code_server_ec2_ssm" { + role = aws_iam_role.code_server_ec2.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" +} + +# Attach CloudWatch policy for logging +resource "aws_iam_role_policy_attachment" "code_server_ec2_cloudwatch" { + role = aws_iam_role.code_server_ec2.name + policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy" +} + +# Custom policy for ECR access (to pull container images) +resource "aws_iam_role_policy" "code_server_ec2_ecr" { + name = "${var.name_prefix}-code-server-ec2-ecr-policy" + role = aws_iam_role.code_server_ec2.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ] + Resource = "*" + } + ] + }) +} + +# IAM Role for EKS Cluster +resource "aws_iam_role" "eks_cluster" { + name = "${var.name_prefix}-eks-cluster-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "eks.amazonaws.com" + } + } + ] + }) + + tags = var.tags +} + +# Attach required policies for EKS cluster +resource "aws_iam_role_policy_attachment" "eks_cluster_policy" { + role = aws_iam_role.eks_cluster.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" +} + +resource "aws_iam_role_policy_attachment" "eks_vpc_resource_controller" { + role = aws_iam_role.eks_cluster.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController" +} + +# IAM Role for EKS Node Group +resource "aws_iam_role" "eks_nodes" { + name = "${var.name_prefix}-eks-nodes-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + } + ] + }) + + tags = var.tags +} + +# Attach required policies for EKS nodes +resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" { + role = aws_iam_role.eks_nodes.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" +} + +resource "aws_iam_role_policy_attachment" "eks_cni_policy" { + role = aws_iam_role.eks_nodes.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" +} + +resource "aws_iam_role_policy_attachment" "eks_container_registry_policy" { + role = aws_iam_role.eks_nodes.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" +} + +resource "aws_iam_role_policy_attachment" "eks_ssm_policy" { + role = aws_iam_role.eks_nodes.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" +} + +# KMS Key for encryption at rest +resource "aws_kms_key" "code_server" { + description = "KMS key for Code-Server encryption" + deletion_window_in_days = 10 + enable_key_rotation = true + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-kms-key" + } + ) +} + +resource "aws_kms_alias" "code_server" { + name = "alias/${var.name_prefix}-code-server" + target_key_id = aws_kms_key.code_server.key_id +} diff --git a/terraform/modules/security/outputs.tf b/terraform/modules/security/outputs.tf new file mode 100644 index 000000000..ce9c14d8c --- /dev/null +++ b/terraform/modules/security/outputs.tf @@ -0,0 +1,51 @@ +# Security Module Outputs + +output "alb_security_group_id" { + description = "ID of the ALB security group" + value = aws_security_group.alb.id +} + +output "code_server_ec2_security_group_id" { + description = "ID of the Code-Server EC2 security group" + value = aws_security_group.code_server_ec2.id +} + +output "eks_cluster_security_group_id" { + description = "ID of the EKS cluster security group" + value = aws_security_group.eks_cluster.id +} + +output "eks_nodes_security_group_id" { + description = "ID of the EKS nodes security group" + value = aws_security_group.eks_nodes.id +} + +output "code_server_ec2_iam_role_arn" { + description = "ARN of the Code-Server EC2 IAM role" + value = aws_iam_role.code_server_ec2.arn +} + +output "code_server_ec2_instance_profile_name" { + description = "Name of the Code-Server EC2 instance profile" + value = aws_iam_instance_profile.code_server_ec2.name +} + +output "eks_cluster_iam_role_arn" { + description = "ARN of the EKS cluster IAM role" + value = aws_iam_role.eks_cluster.arn +} + +output "eks_nodes_iam_role_arn" { + description = "ARN of the EKS nodes IAM role" + value = aws_iam_role.eks_nodes.arn +} + +output "kms_key_id" { + description = "ID of the KMS key" + value = aws_kms_key.code_server.key_id +} + +output "kms_key_arn" { + description = "ARN of the KMS key" + value = aws_kms_key.code_server.arn +} diff --git a/terraform/modules/security/variables.tf b/terraform/modules/security/variables.tf new file mode 100644 index 000000000..70bf7f234 --- /dev/null +++ b/terraform/modules/security/variables.tf @@ -0,0 +1,29 @@ +# Security Module Variables + +variable "name_prefix" { + description = "Prefix for resource names" + type = string +} + +variable "vpc_id" { + description = "ID of the VPC" + type = string +} + +variable "allowed_cidr_blocks" { + description = "CIDR blocks allowed to access the ALB" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "ssh_allowed_cidr_blocks" { + description = "CIDR blocks allowed to SSH into instances" + type = list(string) + default = [] +} + +variable "tags" { + description = "Tags to apply to resources" + type = map(string) + default = {} +} diff --git a/terraform/modules/vpc/main.tf b/terraform/modules/vpc/main.tf new file mode 100644 index 000000000..5d6154b43 --- /dev/null +++ b/terraform/modules/vpc/main.tf @@ -0,0 +1,239 @@ +# VPC Module for Code-Server Deployment +# Creates a secure VPC with public and private subnets, NAT gateway, and VPC endpoints + +locals { + azs = slice(data.aws_availability_zones.available.names, 0, 3) +} + +data "aws_availability_zones" "available" { + state = "available" +} + +# VPC +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + enable_dns_hostnames = true + enable_dns_support = true + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-vpc" + } + ) +} + +# Internet Gateway +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-igw" + } + ) +} + +# Public Subnets +resource "aws_subnet" "public" { + count = length(var.public_subnet_cidrs) + vpc_id = aws_vpc.main.id + cidr_block = var.public_subnet_cidrs[count.index] + availability_zone = local.azs[count.index] + + map_public_ip_on_launch = true + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-public-subnet-${count.index + 1}" + "kubernetes.io/role/elb" = "1" + "kubernetes.io/cluster/${var.cluster_name}" = "shared" + } + ) +} + +# Private Subnets +resource "aws_subnet" "private" { + count = length(var.private_subnet_cidrs) + vpc_id = aws_vpc.main.id + cidr_block = var.private_subnet_cidrs[count.index] + availability_zone = local.azs[count.index] + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-private-subnet-${count.index + 1}" + "kubernetes.io/role/internal-elb" = "1" + "kubernetes.io/cluster/${var.cluster_name}" = "shared" + } + ) +} + +# Elastic IPs for NAT Gateways +resource "aws_eip" "nat" { + count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.public_subnet_cidrs)) : 0 + domain = "vpc" + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-nat-eip-${count.index + 1}" + } + ) + + depends_on = [aws_internet_gateway.main] +} + +# NAT Gateways +resource "aws_nat_gateway" "main" { + count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.public_subnet_cidrs)) : 0 + allocation_id = aws_eip.nat[count.index].id + subnet_id = aws_subnet.public[count.index].id + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-nat-${count.index + 1}" + } + ) + + depends_on = [aws_internet_gateway.main] +} + +# Public Route Table +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-public-rt" + } + ) +} + +# Public Route +resource "aws_route" "public" { + route_table_id = aws_route_table.public.id + destination_cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id +} + +# Public Route Table Association +resource "aws_route_table_association" "public" { + count = length(var.public_subnet_cidrs) + subnet_id = aws_subnet.public[count.index].id + route_table_id = aws_route_table.public.id +} + +# Private Route Tables +resource "aws_route_table" "private" { + count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.private_subnet_cidrs)) : 0 + vpc_id = aws_vpc.main.id + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-private-rt-${count.index + 1}" + } + ) +} + +# Private Routes +resource "aws_route" "private" { + count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.private_subnet_cidrs)) : 0 + route_table_id = aws_route_table.private[count.index].id + destination_cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main[var.single_nat_gateway ? 0 : count.index].id +} + +# Private Route Table Associations +resource "aws_route_table_association" "private" { + count = length(var.private_subnet_cidrs) + subnet_id = aws_subnet.private[count.index].id + route_table_id = aws_route_table.private[var.single_nat_gateway ? 0 : count.index].id +} + +# VPC Endpoints for enhanced security (S3 and ECR for EKS) +resource "aws_vpc_endpoint" "s3" { + count = var.enable_vpc_endpoints ? 1 : 0 + vpc_id = aws_vpc.main.id + service_name = "com.amazonaws.${var.aws_region}.s3" + + tags = merge( + var.tags, + { + Name = "${var.name_prefix}-s3-endpoint" + } + ) +} + +resource "aws_vpc_endpoint_route_table_association" "s3_private" { + count = var.enable_vpc_endpoints ? length(aws_route_table.private) : 0 + route_table_id = aws_route_table.private[count.index].id + vpc_endpoint_id = aws_vpc_endpoint.s3[0].id +} + +# VPC Flow Logs for security monitoring +resource "aws_cloudwatch_log_group" "vpc_flow_logs" { + count = var.enable_flow_logs ? 1 : 0 + name = "/aws/vpc/${var.name_prefix}-flow-logs" + retention_in_days = var.flow_logs_retention_days + + tags = var.tags +} + +resource "aws_iam_role" "vpc_flow_logs" { + count = var.enable_flow_logs ? 1 : 0 + name = "${var.name_prefix}-vpc-flow-logs-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "vpc-flow-logs.amazonaws.com" + } + } + ] + }) + + tags = var.tags +} + +resource "aws_iam_role_policy" "vpc_flow_logs" { + count = var.enable_flow_logs ? 1 : 0 + name = "${var.name_prefix}-vpc-flow-logs-policy" + role = aws_iam_role.vpc_flow_logs[0].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams" + ] + Effect = "Allow" + Resource = "*" + } + ] + }) +} + +resource "aws_flow_log" "main" { + count = var.enable_flow_logs ? 1 : 0 + iam_role_arn = aws_iam_role.vpc_flow_logs[0].arn + log_destination = aws_cloudwatch_log_group.vpc_flow_logs[0].arn + traffic_type = "ALL" + vpc_id = aws_vpc.main.id + + tags = var.tags +} diff --git a/terraform/modules/vpc/outputs.tf b/terraform/modules/vpc/outputs.tf new file mode 100644 index 000000000..cb529178e --- /dev/null +++ b/terraform/modules/vpc/outputs.tf @@ -0,0 +1,41 @@ +# VPC Module Outputs + +output "vpc_id" { + description = "ID of the VPC" + value = aws_vpc.main.id +} + +output "vpc_cidr" { + description = "CIDR block of the VPC" + value = aws_vpc.main.cidr_block +} + +output "public_subnet_ids" { + description = "IDs of public subnets" + value = aws_subnet.public[*].id +} + +output "private_subnet_ids" { + description = "IDs of private subnets" + value = aws_subnet.private[*].id +} + +output "nat_gateway_ids" { + description = "IDs of NAT gateways" + value = aws_nat_gateway.main[*].id +} + +output "internet_gateway_id" { + description = "ID of the internet gateway" + value = aws_internet_gateway.main.id +} + +output "public_route_table_id" { + description = "ID of the public route table" + value = aws_route_table.public.id +} + +output "private_route_table_ids" { + description = "IDs of private route tables" + value = aws_route_table.private[*].id +} diff --git a/terraform/modules/vpc/variables.tf b/terraform/modules/vpc/variables.tf new file mode 100644 index 000000000..bf50b7982 --- /dev/null +++ b/terraform/modules/vpc/variables.tf @@ -0,0 +1,70 @@ +# VPC Module Variables + +variable "name_prefix" { + description = "Prefix for resource names" + type = string +} + +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "public_subnet_cidrs" { + description = "CIDR blocks for public subnets" + type = list(string) + default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +} + +variable "private_subnet_cidrs" { + description = "CIDR blocks for private subnets" + type = list(string) + default = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"] +} + +variable "aws_region" { + description = "AWS region" + type = string +} + +variable "cluster_name" { + description = "Name of the EKS cluster (for subnet tagging)" + type = string +} + +variable "enable_nat_gateway" { + description = "Enable NAT gateway for private subnets" + type = bool + default = true +} + +variable "single_nat_gateway" { + description = "Use a single NAT gateway for all private subnets (cost optimization)" + type = bool + default = false +} + +variable "enable_vpc_endpoints" { + description = "Enable VPC endpoints for AWS services" + type = bool + default = true +} + +variable "enable_flow_logs" { + description = "Enable VPC flow logs" + type = bool + default = true +} + +variable "flow_logs_retention_days" { + description = "Number of days to retain VPC flow logs" + type = number + default = 30 +} + +variable "tags" { + description = "Tags to apply to resources" + type = map(string) + default = {} +} diff --git a/terraform/scripts/deploy-ec2.sh b/terraform/scripts/deploy-ec2.sh new file mode 100755 index 000000000..30fccfbfa --- /dev/null +++ b/terraform/scripts/deploy-ec2.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# Deployment script for Code-Server on EC2 +# This script automates the deployment process + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOYMENT_DIR="${SCRIPT_DIR}/../deployments/ec2" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +echo_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +echo_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check prerequisites +check_prerequisites() { + echo_info "Checking prerequisites..." + + # Check Terraform + if ! command -v terraform &> /dev/null; then + echo_error "Terraform is not installed. Please install Terraform first." + exit 1 + fi + + # Check AWS CLI + if ! command -v aws &> /dev/null; then + echo_error "AWS CLI is not installed. Please install AWS CLI first." + exit 1 + fi + + # Check AWS credentials + if ! aws sts get-caller-identity &> /dev/null; then + echo_error "AWS credentials are not configured. Please configure AWS credentials first." + exit 1 + fi + + echo_info "All prerequisites met!" +} + +# Initialize Terraform +init_terraform() { + echo_info "Initializing Terraform..." + cd "${DEPLOYMENT_DIR}" + terraform init +} + +# Validate Terraform configuration +validate_terraform() { + echo_info "Validating Terraform configuration..." + cd "${DEPLOYMENT_DIR}" + terraform validate +} + +# Plan Terraform deployment +plan_terraform() { + echo_info "Planning Terraform deployment..." + cd "${DEPLOYMENT_DIR}" + terraform plan -out=tfplan +} + +# Apply Terraform deployment +apply_terraform() { + echo_info "Applying Terraform deployment..." + cd "${DEPLOYMENT_DIR}" + + read -p "Do you want to apply this plan? (yes/no): " response + if [ "$response" != "yes" ]; then + echo_warn "Deployment cancelled." + exit 0 + fi + + terraform apply tfplan + rm -f tfplan +} + +# Get outputs +get_outputs() { + echo_info "Getting deployment outputs..." + cd "${DEPLOYMENT_DIR}" + + echo "" + echo_info "=== Deployment Complete ===" + echo "" + + ALB_URL=$(terraform output -raw alb_url 2>/dev/null || echo "N/A") + SECRET_ARN=$(terraform output -raw code_server_password_secret_arn 2>/dev/null || echo "N/A") + REGION=$(terraform output -raw aws_region 2>/dev/null || echo "us-east-1") + + echo "Code-Server URL: $ALB_URL" + echo "" + echo "To get the code-server password, run:" + echo " aws secretsmanager get-secret-value \\" + echo " --secret-id $SECRET_ARN \\" + echo " --region $REGION \\" + echo " --query SecretString \\" + echo " --output text" + echo "" +} + +# Main deployment flow +main() { + echo_info "Starting Code-Server EC2 deployment..." + echo "" + + check_prerequisites + init_terraform + validate_terraform + plan_terraform + apply_terraform + get_outputs + + echo_info "Deployment completed successfully!" +} + +# Run main function +main "$@" diff --git a/terraform/scripts/deploy-eks.sh b/terraform/scripts/deploy-eks.sh new file mode 100755 index 000000000..064021344 --- /dev/null +++ b/terraform/scripts/deploy-eks.sh @@ -0,0 +1,204 @@ +#!/bin/bash +# Deployment script for Code-Server on EKS +# This script automates the deployment process + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOYMENT_DIR="${SCRIPT_DIR}/../deployments/eks" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +echo_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +echo_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check prerequisites +check_prerequisites() { + echo_info "Checking prerequisites..." + + # Check Terraform + if ! command -v terraform &> /dev/null; then + echo_error "Terraform is not installed. Please install Terraform first." + exit 1 + fi + + # Check AWS CLI + if ! command -v aws &> /dev/null; then + echo_error "AWS CLI is not installed. Please install AWS CLI first." + exit 1 + fi + + # Check kubectl + if ! command -v kubectl &> /dev/null; then + echo_error "kubectl is not installed. Please install kubectl first." + exit 1 + fi + + # Check Helm + if ! command -v helm &> /dev/null; then + echo_error "Helm is not installed. Please install Helm first." + exit 1 + fi + + # Check AWS credentials + if ! aws sts get-caller-identity &> /dev/null; then + echo_error "AWS credentials are not configured. Please configure AWS credentials first." + exit 1 + fi + + echo_info "All prerequisites met!" +} + +# Initialize Terraform +init_terraform() { + echo_info "Initializing Terraform..." + cd "${DEPLOYMENT_DIR}" + terraform init +} + +# Validate Terraform configuration +validate_terraform() { + echo_info "Validating Terraform configuration..." + cd "${DEPLOYMENT_DIR}" + terraform validate +} + +# Plan Terraform deployment +plan_terraform() { + echo_info "Planning Terraform deployment..." + cd "${DEPLOYMENT_DIR}" + terraform plan -out=tfplan +} + +# Apply Terraform deployment +apply_terraform() { + echo_info "Applying Terraform deployment..." + cd "${DEPLOYMENT_DIR}" + + read -p "Do you want to apply this plan? (yes/no): " response + if [ "$response" != "yes" ]; then + echo_warn "Deployment cancelled." + exit 0 + fi + + terraform apply tfplan + rm -f tfplan +} + +# Configure kubectl +configure_kubectl() { + echo_info "Configuring kubectl..." + cd "${DEPLOYMENT_DIR}" + + CLUSTER_NAME=$(terraform output -raw eks_cluster_id) + REGION=$(terraform output -raw aws_region 2>/dev/null || echo "us-east-1") + + aws eks update-kubeconfig --region "$REGION" --name "$CLUSTER_NAME" + + echo_info "Waiting for nodes to be ready..." + kubectl wait --for=condition=Ready nodes --all --timeout=300s +} + +# Deploy Code-Server +deploy_code_server() { + echo_info "Deploying Code-Server..." + + read -p "Do you want to deploy Code-Server now? (yes/no): " response + if [ "$response" != "yes" ]; then + echo_warn "Code-Server deployment skipped. You can deploy it manually later." + return + fi + + cd "${SCRIPT_DIR}/../ci/helm-chart" + + helm upgrade --install code-server . \ + --namespace code-server \ + --create-namespace \ + --values "${DEPLOYMENT_DIR}/k8s/code-server-values.yaml" \ + --wait \ + --timeout 10m + + echo_info "Code-Server deployed successfully!" +} + +# Deploy OAuth2 Proxy (optional) +deploy_oauth2_proxy() { + echo_info "OAuth2 Proxy deployment..." + + read -p "Do you want to deploy OAuth2 Proxy for SAML authentication? (yes/no): " response + if [ "$response" != "yes" ]; then + echo_warn "OAuth2 Proxy deployment skipped." + return + fi + + cd "${DEPLOYMENT_DIR}" + kubectl apply -f k8s/oauth2-proxy.yaml + + echo_info "OAuth2 Proxy deployed successfully!" +} + +# Get outputs +get_outputs() { + echo_info "Getting deployment information..." + cd "${DEPLOYMENT_DIR}" + + echo "" + echo_info "=== Deployment Complete ===" + echo "" + + CLUSTER_NAME=$(terraform output -raw eks_cluster_id) + REGION=$(terraform output -raw aws_region 2>/dev/null || echo "us-east-1") + + echo "EKS Cluster: $CLUSTER_NAME" + echo "Region: $REGION" + echo "" + + echo "To get the Load Balancer URL, run:" + echo " kubectl get ingress -n code-server" + echo "" + + echo "To access Code-Server:" + echo " 1. Wait for the ingress to get an ADDRESS (ALB DNS name)" + echo " 2. Access the URL shown in the ADDRESS field" + echo "" + + echo "Useful commands:" + echo " kubectl get pods -n code-server" + echo " kubectl logs -n code-server -l app.kubernetes.io/name=code-server" + echo " kubectl port-forward -n code-server svc/code-server 8080:8080" + echo "" +} + +# Main deployment flow +main() { + echo_info "Starting Code-Server EKS deployment..." + echo "" + + check_prerequisites + init_terraform + validate_terraform + plan_terraform + apply_terraform + configure_kubectl + deploy_code_server + deploy_oauth2_proxy + get_outputs + + echo_info "Deployment completed successfully!" +} + +# Run main function +main "$@" diff --git a/terraform/scripts/destroy-ec2.sh b/terraform/scripts/destroy-ec2.sh new file mode 100755 index 000000000..d93881bce --- /dev/null +++ b/terraform/scripts/destroy-ec2.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Destroy script for Code-Server EC2 deployment + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOYMENT_DIR="${SCRIPT_DIR}/../deployments/ec2" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +echo_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +echo_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +main() { + echo_warn "WARNING: This will destroy all Code-Server EC2 infrastructure!" + echo_warn "This action cannot be undone!" + echo "" + + read -p "Are you sure you want to continue? (type 'yes' to confirm): " response + if [ "$response" != "yes" ]; then + echo_info "Destruction cancelled." + exit 0 + fi + + echo_info "Destroying Code-Server EC2 infrastructure..." + cd "${DEPLOYMENT_DIR}" + terraform destroy + + echo_info "Destruction completed!" +} + +main "$@" diff --git a/terraform/scripts/destroy-eks.sh b/terraform/scripts/destroy-eks.sh new file mode 100755 index 000000000..e58ada1ef --- /dev/null +++ b/terraform/scripts/destroy-eks.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Destroy script for Code-Server EKS deployment + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOYMENT_DIR="${SCRIPT_DIR}/../deployments/eks" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +echo_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +echo_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +cleanup_k8s_resources() { + echo_info "Cleaning up Kubernetes resources..." + + # Delete Code-Server Helm release + helm uninstall code-server -n code-server 2>/dev/null || true + + # Delete OAuth2 Proxy + kubectl delete -f "${DEPLOYMENT_DIR}/k8s/oauth2-proxy.yaml" 2>/dev/null || true + + # Delete namespace + kubectl delete namespace code-server 2>/dev/null || true + + echo_info "Kubernetes resources cleaned up!" +} + +main() { + echo_warn "WARNING: This will destroy all Code-Server EKS infrastructure!" + echo_warn "This action cannot be undone!" + echo "" + + read -p "Are you sure you want to continue? (type 'yes' to confirm): " response + if [ "$response" != "yes" ]; then + echo_info "Destruction cancelled." + exit 0 + fi + + cleanup_k8s_resources + + echo_info "Destroying Code-Server EKS infrastructure..." + cd "${DEPLOYMENT_DIR}" + terraform destroy + + echo_info "Destruction completed!" +} + +main "$@"