Services
What is a service?
Services can be databases, caches, load balancers, or any other dependency an application requires to operate. They can exist on-premises or be a part of a cloud service. Since you model your services, you can create them for other types of dependencies such as monitoring, email, or even other applications (e.g., a banking core).
Why should I use services?
Implementing services offers several key benefits:
-
Streamline provisioning and reduce operational load. Provisioning a piece of infrastructure usually requires intervention from a specialized team, leading to delays and additional workload.
-
Lower cognitive load for developers. By moving infrastructure management to an environment overseen by your Infrastructure-as-Code (IaC) experts, developers can focus on coding without needing deep expertise in infrastructure best practices.
-
Standardize your infrastructure and control drift. Services pushes for golden standards, minimizing inconsistencies that can arise when people make custom adjustments to infrastructure.
-
Custom actions: efficient and compliant operations. Once your service (e.g., Database) is running, you'll likely need to do some operational tasks, such as querying a table. By creating a custom action for these tasks—combined with policies and approvals—you enable your team to work efficiently in a secure environment where actions are standardized, controlled, and auditable.
Main concepts
Specifications
Also known as "specs", these define the name, category, and allowed parameters for the services that you'll provision.
You can think of specifications and services as having a relation similar to classes and objects in object-oriented languages where the first ones govern how the second ones are created.
Here's an example of a service specification:
{
"id": "abcd1324-53ab-b41b-a28f-55ca9c95ef1",
"name": "SQS Queue",
"slug": "sqs-queue",
"type": "dependency",
"visible_to": [
"organization=1234:account=*"
],
"dimensions": {
"country": {
"required": true
},
"environment": {
"required": true
}
},
"assignable_to": "dimension",
"created_at": "2024-01-12T13:14:15.199Z",
"updated_at": "2025-01-12T13:14:15.199Z",
"attributes": {
"schema": {
"type": "object",
"required": [
"queue_arn",
"queue_url"
],
"properties": {
"queue_arn": {
"type": "string",
"title": "Queue ARN",
"export": true,
"pattern": "^arn:aws:sqs:[a-z0-9-]+:[0-9]+:([a-zA-Z0-9_-]+\\.fifo|[a-zA-Z0-9_-]+)$"
},
"queue_url": {
"type": "string",
"title": "Queue URL",
"export": true
},
"account_id": {
"type": "string",
"title": "AWS Account ID",
"config": {
"key": "aws.account_id"
},
"export": false,
"readOnly": true
},
"queue_type": {
"type": "string",
"title": "Queue Type",
"export": false,
"readOnly": true
},
"delay_seconds": {
"type": "integer",
"title": "Delay Seconds",
"maximum": 900,
"minimum": 0,
"description": "The time in seconds to delay the delivery of new messages (0-900). Only available for standard queues."
},
"dead_letter_arn": {
"type": "string",
"title": "Dead Letter Queue ARN",
"export": true,
"pattern": "^arn:aws:sqs:[a-z0-9-]+:[0-9]+:([a-zA-Z0-9_-]+\\.fifo|[a-zA-Z0-9_-]+)$"
},
"max_message_size": {
"type": "integer",
"title": "Maximum Message Size",
"maximum": 262144,
"minimum": 1024,
"description": "The maximum size of messages in bytes (1KB to 256KB)"
},
"visibility_timeout": {
"type": "integer",
"title": "Visibility Timeout",
"maximum": 43200,
"minimum": 0,
"description": "The length of time (in seconds) that a message will be invisible to other receiving components"
},
"message_retention_seconds": {
"type": "integer",
"title": "Message Retention Period",
"maximum": 1209600,
"minimum": 60,
"description": "The number of seconds to retain messages (60s to 14 days)"
},
"receive_wait_time_seconds": {
"type": "integer",
"title": "Receive Wait Time",
"maximum": 20,
"minimum": 0,
"description": "The time to wait for messages on a ReceiveMessage call (0-20 seconds)"
}
},
"additionalProperties": false
},
"values": {}
},
"selectors": {
"category": "Messaging Services",
"imported": false,
"provider": "AWS",
"sub_category": "Message Queue"
}
}
There are three kinds of specifications:
- Service specifications
- Link specifications
- Action specifications
This will make complete sense in the following sections as we introduce links and actions in the following sections.
Actions
These are what you can do with a service, and you'll typically have these:
- Create
- Update
- Delete
On top of these you can create custom actions which have the same capabilities as the former, but will be opaque to nullplatform. For example, you could create these custom actions:
- Send message to queue
- Purge cache
- Fetch transaction by ID
Here's an example of a delete action specification:
{
"created_at": "2024-03-31T19:47:28.623Z",
"id": "8bf38bf3-6b71-4d8c-6b71-34d8cd314d8c",
"link_specification_id": null,
"name": "Delete SQS Queue",
"parameters": {
"schema": {},
"values": {}
},
"results": {
"schema": {},
"values": {}
},
"retryable": false,
"service_specification_id": "441b5afb-a38f-5a84-a38f-e55a8415eeb0",
"slug": "delete-sqs-queue",
"type": "delete",
"updated_at": "2025-01-15T16:28:34.741Z"
}
The same way you have service specifications (~class) and services (~objects), you will have action specifications and concrete actions which have been created out of that specification.
How are actions executed?
An action definition is just a JSON establishing the interface to create the action and receive the results, but the actual execution happens (typically) on a runner (e.g., GitHub Actions).
The flow looks like this:
Links
Once you have your service running, you'll want to connect your applications to it, and this is done through links.
Here are some examples:
- For a Queue, you'll have links to scopes which are message producer or consumers.
- In the case of a Database, you'll have scopes that can read , write, or read and write to the database.
- If your service is a Load balancer or API Gateway, you can use links to grant other applications access to your application.
Links and parameters
When you link a service (e.g., Database) to an application you will likely create parameters for that application to actually "link" or "consume" that service. For example, if you have a service which is a Database, you'll export the DB Connection String, Username and Password as parameters, so your application can get those parameters delivered in runtime to interact with the service.
You'll see in the following sections that you can have secret parameters for your service. A parameter marked as secret will also be marked as secret in your application's parameters list.
Also, these rules will apply when linking a service to scopes or dimensions:
- When linking to a scope, the application's parameters will be created with values for that scope only.
- When linking to dimensions, the application's parameters will be created for those dimensions, therefore any scope from those dimensions will be able to use the service.
When a service creates application parameters, those parameters are shown to the users as 'read-only', meaning that only the service can alter their content.
Link types
The application that will access the service might do it with different purposes, for example, you could use a database to read, write, or read/write. The use you'll give to the service will come in the shape of link types.
When you create different link types, we ask the user which kind of link they want to create, so we can display the appropriate link creation form and send the correct link creation action along with its parameters.
Is it mandatory to have links?
No. While it's a good practice, you can theoretically provision a service upon its creation and make it automatically available for your application.
The same way you have service specifications (~class) and services (~objects), you will have link specifications and concrete links which have been created out of that specification.
Summary
Entity | Description |
---|---|
Service specification | Defines the name, category, visibility, and parameters for the services it defines. |
Service | A service instance for a specific service specification. Can be created, updated, or destroyed. |
Action specification | Defines the type (create, update, delete, custom) of action along its parameters. |
Action | An action instance that runs on a runner such as GitHub actions. |
Link specification | Defines how the service can be linked to scopes. You can have different type of links for a service. |
Link | A link instance that can be created, updated, or destroyed. |
How do I design a service?
Low-level cloud services vs. higher-level abstractions
While there's a tendency to replicate cloud services as nullplatform services, we recommend grouping cloud services into a single, higher-level abstraction. For example, if you need processes to produce messages that will be consumed by others, you could create a single Publisher-Subscriber service that provisions SNS and SQS queues behind the scenes. Generating these higher level abstractions will also shield developers and users from low-level details that are needed to glue the cloud services together.
Choose the essential service options to expose to users
The benefits of creating services have to do with standardization and reducing complexity for users. Therefore, you'll want to narrow down the available options to what really matters. While the underlying service might allow fine-grained tuning of options such as "encryption", you'll likely want to apply your own security criteria rather than relying on user discretion.
Service and link visibility
You can narrow your service availability by specifying which NRN will have access to it. Also, you might specify if the service can be linked to dimensions and/or scopes.
Summary: DO's and DONT's
Let's summarize these recommendations like this:
- ✅ DO favor services that simplify complexity by avoiding unnecessary parameters.
- ✅ DO use JSON schema to validate parameters before running actions.
- ✅ DO leverage on IaC tools such as OpenTofu, hiding complex details from end users.
- ✅ DO design user interfaces that feel intuitive and natural using UI schemas.
- ❌ DON'T create a one-to-one copy of the cloud services you have. For example, try to aggregate SNS+SQS into a more user-friendly "Publisher-Subscriber" model.
- ❌ DON'T create complex interactions that require end users to have a lot of context or follow steps in a specific order.
Next steps
Now that you have the basic concepts on services, it's time to read the subsections to get a thorough guide on how you can go from zero to production with services.
Here are some quick links that can be useful to jump to: