<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aftab Bashir</title>
    <description>The latest articles on DEV Community by Aftab Bashir (@aftabkh4n).</description>
    <link>https://dev.to/aftabkh4n</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3863150%2F2da9fdf0-d0a8-4a55-b7ef-415aaeba5982.jpeg</url>
      <title>DEV Community: Aftab Bashir</title>
      <link>https://dev.to/aftabkh4n</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aftabkh4n"/>
    <language>en</language>
    <item>
      <title>I built a Mini Internal Developer Platform in .NET — GitHub repos + Kubernetes deployments from a single API call</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Mon, 06 Apr 2026 05:15:36 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/i-built-a-mini-internal-developer-platform-in-net-github-repos-kubernetes-deployments-from-a-4l0d</link>
      <guid>https://dev.to/aftabkh4n/i-built-a-mini-internal-developer-platform-in-net-github-repos-kubernetes-deployments-from-a-4l0d</guid>
      <description>&lt;p&gt;Most teams I've worked with waste 2-3 hours every time they spin up a &lt;br&gt;
new microservice. Create the repo, write the Dockerfile, set up CI, &lt;br&gt;
write Kubernetes manifests, configure ingress, same boilerplate, every &lt;br&gt;
single time.&lt;/p&gt;

&lt;p&gt;I decided to automate all of it. One API call. Everything done &lt;br&gt;
automatically.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;POST to &lt;code&gt;/api/services&lt;/code&gt; with a service name and language, and the &lt;br&gt;
platform automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a GitHub repository&lt;/li&gt;
&lt;li&gt;Commits a Dockerfile and GitHub Actions CI pipeline&lt;/li&gt;
&lt;li&gt;Creates a Kubernetes Deployment, Service, and Ingress&lt;/li&gt;
&lt;li&gt;Streams real-time status updates to a live dashboard via SignalR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The HTTP response comes back in under 200ms. All the work happens in a &lt;br&gt;
background worker.&lt;/p&gt;
&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The system is split into four .NET projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Idp.Api&lt;/code&gt; — ASP.NET Core 9 API, the entry point&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Idp.Core&lt;/code&gt; — domain models and interfaces&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Idp.Infrastructure&lt;/code&gt; — GitHub and Kubernetes integrations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Idp.Worker&lt;/code&gt; — background provisioning pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a request comes in, the controller does one thing — saves a record &lt;br&gt;
to PostgreSQL with status &lt;code&gt;queued&lt;/code&gt; and returns &lt;code&gt;202 Accepted&lt;/code&gt;. The &lt;br&gt;
background worker polls every 5 seconds, picks up queued jobs, and runs &lt;br&gt;
the full provisioning pipeline.&lt;/p&gt;
&lt;h2&gt;
  
  
  The GitHub integration
&lt;/h2&gt;

&lt;p&gt;I used Octokit.net to talk to the GitHub REST API. Here is what happens &lt;br&gt;
when you provision a new service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NewRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Private&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AutoInit&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Give GitHub a moment to initialise&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Commit Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organisation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Dockerfile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateFileRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chore: add Dockerfile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;GetDockerfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="c1"&gt;// Commit CI workflow&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organisation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".github/workflows/ci.yml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateFileRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chore: add CI workflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;GetCiWorkflow&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;AutoInit: true&lt;/code&gt; creates an initial commit so we can push files &lt;br&gt;
immediately. The 2 second delay is important — without it, GitHub &lt;br&gt;
sometimes returns a 404 when you try to commit to a freshly created repo.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Kubernetes integration
&lt;/h2&gt;

&lt;p&gt;I used the official &lt;code&gt;KubernetesClient&lt;/code&gt; NuGet package to create K8s &lt;br&gt;
objects from C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create Deployment&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppsV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateNamespacedDeploymentAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create Service&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CoreV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateNamespacedServiceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create Ingress&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NetworkingV1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateNamespacedIngressAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service gets its own namespace, which keeps things clean and makes &lt;br&gt;
it easy to delete a service and all its resources in one command.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real-time updates with SignalR
&lt;/h2&gt;

&lt;p&gt;The background worker pushes status updates to all connected browsers &lt;br&gt;
as provisioning progresses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;hubContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"StatusChanged"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ServiceId&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ServiceName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// queued → creating_repo → deploying_k8s → deployed&lt;/span&gt;
    &lt;span class="n"&gt;RepoUrl&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repoUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ServiceUrl&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceUrl&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard listens for these events and updates the UI in real time &lt;br&gt;
— no polling, no page refresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Always use async provisioning.&lt;/strong&gt; The first version was synchronous — &lt;br&gt;
the HTTP request sat open for 6 seconds while GitHub did its work. &lt;br&gt;
Moving to a background worker with a queue made the API feel instant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub needs a moment after repo creation.&lt;/strong&gt; If you immediately try &lt;br&gt;
to commit files to a freshly created repo, you get a 404. A 2 second &lt;br&gt;
delay after &lt;code&gt;AutoInit&lt;/code&gt; fixes it reliably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handle conflicts gracefully in Kubernetes.&lt;/strong&gt; If you try to create a &lt;br&gt;
resource that already exists, K8s returns a 409 Conflict. Catching that &lt;br&gt;
and either replacing or skipping makes the system idempotent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets in git history are permanent.&lt;/strong&gt; I accidentally committed my &lt;br&gt;
GitHub token inside the &lt;code&gt;bin/&lt;/code&gt; folder early on. Even after deleting it, &lt;br&gt;
it was in git history. I had to force push a rewritten history and &lt;br&gt;
immediately rotate the token. Always add &lt;code&gt;**/bin/&lt;/code&gt; and &lt;code&gt;**/obj/&lt;/code&gt; to &lt;br&gt;
&lt;code&gt;.gitignore&lt;/code&gt; before your first commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;ASP.NET Core 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL + Entity Framework Core 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub automation&lt;/td&gt;
&lt;td&gt;Octokit.net&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes automation&lt;/td&gt;
&lt;td&gt;KubernetesClient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time updates&lt;/td&gt;
&lt;td&gt;SignalR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logging&lt;/td&gt;
&lt;td&gt;Serilog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API docs&lt;/td&gt;
&lt;td&gt;Scalar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The full source code is on GitHub:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/aftabkh4n/idp-platform" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/idp-platform&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clone it, add your GitHub token to &lt;code&gt;appsettings.json&lt;/code&gt;, spin up a &lt;br&gt;
PostgreSQL container, and run it. You should have a working IDP in under &lt;br&gt;
5 minutes.&lt;/p&gt;




&lt;p&gt;This is one of four portfolio projects I am building to demonstrate &lt;br&gt;
senior backend engineering skills. Next up: a full Data Platform API &lt;br&gt;
with search, analytics, and recommendations.&lt;/p&gt;

&lt;p&gt;If you found this useful or have questions, drop a comment below.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3svmzmifke8c2ymngvq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3svmzmifke8c2ymngvq.png" alt=" " width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>kubernetes</category>
      <category>github</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
