Outcome: After this guide, you can manage Formal resources using Pulumi infrastructure-as-code.
Prerequisites: Pulumi CLI, Formal API key, programming language environment (TypeScript, Python, Go, or C#).
Overview
The Formal Pulumi Provider enables you to manage Connectors, Resources, Policies, Users, and all other Formal objects as infrastructure-as-code using your preferred programming language.Installation
The Formal provider is available for multiple programming languages:- TypeScript/JavaScript
- Python
- Go
- C#
Copy
Ask AI
npm install @formalco/pulumi
Copy
Ask AI
pip install pulumi-formal
Copy
Ask AI
go get github.com/formalco/pulumi-formal/sdk/go/formal
Copy
Ask AI
dotnet add package Formal.Pulumi
Authentication
Create an API key in the Formal console:- Navigate to API Keys
- Click Create API Key
- Name the key (e.g., “pulumi-production”)
- Copy the key immediately
Copy
Ask AI
pulumi config set formal:apiKey <YOUR_API_KEY> --secret
Copy
Ask AI
pulumi config get formal:apiKey
# Expected: <YOUR_API_KEY>
Copy
Ask AI
export FORMAL_API_KEY="<YOUR_API_KEY>"
Examples
Full Production Stack
- TypeScript
- Python
- Go
- C#
Copy
Ask AI
import * as pulumi from "@pulumi/pulumi";
import * as formal from "@formalco/pulumi";
// Create a Space
const productionSpace = new formal.Space("production", {
name: "production"
});
// Create a Resource
const postgresResource = new formal.Resource("postgres", {
name: "production-postgres",
technology: "postgres",
hostname: "prod-db.us-east-1.rds.amazonaws.com",
port: 5432,
spaceId: productionSpace.id,
terminationProtection: true
});
// Create a Connector
const productionConnector = new formal.Connector("production", {
name: "production-connector",
spaceId: productionSpace.id,
listener: [{
port: 5432,
rule: [{
type: "resource",
resourceId: postgresResource.id
}]
}]
});
// Create Users
const aliceUser = new formal.User("alice", {
name: "Alice Smith",
email: "[email protected]",
type: "human"
});
const lookerApp = new formal.User("looker_app", {
name: "Looker Application",
type: "machine"
});
// Create Groups
const engineeringGroup = new formal.Group("engineering", {
name: "Engineering Team"
});
const aliceMembership = new formal.GroupMembership("alice_eng", {
groupId: engineeringGroup.id,
userId: aliceUser.id
});
// Create a Policy
const maskPiiPolicy = new formal.Policy("mask_pii", {
name: "mask-pii-data",
description: "Mask PII fields for non-privileged users",
status: "active",
owners: ["[email protected]"],
code: `package formal.v2
import future.keywords.if
import future.keywords.in
post_request := {
"action": "mask",
"type": "nullify",
"columns": pii_columns
} if {
not "pii_access" in input.user.groups
pii_columns := [col |
col := input.row[_]
col["data_label"] in ["email", "ssn", "phone"]
]
count(pii_columns) > 0
}`
});
// AWS Integration
const awsIntegration = new formal.IntegrationCloud("aws", {
name: "aws-production",
cloudRegion: "us-east-1",
aws: {
templateVersion: "1.2.0",
enableRdsAutodiscovery: true,
allowS3Access: true,
s3BucketArn: "<S3_BUCKET_ARN>/*"
}
});
// Log Integration
const datadogIntegration = new formal.IntegrationLog("datadog", {
name: "datadog-logs",
datadog: {
accountId: "<DATADOG_ACCOUNT_ID>",
apiKey: "<DATADOG_API_KEY>",
site: "datadoghq.com"
}
});
// Export outputs
export const connectorToken = productionConnector.apiKey;
export const connectorListeners = productionConnector.listener.map(l => l.port);
export const resourceIds = {
postgres: postgresResource.id
};
Copy
Ask AI
import pulumi
import pulumi_formal as formal
# Create a Space
production_space = formal.Space("production",
name="production"
)
# Create a Resource
postgres_resource = formal.Resource("postgres",
name="production-postgres",
technology="postgres",
hostname="prod-db.us-east-1.rds.amazonaws.com",
port=5432,
space_id=production_space.id,
termination_protection=True
)
# Create a Connector
production_connector = formal.Connector("production",
name="production-connector",
space_id=production_space.id,
listener=[formal.ConnectorListenerArgs(
port=5432,
rule=[formal.ConnectorListenerRuleArgs(
type="resource",
resource_id=postgres_resource.id
)]
)]
)
# Create Users
alice_user = formal.User("alice",
name="Alice Smith",
email="[email protected]",
type="human"
)
looker_app = formal.User("looker_app",
name="Looker Application",
type="machine"
)
# Create Groups
engineering_group = formal.Group("engineering",
name="Engineering Team"
)
alice_membership = formal.GroupMembership("alice_eng",
group_id=engineering_group.id,
user_id=alice_user.id
)
# Create a Policy
mask_pii_policy = formal.Policy("mask_pii",
name="mask-pii-data",
description="Mask PII fields for non-privileged users",
status="active",
owners=["[email protected]"],
code="""package formal.v2
import future.keywords.if
import future.keywords.in
post_request := {
"action": "mask",
"type": "nullify",
"columns": pii_columns
} if {
not "pii_access" in input.user.groups
pii_columns := [col |
col := input.row[_]
col["data_label"] in ["email", "ssn", "phone"]
]
count(pii_columns) > 0
}"""
)
# AWS Integration
aws_integration = formal.IntegrationCloud("aws",
name="aws-production",
cloud_region="us-east-1",
aws=formal.IntegrationCloudAwsArgs(
template_version="1.2.0",
enable_rds_autodiscovery=True,
allow_s3_access=True,
s3_bucket_arn="<S3_BUCKET_ARN>/*"
)
)
# Log Integration
datadog_integration = formal.IntegrationLog("datadog",
name="datadog-logs",
datadog=formal.IntegrationLogDatadogArgs(
account_id="<DATADOG_ACCOUNT_ID>",
api_key="<DATADOG_API_KEY>",
site="datadoghq.com"
)
)
# Export outputs
pulumi.export("connector_token", production_connector.api_key)
pulumi.export("connector_listeners", [l.port for l in production_connector.listener])
pulumi.export("resource_ids", {
"postgres": postgres_resource.id
})
Copy
Ask AI
package main
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
formal "github.com/formalco/pulumi-formal/sdk/go/formal"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Create a Space
productionSpace, err := formal.NewSpace(ctx, "production", &formal.SpaceArgs{
Name: pulumi.String("production"),
})
if err != nil {
return err
}
// Create a Resource
postgresResource, err := formal.NewResource(ctx, "postgres", &formal.ResourceArgs{
Name: pulumi.String("production-postgres"),
Technology: pulumi.String("postgres"),
Hostname: pulumi.String("prod-db.us-east-1.rds.amazonaws.com"),
Port: pulumi.Int(5432),
SpaceId: productionSpace.ID(),
TerminationProtection: pulumi.Bool(true),
})
if err != nil {
return err
}
// Create a Connector
productionConnector, err := formal.NewConnector(ctx, "production", &formal.ConnectorArgs{
Name: pulumi.String("production-connector"),
SpaceId: productionSpace.ID(),
Listener: formal.ConnectorListenerArray{
&formal.ConnectorListenerArgs{
Port: pulumi.Int(5432),
Rule: formal.ConnectorListenerRuleArray{
&formal.ConnectorListenerRuleArgs{
Type: pulumi.String("resource"),
ResourceId: postgresResource.ID(),
},
},
},
},
})
if err != nil {
return err
}
// Create Users
aliceUser, err := formal.NewUser(ctx, "alice", &formal.UserArgs{
Name: pulumi.String("Alice Smith"),
Email: pulumi.String("[email protected]"),
Type: pulumi.String("human"),
})
if err != nil {
return err
}
lookerApp, err := formal.NewUser(ctx, "looker_app", &formal.UserArgs{
Name: pulumi.String("Looker Application"),
Type: pulumi.String("machine"),
})
if err != nil {
return err
}
// Create Groups
engineeringGroup, err := formal.NewGroup(ctx, "engineering", &formal.GroupArgs{
Name: pulumi.String("Engineering Team"),
})
if err != nil {
return err
}
_, err = formal.NewGroupMembership(ctx, "alice_eng", &formal.GroupMembershipArgs{
GroupId: engineeringGroup.ID(),
UserId: aliceUser.ID(),
})
if err != nil {
return err
}
// Create a Policy
maskPiiPolicy, err := formal.NewPolicy(ctx, "mask_pii", &formal.PolicyArgs{
Name: pulumi.String("mask-pii-data"),
Description: pulumi.String("Mask PII fields for non-privileged users"),
Status: pulumi.String("active"),
Owners: pulumi.StringArray{pulumi.String("[email protected]")},
Code: pulumi.String(`package formal.v2
import future.keywords.if
import future.keywords.in
post_request := {
"action": "mask",
"type": "nullify",
"columns": pii_columns
} if {
not "pii_access" in input.user.groups
pii_columns := [col |
col := input.row[_]
col["data_label"] in ["email", "ssn", "phone"]
]
count(pii_columns) > 0
}`),
})
if err != nil {
return err
}
// AWS Integration
awsIntegration, err := formal.NewIntegrationCloud(ctx, "aws", &formal.IntegrationCloudArgs{
Name: pulumi.String("aws-production"),
CloudRegion: pulumi.String("us-east-1"),
Aws: &formal.IntegrationCloudAwsArgs{
TemplateVersion: pulumi.String("1.2.0"),
EnableRdsAutodiscovery: pulumi.Bool(true),
AllowS3Access: pulumi.Bool(true),
S3BucketArn: pulumi.String("<S3_BUCKET_ARN>/*"),
},
})
if err != nil {
return err
}
// Log Integration
datadogIntegration, err := formal.NewIntegrationLog(ctx, "datadog", &formal.IntegrationLogArgs{
Name: pulumi.String("datadog-logs"),
Datadog: &formal.IntegrationLogDatadogArgs{
AccountId: pulumi.String("<DATADOG_ACCOUNT_ID>"),
ApiKey: pulumi.String("<DATADOG_API_KEY>"),
Site: pulumi.String("datadoghq.com"),
},
})
if err != nil {
return err
}
// Export outputs
ctx.Export("connectorToken", productionConnector.ApiKey())
ctx.Export("connectorListeners", pulumi.ToStringArray([]pulumi.StringOutput{
productionConnector.Listener().Index(pulumi.Int(0)).Port(),
}))
ctx.Export("resourceIds", pulumi.StringMap{
"postgres": postgresResource.ID(),
})
return nil
})
}
Copy
Ask AI
using System.Collections.Generic;
using Pulumi;
using Formal.Pulumi;
return await Deployment.RunAsync(() =>
{
// Create a Space
var productionSpace = new Space("production", new SpaceArgs
{
Name = "production"
});
// Create a Resource
var postgresResource = new Resource("postgres", new ResourceArgs
{
Name = "production-postgres",
Technology = "postgres",
Hostname = "prod-db.us-east-1.rds.amazonaws.com",
Port = 5432,
SpaceId = productionSpace.Id,
TerminationProtection = true
});
// Create a Connector
var productionConnector = new Connector("production", new ConnectorArgs
{
Name = "production-connector",
SpaceId = productionSpace.Id,
Listener = new[]
{
new ConnectorListenerArgs
{
Port = 5432,
Rule = new[]
{
new ConnectorListenerRuleArgs
{
Type = "resource",
ResourceId = postgresResource.Id
}
}
}
}
});
// Create Users
var aliceUser = new User("alice", new UserArgs
{
Name = "Alice Smith",
Email = "[email protected]",
Type = "human"
});
var lookerApp = new User("looker_app", new UserArgs
{
Name = "Looker Application",
Type = "machine"
});
// Create Groups
var engineeringGroup = new Group("engineering", new GroupArgs
{
Name = "Engineering Team"
});
var aliceMembership = new GroupMembership("alice_eng", new GroupMembershipArgs
{
GroupId = engineeringGroup.Id,
UserId = aliceUser.Id
});
// Create a Policy
var maskPiiPolicy = new Policy("mask_pii", new PolicyArgs
{
Name = "mask-pii-data",
Description = "Mask PII fields for non-privileged users",
Status = "active",
Owners = new[] { "[email protected]" },
Code = @"package formal.v2
import future.keywords.if
import future.keywords.in
post_request := {
""action"": ""mask"",
""type"": ""nullify"",
""columns"": pii_columns
} if {
not ""pii_access"" in input.user.groups
pii_columns := [col |
col := input.row[_]
col[""data_label""] in [""email"", ""ssn"", ""phone""]
]
count(pii_columns) > 0
}"
});
// AWS Integration
var awsIntegration = new IntegrationCloud("aws", new IntegrationCloudArgs
{
Name = "aws-production",
CloudRegion = "us-east-1",
Aws = new IntegrationCloudAwsArgs
{
TemplateVersion = "1.2.0",
EnableRdsAutodiscovery = true,
AllowS3Access = true,
S3BucketArn = "<S3_BUCKET_ARN>/*"
}
});
// Log Integration
var datadogIntegration = new IntegrationLog("datadog", new IntegrationLogArgs
{
Name = "datadog-logs",
Datadog = new IntegrationLogDatadogArgs
{
AccountId = "<DATADOG_ACCOUNT_ID>",
ApiKey = "<DATADOG_API_KEY>",
Site = "datadoghq.com"
}
});
// Export outputs
return new Dictionary<string, object?>
{
["connectorToken"] = productionConnector.ApiKey,
["connectorListeners"] = new[] { productionConnector.Listener[0].Port },
["resourceIds"] = new Dictionary<string, object?>
{
["postgres"] = postgresResource.Id
}
};
});
Resource Documentation
Full documentation for all resources: Core Objects: Integrations:Example Repositories
Formal provides complete Pulumi examples: Clone and customize for your needs:Copy
Ask AI
git clone https://github.com/formalco/pulumi-formal.git
cd pulumi-formal/examples/deployments/aws/connector
Best Practices
Use Version Constraints
Use Version Constraints
Pin provider versions to avoid unexpected changes:TypeScript:Python:
Copy
Ask AI
{
"dependencies": {
"@formalco/pulumi": "^1.0.0"
}
}
Copy
Ask AI
pulumi-formal==1.0.0
Store State Remotely
Store State Remotely
Use Pulumi Cloud or self-hosted backends for team collaboration:
Copy
Ask AI
pulumi login https://api.pulumi.com
pulumi stack init production
Use Configuration
Use Configuration
Parameterize configurations for reusability:Access in code:
Copy
Ask AI
pulumi config set environment production
pulumi config set formal:apiKey <KEY> --secret
Copy
Ask AI
const config = new pulumi.Config();
const environment = config.require("environment");
Separate Environments
Separate Environments
Use separate stacks for prod/staging/dev:
Copy
Ask AI
pulumi stack init production
pulumi stack init staging
pulumi stack init development
Enable Termination Protection
Enable Termination Protection
Protect production resources:
Copy
Ask AI
const resource = new formal.Resource("prod_db", {
name: "production-postgres",
terminationProtection: true,
// ...
});
Importing Existing Resources
Bring existing Formal objects under Pulumi management:Copy
Ask AI
# Import a Resource
pulumi import formal:index/resource:Resource postgres <resource-id>
# Import a Connector
pulumi import formal:index/connector:Connector main <connector-id>
# Import a User
pulumi import formal:index/user:User alice <user-id>
Outputs
Export useful information:- TypeScript
- Python
- Go
- C#
Copy
Ask AI
export const connectorToken = productionConnector.apiKey;
export const connectorListeners = productionConnector.listener.map(l => l.port);
export const resourceIds = {
postgres: postgresResource.id
};
Copy
Ask AI
pulumi.export("connector_token", production_connector.api_key)
pulumi.export("connector_listeners", [l.port for l in production_connector.listener])
pulumi.export("resource_ids", {
"postgres": postgres_resource.id
})
Copy
Ask AI
ctx.Export("connectorToken", productionConnector.ApiKey())
ctx.Export("connectorListeners", pulumi.ToStringArray([]pulumi.StringOutput{
productionConnector.Listener().Index(pulumi.Int(0)).Port(),
}))
ctx.Export("resourceIds", pulumi.StringMap{
"postgres": postgresResource.ID(),
})
Copy
Ask AI
return new Dictionary<string, object?>
{
["connectorToken"] = productionConnector.ApiKey,
["connectorListeners"] = new[] { productionConnector.Listener[0].Port },
["resourceIds"] = new Dictionary<string, object?>
{
["postgres"] = postgresResource.Id
}
};
Troubleshooting
Authentication failed
Authentication failed
Possible causes:
- Invalid API key
- API key not set correctly
- Verify API key in Formal console
- Check configuration:
pulumi config get formal:apiKey - Ensure no whitespace in key
Provider not found
Provider not found
Possible causes:
- Package not installed
- Wrong package name
- Install correct package for your language
- Verify import statements
- Check package.json/requirements.txt
Resource creation fails
Resource creation fails
Possible causes:
- Invalid resource configuration
- Missing required fields
- Check API documentation
- Verify all required fields are set
- Check resource naming conventions