diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..0622516 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/crewmeister-ci.yml b/.github/workflows/crewmeister-ci.yml new file mode 100644 index 0000000..92f8ba1 --- /dev/null +++ b/.github/workflows/crewmeister-ci.yml @@ -0,0 +1,102 @@ +name: crewmeister-ci + +on: + push: + branches: + - main + - feature/* + +permissions: + id-token: write + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build with Maven + run: mvn clean install -DskipTests + + code-quality: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Run Code Quality Checks + run: mvn verify + + docker: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/feature/') + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Log in to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract Git SHA + run: echo "IMAGE_TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + - name: Build and Push Docker Image + run: | + docker buildx build --platform linux/amd64 \ + -t ${{ secrets.DOCKER_USERNAME }}/devops-coding-challenge:${{ env.IMAGE_TAG }} --push . + + updatek8s: + runs-on: ubuntu-latest + needs: docker + outputs: + aws_access_key_id: ${{ steps.auth.outputs.aws_access_key_id }} + aws_secret_access_key: ${{ steps.auth.outputs.aws_secret_access_key }} + aws_session_token: ${{ steps.auth.outputs.aws_session_token }} + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Configure AWS Credentials (OIDC) + id: auth + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ secrets.AWS_REGION }} + audience: sts.amazonaws.com + + - name: Verify AWS Identity + run: aws sts get-caller-identity + + - name: Update kubeconfig for EKS + run: aws eks update-kubeconfig --name ${{ secrets.EKS_CLUSTER_NAME }} --region ${{ secrets.AWS_REGION }} + + - name: Deploy Helm Chart + run: | + helm upgrade --install my-app ./k8s/helm \ + --namespace dev \ + -f ./k8s/values/dev-values.yaml \ + --set image.tag=${{ env.sha_tag }} \ + --set aws.region=${{ secrets.AWS_REGION }} \ + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 549e00a..4c6cb0f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,21 @@ build/ ### VS Code ### .vscode/ +# Terraform State Files +*.tfstate +*.tfstate.backup +errored.tfstate + +# Ignore Terraform lock file +.terraform.lock.hcl + +# Ignore Terraform Caches and Modules +.terraform/ +.terraform/* + +# Ignore any sensitive variable files +*.auto.tfvars +*.tfvars.json + +# Ignore generated logs or debug files +crash.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f77bef3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# Build Stage +FROM eclipse-temurin:17-jdk-jammy AS builder + +WORKDIR /app + +# Copy Maven wrapper and pom.xml separately for better caching +COPY mvnw pom.xml ./ +COPY .mvn .mvn + +# Download dependencies (cache-friendly) +RUN ./mvnw dependency:go-offline + +# Copy source code +COPY src ./src + +# Package the application +RUN ./mvnw clean package -DskipTests + +# Runtime Stage (minimal image) +FROM eclipse-temurin:17-jre-jammy + +# Create a non-root user to run the app securely +RUN groupadd -r appgroup && useradd -r -g appgroup appuser + +WORKDIR /app + +# Copy jar from builder +COPY --from=builder /app/target/*.jar app.jar + +# Change ownership for security purposes +RUN chown appuser:appgroup app.jar + +USER appuser + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 23a3d90..a948321 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,179 @@ -# Crewmeister Challenge +# 🚀 Spring Boot + MySQL on Kubernetes -## Background +A production-grade Kubernetes deployment for a Spring Boot application backed by MySQL, built with security, scalability, and automation in mind. -At Crewmeister, our development team is continuously growing. We aim to hire the best educated, motivated, and enthusiastic people in the field who have fun building up Crewmeister in our vision to empower small businesses to thrive in a digital world. For this quest, we are continuously getting new applicants from all over the world. To filter which candidates could be a good fit, we provide our candidates with a coding challenge that we manually review and evaluate. +--- + +## 📆 Stack Overview + +| Layer | Tech/Tool | +|------------------------|-----------| +| Application | Spring Boot (Java 17) | +| Database | MySQL (StatefulSet + PVC) | +| Secrets Management | AWS Secrets Manager + External Secrets Operator | +| Kubernetes Templating | Helm | +| Env-specific Overrides | Kustomize | +| CI/CD Pipeline | GitHub Actions | +| Observability | Prometheus-ready integration hooks | + +--- + +## ✨ Key Features + +| Feature | Description | +|----------------------------------|-------------| +| ✅ Dockerized Spring Boot | With Flyway DB migrations | +| ✅ MySQL via StatefulSet | With persistent volumes | +| ✅ AWS Secrets Manager | Centralized secret storage | +| ✅ External Secrets Operator | Automatic sync of AWS secrets to K8s | +| ✅ Helm Charts | Clean, reusable templating | +| ✅ Kustomize Overlays | Per-environment configurations (dev, staging, prod) | +| ✅ TLS via cert-manager + Let's Encrypt | Secure HTTPS via Ingress | +| ✅ Ingress (NGINX) | With routing, HTTPS, and rewrite support | +| ✅ Horizontal Pod Autoscaler | Auto-scales app based on CPU | +| ✅ CI/CD Pipeline | GitHub Actions with multi-stage flow | +| ✅ Secure Image Handling | Versioned image tags, no `latest` used | +| ✅ Health Probes & Limits | Readiness/liveness probes and resource limits | + +--- + +## 📂 Folder Structure + +```bash +k8s/ +├── base/ # Common base manifests for Kustomize +│ ├── deployment.yaml +│ ├── mysql-service.yaml +│ ├── mysql-statefulset.yaml +│ ├── service.yaml +│ └── kustomization.yaml + +├── overlays/ # Environment-specific overlays (Kustomize) +│ ├── dev/ +│ │ ├── configmap.yaml +│ │ ├── kustomization.yaml +│ │ ├── mysql-external-secret.yaml +│ │ ├── namespace.yaml +│ │ ├── secret-store.yaml +│ │ └── cluster-issuer.yaml # cert-manager ClusterIssuer (staging/prod) +│ └── staging/ +│ └── prod/ + +├── helm/ # Helm chart for full app deployment +│ ├── Chart.yaml +│ ├── values.yaml # Default config +│ └── templates/ +│ ├── _helpers.tpl +│ ├── configmap-mysql.yaml +│ ├── secret-external-mysql.yaml +│ ├── secretstore-aws.yaml +│ ├── serviceaccount.yaml +│ ├── mysql/ +│ │ ├── service-mysql-headless.yaml +│ │ └── statefulset-mysql.yaml +│ ├── springboot/ +│ │ ├── deployment-springboot.yaml +│ │ ├── hpa-springboot.yaml +│ │ ├── ingress-springboot.yaml +│ │ └── service-springboot.yaml +│ └── tests/ +│ └── test-connection.yaml + +├── values/ # Helm values per environment +│ ├── dev-values.yaml +│ ├── staging-values.yaml +│ └── prod-values.yaml + +.github/ +└── workflows/ + └── crewmeister-ci.yaml # CI/CD pipeline definition +``` + +--- + +## 🛠️ Usage Guide + +### ✅ Apply Kustomize Overlay (Dev) + +```bash +kubectl apply -k k8s/overlays/dev +``` + +### ❌ Delete Dev Resources + +```bash +kubectl delete -k k8s/overlays/dev +``` --- -## DevOps Engineer Task +### 🥤 Helm Usage -As a DevOps Engineer at Crewmeister, you will be in charge of several challenging tasks in your daily work. One of your core responsibilities will be to ensure that the system is always running smoothly and that the application is deployed successfully to our customers. +```bash +# Install +helm install my-app ./k8s/helm -f ./k8s/values/dev-values.yaml -In this challenge, you should use DevOps best practices to architect and implement the complete cycle of building, packaging, and deploying a Java application (specified later in this document). +# Upgrade +helm upgrade my-app ./k8s/helm -f ./k8s/values/dev-values.yaml -The following are core technologies/tools that should be present in the solution: +# Uninstall +helm uninstall my-app +``` -- Dockerfile -- Helm Chart -- Terraform to interact with the Kubernetes cluster +--- -You have the flexibility to utilize any cloud provider of your choice to deploy and run the application effectively. Additionally, it should be designed to operate seamlessly on local machines, allowing for a versatile setup that caters to various operational preferences and environments. +### 🌐 Port Forward for Local Access -## Plus: +```bash +kubectl port-forward svc/my-app-service -n dev 8080:80 +``` -- Create a CI Pipeline in Github to automate the application lifecycle +Then access: [http://localhost:8080](http://localhost:8080) -- Add monitoring tools to check the health of the application +--- -## Important Points: +## 🔐 AWS Secrets Setup -- At Crewmeister, we value creativity and pushing for better. You are encouraged to expand the solution as you find fit. To do so, you must ensure high-quality documentation and that the base solution is correctly executed. -- All the tools used must be publicly accessible or explicitly documented on how to authenticate. -- All the tools must be free to use. +Before deploying, manually create the AWS credentials secret in your cluster: -## Challenge Application +```bash +kubectl create secret generic aws-credentials \ + --from-literal=access-key-id= \ + --from-literal=secret-access-key= +``` -A Spring Boot application that provides a simple user management REST API. +> **Note**: Never commit your AWS credentials to Git or store them in `values.yaml`. -### Technologies Used +--- -- Java 17 -- Spring Boot 3.3.5 -- MySQL Database -- Flyway Migration -- Maven -- Spring Data JPA -- Spring Actuator +## ⚙️ GitHub Actions CI/CD (`.github/workflows/crewmeister-ci.yaml`) -### Pre-requisites +| Stage | Description | +|---------------|-------------| +| `build` | Compiles the app with Maven | +| `code-quality`| Runs linting/tests | +| `docker` | Builds versioned image, pushes to Docker Hub | +| `updatek8s` | Deploys Helm chart to EKS using OIDC-authenticated GitHub Action | -- JDK 17 -- MySQL -- Maven +--- -### API Endpoints +## 🌟 Observability & Reliability -#### GET /user +- ✅ `readinessProbe` and `livenessProbe` configured +- ✅ `resources.requests` and `limits` defined for CPU/memory +- ✅ Horizontal Pod Autoscaler enabled for autoscaling +- ✅ Ingress TLS via cert-manager (staging & prod) +- ✅ Prepped for Prometheus metrics + +--- -Retrieves a user by ID +## ✅ Best Practices Followed -#### POST /user +- No use of `:latest` tag in Docker builds +- All secrets pulled dynamically via AWS Secret Manager +- Values managed via Helm `values.yaml` +- GitOps-compatible structure via Kustomize overlays +- Externalized environment handling +- Linted and validated YAML +- Modular and extensible folder layout -Creates a new user diff --git a/Terraform/.DS_Store b/Terraform/.DS_Store new file mode 100644 index 0000000..28697e2 Binary files /dev/null and b/Terraform/.DS_Store differ diff --git a/Terraform/backend/main.tf b/Terraform/backend/main.tf new file mode 100644 index 0000000..58ac351 --- /dev/null +++ b/Terraform/backend/main.tf @@ -0,0 +1,20 @@ +provider "aws" { + region = "us-west-2" +} +resource "aws_s3_bucket" "name" { + bucket = "terraform-eks-state-s3-bucket-crewmeister" + lifecycle { + prevent_destroy = false + } +} + +resource aws_dynamodb_table "name" { + name = "terraform-eks-state-lock-table-crewmeister" + billing_mode = "PAY_PER_REQUEST" + hash_key = "LockID" + attribute { + name = "LockID" + type = "S" + +} +} \ No newline at end of file diff --git a/Terraform/main.tf b/Terraform/main.tf new file mode 100644 index 0000000..56e5bab --- /dev/null +++ b/Terraform/main.tf @@ -0,0 +1,37 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + backend "s3" { + bucket = "terraform-eks-state-s3-bucket-crewmeister" + key = "terraform.tfstate" + region = "us-west-2" + dynamodb_table = "terraform-eks-state-lock-table-crewmeister" + encrypt = true + } +} +provider "aws" { + region = var.region +} + +module "vpc" { + source = "./modules/vpc" + vpc_cidr = var.vpc_cidr + availability_zones = var.availability_zones + public_subnet_cidrs = var.public_subnet_cidrs + private_subnet_cidrs = var.private_subnet_cidrs + cluster_name = var.cluster_name + } + +module "eks" { + source = "./modules/eks" + cluster_name = var.cluster_name + cluster_version = var.cluster_version + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnet_ids + node_groups = var.node_groups +} + diff --git a/Terraform/modules/.DS_Store b/Terraform/modules/.DS_Store new file mode 100644 index 0000000..16f4d22 Binary files /dev/null and b/Terraform/modules/.DS_Store differ diff --git a/Terraform/modules/eks/main.tf b/Terraform/modules/eks/main.tf new file mode 100644 index 0000000..e877916 --- /dev/null +++ b/Terraform/modules/eks/main.tf @@ -0,0 +1,179 @@ +resource "aws_iam_role" "cluster" { + name = "${var.cluster_name}-cluster-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "eks.amazonaws.com" + } + }] + }) +} +resource "aws_iam_role_policy_attachment" "cluster_policy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" + role = aws_iam_role.cluster.name +} +resource "aws_eks_cluster" "main" { + name = var.cluster_name + version = var.cluster_version + role_arn = aws_iam_role.cluster.arn + + vpc_config { + subnet_ids = var.subnet_ids + } + depends_on = [aws_iam_role_policy_attachment.cluster_policy] +} +resource "aws_iam_role" "node" { + name = "${var.cluster_name}-node-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + }] + }) +} +resource "aws_iam_role_policy_attachment" "node_policy" { + for_each = toset([ + "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy", + "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy", + "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ]) + + policy_arn = each.value + role = aws_iam_role.node.name +} +resource "aws_eks_node_group" "main" { + for_each = var.node_groups + + cluster_name = aws_eks_cluster.main.name + node_group_name = each.key + node_role_arn = aws_iam_role.node.arn + subnet_ids = var.subnet_ids + + instance_types = each.value.instance_types + capacity_type = each.value.capacity_type +scaling_config { + desired_size = each.value.scaling_config.desired_size + max_size = each.value.scaling_config.max_size + min_size = each.value.scaling_config.min_size + } + + depends_on = [ + aws_iam_role_policy_attachment.node_policy + ] +} +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = ["d89e3bd43d5d909b47a18977aa9d5ce36cee184c"] +} + +resource "aws_iam_role" "github_actions" { + name = "GitHubActions-EKS-Deploy" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + + Effect = "Allow" + Principal = { + Federated = "arn:aws:iam::535002870929:oidc-provider/token.actions.githubusercontent.com" + } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "StringLike": { + "token.actions.githubusercontent.com:sub": "repo:sugesh-cloudops/devops-coding-challenge:*" + } + } + } + ] + }) +} + +resource "aws_iam_policy" "github_eks_access" { + name = "GitHubEKSAccessPolicy" + description = "Allow GitHub Actions to assume the EKS role and interact with Kubernetes" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + # Allow assuming the GitHubActions-EKS-Deploy role + { + Effect = "Allow" + Action = "sts:AssumeRole" + Resource = "arn:aws:iam::535002870929:role/GitHubActions-EKS-Deploy" + }, + + # Allow EKS access + { + Effect = "Allow" + Action = [ + "eks:DescribeCluster", + "eks:ListClusters", + "eks:AccessKubernetesApi", + "eks:DescribeNodegroup", + "eks:ListNodegroups", + "eks:DescribeFargateProfile" + ] + Resource = "*" + }, + + # Allow IAM role management for GitHubActions-EKS-Deploy + { + Effect = "Allow" + Action = [ + "iam:GetRole", + "iam:PassRole", + "iam:LisOpenIDConnectProviders", + "iam:GetOpenIDConnectProvider" + ] + Resource = "arn:aws:iam::535002870929:role/GitHubActions-EKS-Deploy" + }, + + # Allow interaction with Kubernetes resources + { + Effect = "Allow" + Action = [ + "ec2:DescribeInstances", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeVpcs", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeRules", + "eks:ListTagsForResource" + ] + Resource = "*" + }, + + # Allow read-only access to logs for debugging + { + Effect = "Allow" + Action = [ + "logs:GetLogEvents", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams" + ] + Resource = "*" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "github_eks_attach" { + policy_arn = aws_iam_policy.github_eks_access.arn + role = aws_iam_role.github_actions.name +} \ No newline at end of file diff --git a/Terraform/modules/eks/outputs.tf b/Terraform/modules/eks/outputs.tf new file mode 100644 index 0000000..7a4d727 --- /dev/null +++ b/Terraform/modules/eks/outputs.tf @@ -0,0 +1,12 @@ +output "cluster_endpoint" { + description = "EKS cluster endpoint" + value = aws_eks_cluster.main.endpoint +} +output "cluster_name" { + description = "EKS cluster name" + value = aws_eks_cluster.main.name +} +output "github_actions_role_arn" { + description = "ARN of the IAM role for GitHub Actions" + value = aws_iam_role.github_actions.arn +} \ No newline at end of file diff --git a/Terraform/modules/eks/variables.tf b/Terraform/modules/eks/variables.tf new file mode 100644 index 0000000..0187981 --- /dev/null +++ b/Terraform/modules/eks/variables.tf @@ -0,0 +1,28 @@ +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string +} +variable "cluster_version" { + description = "Kubernetes version" + type = string +} +variable "vpc_id" { + description = "VPC ID" + type = string +} +variable "subnet_ids" { + description = "Subnet IDs" + type = list(string) +} +variable "node_groups" { + description = "EKS node group configuration" + type = map(object({ + instance_types = list(string) + capacity_type = string + scaling_config = object({ + desired_size = number + max_size = number + min_size = number + }) + })) +} \ No newline at end of file diff --git a/Terraform/modules/vpc/main.tf b/Terraform/modules/vpc/main.tf new file mode 100644 index 0000000..bc22302 --- /dev/null +++ b/Terraform/modules/vpc/main.tf @@ -0,0 +1,101 @@ +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + enable_dns_hostnames = true + enable_dns_support = true + tags = { + Name = "${var.cluster_name}-vpc" + "kubernetes.io/cluster/${var.cluster_name}" = "shared" + } +} +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 = var.availability_zones[count.index] + + tags = { + Name = "${var.cluster_name}-private-${count.index + 1}" + "kubernetes.io/cluster/${var.cluster_name}" = "shared" + "kubernetes.io/role/internal-elb" = "1" + } +} +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 = var.availability_zones[count.index] + + map_public_ip_on_launch = true + + tags = { + Name = "${var.cluster_name}-public-${count.index + 1}" + "kubernetes.io/cluster/${var.cluster_name}" = "shared" + "kubernetes.io/role/elb" = "1" + } +} + +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.cluster_name}-igw" + } +} + +resource "aws_eip" "nat" { + count = length(var.public_subnet_cidrs) + domain = "vpc" + + tags = { + Name = "${var.cluster_name}-nat-${count.index + 1}" + } +} + +resource "aws_nat_gateway" "main" { + count = length(var.public_subnet_cidrs) + allocation_id = aws_eip.nat[count.index].id + subnet_id = aws_subnet.public[count.index].id + + tags = { + Name = "${var.cluster_name}-nat-${count.index + 1}" + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + + tags = { + Name = "${var.cluster_name}-public" + } +} + +resource "aws_route_table" "private" { + count = length(var.private_subnet_cidrs) + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main[count.index].id + } + + tags = { + Name = "${var.cluster_name}-private-${count.index + 1}" + } +} + +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[count.index].id +} + +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 +} \ No newline at end of file diff --git a/Terraform/modules/vpc/outputs.tf b/Terraform/modules/vpc/outputs.tf new file mode 100644 index 0000000..4ff85f4 --- /dev/null +++ b/Terraform/modules/vpc/outputs.tf @@ -0,0 +1,14 @@ +output "vpc_id" { + description = "VPC ID" + value = aws_vpc.main.id +} + +output "private_subnet_ids" { + description = "Private subnet IDs" + value = aws_subnet.private[*].id +} + +output "public_subnet_ids" { + description = "Public subnet IDs" + value = aws_subnet.public[*].id +} \ No newline at end of file diff --git a/Terraform/modules/vpc/variables.tf b/Terraform/modules/vpc/variables.tf new file mode 100644 index 0000000..50fe184 --- /dev/null +++ b/Terraform/modules/vpc/variables.tf @@ -0,0 +1,24 @@ +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string +} + +variable "availability_zones" { + description = "Availability zones" + type = list(string) +} + +variable "private_subnet_cidrs" { + description = "CIDR blocks for private subnets" + type = list(string) +} + +variable "public_subnet_cidrs" { + description = "CIDR blocks for public subnets" + type = list(string) +} + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string +} \ No newline at end of file diff --git a/Terraform/outputs.tf b/Terraform/outputs.tf new file mode 100644 index 0000000..b8e3a1f --- /dev/null +++ b/Terraform/outputs.tf @@ -0,0 +1,13 @@ +output "cluster_endpoint" { + description = "EKS cluster endpoint" + value = module.eks.cluster_endpoint + +} +output "cluster_name" { + description = "EKS cluster name" + value = module.eks.cluster_name +} +output "vpc_id" { + description = "VPC ID" + value = module.vpc.vpc_id +} \ No newline at end of file diff --git a/Terraform/variables.tf b/Terraform/variables.tf new file mode 100644 index 0000000..a2bb6ab --- /dev/null +++ b/Terraform/variables.tf @@ -0,0 +1,61 @@ +variable "region" { + description = "AWS region" + type = string + default = "us-west-2" + +} +variable "vpc_cidr" { + description = "CIDR block for the VPC" + type = string + default = "10.0.0.0/16" + +} +variable "availability_zones" { + description = "Availability zones" + type = list(string) + default = ["us-west-2a", "us-west-2b", "us-west-2c"] + +} +variable "private_subnet_cidrs" { + description = "CIDR blocks for the private subnets" + type = list(string) + default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +} +variable "public_subnet_cidrs" { + description = "CIDR blocks for public subnets" + type = list(string) + default = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] +} +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string + default = "Crewmeister-cluster" +} +variable "cluster_version" { + description = "Kubernetes version" + type = string + default = "1.30" +} +variable "node_groups" { + description = "EKS node group configuration" + type = map(object({ + instance_types = list(string) + capacity_type = string + scaling_config = object({ + desired_size = number + max_size = number + min_size = number + }) + })) + default = { + general ={ + instance_types = ["t2.medium"] + capacity_type = "ON_DEMAND" + scaling_config = { + desired_size = 3 + max_size = 4 + min_size = 2 + } + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..978a642 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + db: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: dev + MYSQL_DATABASE: challenge + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 30s + timeout: 10s + retries: 10 + + app: + build: . + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/challenge?createDatabaseIfNotExist=true + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: dev + ports: + - "8080:8080" + depends_on: + db: + condition: service_healthy + \ No newline at end of file diff --git a/image.png b/image.png new file mode 100644 index 0000000..17b2748 Binary files /dev/null and b/image.png differ diff --git a/k8s/.DS_Store b/k8s/.DS_Store new file mode 100644 index 0000000..043f4de Binary files /dev/null and b/k8s/.DS_Store differ diff --git a/k8s/base/.DS_Store b/k8s/base/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/k8s/base/.DS_Store differ diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml new file mode 100644 index 0000000..9564e60 --- /dev/null +++ b/k8s/base/deployment.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app + labels: + app: springboot-app +spec: + replicas: 1 + selector: + matchLabels: + app: springboot-app + template: + metadata: + labels: + app: springboot-app + spec: + containers: + - name: springboot-app + image: sugesh23/springboot:latest + ports: + - containerPort: 8080 + env: + - name: SPRING_APPLICATION_NAME + value: app + - name: SPRING_DATASOURCE_USERNAME + value: root + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-secret + key: mysql-root-password + - name: SPRING_DATASOURCE_URL + value: jdbc:mysql://mysql-0.mysql-headless:3306/challenge?createDatabaseIfNotExist=true + - name: SPRING_DATASOURCE_WRITER_URL + value: jdbc:mysql://mysql-0.mysql-headless:3306/challenge?createDatabaseIfNotExist=true + - name: ACTUATOR_ENDPOINTS + value: health,info,prometheus + readinessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 \ No newline at end of file diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml new file mode 100644 index 0000000..c2b581a --- /dev/null +++ b/k8s/base/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - deployment.yaml + - service.yaml + - mysql-statefulset.yaml + - mysql-service.yaml diff --git a/k8s/base/mysql-service.yaml b/k8s/base/mysql-service.yaml new file mode 100644 index 0000000..e878d74 --- /dev/null +++ b/k8s/base/mysql-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql-headless +spec: + ports: + - port: 3306 + name: mysql + clusterIP: None + selector: + app: mysql \ No newline at end of file diff --git a/k8s/base/mysql-statefulset.yaml b/k8s/base/mysql-statefulset.yaml new file mode 100644 index 0000000..a286be1 --- /dev/null +++ b/k8s/base/mysql-statefulset.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mysql +spec: + serviceName: mysql-headless + replicas: 1 + selector: + matchLabels: + app: mysql + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-secret + key: mysql-root-password + - name: MYSQL_DATABASE + valueFrom: + configMapKeyRef: + name: mysql-config + key: database-name + ports: + - containerPort: 3306 + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + volumeClaimTemplates: + - metadata: + name: mysql-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi \ No newline at end of file diff --git a/k8s/base/service.yaml b/k8s/base/service.yaml new file mode 100644 index 0000000..f49aa4e --- /dev/null +++ b/k8s/base/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: my-app-service + labels: + app: springboot-app +spec: + selector: + app: springboot-app + ports: + - port: 80 + targetPort: 8080 + type: LoadBalancer \ No newline at end of file diff --git a/k8s/helm/.DS_Store b/k8s/helm/.DS_Store new file mode 100644 index 0000000..a580bdf Binary files /dev/null and b/k8s/helm/.DS_Store differ diff --git a/k8s/helm/.helmignore b/k8s/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/k8s/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/k8s/helm/Chart.yaml b/k8s/helm/Chart.yaml new file mode 100644 index 0000000..88dce6c --- /dev/null +++ b/k8s/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: crew-mysql-chart +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/k8s/helm/templates/_helpers.tpl b/k8s/helm/templates/_helpers.tpl new file mode 100644 index 0000000..d8b9e3d --- /dev/null +++ b/k8s/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "crew-mysql-chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "crew-mysql-chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "crew-mysql-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "crew-mysql-chart.labels" -}} +helm.sh/chart: {{ include "crew-mysql-chart.chart" . }} +{{ include "crew-mysql-chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "crew-mysql-chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "crew-mysql-chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "crew-mysql-chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "crew-mysql-chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/k8s/helm/templates/cert-manager-clusterissuer.yaml b/k8s/helm/templates/cert-manager-clusterissuer.yaml new file mode 100644 index 0000000..31e0ccc --- /dev/null +++ b/k8s/helm/templates/cert-manager-clusterissuer.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: {{ .Values.certManager.clusterIssuerName }} +spec: + acme: + server: {{ .Values.certManager.acmeServer }} + email: {{ .Values.certManager.email }} + privateKeySecretRef: + name: {{ .Values.certManager.secretName }} + solvers: + - http01: + ingress: + class: nginx diff --git a/k8s/helm/templates/configmap-mysql.yaml b/k8s/helm/templates/configmap-mysql.yaml new file mode 100644 index 0000000..c81b6f9 --- /dev/null +++ b/k8s/helm/templates/configmap-mysql.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mysql-config +data: + database-name: {{ .Values.mysql.database }} \ No newline at end of file diff --git a/k8s/helm/templates/mysql/service-mysql-headless.yaml b/k8s/helm/templates/mysql/service-mysql-headless.yaml new file mode 100644 index 0000000..e878d74 --- /dev/null +++ b/k8s/helm/templates/mysql/service-mysql-headless.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql-headless +spec: + ports: + - port: 3306 + name: mysql + clusterIP: None + selector: + app: mysql \ No newline at end of file diff --git a/k8s/helm/templates/mysql/statefulset-mysql.yaml b/k8s/helm/templates/mysql/statefulset-mysql.yaml new file mode 100644 index 0000000..4570745 --- /dev/null +++ b/k8s/helm/templates/mysql/statefulset-mysql.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mysql +spec: + replicas: 1 + serviceName: mysql-headless + selector: + matchLabels: + app: mysql + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: {{ .Values.mysql.image }} + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.mysql.secretKeyRef.name }} + key: {{ .Values.mysql.secretKeyRef.key }} + - name: MYSQL_DATABASE + valueFrom: + configMapKeyRef: + name: mysql-config + key: database-name + ports: + - containerPort: {{ .Values.mysql.port }} + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + volumeClaimTemplates: + - metadata: + name: mysql-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.mysql.pvc.storage }} \ No newline at end of file diff --git a/k8s/helm/templates/secret-external-mysql.yaml b/k8s/helm/templates/secret-external-mysql.yaml new file mode 100644 index 0000000..e6da7e1 --- /dev/null +++ b/k8s/helm/templates/secret-external-mysql.yaml @@ -0,0 +1,17 @@ +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: mysql-secret + namespace: default +spec: + refreshInterval: 1h + secretStoreRef: + name: aws-secrets-store + kind: SecretStore + target: + name: mysql-secret + data: + - secretKey: {{ .Values.aws.secretProperty }} + remoteRef: + key: {{ .Values.aws.secretName }} + property: {{ .Values.aws.secretProperty }} \ No newline at end of file diff --git a/k8s/helm/templates/secretstore-aws.yaml b/k8s/helm/templates/secretstore-aws.yaml new file mode 100644 index 0000000..7ee9b1d --- /dev/null +++ b/k8s/helm/templates/secretstore-aws.yaml @@ -0,0 +1,17 @@ +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: aws-secrets-store +spec: + provider: + aws: + service: SecretsManager + region: {{ .Values.aws.region }} + auth: + secretRef: + accessKeyIDSecretRef: + name: {{ .Values.aws.credentialsSecretRef }} + key: access-key-id + secretAccessKeySecretRef: + name: {{ .Values.aws.credentialsSecretRef }} + key: secret-access-key \ No newline at end of file diff --git a/k8s/helm/templates/serviceaccount.yaml b/k8s/helm/templates/serviceaccount.yaml new file mode 100644 index 0000000..547691b --- /dev/null +++ b/k8s/helm/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: my-app-sa + labels: + app: {{ .Release.Name }} \ No newline at end of file diff --git a/k8s/helm/templates/springboot/deployment-springboot.yaml b/k8s/helm/templates/springboot/deployment-springboot.yaml new file mode 100644 index 0000000..df25e01 --- /dev/null +++ b/k8s/helm/templates/springboot/deployment-springboot.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-app + labels: + app: {{ .Values.app.name }} +spec: + replicas: {{ .Values.app.replicaCount }} + selector: + matchLabels: + app: {{ .Values.app.name }} + template: + metadata: + labels: + app: {{ .Values.app.name }} + annotations: + prometheus.io/scrape: "true" + prometheus.io/path: "/actuator/prometheus" + prometheus.io/port: "8080" + spec: + serviceAccountName: my-app-sa + containers: + - name: {{ .Values.app.name }} + image: {{ .Values.app.image }}:{{ .Values.app.tag }} + ports: + - containerPort: {{ .Values.readinessProbe.port }} + env: + - name: SPRING_APPLICATION_NAME + value: {{ .Values.app.name }} + - name: SPRING_DATASOURCE_USERNAME + value: root + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.mysql.secretKeyRef.name }} + key: {{ .Values.mysql.secretKeyRef.key }} + - name: SPRING_DATASOURCE_URL + value: jdbc:mysql://mysql-0.mysql-headless:{{ .Values.mysql.port }}/{{ .Values.mysql.database }}?createDatabaseIfNotExist=true + - name: SPRING_DATASOURCE_WRITER_URL + value: jdbc:mysql://mysql-0.mysql-headless:{{ .Values.mysql.port }}/{{ .Values.mysql.database }}?createDatabaseIfNotExist=true + - name: ACTUATOR_ENDPOINTS + value: health,info,prometheus + readinessProbe: + httpGet: + path: {{ .Values.readinessProbe.path }} + port: {{ .Values.readinessProbe.port }} + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + livenessProbe: + httpGet: + path: {{ .Values.livenessProbe.path }} + port: {{ .Values.livenessProbe.port }} + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.livenessProbe.periodSeconds }} \ No newline at end of file diff --git a/k8s/helm/templates/springboot/hpa-springboot.yaml b/k8s/helm/templates/springboot/hpa-springboot.yaml new file mode 100644 index 0000000..9f14913 --- /dev/null +++ b/k8s/helm/templates/springboot/hpa-springboot.yaml @@ -0,0 +1,12 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Release.Name }}-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ .Release.Name }}-app + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 50 \ No newline at end of file diff --git a/k8s/helm/templates/springboot/ingress-springboot.yaml b/k8s/helm/templates/springboot/ingress-springboot.yaml new file mode 100644 index 0000000..ccefc59 --- /dev/null +++ b/k8s/helm/templates/springboot/ingress-springboot.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: springboot-ingress + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/rewrite-target: / + cert-manager.io/cluster-issuer: {{ .Values.ingress.clusterIssuer }} +spec: + ingressClassName: nginx + tls: + - hosts: + - {{ .Values.ingress.host }} + secretName: {{ .Values.ingress.tlsSecretName }} + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: springboot-service + port: + number: {{ .Values.ingress.servicePort }} \ No newline at end of file diff --git a/k8s/helm/templates/springboot/service-springboot.yaml b/k8s/helm/templates/springboot/service-springboot.yaml new file mode 100644 index 0000000..048d19d --- /dev/null +++ b/k8s/helm/templates/springboot/service-springboot.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.name }} +spec: + type: {{ .Values.service.type }} + selector: + app: {{ .Values.app.name }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} diff --git a/k8s/helm/templates/tests/test-connection.yaml b/k8s/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..f921c18 --- /dev/null +++ b/k8s/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "crew-mysql-chart.fullname" . }}-test-connection" + labels: + {{- include "crew-mysql-chart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "crew-mysql-chart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/k8s/helm/values.yaml b/k8s/helm/values.yaml new file mode 100644 index 0000000..6228499 --- /dev/null +++ b/k8s/helm/values.yaml @@ -0,0 +1,67 @@ +app: + name: app + image: sugesh23/springboot + tag: latest # Will be dynamically set via CI/CD (e.g., commit SHA) + replicaCount: 1 + +mysql: + database: challenge + image: mysql:8 + port: 3306 + pvc: + storage: 1Gi + secretKeyRef: + name: mysql-secret + key: mysql-root-password + +service: + name: my-app-service + type: ClusterIP + port: 80 + targetPort: 8080 + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 50 + +readinessProbe: + path: /actuator/health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + +livenessProbe: + path: /actuator/health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + +resources: + limits: + cpu: 250m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + +ingress: + enabled: true + host: crewmiester.test.com + servicePort: 8080 + clusterIssuer: letsencrypt-staging + tlsSecretName: springboot-tls + +certManager: + enabled: true + email: crewmeister-test@test.com + clusterIssuerName: letsencrypt-staging + secretName: letsencrypt-prod-key + acmeServer: https://acme-v02.api.letsencrypt.org/directory + +aws: + region: us-west-2 + secretName: devops-challenge-secret + secretProperty: mysql-root-password + credentialsSecretRef: aws-credentials \ No newline at end of file diff --git a/k8s/overlays/.DS_Store b/k8s/overlays/.DS_Store new file mode 100644 index 0000000..c4fae47 Binary files /dev/null and b/k8s/overlays/.DS_Store differ diff --git a/k8s/overlays/dev/cluster-issuer.yaml b/k8s/overlays/dev/cluster-issuer.yaml new file mode 100644 index 0000000..7c66c11 --- /dev/null +++ b/k8s/overlays/dev/cluster-issuer.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: test-crewmeister@test.com + privateKeySecretRef: + name: letsencrypt-staging-key + solvers: + - http01: + ingress: + class: nginx \ No newline at end of file diff --git a/k8s/overlays/dev/configmap.yaml b/k8s/overlays/dev/configmap.yaml new file mode 100644 index 0000000..7f414a2 --- /dev/null +++ b/k8s/overlays/dev/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mysql-config +data: + database-name: challenge \ No newline at end of file diff --git a/k8s/overlays/dev/kustomization.yaml b/k8s/overlays/dev/kustomization.yaml new file mode 100644 index 0000000..4fbf07c --- /dev/null +++ b/k8s/overlays/dev/kustomization.yaml @@ -0,0 +1,11 @@ +namespace: dev + +resources: + - ../../base + - namespace.yaml + - secret-store.yaml + - configmap.yaml + - mysql-external-secret.yaml + - cluster-issuer.yaml + +patches: [] diff --git a/k8s/overlays/dev/mysql-external-secret.yaml b/k8s/overlays/dev/mysql-external-secret.yaml new file mode 100644 index 0000000..5395727 --- /dev/null +++ b/k8s/overlays/dev/mysql-external-secret.yaml @@ -0,0 +1,18 @@ +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: mysql-secret + namespace: dev +spec: + refreshInterval: 1h + secretStoreRef: + name: aws-secrets-store + kind: SecretStore + target: + name: mysql-secret + creationPolicy: Owner + data: + - secretKey: mysql-root-password + remoteRef: + key: devops-challenge-secret + property: mysql-root-password \ No newline at end of file diff --git a/k8s/overlays/dev/namespace.yaml b/k8s/overlays/dev/namespace.yaml new file mode 100644 index 0000000..8cab297 --- /dev/null +++ b/k8s/overlays/dev/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: dev diff --git a/k8s/overlays/dev/secret-store.yaml b/k8s/overlays/dev/secret-store.yaml new file mode 100644 index 0000000..b8796c9 --- /dev/null +++ b/k8s/overlays/dev/secret-store.yaml @@ -0,0 +1,18 @@ +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: aws-secrets-store + namespace: default +spec: + provider: + aws: + service: SecretsManager + region: us-west-2 + auth: + secretRef: + accessKeyIDSecretRef: + name: aws-credentials + key: access-key-id + secretAccessKeySecretRef: + name: aws-credentials + key: secret-access-key \ No newline at end of file diff --git a/k8s/values/dev-values.yaml b/k8s/values/dev-values.yaml new file mode 100644 index 0000000..97a3046 --- /dev/null +++ b/k8s/values/dev-values.yaml @@ -0,0 +1,67 @@ +app: + name: app + image: sugesh23/springboot + tag: latest # Will be dynamically set via CI/CD (e.g., commit SHA) + replicaCount: 1 + +mysql: + database: challenge + image: mysql:8 + port: 3306 + pvc: + storage: 1Gi + secretKeyRef: + name: mysql-secret + key: mysql-root-password + +service: + name: my-app-service + type: ClusterIP + port: 80 + targetPort: 8080 + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 50 + +readinessProbe: + path: /actuator/health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + +livenessProbe: + path: /actuator/health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + +resources: + limits: + cpu: 250m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + +ingress: + enabled: true + host: crewmiester.test.com + servicePort: 8080 + clusterIssuer: letsencrypt-staging + tlsSecretName: springboot-tls + +certManager: + enabled: true + email: crewmeister-test@test.com + clusterIssuerName: letsencrypt-staging + secretName: letsencrypt-prod-key + acmeServer: https://acme-v02.api.letsencrypt.org/directory + +aws: + region: us-west-2 + secretName: devops-challenge-secret + secretProperty: mysql-root-password + credentialsSecretRef: aws-credentials # Referenced in SecretStore \ No newline at end of file diff --git a/k8s/values/prod-values.yaml b/k8s/values/prod-values.yaml new file mode 100644 index 0000000..a83311c --- /dev/null +++ b/k8s/values/prod-values.yaml @@ -0,0 +1,67 @@ +app: + name: app + image: sugesh23/springboot + tag: latest # Will be dynamically set via CI/CD (e.g., commit SHA) + replicaCount: 1 + +mysql: + database: challenge + image: mysql:8 + port: 3306 + pvc: + storage: 1Gi + secretKeyRef: + name: mysql-secret + key: mysql-root-password + +service: + name: my-app-service + type: ClusterIP + port: 80 + targetPort: 8080 + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 50 + +readinessProbe: + path: /actuator/health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + +livenessProbe: + path: /actuator/health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + +resources: + limits: + cpu: 250m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + +ingress: + enabled: true + host: crewmiester.test.com + servicePort: 8080 + clusterIssuer: letsencrypt-prod + tlsSecretName: springboot-tls + +certManager: + enabled: true + email: crewmeister-test@test.com + clusterIssuerName: letsencrypt-staging + secretName: letsencrypt-prod-key + acmeServer: https://acme-v02.api.letsencrypt.org/directory + +aws: + region: us-west-2 + secretName: devops-challenge-secret + secretProperty: mysql-root-password + credentialsSecretRef: aws-credentials \ No newline at end of file diff --git a/k8s/values/staging-values.yaml b/k8s/values/staging-values.yaml new file mode 100644 index 0000000..67362ac --- /dev/null +++ b/k8s/values/staging-values.yaml @@ -0,0 +1,34 @@ +app: + name: app + image: sugesh23/springboot:latest + replicaCount: 1 + +mysql: + database: challenge + +service: + type: LoadBalancer + port: 80 + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 50 + +readinessProbe: + path: /actuator/health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + +livenessProbe: + path: /actuator/health + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + +aws: + region: us-west-2 + secretName: devops-challenge-secret + secretProperty: mysql-root-password \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f31e7f2..a63dc09 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,10 +4,10 @@ spring: datasource: username: ${SPRING_DATASOURCE_USERNAME:root} password: ${SPRING_DATASOURCE_PASSWORD:dev} - url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/challenge?createDatabaseIfNotExist=true} + url: ${SPRING_DATASOURCE_URL:jdbc:mysql://db:3306/challenge?createDatabaseIfNotExist=true} driver-class-name: com.mysql.cj.jdbc.Driver flyway: - url: ${SPRING_DATASOURCE_WRITER_URL:jdbc:mysql://localhost:3306/challenge?createDatabaseIfNotExist=true} + url: ${SPRING_DATASOURCE_URL:jdbc:mysql://db:3306/challenge?createDatabaseIfNotExist=true} user: ${SPRING_DATASOURCE_USERNAME:root} password: ${SPRING_DATASOURCE_PASSWORD:dev} enabled: true @@ -17,4 +17,5 @@ management: endpoints: web: exposure: - include: ${ACTUATOR_ENDPOINTS:health,info,prometheus} \ No newline at end of file + include: ${ACTUATOR_ENDPOINTS:health,info,prometheus} +