![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 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:
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:
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.