Software Architecture/Engineering  

Building Self Service Deployment Workflows for Development Teams

workflows

As DevOps environments grow, one challenge becomes increasingly clear: developers should not need to understand every Jenkins job, Kubernetes command, cluster credential or deployment script just to release an application safely.

After working with CI/CD, Terraform, Kubernetes, GitOps and automation, I started thinking about how those capabilities could be presented to developers through a simpler and more controlled workflow.

This is where my interest in self service deployment and platform engineering started becoming stronger.

In this article, I will design a simple deployment workflow where a developer chooses an application, version and environment, while the platform handles validation, permissions, approvals and deployment behind the scenes.

Self Service Deployment Flow

Developer

Select Application + Version + Environment

Platform Validation

Permission and Policy Checks

Approval if Required

GitOps Update

Argo CD

Kubernetes Deployment

Deployment Status Returned to Developer

Why Self Service Deployment Matters

In many environments, deployment knowledge gradually becomes concentrated within a small DevOps or infrastructure team.

A developer may need to ask:

  • Which Jenkins pipeline should I run?

  • Which parameters should I use?

  • Which Kubernetes namespace contains my application?

  • Which configuration belongs to test?

  • Do I have permission to deploy?

  • Who approves a production release?

Individually these questions may seem small, but across many teams they can create repeated operational work.

Developer

Ask DevOps Team

DevOps Finds Pipeline

Validate Parameters

Trigger Deployment

Send Result Back

A platform can remove some of this repetitive coordination without removing the controls required for safe deployments.

Self Service Does Not Mean Uncontrolled Access

One of the most important lessons for me is that self service does not mean giving every developer direct administrator access to Kubernetes.

I prefer this model:

Developer Request

Platform Validates Request

Platform Applies Policies

Approved Automation Executes

Infrastructure Remains Protected

Developers receive a simpler experience while the platform team retains control over how infrastructure changes are executed.

Defining a Deployment Request

The first step is defining what information the platform actually needs.

A deployment request can be kept simple:

{
  "application": "cloud-native-api",
  "version": "1.6.2",
  "environment": "test",
  "requested_by": "[email protected]"
}

The developer does not need to provide:

  • Kubernetes credentials

  • Cluster server addresses

  • Namespace credentials

  • Argo CD credentials

  • Registry credentials

Those remain controlled by the platform.

Creating a Small Deployment API

To demonstrate the idea, I can create a small API using Python and FastAPI.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class DeploymentRequest(BaseModel):
    application: str
    version: str
    environment: str
    requested_by: str


@app.post("/deployments")
def create_deployment(
    request: DeploymentRequest
):
    return {
        "status": "RECEIVED",
        "application": request.application,
        "version": request.version,
        "environment": request.environment
    }

At this stage, the API does not deploy anything.

It simply provides a controlled entry point for deployment requests.

Validating Supported Applications

The platform should not accept arbitrary application names.

I can define a small application catalogue:

APPLICATIONS = {
    "cloud-native-api": {
        "repository": "cloud-native-api-gitops",
        "namespace": "cloud-native-api"
    },

    "orders-api": {
        "repository": "orders-api-gitops",
        "namespace": "orders"
    }
}

The validation function can then check the request:

def validate_application(
    application
):
    if application not in APPLICATIONS:
        raise ValueError(
            "Application is not registered "
            "with the deployment platform."
        )

    return APPLICATIONS[application]

Validating Environments

The same principle applies to environments.

SUPPORTED_ENVIRONMENTS = {
    "dev",
    "test",
    "prod"
}


def validate_environment(
    environment
):
    if environment not in SUPPORTED_ENVIRONMENTS:
        raise ValueError(
            "Unsupported deployment environment."
        )

This prevents accidental deployment requests to unexpected or incorrectly named environments.

Adding Permission Checks

A self service platform should understand who is allowed to deploy where.

A simplified permission model could look like:

USER_PERMISSIONS = {
    "[email protected]": {
        "dev",
        "test"
    },

    "[email protected]": {
        "dev",
        "test",
        "prod"
    }
}

We can then validate the request:

def check_permission(
    user,
    environment
):
    allowed_environments = USER_PERMISSIONS.get(
        user,
        set()
    )

    if environment not in allowed_environments:
        raise PermissionError(
            "User is not authorised "
            "for this environment."
        )

Production consideration: A real platform should integrate with the organisation's identity provider and role based access controls rather than maintaining user permissions directly inside application code.

Different Environments Need Different Controls

One of the design decisions I prefer is making lower environments easy to use while keeping stronger controls around production.

Development

Validate request and deploy automatically.

Test

Validate request, run checks and deploy automatically.

Production

Validate request, verify permissions, require approval and then deploy.

Defining Deployment Policies

Policies can determine which controls are required.

ENVIRONMENT_POLICIES = {
    "dev": {
        "approval_required": False
    },

    "test": {
        "approval_required": False
    },

    "prod": {
        "approval_required": True
    }
}


def requires_approval(
    environment
):
    return ENVIRONMENT_POLICIES[
        environment
    ]["approval_required"]

Now approval behaviour is defined by policy rather than scattered across the deployment code.

Validating the Application Version

Another important control is verifying that the requested container version actually exists.

The platform should ideally check the container registry before accepting the deployment.

Developer Requests Version 1.6.2

Platform Queries Registry

Image Exists?

Yes → Continue

No → Reject Request

This prevents invalid versions from reaching the deployment stage.

Creating a Deployment Record

Every request should have a unique identifier so that the developer can track its progress.

from uuid import uuid4


def create_deployment_record(
    request
):
    return {
        "deployment_id": str(uuid4()),
        "application": request.application,
        "version": request.version,
        "environment": request.environment,
        "requested_by": request.requested_by,
        "status": "PENDING"
    }

The deployment can then move through states such as:

PENDING

VALIDATING

AWAITING APPROVAL

DEPLOYING

VERIFYING

COMPLETED

If something fails, the status could become:

FAILED

Connecting Self Service with GitOps

Because I already want Git to remain the source of truth, the self service platform does not need to run kubectl apply directly.

Instead, the platform can update the GitOps configuration.

Suppose production currently contains:

images:
  - name: myregistry/cloud-native-api
    newTag: 1.6.1

The approved deployment request changes it to:

images:
  - name: myregistry/cloud-native-api
    newTag: 1.6.2

Argo CD then detects the desired state change and performs the Kubernetes deployment.

Why I Prefer This Over Direct Cluster Access

This gives developers the outcome they need without exposing unnecessary infrastructure access.

Developer

Deployment Request

Platform API

Policy Engine

GitOps Repository

Argo CD

Kubernetes

The developer does not need direct credentials to each layer.

Adding Production Approval

For production, the request can pause after validation.

Production Request

Application Validation

Permission Check

Version Validation

AWAITING APPROVAL

Approved?

GitOps Update

Approval itself should also be recorded.

{
  "deployment_id": "dep-2048",
  "status": "APPROVED",
  "approved_by": "[email protected]",
  "environment": "prod",
  "version": "1.6.2"
}

Deployment Status Should Be Visible

Self service becomes frustrating if the developer submits a request and then has no idea what is happening.

I want the platform to return clear states.

Deployment ID: dep-2048

Application: cloud-native-api

Version: 1.6.2

Environment: test

Status: VERIFYING

The developer should not need to open Jenkins, Argo CD and Kubernetes separately to understand basic deployment status.

Verifying the Deployment

A successful GitOps sync does not automatically prove that the application is healthy.

After deployment, I would check:

  • Argo CD sync status

  • Kubernetes rollout status

  • Pod readiness

  • Application health endpoint

  • Basic smoke tests

Argo CD Synced

Kubernetes Rollout Complete

Pods Ready

Health Endpoint Responding

Deployment Completed

Handling Deployment Failure

The platform also needs to communicate failure clearly.

Instead of returning only:

Deployment failed.

I prefer something more useful:

Status: FAILED

Stage: Deployment Verification

Reason: Two pods failed readiness checks.

Next Step: Review application health and deployment logs.

Creating a Golden Path

This is where the platform engineering idea becomes more interesting.

Instead of every development team building a completely different deployment process, the platform team can define a standard path.

Application

Standard Build Pattern

Standard Container Pattern

Standard Security Checks

Standard Deployment Request

Standard GitOps Workflow

Standard Health Validation

Developers still build their applications.

The platform provides a safer path for getting those applications into production.

Reducing Cognitive Load for Developers

One of the ideas I started appreciating more through platform engineering is developer cognitive load.

A developer may need to understand:

  • Jenkins

  • Kubernetes

  • Docker

  • Argo CD

  • Helm or Kustomize

  • Cloud infrastructure

  • Secrets

  • Networking

  • Monitoring

  • Security policies

Developers should understand enough of the underlying platform to operate responsibly, but they should not need to manually coordinate every infrastructure component for every deployment.

Auditability Becomes Part of the Platform

Because every request passes through one controlled workflow, the platform can record useful information.

For example:

{
  "deployment_id": "dep-2048",
  "application": "cloud-native-api",
  "version": "1.6.2",
  "environment": "prod",
  "requested_by": "[email protected]",
  "approved_by": "[email protected]",
  "status": "COMPLETED"
}

This provides a clearer deployment history than relying on people remembering which commands were executed.

Platform Engineering Is More Than a Portal

It is easy to think platform engineering simply means building a dashboard.

For me, the interface is only one part.

The real platform includes:

  • Reusable deployment workflows

  • Environment policies

  • Identity and permissions

  • Approval controls

  • CI/CD integration

  • GitOps integration

  • Secrets management

  • Observability

  • Audit history

  • Developer experience

The portal or API simply gives developers an easier way to interact with those capabilities.

What I Would Improve Next

The example in this article is intentionally small.

A more mature platform could add:

  • Single sign on

  • Role based access control

  • Application catalogue

  • Environment ownership

  • Automated security validation

  • Deployment history

  • Rollback requests

  • Release dashboards

  • Service ownership information

  • Observability links

  • Reusable application templates

  • Developer documentation

What I Learned from Building Self Service Workflows

This was an important change in how I started thinking about DevOps.

Earlier, my focus was mainly:

How can I automate this infrastructure task?

Platform engineering introduced a different question:

How can I turn this automation into a safe and reusable capability that other developers can use without needing to understand every implementation detail?

That shift is important.

Automation solves individual tasks. A platform begins to organise those tasks into reusable products and workflows for other engineering teams.

Final Architecture

Developer

Self Service Portal or API

Application Catalogue

Validation + Permissions + Policies

Approval Workflow

GitOps Repository

Argo CD

Kubernetes Platform

Health Verification

Deployment Result

Developer

Conclusion

In this article, I explored how existing DevOps capabilities can be turned into a self-service deployment experience for development teams.

The workflow we designed includes:

  • A simple deployment API

  • Application registration

  • Environment validation

  • Permission checks

  • Environment policies

  • Production approvals

  • Container version validation

  • Deployment tracking

  • GitOps integration

  • Argo CD reconciliation

  • Health verification

  • Audit history

The most important idea for me is that developers receive more independence without infrastructure controls being removed.

They request the outcome they need, while the platform determines how that request can be executed safely.

This was one of the points where my thinking started moving from DevOps automation towards platform engineering and developer experience.

My next step: Building self-service workflows made me think beyond individual deployment tools and towards complete developer platforms. In the next stage of my journey, I will explore event driven application architecture with Apache Kafka and how loosely coupled services can communicate reliably at scale.