DEV Community

Cover image for I Got Rejected for "Not Enough Experience" — So I Built My Resume as Infrastructure
Alexander Gusarov
Alexander Gusarov

Posted on

I Got Rejected for "Not Enough Experience" — So I Built My Resume as Infrastructure

Let me paint you a picture.

It's 2024. I've been in IT for 14 years — Flash games at 13, mobile GameDev, frontend, fullstack, and for the last few years, DevOps. I've managed 750+ servers with Ansible, cut deploy times from 8 minutes to 2, saved ~$250/month on AWS costs through Terraform optimizations. I've shipped games with 100M+ downloads. I have an Upwork Top Rated Plus badge.

And HR keeps telling me I don't have enough experience.

Not because I'm unqualified. Because my resume didn't tell the story right. Because I was sending a Google Doc that tried to be everything to everyone — and ended up being nothing to anyone.

So I did what any reasonable DevOps engineer would do when faced with a tooling problem.

I over-engineered a solution.


The Actual Problem

Here's what my resume situation looked like before:

  • One Russian .docx for local job applications
  • One English .docx for international / remote
  • A "DevOps focused" copy somewhere
  • A "GameDev focused" copy somewhere else
  • A file literally named CV_Gusarov_FINAL_v3_actually_final.docx

Every time I updated one, I forgot to update the others. Every time I applied to a GameDev role, I had to manually shuffle sections around. Every time I wanted to send a PDF, I opened LibreOffice, crossed my fingers, and watched the formatting break.

This is a data consistency problem. And data consistency problems have a solution: a single source of truth.


The Architecture

I called the project CV Hub. Here's what I wanted it to do:

  1. Store all resume data in one place
  2. Generate a website automatically
  3. Export to PDF, DOCX, and TXT on every push
  4. Support multiple languages without duplicating content
  5. Support multiple role-specific versions from the same data

The stack I landed on: Astro for the site, YAML as the data layer, GitHub Actions for CI/CD, GitHub Pages for hosting. Total infrastructure cost: $0.

Let me walk through each piece.


The YAML Structure

Everything lives in YAML. Not a database, not a CMS, not Notion — plain YAML files that you can read, diff, and version control like any other config.

There's no bilingual key soup (title.en / title.ru inside the same file). Instead, each language is a separate file: en.yaml and ru.yaml. Same structure, different content. Clean diffs, no mental overhead when editing.

A base CV file looks like this:

name: "Alexander Gusarov"
title: "DevOps / Platform Engineer | Kubernetes · Terraform · AWS"
summary: >
  DevOps engineer with 14+ years in IT. Managed production Kubernetes
  clusters, built IaC from scratch with Terraform + Ansible on AWS,
  shipped monitoring microservices in Go.

contacts:
  - label: GitHub
    url: https://github.com/KeeGooRoomiE
  - label: Telegram
    url: https://t.me/spartan121

achievements:
  - "Managed 750+ Linux servers with Ansible, SLA 1.5–2 min"
  - "Reduced AWS costs from $550 to $300/month (−45%)"
  - "Cut deploy time from 8 to 2 minutes (−75%)"

skills:
  - group: Orchestration
    items: [Kubernetes, Helm, Docker]
  - group: IaC & Automation
    items: [Terraform, Ansible]
  - group: Cloud
    items: [AWS — EC2, S3, RDS, VPC, IAM, ALB]
  - group: Languages
    items: [Go, Python, Bash]

experience:
  - company: "InfoScale"
    role: "DevOps Engineer"
    period: "Dec 2024  Jan 2026"
    description:
      - "Administered Kubernetes production clusters in Yandex Cloud"
      - "Built full IaC solution with Terraform + Ansible on AWS (~20 services)"
      - "Reduced MTTR by 60% with custom Grafana/VictoriaMetrics dashboards"
    stack: [Kubernetes, Helm, Terraform, Ansible, AWS, Go, Grafana]
Enter fullscreen mode Exit fullscreen mode

The key insight: your resume data and your resume presentation are two different concerns. YAML handles the data. Astro handles the presentation. They never mix.


Multi-Role Overlay System

This was the part I was most excited about architecting.

I have genuinely different professional identities: DevOps engineer, GameDev (shipped titles with 100M+ downloads), Frontend/Fullstack developer. Each of these roles needs a different emphasis — different projects front and center, different skills highlighted, different summary framing.

The naive solution: maintain separate full YAML files per role. The problem: now you have four sources of truth, and the sync nightmare is back.

My solution: per-language base files + minimal delta files per profile.

src/content/cv/
  en.yaml              ← base CV in English
  ru.yaml              ← base CV in Russian
  en_devops.yaml       ← DevOps delta (only what changes)
  ru_devops.yaml       ← DevOps delta in Russian
  en_gamedev.yaml      ← GameDev delta
  ru_gamedev.yaml
Enter fullscreen mode Exit fullscreen mode

The delta file contains only what changes. Everything else is inherited from the base:

# en_devops.yaml — only overrides
title: "DevOps / Platform Engineer | Kubernetes · Terraform · AWS"

summary: >
  DevOps-focused summary with emphasis on infrastructure...

skills:
  - group: Orchestration
    items: [Kubernetes, Helm, Docker]
  - group: IaC & Automation
    items: [Terraform, Ansible]

experience:
  - company: InfoScale        # no fields = take everything from base
  - company: AZNResearch
    role: "Backend Engineer"  # override role, rest from base
    description:
      - "Developed backend microservices with .NET Core"
      - "Introduced Git workflow and CI pipelines"
Enter fullscreen mode Exit fullscreen mode

Before the build, a merge.mjs script runs and produces merged artifacts in public/cv/:

src/content/cv/en.yaml + en_devops.yaml
        ↓ merge.mjs
public/cv/en_devops.yaml  ← merged, ready to render
Enter fullscreen mode Exit fullscreen mode

Astro pages read from public/cv/ — not from source directly. This keeps the merge logic in one place and out of the component layer.

The profiles registry ties everything together:

# src/content/profiles/profiles.yml
profiles:
  - id: default
    label: "Generalist"
    slug: ""        # URL root /
    spec: null      # no delta, use base as-is

  - id: devops
    label: "DevOps"
    slug: "devops"  # URL: /devops, /devops/ru
    spec: devops    # reads en_devops.yaml, ru_devops.yaml

  - id: gamedev
    label: "Game Developer"
    slug: "gamedev"
    spec: gamedev
Enter fullscreen mode Exit fullscreen mode

One update to an experience entry in en.yaml propagates to all profiles automatically. The delta only needs to exist where something actually differs.


i18n Without a Framework

I deliberately avoided i18n libraries. They add complexity, and for a resume site the translation problem is actually simple.

The content layer uses the "one file per language" approach — en.yaml and ru.yaml are independent, same structure, different text. No nested keys, no bilingual soup, clean diffs.

For UI strings (labels, navigation, button text), there's a separate translations.yaml:

# src/content/i18n/translations.yaml
nav:
  showcase:
    en: "Showcase"
    ru: "Проекты"

cv:
  skills:
    en: "Skills"
    ru: "Навыки"
  experience:
    en: "Experience"
    ru: "Опыт"
  download:
    en: "Download"
    ru: "Скачать"
Enter fullscreen mode Exit fullscreen mode

A small makeT helper resolves keys with a three-level fallback:

const t = makeT(translations.data, lang);

t('nav.showcase')   // → "Showcase" or "Проекты"
t('cv.skills')      // → "Skills" or "Навыки"
Enter fullscreen mode Exit fullscreen mode

Fallback chain: requested lang → en → the key path itself as a string. So missing translations never crash the build — they just show the key, which is immediately obvious in review.

Language state lives in the URL: /devops is English, /devops/ru is Russian. Every page is independently shareable and indexable. The language switcher preserves the current profile, the profile dropdown preserves the current language. No cookies, no localStorage, no hydration shenanigans.


The Export Pipeline

This was honestly the trickiest part to get right.

I wanted three export formats: PDF (for HR), DOCX (for ATS systems that parse Word files better), and TXT (plain text for copy-paste into application forms).

The build runs in four steps, in order:

npm run cv:build          # 1. merge YAMLs → public/cv/
npm run resume:generate   # 2. DOCX + TXT for all merged files
npm run resume:pdf        # 3. PDF via Playwright headless render
astro build               # 4. static site
Enter fullscreen mode Exit fullscreen mode

Step 1 (merge.mjs) produces all the merged artifacts in public/cv/. Steps 2 and 3 read those artifacts and output files into public/downloads/. Step 4 builds the Astro site, which already has the downloads sitting in the right place.

File naming follows a consistent pattern:

resume_en.pdf              ← default profile, English
resume_ru.pdf              ← default profile, Russian
resume_en_devops.pdf       ← DevOps profile, English
resume_ru_devops.pdf       ← DevOps profile, Russian
resume_en_gamedev.pdf      ← GameDev profile, English
Enter fullscreen mode Exit fullscreen mode

The download URLs are built at page generation time:

const specSuffix = profileData.spec ? `_${profileData.spec}` : '';
const pdfUrl = `${base}/downloads/resume_${langId}${specSuffix}.pdf`;
Enter fullscreen mode Exit fullscreen mode

So /devops serves resume_en_devops.pdf, /gamedev/ru serves resume_ru_gamedev.pdf. Each page offers the right file for its profile × language combination.

PDF generation uses Playwright — headless browser renders the built HTML with a proper two-column A4 layout. This took the most time to get right. Print CSS is a dark art: @page rules, page break behavior, orphan/widow control, font embedding — all of it needs careful attention. Budget more time for this than you think you'll need.


Astro as the Rendering Layer

Why Astro specifically?

I evaluated a few options: plain HTML templates (too manual), Next.js (overkill for a static site), Jekyll (Ruby, no thanks), Hugo (fine, but Go templating is awkward for this use case).

Astro won because:

  1. Zero JS by default. The resume site doesn't need interactivity beyond language switching and role dropdown. Astro ships zero client-side JavaScript unless you explicitly opt in. The result is a site that loads instantly.

  2. Component model. I could build <ResumeSection>, <ProjectCard>, <ExperienceEntry> as proper components with typed props, rather than copypasting HTML fragments.

  3. Static generation with dynamic routing. The [...slug].astro pattern lets me define one template and generate pages for every profile at build time. No runtime server needed.

  4. Content collections. Astro has a built-in concept of typed content collections — perfect for YAML-driven data. I get TypeScript types for my resume data for free.

The project structure ended up clean:

src/
  components/
    ResumeHeader.astro
    ExperienceSection.astro
    ProjectCard.astro
    LanguageToggle.astro
    RoleDropdown.astro
  pages/
    index.astro           redirects to default profile
    [...slug].astro       dynamic profile pages
    404.astro
  data/
    profiles/
      base.yaml
      devops.yaml
      gamedev.yaml
      frontend.yaml
  utils/
    i18n.ts
    profileLoader.ts
    exportHelpers.ts
Enter fullscreen mode Exit fullscreen mode

What I Learned

A few things surprised me during this build.

YAML is surprisingly good for structured content. I went in skeptical — it's finicky about indentation, and deep nesting gets messy. But for resume data specifically, the structure maps cleanly to YAML's strengths. Anchors and aliases let me deduplicate repeated values. The files are readable without tooling. Git diffs on YAML are meaningful.

Static generation is underrated for personal sites. The whole site is a folder of HTML files. No server, no database, no runtime. It deploys to GitHub Pages in under 2 minutes and serves from CDN globally. For something that updates maybe once a week, this is exactly the right architecture.

Print CSS is a dark art. If you're generating PDFs from HTML, budget more time than you think for @media print styles. Page break behavior, orphan/widow control, margin boxes — all of it requires careful attention and lots of test prints.

Building tools for yourself is the best way to understand what you actually need. I shipped features I thought I needed (fancy animations, a complex filtering system for projects) and ended up removing them. The things that stayed were the things I actually used every day: clean export, fast language switching, role overlays.


The Result

The live site is at keegooroomie.github.io/cv_hub.

Four profiles, two languages, three export formats. Fully automated. Hosted for free. Updated with a git push.

Does it help with the job search? Honestly, yes. Having a URL to send instead of an attachment changes the conversation a little. Having role-specific versions means I'm not asking a GameDev hiring manager to mentally filter through Kubernetes metrics to find the relevant parts.

More importantly — I never have to manually sync resume versions again. That alone was worth the weekend.


Use It Yourself

The whole thing is open source and built to be forked:

👉 github.com/KeeGooRoomiE/cv_hub

Fork it, replace my YAML with yours, push to GitHub, enable Pages — you have a working versioned resume site with automated exports. The README has a setup guide.

If you find it useful, a ⭐ on the repo goes a long way.

And if you build something on top of it or run into issues — I'm genuinely curious to hear about it. Drop a comment or open an issue.


Alexander Gusarov — DevOps / Platform Engineer. 14+ years in IT, currently open to new opportunities.
GitHub: KeeGooRoomiE · Telegram: @spartan121

Top comments (0)