Overview
PriceSignal includes a complete Infrastructure as Code (IaC) solution using Pulumi with .NET. The infrastructure code deploys the application to a Kubernetes cluster with proper configuration, secrets management, and HTTPS support via cert-manager.
Project Structure
infra/
├── Program.cs # Main Pulumi program
├── TelegramBotDeployment.cs # Telegram bot deployment
├── Pulumi.yaml # Pulumi project configuration
└── Pulumi.dev.yaml # Development stack configuration
Prerequisites
Install Pulumi CLI
# macOS
brew install pulumi/tap/pulumi
# Linux
curl -fsSL https://get.pulumi.com | sh
# Windows
choco install pulumi
Install .NET 8 SDK
# Download from https://dotnet.microsoft.com/download
# Or use package manager
brew install dotnet@8 # macOS
Configure kubectl
Ensure you have access to your Kubernetes cluster: kubectl cluster-info
kubectl get nodes
Install cert-manager (for HTTPS)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
# Create Let's Encrypt issuer
kubectl apply -f - << EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
EOF
Pulumi Configuration
Project Configuration
The Pulumi.yaml defines the project:
name : PriceSignal.IaC
runtime : dotnet
description : A C# program to deploy a web application onto a Kubernetes cluster
config :
pulumi:tags :
value :
pulumi:template : ""
Stack Configuration
Pulumi stacks allow you to manage multiple environments (dev, staging, prod) with different configurations.
The Pulumi.dev.yaml contains environment-specific settings:
config :
PriceSignal.IaC:alpacaApiKey :
secure : AAABAL1UuhMuiCRj5FsZF6i3bECP4RB+kYuI6x8kyBlREdWmx9WTXAqVE7MXpci6THOeVw==
PriceSignal.IaC:alpacaApiSecret :
secure : AAABAGwUu6rmkzuvAnRY4/xqTkJ6TuAsJcgU2M8reHdff17oO/zRc8HG/pyGNgnTOMEYwVlNwiMfmCz+IvAdLX1IcB2KTJqv
PriceSignal.IaC:namespace : default
PriceSignal.IaC:natsUrl : nats://nats:4222
PriceSignal.IaC:replicas : "1"
PriceSignal.IaC:telegramBotToken :
secure : AAABAMnip4ACZ/EVYId+Ujvmmk1ZS0solIWkqY7UkeFC7UIRc4tmF2z7yefT7WJrkL/+25N4ocdOrampBDO2K7wZ5dhR58FadUJu6Tic
Setting Configuration Values
Set Plain Values
Set Secret Values
# Set namespace
pulumi config set namespace production
# Set number of replicas
pulumi config set replicas 3
# Set NATS URL
pulumi config set natsUrl nats://nats.production.svc.cluster.local:4222
Deployment Components
The Pulumi program deploys the following Kubernetes resources:
Namespace
var webserverNs = new Namespace ( "webserverNs" , new ()
{
Metadata = new ObjectMetaArgs
{
Name = k8sNamespace , // From config
},
});
ConfigMap
Mounts appsettings.json into the container:
var webserverconfig = new ConfigMap ( "appsettings" , new ()
{
Metadata = new ObjectMetaArgs
{
Namespace = webserverNs . Metadata . Apply ( m => m . Name ),
},
Data =
{
{ "appsettings.json" , File . ReadAllText ( "../src/PriceSignal/appsettings.json" ) },
},
});
Deployment
Defines the main application deployment:
Container Configuration
Environment Variables
new ContainerArgs
{
Image = "nayth/price-signal-graph:latest" ,
ImagePullPolicy = "Always" ,
Name = "price-signal-graph" ,
Ports = new []
{
new ContainerPortArgs
{
ContainerPortValue = 8080
},
},
VolumeMounts = new []
{
new VolumeMountArgs
{
MountPath = "/app/appsettings.json" ,
Name = "appsettings-volume" ,
ReadOnly = true ,
SubPath = "appsettings.json" ,
},
new VolumeMountArgs
{
MountPath = "/app/secrets" ,
Name = "price-signal-secret-volume" ,
ReadOnly = true ,
},
},
Env = env
}
Service
Exposes the application within the cluster:
var webserverservice = new Service ( "price-signal-graph-service" , new ()
{
Metadata = new ObjectMetaArgs
{
Namespace = webserverNs . Metadata . Apply ( m => m . Name ),
},
Spec = new ServiceSpecArgs
{
Ports = new []
{
new ServicePortArgs
{
Port = 80 ,
TargetPort = 8080 ,
},
},
Selector = appLabels ,
},
});
Ingress
Provides HTTPS access with automatic TLS certificates:
var webserveringress = new Ingress ( "price-signal-graph-ingress" , new ()
{
Metadata = new ObjectMetaArgs
{
Namespace = webserverNs . Metadata . Apply ( m => m . Name ),
Annotations = new InputMap < string >
{
{ "kubernetes.io/ingress.class" , "nginx" },
{ "cert-manager.io/cluster-issuer" , "letsencrypt-prod" }
},
Name = "price-signal-graph-ingress" ,
},
Spec = new IngressSpecArgs
{
Tls = new InputList < IngressTLSArgs >()
{
new IngressTLSArgs
{
Hosts = new [] { "price-signal-graph.nxtspec.com" },
SecretName = "price-signal-graph-tls" ,
},
},
Rules = new []
{
new IngressRuleArgs
{
Host = "price-signal-graph.nxtspec.com" ,
Http = new HTTPIngressRuleValueArgs
{
Paths = new []
{
new HTTPIngressPathArgs
{
Path = "/" ,
PathType = "Prefix" ,
Backend = new IngressBackendArgs
{
Service = new IngressServiceBackendArgs
{
Name = webserverservice . Metadata . Apply ( m => m . Name ),
Port = new ServiceBackendPortArgs
{
Number = webserverservice . Spec . Apply ( s => s . Ports [ 0 ]. Port ),
}
}
},
},
},
},
},
},
},
});
Telegram Bot Deployment
The infrastructure also deploys a separate Telegram bot service:
var telegramBot = new TelegramBotDeployment ( webserverNs );
This creates a deployment for:
Image : nayth/price-signal-telegram-bot:latest
Environment Variables : TELEGRAM_BOT_TOKEN, NATS_URL
Port : 80
See TelegramBotDeployment.cs for full implementation.
Deployment Workflow
Navigate to Infrastructure Directory
Login to Pulumi
# Use Pulumi Cloud (free for individuals)
pulumi login
# Or use local backend
pulumi login --local
Initialize or Select Stack
# Create new stack
pulumi stack init production
# Or select existing stack
pulumi stack select dev
Configure Stack
# Set configuration values
pulumi config set namespace production
pulumi config set replicas 3
pulumi config set natsUrl nats://nats:4222
# Set secrets
pulumi config set --secret alpacaApiKey your_key
pulumi config set --secret alpacaApiSecret your_secret
pulumi config set --secret telegramBotToken your_token
Preview Changes
This shows what resources will be created/updated/deleted.
Deploy Infrastructure
Review the changes and confirm to deploy.
Verify Deployment
# Get deployment name from Pulumi outputs
pulumi stack output deploymentName
pulumi stack output serviceName
# Check Kubernetes resources
kubectl get pods -n production
kubectl get services -n production
kubectl get ingress -n production
Required Kubernetes Secrets
Before deploying, ensure the TimescaleDB secret exists in your cluster.
The deployment expects a secret named timescale-cluster-app containing the database URI:
kubectl create secret generic timescale-cluster-app \
--from-literal=uri= "postgresql://username:password@timescale-host:5432/price_signal" \
-n production
Or from a file:
echo "postgresql://username:password@timescale-host:5432/price_signal" > uri.txt
kubectl create secret generic timescale-cluster-app \
--from-file=uri=uri.txt \
-n production
rm uri.txt # Don't leave credentials on disk
Docker Registry Authentication
The deployment uses a Docker Hub image pull secret:
ImagePullSecrets = new InputList < LocalObjectReferenceArgs >
{
new LocalObjectReferenceArgs
{
Name = "dockerhub-f4806cff"
}
},
Create this secret:
kubectl create secret docker-registry dockerhub-f4806cff \
--docker-server=https://index.docker.io/v1/ \
--docker-username=your-dockerhub-username \
--docker-password=your-dockerhub-password \
[email protected] \
-n production
Customizing the Deployment
Change Domain Name
Update the ingress host in Program.cs:208:
Host = "your-domain.com" ,
And TLS configuration at Program.cs:200:
Hosts = new [] { "your-domain.com" },
SecretName = "your-domain-tls" ,
Adjust Replica Count
# Via Pulumi config
pulumi config set replicas 5
pulumi up
# Or directly with kubectl
kubectl scale deployment price-signal-graph --replicas=5 -n production
Change Namespace
pulumi config set namespace staging
pulumi up
Update Image Version
Modify Program.cs:101 to use a specific version:
Image = "nayth/price-signal-graph:v1.2.3" ,
Or use a config value:
Image = config . Get ( "imageTag" ) ?? "nayth/price-signal-graph:latest" ,
Managing Deployments
Update Deployment
# Pull latest code
cd infra/
# Preview changes
pulumi preview
# Apply updates
pulumi up
Rollback Changes
# View deployment history
pulumi history
# Rollback to previous version
pulumi stack export --version 5 > previous.json
pulumi stack import --file previous.json
pulumi up
Destroy Infrastructure
This will delete all resources managed by Pulumi. Make sure you have backups!
# Preview what will be deleted
pulumi destroy --preview
# Destroy resources
pulumi destroy
Monitoring Deployments
View Stack Outputs
Check Resource Status
# List all stack resources
pulumi stack --show-urns
# View specific resource
pulumi stack export | jq '.deployment.resources[] | select(.type=="kubernetes:apps/v1:Deployment")'
Kubernetes Status
# Get pod status
kubectl get pods -n production -l app=price-signal-graph
# View logs
kubectl logs -f -n production -l app=price-signal-graph
# Describe deployment
kubectl describe deployment price-signal-graph -n production
# Check ingress
kubectl get ingress -n production
kubectl describe ingress price-signal-graph-ingress -n production
Troubleshooting
Pulumi State Issues
# Refresh state from Kubernetes
pulumi refresh
# Force refresh (use with caution)
pulumi refresh --force
Image Pull Errors
# Check image pull secret
kubectl get secret dockerhub-f4806cff -n production
# Verify secret is correctly formatted
kubectl get secret dockerhub-f4806cff -n production -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
Certificate Issues
# Check cert-manager status
kubectl get certificates -n production
kubectl describe certificate price-signal-graph-tls -n production
# Check cert-manager logs
kubectl logs -n cert-manager -l app=cert-manager
Database Connection Issues
# Verify database secret exists
kubectl get secret timescale-cluster-app -n production
# Check secret content (base64 encoded)
kubectl get secret timescale-cluster-app -n production -o jsonpath='{.data.uri}' | base64 -d
# Test from pod
kubectl exec -it < pod-nam e > -n production -- cat /app/secrets/uri
Best Practices
Use separate stacks for different environments (dev, staging, prod)
Always run pulumi preview before pulumi up
Store Pulumi state remotely (Pulumi Cloud or S3) for team collaboration
Use encrypted secrets with pulumi config set --secret
Tag resources appropriately for cost tracking and organization
Test in dev/staging before deploying to production
Monitor deployments after applying changes
Keep infrastructure code in version control
Next Steps