Building a Platform: an Architecture for Developer Autonomy

June 29, 2023

Building a Platform: an Architecture for Developer Autonomy

Ready to start?

Introduction

Building an internal developer platform that empowers developers to work autonomously requires careful consideration of the technology stack and reference architecture. In this article, based on a video by Viktor Farcic (here’s another one about building an internal developer platform for infrastructure) we’ll provide insights into the essential components and tools needed to establish a fully functional internal developer platform. This is just the MVP, of course, but it can serve as an excellent beginning.  

Why platform engineering and why an internal developer platform?

The primary objective of platform engineering is to foster developer autonomy. Regardless of whether a developer is proficient in K8s, a junior or an architect,  they should be able to access the resources they need effortlessly. For instance, if a developer requires a database or wants to manage an application in K8s, they should not be burdened with extensive knowledge acquisition that would kill their flow or, in some cases, just isn’t practical or feasible. Instead, these tasks should be simple to accomplish, allowing developers to easily define their requirements or just perform self-service actions using a user-friendly interface. 

The minimum set of elements for an IDP

Here are the key components of a working internal developer platform:

  1. A Control Plane to manage all resources, whether they are applications running in a K8s cluster or infrastructure and services running in various cloud providers.
  2. A Control Plane Interface  
  3. Git: for storing desired states in a Git repository and then synchronizing with the control plane using GitOps. 
  4. As stateful components, databases are an essential part of the internal developer platform. To ensure efficient management, a method for handling schemas within these databases is required. 
  5. Sensitive information that cannot be stored in Git, such as passwords, needs to be managed separately. A secrets manager is recommended. 
  6. Internal Developer Portal to act as a user interface.  It should enable developers to execute processes that create new resources and store them in Git. They should also be able to use the software catalog to understand everything in context and with the right abstractions. 
  7. CI/CD Pipelines: to execute actions such as creating repositories based on templates or building images with updated release changes to manifests. 

To better understand the interplay between these elements, refer to the diagram: 

{{cta_3}}

Control Plane

The control plane provides us with the one API that is an entry point. It is the central point where resources are managed in the platform regardless of their location. 

A control plane isn’t enough for a full platform, since applications use multiple resources and will be difficult for developers to track where everything resides and what are the dependencies.

User-Friendly Control Plane Interface

When we’re 100% GitOps the control plane interface can act as the platform API but this won’t work for developers, who will need the internal developer portal, such as Port. 

Determining the appropriate level of abstraction for platform users is crucial. The rule of thumb is to abstract any unnecessary details that users typically don't care about. For example, database storage details may be irrelevant. The ideal level of abstraction needs to be determined by taking a product as a platform approach.

Remember that direct interaction with the cluster or control plane should be avoided. Instead, users should store their desired states in Git.

Synchronization from Git with GitOps

Directly altering resource states by communicating with the control plane is discouraged, as it becomes difficult to track who made what changes and when. Instead, pushing the desired state to Git, optionally utilizing pull requests for reviews, is preferred. By incorporating GitOps tools into the platform, the desired state can be synchronized with the control plane, effectively transforming it into the actual state.

Schemas 

To complete the platform, efficient schema management is necessary, preferably as part of application definitions stored in Git. While various methods exist, only a few allow schemas to fit seamlessly within the Git model. This becomes challenging as GitOps tools typically focus on K8s resources, necessitating the definition of schemas as K8s resources. Extending the K8s API with CRDs enables the definition of schemas as K8s resources. 

Secret Management

Use a secrets manager. Secrets must be accessible to the control plane, enabling processes within it to authenticate with external APIs or access services, such as databases. 

Internal Developer Portal 

The portal is the user interface that consolidates all previously implemented components. This portal functions as a software catalog and allows developers to perform various actions autonomously. It should provide mechanisms to initialize processes that create new repositories, add sample code, define manifests for databases and other dependencies, establish CI/CD pipelines, and more.

CI/CD 

Pipelines are the final piece of the puzzle. While GitOps ensures synchronization between actual and desired states, pipelines are required for executing one-shot actions triggered by each commit. These actions might involve building binaries, running tests, building and pushing container images, and similar tasks.

The Platform in Action

From a developer's perspective, creating a new application is as simple as clicking a button in the self-service section in the developer portal or defining a minimalistic manifest and pushing it to Git. The same interface allows developers to monitor application details and associated dependencies.

Behind the scenes, the following workflow unfolds:

  1. The user interacts with Port’s internal developer portal or directly with Git. The internal developer portal is responsible for triggering actions that create all the necessary resources.
  2. The pipeline, such as GitHub Actions, handles the creation of relevant resources. It generates a new repository containing essential files, including source code, pipelines, and application manifests.
  3. Pushing changes to the application repository, whether resulting from previous actions or subsequent code modifications, triggers an application-specific pipeline (GitHub Actions). This pipeline, at a minimum, builds a container image, pushes it to the image registry, and updates manifests in the management repository monitored by GitOps tools like Argo CD or Flux.
  4. GitOps tools detect changes in the management repository and synchronize them with resources in the control plane cluster.
  5. Corresponding controllers (Crossplane) within the control plane cluster leverage the resources, creating application resources in other K8s clusters or as hyperscaler services (e.g., AWS Lambda, Azure Container Apps, or Google Cloud Run), along with dependent resources like databases (either self-managed or as hyperscaler services).

{{cta_4}}

Conclusion

The internal developer platform, as we’ve shown above, is there to provide the backend of the reusable actions that developers can use. The portal is the interface to the platform. As shown here, you can construct a platform using best practices to enable developer self-service. The portal will create the software catalog and allow self-service. Your platform is likely more complex and nuanced than what’s shown here, but this basic architecture is where it all begins. Happy building!

{{cta_1}}

Check out Port's pre-populated demo and see what it's all about.

Check live demo

No email required

{{cta_2}}

Contact sales for a technical product walkthrough

Let’s start
{{cta_3}}

Open a free Port account. No credit card required

Let’s start
{{cta_4}}

Watch Port live coding videos - setting up an internal developer portal & platform

Let’s start
{{cta_5}}

Check out Port's pre-populated demo and see what it's all about.

(no email required)

Let’s start
{{cta_6}}

Contact sales for a technical product walkthrough

Let’s start
{{cta_7}}

Open a free Port account. No credit card required

Let’s start
{{cta_8}}

Watch Port live coding videos - setting up an internal developer portal & platform

Let’s start
{{cta-demo}}
{{reading-box-backstage-vs-port}}

Example JSON block

{
  "foo": "bar"
}

Order Domain

{
  "properties": {},
  "relations": {},
  "title": "Orders",
  "identifier": "Orders"
}

Cart System

{
  "properties": {},
  "relations": {
    "domain": "Orders"
  },
  "identifier": "Cart",
  "title": "Cart"
}

Products System

{
  "properties": {},
  "relations": {
    "domain": "Orders"
  },
  "identifier": "Products",
  "title": "Products"
}

Cart Resource

{
  "properties": {
    "type": "postgress"
  },
  "relations": {},
  "icon": "GPU",
  "title": "Cart SQL database",
  "identifier": "cart-sql-sb"
}

Cart API

{
 "identifier": "CartAPI",
 "title": "Cart API",
 "blueprint": "API",
 "properties": {
   "type": "Open API"
 },
 "relations": {
   "provider": "CartService"
 },
 "icon": "Link"
}

Core Kafka Library

{
  "properties": {
    "type": "library"
  },
  "relations": {
    "system": "Cart"
  },
  "title": "Core Kafka Library",
  "identifier": "CoreKafkaLibrary"
}

Core Payment Library

{
  "properties": {
    "type": "library"
  },
  "relations": {
    "system": "Cart"
  },
  "title": "Core Payment Library",
  "identifier": "CorePaymentLibrary"
}

Cart Service JSON

{
 "identifier": "CartService",
 "title": "Cart Service",
 "blueprint": "Component",
 "properties": {
   "type": "service"
 },
 "relations": {
   "system": "Cart",
   "resources": [
     "cart-sql-sb"
   ],
   "consumesApi": [],
   "components": [
     "CorePaymentLibrary",
     "CoreKafkaLibrary"
   ]
 },
 "icon": "Cloud"
}

Products Service JSON

{
  "identifier": "ProductsService",
  "title": "Products Service",
  "blueprint": "Component",
  "properties": {
    "type": "service"
  },
  "relations": {
    "system": "Products",
    "consumesApi": [
      "CartAPI"
    ],
    "components": []
  }
}

Component Blueprint

{
 "identifier": "Component",
 "title": "Component",
 "icon": "Cloud",
 "schema": {
   "properties": {
     "type": {
       "enum": [
         "service",
         "library"
       ],
       "icon": "Docs",
       "type": "string",
       "enumColors": {
         "service": "blue",
         "library": "green"
       }
     }
   },
   "required": []
 },
 "mirrorProperties": {},
 "formulaProperties": {},
 "calculationProperties": {},
 "relations": {
   "system": {
     "target": "System",
     "required": false,
     "many": false
   },
   "resources": {
     "target": "Resource",
     "required": false,
     "many": true
   },
   "consumesApi": {
     "target": "API",
     "required": false,
     "many": true
   },
   "components": {
     "target": "Component",
     "required": false,
     "many": true
   },
   "providesApi": {
     "target": "API",
     "required": false,
     "many": false
   }
 }
}

Resource Blueprint

{
 “identifier”: “Resource”,
 “title”: “Resource”,
 “icon”: “DevopsTool”,
 “schema”: {
   “properties”: {
     “type”: {
       “enum”: [
         “postgress”,
         “kafka-topic”,
         “rabbit-queue”,
         “s3-bucket”
       ],
       “icon”: “Docs”,
       “type”: “string”
     }
   },
   “required”: []
 },
 “mirrorProperties”: {},
 “formulaProperties”: {},
 “calculationProperties”: {},
 “relations”: {}
}

API Blueprint

{
 "identifier": "API",
 "title": "API",
 "icon": "Link",
 "schema": {
   "properties": {
     "type": {
       "type": "string",
       "enum": [
         "Open API",
         "grpc"
       ]
     }
   },
   "required": []
 },
 "mirrorProperties": {},
 "formulaProperties": {},
 "calculationProperties": {},
 "relations": {
   "provider": {
     "target": "Component",
     "required": true,
     "many": false
   }
 }
}

Domain Blueprint

{
 "identifier": "Domain",
 "title": "Domain",
 "icon": "Server",
 "schema": {
   "properties": {},
   "required": []
 },
 "mirrorProperties": {},
 "formulaProperties": {},
 "calculationProperties": {},
 "relations": {}
}

System Blueprint

{
 "identifier": "System",
 "title": "System",
 "icon": "DevopsTool",
 "schema": {
   "properties": {},
   "required": []
 },
 "mirrorProperties": {},
 "formulaProperties": {},
 "calculationProperties": {},
 "relations": {
   "domain": {
     "target": "Domain",
     "required": true,
     "many": false
   }
 }
}
{{tabel-1}}

Microservices SDLC

  • Scaffold a new microservice

  • Deploy (canary or blue-green)

  • Feature flagging

  • Revert

  • Lock deployments

  • Add Secret

  • Force merge pull request (skip tests on crises)

  • Add environment variable to service

  • Add IaC to the service

  • Upgrade package version

Development environments

  • Spin up a developer environment for 5 days

  • ETL mock data to environment

  • Invite developer to the environment

  • Extend TTL by 3 days

Cloud resources

  • Provision a cloud resource

  • Modify a cloud resource

  • Get permissions to access cloud resource

SRE actions

  • Update pod count

  • Update auto-scaling group

  • Execute incident response runbook automation

Data Engineering

  • Add / Remove / Update Column to table

  • Run Airflow DAG

  • Duplicate table

Backoffice

  • Change customer configuration

  • Update customer software version

  • Upgrade - Downgrade plan tier

  • Create - Delete customer

Machine learning actions

  • Train model

  • Pre-process dataset

  • Deploy

  • A/B testing traffic route

  • Revert

  • Spin up remote Jupyter notebook

{{tabel-2}}

Engineering tools

  • Observability

  • Tasks management

  • CI/CD

  • On-Call management

  • Troubleshooting tools

  • DevSecOps

  • Runbooks

Infrastructure

  • Cloud Resources

  • K8S

  • Containers & Serverless

  • IaC

  • Databases

  • Environments

  • Regions

Software and more

  • Microservices

  • Docker Images

  • Docs

  • APIs

  • 3rd parties

  • Runbooks

  • Cron jobs

Starting with Port is simple, fast and free.

Let’s start