diff --git a/.gitignore b/.gitignore index 20ded3b..87c8268 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,10 @@ coverage/ # Logs *.log logs/ + +# Terraform +**/.terraform/* +*.tfstate +*.tfstate.* +*.tfvars +.terraform.lock.hcl diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..3af68df --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,221 @@ +# Terraform Infrastructure as Code + +This directory contains Terraform configurations for deploying the Quote App to different environments. + +## Structure + +``` +terraform/ +├── modules/ +│ ├── docker/ # Docker module (containers, images, networks) +│ └── kubernetes/ # Kubernetes module (pods, services, HPA) +├── environments/ +│ ├── dev/ # Development environment (Docker) +│ └── prod/ # Production environment (Kubernetes) +└── main.tf # Root configuration +``` + +## Prerequisites + +- Terraform >= 1.0 +- Docker (for dev environment) +- kubectl and Minikube (for prod environment) + +## Usage + +### Development Environment (Docker) + +```bash +cd environments/dev + +# Initialize Terraform +terraform init + +# Preview changes +terraform plan + +# Apply configuration +terraform apply + +# Access application +curl http://localhost:3000/health + +# Destroy resources +terraform destroy +``` + +### Production Environment (Kubernetes) + +```bash +cd environments/prod + +# Ensure Minikube is running +minikube status + +# Initialize Terraform +terraform init + +# Preview changes +terraform plan + +# Apply configuration +terraform apply + +# Check deployment +kubectl get all -n quote-app + +# Access application +minikube service quote-app-service -n quote-app --url + +# Destroy resources +terraform destroy +``` + +## Modules + +### Docker Module + +Manages Docker resources: +- Docker images +- Docker containers +- Docker networks +- Health checks + +**Variables:** +- `image_name` - Docker image name +- `image_tag` - Image tag (default: latest) +- `container_name` - Container name +- `internal_port` - Container port (default: 3000) +- `external_port` - Host port (default: 3000) +- `environment_vars` - Environment variables (map) +- `restart_policy` - Restart policy (default: unless-stopped) + +**Outputs:** +- `container_id` - Container ID +- `container_name` - Container name +- `network_name` - Network name +- `access_url` - Application URL + +### Kubernetes Module + +Manages Kubernetes resources: +- Namespace +- ConfigMap +- Deployment +- Service (NodePort) +- Horizontal Pod Autoscaler + +**Variables:** +- `namespace` - Kubernetes namespace +- `app_name` - Application name +- `image` - Docker image +- `replicas` - Number of replicas (default: 3) +- `container_port` - Container port (default: 3000) +- `service_port` - Service port (default: 80) +- `node_port` - NodePort (default: 30080) +- `environment_vars` - Environment variables (map) +- `cpu_request` - CPU request (default: 100m) +- `cpu_limit` - CPU limit (default: 500m) +- `memory_request` - Memory request (default: 128Mi) +- `memory_limit` - Memory limit (default: 256Mi) +- `hpa_enabled` - Enable HPA (default: true) +- `hpa_min_replicas` - Min replicas (default: 2) +- `hpa_max_replicas` - Max replicas (default: 10) +- `hpa_cpu_threshold` - CPU threshold % (default: 70) + +**Outputs:** +- `namespace` - Namespace name +- `deployment_name` - Deployment name +- `service_name` - Service name +- `service_port` - Service port +- `node_port` - NodePort + +## Common Commands + +```bash +# Initialize +terraform init + +# Format code +terraform fmt -recursive + +# Validate configuration +terraform validate + +# Plan changes +terraform plan + +# Apply changes +terraform apply + +# Show current state +terraform show + +# List resources +terraform state list + +# Show outputs +terraform output + +# Destroy resources +terraform destroy +``` + +## State Management + +Terraform stores state in `terraform.tfstate` file. This file contains: +- Current infrastructure state +- Resource dependencies +- Metadata + +**Important:** +- Never edit state files manually +- Keep state files secure (contain sensitive data) +- For production, use remote state (S3, Terraform Cloud) + +## Best Practices + +1. **Use modules** for reusable configurations +2. **Separate environments** (dev, staging, prod) +3. **Use variables** for configurable values +4. **Output important values** for easy access +5. **Version control** all Terraform files +6. **Use remote state** for production +7. **Plan before apply** to review changes +8. **Use workspaces** for environment isolation + +## Troubleshooting + +### Issue: Provider authentication failed + +```bash +# For Docker +docker ps # Verify Docker is running + +# For Kubernetes +kubectl cluster-info # Verify cluster access +``` + +### Issue: Resource already exists + +```bash +# Import existing resource +terraform import module.quote_app_k8s.kubernetes_namespace.app quote-app + +# Or destroy and recreate +terraform destroy +terraform apply +``` + +### Issue: State lock + +```bash +# Force unlock (use with caution) +terraform force-unlock +``` + +## Additional Resources + +- [Terraform Documentation](https://www.terraform.io/docs) +- [Docker Provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs) +- [Kubernetes Provider](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs) diff --git a/terraform/environments/dev/main.tf b/terraform/environments/dev/main.tf new file mode 100644 index 0000000..8b11045 --- /dev/null +++ b/terraform/environments/dev/main.tf @@ -0,0 +1,49 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0" + } + } +} + +provider "docker" { + host = "unix:///var/run/docker.sock" +} + +module "quote_app_docker" { + source = "../../modules/docker" + + image_name = "adityajareda/quote-app" + image_tag = "latest" + container_name = "quote-app-dev" + internal_port = 3000 + external_port = 3000 + + environment_vars = { + NODE_ENV = "development" + PORT = "3000" + APP_NAME = "Quote App" + APP_VERSION = "1.0.0" + LOG_LEVEL = "debug" + } + + restart_policy = "unless-stopped" +} + +output "container_id" { + description = "Docker container ID" + value = module.quote_app_docker.container_id +} + +output "access_url" { + description = "Application access URL" + value = module.quote_app_docker.access_url +} + +output "container_name" { + description = "Container name" + value = module.quote_app_docker.container_name +} diff --git a/terraform/environments/dev/variables.tf b/terraform/environments/dev/variables.tf new file mode 100644 index 0000000..76f420a --- /dev/null +++ b/terraform/environments/dev/variables.tf @@ -0,0 +1,5 @@ +variable "docker_host" { + description = "Docker daemon host" + type = string + default = "unix:///var/run/docker.sock" +} diff --git a/terraform/environments/prod/main.tf b/terraform/environments/prod/main.tf new file mode 100644 index 0000000..20cf00e --- /dev/null +++ b/terraform/environments/prod/main.tf @@ -0,0 +1,64 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.24" + } + } +} + +provider "kubernetes" { + config_path = "~/.kube/config" +} + +module "quote_app_k8s" { + source = "../../modules/kubernetes" + + namespace = "quote-app" + app_name = "quote-app" + image = "adityajareda/quote-app:latest" + replicas = 3 + container_port = 3000 + service_port = 80 + node_port = 30080 + + environment_vars = { + NODE_ENV = "production" + PORT = "3000" + APP_NAME = "Quote App" + APP_VERSION = "1.0.0" + LOG_LEVEL = "info" + } + + cpu_request = "100m" + cpu_limit = "500m" + memory_request = "128Mi" + memory_limit = "256Mi" + + hpa_enabled = true + hpa_min_replicas = 2 + hpa_max_replicas = 10 + hpa_cpu_threshold = 70 +} + +output "namespace" { + description = "Kubernetes namespace" + value = module.quote_app_k8s.namespace +} + +output "deployment_name" { + description = "Deployment name" + value = module.quote_app_k8s.deployment_name +} + +output "service_name" { + description = "Service name" + value = module.quote_app_k8s.service_name +} + +output "access_port" { + description = "NodePort for external access" + value = module.quote_app_k8s.node_port +} diff --git a/terraform/environments/prod/variables.tf b/terraform/environments/prod/variables.tf new file mode 100644 index 0000000..94dbced --- /dev/null +++ b/terraform/environments/prod/variables.tf @@ -0,0 +1,5 @@ +variable "kubeconfig_path" { + description = "Path to kubeconfig file" + type = string + default = "~/.kube/config" +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..1db5717 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,31 @@ +# ============================================ +# ROOT TERRAFORM CONFIGURATION +# ============================================ +# This is the main entry point for Terraform + +terraform { + required_version = ">= 1.0" +} + +# ============================================ +# INSTRUCTIONS +# ============================================ +# +# To use this Terraform configuration: +# +# 1. For Development (Docker): +# cd environments/dev +# terraform init +# terraform plan +# terraform apply +# +# 2. For Production (Kubernetes): +# cd environments/prod +# terraform init +# terraform plan +# terraform apply +# +# 3. To destroy: +# terraform destroy +# +# ============================================ diff --git a/terraform/modules/docker/main.tf b/terraform/modules/docker/main.tf new file mode 100644 index 0000000..bc7403d --- /dev/null +++ b/terraform/modules/docker/main.tf @@ -0,0 +1,106 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0" + } + } +} + +variable "image_name" { + description = "Docker image name" + type = string +} + +variable "image_tag" { + description = "Docker image tag" + type = string + default = "latest" +} + +variable "container_name" { + description = "Container name" + type = string +} + +variable "internal_port" { + description = "Internal container port" + type = number + default = 3000 +} + +variable "external_port" { + description = "External host port" + type = number + default = 3000 +} + +variable "environment_vars" { + description = "Environment variables" + type = map(string) + default = {} +} + +variable "restart_policy" { + description = "Container restart policy" + type = string + default = "unless-stopped" +} + + +resource "docker_network" "app_network" { + name = "${var.container_name}-network" + driver = "bridge" +} + +resource "docker_image" "app" { + name = "${var.image_name}:${var.image_tag}" + keep_locally = false +} + +resource "docker_container" "app" { + name = var.container_name + image = docker_image.app.image_id + + ports { + internal = var.internal_port + external = var.external_port + } + + env = [ + for key, value in var.environment_vars : "${key}=${value}" + ] + + networks_advanced { + name = docker_network.app_network.name + } + + restart = var.restart_policy + + healthcheck { + test = ["CMD", "node", "-e", "require('http').get('http://localhost:${var.internal_port}/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval = "30s" + timeout = "3s" + retries = 3 + } +} + +output "container_id" { + description = "Container ID" + value = docker_container.app.id +} + +output "container_name" { + description = "Container name" + value = docker_container.app.name +} + +output "network_name" { + description = "Network name" + value = docker_network.app_network.name +} + +output "access_url" { + description = "Access URL" + value = "http://localhost:${var.external_port}" +} diff --git a/terraform/modules/kubernetes/main.tf b/terraform/modules/kubernetes/main.tf new file mode 100644 index 0000000..7a32c31 --- /dev/null +++ b/terraform/modules/kubernetes/main.tf @@ -0,0 +1,349 @@ +terraform { + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.24" + } + } +} + +variable "namespace" { + description = "Kubernetes namespace" + type = string +} + +variable "app_name" { + description = "Application name" + type = string +} + +variable "image" { + description = "Docker image" + type = string +} + +variable "replicas" { + description = "Number of replicas" + type = number + default = 3 +} + +variable "container_port" { + description = "Container port" + type = number + default = 3000 +} + +variable "service_port" { + description = "Service port" + type = number + default = 80 +} + +variable "node_port" { + description = "NodePort (30000-32767)" + type = number + default = 30080 +} + +variable "environment_vars" { + description = "Environment variables" + type = map(string) + default = {} +} + +variable "cpu_request" { + description = "CPU request" + type = string + default = "100m" +} + +variable "cpu_limit" { + description = "CPU limit" + type = string + default = "500m" +} + +variable "memory_request" { + description = "Memory request" + type = string + default = "128Mi" +} + +variable "memory_limit" { + description = "Memory limit" + type = string + default = "256Mi" +} + +variable "hpa_enabled" { + description = "Enable Horizontal Pod Autoscaler" + type = bool + default = true +} + +variable "hpa_min_replicas" { + description = "HPA minimum replicas" + type = number + default = 2 +} + +variable "hpa_max_replicas" { + description = "HPA maximum replicas" + type = number + default = 10 +} + +variable "hpa_cpu_threshold" { + description = "HPA CPU threshold percentage" + type = number + default = 70 +} + +resource "kubernetes_namespace" "app" { + metadata { + name = var.namespace + labels = { + name = var.namespace + environment = terraform.workspace + managed-by = "terraform" + } + } +} + +resource "kubernetes_config_map" "app_config" { + metadata { + name = "${var.app_name}-config" + namespace = kubernetes_namespace.app.metadata[0].name + labels = { + app = var.app_name + } + } + + data = var.environment_vars +} + +resource "kubernetes_deployment" "app" { + metadata { + name = "${var.app_name}-deployment" + namespace = kubernetes_namespace.app.metadata[0].name + labels = { + app = var.app_name + version = "v1" + } + } + + spec { + replicas = var.replicas + + strategy { + type = "RollingUpdate" + rolling_update { + max_surge = "1" + max_unavailable = "0" + } + } + + selector { + match_labels = { + app = var.app_name + } + } + + template { + metadata { + labels = { + app = var.app_name + version = "v1" + } + } + + spec { + container { + name = var.app_name + image = var.image + image_pull_policy = "Always" + + port { + name = "http" + container_port = var.container_port + protocol = "TCP" + } + + env_from { + config_map_ref { + name = kubernetes_config_map.app_config.metadata[0].name + } + } + + resources { + requests = { + cpu = var.cpu_request + memory = var.memory_request + } + limits = { + cpu = var.cpu_limit + memory = var.memory_limit + } + } + + liveness_probe { + http_get { + path = "/health" + port = var.container_port + } + initial_delay_seconds = 10 + period_seconds = 10 + timeout_seconds = 5 + failure_threshold = 3 + } + + readiness_probe { + http_get { + path = "/health" + port = var.container_port + } + initial_delay_seconds = 5 + period_seconds = 5 + timeout_seconds = 3 + failure_threshold = 3 + } + + security_context { + run_as_non_root = true + run_as_user = 1001 + allow_privilege_escalation = false + read_only_root_filesystem = false + } + } + + restart_policy = "Always" + } + } + } +} + +resource "kubernetes_service" "app" { + metadata { + name = "${var.app_name}-service" + namespace = kubernetes_namespace.app.metadata[0].name + labels = { + app = var.app_name + } + } + + spec { + type = "NodePort" + + selector = { + app = var.app_name + } + + port { + name = "http" + protocol = "TCP" + port = var.service_port + target_port = var.container_port + node_port = var.node_port + } + + session_affinity = "None" + } +} + +resource "kubernetes_horizontal_pod_autoscaler_v2" "app" { + count = var.hpa_enabled ? 1 : 0 + + metadata { + name = "${var.app_name}-hpa" + namespace = kubernetes_namespace.app.metadata[0].name + labels = { + app = var.app_name + } + } + + spec { + scale_target_ref { + api_version = "apps/v1" + kind = "Deployment" + name = kubernetes_deployment.app.metadata[0].name + } + + min_replicas = var.hpa_min_replicas + max_replicas = var.hpa_max_replicas + + metric { + type = "Resource" + resource { + name = "cpu" + target { + type = "Utilization" + average_utilization = var.hpa_cpu_threshold + } + } + } + + metric { + type = "Resource" + resource { + name = "memory" + target { + type = "Utilization" + average_utilization = 80 + } + } + } + + behavior { + scale_down { + stabilization_window_seconds = 300 + select_policy = "Max" + policy { + type = "Percent" + value = 50 + period_seconds = 60 + } + } + + scale_up { + stabilization_window_seconds = 0 + policy { + type = "Percent" + value = 100 + period_seconds = 15 + } + policy { + type = "Pods" + value = 2 + period_seconds = 15 + } + select_policy = "Max" + } + } + } +} + +output "namespace" { + description = "Kubernetes namespace" + value = kubernetes_namespace.app.metadata[0].name +} + +output "deployment_name" { + description = "Deployment name" + value = kubernetes_deployment.app.metadata[0].name +} + +output "service_name" { + description = "Service name" + value = kubernetes_service.app.metadata[0].name +} + +output "service_port" { + description = "Service port" + value = var.service_port +} + +output "node_port" { + description = "NodePort" + value = var.node_port +}