Initial commit: Resume site with Flux CD automation
- Add HTML comment for hiring pipeline - Configure Helm chart for Kubernetes deployment - Set up ingress for resume.caffeinetux.com - Configure Harbor registry at images.caffeinetux.com - Add Flux CD manifests for GitOps deployment - Update CI workflow for Harbor integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
20
.dockerignore
Normal file
20
.dockerignore
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Helm chart (not needed in Docker image)
|
||||||
|
helm/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
106
.github/workflows/ci.yaml
vendored
Normal file
106
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
name: Build and Deploy Resume
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'index.html'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'helm/**'
|
||||||
|
- '.github/workflows/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: images.caffeinetux.com
|
||||||
|
IMAGE_NAME: production/resume-site
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Harbor Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.HARBOR_USERNAME }}
|
||||||
|
password: ${{ secrets.HARBOR_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=sha,prefix=
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
helm-lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Helm
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.13.0
|
||||||
|
|
||||||
|
- name: Lint Helm chart
|
||||||
|
run: helm lint ./helm
|
||||||
|
|
||||||
|
- name: Template Helm chart
|
||||||
|
run: |
|
||||||
|
helm template resume ./helm \
|
||||||
|
--set image.repository=test \
|
||||||
|
--set ingress.hosts[0].host=test.example.com \
|
||||||
|
--set ingress.hosts[0].paths[0].path=/ \
|
||||||
|
--set ingress.hosts[0].paths[0].pathType=Prefix
|
||||||
|
|
||||||
|
# Optional: Deploy to cluster (uncomment and configure)
|
||||||
|
# deploy:
|
||||||
|
# needs: [build, helm-lint]
|
||||||
|
# if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - name: Checkout
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
#
|
||||||
|
# - name: Set up Helm
|
||||||
|
# uses: azure/setup-helm@v3
|
||||||
|
#
|
||||||
|
# - name: Configure kubectl
|
||||||
|
# uses: azure/k8s-set-context@v3
|
||||||
|
# with:
|
||||||
|
# kubeconfig: ${{ secrets.KUBECONFIG }}
|
||||||
|
#
|
||||||
|
# - name: Deploy to Kubernetes
|
||||||
|
# run: |
|
||||||
|
# helm upgrade --install resume ./helm \
|
||||||
|
# --namespace resume \
|
||||||
|
# --create-namespace \
|
||||||
|
# --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
|
||||||
|
# --set image.tag=${{ github.sha }} \
|
||||||
|
# --wait
|
||||||
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy the resume site
|
||||||
|
COPY index.html /usr/share/nginx/html/index.html
|
||||||
|
|
||||||
|
# Custom nginx config for SPA routing
|
||||||
|
RUN echo 'server { \
|
||||||
|
listen 80; \
|
||||||
|
server_name _; \
|
||||||
|
root /usr/share/nginx/html; \
|
||||||
|
index index.html; \
|
||||||
|
location / { \
|
||||||
|
try_files $uri $uri/ /index.html; \
|
||||||
|
} \
|
||||||
|
location ~* \.(html|css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { \
|
||||||
|
expires 1d; \
|
||||||
|
add_header Cache-Control "public, immutable"; \
|
||||||
|
} \
|
||||||
|
gzip on; \
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; \
|
||||||
|
}' > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
165
README.md
Normal file
165
README.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Nicholas Haven - Resume Website
|
||||||
|
|
||||||
|
A professional, responsive resume website designed for deployment on Kubernetes.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Modern Design**: Dark theme with cyan accents, animated backgrounds, and smooth transitions
|
||||||
|
- **Responsive**: Works on all device sizes
|
||||||
|
- **Production-Ready**: Includes Dockerfile and Helm chart
|
||||||
|
- **Kubernetes-Native**: Configured with health checks, PDB, HPA, and proper security contexts
|
||||||
|
- **TLS Ready**: Pre-configured for cert-manager integration
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Build the Docker Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build locally
|
||||||
|
docker build -t resume-site:latest .
|
||||||
|
|
||||||
|
# Or build and push to your registry
|
||||||
|
docker build -t your-registry.com/resume-site:latest .
|
||||||
|
docker push your-registry.com/resume-site:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Deploy with Helm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create namespace (optional)
|
||||||
|
kubectl create namespace resume
|
||||||
|
|
||||||
|
# Install with custom values
|
||||||
|
helm upgrade --install resume ./helm \
|
||||||
|
--namespace resume \
|
||||||
|
--set image.repository=your-registry.com/resume-site \
|
||||||
|
--set image.tag=latest \
|
||||||
|
--set ingress.hosts[0].host=resume.yourdomain.com \
|
||||||
|
--set ingress.hosts[0].paths[0].path=/ \
|
||||||
|
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||||
|
--set ingress.tls[0].secretName=resume-tls \
|
||||||
|
--set ingress.tls[0].hosts[0]=resume.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Using a Values File (Recommended)
|
||||||
|
|
||||||
|
Create a `values-production.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
image:
|
||||||
|
repository: your-registry.com/resume-site
|
||||||
|
tag: "1.0.0"
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: nginx
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
hosts:
|
||||||
|
- host: resume.yourdomain.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: resume-tls
|
||||||
|
hosts:
|
||||||
|
- resume.yourdomain.com
|
||||||
|
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 64Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
|
```
|
||||||
|
|
||||||
|
Then deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm upgrade --install resume ./helm \
|
||||||
|
--namespace resume \
|
||||||
|
-f values-production.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|-----------|-------------|---------|
|
||||||
|
| `replicaCount` | Number of pods | `2` |
|
||||||
|
| `image.repository` | Docker image repository | `resume-site` |
|
||||||
|
| `image.tag` | Docker image tag | `latest` |
|
||||||
|
| `ingress.enabled` | Enable ingress | `true` |
|
||||||
|
| `ingress.className` | Ingress class name | `nginx` |
|
||||||
|
| `ingress.hosts` | Ingress hosts configuration | `[]` |
|
||||||
|
| `autoscaling.enabled` | Enable HPA | `false` |
|
||||||
|
| `podDisruptionBudget.enabled` | Enable PDB | `true` |
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with Docker
|
||||||
|
docker build -t resume-site:dev .
|
||||||
|
docker run -p 8080:80 resume-site:dev
|
||||||
|
|
||||||
|
# Open http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── index.html # Main resume page
|
||||||
|
├── Dockerfile # Multi-stage Docker build
|
||||||
|
├── README.md # This file
|
||||||
|
└── helm/
|
||||||
|
├── Chart.yaml # Helm chart metadata
|
||||||
|
├── values.yaml # Default values
|
||||||
|
└── templates/
|
||||||
|
├── _helpers.tpl
|
||||||
|
├── deployment.yaml
|
||||||
|
├── service.yaml
|
||||||
|
├── ingress.yaml
|
||||||
|
├── serviceaccount.yaml
|
||||||
|
├── pdb.yaml
|
||||||
|
└── hpa.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Updating Content
|
||||||
|
|
||||||
|
Edit `index.html` directly. The file is self-contained with embedded CSS.
|
||||||
|
|
||||||
|
### Changing Colors
|
||||||
|
|
||||||
|
CSS variables are defined at the top of the `<style>` block:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--accent-cyan: #06b6d4;
|
||||||
|
--accent-emerald: #10b981;
|
||||||
|
--accent-violet: #8b5cf6;
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Sections
|
||||||
|
|
||||||
|
Follow the existing pattern using `section` elements with appropriate class names.
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
The nginx configuration includes a `/health` endpoint that returns `200 OK` for Kubernetes probes.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Runs as non-root user (nginx:101)
|
||||||
|
- Drops all capabilities
|
||||||
|
- Configured with proper security contexts
|
||||||
|
- TLS termination at ingress level
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Built with clean code and deployed on Kubernetes* ☸️
|
||||||
86
flux/README.md
Normal file
86
flux/README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Flux Deployment for Resume Site
|
||||||
|
|
||||||
|
This directory contains Flux CD manifests for automated deployment of the resume site to Kubernetes.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Flux CD installed in your cluster
|
||||||
|
2. Gitea repository created and pushed
|
||||||
|
3. Harbor credentials configured
|
||||||
|
4. Docker image built and pushed to Harbor
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Update GitRepository URL
|
||||||
|
|
||||||
|
Edit `gitrepository.yaml` and replace the placeholder URL with your actual Gitea repository URL:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: https://your-gitea-url/username/resume-site.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build and Push Docker Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login to Harbor
|
||||||
|
docker login images.caffeinetux.com
|
||||||
|
|
||||||
|
# Build the image
|
||||||
|
docker build -t images.caffeinetux.com/production/resume-site:latest .
|
||||||
|
|
||||||
|
# Push to Harbor
|
||||||
|
docker push images.caffeinetux.com/production/resume-site:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy with Flux
|
||||||
|
|
||||||
|
Apply the Flux manifests to your cluster:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -k flux/
|
||||||
|
```
|
||||||
|
|
||||||
|
Flux will:
|
||||||
|
- Clone the Git repository
|
||||||
|
- Deploy the Helm chart from `./helm`
|
||||||
|
- Create an Ingress at https://resume.caffeinetux.com
|
||||||
|
- Automatically sync changes from Git
|
||||||
|
|
||||||
|
### 4. Verify Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Flux GitRepository
|
||||||
|
kubectl get gitrepository -n flux-system resume-site
|
||||||
|
|
||||||
|
# Check Flux HelmRelease
|
||||||
|
kubectl get helmrelease -n default resume-site
|
||||||
|
|
||||||
|
# Check pods
|
||||||
|
kubectl get pods -n default -l app.kubernetes.io/name=resume-site
|
||||||
|
|
||||||
|
# Check ingress
|
||||||
|
kubectl get ingress -n default
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatic Updates
|
||||||
|
|
||||||
|
Flux checks the Git repository every minute. Any changes to the `helm/` directory will trigger an automatic update of the deployment.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
If you need to configure Harbor image pull secrets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create secret docker-registry harbor-creds \
|
||||||
|
--docker-server=images.caffeinetux.com \
|
||||||
|
--docker-username=YOUR_USERNAME \
|
||||||
|
--docker-password=YOUR_PASSWORD \
|
||||||
|
--namespace=default
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update `helm/values.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-creds
|
||||||
|
```
|
||||||
15
flux/gitrepository.yaml
Normal file
15
flux/gitrepository.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: source.toolkit.fluxcd.io/v1
|
||||||
|
kind: GitRepository
|
||||||
|
metadata:
|
||||||
|
name: resume-site
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
interval: 1m
|
||||||
|
url: https://GITEA_URL/GITEA_USER/resume-site.git # Update with actual Gitea URL
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
ignore: |
|
||||||
|
# exclude all
|
||||||
|
/*
|
||||||
|
# include helm chart
|
||||||
|
!/helm/
|
||||||
36
flux/helmrelease.yaml
Normal file
36
flux/helmrelease.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||||
|
kind: HelmRelease
|
||||||
|
metadata:
|
||||||
|
name: resume-site
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
interval: 5m
|
||||||
|
chart:
|
||||||
|
spec:
|
||||||
|
chart: ./helm
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: resume-site
|
||||||
|
namespace: flux-system
|
||||||
|
interval: 1m
|
||||||
|
values:
|
||||||
|
replicaCount: 2
|
||||||
|
image:
|
||||||
|
repository: images.caffeinetux.com/production/resume-site
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
tag: "latest"
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: nginx
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
hosts:
|
||||||
|
- host: resume.caffeinetux.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: resume-tls
|
||||||
|
hosts:
|
||||||
|
- resume.caffeinetux.com
|
||||||
5
flux/kustomization.yaml
Normal file
5
flux/kustomization.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- gitrepository.yaml
|
||||||
|
- helmrelease.yaml
|
||||||
13
helm/Chart.yaml
Normal file
13
helm/Chart.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: nicholas-haven-resume
|
||||||
|
description: Personal resume website for Nicholas Haven
|
||||||
|
type: application
|
||||||
|
version: 1.0.0
|
||||||
|
appVersion: "1.0.0"
|
||||||
|
keywords:
|
||||||
|
- resume
|
||||||
|
- portfolio
|
||||||
|
- personal-site
|
||||||
|
maintainers:
|
||||||
|
- name: Nicholas Haven
|
||||||
|
email: NickH@libem.one
|
||||||
42
helm/templates/_helpers.tpl
Normal file
42
helm/templates/_helpers.tpl
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{{- define "resume.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "resume.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 }}
|
||||||
|
|
||||||
|
{{- define "resume.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "resume.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "resume.chart" . }}
|
||||||
|
{{ include "resume.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "resume.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "resume.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "resume.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "resume.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
69
helm/templates/deployment.yaml
Normal file
69
helm/templates/deployment.yaml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "resume.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "resume.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
{{- if not .Values.autoscaling.enabled }}
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "resume.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "resume.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "resume.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: {{ .Values.healthCheck.path }}
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.healthCheck.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.healthCheck.periodSeconds }}
|
||||||
|
timeoutSeconds: {{ .Values.healthCheck.timeoutSeconds }}
|
||||||
|
failureThreshold: {{ .Values.healthCheck.failureThreshold }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: {{ .Values.healthCheck.path }}
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.healthCheck.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.healthCheck.periodSeconds }}
|
||||||
|
timeoutSeconds: {{ .Values.healthCheck.timeoutSeconds }}
|
||||||
|
failureThreshold: {{ .Values.healthCheck.failureThreshold }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
24
helm/templates/hpa.yaml
Normal file
24
helm/templates/hpa.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{{- if .Values.autoscaling.enabled }}
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: {{ include "resume.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "resume.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: {{ include "resume.fullname" . }}
|
||||||
|
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||||
|
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||||
|
metrics:
|
||||||
|
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
41
helm/templates/ingress.yaml
Normal file
41
helm/templates/ingress.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "resume.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "resume.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.className }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "resume.fullname" $ }}
|
||||||
|
port:
|
||||||
|
number: {{ $.Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
13
helm/templates/pdb.yaml
Normal file
13
helm/templates/pdb.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{{- if .Values.podDisruptionBudget.enabled }}
|
||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: {{ include "resume.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "resume.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "resume.selectorLabels" . | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
15
helm/templates/service.yaml
Normal file
15
helm/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "resume.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "resume.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "resume.selectorLabels" . | nindent 4 }}
|
||||||
12
helm/templates/serviceaccount.yaml
Normal file
12
helm/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "resume.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "resume.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
91
helm/values.yaml
Normal file
91
helm/values.yaml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
replicaCount: 2
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: images.caffeinetux.com/production/resume-site
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
tag: "latest"
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
create: true
|
||||||
|
annotations: {}
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
|
podSecurityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 101
|
||||||
|
runAsGroup: 101
|
||||||
|
fsGroup: 101
|
||||||
|
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: nginx
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
hosts:
|
||||||
|
- host: resume.caffeinetux.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: resume-tls
|
||||||
|
hosts:
|
||||||
|
- resume.caffeinetux.com
|
||||||
|
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 64Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
|
|
||||||
|
autoscaling:
|
||||||
|
enabled: false
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 5
|
||||||
|
targetCPUUtilizationPercentage: 80
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- weight: 100
|
||||||
|
podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchExpressions:
|
||||||
|
- key: app.kubernetes.io/name
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- nicholas-haven-resume
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
|
||||||
|
healthCheck:
|
||||||
|
path: /health
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
|
||||||
|
podDisruptionBudget:
|
||||||
|
enabled: true
|
||||||
|
minAvailable: 1
|
||||||
853
index.html
Normal file
853
index.html
Normal file
@@ -0,0 +1,853 @@
|
|||||||
|
<!-- If you're reading this, the hiring pipeline has reached manual approval. -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Nicholas Haven | DevOps Engineer</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-primary: #0a0e14;
|
||||||
|
--bg-secondary: #11151c;
|
||||||
|
--bg-card: #151a23;
|
||||||
|
--bg-hover: #1a2029;
|
||||||
|
--text-primary: #e6e6e6;
|
||||||
|
--text-secondary: #8b949e;
|
||||||
|
--text-muted: #5c6370;
|
||||||
|
--accent-cyan: #39c5cf;
|
||||||
|
--accent-green: #7ee787;
|
||||||
|
--accent-orange: #f0883e;
|
||||||
|
--accent-purple: #a371f7;
|
||||||
|
--accent-blue: #58a6ff;
|
||||||
|
--accent-red: #ff7b72;
|
||||||
|
--border-color: #21262d;
|
||||||
|
--glow-cyan: rgba(57, 197, 207, 0.15);
|
||||||
|
--glow-green: rgba(126, 231, 135, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.7;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated background grid */
|
||||||
|
.bg-grid {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(57, 197, 207, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(57, 197, 207, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 50px 50px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100vh;
|
||||||
|
background: radial-gradient(ellipse at 50% 0%, var(--glow-cyan) 0%, transparent 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main container */
|
||||||
|
.container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: rgba(10, 14, 20, 0.85);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo::before {
|
||||||
|
content: '>';
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 32px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a::before {
|
||||||
|
content: '#';
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 120px 0 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 100px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--accent-green);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge::before {
|
||||||
|
content: '';
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: var(--accent-green);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: clamp(2.5rem, 6vw, 4.5rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
animation: fadeInUp 0.6s ease-out 0.1s backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 .name {
|
||||||
|
display: block;
|
||||||
|
background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-blue) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: clamp(1.1rem, 2.5vw, 1.4rem);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
max-width: 600px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
animation: fadeInUp 0.6s ease-out 0.2s backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-contact {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
animation: fadeInUp 0.6s ease-out 0.3s backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Styles */
|
||||||
|
section {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, var(--border-color) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-number {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skills Section */
|
||||||
|
.skills-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-category {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-category:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 30px var(--glow-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-category h3 {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-category h3 svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-tag {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-tag:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Experience Section */
|
||||||
|
.experience-timeline {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 2px;
|
||||||
|
background: linear-gradient(180deg, var(--accent-cyan) 0%, var(--accent-purple) 50%, var(--accent-green) 100%);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-item {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
padding: 28px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-item:hover {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
transform: translateX(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -38px;
|
||||||
|
top: 34px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 3px solid var(--accent-cyan);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-item:nth-child(2)::before {
|
||||||
|
border-color: var(--accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-item:nth-child(3)::before {
|
||||||
|
border-color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-item:nth-child(4)::before {
|
||||||
|
border-color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-title h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-title .company {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-date {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-highlights {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-highlights li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-highlights li::before {
|
||||||
|
content: '▹';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-award {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: linear-gradient(135deg, rgba(240, 136, 62, 0.1) 0%, rgba(163, 113, 247, 0.1) 100%);
|
||||||
|
border: 1px solid rgba(240, 136, 62, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-award svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Certifications / Clearance */
|
||||||
|
.clearance-card {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: linear-gradient(135deg, rgba(126, 231, 135, 0.1) 0%, rgba(57, 197, 207, 0.1) 100%);
|
||||||
|
border: 1px solid rgba(126, 231, 135, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearance-card svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearance-card span {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Education */
|
||||||
|
.education-card {
|
||||||
|
padding: 28px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.education-card h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.education-card .school {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--accent-purple);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.education-card .date {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
padding: 48px 0;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text span {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-links {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding: 100px 0 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-timeline {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-item::before {
|
||||||
|
left: -30px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="bg-grid"></div>
|
||||||
|
<div class="bg-gradient"></div>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<div class="container">
|
||||||
|
<a href="#" class="nav-logo">nicholas_haven</a>
|
||||||
|
<ul class="nav-links">
|
||||||
|
<li><a href="#skills">skills</a></li>
|
||||||
|
<li><a href="#experience">experience</a></li>
|
||||||
|
<li><a href="#education">education</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="hero">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero-content">
|
||||||
|
<div class="hero-badge">Available for Consulting</div>
|
||||||
|
<h1>
|
||||||
|
<span class="name">Nicholas Haven</span>
|
||||||
|
DevOps / Platform Engineer
|
||||||
|
</h1>
|
||||||
|
<p class="hero-subtitle">
|
||||||
|
6+ years designing and operating production Kubernetes infrastructure across multi-cloud environments. Expert in GitOps, infrastructure automation, and security-first DevOps practices.
|
||||||
|
</p>
|
||||||
|
<div class="hero-contact">
|
||||||
|
<a href="mailto:NickH@libem.one" class="contact-item">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
||||||
|
NickH@libem.one
|
||||||
|
</a>
|
||||||
|
<a href="tel:+15136800062" class="contact-item">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
|
||||||
|
+1.513.680.0062
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="skills">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-number">01.</span>
|
||||||
|
<h2>Technical Skills</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clearance-card">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><polyline points="9 12 11 14 15 10"></polyline></svg>
|
||||||
|
<span>Active Security Clearance (July 2021)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="skills-grid">
|
||||||
|
<div class="skill-category">
|
||||||
|
<h3>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg>
|
||||||
|
Multi-Cloud Platforms
|
||||||
|
</h3>
|
||||||
|
<div class="skill-tags">
|
||||||
|
<span class="skill-tag">AWS (EKS, EC2, S3, RDS)</span>
|
||||||
|
<span class="skill-tag">GCP (GKE)</span>
|
||||||
|
<span class="skill-tag">Azure (AKS)</span>
|
||||||
|
<span class="skill-tag">Kops</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="skill-category">
|
||||||
|
<h3>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>
|
||||||
|
Container Orchestration
|
||||||
|
</h3>
|
||||||
|
<div class="skill-tags">
|
||||||
|
<span class="skill-tag">Kubernetes</span>
|
||||||
|
<span class="skill-tag">Docker</span>
|
||||||
|
<span class="skill-tag">Helm</span>
|
||||||
|
<span class="skill-tag">Kustomize</span>
|
||||||
|
<span class="skill-tag">Karpenter</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="skill-category">
|
||||||
|
<h3>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
|
||||||
|
Infrastructure as Code & GitOps
|
||||||
|
</h3>
|
||||||
|
<div class="skill-tags">
|
||||||
|
<span class="skill-tag">Terraform</span>
|
||||||
|
<span class="skill-tag">Crossplane</span>
|
||||||
|
<span class="skill-tag">ArgoCD</span>
|
||||||
|
<span class="skill-tag">Atlantis</span>
|
||||||
|
<span class="skill-tag">Ansible</span>
|
||||||
|
<span class="skill-tag">Helmfile</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="skill-category">
|
||||||
|
<h3>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
||||||
|
CI/CD Pipelines
|
||||||
|
</h3>
|
||||||
|
<div class="skill-tags">
|
||||||
|
<span class="skill-tag">GitLab-CI</span>
|
||||||
|
<span class="skill-tag">CircleCI</span>
|
||||||
|
<span class="skill-tag">GitHub Actions</span>
|
||||||
|
<span class="skill-tag">Tekton</span>
|
||||||
|
<span class="skill-tag">Jenkins</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="skill-category">
|
||||||
|
<h3>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||||
|
Observability & Monitoring
|
||||||
|
</h3>
|
||||||
|
<div class="skill-tags">
|
||||||
|
<span class="skill-tag">Prometheus</span>
|
||||||
|
<span class="skill-tag">Grafana</span>
|
||||||
|
<span class="skill-tag">DataDog</span>
|
||||||
|
<span class="skill-tag">Custom Dashboards</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="skill-category">
|
||||||
|
<h3>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
||||||
|
Security & Compliance
|
||||||
|
</h3>
|
||||||
|
<div class="skill-tags">
|
||||||
|
<span class="skill-tag">Vault</span>
|
||||||
|
<span class="skill-tag">Kyverno</span>
|
||||||
|
<span class="skill-tag">OPA</span>
|
||||||
|
<span class="skill-tag">Istio</span>
|
||||||
|
<span class="skill-tag">ModSecurity</span>
|
||||||
|
<span class="skill-tag">cert-manager</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="skill-category">
|
||||||
|
<h3>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>
|
||||||
|
Languages & Scripting
|
||||||
|
</h3>
|
||||||
|
<div class="skill-tags">
|
||||||
|
<span class="skill-tag">Python</span>
|
||||||
|
<span class="skill-tag">Go</span>
|
||||||
|
<span class="skill-tag">Bash</span>
|
||||||
|
<span class="skill-tag">Node.js</span>
|
||||||
|
<span class="skill-tag">SQL</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="experience">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-number">02.</span>
|
||||||
|
<h2>Experience</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-timeline">
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="experience-header">
|
||||||
|
<div class="experience-title">
|
||||||
|
<h3>Site Reliability Engineer</h3>
|
||||||
|
<span class="company">Fairwinds Ops Inc.</span>
|
||||||
|
</div>
|
||||||
|
<span class="experience-date">July 2022 – Present</span>
|
||||||
|
</div>
|
||||||
|
<p class="experience-description">
|
||||||
|
Primary DevOps consultant for 20+ enterprise clients, architecting production Kubernetes infrastructure across multi-cloud environments.
|
||||||
|
</p>
|
||||||
|
<ul class="experience-highlights">
|
||||||
|
<li>Architect and maintain production clusters across AWS (EKS), GCP (GKE), Azure (AKS), and Kops serving millions of requests with 99.95% uptime</li>
|
||||||
|
<li>Lead direct client engagements through regular 1:1 meetings and technical consultations, delivering tailored infrastructure solutions</li>
|
||||||
|
<li>Automate multi-cloud deployments using Atlantis for Terraform and ArgoCD for GitOps-based application delivery</li>
|
||||||
|
<li>Design zero-trust security architectures using Vault, Kyverno, OPA, and Istio service mesh</li>
|
||||||
|
<li>Pioneer Terraform templating strategies reducing deployment time by 60% across client projects</li>
|
||||||
|
<li>Maintain and contribute to open-source Kubernetes tooling including custom operators and security tools</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="experience-header">
|
||||||
|
<div class="experience-title">
|
||||||
|
<h3>Senior DevOps Engineer</h3>
|
||||||
|
<span class="company">Mile Two LLC</span>
|
||||||
|
</div>
|
||||||
|
<span class="experience-date">Aug 2020 – May 2022</span>
|
||||||
|
</div>
|
||||||
|
<p class="experience-description">
|
||||||
|
Delivered technical solutions for government and enterprise clients in secure and classified environments.
|
||||||
|
</p>
|
||||||
|
<ul class="experience-highlights">
|
||||||
|
<li>Collaborated with government teams to deliver solutions into classified environments with full compliance</li>
|
||||||
|
<li>Upgraded Infrastructure as Code from Terraform to Crossplane with Helmfile templating</li>
|
||||||
|
<li>Designed CI/CD pipelines with CVE scanning, secret detection, and compliance validation</li>
|
||||||
|
<li>Created Kubernetes dashboard for resource tracking, observability, and cost analysis</li>
|
||||||
|
<li>Enabled autonomous deployment capabilities through self-service automation</li>
|
||||||
|
</ul>
|
||||||
|
<div class="experience-award">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
|
||||||
|
Humble Expertise Award Q3 2021
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="experience-header">
|
||||||
|
<div class="experience-title">
|
||||||
|
<h3>DevOps Engineer</h3>
|
||||||
|
<span class="company">Hobsons</span>
|
||||||
|
</div>
|
||||||
|
<span class="experience-date">Mar 2019 – Jul 2020</span>
|
||||||
|
</div>
|
||||||
|
<p class="experience-description">
|
||||||
|
Modernized legacy cloud infrastructure and established infrastructure-as-code practices.
|
||||||
|
</p>
|
||||||
|
<ul class="experience-highlights">
|
||||||
|
<li>Modernized legacy cloud infrastructure to current-generation solutions</li>
|
||||||
|
<li>Implemented comprehensive metrics and alerting for improved observability</li>
|
||||||
|
<li>Established version-controlled infrastructure management with Terraform</li>
|
||||||
|
</ul>
|
||||||
|
<div class="experience-award">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
|
||||||
|
Modernization Engineer Award Q4 2019
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="experience-header">
|
||||||
|
<div class="experience-title">
|
||||||
|
<h3>Senior DevOps Engineer</h3>
|
||||||
|
<span class="company">SC E-Learning</span>
|
||||||
|
</div>
|
||||||
|
<span class="experience-date">Aug 2016 – Mar 2019</span>
|
||||||
|
</div>
|
||||||
|
<p class="experience-description">
|
||||||
|
Managed all client-facing services with 99.99% uptime and led major cloud migration initiative.
|
||||||
|
</p>
|
||||||
|
<ul class="experience-highlights">
|
||||||
|
<li>Managed 300+ client services with 99.99% uptime including maintenance windows</li>
|
||||||
|
<li>Orchestrated AWS migration achieving $250,000+ annual cost savings</li>
|
||||||
|
<li>Implemented CI/CD with GitHub, Jenkins, S3, Lambda, and auto-scaling across multiple AZs</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="education">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-number">03.</span>
|
||||||
|
<h2>Education</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="education-card">
|
||||||
|
<h3>Advanced Networking & Computer Science</h3>
|
||||||
|
<p class="school">Cincinnati State</p>
|
||||||
|
<p class="date">September 2006 – December 2008</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<p class="footer-text">
|
||||||
|
<span><</span> Built with passion for infrastructure <span>/></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user