Pulumi Infrastructure Troubleshooting Skill
Common Pulumi TypeScript Errors and Solutions
- "This expression is not callable. Type 'never' has no call signatures"
Cause: TypeScript infers a type as never when working with Pulumi Outputs, especially with arrays.
Solution: Wrap the value in pulumi.output() and properly type the callback:
// ❌ Bad - TypeScript can't infer the type value: pulumi.all(config.vpc.publicSubnets.map((s: any) => s.id))
// ✅ Good - Explicitly wrap and type value: pulumi.output(config.vpc.publicSubnets).apply((subnets: any[]) => pulumi.all(subnets.map((s: any) => s.id)).apply(ids => ids.join(",")) )
- "Modifiers cannot appear here" (export in conditional blocks)
Cause: TypeScript doesn't allow export statements inside if blocks.
Solution: Use optional chaining for conditional exports:
// ❌ Bad if (opensearch) { export const opensearchEndpoint = opensearch.endpoint; }
// ✅ Good export const opensearchEndpoint = opensearch?.endpoint;
- "Configuration key 'aws:region' is not namespaced by the project"
Cause: Pulumi.yaml config with namespaced keys (e.g., aws:region ) cannot use default attribute.
Solution: Remove the config section or don't set defaults for provider configs:
❌ Bad
config: aws:region: description: AWS region default: us-east-1
✅ Good - set via workflow/CLI instead
config: app:domainName: description: Domain name
- Stack Not Found Errors
Cause: Pulumi stack doesn't exist yet in new environments.
Solution: Use || operator to create if not exists:
pulumi stack select $STACK || pulumi stack init $STACK
- Working with Pulumi Outputs
Key Concepts:
-
pulumi.Output<T> is a promise-like wrapper for async values
-
Use .apply() to transform Output values
-
Use pulumi.all([...]) to combine multiple Outputs
-
Use pulumi.output(value) to wrap plain values as Outputs
Common Patterns:
// Transforming a single Output
const url = endpoint.apply(e => https://${e});
// Combining multiple Outputs
const connectionString = pulumi.all([host, port, db]).apply(
([h, p, d]) => postgres://${h}:${p}/${d}
);
// Interpolating Outputs
const message = pulumi.interpolateServer at ${endpoint}:${port};
Nested Outputs (Properties that are themselves Outputs):
// ❌ Bad - resource.property might be an Output<string> const endpoint = instance.apply(i => i.endpoint.split(":")[0]); // ERROR: Property 'split' does not exist
// ✅ Good - unwrap nested Output with pulumi.output() const endpoint = instance.apply(i => pulumi.output(i.endpoint).apply(e => e.split(":")[0]) );
// ✅ Alternative - use pulumi.all to flatten const endpoint = pulumi.all([instance]).apply(([inst]) => pulumi.output(inst.endpoint).apply(e => e.split(":")[0]) );
- Beanstalk Environment Variables
Issue: Complex objects or arrays need to be serialized.
Solution: Use JSON.stringify for complex values:
{ namespace: "aws:elasticbeanstalk:application:environment", name: "ALLOWED_ORIGINS", value: allowedOrigins.apply(origins => JSON.stringify(origins)), }
- ACM Certificate Validation
Issue: Certificate validation hangs or times out.
Solution: Ensure DNS records are created and wait for validation:
// 1. Create certificate const cert = new aws.acm.Certificate(...);
// 2. Create DNS validation record const validationRecord = new aws.route53.Record(..., { name: cert.domainValidationOptions[0].resourceRecordName, type: cert.domainValidationOptions[0].resourceRecordType, records: [cert.domainValidationOptions[0].resourceRecordValue], });
// 3. Wait for validation to complete const validation = new aws.acm.CertificateValidation(..., { certificateArn: cert.arn, validationRecordFqdns: [validationRecord.fqdn], });
- GitHub Actions Pulumi Setup
Best Practices:
-
name: Setup Pulumi uses: pulumi/actions@v5
-
name: Configure Stack run: | STACK="${{ inputs.stack || 'prod' }}" pulumi stack select $STACK || pulumi stack init $STACK pulumi config set aws:region ${{ env.AWS_REGION }}
Set other non-secret configs here
-
name: Pulumi Up run: pulumi up --yes --non-interactive env: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
- Debugging TypeScript Compilation
Quick checks:
-
Run npm run build in the infra package locally
-
Check for conditional exports inside blocks
-
Verify all Pulumi Outputs are properly typed
-
Look for .map() calls on potentially undefined arrays
-
Ensure all imports are correct
- Cost Optimization Tips
Beanstalk vs ECS Fargate:
-
Beanstalk with t3.micro: ~$32/month
-
ECS Fargate: ~$126/month
-
Key difference: Beanstalk runs on EC2 instances you control
-
Use public subnets to avoid NAT Gateway costs ($32/month)
Checklist Before Deploying
-
Run npm run build locally to catch TypeScript errors
-
Test with pulumi preview before pulumi up
-
Verify all secrets are in GitHub Secrets (not hardcoded)
-
Check stack name matches environment (dev/staging/prod)
-
Ensure domain/DNS is configured if using custom domains
-
Verify VPC/subnets exist if using existing infrastructure
-
Check that all required extensions/providers are installed
Common Environment Variables to Set
// Database
DATABASE_URL: pulumi.interpolatepostgres://${user}:${pass}@${host}:5432/${db}
// Redis
REDIS_URL: redisEndpoint.apply(e => redis://${e}:6379)
// S3 S3_BUCKET: bucketName S3_REGION: region
// Auth GITHUB_CLIENT_ID: clientId GITHUB_CLIENT_SECRET: clientSecret
// App Config NODE_ENV: "production" PORT: "8080" LOG_LEVEL: "info"
Resources
-
Pulumi TypeScript Docs
-
AWS Provider Docs
-
Pulumi Outputs Guide