Adding New Resource Types
This guide explains how to extend the Nitric CLI to support custom resource types, covering both deployment (nitric up) and local development (nitric start).
This guide is for contributors who want to add entirely new resource types to Nitric's core. If you want to replace existing resources in a provider or build a custom provider, see Extending Standard Providers or Building Custom Providers.
Overview
Adding a new resource type to Nitric requires changes across three repositories:
- nitric/core - Proto definitions and gRPC service interfaces
- nitric/cli - Resource collection, spec building, and local development
- Your custom provider - Cloud-specific deployment logic
Architecture
The following diagram shows how resources flow through the Nitric system:
Your application code makes SDK calls that are sent via gRPC to the Nitric CLI's collector. The collector builds a deployment spec (protobuf), which is then sent to your provider for cloud-specific resource creation.
Part 1: Nitric Core Changes
The first step is defining your new resource type in the nitric/core repository.
Clone the Repository
git clone https://github.com/nitrictech/nitric.git
Add Resource Type Enum
The proto source files are in nitric/proto/, and Go code is generated into core/pkg/proto/.
In nitric/proto/resources/v1/resources.proto, add your new resource type to the ResourceType enum:
enum ResourceType {// ... existing types ...YourResourceType = N; // Use the next available number}
Define Resource Proto Message
In the same file, add a message for your resource configuration:
message YourResource {// Resource-specific configuration fieldsstring some_config = 1;}
Update ResourceDeclareRequest
Add your resource to the ResourceDeclareRequest oneof:
message ResourceDeclareRequest {ResourceIdentifier id = 1;oneof config {// ... existing configs ...YourResource your_resource = N;}}
Add Deployment Resource
If your resource needs deployment-specific configuration, update nitric/proto/deployments/v1/deployments.proto:
message YourDeploymentResource {// Deployment-specific fields (image URIs, targets, etc.)}message Resource {// In the config oneof:oneof config {// ... existing configs ...YourDeploymentResource your_resource = N;}}
Regenerate Proto Code
Run the proto generation in the core directory to create Go code from your proto definitions:
cd core && make generate-proto
Part 2: CLI Changes - Resource Collection
The following changes are made in the nitric/cli repository, not nitric/core.
These changes enable nitric up to collect your new resource type from application code.
Update ServiceRequirements Struct
In pkg/collector/service.go, add a field for your new resource in the ServiceRequirements struct:
type ServiceRequirements struct {// ... existing fields ...yourResources map[string]*resourcespb.YourResource}
Handle Resource Declaration
Add a case in the Declare() method to handle your resource type:
func (s *ServiceRequirements) Declare(ctx context.Context, req *resourcespb.ResourceDeclareRequest) (*resourcespb.ResourceDeclareResponse, error) {s.resourceLock.Lock()defer s.resourceLock.Unlock()// ... existing validation ...switch req.Id.Type {// ... existing cases ...case resourcespb.ResourceType_YourResourceType:s.yourResources[req.Id.GetName()] = req.GetYourResource()}return &resourcespb.ResourceDeclareResponse{}, nil}
Initialize the Map
In NewServiceRequirements(), initialize your map:
func NewServiceRequirements(serviceName string, serviceFile string, serviceType string) *ServiceRequirements {requirements := &ServiceRequirements{// ... existing initializations ...yourResources: make(map[string]*resourcespb.YourResource),}return requirements}
Update BatchRequirements
If your resource should be available to batch jobs, make similar changes in pkg/collector/batch.go:
- Add field to
BatchRequirementsstruct - Add case in
Declare()method - Initialize in
NewBatchRequirements()
Build Deployment Spec
In pkg/collector/spec.go, create a builder function for your resource:
func buildYourResourceRequirements(allServiceRequirements []*ServiceRequirements, allBatchRequirements []*BatchRequirements, projectErrors *ProjectErrors) ([]*deploymentspb.Resource, error) {resources := []*deploymentspb.Resource{}for _, serviceRequirements := range allServiceRequirements {for resourceName, config := range serviceRequirements.yourResources {_, exists := lo.Find(resources, func(item *deploymentspb.Resource) bool {return item.Id.Name == resourceName})if !exists {res := &deploymentspb.Resource{Id: &resourcespb.ResourceIdentifier{Name: resourceName,Type: resourcespb.ResourceType_YourResourceType,},Config: &deploymentspb.Resource_YourResource{YourResource: &deploymentspb.YourDeploymentResource{// Map configuration},},}resources = append(resources, res)}}}// Similar loop for batch requirements if neededreturn resources, nil}
Call Your Builder
Add your builder call in ServiceRequirementsToSpec():
func ServiceRequirementsToSpec(...) (*deploymentspb.Spec, error) {// ... existing code ...yourResources, err := buildYourResourceRequirements(allServiceRequirements, allBatchRequirements, projectErrors)if err != nil {return nil, err}newSpec.Resources = append(newSpec.Resources, yourResources...)// ... rest of function ...}
Part 3: CLI Changes - Local Development
The following changes are also made in the nitric/cli repository.
To support your new resource in nitric start, you'll need to implement a local service.
Create Local Service
Create a new file pkg/cloud/yourresource/yourresource.go:
package yourresourceimport ("context""sync""github.com/asaskevich/EventBus"yourpb "github.com/nitrictech/nitric/core/pkg/proto/yourresource/v1")type LocalYourResourceState struct {// State that the dashboard/gateway needs to see}type LocalYourResourceService struct {stateLock sync.RWMutexstate LocalYourResourceStatebus EventBus.Bus}func (s *LocalYourResourceService) SubscribeToState(fn func(LocalYourResourceState)) {_ = s.bus.Subscribe("your_resource_topic", fn)}func (s *LocalYourResourceService) SomeMethod(ctx context.Context, req *yourpb.SomeRequest) (*yourpb.SomeResponse, error) {// Implementationreturn &yourpb.SomeResponse{}, nil}func NewLocalYourResourceService() (*LocalYourResourceService, error) {return &LocalYourResourceService{bus: EventBus.New(),}, nil}
Register with LocalCloud
In pkg/cloud/cloud.go:
- Import your package:
import (// ... existing imports ..."github.com/nitrictech/cli/pkg/cloud/yourresource")
- Add field to
LocalCloudstruct:
type LocalCloud struct {// ... existing fields ...YourResource *yourresource.LocalYourResourceService}
- Initialize in
New()function:
func New(projectName string, opts LocalCloudOptions) (*LocalCloud, error) {// ... existing initializations ...localYourResource, err := yourresource.NewLocalYourResourceService()if err != nil {return nil, err}return &LocalCloud{// ... existing fields ...YourResource: localYourResource,}, nil}
- Wire into server plugins in
AddService()andAddBatch():
First, you'll need to add the WithYourResourcePlugin option to the nitric/core
server package (in core/pkg/server/options.go). This function must accept
your proto-generated service interface (e.g., yourresourcepb.YourResourceServer)
and follow the same pattern as existing plugins like WithStoragePlugin:
func WithYourResourcePlugin(plugin yourresourcepb.YourResourceServer) ServerOption {return func(s *Server) {yourresourcepb.RegisterYourResourceServer(s.grpcServer, plugin)}}
See core/pkg/server/options.go for complete examples.
nitricRuntimeServer, _ := server.New(// ... existing plugins ...server.WithYourResourcePlugin(lc.YourResource),// ...)
Update Local Resources Service
In pkg/cloud/resources/resources.go, if your resource should be tracked in the dashboard:
- Add to
LocalResourcesState:
type LocalResourcesState struct {// ... existing fields ...YourResources *ResourceRegistrar[resourcespb.YourResource]}
- Handle in
Declare()method:
case resourcespb.ResourceType_YourResourceType:err = l.state.YourResources.Register(req.Id.Name, serviceName, req.GetYourResource())
- Initialize in
NewLocalResourcesService():
YourResources: NewResourceRegistrar[resourcespb.YourResource](),
- Clear in
ClearServiceResources():
l.state.YourResources.ClearRequestingService(serviceName)
Part 4: Provider Implementation
Your custom provider receives the deployment spec via gRPC and creates cloud resources.
Implement Resource Type Method
Providers implement the NitricPulumiProvider interface. Create a new file deploy/yourresource.go with a method for your resource type:
package deployimport (deploymentspb "github.com/nitrictech/nitric/core/pkg/proto/deployments/v1""github.com/pulumi/pulumi/sdk/v3/go/pulumi"// Import your cloud provider's SDK (e.g., AWS, GCP, Azure))func (p *NitricYourCloudProvider) YourResourceType(ctx *pulumi.Context,parent pulumi.Resource,name string,config *deploymentspb.YourDeploymentResource,) error {// Create your cloud resource using the Pulumi SDK// For example, using AWS SDK with Pulumi:resource, err := yourservice.NewResource(ctx, name, &yourservice.ResourceArgs{// Map config to cloud provider arguments}, pulumi.Parent(parent))if err != nil {return err}// Store resource reference if needed for later usep.YourResources[name] = resourcereturn nil}
The deployment framework automatically calls your method for each resource of this type. Follow the one-file-per-resource pattern used by existing providers - see cloud/aws/deploy/queue.go (simple) or cloud/aws/deploy/api.go (complex) for examples.
You'll also need to add a field to your provider struct in deploy/deploy.go:
type NitricYourCloudProvider struct {// ... existing fields ...YourResources map[string]*YourResourceType}
Implement gRPC Runtime Service
If your resource has runtime operations (e.g., read/write), first create a service proto in nitric/proto/yourresource/v1/yourresource.proto:
syntax = "proto3";package nitric.proto.yourresource.v1;option go_package = "github.com/nitrictech/nitric/core/pkg/proto/yourresource/v1;yourresourcepb";service YourResource {rpc Get (GetRequest) returns (GetResponse);rpc Set (SetRequest) returns (SetResponse);rpc Delete (DeleteRequest) returns (DeleteResponse);}// Define request/response messages...
See nitric/proto/storage/v1/storage.proto or nitric/proto/kvstore/v1/kvstore.proto for complete examples. Run make generate-proto again after adding this file.
Then implement the generated service interface in your provider.
Part 5: Update go.mod
Point to your forked nitric/core:
replace github.com/nitrictech/nitric/core => github.com/your-org/nitric/core v0.0.0-xxxxx
Or for local development:
replace github.com/nitrictech/nitric/core => ../path/to/your/nitric/core
Key Files Reference
| Purpose | File Path |
|---|---|
| Service resource collection | pkg/collector/service.go |
| Batch resource collection | pkg/collector/batch.go |
| Spec building | pkg/collector/spec.go |
| Deployment flow | cmd/stack.go |
| Provider interface | pkg/provider/provider.go |
| Deployment client | pkg/provider/client.go |
| Local cloud setup | pkg/cloud/cloud.go |
| Local resources | pkg/cloud/resources/resources.go |
Deployment Flow Summary
- Build - CLI builds Docker images for services
- Collect - CLI starts gRPC server, runs containers, collects resource declarations
- Spec - CLI converts collected requirements to deployment spec
- Provider Start - CLI starts provider binary
- Deploy - CLI sends spec to provider via gRPC
- Events - Provider streams deployment progress back to CLI
Testing Your Changes
Build the CLI:
make build
Test resource collection:
./nitric up -s your-stack --debug
For local development testing:
./nitric start
Notes
- The CLI uses proto definitions from
github.com/nitrictech/nitric/core - Proto packages are imported from
github.com/nitrictech/nitric/core/pkg/proto/*/v1 - Resources must have valid names (checked by
validation.IsValidResourceName()) - Policies are handled specially - principals are auto-populated with service names
Have feedback on this page?
Open GitHub Issue