# Architecture This guide explains how Pulumi Any Terraform works internally, how the bridge operates, and the technical design decisions behind the project. ## Overview [#overview] Pulumi Any Terraform is a **dynamic bridge** that automatically converts Terraform providers into native Pulumi providers. This is achieved through Pulumi's [`pulumi-terraform-bridge`](https://github.com/pulumi/pulumi-terraform-bridge), which translates Terraform's schema and resources into Pulumi's type system. ## How It Works [#how-it-works] ```mermaid graph TB A[Terraform Provider] --> B[Pulumi Bridge] B --> C[Generated TypeScript Types] B --> D[Resource Definitions] B --> E[Provider Configuration] C --> F[npm Package] D --> F E --> F F --> G[Your Pulumi Program] G --> H[Pulumi Engine] H --> I[Terraform Provider Runtime] I --> J[Target API/Service] ``` ### The Bridge Process [#the-bridge-process] 1. **Terraform Provider Schema**: Each Terraform provider defines its resources, data sources, and configuration in Go code with schemas. 2. **Bridge Configuration**: The bridge reads the provider's schema and generates corresponding Pulumi resources. 3. **Type Generation**: TypeScript type definitions are automatically generated, providing full IntelliSense and type safety. 4. **Resource Mapping**: Terraform resources are mapped to Pulumi custom resources with appropriate CRUD operations. 5. **Runtime Execution**: When you run `pulumi up`, the Pulumi engine invokes the bridged provider, which in turn calls the original Terraform provider. ## Project Structure [#project-structure] ``` pulumi-any-terraform/ ├── packages/ # Individual provider packages │ ├── better-uptime/ # Better Uptime provider │ │ ├── bin/ # Compiled output │ │ ├── index.ts # Main exports │ │ ├── types/ # Type definitions │ │ ├── config/ # Provider configuration │ │ ├── package.json # Package metadata │ │ └── README.md # Provider documentation │ ├── namecheap/ # Namecheap provider │ └── ... # Other providers ├── tools/ # Build system plugins │ ├── build.ts # Build orchestration │ ├── linter.ts # Linting plugin │ ├── prettier.ts # Code formatting │ └── syncpack.ts # Dependency sync ├── docs/ # Documentation site (Next.js) ├── .github/ # CI/CD workflows │ └── workflows/ │ ├── publish.yml # Package publishing │ ├── test.yml # Testing & linting │ └── update.yml # Dependency updates ├── nx.json # Nx workspace config ├── pnpm-workspace.yaml # PNPM workspace └── package.json # Root configuration ``` ## Provider Package Structure [#provider-package-structure] Each provider package follows a consistent structure: ```typescript // Generated index.ts import * as pulumi from "@pulumi/pulumi"; import * as utilities from "./utilities"; // Provider configuration export class Provider extends pulumi.ProviderResource { // ... } // Resources export class SomeResource extends pulumi.CustomResource { // Properties with full type definitions public readonly someProperty!: pulumi.Output; // Constructor constructor(name: string, args: SomeResourceArgs, opts?: pulumi.CustomResourceOptions) { // ... } } // Type definitions export interface SomeResourceArgs { someProperty: pulumi.Input; // ... } ``` ### Key Components [#key-components] #### 1. Provider Resource [#1-provider-resource] Each package exports a `Provider` resource that configures the Terraform provider: ```typescript const provider = new Provider("my-provider", { apiKey: config.requireSecret("apiKey"), endpoint: "https://api.example.com", }); ``` #### 2. Custom Resources [#2-custom-resources] Resources are wrapped as Pulumi `CustomResource` classes with: * Full TypeScript types * Input/output properties * Proper lifecycle management * State tracking #### 3. Type Definitions [#3-type-definitions] All inputs and outputs are strongly typed: ```typescript export interface MonitorArgs { url: pulumi.Input; checkFrequency?: pulumi.Input; monitorType: pulumi.Input<"status" | "ping" | "keyword">; } export interface MonitorState { id: pulumi.Output; createdAt: pulumi.Output; status: pulumi.Output; } ``` ## Parameterization [#parameterization] Each provider uses **parameterization** to specify which Terraform provider to bridge. This is stored as a base64-encoded value in `package.json`: ```json { "pulumi": { "resource": true, "name": "terraform-provider", "version": "0.14.0", "parameterization": { "name": "namecheap", "version": "2.2.0", "value": "eyJyZW1vdGUiOnsidXJsIjoicmVnaXN0cnkub3BlbnRvZnUub3JnL25hbWVjaGVhcC9uYW1lY2hlYXAiLCJ2ZXJzaW9uIjoiMi4yLjAifX0=" } } } ``` Decoded, this specifies: ```json { "remote": { "url": "registry.opentofu.org/namecheap/namecheap", "version": "2.2.0" } } ``` This tells Pulumi: * Which Terraform provider to use (`namecheap/namecheap`) * Which version to bridge (`2.2.0`) * Where to download it from (`registry.opentofu.org`) ## Build System [#build-system] The project uses **Nx** for monorepo management and build orchestration: ### Nx Workspace [#nx-workspace] ```json { "targetDefaults": { "build": { "outputs": ["{projectRoot}/bin"], "dependsOn": ["^build"], "cache": true } }, "plugins": [ "@nx/js/typescript", "./tools/audit.ts", "./tools/build.ts", "./tools/linter.ts", "./tools/prettier.ts", "./tools/syncpack.ts" ] } ``` ### Build Plugins [#build-plugins] Custom Nx plugins in `/tools/` provide: 1. **Build Plugin** (`build.ts`): TypeScript compilation orchestration 2. **Linter Plugin** (`linter.ts`): Code quality checks 3. **Prettier Plugin** (`prettier.ts`): Code formatting 4. **Syncpack Plugin** (`syncpack.ts`): Dependency synchronization 5. **Audit Plugin** (`audit.ts`): Security vulnerability scanning ### Caching [#caching] Nx provides smart caching to speed up builds: * **Local cache**: Stores build outputs locally * **Computation caching**: Skips unnecessary rebuilds ## CI/CD Pipeline [#cicd-pipeline] ### Workflow Architecture [#workflow-architecture] ```mermaid graph LR A[Push to main] --> B[Test Workflow] B --> C[Lint & Build] C --> D[Autofix.ci] D --> E[Update Workflow] E --> F[Check Updates] F --> G[Publish Workflow] G --> H[Release Packages] H --> I[NPM Registry] ``` ### Key Workflows [#key-workflows] #### 1. Test Workflow (`test.yml`) [#1-test-workflow-testyml] * Runs on every push and PR * Lints code with Biome * Builds all packages * Runs type checking * Uses Nx affected commands for efficiency #### 2. Update Workflow (`update.yml`) [#2-update-workflow-updateyml] * Runs daily or on-demand * Checks for dependency updates * Creates automated PRs * Uses Renovate for dependency management #### 3. Publish Workflow (`publish.yml`) [#3-publish-workflow-publishyml] * Runs on main branch after successful tests * Uses Changesets for version management * Publishes packages to NPM * Creates release notes automatically ## External Integrations [#external-integrations] ### 1. Terraform Registry [#1-terraform-registry] * **Purpose**: Source for Terraform providers * **Usage**: Downloads provider schemas and binaries * **URL**: `registry.terraform.io` and `registry.opentofu.org` ### 2. NPM Registry [#2-npm-registry] * **Purpose**: Distribution of Pulumi packages * **Usage**: Publishing and installing packages * **Packages**: All under `pulumi-*` namespace ### 3. GitHub [#3-github] * **Purpose**: Source control and CI/CD * **Features**: * Issue tracking * Pull requests * GitHub Actions * Package registry ### 4. Aikido Safe Chain [#4-aikido-safe-chain] * **Purpose**: Dependency security scanning * **Usage**: Blocks malicious packages during install * **Integration**: GitHub Actions ### 5. Autofix.ci [#5-autofixci] * **Purpose**: Automated code fixes * **Usage**: Auto-fixes linting and formatting issues * **Integration**: Automatic PR commits ## State Management [#state-management] Pulumi manages state differently from Terraform: ### Pulumi State [#pulumi-state] * Stored in Pulumi Service (default) or self-managed backend * Contains resource metadata and outputs * Supports encryption and access control * Enables collaboration and history tracking ### Bridge State Translation [#bridge-state-translation] The bridge translates between Pulumi and Terraform state: 1. Pulumi tracks resources in its own state 2. Bridge invokes Terraform provider with translated inputs 3. Provider returns outputs 4. Bridge translates back to Pulumi format 5. Pulumi updates its state ## Type Safety [#type-safety] One of the main benefits of this bridge is **complete type safety**: ### Input Types [#input-types] ```typescript interface MonitorArgs { url: pulumi.Input; // Required string input checkFrequency?: pulumi.Input; // Optional number input ssl?: pulumi.Input<{ // Nested object checkExpiry: boolean; expiryThreshold: number; }>; } ``` ### Output Types [#output-types] ```typescript interface MonitorOutputs { id: pulumi.Output; // Output string createdAt: pulumi.Output; // Output timestamp status: pulumi.Output<"up" | "down">; // Output enum } ``` ### Type Inference [#type-inference] ```typescript const monitor = new betteruptime.Monitor("api", { url: "https://api.example.com", checkFrequency: 60, }); // TypeScript knows these are Output export const monitorId = monitor.id; export const monitorStatus = monitor.status; ``` ## Resource Lifecycle [#resource-lifecycle] Resources follow Pulumi's standard lifecycle: 1. **Create**: When resource doesn't exist * Bridge calls Terraform's Create method * Returns resource ID and properties 2. **Read**: To refresh state * Bridge calls Terraform's Read method * Updates Pulumi state with current values 3. **Update**: When properties change * Bridge calls Terraform's Update method * Handles partial updates if supported 4. **Delete**: When resource is removed * Bridge calls Terraform's Delete method * Removes from Pulumi state ## Performance Considerations [#performance-considerations] ### Build Performance [#build-performance] * **Parallel compilation**: Multiple packages build simultaneously * **Incremental builds**: Only rebuild changed packages * **Caching**: Skip unchanged builds entirely * **Nx affected**: Only process affected packages ### Runtime Performance [#runtime-performance] * **Lazy loading**: Resources loaded on-demand * **Concurrent operations**: Multiple resources created in parallel * **State optimization**: Minimal state queries * **Provider reuse**: Single provider instance per program ## Security [#security] ### Dependency Security [#dependency-security] * **Aikido Safe Chain**: Scans npm packages during install * **Dependabot/Renovate**: Automated security updates * **Audit checks**: Regular security audits ### Secrets Management [#secrets-management] * **Pulumi secrets**: Encrypted at rest and in transit * **Environment variables**: For local development * **Config encryption**: Sensitive config encrypted in state ### Package Publishing [#package-publishing] * **NPM 2FA**: Required for publishing * **Provenance**: Supply chain security * **Automated publishing**: Reduces human error ## Debugging [#debugging] ### Enable Debug Logging [#enable-debug-logging] ```bash # Pulumi debug logs export PULUMI_DEBUG_COMMANDS=true export PULUMI_DEBUG_PROMISE_LEAKS=true # Terraform provider logs export TF_LOG=DEBUG export TF_LOG_PATH=./terraform.log ``` ### Inspect Bridge Behavior [#inspect-bridge-behavior] ```typescript import * as pulumi from "@pulumi/pulumi"; // Log all inputs pulumi.log.info(`Creating resource with: ${JSON.stringify(args)}`); // Log outputs resource.id.apply(id => pulumi.log.info(`Created with ID: ${id}`)); ``` ## Future Enhancements [#future-enhancements] Potential improvements to the architecture: 1. **Dynamic provider generation**: Generate providers on-demand 2. **Better error messages**: More helpful error context 3. **Performance optimization**: Faster bridge overhead 4. **Multi-version support**: Support multiple Terraform versions 5. **Enhanced types**: More precise TypeScript types # CI/CD Pipeline This document explains the CI/CD architecture, workflows, and automation used in the Pulumi Any Terraform project. ## Overview [#overview] The project uses **GitHub Actions** for continuous integration and deployment, with three main workflows: 1. **Test Workflow** - Automated quality checks 2. **Update Workflow** - Dependency management 3. **Publish Workflow** - Package publishing ## Workflow Architecture [#workflow-architecture] ```mermaid graph LR A[Push/PR] --> B[Test Workflow] B --> C{Tests Pass?} C -->|Yes| D[Autofix.ci] C -->|No| E[Fail] D --> F[Merge to main] F --> G[Update Workflow] G --> H[Check Dependencies] H --> I{Updates?} I -->|Yes| J[Create PR] I -->|No| K[Publish Workflow] J --> B K --> L[Release Packages] L --> M[NPM Registry] ``` ## Test Workflow [#test-workflow] **Trigger**: Every push and pull request\ **File**: `.github/workflows/test.yml` ### Jobs [#jobs] #### 1. Fix Job [#1-fix-job] Automatically fixes formatting and linting issues: ```yaml jobs: fix: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: jdx/mise-action@v3 - run: pnpm install - run: pnpm nx affected -t fix - uses: autofix-ci/action@v1 ``` **What it does**: * Installs dependencies with pnpm * Runs formatters (Prettier) on affected packages * Runs linters (Biome) on affected packages * Auto-commits fixes via Autofix.ci #### 2. Lint & Build Job [#2-lint--build-job] Validates code quality and builds packages: ```yaml jobs: lint: runs-on: ubuntu-latest needs: [fix] steps: - uses: actions/checkout@v5 - uses: jdx/mise-action@v3 - run: pnpm install - run: pnpm nx affected -t check ``` **What it does**: * Type checks TypeScript code * Lints with Biome * Builds all affected packages * Validates package.json files * Checks dependency versions with Syncpack ### Performance Optimizations [#performance-optimizations] * **Nx Affected**: Only processes changed packages * **GitHub Actions Cache**: pnpm store cached across runs on the lockfile hash * **Concurrent Execution**: Multiple jobs run in parallel ## Update Workflow [#update-workflow] **Trigger**: Daily at 00:00 UTC or manual dispatch\ **File**: `.github/workflows/update.yml` ### Purpose [#purpose] Keeps dependencies up to date automatically: ```yaml jobs: update: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: node .github/scripts/check-updates.js - run: pnpm install - run: pnpm nx run-many -t fix - name: Create Pull Request # ... ``` **What it does**: * Checks for dependency updates * Updates package.json files * Runs tests and fixes * Creates automated PR with changes ### Update Strategy [#update-strategy] * **Patch versions**: Auto-merge after tests pass * **Minor versions**: Create PR for review * **Major versions**: Create PR with breaking change notice ## Publish Workflow [#publish-workflow] **Trigger**: Push to main branch (after tests pass)\ **File**: `.github/workflows/publish.yml` ### Jobs [#jobs-1] #### NPM Package Publishing [#npm-package-publishing] Publishes packages to NPM registry: ```yaml jobs: npm-packages: runs-on: ubuntu-latest permissions: id-token: write contents: write pull-requests: write steps: - uses: actions/checkout@v5 - uses: jdx/mise-action@v3 - run: pnpm install - uses: changesets/action@v1 with: publish: pnpm release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ``` **What it does**: 1. Checks for changesets 2. Versions packages based on changesets 3. Builds all packages 4. Publishes to NPM registry 5. Creates Git tags 6. Generates release notes 7. Creates GitHub release ### Version Management [#version-management] Uses [Changesets](https://github.com/changesets/changesets): ```bash # Developer creates changeset pnpm changeset # CI reads changesets and versions packages # CI publishes to NPM # CI creates GitHub release ``` ## Security Measures [#security-measures] ### 1. Aikido Safe Chain [#1-aikido-safe-chain] Protects against supply chain attacks: ```yaml - name: Setup Aikido Safe Chain run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci ``` **Features**: * Blocks malicious packages * Detects supply chain attacks * Monitors network activity * Validates package integrity ### 2. Dependency Scanning [#2-dependency-scanning] * **Dependabot**: Automated security updates * **npm audit**: Vulnerability scanning * **Syncpack**: Version consistency checks ### 3. Secret Management [#3-secret-management] ```yaml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ``` **Secrets used**: * `GITHUB_TOKEN`: Repository access * `NPM_TOKEN`: Package publishing ## Caching Strategy [#caching-strategy] ### Nx Local Cache [#nx-local-cache] Nx caches task outputs locally and skips tasks whose inputs are unchanged. There is **no remote or shared cache** -- build correctness comes from Nx's input-hashing, and the machine-bound `.nx/cache` database is not restored across CI runs. **Benefits**: * Faster local rebuilds * Skips unchanged tasks ### GitHub Actions Cache [#github-actions-cache] Caches the pnpm store across runs: ```yaml - uses: actions/cache@v3 with: path: ~/.pnpm-store key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} ``` ## Automated Code Fixes [#automated-code-fixes] ### Autofix.ci Integration [#autofixci-integration] Automatically fixes and commits: ```yaml - uses: autofix-ci/action@v1 ``` **Fixes**: * Code formatting (Prettier) * Linting issues (Biome) * Package.json formatting * Import sorting ### Manual Overrides [#manual-overrides] To skip autofix on a commit: ```bash git commit -m "feat: new feature [skip autofix]" ``` ## Monitoring & Notifications [#monitoring--notifications] ### GitHub Status Checks [#github-status-checks] * ✅ Tests pass * ✅ Build succeeds * ✅ No linting errors * ✅ Dependencies secure ### Notifications [#notifications] * **PR comments**: Test results * **Slack**: (if configured) Build notifications * **Email**: Workflow failures ## Local Development Workflow [#local-development-workflow] Simulate CI locally: ```bash # Run affected checks pnpm nx affected -t check # Run affected builds pnpm nx affected -t build # Run affected fixes pnpm nx affected -t fix # Check all packages pnpm nx run-many -t check ``` ## CI Configuration Files [#ci-configuration-files] ### mise.toml [#misetoml] Defines tool versions: ```toml [tools] node = "22.18.0" pnpm = "10.14.0" ``` ### nx.json [#nxjson] Build orchestration: ```json { "targetDefaults": { "build": { "dependsOn": ["^build"], "cache": true } } } ``` ### .syncpackrc.json [#syncpackrcjson] Dependency management: ```json { "versionGroups": [ { "dependencies": ["@pulumi/pulumi"], "policy": "sameRange" } ] } ``` ## Troubleshooting CI Issues [#troubleshooting-ci-issues] ### Build Failures [#build-failures] 1. Check workflow logs in GitHub Actions 2. Reproduce locally: `pnpm nx affected -t build` 3. Clear caches: `pnpm nx reset` 4. Verify Node.js and pnpm versions ### Test Failures [#test-failures] 1. Run tests locally: `pnpm nx affected -t check` 2. Check for flaky tests 3. Verify dependencies are installed 4. Review error messages in logs ### Publishing Failures [#publishing-failures] 1. Check NPM token validity 2. Verify package versions 3. Ensure changesets exist 4. Review publish logs ### Cache Issues [#cache-issues] 1. Clear Nx cache: `pnpm nx reset` 2. Clear GitHub Actions cache (in repository settings) ## Best Practices [#best-practices] ### 1. Use Changesets [#1-use-changesets] Always create changesets for changes: ```bash pnpm changeset ``` ### 2. Fix Before Commit [#2-fix-before-commit] Run formatters and linters: ```bash pnpm nx affected -t fix ``` ### 3. Test Locally [#3-test-locally] Before pushing: ```bash pnpm nx affected -t check pnpm nx affected -t build ``` ### 4. Keep Workflows Updated [#4-keep-workflows-updated] Regularly update GitHub Actions: ```yaml - uses: actions/checkout@v5 # Use latest ``` ### 5. Monitor Build Times [#5-monitor-build-times] Review per-job timings in the GitHub Actions run summary. ## Metrics & Analytics [#metrics--analytics] ### Build Performance [#build-performance] * Average build time * Cache hit rate * Test execution time * Package size ### Deployment Frequency [#deployment-frequency] * Commits per day * PRs merged per week * Releases per month * Update frequency ## Future Enhancements [#future-enhancements] Planned improvements: 1. **E2E Testing**: Add end-to-end tests 2. **Visual Regression**: Screenshot comparisons 3. **Performance Testing**: Benchmark tests 4. **Documentation Testing**: Link validation 5. **Security Scanning**: Advanced vulnerability detection # Contributing Thank you for your interest in contributing to Pulumi Any Terraform! This guide will help you get started with development, testing, and submitting contributions. ## Development Setup [#development-setup] ### Prerequisites [#prerequisites] * **Node.js** 22.18.0 or later * **pnpm** 10.14.0 or later * **Git** for version control * A GitHub account ### Setting Up Your Environment [#setting-up-your-environment] 1. **Fork and clone the repository**: ```bash git clone https://github.com/YOUR_USERNAME/pulumi-any-terraform.git cd pulumi-any-terraform ``` 2. **Install dependencies**: ```bash pnpm install ``` 3. **Verify your setup**: ```bash pnpm run syncpack:check ``` ## Project Structure [#project-structure] ``` pulumi-any-terraform/ ├── packages/ # Provider packages │ ├── namecheap/ # Individual provider │ │ ├── bin/ # Compiled output (generated) │ │ ├── types/ # TypeScript types (generated) │ │ ├── package.json # Package metadata │ │ └── README.md # Provider docs │ └── ... ├── tools/ # Build system plugins │ ├── build.ts # Build orchestration │ ├── linter.ts # Code linting │ └── prettier.ts # Code formatting ├── docs/ # Documentation site │ ├── content/ # MDX documentation │ └── src/ # Next.js app ├── .github/ # CI/CD workflows └── nx.json # Nx configuration ``` ## Adding a New Provider [#adding-a-new-provider] ### 1. Create Package Directory [#1-create-package-directory] ```bash mkdir -p packages/my-provider cd packages/my-provider ``` ### 2. Create package.json [#2-create-packagejson] ```json { "name": "pulumi-my-provider", "description": "A Pulumi provider for [Service Name]", "version": "0.1.0", "homepage": "https://github.com/hckhanh/pulumi-any-terraform", "repository": { "type": "git", "url": "git+https://github.com/hckhanh/pulumi-any-terraform.git", "directory": "packages/my-provider" }, "private": false, "sideEffects": false, "main": "./bin/index.js", "module": "./bin/index.js", "types": "./bin/index.d.ts", "exports": { ".": "./bin/index.js", "./package.json": "./package.json" }, "dependencies": { "async-mutex": "0.5.0" }, "devDependencies": { "@pulumi/pulumi": "3.205.0", "@types/node": "24.9.2", "typescript": "5.9.3" }, "peerDependencies": { "@pulumi/pulumi": ">=3.190.0 <4" }, "files": [ "bin", "README.md" ], "keywords": [ "pulumi", "terraform", "provider", "my-provider" ], "license": "MIT", "publishConfig": { "access": "public" }, "pulumi": { "resource": true, "name": "terraform-provider", "version": "0.14.0", "parameterization": { "name": "my-provider", "version": "1.0.0", "value": "BASE64_ENCODED_VALUE_HERE" } } } ``` ### 3. Generate Parameterization Value [#3-generate-parameterization-value] The `parameterization.value` field must be a base64-encoded JSON: ```bash # Create the JSON echo '{"remote":{"url":"registry.opentofu.org/org/provider","version":"1.0.0"}}' | base64 # Add to package.json ``` ### 4. Create README.md [#4-create-readmemd] Create comprehensive documentation for your provider (see existing providers for examples). ### 5. Add TypeScript Configuration [#5-add-typescript-configuration] Create `tsconfig.json`: ```json { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./bin", "rootDir": "." }, "include": ["**/*.ts"], "exclude": ["node_modules", "bin"] } ``` ### 6. Test Your Provider [#6-test-your-provider] ```bash # Build the provider pnpm nx run my-provider:build # Check for issues pnpm nx run my-provider:check ``` ## Development Workflow [#development-workflow] ### Making Changes [#making-changes] 1. **Create a feature branch**: ```bash git checkout -b feature/your-feature-name ``` 2. **Make your changes** following the coding standards 3. **Format your code**: ```bash pnpm run prettier:write ``` 4. **Check for issues**: ```bash pnpm nx affected -t check ``` 5. **Build affected packages**: ```bash pnpm nx affected -t build ``` ### Running Quality Checks [#running-quality-checks] ```bash # Check all packages pnpm nx run-many -t check # Fix formatting issues pnpm nx run-many -t fix # Check dependency versions pnpm run syncpack:check # Fix dependency versions pnpm run syncpack:fix ``` ## Coding Standards [#coding-standards] ### TypeScript Best Practices [#typescript-best-practices] * Use `import type` for type-only imports * Avoid using `any` - prefer proper typing * Use `const` for variables that are never reassigned * Prefer arrow functions for consistency * Use template literals over string concatenation ### File Organization [#file-organization] * Keep generated files in `bin/` directory * Store type definitions in `types/` directory * Place documentation in provider README.md ### Naming Conventions [#naming-conventions] * Use kebab-case for file names: `my-component.ts` * Use PascalCase for classes: `MyResource` * Use camelCase for variables and functions: `myVariable` ## Testing [#testing] ### Manual Testing [#manual-testing] Test your provider with a real Pulumi program: ```typescript // test-program.ts import * as pulumi from "@pulumi/pulumi"; import * as myprovider from "pulumi-my-provider"; const resource = new myprovider.SomeResource("test", { // Resource properties }); export const resourceId = resource.id; ``` Run the test: ```bash pulumi preview pulumi up ``` ### Integration Testing [#integration-testing] Test with actual provider APIs: 1. Set up required credentials 2. Deploy test resources 3. Verify resource creation 4. Clean up resources ## Documentation [#documentation] ### Provider Documentation [#provider-documentation] Each provider needs comprehensive documentation: 1. **Installation instructions** 2. **Configuration guide** 3. **Quick start examples** 4. **Common use cases** 5. **Resource reference** 6. **Troubleshooting guide** ### Documentation Site [#documentation-site] Add provider documentation to the docs site: ```bash # Create provider docs docs/content/docs/providers/my-provider.mdx ``` Update navigation in `docs/content/docs/meta.json`. ## Submitting Changes [#submitting-changes] ### Creating a Pull Request [#creating-a-pull-request] 1. **Push your branch**: ```bash git push origin feature/your-feature-name ``` 2. **Open a Pull Request** on GitHub 3. **Fill out the PR template** with: * Description of changes * Related issue number * Testing performed * Breaking changes (if any) ### PR Review Process [#pr-review-process] 1. Automated checks run (linting, building, testing) 2. Maintainers review your code 3. Address any feedback 4. Once approved, your PR will be merged ### After Merge [#after-merge] * Your changes are automatically deployed * Packages are published to npm * Documentation is updated ## Release Process [#release-process] ### Version Management [#version-management] We use [Changesets](https://github.com/changesets/changesets) for version management: 1. **Create a changeset**: ```bash pnpm changeset ``` 2. **Select packages** to version 3. **Choose version bump** (patch, minor, major) 4. **Describe changes** 5. **Commit the changeset**: ```bash git add .changeset/ git commit -m "chore: add changeset" ``` ### Publishing [#publishing] Publishing happens automatically when: 1. PR is merged to main 2. Changesets exist 3. GitHub Actions runs the publish workflow 4. Packages are published to npm ## Getting Help [#getting-help] ### Resources [#resources] * **GitHub Issues**: Report bugs and request features * **Pull Requests**: Browse existing contributions * **Documentation**: Read the full documentation site * **Pulumi Slack**: Join the community discussions ### Common Questions [#common-questions] **Q: How do I find the right Terraform provider version?** A: Check the [Terraform Registry](https://registry.terraform.io/) for the latest version. **Q: My build is failing. What should I do?** A: Run `pnpm nx reset` to clear caches, then try again. **Q: How do I test provider changes locally?** A: Use `pnpm link` to link your local package to a test Pulumi program. **Q: Can I add support for a private Terraform provider?** A: Yes, but you'll need to host it in a private registry and update the parameterization URL. ## Code of Conduct [#code-of-conduct] We follow a Code of Conduct to ensure a welcoming environment: * Be respectful and inclusive * Provide constructive feedback * Focus on what is best for the community * Show empathy towards other community members ## License [#license] By contributing, you agree that your contributions will be licensed under the MIT License. ## Thank You! [#thank-you] Your contributions make this project better for everyone. We appreciate your time and effort! # FAQ Common questions and answers about Pulumi Any Terraform providers. ## General Questions [#general-questions] ### What is Pulumi Any Terraform? [#what-is-pulumi-any-terraform] Pulumi Any Terraform is a collection of dynamically bridged Pulumi providers that enable you to use Terraform providers within the Pulumi ecosystem. It automatically converts Terraform providers into fully-typed Pulumi packages with complete TypeScript support. ### How is this different from using Terraform directly? [#how-is-this-different-from-using-terraform-directly] While Terraform uses HCL (HashiCorp Configuration Language), Pulumi lets you use real programming languages like TypeScript, Python, Go, and C#. This means: * Full IDE support with IntelliSense * Type safety and compile-time checks * Real programming constructs (loops, conditionals, functions) * Better code reusability and modularity * Native integration with existing codebases ### How is this different from regular Pulumi providers? [#how-is-this-different-from-regular-pulumi-providers] Regular Pulumi providers are built specifically for Pulumi. These bridged providers use Pulumi's bridge to automatically convert existing Terraform providers, giving you access to the vast Terraform ecosystem within Pulumi. ### Is this production-ready? [#is-this-production-ready] Yes! The bridge technology is mature and used by many Pulumi users in production. However, the stability of each provider depends on the underlying Terraform provider quality. ## Installation & Setup [#installation--setup] ### Which package manager should I use? [#which-package-manager-should-i-use] Any of npm, yarn, or pnpm will work. The project itself uses pnpm for development, but you can use whichever you prefer: ```bash npm install pulumi-namecheap # or yarn add pulumi-namecheap # or pnpm add pulumi-namecheap ``` ### Do I need to install Terraform? [#do-i-need-to-install-terraform] No! The Terraform provider runtime is embedded in each Pulumi package. You only need Pulumi CLI and Node.js. ### Can I use these providers with other Pulumi providers? [#can-i-use-these-providers-with-other-pulumi-providers] Absolutely! You can mix and match any Pulumi providers in the same program: ```typescript import * as aws from "@pulumi/aws"; import * as namecheap from "pulumi-namecheap"; const instance = new aws.ec2.Instance(/* ... */); const record = new namecheap.Record(/* ... */); ``` ### What versions of Pulumi are supported? [#what-versions-of-pulumi-are-supported] These providers require Pulumi 3.190.0 or later. We recommend using the latest stable version. ### What Node.js version do I need? [#what-nodejs-version-do-i-need] Node.js 18.0 or later is recommended. The project is developed with Node.js 22.18.0. ## Usage Questions [#usage-questions] ### How do I find provider documentation? [#how-do-i-find-provider-documentation] 1. Check this documentation site for provider guides 2. Refer to the original Terraform provider documentation 3. Use TypeScript IntelliSense in your IDE 4. Check the provider's README in the package ### Can I import existing resources? [#can-i-import-existing-resources] Yes! Use Pulumi's import command: ```bash pulumi import namecheap:index:Record www domain.com/www/A ``` ### How do I handle secrets? [#how-do-i-handle-secrets] Use Pulumi's built-in secret management: ```bash pulumi config set provider:apiKey value --secret ``` Or in code: ```typescript import * as pulumi from "@pulumi/pulumi"; const config = new pulumi.Config(); const apiKey = config.requireSecret("apiKey"); ``` ### Can I use these providers in multiple languages? [#can-i-use-these-providers-in-multiple-languages] Currently, these packages are published for TypeScript/JavaScript. Support for Python, Go, and C# could be added in the future. ### How do I update a provider? [#how-do-i-update-a-provider] Update the package version: ```bash npm update pulumi-namecheap ``` Then run your program: ```bash pulumi preview pulumi up ``` ## Technical Questions [#technical-questions] ### How does the bridge work? [#how-does-the-bridge-work] The bridge uses Pulumi's `pulumi-terraform-bridge` to: 1. Read the Terraform provider's schema 2. Generate Pulumi resource definitions 3. Create TypeScript type definitions 4. Map Terraform operations to Pulumi lifecycle See the [Architecture](/docs/architecture) guide for details. ### What happens to my Terraform state? [#what-happens-to-my-terraform-state] You don't use Terraform state files. Pulumi manages its own state, which can be: * Stored in Pulumi Cloud (default) * Self-hosted in S3, Azure Blob, GCS, or local files ### Can I migrate from Terraform to these providers? [#can-i-migrate-from-terraform-to-these-providers] Yes, but you'll need to: 1. Convert HCL to TypeScript 2. Import existing resources into Pulumi state 3. Test thoroughly before production ### Are there any limitations? [#are-there-any-limitations] Some Terraform features don't translate perfectly: * Terraform modules don't exist (use TypeScript functions instead) * Some Terraform-specific syntax doesn't apply * Provider-specific quirks may exist ### How are resources named? [#how-are-resources-named] Resources use Pulumi's naming: * Resource name: Logical name in your code * Physical name: Auto-generated or explicit * URN: Unique Pulumi identifier ### Can I use Terraform providers not in this collection? [#can-i-use-terraform-providers-not-in-this-collection] Yes! You can create your own bridged provider by: 1. Creating a new package structure 2. Configuring the parameterization 3. Following the [Contributing Guide](/docs/contributing) ## Performance Questions [#performance-questions] ### Are these providers slower than native Pulumi? [#are-these-providers-slower-than-native-pulumi] The bridge adds minimal overhead. Most time is spent in: * API calls to target services * Network latency * Resource creation/update time ### How can I speed up deployments? [#how-can-i-speed-up-deployments] 1. Use `--parallel` flag for concurrent operations 2. Split large stacks into smaller ones 3. Use stack references for dependencies 4. Enable Pulumi state caching ### Do these providers support preview/dry-run? [#do-these-providers-support-previewdry-run] Yes! `pulumi preview` shows what will change before applying. ## Troubleshooting Questions [#troubleshooting-questions] ### My provider isn't working. What do I check? [#my-provider-isnt-working-what-do-i-check] 1. Verify credentials are configured correctly 2. Check API access and IP whitelists 3. Enable debug logging: `pulumi up -v=9` 4. Review the [Troubleshooting Guide](/docs/troubleshooting) ### I'm getting type errors. How do I fix them? [#im-getting-type-errors-how-do-i-fix-them] 1. Ensure TypeScript version is compatible 2. Regenerate type definitions if needed 3. Check for type mismatches in your code 4. Use `pulumi.Input` for inputs ### How do I debug provider issues? [#how-do-i-debug-provider-issues] Enable debug logging: ```bash export PULUMI_DEBUG_COMMANDS=true export TF_LOG=DEBUG pulumi up -v=9 ``` ### Resources aren't updating. Why? [#resources-arent-updating-why] 1. Run `pulumi refresh` to sync state 2. Check for manual changes outside Pulumi 3. Verify update is allowed by provider 4. Review resource protection settings ## Development Questions [#development-questions] ### How do I contribute a new provider? [#how-do-i-contribute-a-new-provider] Follow the [Contributing Guide](/docs/contributing): 1. Fork the repository 2. Create a new package 3. Configure parameterization 4. Submit a pull request ### Can I modify generated code? [#can-i-modify-generated-code] No! Generated code is overwritten on rebuild. Instead: * Use wrapper functions * Create helper modules * Extend with TypeScript classes ### How do I test my changes? [#how-do-i-test-my-changes] 1. Build the provider locally 2. Link to a test program: `pnpm link` 3. Run `pulumi preview` and `pulumi up` 4. Verify resources in provider dashboard ### How do I report bugs? [#how-do-i-report-bugs] 1. Check [existing issues](https://github.com/hckhanh/pulumi-any-terraform/issues) 2. Create a new issue with: * Provider and version * Error message * Minimal reproduction code * Environment details ## Licensing Questions [#licensing-questions] ### What license is this project under? [#what-license-is-this-project-under] MIT License. See the LICENSE file for details. ### Can I use this commercially? [#can-i-use-this-commercially] Yes! MIT license allows commercial use. ### Are there any restrictions? [#are-there-any-restrictions] The MIT license requires: * Include copyright notice * Include license text ### What about the Terraform providers? [#what-about-the-terraform-providers] Each Terraform provider has its own license. Check the provider's repository for details. ## Cost Questions [#cost-questions] ### Are these providers free? [#are-these-providers-free] The Pulumi packages are free and open source. However: * Target services may have costs (API usage, resources) * Pulumi Cloud has free and paid tiers * Consider resource costs in your planning ### Do I need a Pulumi subscription? [#do-i-need-a-pulumi-subscription] No for basic usage. Pulumi offers: * **Free tier**: Individual developers * **Team tier**: Collaboration features * **Enterprise tier**: Advanced capabilities You can also self-host Pulumi state. ## Comparison Questions [#comparison-questions] ### Pulumi Any Terraform vs Terraformer? [#pulumi-any-terraform-vs-terraformer] Terraformer converts cloud resources to Terraform code. This project lets you use Terraform providers with Pulumi. ### Pulumi Any Terraform vs CDK for Terraform? [#pulumi-any-terraform-vs-cdk-for-terraform] CDK for Terraform generates Terraform JSON. This project creates native Pulumi resources with the full Pulumi experience. ### Pulumi Any Terraform vs native Pulumi providers? [#pulumi-any-terraform-vs-native-pulumi-providers] Native providers are built specifically for Pulumi and may have: * Better error messages * Pulumi-specific features * Tighter integration Bridged providers give you: * Access to any Terraform provider * Wider ecosystem * Regular updates from Terraform community ## Future Questions [#future-questions] ### Will more providers be added? [#will-more-providers-be-added] Yes! Providers are added based on: * Community demand * Provider popularity * Maintenance requirements ### Will Python/Go/C# support be added? [#will-pythongoc-support-be-added] Potentially! This would require: * Multi-language code generation * Additional testing * Maintenance overhead ### Can I request a new feature? [#can-i-request-a-new-feature] Yes! [Open an issue](https://github.com/hckhanh/pulumi-any-terraform/issues) with: * Feature description * Use case * Benefits ## Getting More Help [#getting-more-help] ### Where can I get help? [#where-can-i-get-help] * [GitHub Issues](https://github.com/hckhanh/pulumi-any-terraform/issues) * [Pulumi Community Slack](https://slack.pulumi.com/) * [Troubleshooting Guide](/docs/troubleshooting) * Provider-specific documentation ### How do I stay updated? [#how-do-i-stay-updated] * Watch the [GitHub repository](https://github.com/hckhanh/pulumi-any-terraform) * Follow [Pulumi blog](https://www.pulumi.com/blog/) * Check provider release notes ### Can I contribute to documentation? [#can-i-contribute-to-documentation] Absolutely! Documentation improvements are always welcome. See the [Contributing Guide](/docs/contributing). ## Still Have Questions? [#still-have-questions] If your question isn't answered here: # Getting Started This guide will help you get up and running with Pulumi Any Terraform providers in minutes. ## Prerequisites [#prerequisites] Before you begin, make sure you have: * **Node.js** 18.0 or later (for TypeScript/JavaScript) * **Pulumi CLI** 3.190.0 or later * **Package Manager**: npm, yarn, or pnpm * A Pulumi account (sign up at [app.pulumi.com](https://app.pulumi.com)) ### Installing Pulumi [#installing-pulumi] If you don't have Pulumi installed: ```bash # macOS brew install pulumi/tap/pulumi # Windows choco install pulumi # Linux curl -fsSL https://get.pulumi.com | sh ``` Verify your installation: ```bash pulumi version ``` ## Installation [#installation] Each provider can be installed independently as an npm package. ### Using npm [#using-npm] ```bash npm install pulumi-namecheap ``` ### Using yarn [#using-yarn] ```bash yarn add pulumi-namecheap ``` ### Using pnpm [#using-pnpm] ```bash pnpm add pulumi-namecheap ``` ## Creating Your First Project [#creating-your-first-project] ### 1. Initialize a Pulumi Project [#1-initialize-a-pulumi-project] Create a new directory and initialize a Pulumi project: ```bash mkdir my-infrastructure cd my-infrastructure pulumi new typescript ``` Follow the prompts to set up your project. This creates: * `Pulumi.yaml` - Project configuration * `index.ts` - Your infrastructure code * `package.json` - Node.js dependencies ### 2. Install a Provider [#2-install-a-provider] Add a provider to your project: ```bash npm install pulumi-namecheap ``` ### 3. Configure Provider Credentials [#3-configure-provider-credentials] Most providers require API credentials. Configure them using Pulumi config: ```bash # For Namecheap pulumi config set namecheap:apiUser YOUR_API_USER pulumi config set namecheap:apiKey YOUR_API_KEY --secret pulumi config set namecheap:userName YOUR_USERNAME ``` Alternatively, use environment variables: ```bash export NAMECHEAP_API_USER="your-api-user" export NAMECHEAP_API_KEY="your-api-key" export NAMECHEAP_USER_NAME="your-username" ``` ### 4. Write Your Infrastructure Code [#4-write-your-infrastructure-code] Edit `index.ts` to define your infrastructure: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "pulumi-namecheap"; // Create a DNS record const wwwRecord = new namecheap.Record("www-record", { domain: "example.com", hostname: "www", type: "A", address: "192.168.1.1", ttl: 300, }); // Export the record ID export const recordId = wwwRecord.id; ``` ### 5. Deploy Your Infrastructure [#5-deploy-your-infrastructure] Preview and deploy your infrastructure: ```bash # Preview changes pulumi preview # Deploy changes pulumi up ``` Pulumi will show you what resources will be created. Type "yes" to proceed. ### 6. Update and Manage [#6-update-and-manage] To update your infrastructure, modify your code and run `pulumi up` again: ```typescript // Update the IP address const wwwRecord = new namecheap.Record("www-record", { domain: "example.com", hostname: "www", type: "A", address: "192.168.1.2", // Changed ttl: 300, }); ``` ```bash pulumi up ``` ### 7. Clean Up [#7-clean-up] When you're done, destroy your resources: ```bash pulumi destroy ``` ## Example: Multi-Resource Deployment [#example-multi-resource-deployment] Here's a more complete example using Better Uptime to set up monitoring: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as betteruptime from "pulumi-better-uptime"; // Create a monitor const monitor = new betteruptime.Monitor("api-monitor", { url: "https://api.example.com/health", monitorType: "status", checkFrequency: 60, // Check every 60 seconds confirmationPeriod: 120, requestTimeout: 30, recoveryPeriod: 0, ssl: { checkExpiry: true, expiryThreshold: 30, }, pronounceableName: "API Health Monitor", }); // Create an incident policy const policy = new betteruptime.Policy("critical-policy", { name: "Critical Alerts", repeatCount: 3, repeatDelay: 300, }); // Associate monitor with policy const policyMonitor = new betteruptime.MonitorGroupPolicy("monitor-policy", { monitorGroupId: monitor.id, policyId: policy.id, }); // Export monitor URL export const monitorUrl = pulumi.interpolate`https://betteruptime.com/monitors/${monitor.id}`; ``` ## Using Multiple Providers [#using-multiple-providers] You can use multiple providers in the same project: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "pulumi-namecheap"; import * as betteruptime from "pulumi-better-uptime"; // Domain configuration const apiRecord = new namecheap.Record("api-record", { domain: "example.com", hostname: "api", type: "A", address: "192.168.1.100", }); // Monitor the domain const apiMonitor = new betteruptime.Monitor("api-monitor", { url: pulumi.interpolate`https://api.example.com/health`, monitorType: "status", checkFrequency: 60, }); export const apiUrl = pulumi.interpolate`https://api.example.com`; export const monitorId = apiMonitor.id; ``` ## Configuration Best Practices [#configuration-best-practices] ### 1. Use Pulumi Config for Secrets [#1-use-pulumi-config-for-secrets] Always use `--secret` flag for sensitive data: ```bash pulumi config set myProvider:apiKey xyz123 --secret ``` ### 2. Organize with Stacks [#2-organize-with-stacks] Use different stacks for different environments: ```bash # Create production stack pulumi stack init production pulumi config set myProvider:endpoint https://api.production.com # Create staging stack pulumi stack init staging pulumi config set myProvider:endpoint https://api.staging.com ``` ### 3. Use Stack References [#3-use-stack-references] Share outputs between stacks: ```typescript import * as pulumi from "@pulumi/pulumi"; // Reference another stack's outputs const networkStack = new pulumi.StackReference("my-org/network/production"); const vpcId = networkStack.getOutput("vpcId"); ``` ## Next Steps [#next-steps] Now that you've created your first infrastructure: ## Common Commands [#common-commands] Here's a quick reference of Pulumi commands: | Command | Description | | ---------------- | --------------------------------------- | | `pulumi new` | Create a new project | | `pulumi preview` | Preview changes before deploying | | `pulumi up` | Deploy infrastructure | | `pulumi destroy` | Destroy all resources | | `pulumi stack` | Manage stacks | | `pulumi config` | Manage configuration | | `pulumi state` | Manage state | | `pulumi refresh` | Refresh state to match actual resources | | `pulumi cancel` | Cancel an in-progress update | ## Getting Help [#getting-help] If you run into issues: 1. Check the [Troubleshooting Guide](/docs/troubleshooting) 2. Review [Pulumi Documentation](https://www.pulumi.com/docs/) 3. Search [GitHub Issues](https://github.com/hckhanh/pulumi-any-terraform/issues) 4. Open a new issue with details about your problem # Introduction Welcome to the Pulumi Any Terraform documentation! This project provides seamless integration between Terraform providers and Pulumi, allowing you to leverage the extensive Terraform provider ecosystem while enjoying Pulumi's modern infrastructure-as-code experience. ## What is Pulumi Any Terraform? [#what-is-pulumi-any-terraform] Pulumi Any Terraform is a collection of **dynamically bridged Pulumi providers** that automatically converts Terraform providers into fully-typed Pulumi packages. This means you can: * Use any Terraform provider with Pulumi's state management * Get complete TypeScript/JavaScript type safety * Enjoy native integration with Pulumi's deployment engine * Stay automatically synced with upstream Terraform providers ## Key Features [#key-features] ### 🔄 Full Terraform Compatibility [#-full-terraform-compatibility] Use any Terraform provider with Pulumi without manual conversion. The bridge automatically handles the translation between Terraform's schema and Pulumi's resource model. ### 📘 Type Safety [#-type-safety] Complete TypeScript definitions for all resources, inputs, and outputs. Get IntelliSense, auto-completion, and compile-time type checking. ### 🌍 Multi-Language Support [#-multi-language-support] Generated providers work with TypeScript, JavaScript, Python, Go, and C#. Write infrastructure code in your preferred language. ### ⚡ Dynamic Updates [#-dynamic-updates] Automatically stay in sync with upstream Terraform providers. No manual regeneration needed when providers update. ### 🎯 Native Pulumi Experience [#-native-pulumi-experience] Full integration with Pulumi's state management, stack outputs, secrets, and deployment engine. All Pulumi features work seamlessly. ## Available Providers [#available-providers] ## Quick Start [#quick-start] Get started with any provider in seconds: ```bash # Install a provider npm install pulumi-namecheap # Use in your Pulumi program import * as namecheap from "pulumi-namecheap"; const record = new namecheap.Record("www", { domain: "example.com", type: "A", address: "192.168.1.1" }); ``` ## Next Steps [#next-steps] ## Community & Support [#community--support] * **GitHub**: [hckhanh/pulumi-any-terraform](https://github.com/hckhanh/pulumi-any-terraform) * **Issues**: Report bugs and request features on GitHub * **Pulumi Docs**: [pulumi.com/docs](https://www.pulumi.com/docs/) * **Terraform Registry**: [registry.terraform.io](https://registry.terraform.io/) ## License [#license] MIT - see individual package licenses for specific details. # Troubleshooting This guide covers common issues you might encounter when using Pulumi Any Terraform providers and how to resolve them. ## Installation Issues [#installation-issues] ### pnpm/npm Installation Fails [#pnpmnpm-installation-fails] **Problem**: Package installation fails with network errors ``` ERR_PNPM_FETCH_404 ``` **Solutions**: 1. Check your internet connection 2. Clear npm cache: `npm cache clean --force` 3. Clear pnpm cache: `pnpm store prune` 4. Try using a different registry mirror 5. Check if the package exists: `npm view pulumi-namecheap` ### Dependency Conflicts [#dependency-conflicts] **Problem**: Conflicting peer dependencies ``` ERESOLVE unable to resolve dependency tree ``` **Solutions**: 1. Update Pulumi CLI: `pulumi upgrade` 2. Use `--legacy-peer-deps` flag: `npm install --legacy-peer-deps` 3. Check package.json for version conflicts 4. Update @pulumi/pulumi to latest compatible version ### TypeScript Version Issues [#typescript-version-issues] **Problem**: TypeScript compilation errors ``` error TS2307: Cannot find module 'pulumi-namecheap' ``` **Solutions**: 1. Ensure TypeScript version matches requirements (5.x) 2. Install @types packages: `npm install @types/node` 3. Check tsconfig.json configuration 4. Rebuild: `npm run build` ## Configuration Issues [#configuration-issues] ### Provider Authentication Fails [#provider-authentication-fails] **Problem**: API authentication errors ``` Error: Authentication failed: Invalid API key ``` **Solutions**: 1. Verify credentials are correct 2. Check environment variables are set: `echo $NAMECHEAP_API_KEY` 3. Use Pulumi config: `pulumi config get namecheap:apiKey` 4. Ensure secret flag is used: `pulumi config set key value --secret` 5. Check IP whitelist settings (for providers that require it) ### Missing Configuration [#missing-configuration] **Problem**: Required configuration not set ``` Error: Missing required configuration 'apiKey' ``` **Solutions**: 1. Set missing configuration: `pulumi config set provider:key value` 2. Check for typos in config keys 3. Verify stack is selected: `pulumi stack select` 4. Review provider documentation for required config ### Environment Variable Not Loaded [#environment-variable-not-loaded] **Problem**: Environment variables not recognized **Solutions**: 1. Export variables in current shell: `export KEY=value` 2. Add to `.env` file (if supported) 3. Check variable name matches provider expectations 4. Restart terminal/IDE after setting variables ## Runtime Issues [#runtime-issues] ### Resource Creation Fails [#resource-creation-fails] **Problem**: Resource fails to create ``` Error: creating Resource: API error ``` **Solutions**: 1. Enable debug logging: `pulumi up --logtostderr -v=9` 2. Check API rate limits 3. Verify resource parameters are valid 4. Check provider API status 5. Review API documentation for constraints ### State Conflicts [#state-conflicts] **Problem**: State file conflicts ``` Error: resource already exists in state ``` **Solutions**: 1. Refresh state: `pulumi refresh` 2. Import existing resource: `pulumi import` 3. Remove from state: `pulumi state delete` 4. Check for duplicate resources in code ### Timeout Errors [#timeout-errors] **Problem**: Operations timeout ``` Error: timeout waiting for resource to become ready ``` **Solutions**: 1. Increase timeout in resource options 2. Check network connectivity 3. Verify external service is available 4. Review provider API performance ### Update Failures [#update-failures] **Problem**: Resource update fails ``` Error: updating Resource: conflict ``` **Solutions**: 1. Run `pulumi refresh` first 2. Check for manual changes outside Pulumi 3. Review resource locking mechanisms 4. Verify update is allowed by provider ## Type Safety Issues [#type-safety-issues] ### Type Errors in TypeScript [#type-errors-in-typescript] **Problem**: Type checking errors ``` Type 'string' is not assignable to type 'Input' ``` **Solutions**: 1. Use proper Input types: `pulumi.Input` 2. Use `pulumi.output()` for transformations 3. Check for type mismatches in args 4. Regenerate types if stale ### Missing Type Definitions [#missing-type-definitions] **Problem**: No IntelliSense or autocomplete **Solutions**: 1. Ensure package is installed correctly 2. Restart TypeScript language server 3. Check types are exported properly 4. Verify tsconfig.json includes node\_modules ## Performance Issues [#performance-issues] ### Slow Deployments [#slow-deployments] **Problem**: `pulumi up` takes too long **Solutions**: 1. Use `--parallel` flag for concurrent operations 2. Reduce resource count per stack 3. Check network latency to provider APIs 4. Enable Pulumi state caching 5. Review resource dependencies ### Build Performance [#build-performance] **Problem**: Build times are slow **Solutions**: 1. Use Nx caching: `nx reset` then rebuild 2. Enable incremental TypeScript compilation 3. Reduce package size 4. Update Node.js and TypeScript versions ## Debugging [#debugging] ### Enable Debug Logging [#enable-debug-logging] Enable comprehensive logging: ```bash # Pulumi debug logs export PULUMI_DEBUG_COMMANDS=true export PULUMI_DEBUG_PROMISE_LEAKS=true pulumi up --logtostderr -v=9 # Terraform provider logs export TF_LOG=DEBUG export TF_LOG_PATH=./terraform.log ``` ### Inspect State [#inspect-state] ```bash # View current state pulumi stack export # View specific resource pulumi stack export | jq '.deployment.resources[] | select(.type=="namecheap:index:Record")' # List all resources pulumi stack --show-urns ``` ### Test Provider Connection [#test-provider-connection] ```typescript import * as pulumi from "@pulumi/pulumi"; pulumi.log.info("Testing provider connection..."); // Try a simple operation const test = new Provider("test", { // minimal config }); pulumi.log.info("Provider initialized successfully"); ``` ## Provider-Specific Issues [#provider-specific-issues] ### Namecheap: IP Not Whitelisted [#namecheap-ip-not-whitelisted] **Problem**: API access denied **Solution**: Add your IP to Namecheap API whitelist in account settings ### Better Uptime: Rate Limiting [#better-uptime-rate-limiting] **Problem**: Too many requests **Solution**: Reduce check frequency or contact support for higher limits ### Bunnynet: Region Not Available [#bunnynet-region-not-available] **Problem**: Storage region unavailable **Solution**: Check [Bunny.net documentation](https://docs.bunny.net/) for available regions ### Infisical: Token Expired [#infisical-token-expired] **Problem**: Service token expired **Solution**: Generate new service token in Infisical dashboard ### Portainer: Connection Refused [#portainer-connection-refused] **Problem**: Cannot connect to Portainer **Solution**: Verify Portainer URL and check firewall rules ## Common Error Messages [#common-error-messages] ### "Resource not found" [#resource-not-found] **Cause**: Resource was deleted outside Pulumi **Solution**: ```bash pulumi refresh # Sync state with reality # or pulumi state delete urn # Remove from state ``` ### "Concurrent update detected" [#concurrent-update-detected] **Cause**: Multiple users updating same stack **Solution**: ```bash pulumi cancel # Cancel conflicting update pulumi refresh # Sync state pulumi up # Retry ``` ### "Output cannot be used in input" [#output-cannot-be-used-in-input] **Cause**: Using Output type where Input type expected **Solution**: ```typescript // Wrong const value = output.value; // Correct const value = pulumi.output(someOutput).apply(v => v); ``` ### "Secret must be encrypted" [#secret-must-be-encrypted] **Cause**: Plain text used for secret value **Solution**: ```bash pulumi config set key value --secret ``` ## Best Practices to Avoid Issues [#best-practices-to-avoid-issues] ### 1. Always Use Version Constraints [#1-always-use-version-constraints] ```json { "dependencies": { "pulumi-namecheap": "^2.0.0" // Use caret for compatible updates } } ``` ### 2. Pin Critical Dependencies [#2-pin-critical-dependencies] ```json { "devDependencies": { "@pulumi/pulumi": "3.205.0" // Exact version for stability } } ``` ### 3. Use Separate Stacks [#3-use-separate-stacks] ```bash pulumi stack init dev pulumi stack init staging pulumi stack init production ``` ### 4. Implement Resource Protection [#4-implement-resource-protection] ```typescript const criticalResource = new Resource("critical", { // ... }, { protect: true // Prevent accidental deletion }); ``` ### 5. Use Stack References [#5-use-stack-references] ```typescript const networkStack = new pulumi.StackReference("org/network/prod"); const vpcId = networkStack.getOutput("vpcId"); ``` ### 6. Regular State Refreshes [#6-regular-state-refreshes] ```bash # Before major operations pulumi refresh pulumi preview pulumi up ``` ## Getting Additional Help [#getting-additional-help] ### Check Documentation [#check-documentation] 1. Provider-specific documentation in this site 2. [Pulumi Documentation](https://www.pulumi.com/docs/) 3. Terraform provider documentation 4. Provider API documentation ### Community Support [#community-support] * **GitHub Issues**: [Report bugs](https://github.com/hckhanh/pulumi-any-terraform/issues) * **Pulumi Community Slack**: Get help from the community * **Stack Overflow**: Search for similar issues ### Reporting Bugs [#reporting-bugs] When reporting bugs, include: 1. **Provider and version**: `pulumi-namecheap@2.2.13` 2. **Pulumi version**: `pulumi version` 3. **Node.js version**: `node --version` 4. **Error message**: Full error output 5. **Minimal reproduction**: Simplified code that reproduces the issue 6. **Environment**: OS, shell, terminal 7. **Steps to reproduce**: Clear instructions ### Example Bug Report [#example-bug-report] ```markdown **Provider**: pulumi-namecheap@2.2.13 **Pulumi**: 3.205.0 **Node.js**: v22.18.0 **OS**: Ubuntu 22.04 **Issue**: DNS record creation fails with timeout **Error**: ``` Error: creating Record: timeout waiting for completion ```` **Code**: ```typescript const record = new namecheap.Record("test", { domain: "example.com", hostname: "test", type: "A", address: "192.168.1.1", }); ```` **Steps**: 1. Run `pulumi up` 2. See timeout after 5 minutes 3. Record is not created in Namecheap dashboard **Expected**: Record should be created successfully ```` ## Preventive Measures ### Regular Maintenance ```bash # Weekly pulumi refresh # Sync state pnpm update # Update dependencies # Monthly pulumi stack export --file backup.json # Backup state pnpm outdated # Check for updates ```` ### Monitoring [#monitoring] Set up monitoring for: * Resource health * API quota usage * Stack update frequency * Error rates ### Documentation [#documentation] Keep internal documentation for: * Custom resource patterns * Team conventions * Known issues and workarounds * Deployment procedures ## Quick Reference [#quick-reference] | Issue | Quick Fix | | ------------------------ | ----------------------------- | | State out of sync | `pulumi refresh` | | Build cache issues | `pnpm nx reset` | | Dependency conflicts | `pnpm install --force` | | Config not found | `pulumi config set key value` | | Import existing resource | `pulumi import type name id` | | Cancel stuck update | `pulumi cancel` | | View state | `pulumi stack export` | | Debug logging | `pulumi up -v=9` | # Configuration This guide covers how to configure the Better Uptime provider for Pulumi. ## Overview [#overview] The Better Uptime provider requires an API token for authentication. You can manage uptime monitoring, status pages, incidents, and integrations. ## Provider Configuration [#provider-configuration] ### Basic Setup [#basic-setup] ```typescript import * as betteruptime from "pulumi-better-uptime"; // Configure the provider with API token const config = new pulumi.Config("better-uptime"); const apiToken = config.requireSecret("apiToken"); ``` ### Configuration Methods [#configuration-methods] #### 1. Provider Block (Recommended) [#1-provider-block-recommended] ```typescript import * as betteruptime from "pulumi-better-uptime"; import * as pulumi from "@pulumi/pulumi"; const config = new pulumi.Config("better-uptime"); // API token from Pulumi config const provider = new betteruptime.Provider("betteruptime", { apiToken: config.requireSecret("apiToken"), }); ``` #### 2. Environment Variables [#2-environment-variables] ```bash export BETTERUPTIME_API_TOKEN="your-api-token-here" ``` #### 3. Pulumi Configuration [#3-pulumi-configuration] ```bash # Set API token (encrypted) pulumi config set better-uptime:apiToken --secret # Verify configuration pulumi config get better-uptime:apiToken ``` ## Getting API Credentials [#getting-api-credentials] ### Create API Token [#create-api-token] 1. Log in to Better Uptime dashboard 2. Navigate to **Settings** → **API Tokens** 3. Click **Create API Token** 4. Name your token (e.g., "Pulumi Integration") 5. Set appropriate permissions 6. Copy the generated token ### Required Permissions [#required-permissions] For full management capabilities: * **Monitors**: Read, Write * **Status Pages**: Read, Write * **Incidents**: Read, Write * **Integrations**: Read, Write * **Heartbeats**: Read, Write ## Configuration Options [#configuration-options] | Property | Type | Required | Description | | ---------- | ------ | -------- | ----------------------- | | `apiToken` | string | Yes | Better Uptime API token | ## Best Practices [#best-practices] ### Security [#security] ```typescript // ✅ DO: Use Pulumi secrets for API tokens const config = new pulumi.Config("better-uptime"); const apiToken = config.requireSecret("apiToken"); // ❌ DON'T: Hardcode API tokens const provider = new betteruptime.Provider("bad", { apiToken: "bup_abc123...", // Never do this! }); ``` ### Multi-Account Setup [#multi-account-setup] ```typescript // Production account const prodProvider = new betteruptime.Provider("prod", { apiToken: prodConfig.requireSecret("apiToken"), }); // Staging account const stagingProvider = new betteruptime.Provider("staging", { apiToken: stagingConfig.requireSecret("apiToken"), }); ``` ## Integration Examples [#integration-examples] ### With Better Uptime Monitor [#with-better-uptime-monitor] ```typescript import * as betteruptime from "pulumi-better-uptime"; import * as pulumi from "@pulumi/pulumi"; const config = new pulumi.Config("better-uptime"); // Create a website monitor const monitor = new betteruptime.Monitor("api-monitor", { url: "https://api.example.com/health", monitorType: "status", checkFrequency: 60, requestTimeout: 30, confirmationPeriod: 60, }); ``` ### With Status Page [#with-status-page] ```typescript // Create a status page const statusPage = new betteruptime.StatusPage("service-status", { companyName: "Example Inc", companyUrl: "https://example.com", subdomain: "status-example", timezone: "America/New_York", }); ``` ## Troubleshooting [#troubleshooting] ### Invalid API Token [#invalid-api-token] **Error**: "Authentication failed: Invalid API token" **Solution**: 1. Verify token is correct 2. Check token hasn't expired 3. Ensure token has required permissions ```bash # Test API token curl -H "Authorization: Bearer $BETTERUPTIME_API_TOKEN" \ https://betteruptime.com/api/v2/monitors ``` ### Rate Limiting [#rate-limiting] **Error**: "Rate limit exceeded" **Solution**: * Better Uptime has rate limits (default: 100 requests/minute) * Add delays between resource creations * Use resource `dependsOn` to sequence operations ### Permission Errors [#permission-errors] **Error**: "Insufficient permissions" **Solution**: * Verify API token has correct permissions * Check account plan supports the feature * Ensure you're accessing resources you own ## Additional Resources [#additional-resources] * [Better Uptime API Documentation](https://betteruptime.com/docs/api/v2) * [Monitor Resource](/docs/providers/better-uptime/monitor) * [Status Page Resource](/docs/providers/better-uptime/status-page) * [Heartbeat Resource](/docs/providers/better-uptime/heartbeat) # Heartbeat The Heartbeat resource creates monitors that expect regular "pings" from your jobs, cron tasks, or scheduled processes. ## Example Usage [#example-usage] ### Basic Heartbeat [#basic-heartbeat] ```typescript import * as betteruptime from "pulumi-better-uptime"; const heartbeat = new betteruptime.Heartbeat("backup-job", { name: "Database Backup", period: 86400, // 24 hours grace: 3600, // 1 hour grace period }); // Export the heartbeat URL to ping export const heartbeatUrl = heartbeat.url; ``` ### Cron Job Heartbeat [#cron-job-heartbeat] ```typescript const cronHeartbeat = new betteruptime.Heartbeat("daily-sync", { name: "Daily Data Sync", period: 86400, // Every 24 hours grace: 1800, // 30 minute grace period call: true, sms: true, email: true, }); ``` ### Short-Interval Heartbeat [#short-interval-heartbeat] ```typescript const frequentJob = new betteruptime.Heartbeat("health-check-job", { name: "Health Check Job", period: 300, // Every 5 minutes grace: 60, // 1 minute grace period }); ``` ### Heartbeat with Policy [#heartbeat-with-policy] ```typescript import * as betteruptime from "pulumi-better-uptime"; const policy = new betteruptime.Policy("ops-team", { name: "Operations Team", }); const criticalJob = new betteruptime.Heartbeat("critical-batch", { name: "Critical Batch Process", period: 3600, // Hourly grace: 300, // 5 minutes policyId: policy.id, call: true, sms: true, }); ``` ### Heartbeat Group [#heartbeat-group] ```typescript const group = new betteruptime.HeartbeatGroup("batch-jobs", { name: "Batch Processing Jobs", }); const heartbeat1 = new betteruptime.Heartbeat("job1", { name: "ETL Job 1", period: 43200, // 12 hours grace: 1800, heartbeatGroupId: group.id, }); const heartbeat2 = new betteruptime.Heartbeat("job2", { name: "ETL Job 2", period: 43200, grace: 1800, heartbeatGroupId: group.id, }); ``` ## Argument Reference [#argument-reference] ### Required Arguments [#required-arguments] * **`name`** (String) - Name of the heartbeat monitor * **`period`** (Number) - Expected period between pings in seconds ### Optional Arguments [#optional-arguments] * **`grace`** (Number) - Grace period in seconds before alerting. Default: 0 * **`call`** (Boolean) - Enable phone call notifications. Default: false * **`sms`** (Boolean) - Enable SMS notifications. Default: false * **`email`** (Boolean) - Enable email notifications. Default: true * **`push`** (Boolean) - Enable push notifications. Default: true * **`heartbeatGroupId`** (Number) - Heartbeat group ID * **`policyId`** (Number) - Escalation policy ID * **`paused`** (Boolean) - Whether the heartbeat is paused. Default: false * **`sort_index`** (Number) - Sort index for ordering ## Attribute Reference [#attribute-reference] * **`id`** (String) - The heartbeat ID * **`url`** (String) - The unique URL to ping * **`status`** (String) - Current heartbeat status (up, down, paused) * **`createdAt`** (String) - Creation timestamp * **`updatedAt`** (String) - Last update timestamp * **`lastPingAt`** (String) - Last successful ping timestamp ## Using Heartbeat URLs [#using-heartbeat-urls] ### In Shell Scripts [#in-shell-scripts] ```bash #!/bin/bash # Your backup script # Perform backup pg_dump mydb > backup.sql # Ping heartbeat on success curl -fsS --retry 3 "https://uptime.betterstack.com/api/v1/heartbeat/xxx" ``` ### In Python [#in-python] ```python import requests def backup_job(): try: # Perform backup perform_backup() # Ping heartbeat requests.get("https://uptime.betterstack.com/api/v1/heartbeat/xxx") except Exception as e: print(f"Backup failed: {e}") # Don't ping on failure - will trigger alert ``` ### In Node.js [#in-nodejs] ```javascript const https = require('https'); async function cronJob() { try { await performTask(); // Ping heartbeat https.get('https://uptime.betterstack.com/api/v1/heartbeat/xxx'); } catch (error) { console.error('Job failed:', error); // Missing ping will trigger alert } } ``` ### In Docker [#in-docker] ```dockerfile FROM alpine:latest # Install curl RUN apk add --no-cache curl # Your cron job COPY backup.sh /backup.sh RUN chmod +x /backup.sh # Add heartbeat ping RUN echo "*/5 * * * * /backup.sh && curl -fsS https://uptime.betterstack.com/api/v1/heartbeat/xxx" > /etc/crontabs/root CMD ["crond", "-f"] ``` ## Best Practices [#best-practices] ### Period and Grace Configuration [#period-and-grace-configuration] | Job Frequency | Period | Grace | Use Case | | ------------- | ------- | ----- | ---------------------- | | Every 5 min | 300s | 60s | Frequent health checks | | Hourly | 3600s | 300s | Regular maintenance | | Every 6 hours | 21600s | 1800s | Periodic sync jobs | | Daily | 86400s | 3600s | Daily backups | | Weekly | 604800s | 7200s | Weekly reports | ### Error Handling [#error-handling] ```typescript // Only ping on success const heartbeat = new betteruptime.Heartbeat("reliable-job", { name: "Data Processing", period: 3600, grace: 300, call: true, // Alert immediately via call }); // In your job script: // curl "$HEARTBEAT_URL" || exit 0 # Don't fail job if ping fails ``` ### Monitoring Multiple Environments [#monitoring-multiple-environments] ```typescript // Production heartbeat const prodHeartbeat = new betteruptime.Heartbeat("prod-backup", { name: "Production Backup", period: 86400, grace: 1800, call: true, sms: true, }); // Staging heartbeat const stagingHeartbeat = new betteruptime.Heartbeat("staging-backup", { name: "Staging Backup", period: 86400, grace: 3600, // More grace for staging email: true, }); ``` ### Grouped Heartbeats [#grouped-heartbeats] ```typescript // Create a group for related jobs const group = new betteruptime.HeartbeatGroup("etl-pipeline", { name: "ETL Pipeline Jobs", }); const steps = ["extract", "transform", "load"].map((step, i) => new betteruptime.Heartbeat(`etl-${step}`, { name: `ETL ${step.toUpperCase()}`, period: 7200, // 2 hours grace: 600, heartbeatGroupId: group.id, sortIndex: i, }) ); ``` ## Common Patterns [#common-patterns] ### Database Backup Monitoring [#database-backup-monitoring] ```typescript const dbBackup = new betteruptime.Heartbeat("postgres-backup", { name: "PostgreSQL Backup", period: 86400, // Daily grace: 3600, // 1 hour grace call: true, sms: true, }); // In backup script: // pg_dump db | gzip > backup.gz && curl "$HEARTBEAT_URL" ``` ### Cron Job Monitoring [#cron-job-monitoring] ```typescript const cronJob = new betteruptime.Heartbeat("report-generation", { name: "Daily Report Generation", period: 86400, grace: 1800, }); // Crontab: // 0 2 * * * /usr/local/bin/generate-report.sh && curl "$HEARTBEAT_URL" ``` ### API Scheduled Task [#api-scheduled-task] ```typescript const scheduledTask = new betteruptime.Heartbeat("cleanup-task", { name: "Cleanup Old Records", period: 21600, // Every 6 hours grace: 900, // 15 minutes }); // In your API: // schedule.every(6).hours.do(cleanup_and_ping) ``` ## Import [#import] Heartbeats can be imported using their ID: ```bash pulumi import better-uptime:index/heartbeat:Heartbeat example 123456 ``` ## Troubleshooting [#troubleshooting] ### Heartbeat Not Receiving Pings [#heartbeat-not-receiving-pings] **Causes**: * Incorrect URL * Network/firewall issues * Job failing before ping **Solution**: * Verify heartbeat URL is correct * Test with curl manually * Add logging to job ```bash # Debug heartbeat ping curl -v "https://uptime.betterstack.com/api/v1/heartbeat/xxx" ``` ### False Alerts [#false-alerts] **Cause**: Grace period too short for job variability **Solution**: ```typescript // Increase grace period for variable jobs const variableJob = new betteruptime.Heartbeat("variable", { name: "Variable Duration Job", period: 3600, grace: 1800, // 50% of period as grace }); ``` ### Missing Heartbeat Groups [#missing-heartbeat-groups] **Error**: "Heartbeat group not found" **Solution**: Ensure group is created before heartbeats: ```typescript const group = new betteruptime.HeartbeatGroup("group", { name: "My Group", }); const heartbeat = new betteruptime.Heartbeat("hb", { name: "Heartbeat", period: 3600, heartbeatGroupId: group.id, }, { dependsOn: [group] }); ``` # Better Uptime Provider The Better Uptime provider enables you to manage monitoring, alerting, and incident management resources using Pulumi. This provider is dynamically bridged from the [Terraform Better Uptime Provider](https://registry.terraform.io/providers/BetterStackHQ/better-uptime). ## Installation [#installation] Install the Better Uptime provider package using your preferred package manager: ```bash bun add pulumi-better-uptime ``` ```bash pnpm add pulumi-better-uptime ``` ```bash yarn add pulumi-better-uptime ``` ```bash npm install pulumi-better-uptime ``` ## Configuration [#configuration] ### Getting API Token [#getting-api-token] 1. Log in to your Better Uptime account at [betteruptime.com](https://betteruptime.com) 2. Navigate to Settings → API Tokens 3. Create a new API token 4. Copy the token value ### Provider Setup [#provider-setup] ```bash pulumi config set better-uptime:apiToken YOUR_API_TOKEN --secret ``` Or using environment variables: ```bash export BETTER_UPTIME_API_TOKEN="your-api-token" ``` ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as betteruptime from "pulumi-better-uptime"; // Create a monitor const monitor = new betteruptime.Monitor("website-monitor", { url: "https://example.com", monitorType: "status", checkFrequency: 60, requestTimeout: 30, pronounceableName: "Example Website", }); // Export monitor URL export const monitorUrl = pulumi.interpolate`https://betteruptime.com/monitors/${monitor.id}`; ``` ## Key Features [#key-features] ### Uptime Monitoring [#uptime-monitoring] Monitor websites, APIs, and services: ```typescript // HTTP status monitoring const httpMonitor = new betteruptime.Monitor("http-check", { url: "https://api.example.com/health", monitorType: "status", checkFrequency: 60, confirmationPeriod: 120, requestTimeout: 30, recoveryPeriod: 0, }); // Ping monitoring const pingMonitor = new betteruptime.Monitor("server-ping", { url: "192.168.1.1", monitorType: "ping", checkFrequency: 30, }); // Keyword monitoring const keywordMonitor = new betteruptime.Monitor("keyword-check", { url: "https://example.com", monitorType: "keyword", requiredKeyword: "operational", checkFrequency: 300, }); ``` ### SSL Certificate Monitoring [#ssl-certificate-monitoring] ```typescript const sslMonitor = new betteruptime.Monitor("ssl-check", { url: "https://example.com", monitorType: "status", ssl: { checkExpiry: true, expiryThreshold: 30, // Alert 30 days before expiry }, }); ``` ### Heartbeat Monitoring [#heartbeat-monitoring] Monitor cron jobs and scheduled tasks: ```typescript const heartbeat = new betteruptime.Heartbeat("backup-job", { name: "Nightly Backup", period: 86400, // 24 hours grace: 3600, // 1 hour grace period }); // Use heartbeat URL in your cron job export const heartbeatUrl = heartbeat.url; ``` ### Incident Policies [#incident-policies] ```typescript const policy = new betteruptime.Policy("critical-policy", { name: "Critical Alerts", repeatCount: 3, repeatDelay: 300, }); ``` ### Integrations [#integrations] Connect with popular tools: ```typescript // Slack integration const slackIntegration = new betteruptime.SlackIntegration("slack", { webhookUrl: config.requireSecret("slackWebhook"), }); // PagerDuty integration const pagerdutyIntegration = new betteruptime.PagerDutyIntegration("pagerduty", { routingKey: config.requireSecret("pagerdutyKey"), }); ``` ## Common Use Cases [#common-use-cases] ### Complete Monitoring Setup [#complete-monitoring-setup] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as betteruptime from "pulumi-better-uptime"; // Monitor group const monitorGroup = new betteruptime.MonitorGroup("production", { name: "Production Services", }); // Website monitor const webMonitor = new betteruptime.Monitor("website", { url: "https://example.com", monitorType: "status", checkFrequency: 60, monitorGroupId: monitorGroup.id, }); // API monitor const apiMonitor = new betteruptime.Monitor("api", { url: "https://api.example.com/health", monitorType: "status", checkFrequency: 30, monitorGroupId: monitorGroup.id, }); // Incident policy const policy = new betteruptime.Policy("critical", { name: "Critical Incidents", repeatCount: 5, repeatDelay: 300, }); // Link monitors to policy const policyLink = new betteruptime.MonitorGroupPolicy("policy-link", { monitorGroupId: monitorGroup.id, policyId: policy.id, }); export const dashboardUrl = pulumi.interpolate`https://betteruptime.com/team/monitors`; ``` ## Best Practices [#best-practices] ### 1. Set Appropriate Check Frequencies [#1-set-appropriate-check-frequencies] ```typescript // Critical services - check frequently const criticalMonitor = new betteruptime.Monitor("critical", { url: "https://api.example.com", checkFrequency: 30, // Every 30 seconds }); // Non-critical services - check less frequently const nonCriticalMonitor = new betteruptime.Monitor("non-critical", { url: "https://blog.example.com", checkFrequency: 300, // Every 5 minutes }); ``` ### 2. Use Confirmation Periods [#2-use-confirmation-periods] Avoid false positives: ```typescript const monitor = new betteruptime.Monitor("api", { url: "https://api.example.com", checkFrequency: 60, confirmationPeriod: 120, // Confirm down for 2 minutes before alerting }); ``` ### 3. Organize with Monitor Groups [#3-organize-with-monitor-groups] ```typescript const prodGroup = new betteruptime.MonitorGroup("production", { name: "Production", }); const stagingGroup = new betteruptime.MonitorGroup("staging", { name: "Staging", }); ``` ## Monitor Types [#monitor-types] | Type | Description | Use Case | | ----------- | ---------------------- | -------------------------- | | `status` | HTTP status code check | Websites, APIs | | `ping` | ICMP ping | Servers, network devices | | `keyword` | Content verification | Page content monitoring | | `port` | Port availability | Custom services | | `heartbeat` | Expected check-ins | Cron jobs, batch processes | ## Troubleshooting [#troubleshooting] ### Common Issues [#common-issues] #### API Rate Limiting [#api-rate-limiting] ``` Error: Rate limit exceeded ``` **Solution**: Reduce the frequency of API calls or contact Better Uptime support. #### Invalid URL Format [#invalid-url-format] ``` Error: Invalid URL format ``` **Solution**: Ensure URLs include the protocol (http\:// or https\://). ## Resource Reference [#resource-reference] ### Monitor [#monitor] **Key Arguments:** * `url` (Required): URL or IP to monitor * `monitorType` (Required): Type of monitor (status, ping, keyword, port, heartbeat) * `checkFrequency` (Optional): Check interval in seconds (default: 60) * `requestTimeout` (Optional): Request timeout in seconds (default: 30) * `confirmationPeriod` (Optional): Confirmation period in seconds (default: 0) **Attributes:** * `id`: Monitor ID * `status`: Current status * `createdAt`: Creation timestamp ### Heartbeat [#heartbeat] **Key Arguments:** * `name` (Required): Heartbeat name * `period` (Required): Expected period in seconds * `grace` (Optional): Grace period in seconds **Attributes:** * `id`: Heartbeat ID * `url`: Check-in URL # Monitor The Monitor resource allows you to create and manage uptime monitoring checks in Better Uptime. ## Example Usage [#example-usage] ### Basic HTTP Monitor [#basic-http-monitor] ```typescript import * as betteruptime from "pulumi-better-uptime"; const monitor = new betteruptime.Monitor("website-monitor", { url: "https://www.example.com", monitorType: "status", checkFrequency: 180, }); ``` ### API Endpoint Monitor [#api-endpoint-monitor] ```typescript const apiMonitor = new betteruptime.Monitor("api-health-check", { url: "https://api.example.com/health", monitorType: "status", checkFrequency: 60, requestTimeout: 30, confirmationPeriod: 60, call: true, sms: true, email: true, push: true, }); ``` ### Monitor with Expected Status [#monitor-with-expected-status] ```typescript const statusMonitor = new betteruptime.Monitor("service-status", { url: "https://service.example.com/status", monitorType: "expected_status_code", expectedStatusCodes: [200, 201], checkFrequency: 120, requestTimeout: 20, }); ``` ### Monitor with Keyword Check [#monitor-with-keyword-check] ```typescript const keywordMonitor = new betteruptime.Monitor("content-check", { url: "https://www.example.com", monitorType: "keyword", requiredKeyword: "Online", checkFrequency: 300, }); ``` ### Monitor with Custom Headers [#monitor-with-custom-headers] ```typescript const authMonitor = new betteruptime.Monitor("authenticated-endpoint", { url: "https://api.example.com/private", monitorType: "status", checkFrequency: 60, requestHeaders: [ { name: "Authorization", value: "Bearer token123" }, { name: "X-API-Key", value: "key456" }, ], }); ``` ### SSL Certificate Monitor [#ssl-certificate-monitor] ```typescript const sslMonitor = new betteruptime.Monitor("ssl-check", { url: "https://www.example.com", monitorType: "ssl", checkFrequency: 86400, // Once per day rememberCookies: false, }); ``` ### Monitor with Policy [#monitor-with-policy] ```typescript import * as betteruptime from "pulumi-better-uptime"; const policy = new betteruptime.Policy("oncall-policy", { name: "24/7 On-Call", }); const monitor = new betteruptime.Monitor("critical-service", { url: "https://critical.example.com", monitorType: "status", checkFrequency: 30, policyId: policy.id, call: true, sms: true, }); ``` ### Ping Monitor [#ping-monitor] ```typescript const pingMonitor = new betteruptime.Monitor("server-ping", { url: "192.168.1.100", monitorType: "ping", checkFrequency: 60, }); ``` ### Port Monitor [#port-monitor] ```typescript const portMonitor = new betteruptime.Monitor("database-port", { url: "db.example.com", port: 5432, monitorType: "tcp", checkFrequency: 120, }); ``` ### Monitor with Maintenance Windows [#monitor-with-maintenance-windows] ```typescript const monitor = new betteruptime.Monitor("maintenance-aware", { url: "https://api.example.com", monitorType: "status", checkFrequency: 60, maintenanceFrom: "2024-01-15T00:00:00Z", maintenanceTo: "2024-01-15T04:00:00Z", maintenanceTimezone: "America/New_York", }); ``` ## Argument Reference [#argument-reference] ### Required Arguments [#required-arguments] * **`url`** (String) - The URL, IP address, or host to monitor * **`monitorType`** (String) - Type of monitor. Options: `status`, `expected_status_code`, `keyword`, `keyword_absence`, `ping`, `tcp`, `udp`, `smtp`, `pop`, `imap`, `ssl` ### Optional Arguments [#optional-arguments] * **`checkFrequency`** (Number) - Check frequency in seconds. Options: 30, 60, 120, 180, 300, 600, 1800, 3600, 86400. Default: 180 * **`requestTimeout`** (Number) - Request timeout in seconds (1-60). Default: 15 * **`confirmationPeriod`** (Number) - Confirmation period in seconds before marking as down. Default: 0 * **`monitorGroupId`** (Number) - Monitor group ID * **`pronounceableName`** (String) - Pronounceable name for phone calls * **`paused`** (Boolean) - Whether the monitor is paused. Default: false * **`port`** (Number) - Port number (for TCP/UDP monitors) * **`regions`** (List) - Regions to check from (e.g., \["us", "eu", "as"]) * **`expectedStatusCodes`** (List) - Expected HTTP status codes (for expected\_status\_code type) * **`requiredKeyword`** (String) - Required keyword in response (for keyword type) * **`call`** (Boolean) - Enable phone call notifications. Default: false * **`sms`** (Boolean) - Enable SMS notifications. Default: false * **`email`** (Boolean) - Enable email notifications. Default: true * **`push`** (Boolean) - Enable push notifications. Default: true * **`policyId`** (Number) - Escalation policy ID * **`requestHeaders`** (List) - Custom HTTP headers * **`requestBody`** (String) - HTTP request body (for POST requests) * **`followRedirects`** (Boolean) - Follow HTTP redirects. Default: false * **`rememberCookies`** (Boolean) - Remember cookies between checks. Default: false * **`verifySSL`** (Boolean) - Verify SSL certificates. Default: true * **`maintenanceFrom`** (String) - Maintenance window start time (ISO 8601) * **`maintenanceTo`** (String) - Maintenance window end time (ISO 8601) * **`maintenanceTimezone`** (String) - Maintenance window timezone ## Attribute Reference [#attribute-reference] * **`id`** (String) - The monitor ID * **`status`** (String) - Current monitor status (up, down, paused, maintenance) * **`createdAt`** (String) - Creation timestamp * **`updatedAt`** (String) - Last update timestamp ## Monitor Types [#monitor-types] ### HTTP Monitors [#http-monitors] **status** - Checks if URL returns 2xx status code ```typescript { "status", url: "https://example.com" } ``` **expected\_status\_code** - Checks for specific status codes ```typescript { "expected_status_code", expectedStatusCodes: [200, 201, 204] } ``` **keyword** - Checks if response contains keyword ```typescript { "keyword", requiredKeyword: "success" } ``` **keyword\_absence** - Checks if response doesn't contain keyword ```typescript { "keyword_absence", requiredKeyword: "error" } ``` ### Network Monitors [#network-monitors] **ping** - ICMP ping check ```typescript { "ping", url: "192.168.1.1" } ``` **tcp** - TCP port check ```typescript { "tcp", url: "example.com", port: 443 } ``` **udp** - UDP port check ```typescript { "udp", url: "example.com", port: 53 } ``` ### Email Monitors [#email-monitors] **smtp** - SMTP server check ```typescript { "smtp", url: "mail.example.com", port: 587 } ``` **pop** - POP3 server check ```typescript { "pop", url: "mail.example.com", port: 110 } ``` **imap** - IMAP server check ```typescript { "imap", url: "mail.example.com", port: 993 } ``` ### SSL Monitor [#ssl-monitor] **ssl** - SSL certificate expiration check ```typescript { "ssl", url: "https://example.com" } ``` ## Best Practices [#best-practices] ### Check Frequency Guidelines [#check-frequency-guidelines] | Service Type | Recommended Frequency | Reason | | ---------------- | --------------------- | ---------------------- | | Critical API | 30-60 seconds | Immediate detection | | Public Website | 60-180 seconds | Balance cost/detection | | Internal Service | 180-300 seconds | Reduced load | | SSL Certificates | 86400 seconds (daily) | Infrequent changes | ### Timeout Configuration [#timeout-configuration] ```typescript // Fast APIs const fastApi = new betteruptime.Monitor("fast-api", { url: "https://fast-api.example.com", requestTimeout: 5, checkFrequency: 60, }); // Slow Services const slowService = new betteruptime.Monitor("slow-service", { url: "https://slow.example.com", requestTimeout: 30, checkFrequency: 180, }); ``` ### Confirmation Period [#confirmation-period] ```typescript // Avoid false alerts with confirmation period const reliableMonitor = new betteruptime.Monitor("reliable-check", { url: "https://api.example.com", checkFrequency: 60, confirmationPeriod: 60, // Wait 60s before alerting }); ``` ### Regional Monitoring [#regional-monitoring] ```typescript // Multi-region monitoring for global services const globalMonitor = new betteruptime.Monitor("global-service", { url: "https://global.example.com", monitorType: "status", regions: ["us", "eu", "as"], checkFrequency: 60, }); ``` ## Import [#import] Monitors can be imported using their ID: ```bash pulumi import better-uptime:index/monitor:Monitor example 123456 ``` ## Troubleshooting [#troubleshooting] ### Monitor Shows Down but Service is Up [#monitor-shows-down-but-service-is-up] **Causes**: * Firewall blocking Better Uptime IPs * Geographic restrictions * Rate limiting **Solution**: * Whitelist Better Uptime IP ranges * Adjust geographic restrictions * Increase rate limits ### False Positives [#false-positives] **Causes**: * Network instability * Service intermittent issues * Too short timeout **Solution**: ```typescript const stableMonitor = new betteruptime.Monitor("stable", { url: "https://api.example.com", requestTimeout: 30, confirmationPeriod: 120, // 2 minute confirmation checkFrequency: 180, }); ``` ### SSL Verification Failures [#ssl-verification-failures] **Cause**: Self-signed certificates or certificate issues **Solution**: ```typescript const devMonitor = new betteruptime.Monitor("dev-env", { url: "https://dev.example.com", monitorType: "status", verifySSL: false, // Only for development }); ``` # Status Page The Status Page resource creates public-facing status pages to communicate service health to customers. ## Example Usage [#example-usage] ### Basic Status Page [#basic-status-page] ```typescript import * as betteruptime from "pulumi-better-uptime"; const statusPage = new betteruptime.StatusPage("service-status", { companyName: "Example Inc", companyUrl: "https://www.example.com", subdomain: "status-example", timezone: "America/New_York", }); export const statusPageUrl = statusPage.url; ``` ### Status Page with Custom Design [#status-page-with-custom-design] ```typescript const brandedStatusPage = new betteruptime.StatusPage("branded-status", { companyName: "Acme Corporation", companyUrl: "https://acme.com", subdomain: "status-acme", timezone: "UTC", design: "v2", layout: "horizontal", theme: "dark", customCss: ` .header { background: #0066cc; } .status-badge { border-radius: 4px; } `, }); ``` ### Status Page with Logo [#status-page-with-logo] ```typescript const statusWithLogo = new betteruptime.StatusPage("company-status", { companyName: "Tech Startup", companyUrl: "https://techstartup.com", subdomain: "status", timezone: "America/Los_Angeles", logoUrl: "https://techstartup.com/logo.png", faviconUrl: "https://techstartup.com/favicon.ico", }); ``` ### Multi-Language Status Page [#multi-language-status-page] ```typescript const multiLangStatus = new betteruptime.StatusPage("global-status", { companyName: "Global Services", companyUrl: "https://global.example.com", subdomain: "status-global", timezone: "UTC", subscribeBySms: true, smsNotificationsEnabled: true, supportedLanguages: ["en", "es", "fr", "de"], automaticTranslation: true, }); ``` ### Status Page with Announcement [#status-page-with-announcement] ```typescript const statusWithAnnouncement = new betteruptime.StatusPage("api-status", { companyName: "API Platform", companyUrl: "https://api.example.com", subdomain: "apistatus", timezone: "America/New_York", announcement: "Scheduled maintenance on Saturday 2AM-4AM EST", announcementType: "maintenance", }); ``` ### Private Status Page [#private-status-page] ```typescript const privateStatus = new betteruptime.StatusPage("internal-status", { companyName: "Internal Services", companyUrl: "https://internal.example.com", subdomain: "status-internal", timezone: "UTC", passwordEnabled: true, password: "secure-password-123", }); ``` ## Argument Reference [#argument-reference] ### Required Arguments [#required-arguments] * **`companyName`** (String) - Company name displayed on status page * **`companyUrl`** (String) - Company website URL * **`subdomain`** (String) - Subdomain for status page (status-example.betteruptime.com) * **`timezone`** (String) - Timezone for incident times ### Optional Arguments [#optional-arguments] #### Branding [#branding] * **`logoUrl`** (String) - URL to company logo * **`faviconUrl`** (String) - URL to favicon * **`customCss`** (String) - Custom CSS for styling * **`theme`** (String) - Theme: "light" or "dark". Default: "light" * **`layout`** (String) - Layout: "vertical" or "horizontal". Default: "vertical" * **`design`** (String) - Design version: "v1" or "v2". Default: "v2" #### Features [#features] * **`subscribeBySms`** (Boolean) - Enable SMS subscriptions. Default: false * **`smsNotificationsEnabled`** (Boolean) - Enable SMS notifications. Default: false * **`hideFromSearchEngines`** (Boolean) - Hide from search engines. Default: false * **`passwordEnabled`** (Boolean) - Enable password protection. Default: false * **`password`** (String) - Password for private status page * **`minIncidentLength`** (Number) - Minimum incident duration to display (seconds) * **`announcementEmbedEnabled`** (Boolean) - Enable announcement embed. Default: false * **`announcement`** (String) - Announcement text * **`announcementType`** (String) - Announcement type: "info", "maintenance", "warning" #### Localization [#localization] * **`supportedLanguages`** (List) - Supported language codes (e.g., \["en", "es", "fr"]) * **`automaticTranslation`** (Boolean) - Enable automatic translation. Default: false ## Attribute Reference [#attribute-reference] * **`id`** (String) - The status page ID * **`url`** (String) - Public status page URL * **`createdAt`** (String) - Creation timestamp * **`updatedAt`** (String) - Last update timestamp ## Adding Monitors to Status Page [#adding-monitors-to-status-page] ### Status Page Resources [#status-page-resources] ```typescript import * as betteruptime from "pulumi-better-uptime"; const statusPage = new betteruptime.StatusPage("services", { companyName: "Example Inc", subdomain: "status", timezone: "UTC", }); const apiMonitor = new betteruptime.Monitor("api", { url: "https://api.example.com", monitorType: "status", }); const webMonitor = new betteruptime.Monitor("website", { url: "https://www.example.com", monitorType: "status", }); // Add monitors to status page const apiResource = new betteruptime.StatusPageResource("api-resource", { statusPageId: statusPage.id, monitorId: apiMonitor.id, publicName: "API", position: 1, }); const webResource = new betteruptime.StatusPageResource("web-resource", { statusPageId: statusPage.id, monitorId: webMonitor.id, publicName: "Website", position: 2, }); ``` ### Status Page Sections [#status-page-sections] ```typescript // Create sections for organization const section = new betteruptime.StatusPageSection("core-services", { statusPageId: statusPage.id, name: "Core Services", position: 1, }); const sectionResource = new betteruptime.StatusPageResource("api-in-section", { statusPageId: statusPage.id, statusPageSectionId: section.id, monitorId: apiMonitor.id, publicName: "API Service", position: 1, }); ``` ## Best Practices [#best-practices] ### Subdomain Naming [#subdomain-naming] ```typescript // ✅ Good subdomain names "status" // Simple and clear "status-api" // Service-specific "status-platform" // Platform-specific // ❌ Avoid "my-cool-status" // Too casual "status123" // Meaningless numbers "asdfstatus" // Unprofessional ``` ### Theme Selection [#theme-selection] ```typescript // Match your brand const lightTheme = new betteruptime.StatusPage("light", { companyName: "Bright Corp", subdomain: "status-bright", timezone: "UTC", theme: "light", customCss: ".header { background: #ffffff; }", }); const darkTheme = new betteruptime.StatusPage("dark", { companyName: "Dark Corp", subdomain: "status-dark", timezone: "UTC", theme: "dark", customCss: ".header { background: #1a1a1a; }", }); ``` ### Privacy Settings [#privacy-settings] ```typescript // Public status page const publicStatus = new betteruptime.StatusPage("public", { companyName: "Public Services", subdomain: "status", timezone: "UTC", hideFromSearchEngines: false, }); // Private internal status page const privateStatus = new betteruptime.StatusPage("private", { companyName: "Internal Services", subdomain: "status-internal", timezone: "UTC", passwordEnabled: true, password: process.env.STATUS_PAGE_PASSWORD, hideFromSearchEngines: true, }); ``` ### Announcement Management [#announcement-management] ```typescript // Planned maintenance announcement const maintenanceStatus = new betteruptime.StatusPage("maintenance", { companyName: "Services", subdomain: "status", timezone: "America/New_York", announcement: "Scheduled maintenance: Saturday 2-4 AM EST", announcementType: "maintenance", }); // General information const infoStatus = new betteruptime.StatusPage("info", { companyName: "Services", subdomain: "status", timezone: "UTC", announcement: "New features coming soon!", announcementType: "info", }); ``` ## Common Patterns [#common-patterns] ### Complete Status Page Setup [#complete-status-page-setup] ```typescript import * as betteruptime from "pulumi-better-uptime"; // Create status page const statusPage = new betteruptime.StatusPage("company-status", { companyName: "Acme Corp", companyUrl: "https://acme.com", subdomain: "status", timezone: "America/New_York", logoUrl: "https://acme.com/logo.png", subscribeBySms: true, smsNotificationsEnabled: true, }); // Create sections const coreSection = new betteruptime.StatusPageSection("core", { statusPageId: statusPage.id, name: "Core Services", position: 1, }); const apiSection = new betteruptime.StatusPageSection("apis", { statusPageId: statusPage.id, name: "APIs", position: 2, }); // Add monitors const webMonitor = new betteruptime.Monitor("web", { url: "https://www.acme.com", monitorType: "status", }); const apiMonitor = new betteruptime.Monitor("api", { url: "https://api.acme.com", monitorType: "status", }); // Add to status page const webResource = new betteruptime.StatusPageResource("web-resource", { statusPageId: statusPage.id, statusPageSectionId: coreSection.id, monitorId: webMonitor.id, publicName: "Website", position: 1, }); const apiResource = new betteruptime.StatusPageResource("api-resource", { statusPageId: statusPage.id, statusPageSectionId: apiSection.id, monitorId: apiMonitor.id, publicName: "REST API", position: 1, }); export const statusUrl = statusPage.url; ``` ## Import [#import] Status pages can be imported using their ID: ```bash pulumi import better-uptime:index/statusPage:StatusPage example 123456 ``` ## Troubleshooting [#troubleshooting] ### Subdomain Already Taken [#subdomain-already-taken] **Error**: "Subdomain is already in use" **Solution**: * Choose a different subdomain * Add suffix like `-prod` or `-services` * Use company/product name ### Custom CSS Not Applied [#custom-css-not-applied] **Causes**: * Invalid CSS syntax * Cache not cleared * Design version incompatibility **Solution**: ```typescript const statusPage = new betteruptime.StatusPage("fixed", { companyName: "Company", subdomain: "status", timezone: "UTC", design: "v2", // Ensure using v2 customCss: ` /* Valid CSS only */ .header { background-color: #0066cc; } `, }); ``` ### Password Protection Not Working [#password-protection-not-working] **Cause**: Password not enabled or incorrect **Solution**: ```typescript const protectedStatus = new betteruptime.StatusPage("protected", { companyName: "Private Services", subdomain: "status-private", timezone: "UTC", passwordEnabled: true, // Must be true password: "your-secure-password", }); ``` # Configuration The Buildkite provider allows you to manage CI/CD pipelines, clusters, teams, and agents in your Buildkite organization. ## Provider Configuration [#provider-configuration] ### Required Settings [#required-settings] | Parameter | Description | Environment Variable | | -------------- | --------------------------- | ----------------------------- | | `apiToken` | Buildkite API access token | `BUILDKITE_API_TOKEN` | | `organization` | Buildkite organization slug | `BUILDKITE_ORGANIZATION_SLUG` | ### Environment Variables [#environment-variables] ```bash export BUILDKITE_API_TOKEN="bkua_your_api_token_here" export BUILDKITE_ORGANIZATION_SLUG="your-org-slug" ``` ### Pulumi Configuration [#pulumi-configuration] ```bash pulumi config set buildkite:apiToken "bkua_your_api_token_here" --secret pulumi config set buildkite:organization "your-org-slug" ``` ### Provider Instance [#provider-instance] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as buildkite from "pulumi-buildkite"; const provider = new buildkite.Provider("buildkite-provider", { apiToken: "bkua_your_api_token_here", organization: "your-org-slug", }); const pipeline = new buildkite.Pipeline("example", { name: "Example Pipeline", repository: "https://github.com/myorg/myapp.git", defaultBranch: "main", steps: `steps:\n - command: "echo hello"`, }, { provider }); ``` ## Getting Your API Token [#getting-your-api-token] 1. Log in to [Buildkite](https://buildkite.com) 2. Go to **Personal Settings** → **API Access Tokens** 3. Click **New API Access Token** 4. Give it a descriptive name (e.g., "Pulumi Provider") 5. Select the required scopes: * `read_pipelines` and `write_pipelines` for pipeline management * `read_builds` and `write_builds` for build management * `read_agents` for agent information * `read_teams` and `write_teams` for team management 6. Click **Create API Access Token** 7. Copy the token and store it securely ## Required Scopes [#required-scopes] | Operation | Required Scopes | | ------------------ | ----------------------------------- | | Manage pipelines | `read_pipelines`, `write_pipelines` | | Manage teams | `read_teams`, `write_teams` | | Manage agents | `read_agents` | | Manage clusters | `read_clusters`, `write_clusters` | | Read organization | `read_organizations` | | GraphQL API access | `graphql` | ## Multi-Environment Setup [#multi-environment-setup] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as buildkite from "pulumi-buildkite"; const stack = pulumi.getStack(); const config = new pulumi.Config("buildkite"); // Use stack-specific configuration const provider = new buildkite.Provider("buildkite", { apiToken: config.requireSecret("apiToken"), organization: config.require("organization"), }); ``` ## Best Practices [#best-practices] ### Store Tokens Securely [#store-tokens-securely] ```bash # Always use --secret for API tokens pulumi config set buildkite:apiToken "bkua_..." --secret ``` ### Use Least Privilege [#use-least-privilege] Create API tokens with only the scopes needed for your use case. Avoid using tokens with full access when only pipeline management is needed. ### Separate Tokens per Environment [#separate-tokens-per-environment] Use different API tokens for different Pulumi stacks to maintain environment isolation: ```bash # Staging stack pulumi stack select staging pulumi config set buildkite:apiToken "bkua_staging_token" --secret # Production stack pulumi stack select production pulumi config set buildkite:apiToken "bkua_production_token" --secret ``` # Buildkite Provider The Buildkite provider enables you to manage CI/CD pipelines, agent clusters, teams, and organization resources in Buildkite using Pulumi. This provider is dynamically bridged from the [Terraform Buildkite Provider](https://registry.terraform.io/providers/buildkite/buildkite). ## Installation [#installation] Install the Buildkite provider package using your preferred package manager: ```bash bun add pulumi-buildkite ``` ```bash pnpm add pulumi-buildkite ``` ```bash yarn add pulumi-buildkite ``` ```bash npm install pulumi-buildkite ``` ## Configuration [#configuration] Set up your Buildkite API credentials: ```bash pulumi config set buildkite:apiToken "your-api-token" --secret pulumi config set buildkite:organization "your-org-slug" ``` Or use environment variables: ```bash export BUILDKITE_API_TOKEN="your-api-token" export BUILDKITE_ORGANIZATION_SLUG="your-org-slug" ``` ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as buildkite from "pulumi-buildkite"; // Create a pipeline const pipeline = new buildkite.Pipeline("my-app", { name: "My Application", repository: "https://github.com/myorg/myapp.git", defaultBranch: "main", steps: ` steps: - label: ":hammer: Build" command: "npm run build" - label: ":test_tube: Test" command: "npm test" `, }); export const pipelineSlug = pipeline.slug; ``` ## Key Features [#key-features] ### Pipelines [#pipelines] Create and manage CI/CD pipelines with build steps, schedules, and webhooks: ```typescript const pipeline = new buildkite.Pipeline("deploy", { name: "Deploy Pipeline", repository: "https://github.com/myorg/myapp.git", defaultBranch: "main", steps: ` steps: - label: ":rocket: Deploy" command: "./deploy.sh" branches: "main" `, }); // Add a nightly build schedule const schedule = new buildkite.PipelineSchedule("nightly", { pipelineId: pipeline.id, label: "Nightly Build", cronline: "0 0 * * *", branch: "main", enabled: true, }); ``` ### Clusters [#clusters] Organize build agents into clusters with queues and secrets: ```typescript const cluster = new buildkite.Cluster("production", { name: "Production", description: "Production build agents", }); const queue = new buildkite.ClusterQueue("default", { clusterId: cluster.id, key: "default", description: "Default build queue", }); const secret = new buildkite.ClusterSecret("npm-token", { clusterId: cluster.id, key: "NPM_TOKEN", value: "your-npm-token", }); ``` ### Teams [#teams] Configure teams and manage access to pipelines: ```typescript const team = new buildkite.Team("developers", { name: "Developers", privacy: "VISIBLE", defaultMemberRole: "MEMBER", isDefaultTeam: false, }); const access = new buildkite.PipelineTeam("dev-access", { pipelineId: pipeline.id, teamId: team.id, accessLevel: "BUILD_AND_READ", }); ``` ### Agent Tokens [#agent-tokens] Manage agent registration tokens: ```typescript const agentToken = new buildkite.AgentToken("ci-agents", { description: "Token for CI build agents", }); export const token = agentToken.token; ``` ## Common Use Cases [#common-use-cases] ### CI/CD Pipeline with Testing [#cicd-pipeline-with-testing] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as buildkite from "pulumi-buildkite"; const testPipeline = new buildkite.Pipeline("test-pipeline", { name: "Test & Deploy", repository: "https://github.com/myorg/myapp.git", defaultBranch: "main", steps: ` steps: - group: ":test_tube: Tests" steps: - label: ":npm: Unit Tests" command: "npm test" - label: ":cypress: E2E Tests" command: "npm run test:e2e" - wait - label: ":rocket: Deploy" command: "./deploy.sh" branches: "main" `, }); ``` ### Multi-Cluster Setup [#multi-cluster-setup] ```typescript const stagingCluster = new buildkite.Cluster("staging", { name: "Staging", description: "Staging environment agents", }); const prodCluster = new buildkite.Cluster("production", { name: "Production", description: "Production environment agents", }); // Create agent tokens for each cluster const stagingToken = new buildkite.ClusterAgentToken("staging-token", { clusterId: stagingCluster.id, description: "Staging agent token", }); const prodToken = new buildkite.ClusterAgentToken("prod-token", { clusterId: prodCluster.id, description: "Production agent token", }); ``` ## Resource Types [#resource-types] | Resource | Description | | ----------------- | ------------------------------ | | Pipeline | CI/CD pipeline management | | PipelineSchedule | Scheduled builds for pipelines | | PipelineTeam | Team access to pipelines | | PipelineTemplate | Reusable pipeline templates | | PipelineWebhook | Webhook integrations | | Cluster | Agent cluster management | | ClusterQueue | Queues within clusters | | ClusterSecret | Secrets stored in clusters | | ClusterAgentToken | Agent tokens for clusters | | Team | Team management | | TeamMember | Team membership | | AgentToken | Agent registration tokens | | Organization | Organization settings | | OrganizationRule | Organization rules | | TestSuite | Test analytics suites | | Registry | Package registries | ## Getting Help [#getting-help] * [Buildkite Documentation](https://buildkite.com/docs) * [Buildkite API Reference](https://buildkite.com/docs/apis) * [Terraform Buildkite Provider](https://registry.terraform.io/providers/buildkite/buildkite/latest/docs) * [Configuration Guide](/docs/providers/buildkite/configuration) # Configuration This guide covers how to configure the Bunny provider for Pulumi. ## Overview [#overview] The Bunny provider requires an API key for authentication. You can manage CDN pull zones, storage zones, and edge compute scripts. ## Provider Configuration [#provider-configuration] ### Basic Setup [#basic-setup] ```typescript import * as bunnynet from "pulumi-bunnynet"; // Configure the provider with API key const config = new pulumi.Config("bunnynet"); const apiKey = config.requireSecret("apiKey"); ``` ### Configuration Methods [#configuration-methods] #### 1. Provider Block (Recommended) [#1-provider-block-recommended] ```typescript import * as bunnynet from "pulumi-bunnynet"; import * as pulumi from "@pulumi/pulumi"; const config = new pulumi.Config("bunnynet"); // API key from Pulumi config const provider = new bunnynet.Provider("bunnynet", { apiKey: config.requireSecret("apiKey"), }); ``` #### 2. Environment Variables [#2-environment-variables] ```bash export BUNNY_API_KEY="your-api-key-here" ``` #### 3. Pulumi Configuration [#3-pulumi-configuration] ```bash # Set API key (encrypted) pulumi config set bunnynet:apiKey --secret # Verify configuration pulumi config get bunnynet:apiKey ``` ## Getting API Credentials [#getting-api-credentials] ### Create API Key [#create-api-key] 1. Log in to Bunny.net dashboard 2. Navigate to **Account** → **API** 3. Click **Add API Key** 4. Name your key (e.g., "Pulumi Integration") 5. Set appropriate permissions 6. Copy the generated API key ### Required Permissions [#required-permissions] For full management capabilities: * **Pull Zones**: Read, Write * **Storage Zones**: Read, Write * **Billing**: Read (for usage monitoring) * **Statistics**: Read (for analytics) ## Configuration Options [#configuration-options] | Property | Type | Required | Description | | -------- | ------ | -------- | ----------------- | | `apiKey` | string | Yes | Bunny.net API key | ## Best Practices [#best-practices] ### Security [#security] ```typescript // ✅ DO: Use Pulumi secrets for API keys const config = new pulumi.Config("bunnynet"); const apiKey = config.requireSecret("apiKey"); // ❌ DON'T: Hardcode API keys const provider = new bunnynet.Provider("bad", { apiKey: "abc123...", // Never do this! }); ``` ### Multi-Account Setup [#multi-account-setup] ```typescript // Production account const prodProvider = new bunnynet.Provider("prod", { apiKey: prodConfig.requireSecret("apiKey"), }); // Development account const devProvider = new bunnynet.Provider("dev", { apiKey: devConfig.requireSecret("apiKey"), }); ``` ## Integration Examples [#integration-examples] ### With Pull Zone [#with-pull-zone] ```typescript import * as bunnynet from "pulumi-bunnynet"; // Create a CDN pull zone with a URL origin const pullZone = new bunnynet.Pullzone("website-cdn", { name: "example-cdn", origin: { type: "OriginUrl", url: "https://origin.example.com", }, }); ``` ### With Storage Zone [#with-storage-zone] ```typescript // Create a storage zone const storageZone = new bunnynet.StorageZone("assets", { name: "example-assets", region: "NY", zoneTier: "Standard", replicationRegions: ["LA", "SG"], }); ``` ### Linking Storage Zone to Pull Zone [#linking-storage-zone-to-pull-zone] To use a Bunny Storage Zone as the CDN origin, set `origin.type` to `"StorageZone"` and pass the storage zone ID via `origin.storagezone`. The `storageZoneId` property on `StorageZone` is output-only — create the storage zone first, then reference its ID from the pull zone. ```typescript import * as bunnynet from "pulumi-bunnynet"; const storageZone = new bunnynet.StorageZone("assets", { name: "example-assets", region: "NY", zoneTier: "Standard", }); const pullZone = new bunnynet.Pullzone("website-cdn", { name: "example-cdn", origin: { type: "StorageZone", storagezone: storageZone.storageZoneId, }, }, { dependsOn: [storageZone] }); ``` Do not confuse `origin.storagezone` with other pull zone storage fields: * `logStorageZone` — storage zone used for access log storage * `permacacheStoragezone` — storage zone used for Perma-Cache ## Troubleshooting [#troubleshooting] ### Invalid API Key [#invalid-api-key] **Error**: "Authentication failed: Invalid API key" **Solution**: 1. Verify API key is correct 2. Check key hasn't been revoked 3. Ensure key has required permissions ```bash # Test API key curl -H "AccessKey: $BUNNY_API_KEY" \ https://api.bunny.net/pullzone ``` ### Rate Limiting [#rate-limiting] **Error**: "Rate limit exceeded" **Solution**: * Bunny.net has rate limits (varies by plan) * Add delays between resource creations * Use resource `dependsOn` to sequence operations ### Permission Errors [#permission-errors] **Error**: "Insufficient permissions" **Solution**: * Verify API key has correct permissions * Check account plan supports the feature * Ensure you're accessing resources you own ## Additional Resources [#additional-resources] * [Bunny.net API Documentation](https://docs.bunny.net/reference/bunnynet-api-overview) * [Pull Zone Resource](/docs/providers/bunnynet/pull-zone) * [Storage Zone Resource](/docs/providers/bunnynet/storage-zone) # Bunny Provider The Bunny provider enables you to manage Bunny.net CDN, storage, DNS, and edge computing resources using Pulumi. This provider is dynamically bridged from the [Terraform Bunnynet Provider](https://registry.terraform.io/providers/simplesurance/bunny). ## Installation [#installation] Install the Bunnynet provider package using your preferred package manager: ```bash bun add pulumi-bunnynet ``` ```bash pnpm add pulumi-bunnynet ``` ```bash yarn add pulumi-bunnynet ``` ```bash npm install pulumi-bunnynet ``` ## Configuration [#configuration] ### Getting API Key [#getting-api-key] 1. Log in to your Bunny.net account at [bunny.net](https://bunny.net) 2. Navigate to Account → API 3. Copy your API key ### Provider Setup [#provider-setup] ```bash pulumi config set bunnynet:apiKey YOUR_API_KEY --secret ``` Or using environment variables: ```bash export BUNNY_API_KEY="your-api-key" ``` ## Quick Start [#quick-start] ```typescript import * as bunnynet from "pulumi-bunnynet"; // Create a pull zone (CDN) const pullZone = new bunnynet.Pullzone("my-cdn", { name: "my-website-cdn", origin: { type: "OriginUrl", url: "https://example.com", }, }); export const cdnDomain = pullZone.cdnDomain; ``` ## Key Features [#key-features] ### Pull Zones (CDN) [#pull-zones-cdn] ```typescript const pullZone = new bunnynet.Pullzone("cdn", { name: "my-cdn", origin: { type: "OriginUrl", url: "https://origin.example.com", }, cacheEnabled: true, cacheExpirationTime: 3600, }); ``` To use a storage zone as the origin instead of a URL: ```typescript const storageZone = new bunnynet.StorageZone("storage", { name: "my-storage", region: "DE", zoneTier: "Standard", }); const pullZone = new bunnynet.Pullzone("cdn", { name: "my-cdn", origin: { type: "StorageZone", storagezone: storageZone.storageZoneId, }, }, { dependsOn: [storageZone] }); ``` ### Storage Zones [#storage-zones] ```typescript const storageZone = new bunnynet.StorageZone("storage", { name: "my-storage", region: "DE", // Germany zoneTier: "Standard", replicationRegions: ["NY", "LA"], }); ``` ### DNS Management [#dns-management] ```typescript const dnsZone = new bunnynet.DnsZone("dns", { domain: "example.com", }); const dnsRecord = new bunnynet.DnsRecord("www", { zone: dnsZone.dnsZoneId, name: "www", type: "A", value: "192.168.1.1", ttl: 300, }); ``` # Infisical Provider The Infisical provider enables you to manage secrets, projects, and access controls in Infisical using Pulumi. This provider is dynamically bridged from the [Terraform Infisical Provider](https://registry.terraform.io/providers/Infisical/infisical). ## Installation [#installation] Install the Infisical provider package using your preferred package manager: ```bash bun add pulumi-infisical ``` ```bash pnpm add pulumi-infisical ``` ```bash yarn add pulumi-infisical ``` ```bash npm install pulumi-infisical ``` ## Configuration [#configuration] ### Getting Service Token [#getting-service-token] 1. Log in to Infisical at [app.infisical.com](https://app.infisical.com) 2. Navigate to your project → Settings → Service Tokens 3. Create a new service token 4. Copy the token value ### Provider Setup [#provider-setup] ```bash pulumi config set infisical:token YOUR_SERVICE_TOKEN --secret ``` Or using environment variables: ```bash export INFISICAL_TOKEN="your-service-token" ``` ### Self-Hosted Infisical [#self-hosted-infisical] If you're using a self-hosted Infisical instance, configure the custom host URL: ```bash pulumi config set infisical:hostUrl https://infisical.your-domain.com ``` Or using environment variables: ```bash export INFISICAL_HOST_URL="https://infisical.your-domain.com" ``` ```typescript import * as pulumi from "@pulumi/pulumi"; import * as infisical from "pulumi-infisical"; // Configure provider for self-hosted instance const provider = new infisical.Provider("self-hosted", { hostUrl: "https://infisical.your-domain.com", token: config.requireSecret("token"), }); // Use the provider const project = new infisical.Project("project", { name: "Backend Service", slug: "backend-service", }, { provider }); ``` ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as infisical from "pulumi-infisical"; // Create a project const project = new infisical.Project("api-project", { name: "API Service", slug: "api-service", }); // Create a secret const secret = new infisical.Secret("api-key", { projectId: project.id, environment: "production", key: "API_KEY", value: "super-secret-value", }); export const projectId = project.id; ``` ## Key Features [#key-features] ### Project Management [#project-management] ```typescript const project = new infisical.Project("backend-project", { name: "Backend Service", slug: "backend-service", }); ``` ### Secret Management [#secret-management] ```typescript const secret = new infisical.Secret("database-password", { projectId: project.id, environment: "production", key: "DATABASE_PASSWORD", value: dbPassword, type: "shared", }); ``` ### Identity Management [#identity-management] ```typescript const identity = new infisical.Identity("api-identity", { name: "API Service", roleSlug: "developer", projectId: project.id, }); ``` # Configuration The Local provider allows you to manage local filesystem resources in your infrastructure, useful for generating configuration files, storing deployment artifacts, and managing sensitive credentials. ## Provider Configuration [#provider-configuration] The Local provider doesn't require authentication or API credentials. It's a utility provider that operates on the local filesystem. ### Basic Setup [#basic-setup] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as local from "pulumi-local"; // No provider configuration needed // Resources can be used directly const file = new local.File("example", { filename: "/tmp/example.txt", content: "Hello, world!", }); ``` ### Provider Instance (Optional) [#provider-instance-optional] While not required, you can explicitly create a provider instance: ```typescript const provider = new local.Provider("local-provider", { // No configuration options required }); const file = new local.File("with-provider", { filename: "/tmp/example.txt", content: "Hello, world!", }, { provider }); ``` ## Configuration Reference [#configuration-reference] The Local provider has no required configuration options. All functionality is available without setup. ### Resource Options [#resource-options] Individual resources support the following common options: * **filename**: Path to the file to create or read (required) * **content**: Content to write to the file * **contentBase64**: Base64-encoded content to write * **filePermission**: File permissions (e.g., `"0644"`) * **directoryPermission**: Directory permissions for parent directories (e.g., `"0755"`) ## Use Cases [#use-cases] The Local provider is commonly used for: 1. **Configuration Generation**: Generate config files from Pulumi outputs 2. **Deployment Artifacts**: Write deployment metadata and artifacts 3. **Credential Management**: Store credentials from other providers locally 4. **Template Rendering**: Generate files from templates with dynamic values 5. **Command Execution**: Run local commands to gather information ## File Permissions [#file-permissions] ### Standard Files [#standard-files] ```typescript const configFile = new local.File("config", { filename: "/tmp/app.conf", content: "key=value", filePermission: "0644", directoryPermission: "0755", }); ``` ### Sensitive Files [#sensitive-files] Sensitive files automatically use restrictive permissions: ```typescript const secretFile = new local.SensitiveFile("secret", { filename: "/tmp/secret.key", content: "sensitive-data", filePermission: "0600", // Owner read/write only }); ``` ## Integration Examples [#integration-examples] ### With Cloud Providers [#with-cloud-providers] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as local from "pulumi-local"; // Write cloud provider outputs to local files const outputs = new local.File("stack-outputs", { filename: "./outputs.json", content: pulumi.interpolate`{ "stack": "${pulumi.getStack()}", "project": "${pulumi.getProject()}" }`, }); ``` ### With CI/CD Pipelines [#with-cicd-pipelines] ```typescript import * as local from "pulumi-local"; // Generate CI configuration const ciConfig = new local.File("ci-config", { filename: "./.generated/ci-vars.env", content: `DEPLOY_ENV=production VERSION=1.0.0 REGION=us-east-1`, }); ``` ## Best Practices [#best-practices] ### Use Absolute Paths [#use-absolute-paths] ```typescript // Good: Absolute path const file = new local.File("config", { filename: "/opt/app/config.json", content: "{}", }); // Caution: Relative paths depend on working directory const relative = new local.File("relative", { filename: "./config.json", content: "{}", }); ``` ### Protect Sensitive Data [#protect-sensitive-data] ```typescript // Use SensitiveFile for credentials const creds = new local.SensitiveFile("creds", { filename: "/etc/app/credentials.json", content: sensitiveContent, filePermission: "0600", }); ``` ### Clean Up Generated Files [#clean-up-generated-files] Generated files are managed by Pulumi state. When a resource is destroyed, the corresponding file is removed. ## Troubleshooting [#troubleshooting] ### Common Issues [#common-issues] **Issue: Permission denied** ``` Error: EACCES: permission denied ``` Solution: Ensure the Pulumi process has write permissions to the target directory. **Issue: Directory not found** ``` Error: ENOENT: no such file or directory ``` Solution: The parent directory must exist. Use `directoryPermission` to create parent directories automatically. **Issue: File not updating** Solution: Verify the `content` input has actually changed. Pulumi only updates resources when inputs differ. ## Next Steps [#next-steps] * [File Resource](/docs/providers/local/file) - Create and manage local files * [Sensitive File Resource](/docs/providers/local/sensitive-file) - Handle sensitive file content # Local Provider The Local provider enables you to manage local filesystem resources like files, sensitive files, and command execution using Pulumi. This provider is dynamically bridged from the [Terraform Local Provider](https://registry.terraform.io/providers/hashicorp/local). ## Installation [#installation] Install the Local provider package using your preferred package manager: ```bash bun add pulumi-local ``` ```bash pnpm add pulumi-local ``` ```bash yarn add pulumi-local ``` ```bash npm install pulumi-local ``` ## Configuration [#configuration] No configuration required - the Local provider works out of the box with your local filesystem. ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as local from "pulumi-local"; // Create a local file const config = new local.File("app-config", { filename: "/tmp/config.json", content: JSON.stringify({ environment: "production", debug: false, }), }); export const configPath = config.filename; ``` ## Key Features [#key-features] ### [File Management](/docs/providers/local/file) [#file-management] Create and manage local files with specific content and permissions: ```typescript const readme = new local.File("readme", { filename: "./output/README.md", content: "# My Project\n\nGenerated by Pulumi", }); ``` ### [Sensitive Files](/docs/providers/local/sensitive-file) [#sensitive-files] Handle files containing sensitive data with restricted permissions: ```typescript const credentials = new local.SensitiveFile("credentials", { filename: "/tmp/credentials.json", content: JSON.stringify({ apiKey: "secret-key-value", dbPassword: "secret-password", }), }); ``` ### Read Existing Files [#read-existing-files] Read the content of existing files as data sources: ```typescript const hostname = local.getFile({ filename: "/etc/hostname", }); ``` ### Execute Commands [#execute-commands] Run local commands and capture their output: ```typescript const gitHash = local.getCommand({ command: "git rev-parse HEAD", }); ``` ## Common Use Cases [#common-use-cases] ### Generate Configuration Files [#generate-configuration-files] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as local from "pulumi-local"; const stack = pulumi.getStack(); const envFile = new local.File("env-config", { filename: `.env.${stack}`, content: pulumi.interpolate`NODE_ENV=${stack} API_URL=https://api.${stack}.example.com LOG_LEVEL=${stack === "production" ? "warn" : "debug"}`, }); export const envPath = envFile.filename; ``` ### Write Deployment Artifacts [#write-deployment-artifacts] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as local from "pulumi-local"; // Write deployment metadata const metadata = new local.File("deploy-metadata", { filename: "./deploy/metadata.json", content: pulumi.interpolate`{ "stack": "${pulumi.getStack()}", "project": "${pulumi.getProject()}", "timestamp": "${new Date().toISOString()}" }`, }); ``` ### Store Sensitive Credentials [#store-sensitive-credentials] ```typescript import * as local from "pulumi-local"; const kubeconfig = new local.SensitiveFile("kubeconfig", { filename: "~/.kube/config", content: kubeconfigContent, // From another provider }); ``` ## Resource Types [#resource-types] | Resource | Description | | ----------------------------------------------------- | -------------------------------------------------------------- | | [File](/docs/providers/local/file) | Create and manage local files | | [SensitiveFile](/docs/providers/local/sensitive-file) | Create files with sensitive content and restricted permissions | ## Data Sources [#data-sources] | Data Source | Description | | ---------------- | ------------------------------------------ | | getFile | Read content of an existing local file | | getSensitiveFile | Read content of an existing sensitive file | | getCommand | Execute a local command and capture output | ## Getting Help [#getting-help] * [Terraform Local Provider](https://registry.terraform.io/providers/hashicorp/local/latest/docs) * [Pulumi Documentation](https://www.pulumi.com/docs/) * [Configuration Guide](/docs/providers/local/configuration) # Logtail Provider The Logtail provider enables you to manage log sources, metrics, and analytics resources in Logtail using Pulumi. This provider is dynamically bridged from the [Terraform Logtail Provider](https://registry.terraform.io/providers/BetterStackHQ/logtail). ## Installation [#installation] Install the Logtail provider package using your preferred package manager: ```bash bun add pulumi-logtail ``` ```bash pnpm add pulumi-logtail ``` ```bash yarn add pulumi-logtail ``` ```bash npm install pulumi-logtail ``` ## Configuration [#configuration] ### Getting API Token [#getting-api-token] 1. Log in to Logtail at [logs.betterstack.com](https://logs.betterstack.com) 2. Navigate to Settings → API Tokens 3. Create a new API token 4. Copy the token value ### Provider Setup [#provider-setup] ```bash pulumi config set logtail:apiToken YOUR_API_TOKEN --secret ``` Or using environment variables: ```bash export LOGTAIL_API_TOKEN="your-api-token" ``` ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as logtail from "pulumi-logtail"; // Create a log source const source = new logtail.Source("app-logs", { name: "Application Logs", platform: "docker", }); export const sourceToken = source.token; ``` ## Key Features [#key-features] ### Log Sources [#log-sources] ```typescript const dockerSource = new logtail.Source("docker", { name: "Docker Containers", platform: "docker", }); const syslogSource = new logtail.Source("syslog", { name: "System Logs", platform: "syslog", }); ``` ### Linking an AWS Account [#linking-an-aws-account] For `aws` platform sources, attach the AWS account that Better Stack pulls logs from with `logtail.SourceAwsAccount` (added in 10.14.0). Connect a new account using the Better Stack CloudFormation stack outputs, or reuse one you've already connected via `awsAccountId`: ```typescript const awsSource = new logtail.Source("aws-logs", { name: "AWS Logs", platform: "aws", }); new logtail.SourceAwsAccount("aws-account", { sourceId: awsSource.id, awsRoleArn: "arn:aws:iam::123456789012:role/BetterStackIntegration", awsExternalId: "cf-stack-external-id", }); ``` ### Collectors and Targets [#collectors-and-targets] Collectors are agents that run inside your infrastructure to collect metrics from databases and processes. Use `logtail.CollectorTarget` (added in 10.11.3) to attach scrape targets to a collector: ```typescript const collector = new logtail.Collector("infra-collector", { name: "Infra Collector", dataRegion: "eu", }); new logtail.CollectorTarget("postgres-primary", { collectorId: collector.id, kind: "postgres", host: "db.internal", port: 5432, username: "metrics", password: "secret", }); new logtail.CollectorTarget("nginx-edge", { collectorId: collector.id, kind: "nginx", collectorHost: "edge-01.internal", endpoint: "http://127.0.0.1/nginx_status", }); ``` ### Dashboards [#dashboards] ```typescript const dashboard = new logtail.Dashboard("errors-overview", { name: "Errors Overview", }); ``` # Configuration This guide covers all configuration options for the Namecheap provider, including authentication methods, API access setup, and provider settings. ## Provider Configuration [#provider-configuration] The Namecheap provider requires API credentials to interact with the Namecheap API. You can configure the provider using one of several methods. ### Provider Block [#provider-block] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi/namecheap"; const provider = new namecheap.Provider("namecheap-provider", { userName: "your_username", apiUser: "your_username", apiKey: "your_api_key", clientIp: "your.ip.address", useSandbox: false, }); ``` ### Environment Variables [#environment-variables] Set environment variables to configure the provider without hardcoding credentials: ```bash export NAMECHEAP_USER_NAME="your_username" export NAMECHEAP_API_USER="your_username" export NAMECHEAP_API_KEY="your_api_key" export NAMECHEAP_CLIENT_IP="your.ip.address" export NAMECHEAP_USE_SANDBOX="false" ``` ### Pulumi Configuration [#pulumi-configuration] Store credentials securely using Pulumi config: ```bash pulumi config set namecheap:userName your_username pulumi config set namecheap:apiUser your_username pulumi config set --secret namecheap:apiKey your_api_key pulumi config set namecheap:clientIp your.ip.address pulumi config set namecheap:useSandbox false ``` Then reference in your code: ```typescript const config = new pulumi.Config("namecheap"); const provider = new namecheap.Provider("namecheap-provider", { userName: config.require("userName"), apiUser: config.require("apiUser"), apiKey: config.requireSecret("apiKey"), clientIp: config.require("clientIp"), useSandbox: config.requireBoolean("useSandbox"), }); ``` ## Configuration Reference [#configuration-reference] ### Required Arguments [#required-arguments] * **`userName`** (string): Your Namecheap username * **`apiUser`** (string): API username (typically the same as userName) * **`apiKey`** (string): Your Namecheap API key * **`clientIp`** (string): Your whitelisted IP address ### Optional Arguments [#optional-arguments] * **`useSandbox`** (boolean): Use the Namecheap sandbox environment. Default: `false` ## Getting API Credentials [#getting-api-credentials] ### Enable API Access [#enable-api-access] 1. Log in to your [Namecheap account](https://www.namecheap.com/) 2. Navigate to **Profile** → **Tools** → **Business & Dev Tools** → **API Access** 3. Enable API Access 4. Whitelist your IP address 5. Copy your API key ### Sandbox Environment [#sandbox-environment] For testing, you can use the Namecheap sandbox: 1. Create a sandbox account at [Namecheap Sandbox](https://www.sandbox.namecheap.com/) 2. Enable API access in the sandbox 3. Set `useSandbox: true` in your provider configuration ## IP Whitelisting [#ip-whitelisting] Namecheap requires you to whitelist the IP address that will make API calls: 1. Go to **Profile** → **Tools** → **API Access** 2. Click **Whitelist IP** 3. Add your public IP address **Important**: If your IP changes, you must update the whitelist. ### Finding Your Public IP [#finding-your-public-ip] ```bash curl ifconfig.me ``` ## Authentication Methods Comparison [#authentication-methods-comparison] | Method | Security | Ease of Use | CI/CD Friendly | Best For | | --------------------- | -------- | ----------- | -------------- | ----------------- | | Provider Block | Low | Easy | No | Local Development | | Environment Variables | Medium | Medium | Yes | CI/CD Pipelines | | Pulumi Config | High | Easy | Yes | Production | ## Production Best Practices [#production-best-practices] ### Secret Management [#secret-management] ```typescript // ✅ Good: Use Pulumi secrets const config = new pulumi.Config("namecheap"); const apiKey = config.requireSecret("apiKey"); // ❌ Bad: Hardcode secrets const apiKey = "abc123..."; ``` ### Provider Aliasing [#provider-aliasing] Create multiple provider instances for different accounts: ```typescript const productionProvider = new namecheap.Provider("prod", { userName: config.require("prodUserName"), apiUser: config.require("prodApiUser"), apiKey: config.requireSecret("prodApiKey"), clientIp: config.require("prodClientIp"), }); const stagingProvider = new namecheap.Provider("staging", { userName: config.require("stagingUserName"), apiUser: config.require("stagingApiUser"), apiKey: config.requireSecret("stagingApiKey"), clientIp: config.require("stagingClientIp"), useSandbox: true, }); // Use different providers for different resources const prodDns = new namecheap.DomainRecords("prod-dns", { domain: "example.com", // ... }, { provider: productionProvider }); const stagingDns = new namecheap.DomainRecords("staging-dns", { domain: "staging.example.com", // ... }, { provider: stagingProvider }); ``` ## Troubleshooting [#troubleshooting] ### Common Configuration Errors [#common-configuration-errors] **Error: IP not whitelisted** ``` Error: Your IP address is not whitelisted ``` Solution: Add your IP to the whitelist in Namecheap dashboard **Error: Invalid API credentials** ``` Error: Authentication failed ``` Solution: Verify your apiKey, userName, and apiUser are correct **Error: Sandbox mode mismatch** ``` Error: Domain not found ``` Solution: Ensure `useSandbox` matches where your domain is registered (production vs sandbox) ### Testing Configuration [#testing-configuration] Test your configuration with a simple program: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi/namecheap"; // This will validate your credentials on pulumi up const test = new namecheap.DomainRecords("test", { domain: "yourdomain.com", mode: "MERGE", records: [], }); export const configured = "Namecheap provider is configured correctly"; ``` ## Next Steps [#next-steps] * [Domain Records Resource](/docs/providers/namecheap/domain-records) - Manage DNS records * [DNS Management Guide](/docs/providers/namecheap/dns-guide) - Best practices for DNS * [Migration Guide](/docs/providers/namecheap/migration-guide) - Migrate existing domains # DNS Management Guide This guide covers DNS management strategies, best practices, and common patterns when using the Namecheap provider. ## DNS Record Management Strategies [#dns-record-management-strategies] ### OVERWRITE vs MERGE Mode [#overwrite-vs-merge-mode] The Namecheap provider supports two modes for managing DNS records: **OVERWRITE Mode** (Recommended for Infrastructure as Code) * Replaces all existing records with those defined in your code * Provides full control and predictability * Any manual changes will be overwritten on next deployment ```typescript const dns = new namecheap.DomainRecords("example-com", { domain: "example.com", mode: "OVERWRITE", records: [ { hostname: "@", type: "A", address: "192.0.2.1" }, { hostname: "www", type: "CNAME", address: "example.com." }, ], }); ``` **MERGE Mode** (Use with Caution) * Adds or updates records without removing existing ones * Allows manual and automated management * Can lead to configuration drift ```typescript const additionalDns = new namecheap.DomainRecords("additional-records", { domain: "example.com", mode: "MERGE", records: [ { hostname: "api", type: "A", address: "192.0.2.2" }, ], }); ``` ## Common DNS Patterns [#common-dns-patterns] ### Multi-Region Setup with Health Checking [#multi-region-setup-with-health-checking] Configure DNS for a multi-region application: ```typescript const primaryRegion = new namecheap.DomainRecords("multi-region-dns", { domain: "example.com", mode: "OVERWRITE", records: [ // Primary region { hostname: "@", type: "A", address: "192.0.2.1", ttl: 300 // Low TTL for quick failover }, // Secondary region (manual failover) { hostname: "backup", type: "A", address: "192.0.2.2", ttl: 300 }, // Region-specific endpoints { hostname: "us-east", type: "A", address: "192.0.2.1", }, { hostname: "eu-west", type: "A", address: "192.0.2.3", }, ], }); ``` ### Subdomain Delegation [#subdomain-delegation] Delegate a subdomain to different nameservers: ```typescript const mainDomain = new namecheap.DomainRecords("main-domain", { domain: "example.com", mode: "OVERWRITE", records: [ // Delegate dev.example.com to different nameservers { hostname: "dev", type: "NS", address: "ns1.development.example.com.", }, { hostname: "dev", type: "NS", address: "ns2.development.example.com.", }, ], }); ``` ### Email Configuration [#email-configuration] Set up email with MX records and SPF: ```typescript const emailSetup = new namecheap.DomainRecords("email-setup", { domain: "example.com", mode: "OVERWRITE", emailType: "MX", records: [ // MX records for Google Workspace { hostname: "@", type: "MX", address: "ASPMX.L.GOOGLE.COM.", mxPref: 1, }, { hostname: "@", type: "MX", address: "ALT1.ASPMX.L.GOOGLE.COM.", mxPref: 5, }, { hostname: "@", type: "MX", address: "ALT2.ASPMX.L.GOOGLE.COM.", mxPref: 5, }, // SPF record { hostname: "@", type: "TXT", address: "v=spf1 include:_spf.google.com ~all", }, // DKIM record { hostname: "google._domainkey", type: "TXT", address: "v=DKIM1; k=rsa; p=MIGfMA0GCS...", }, // DMARC record { hostname: "_dmarc", type: "TXT", address: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com", }, ], }); ``` ### CDN and Load Balancer Setup [#cdn-and-load-balancer-setup] Configure DNS for CDN and load balancer: ```typescript const cdnSetup = new namecheap.DomainRecords("cdn-setup", { domain: "example.com", mode: "OVERWRITE", records: [ // Main site through CDN { hostname: "@", type: "CNAME", address: "example.cdn.cloudflare.net.", }, { hostname: "www", type: "CNAME", address: "example.cdn.cloudflare.net.", }, // Assets subdomain { hostname: "assets", type: "CNAME", address: "assets.example.cdn.net.", }, // API through load balancer { hostname: "api", type: "A", address: "192.0.2.10", }, ], }); ``` ### Development and Staging Environments [#development-and-staging-environments] Organize DNS for multiple environments: ```typescript const productionDns = new namecheap.DomainRecords("production", { domain: "example.com", mode: "OVERWRITE", records: [ { hostname: "@", type: "A", address: prodIp }, { hostname: "www", type: "CNAME", address: "example.com." }, { hostname: "api", type: "A", address: prodApiIp }, ], }); const stagingDns = new namecheap.DomainRecords("staging", { domain: "example.com", mode: "MERGE", // Won't conflict with production records: [ { hostname: "staging", type: "A", address: stagingIp }, { hostname: "staging-api", type: "A", address: stagingApiIp }, ], }); const devDns = new namecheap.DomainRecords("development", { domain: "example.com", mode: "MERGE", records: [ { hostname: "dev", type: "A", address: devIp }, { hostname: "dev-api", type: "A", address: devApiIp }, ], }); ``` ## TTL Configuration Best Practices [#ttl-configuration-best-practices] ### Choosing the Right TTL [#choosing-the-right-ttl] | Use Case | Recommended TTL | Reason | | ------------------- | ----------------------- | --------------------------------------- | | Production (stable) | 3600-86400 (1-24 hours) | Reduce DNS queries, improve performance | | Pre-migration | 300-600 (5-10 minutes) | Quick switchover during migration | | Active deployment | 60-300 (1-5 minutes) | Fast updates during changes | | Development/Testing | 60 (1 minute) | Rapid iteration | ```typescript const stableDns = new namecheap.DomainRecords("stable", { domain: "example.com", mode: "OVERWRITE", records: [ { hostname: "@", type: "A", address: "192.0.2.1", ttl: 3600, // 1 hour for stable production }, ], }); const migrationDns = new namecheap.DomainRecords("migration", { domain: "example.com", mode: "OVERWRITE", records: [ { hostname: "@", type: "A", address: "192.0.2.1", ttl: 300, // 5 minutes for quick switchover }, ], }); ``` ## Security Best Practices [#security-best-practices] ### CAA Records for Certificate Authority Authorization [#caa-records-for-certificate-authority-authorization] ```typescript const securityDns = new namecheap.DomainRecords("security", { domain: "example.com", mode: "OVERWRITE", records: [ // Allow Let's Encrypt { hostname: "@", type: "CAA", address: "0 issue \"letsencrypt.org\"", }, // Allow DigiCert { hostname: "@", type: "CAA", address: "0 issue \"digicert.com\"", }, // Incident reporting { hostname: "@", type: "CAA", address: "0 iodef \"mailto:security@example.com\"", }, ], }); ``` ### Wildcard Certificates [#wildcard-certificates] ```typescript const wildcardDns = new namecheap.DomainRecords("wildcard", { domain: "example.com", mode: "OVERWRITE", records: [ // Wildcard for all subdomains { hostname: "*", type: "A", address: "192.0.2.1", }, // Specific subdomain override { hostname: "api", type: "A", address: "192.0.2.2", }, ], }); ``` ## DNS Verification and Testing [#dns-verification-and-testing] ### Verification Commands [#verification-commands] After deploying DNS changes, verify with these commands: ```bash # Check A record dig example.com A +short # Check CNAME record dig www.example.com CNAME +short # Check MX records dig example.com MX +short # Check TXT records dig example.com TXT +short # Check nameservers dig example.com NS +short # Query specific DNS server dig @8.8.8.8 example.com A +short ``` ### DNS Propagation [#dns-propagation] DNS changes can take time to propagate: ```typescript import * as pulumi from "@pulumi/pulumi"; // Export DNS records for verification export const dnsRecords = dns.records.apply(records => records.map(r => `${r.hostname}.example.com -> ${r.address}`) ); // Add a note about propagation export const note = "DNS changes may take up to 48 hours to fully propagate"; ``` ## Common Issues and Solutions [#common-issues-and-solutions] ### Issue: CNAME at Root Domain [#issue-cname-at-root-domain] **Problem**: Cannot create CNAME record at root (@) domain ```typescript // ❌ This will fail { hostname: "@", type: "CNAME", address: "example.com.", } ``` **Solution**: Use A record or ALIAS record instead ```typescript // ✅ Use A record { hostname: "@", type: "A", address: "192.0.2.1", } ``` ### Issue: Conflicting Records [#issue-conflicting-records] **Problem**: Multiple records of different types for same hostname **Solution**: Use different hostnames or consolidate records ```typescript // ✅ Correct: Different hostnames const records = [ { hostname: "@", type: "A", address: "192.0.2.1" }, { hostname: "www", type: "CNAME", address: "example.com." }, ]; ``` ### Issue: Email Delivery Problems [#issue-email-delivery-problems] **Problem**: Emails not being delivered **Solution**: Verify MX, SPF, DKIM, and DMARC records ```bash # Check MX records dig example.com MX # Check SPF dig example.com TXT | grep spf # Test email configuration https://mxtoolbox.com/ ``` ## Monitoring and Alerting [#monitoring-and-alerting] ### DNS Health Checks [#dns-health-checks] Use Pulumi to create monitoring for DNS records: ```typescript // Example integration with monitoring service const dnsMonitor = dns.records.apply(records => { records.forEach(record => { // Set up monitoring for each critical record console.log(`Monitor: ${record.hostname}.example.com`); }); }); ``` ## Next Steps [#next-steps] * [Domain Records Resource](/docs/providers/namecheap/domain-records) - Full API reference * [Configuration Guide](/docs/providers/namecheap/configuration) - Provider setup * [Migration Guide](/docs/providers/namecheap/migration-guide) - Import existing domains # DomainRecords The `DomainRecords` resource allows you to manage DNS records for a domain registered on Namecheap. ## Example Usage [#example-usage] ### Basic A Record [#basic-a-record] ```typescript import * as namecheap from "@pulumi-any-terraform/namecheap"; const records = new namecheap.DomainRecords("my-domain", { domain: "example.com", records: [ { hostname: "@", type: "A", address: "192.0.2.1", ttl: 300, }, ], }); ``` ### Complete DNS Configuration [#complete-dns-configuration] ```typescript import * as namecheap from "@pulumi-any-terraform/namecheap"; const dnsConfig = new namecheap.DomainRecords("complete-dns", { domain: "example.com", mode: "OVERWRITE", emailType: "MX", records: [ // Root domain A record { hostname: "@", type: "A", address: "192.0.2.1", ttl: 3600, }, // WWW subdomain { hostname: "www", type: "CNAME", address: "example.com", ttl: 3600, }, // Mail exchanger { hostname: "@", type: "MX", address: "mail.example.com", mxPref: 10, ttl: 3600, }, { hostname: "@", type: "MX", address: "mail2.example.com", mxPref: 20, ttl: 3600, }, // TXT record for SPF { hostname: "@", type: "TXT", address: "v=spf1 include:_spf.example.com ~all", ttl: 3600, }, // API subdomain { hostname: "api", type: "A", address: "192.0.2.2", ttl: 1800, }, ], }); export const recordsId = dnsConfig.domainRecordsId; ``` ### Multiple Environments [#multiple-environments] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi-any-terraform/namecheap"; const stack = pulumi.getStack(); const domain = "example.com"; // Production uses root domain const prodRecords = stack === "production" ? new namecheap.DomainRecords("prod-dns", { domain: domain, mode: "MERGE", records: [ { hostname: "@", type: "A", address: "203.0.113.10", ttl: 3600 }, { hostname: "www", type: "A", address: "203.0.113.10", ttl: 3600 }, ], }) : undefined; // Staging uses subdomain const stagingRecords = stack === "staging" ? new namecheap.DomainRecords("staging-dns", { domain: domain, mode: "MERGE", records: [ { hostname: "staging", type: "A", address: "203.0.113.20", ttl: 600 }, { hostname: "staging-api", type: "A", address: "203.0.113.21", ttl: 600 }, ], }) : undefined; ``` ### Custom Nameservers [#custom-nameservers] ```typescript import * as namecheap from "@pulumi-any-terraform/namecheap"; const customNs = new namecheap.DomainRecords("custom-nameservers", { domain: "example.com", nameservers: [ "ns1.customdns.com", "ns2.customdns.com", "ns3.customdns.com", ], }); ``` ### CNAME Records [#cname-records] ```typescript import * as namecheap from "@pulumi-any-terraform/namecheap"; const cnameRecords = new namecheap.DomainRecords("cname-setup", { domain: "example.com", mode: "MERGE", records: [ { hostname: "blog", type: "CNAME", address: "myblog.platform.com", ttl: 300, }, { hostname: "shop", type: "CNAME", address: "mystore.shopify.com", ttl: 300, }, ], }); ``` ## Resource Arguments [#resource-arguments] ### Required Arguments [#required-arguments] * **domain** (string, required)\ The purchased domain name on your Namecheap account (e.g., "example.com") ### Optional Arguments [#optional-arguments] * **domainRecordsId** (string, optional)\ Custom identifier for the domain records resource * **emailType** (string, optional)\ Email forwarding type for the domain\ **Possible values**: `NONE`, `MXE`, `MX`, `FWD`, `OX`, `GMAIL`\ **Default**: `NONE` * **mode** (string, optional)\ How to handle existing records\ **Possible values**: * `MERGE` (default): Merge new records with existing records * `OVERWRITE`: Replace all existing records with new records **Default**: `MERGE` * **nameservers** (string\[], optional)\ Custom nameservers for the domain. When specified, this overrides Namecheap's default nameservers.\ **Example**: `["ns1.example.com", "ns2.example.com"]` * **records** (Record\[], optional)\ Array of DNS records to create. See [Record Object](#record-object) below. ### Record Object [#record-object] Each record in the `records` array supports the following properties: * **address** (string, required)\ The value for the DNS record. Can be an IP address or hostname depending on record type.\ **Examples**: * A record: `"192.0.2.1"` * CNAME record: `"example.com"` * MX record: `"mail.example.com"` * TXT record: `"v=spf1 include:_spf.example.com ~all"` * **hostname** (string, required)\ The subdomain or hostname for the record. Use `"@"` for the root domain.\ **Examples**: `"@"`, `"www"`, `"api"`, `"mail"` * **type** (string, required)\ The DNS record type\ **Possible values**: `A`, `AAAA`, `ALIAS`, `CAA`, `CNAME`, `MX`, `MXE`, `NS`, `TXT`, `URL`, `URL301`, `FRAME` * **mxPref** (number, optional)\ MX preference (priority) for the mail server. Lower values have higher priority.\ **Required for MX records only**\ **Typical values**: `10`, `20`, `30` * **ttl** (number, optional)\ Time to live in seconds - how long DNS resolvers should cache this record\ **Range**: `60` to `60000` seconds\ **Common values**: * `300` (5 minutes): For frequently changing records * `1800` (30 minutes): For moderately stable records * `3600` (1 hour): For stable records * `86400` (24 hours): For very stable records ## Resource Attributes [#resource-attributes] The following attributes are exported by the resource: * **domain** (string)\ The domain name * **domainRecordsId** (string)\ The unique identifier for the domain records resource * **emailType** (string)\ The configured email type * **mode** (string)\ The merge mode used * **nameservers** (string\[])\ The configured nameservers * **records** (Record\[])\ The configured DNS records ## Import [#import] Existing domain records can be imported using the domain name: ```bash pulumi import namecheap:index/domainRecords:DomainRecords my-records example.com ``` ## Record Type Reference [#record-type-reference] ### A Record [#a-record] Maps a hostname to an IPv4 address. ```typescript { hostname: "@", type: "A", address: "192.0.2.1", ttl: 3600, } ``` ### AAAA Record [#aaaa-record] Maps a hostname to an IPv6 address. ```typescript { hostname: "@", type: "AAAA", address: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ttl: 3600, } ``` ### CNAME Record [#cname-record] Creates an alias from one domain name to another. ```typescript { hostname: "www", type: "CNAME", address: "example.com", ttl: 3600, } ``` **Note**: CNAME records cannot be used for the root domain (@). ### MX Record [#mx-record] Specifies mail servers for the domain. ```typescript { hostname: "@", type: "MX", address: "mail.example.com", mxPref: 10, ttl: 3600, } ``` ### TXT Record [#txt-record] Stores text information for various purposes (SPF, DKIM, domain verification). ```typescript { hostname: "@", type: "TXT", address: "v=spf1 include:_spf.example.com ~all", ttl: 3600, } ``` ### NS Record [#ns-record] Delegates a subdomain to different nameservers. ```typescript { hostname: "subdomain", type: "NS", address: "ns1.subdomain-host.com", ttl: 3600, } ``` ### CAA Record [#caa-record] Specifies which certificate authorities can issue certificates for the domain. ```typescript { hostname: "@", type: "CAA", address: "0 issue \"letsencrypt.org\"", ttl: 3600, } ``` ### URL/URL301 Records [#urlurl301-records] Creates URL redirects (Namecheap-specific feature). ```typescript { hostname: "old-page", type: "URL301", address: "https://example.com/new-page", ttl: 300, } ``` ## Best Practices [#best-practices] ### TTL Selection [#ttl-selection] * **Development/Testing**: 300-600 seconds (5-10 minutes) * **Staging**: 1800 seconds (30 minutes) * **Production (stable)**: 3600+ seconds (1+ hours) * **Before major changes**: Lower TTL 24-48 hours before planned changes ### Record Organization [#record-organization] Group related records together: ```typescript const records = [ // Web servers { hostname: "@", type: "A", address: "192.0.2.1", ttl: 3600 }, { hostname: "www", type: "A", address: "192.0.2.1", ttl: 3600 }, // Mail servers { hostname: "@", type: "MX", address: "mail1.example.com", mxPref: 10, ttl: 3600 }, { hostname: "@", type: "MX", address: "mail2.example.com", mxPref: 20, ttl: 3600 }, // Email authentication { hostname: "@", type: "TXT", address: "v=spf1 include:_spf.example.com ~all", ttl: 3600 }, { hostname: "_dmarc", type: "TXT", address: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com", ttl: 3600 }, // Services { hostname: "api", type: "A", address: "192.0.2.2", ttl: 1800 }, { hostname: "cdn", type: "CNAME", address: "cdn.provider.com", ttl: 3600 }, ]; ``` ### Mode Selection [#mode-selection] * **OVERWRITE**: Use when you want complete control and Pulumi should manage ALL records * **MERGE**: Use when you want to manage specific records while preserving others (e.g., manual records or records managed elsewhere) ### Security Considerations [#security-considerations] * Keep production domains separate from test domains * Use appropriate SPF, DKIM, and DMARC records for email security * Consider using CAA records to restrict certificate issuance * Monitor DNS changes through Pulumi state ## Common Patterns [#common-patterns] ### Multi-Region Setup [#multi-region-setup] ```typescript import * as namecheap from "@pulumi-any-terraform/namecheap"; const regions = [ { hostname: "us", ip: "192.0.2.1" }, { hostname: "eu", ip: "198.51.100.1" }, { hostname: "asia", ip: "203.0.113.1" }, ]; const regionalDns = new namecheap.DomainRecords("regional-dns", { domain: "example.com", mode: "MERGE", records: regions.map(region => ({ hostname: region.hostname, type: "A", address: region.ip, ttl: 300, })), }); ``` ### Dynamic Records from Stack Outputs [#dynamic-records-from-stack-outputs] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi-any-terraform/namecheap"; import * as aws from "@pulumi/aws"; // Assume we have an ELB or other resource const loadBalancer = new aws.lb.LoadBalancer("app-lb", { // ... configuration }); const dnsRecords = new namecheap.DomainRecords("app-dns", { domain: "example.com", mode: "MERGE", records: [ { hostname: "app", type: "CNAME", address: loadBalancer.dnsName, ttl: 300, }, ], }); ``` ## Troubleshooting [#troubleshooting] ### Common Errors [#common-errors] **Error: Domain not found** ``` Solution: Ensure the domain is purchased and active in your Namecheap account ``` **Error: Invalid record type** ``` Solution: Check that the record type is one of the supported values (A, AAAA, CNAME, MX, TXT, etc.) ``` **Error: CNAME conflict** ``` Solution: CNAME records cannot coexist with other record types for the same hostname ``` **Error: Invalid TTL value** ``` Solution: TTL must be between 60 and 60000 seconds ``` ### DNS Not Updating [#dns-not-updating] 1. Check the TTL of the previous record - changes may be cached 2. Verify the record was actually updated in Namecheap's control panel 3. Use `dig` or online DNS checkers to verify propagation 4. Allow up to 48 hours for complete global propagation (though usually much faster) ### Verification [#verification] Check your DNS records after deployment: ```bash # Check A record dig example.com A # Check MX records dig example.com MX # Check TXT records dig example.com TXT # Check specific subdomain dig api.example.com A ``` # Namecheap Provider The Namecheap provider allows you to manage DNS records and domain configurations on Namecheap through Pulumi's infrastructure as code approach. ## Overview [#overview] The Namecheap provider bridges the [Terraform Namecheap provider](https://registry.terraform.io/providers/namecheap/namecheap/latest/docs) to Pulumi, enabling you to: * Manage DNS records for your domains * Configure email forwarding and MX records * Use custom nameservers * Automate domain configuration ## Installation [#installation] Install the Namecheap provider package using your preferred package manager: ```bash bun add @pulumi-any-terraform/namecheap ``` ```bash pnpm add @pulumi-any-terraform/namecheap ``` ```bash yarn add @pulumi-any-terraform/namecheap ``` ```bash npm install @pulumi-any-terraform/namecheap ``` ## Configuration [#configuration] The provider requires authentication with Namecheap's API. You need: * **API Key**: Your Namecheap API key * **API User**: Your API username (often same as your account username) * **Username**: Your Namecheap account username * **Client IP** (optional): Your whitelisted IP address for API access ### Getting API Credentials [#getting-api-credentials] 1. Log in to your Namecheap account 2. Navigate to Profile → Tools → API Access 3. Enable API access and whitelist your IP address 4. Generate an API key ### Configuration Options [#configuration-options] Configure the provider using one of these methods: #### Method 1: Configuration Block [#method-1-configuration-block] ```typescript import * as namecheap from "@pulumi-any-terraform/namecheap"; const provider = new namecheap.Provider("namecheap-provider", { apiKey: "your-api-key", apiUser: "your-api-username", userName: "your-username", clientIp: "your-ip-address", // Optional useSandbox: false, // Set to true for testing }); ``` #### Method 2: Environment Variables [#method-2-environment-variables] ```bash export NAMECHEAP_API_KEY="your-api-key" export NAMECHEAP_API_USER="your-api-username" export NAMECHEAP_USER_NAME="your-username" export NAMECHEAP_CLIENT_IP="your-ip-address" export NAMECHEAP_USE_SANDBOX="false" ``` #### Method 3: Pulumi Configuration [#method-3-pulumi-configuration] ```bash pulumi config set namecheap:apiKey "your-api-key" --secret pulumi config set namecheap:apiUser "your-api-username" pulumi config set namecheap:userName "your-username" pulumi config set namecheap:clientIp "your-ip-address" pulumi config set namecheap:useSandbox false ``` ## Quick Start [#quick-start] Here's a simple example to create DNS records for a domain: ```typescript import * as namecheap from "@pulumi-any-terraform/namecheap"; // Create DNS records for example.com const dnsRecords = new namecheap.DomainRecords("example-dns", { domain: "example.com", mode: "OVERWRITE", // or "MERGE" to merge with existing records emailType: "MX", records: [ { hostname: "@", type: "A", address: "192.0.2.1", ttl: 300, }, { hostname: "www", type: "A", address: "192.0.2.1", ttl: 300, }, { hostname: "@", type: "MX", address: "mail.example.com", mxPref: 10, ttl: 300, }, ], }); export const domainRecordsId = dnsRecords.domainRecordsId; ``` ## Sandbox Mode [#sandbox-mode] For testing and development, enable sandbox mode to use Namecheap's sandbox environment: ```typescript const provider = new namecheap.Provider("namecheap-sandbox", { apiKey: "sandbox-api-key", apiUser: "sandbox-username", userName: "sandbox-username", useSandbox: true, }); ``` ## Resources [#resources] The Namecheap provider includes the following resources: * [DomainRecords](/docs/providers/namecheap/domain-records) - Manage DNS records for a domain ## Best Practices [#best-practices] ### Security [#security] * **Never commit API keys** to source control * Use Pulumi secrets for sensitive configuration * Whitelist only necessary IP addresses for API access * Use separate API keys for different environments ### DNS Management [#dns-management] * **Use TTL wisely**: Lower TTL (300-600 seconds) for frequently changing records, higher TTL (3600+ seconds) for stable records * **OVERWRITE vs MERGE mode**: Use OVERWRITE for full control, MERGE to preserve existing records * **Test in sandbox**: Always test configuration changes in sandbox mode first ### Example Project Structure [#example-project-structure] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi-any-terraform/namecheap"; const config = new pulumi.Config(); const domain = config.require("domain"); // Production DNS configuration const prodDns = new namecheap.DomainRecords("production-dns", { domain: domain, mode: "OVERWRITE", records: [ // Web server { hostname: "@", type: "A", address: "203.0.113.1", ttl: 3600 }, { hostname: "www", type: "A", address: "203.0.113.1", ttl: 3600 }, // Mail server { hostname: "@", type: "MX", address: "mail.example.com", mxPref: 10, ttl: 3600 }, // API subdomain { hostname: "api", type: "A", address: "203.0.113.2", ttl: 1800 }, // TXT records for verification { hostname: "@", type: "TXT", address: "v=spf1 include:_spf.example.com ~all", ttl: 3600 }, ], }); ``` ## Troubleshooting [#troubleshooting] ### API Access Issues [#api-access-issues] **Error: Invalid API credentials** * Verify API key, API user, and username are correct * Ensure your IP address is whitelisted in Namecheap settings **Error: Domain not found** * Confirm the domain is purchased and active in your Namecheap account * Check for typos in the domain name ### DNS Propagation [#dns-propagation] DNS changes may take time to propagate: * **TTL determines cache time**: If previous TTL was 3600 seconds, changes may take up to 1 hour to fully propagate * **Use DNS tools**: Check propagation with tools like `dig` or online DNS checkers * **Lower TTL before changes**: If planning major DNS changes, lower TTL 24-48 hours in advance ## Additional Resources [#additional-resources] * [Namecheap API Documentation](https://www.namecheap.com/support/api/) * [Terraform Namecheap Provider](https://registry.terraform.io/providers/namecheap/namecheap/latest/docs) * [Namecheap Knowledge Base](https://www.namecheap.com/support/knowledgebase/) # Migration Guide This guide explains how to import existing Namecheap domains and DNS records into Pulumi, allowing you to manage them as Infrastructure as Code. ## Why Migrate to Pulumi? [#why-migrate-to-pulumi] * **Version Control**: Track all DNS changes in Git * **Reproducibility**: Recreate environments easily * **Collaboration**: Team members can review and approve changes * **Automation**: Integrate with CI/CD pipelines * **Documentation**: Code serves as living documentation ## Pre-Migration Checklist [#pre-migration-checklist] Before migrating, ensure you have: * [ ] Namecheap API access enabled * [ ] API credentials (username, API key) * [ ] IP address whitelisted * [ ] Backup of current DNS settings * [ ] List of domains to migrate * [ ] Understanding of current DNS configuration ### Backup Current Configuration [#backup-current-configuration] Export your current DNS records manually or via API: ```bash # Using dig to document current records dig example.com ANY +noall +answer > example.com-backup.txt # Check MX records dig example.com MX +short >> example.com-backup.txt # Check TXT records dig example.com TXT +short >> example.com-backup.txt ``` ## Import Strategy [#import-strategy] ### Option 1: Import Existing Resources (Recommended) [#option-1-import-existing-resources-recommended] This approach imports existing DNS records into Pulumi state without changes. **Step 1: Create Pulumi code matching existing configuration** ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi/namecheap"; const exampleDns = new namecheap.DomainRecords("example-com", { domain: "example.com", mode: "OVERWRITE", records: [ { hostname: "@", type: "A", address: "192.0.2.1", }, { hostname: "www", type: "CNAME", address: "example.com.", }, // Add all existing records here ], }); ``` **Step 2: Import the resource** ```bash # Get the resource ID (domain name) pulumi import namecheap:index/domainRecords:DomainRecords example-com example.com ``` **Step 3: Verify the import** ```bash pulumi preview # Should show no changes ``` ### Option 2: Recreate Resources [#option-2-recreate-resources] For simpler setups, you can define resources from scratch: **Step 1: Document existing configuration** Take screenshots or notes of your current Namecheap DNS settings. **Step 2: Create Pulumi program** ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi/namecheap"; const dns = new namecheap.DomainRecords("new-dns", { domain: "example.com", mode: "OVERWRITE", records: [ // Define all records ], }); ``` **Step 3: Apply with caution** ```bash # Preview changes carefully pulumi preview # Apply only when satisfied pulumi up ``` ## Migration Patterns [#migration-patterns] ### Single Domain Migration [#single-domain-migration] Simplest case - one domain with standard DNS: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi/namecheap"; // Export current configuration to review const currentRecords = [ { hostname: "@", type: "A", address: "192.0.2.1" }, { hostname: "www", type: "CNAME", address: "example.com." }, { hostname: "mail", type: "A", address: "192.0.2.2" }, { hostname: "@", type: "MX", address: "mail.example.com.", mxPref: 10 }, { hostname: "@", type: "TXT", address: "v=spf1 mx ~all" }, ]; const dns = new namecheap.DomainRecords("single-domain", { domain: "example.com", mode: "OVERWRITE", records: currentRecords, }); export const domainRecords = currentRecords.length; ``` ### Multiple Domains Migration [#multiple-domains-migration] Organize multiple domains efficiently: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi/namecheap"; // Define shared configuration const commonIp = "192.0.2.1"; const mailServer = "mail.example.com."; interface DomainConfig { domain: string; records: Array<{ hostname: string; type: string; address: string; mxPref?: number; }>; } const domains: DomainConfig[] = [ { domain: "example.com", records: [ { hostname: "@", type: "A", address: commonIp }, { hostname: "www", type: "CNAME", address: "example.com." }, ], }, { domain: "example.org", records: [ { hostname: "@", type: "A", address: commonIp }, { hostname: "www", type: "CNAME", address: "example.org." }, ], }, ]; // Create DNS records for each domain const dnsResources = domains.map(config => new namecheap.DomainRecords(`${config.domain.replace(".", "-")}`, { domain: config.domain, mode: "OVERWRITE", records: config.records, }) ); export const managedDomains = domains.map(d => d.domain); ``` ### Environment-Based Migration [#environment-based-migration] Migrate dev, staging, and production separately: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as namecheap from "@pulumi/namecheap"; const config = new pulumi.Config(); const environment = config.require("environment"); // dev, staging, prod interface EnvironmentConfig { domain: string; ip: string; apiIp: string; } const envConfigs: Record = { dev: { domain: "example.com", ip: "192.0.2.10", apiIp: "192.0.2.11", }, staging: { domain: "example.com", ip: "192.0.2.20", apiIp: "192.0.2.21", }, prod: { domain: "example.com", ip: "192.0.2.1", apiIp: "192.0.2.2", }, }; const envConfig = envConfigs[environment]; const dns = new namecheap.DomainRecords(`${environment}-dns`, { domain: envConfig.domain, mode: environment === "prod" ? "OVERWRITE" : "MERGE", records: [ { hostname: environment === "prod" ? "@" : environment, type: "A", address: envConfig.ip, }, { hostname: environment === "prod" ? "api" : `${environment}-api`, type: "A", address: envConfig.apiIp, }, ], }); export const environmentDns = { environment, domain: envConfig.domain, records: dns.records, }; ``` ## Migration Steps [#migration-steps] ### Phase 1: Preparation (Week 1) [#phase-1-preparation-week-1] 1. **Audit Current Configuration** * Document all DNS records * Identify critical services * Note custom configurations 2. **Set Up Development Environment** ```bash mkdir namecheap-migration cd namecheap-migration pulumi new typescript npm install @pulumi/namecheap ``` 3. **Test in Sandbox** ```typescript const provider = new namecheap.Provider("sandbox", { useSandbox: true, // ... credentials }); ``` ### Phase 2: Non-Critical Domains (Week 2) [#phase-2-non-critical-domains-week-2] 1. **Start with development domains** 2. **Import or recreate resources** 3. **Verify DNS resolution** 4. **Monitor for 24-48 hours** ### Phase 3: Production Domains (Week 3-4) [#phase-3-production-domains-week-3-4] 1. **Lower TTL values** ```typescript // Lower TTL 24-48 hours before migration const dns = new namecheap.DomainRecords("pre-migration", { domain: "example.com", mode: "OVERWRITE", records: records.map(r => ({ ...r, ttl: 300 })), }); ``` 2. **Migrate during low-traffic period** 3. **Monitor closely** 4. **Keep backup plan ready** 5. **Restore normal TTL after 48 hours** ## Rollback Plan [#rollback-plan] Always have a rollback strategy: ```typescript // Store previous configuration in code comments or separate file /* Previous Configuration: @ A 192.0.2.100 www CNAME example.com. */ const rollbackRecords = [ { hostname: "@", type: "A", address: "192.0.2.100" }, { hostname: "www", type: "CNAME", address: "example.com." }, ]; // Keep rollback configuration ready if (config.getBoolean("rollback")) { const rollbackDns = new namecheap.DomainRecords("rollback", { domain: "example.com", mode: "OVERWRITE", records: rollbackRecords, }); } ``` ## Post-Migration Tasks [#post-migration-tasks] ### Verification [#verification] ```bash # Verify DNS resolution dig example.com A +short dig www.example.com CNAME +short dig example.com MX +short # Check from multiple locations # Use online tools like: # - https://dnschecker.org/ # - https://www.whatsmydns.net/ ``` ### Documentation [#documentation] ```typescript // Add exports for documentation export const migrationDate = new Date().toISOString(); export const migratedDomains = ["example.com", "example.org"]; export const dnsRecordCount = dns.records.apply(r => r?.length || 0); ``` ### Monitoring Setup [#monitoring-setup] Set up alerts for DNS changes: ```typescript // Example: Log all DNS changes dns.records.apply(records => { pulumi.log.info(`DNS Records Updated: ${JSON.stringify(records)}`); }); ``` ## Common Migration Issues [#common-migration-issues] ### Issue: Import Fails [#issue-import-fails] **Problem**: Cannot import existing resource ```bash error: resource 'example-com' already exists ``` **Solution**: Use different resource name or remove from state first ```bash pulumi state delete namecheap:index/domainRecords:DomainRecords::example-com ``` ### Issue: Records Not Matching [#issue-records-not-matching] **Problem**: Pulumi shows changes after import **Solution**: Ensure your code exactly matches existing configuration, including: * TTL values (use actual values, not defaults) * Record order (may need to reorder) * Trailing dots on CNAMEs ### Issue: Downtime During Migration [#issue-downtime-during-migration] **Problem**: DNS resolution fails after migration **Solution**: 1. Check Namecheap API responded successfully 2. Verify records in Namecheap dashboard 3. Use low TTL values before migration 4. Have rollback plan ready ## Best Practices [#best-practices] ### Incremental Migration [#incremental-migration] Don't migrate everything at once: ```typescript // Phase 1: Import without changes pulumi import namecheap:index/domainRecords:DomainRecords example-com example.com // Phase 2: Make small changes // Phase 3: Refactor and optimize ``` ### Use Stack Tags [#use-stack-tags] Track migration status: ```bash pulumi stack tag set migration:status "in-progress" pulumi stack tag set migration:phase "phase-2" pulumi stack tag set migration:date "2024-01-15" ``` ### Document Everything [#document-everything] ```typescript // Add comments explaining existing configuration const dns = new namecheap.DomainRecords("example-com", { domain: "example.com", mode: "OVERWRITE", records: [ // Legacy web server - migrating to new IP in Q2 { hostname: "@", type: "A", address: "192.0.2.1" }, // CDN CNAME - added 2023-06-15 { hostname: "www", type: "CNAME", address: "example.cdn.com." }, ], }); ``` ## Next Steps [#next-steps] * [Configuration Guide](/docs/providers/namecheap/configuration) - Set up provider * [DNS Guide](/docs/providers/namecheap/dns-guide) - Best practices * [Domain Records](/docs/providers/namecheap/domain-records) - API reference # OpenFGA Provider The OpenFGA provider enables you to manage [OpenFGA](https://openfga.dev) stores, authorization models, and relationship tuples with Pulumi. OpenFGA is a fine-grained, relationship-based authorization system inspired by [Google Zanzibar](https://research.google/pubs/pub48190/). This provider is dynamically bridged from the [Terraform OpenFGA Provider](https://registry.opentofu.org/openfga/openfga). ## Installation [#installation] Install the OpenFGA provider package using your preferred package manager: ```bash bun add pulumi-openfga ``` ```bash pnpm add pulumi-openfga ``` ```bash yarn add pulumi-openfga ``` ```bash npm install pulumi-openfga ``` ## Configuration [#configuration] The provider supports two authentication modes: a pre-shared API token, or OAuth 2.0 client credentials. ### API Token [#api-token] ```bash pulumi config set openfga:apiUrl https://api.us1.fga.dev pulumi config set openfga:apiToken YOUR_API_TOKEN --secret ``` Or via environment variables: ```bash export FGA_API_URL="https://api.us1.fga.dev" export FGA_API_TOKEN="your-api-token" ``` ### OAuth Client Credentials [#oauth-client-credentials] ```bash pulumi config set openfga:apiUrl https://api.us1.fga.dev pulumi config set openfga:clientId YOUR_CLIENT_ID pulumi config set openfga:clientSecret YOUR_CLIENT_SECRET --secret pulumi config set openfga:apiTokenIssuer https://fga.us.auth0.com pulumi config set openfga:apiAudience https://api.us1.fga.dev/ ``` Equivalent environment variables: `FGA_CLIENT_ID`, `FGA_CLIENT_SECRET`, `FGA_API_TOKEN_ISSUER`, `FGA_API_AUDIENCE`, `FGA_API_SCOPES`. ### Self-Hosted OpenFGA [#self-hosted-openfga] For a self-hosted OpenFGA server, point `apiUrl` at the deployment and supply the matching auth credentials: ```typescript import * as openfga from "pulumi-openfga"; const provider = new openfga.Provider("self-hosted", { apiUrl: "https://openfga.your-domain.com", apiToken: config.requireSecret("openfgaToken"), }); const store = new openfga.Store("app", { name: "app" }, { provider }); ``` ## Quick Start [#quick-start] ```typescript import * as openfga from "pulumi-openfga"; // 1. Create a store. const store = new openfga.Store("docs-app", { name: "docs-app", }); // 2. Define an authorization model from an OpenFGA DSL document. const modelDoc = openfga.getAuthorizationModelDocumentOutput({ dsl: ` model schema 1.1 type user type document relations define viewer: [user] define editor: [user] `, }); const model = new openfga.AuthorizationModel("docs-app-model", { storeId: store.id, modelJson: modelDoc.result, }); // 3. Write a relationship tuple: alice can view document:readme. const tuple = new openfga.RelationshipTuple("alice-can-view-readme", { storeId: store.id, authorizationModelId: model.id, user: "user:alice", relation: "viewer", object: "document:readme", }); export const storeId = store.id; export const modelId = model.id; ``` ## Key Resources [#key-resources] ### Store [#store] A logical container for an authorization model and its relationship tuples. ```typescript const store = new openfga.Store("billing", { name: "billing-service", }); ``` ### Authorization Model [#authorization-model] The schema describing the object types, relations, and rewrite rules. Use `getAuthorizationModelDocument` to author the model in DSL form and convert it to canonical JSON. ```typescript const model = new openfga.AuthorizationModel("billing-model", { storeId: store.id, modelJson: openfga.getAuthorizationModelDocumentOutput({ dsl: ` model schema 1.1 type user type group relations define member: [user] type invoice relations define owner: [user] define viewer: [user, group#member] or owner `, }).result, }); ``` ### Relationship Tuple [#relationship-tuple] A single fact in the form `(user, relation, object)`. Optionally pinned to a specific authorization model. ```typescript const tuple = new openfga.RelationshipTuple("finance-can-view-invoice-42", { storeId: store.id, authorizationModelId: model.id, user: "group:finance#member", relation: "viewer", object: "invoice:42", }); ``` ## Read-Side Data Sources [#read-side-data-sources] The provider exposes data sources for offline checks against an authorization model without writing to the store: `getCheckQuery`, `getListObjectsQuery`, `getListUsersQuery`, plus lookups for stores, models, and tuples (`getStore`, `getAuthorizationModel`, `getRelationshipTuple`, etc.). ```typescript const canRead = openfga.getCheckQueryOutput({ storeId: store.id, authorizationModelId: model.id, tupleKey: { user: "user:alice", relation: "viewer", object: "document:readme", }, }); export const aliceCanRead = canRead.allowed; ``` # Portainer Provider The Portainer provider enables you to manage container environments, stacks, users, and teams in Portainer using Pulumi. This provider is dynamically bridged from the [Terraform Portainer Provider](https://registry.terraform.io/providers/portainer/portainer). ## Installation [#installation] Install the Portainer provider package using your preferred package manager: ```bash bun add pulumi-portainer ``` ```bash pnpm add pulumi-portainer ``` ```bash yarn add pulumi-portainer ``` ```bash npm install pulumi-portainer ``` ## Configuration [#configuration] ### Provider Setup [#provider-setup] ```bash pulumi config set portainer:url https://portainer.example.com pulumi config set portainer:username admin pulumi config set portainer:password YOUR_PASSWORD --secret ``` Or using environment variables: ```bash export PORTAINER_URL="https://portainer.example.com" export PORTAINER_USERNAME="admin" export PORTAINER_PASSWORD="your-password" ``` ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as portainer from "pulumi-portainer"; // Deploy a stack const stack = new portainer.Stack("web-app", { name: "web-application", environmentId: 1, stackFile: ` version: '3' services: web: image: nginx:latest ports: - "80:80" `, }); export const stackId = stack.id; ``` ## Key Features [#key-features] ### Stack Management [#stack-management] ```typescript const appStack = new portainer.Stack("app", { name: "web-application", environmentId: environment.id, stackFile: stackContent, }); ``` ### Environment Management [#environment-management] ```typescript const environment = new portainer.Environment("docker", { name: "Production Docker", type: "docker", url: "tcp://docker.example.com:2375", }); ``` ### User Management [#user-management] ```typescript const user = new portainer.User("developer", { username: "dev-user", password: "secure-password", role: 2, // Standard user }); ``` # Alert The Alert resource allows you to create and manage alerts that notify you when metrics exceed or fall below specified thresholds in PostHog. ## Example Usage [#example-usage] ### Basic Alert [#basic-alert] ```typescript import * as posthog from "pulumi-posthog"; const errorAlert = new posthog.Alert("high-errors", { name: "High Error Rate", // Use the numeric insight ID from PostHog (find in insight URL or API) insight: 12345, enabled: true, thresholdType: "absolute", thresholdUpper: 100, // Array of PostHog user IDs to notify (find in user settings) subscribedUsers: [67890], }); ``` ### Low Signup Alert [#low-signup-alert] ```typescript const signupAlert = new posthog.Alert("low-signups", { name: "Low Signup Alert", // Get insight ID from PostHog insight URL or API insight: 12345, enabled: true, thresholdType: "absolute", thresholdLower: 10, // Alert when below 10 signups // Get user IDs from PostHog user settings or API subscribedUsers: [67890, 67891], }); ``` ### Percentage Change Alert [#percentage-change-alert] ```typescript const growthAlert = new posthog.Alert("growth-alert", { name: "Revenue Growth Alert", insight: 23456, enabled: true, thresholdType: "percentage", thresholdLower: -10, // Alert on 10% decrease thresholdUpper: 50, // Alert on 50% increase subscribedUsers: [67890], }); ``` ### Daily Check Alert [#daily-check-alert] ```typescript const dailyAlert = new posthog.Alert("daily-check", { name: "Daily Active Users Check", insight: 34567, enabled: true, thresholdType: "absolute", thresholdLower: 1000, calculationInterval: "daily", subscribedUsers: [67890], }); ``` ### Weekend-Skipping Alert [#weekend-skipping-alert] ```typescript const weekdayAlert = new posthog.Alert("weekday-only", { name: "Weekday Metrics Alert", insight: 45678, enabled: true, thresholdType: "absolute", thresholdUpper: 500, skipWeekend: true, // Don't check on weekends subscribedUsers: [67890], }); ``` ### Series-Specific Alert [#series-specific-alert] ```typescript const seriesAlert = new posthog.Alert("series-alert", { name: "Specific Metric Alert", insight: 56789, enabled: true, thresholdType: "absolute", thresholdUpper: 200, seriesIndex: 0, // Monitor first series in multi-series insight subscribedUsers: [67890], }); ``` ## Resource Properties [#resource-properties] ### Required Arguments [#required-arguments] * `insight` (number): Numeric ID of the insight this alert monitors * `thresholdType` (string): Type of threshold - `"absolute"` for fixed values or `"percentage"` for relative changes * `subscribedUsers` (number\[]): List of user IDs to notify when the alert fires ### Optional Arguments [#optional-arguments] * `name` (string): Alert name * `enabled` (boolean): Whether alert is enabled (default: true) * `thresholdLower` (number): Lower bound of the threshold - alert fires when value goes below this * `thresholdUpper` (number): Upper bound of the threshold - alert fires when value goes above this * `calculationInterval` (string): How often to check - `"hourly"`, `"daily"`, `"weekly"`, or `"monthly"` * `conditionType` (string): Condition type - `"absoluteValue"`, `"relativeIncrease"`, or `"relativeDecrease"` * `seriesIndex` (number): Index of the trend series to monitor (0-based) * `checkOngoingInterval` (boolean): Whether to check the ongoing (incomplete) interval * `skipWeekend` (boolean): Whether to skip checking the alert on weekends ### Attributes [#attributes] * All configured properties ## Threshold Types [#threshold-types] ### Absolute Value [#absolute-value] Fixed numeric thresholds: ```typescript const alert = new posthog.Alert("absolute-alert", { name: "Absolute Threshold", insight: 12345, thresholdType: "absolute", thresholdLower: 50, // Alert when below 50 thresholdUpper: 200, // Alert when above 200 subscribedUsers: [67890], }); ``` ### Percentage Change [#percentage-change] Relative change thresholds: ```typescript const alert = new posthog.Alert("percentage-alert", { name: "Percentage Change", insight: 12345, thresholdType: "percentage", thresholdLower: -20, // Alert on 20% decrease thresholdUpper: 50, // Alert on 50% increase subscribedUsers: [67890], }); ``` ## Calculation Intervals [#calculation-intervals] ### Hourly [#hourly] Check metrics every hour: ```typescript calculationInterval: "hourly" ``` ### Daily [#daily] Check once per day: ```typescript calculationInterval: "daily" ``` ### Weekly [#weekly] Check once per week: ```typescript calculationInterval: "weekly" ``` ### Monthly [#monthly] Check once per month: ```typescript calculationInterval: "monthly" ``` ## Best Practices [#best-practices] ### 1. Use Descriptive Names [#1-use-descriptive-names] ```typescript // ✅ Good const alert = new posthog.Alert("critical-error-spike", { name: "Critical: Error Rate Spike (>100/hour)", }); // ❌ Avoid const alert = new posthog.Alert("alert1", { name: "Alert 1", }); ``` ### 2. Set Appropriate Thresholds [#2-set-appropriate-thresholds] ```typescript // Start with conservative thresholds const alert = new posthog.Alert("signup-alert", { name: "Low Signup Rate", insight: 12345, thresholdType: "absolute", thresholdLower: 50, // Alert if below normal baseline subscribedUsers: [67890], }); ``` ### 3. Notify the Right People [#3-notify-the-right-people] ```typescript // Production alerts - notify on-call team const prodAlert = new posthog.Alert("prod-critical", { name: "Production Critical Alert", insight: 12345, thresholdType: "absolute", thresholdUpper: 1000, subscribedUsers: [111, 222, 333], // On-call team }); // Business alerts - notify stakeholders const bizAlert = new posthog.Alert("revenue-drop", { name: "Revenue Drop Alert", insight: 23456, thresholdType: "percentage", thresholdLower: -10, subscribedUsers: [444, 555], // Business team }); ``` ## Common Use Cases [#common-use-cases] ### Error Rate Monitoring [#error-rate-monitoring] ```typescript const errorMonitor = new posthog.Alert("error-monitor", { name: "High Error Rate Detection", insight: 12345, enabled: true, thresholdType: "absolute", thresholdUpper: 50, // Alert when errors exceed 50 calculationInterval: "hourly", subscribedUsers: [67890], }); ``` ### Conversion Drop Detection [#conversion-drop-detection] ```typescript const conversionAlert = new posthog.Alert("conversion-drop", { name: "Conversion Rate Drop", insight: 23456, enabled: true, thresholdType: "percentage", thresholdLower: -15, // Alert on 15% drop calculationInterval: "daily", subscribedUsers: [67890, 67891], }); ``` ### Traffic Spike Alert [#traffic-spike-alert] ```typescript const trafficSpike = new posthog.Alert("traffic-spike", { name: "Unusual Traffic Spike", insight: 34567, enabled: true, thresholdType: "percentage", thresholdUpper: 100, // Alert on 100% increase calculationInterval: "hourly", subscribedUsers: [67890], }); ``` ## Troubleshooting [#troubleshooting] ### Alert Not Firing [#alert-not-firing] **Issue**: Alert doesn't trigger when threshold is exceeded **Solution**: * Verify the alert is enabled * Check calculation interval matches expectation * Confirm insight ID is correct * Ensure threshold values are set appropriately ### Wrong Users Notified [#wrong-users-notified] **Issue**: Incorrect users receiving alerts **Solution**: * Verify user IDs are correct numeric values * Check user IDs in PostHog user settings * Update `subscribedUsers` array with correct IDs ### Getting User IDs [#getting-user-ids] To find PostHog user IDs: 1. Go to PostHog Settings → Team 2. View team members list 3. Note the numeric user ID (visible in URLs or API responses) Or use the PostHog API: ```bash curl -H "Authorization: Bearer phx_your_key" \ https://us.posthog.com/api/users/ ``` ### Getting Insight IDs [#getting-insight-ids] To find insight IDs: 1. Open the insight in PostHog 2. Check the URL: `/insights/12345` - the number is the ID 3. Or use the PostHog API to list insights ## Additional Resources [#additional-resources] * [Insight Resource](/docs/providers/posthog/insight) * [PostHog Alerts Documentation](https://posthog.com/docs/user-guides/alerts) * [Configuration](/docs/providers/posthog/configuration) # Configuration This guide covers how to configure the PostHog provider for Pulumi. ## Overview [#overview] The PostHog provider enables management of product analytics, feature flags, and event tracking resources. It requires an API key for authentication and supports both PostHog Cloud and self-hosted instances. ## Provider Configuration [#provider-configuration] ### Basic Setup [#basic-setup] ```typescript import * as posthog from "pulumi-posthog"; import * as pulumi from "@pulumi/pulumi"; const config = new pulumi.Config("posthog"); const apiKey = config.requireSecret("apiKey"); const host = config.get("host") || "https://us.posthog.com"; ``` ### Configuration Methods [#configuration-methods] #### 1. Pulumi Configuration (Recommended) [#1-pulumi-configuration-recommended] ```bash # Set API key (encrypted) pulumi config set posthog:apiKey YOUR_API_KEY --secret # Set host (optional, defaults to https://us.posthog.com) pulumi config set posthog:host https://us.posthog.com ``` #### 2. Environment Variables [#2-environment-variables] ```bash export POSTHOG_API_KEY="phx_your_api_key_here" export POSTHOG_HOST="https://us.posthog.com" ``` #### 3. Provider Block [#3-provider-block] ```typescript const provider = new posthog.Provider("posthog", { apiKey: config.requireSecret("apiKey"), host: "https://us.posthog.com", }); ``` ## Getting API Credentials [#getting-api-credentials] ### Create Personal API Key [#create-personal-api-key] 1. Log in to your PostHog instance at [app.posthog.com](https://app.posthog.com) (or your self-hosted URL) 2. Navigate to **Settings** → **User** → **Personal API Keys** 3. Click **Create Personal API Key** 4. Copy the generated API key (starts with `phx_`) ### Required Permissions [#required-permissions] The API key should have access to: * Feature flags (read/write) * Insights (read/write) * Dashboards (read/write) * Alerts (read/write) * Hog functions (read/write) ## Self-Hosted PostHog [#self-hosted-posthog] If you're using a self-hosted PostHog instance, configure the custom host URL: ```bash pulumi config set posthog:host https://posthog.yourcompany.com ``` Or using environment variables: ```bash export POSTHOG_HOST="https://posthog.yourcompany.com" ``` ### Example with Self-Hosted Instance [#example-with-self-hosted-instance] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as posthog from "pulumi-posthog"; const config = new pulumi.Config("posthog"); // Configure provider for self-hosted instance const provider = new posthog.Provider("self-hosted", { host: "https://posthog.yourcompany.com", apiKey: config.requireSecret("apiKey"), }); // Use the provider const flag = new posthog.FeatureFlag("beta-feature", { key: "new-feature", name: "New Feature", active: true, }, { provider }); ``` ## Configuration Options [#configuration-options] | Property | Type | Required | Default | Description | | -------- | ------ | -------- | ------------------------ | --------------------------------------------- | | `apiKey` | string | Yes | - | PostHog Personal API Key (starts with `phx_`) | | `host` | string | No | `https://us.posthog.com` | PostHog instance URL | ## Best Practices [#best-practices] ### Security [#security] ```typescript // ✅ DO: Use Pulumi secrets for API keys const config = new pulumi.Config("posthog"); const apiKey = config.requireSecret("apiKey"); // ❌ DON'T: Hardcode API keys const flag = new posthog.FeatureFlag("bad", { key: "feature", name: "Feature", // Never include secrets in code! }); ``` ### Multi-Environment Setup [#multi-environment-setup] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as posthog from "pulumi-posthog"; const stack = pulumi.getStack(); const config = new pulumi.Config("posthog"); // Different API keys per environment const apiKey = config.requireSecret(`${stack}-apiKey`); // Environment-specific feature flag const flag = new posthog.FeatureFlag(`${stack}-feature`, { key: `${stack}-new-feature`, name: `${stack} New Feature`, active: stack === "production", }); ``` ### Multi-Project Setup [#multi-project-setup] ```typescript // Project A const projectAConfig = new pulumi.Config("posthog-project-a"); const projectAProvider = new posthog.Provider("project-a", { apiKey: projectAConfig.requireSecret("apiKey"), host: "https://us.posthog.com", }); // Project B const projectBConfig = new pulumi.Config("posthog-project-b"); const projectBProvider = new posthog.Provider("project-b", { apiKey: projectBConfig.requireSecret("apiKey"), host: "https://eu.posthog.com", }); ``` ## Troubleshooting [#troubleshooting] ### Authentication Errors [#authentication-errors] **Error**: `401 Unauthorized` **Solution**: 1. Verify your API key is correct and starts with `phx_` 2. Check that the API key hasn't expired 3. Ensure the API key has necessary permissions ```bash # Test API key curl -H "Authorization: Bearer phx_your_key_here" \ https://us.posthog.com/api/projects/@current/feature_flags/ ``` ### Invalid Host Configuration [#invalid-host-configuration] **Error**: `Connection refused` **Solution**: * Ensure your host URL is correct and includes the protocol (https\://) * For PostHog Cloud, use `https://us.posthog.com` or `https://eu.posthog.com` * For self-hosted, use your instance URL ### Permission Denied [#permission-denied] **Error**: `403 Forbidden` **Solution**: * Ensure your API key has appropriate project access * Personal API keys need appropriate permissions * Check that you're accessing resources in the correct project ## Additional Resources [#additional-resources] * [PostHog API Documentation](https://posthog.com/docs/api) * [Feature Flag Resource](/docs/providers/posthog/feature-flag) * [Insight Resource](/docs/providers/posthog/insight) * [Dashboard Resource](/docs/providers/posthog/dashboard) * [Alert Resource](/docs/providers/posthog/alert) # Dashboard The Dashboard resource allows you to create and manage custom dashboards for visualizing analytics data in PostHog. ## Example Usage [#example-usage] ### Basic Dashboard [#basic-dashboard] ```typescript import * as posthog from "pulumi-posthog"; const dashboard = new posthog.Dashboard("analytics", { name: "Product Analytics", description: "Key metrics for product performance", }); ``` ### Pinned Dashboard [#pinned-dashboard] ```typescript const pinnedDashboard = new posthog.Dashboard("main-dashboard", { name: "Main Metrics Dashboard", description: "Primary dashboard for daily monitoring", pinned: true, }); ``` ### Dashboard with Multiple Insights [#dashboard-with-multiple-insights] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as posthog from "pulumi-posthog"; // Create insights const signupInsight = new posthog.Insight("signups", { name: "User Signups", queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "user_signed_up", name: "User Signed Up", }], dateRange: { date_from: "-30d", }, }), }); const pageviewInsight = new posthog.Insight("pageviews", { name: "Pageviews", queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "$pageview", name: "Pageviews", }], dateRange: { date_from: "-7d", }, }), }); // Create dashboard const dashboard = new posthog.Dashboard("overview", { name: "Weekly Overview", description: "Weekly metrics overview", pinned: true, }); // Add insights to dashboard (done via Insight resource) const signupWithDash = new posthog.Insight("signup-on-dash", { name: "Signups", dashboardIds: [12345], // Reference dashboard ID queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "user_signed_up", }], dateRange: { date_from: "-30d" }, }), }); ``` ### Team Dashboard [#team-dashboard] ```typescript const teamDashboard = new posthog.Dashboard("team-metrics", { name: "Engineering Team Metrics", description: "Performance and adoption metrics for the engineering team", }); ``` ## Resource Properties [#resource-properties] ### Optional Arguments [#optional-arguments] * `name` (string): Dashboard name * `description` (string): Dashboard description * `pinned` (boolean): Pin to top of dashboard list ### Attributes [#attributes] * `dashboardId`: Dashboard ID * `name`: Dashboard name * `description`: Dashboard description * `pinned`: Whether the dashboard is pinned ## Best Practices [#best-practices] ### 1. Descriptive Names [#1-descriptive-names] Use clear, descriptive names for dashboards: ```typescript // ✅ Good const dashboard = new posthog.Dashboard("customer-health", { name: "Customer Health Metrics", description: "Track user engagement and retention", }); // ❌ Avoid const dashboard = new posthog.Dashboard("dash1", { name: "Dashboard 1", }); ``` ### 2. Pin Important Dashboards [#2-pin-important-dashboards] Pin frequently accessed dashboards for quick access: ```typescript const mainDashboard = new posthog.Dashboard("main", { name: "Main Dashboard", pinned: true, // Shows at top }); ``` ### 3. Organize by Theme [#3-organize-by-theme] Group related metrics in themed dashboards: ```typescript // User engagement dashboard const engagement = new posthog.Dashboard("engagement", { name: "User Engagement", description: "Daily active users, session length, feature usage", }); // Business metrics dashboard const business = new posthog.Dashboard("business", { name: "Business Metrics", description: "Signups, conversions, revenue tracking", }); // Product health dashboard const health = new posthog.Dashboard("health", { name: "Product Health", description: "Error rates, performance, uptime", }); ``` ## Common Use Cases [#common-use-cases] ### Executive Dashboard [#executive-dashboard] High-level metrics for leadership: ```typescript const executive = new posthog.Dashboard("executive", { name: "Executive Summary", description: "Key business metrics and growth indicators", pinned: true, }); ``` ### Product Team Dashboard [#product-team-dashboard] Metrics for product development: ```typescript const product = new posthog.Dashboard("product-metrics", { name: "Product Team Metrics", description: "Feature adoption, user feedback, A/B test results", }); ``` ### Operations Dashboard [#operations-dashboard] Operational and technical metrics: ```typescript const ops = new posthog.Dashboard("operations", { name: "Operations Dashboard", description: "System health, error rates, performance metrics", }); ``` ## Troubleshooting [#troubleshooting] ### Dashboard Not Visible [#dashboard-not-visible] **Issue**: Dashboard doesn't appear in PostHog UI **Solution**: * Check that the dashboard was created successfully * Verify you're logged into the correct PostHog project * Refresh the PostHog UI ### Cannot Add Insights [#cannot-add-insights] **Issue**: Insights don't appear on dashboard **Solution**: * Insights are added via the Insight resource's `dashboardIds` property * Use the dashboard's numeric ID (from PostHog) when creating insights * Ensure the insight and dashboard are in the same project ## Additional Resources [#additional-resources] * [Insight Resource](/docs/providers/posthog/insight) * [PostHog Dashboards Documentation](https://posthog.com/docs/user-guides/dashboards) * [Configuration](/docs/providers/posthog/configuration) # Feature Flag The FeatureFlag resource allows you to create and manage feature flags for gradual rollouts, A/B testing, and targeted feature releases in PostHog. ## Example Usage [#example-usage] ### Basic Feature Flag [#basic-feature-flag] ```typescript import * as posthog from "pulumi-posthog"; const betaFeature = new posthog.FeatureFlag("beta-feature", { key: "new-dashboard", name: "New Dashboard Beta", active: true, }); ``` ### Percentage Rollout [#percentage-rollout] ```typescript const gradualRollout = new posthog.FeatureFlag("gradual-rollout", { key: "new-feature", name: "New Feature", active: true, filters: JSON.stringify({ groups: [{ properties: [], rolloutPercentage: 10, // 10% of users }], }), }); ``` ### Targeted Feature Flag [#targeted-feature-flag] ```typescript const premiumFeature = new posthog.FeatureFlag("premium-feature", { key: "premium-features", name: "Premium Features", active: true, filters: JSON.stringify({ groups: [{ properties: [{ key: "plan", value: "premium", type: "person", operator: "exact", }], rolloutPercentage: 100, }], }), }); ``` ### Multi-Group Targeting [#multi-group-targeting] ```typescript const betaRollout = new posthog.FeatureFlag("beta-rollout", { key: "new-checkout", name: "New Checkout Experience", active: true, filters: JSON.stringify({ groups: [ { // 100% for beta testers properties: [{ key: "is_beta_tester", value: true, type: "person", operator: "exact", }], rolloutPercentage: 100, }, { // 5% for everyone else properties: [], rolloutPercentage: 5, }, ], }), }); ``` ### A/B Test Experiment [#ab-test-experiment] ```typescript const experiment = new posthog.FeatureFlag("checkout-experiment", { key: "checkout-variant", name: "Checkout A/B Test", active: true, filters: JSON.stringify({ groups: [{ properties: [], rolloutPercentage: 100, }], multivariate: { variants: [ { key: "control", name: "Control", rolloutPercentage: 50, }, { key: "variant-a", name: "Variant A", rolloutPercentage: 50, }, ], }, }), }); ``` ### Feature Flag with Tags [#feature-flag-with-tags] ```typescript const taggedFlag = new posthog.FeatureFlag("tagged-feature", { key: "tagged-feature", name: "Tagged Feature", active: true, tags: ["frontend", "ui-refresh", "q1-2024"], filters: JSON.stringify({ groups: [{ properties: [], rolloutPercentage: 25, }], }), }); ``` ### Complex User Targeting [#complex-user-targeting] ```typescript const enterpriseFlag = new posthog.FeatureFlag("enterprise-features", { key: "enterprise-features", name: "Enterprise Features", active: true, filters: JSON.stringify({ groups: [{ properties: [ { key: "plan", value: ["enterprise", "business"], type: "person", operator: "in", }, { key: "company_size", value: 100, type: "person", operator: "gte", }, ], rolloutPercentage: 100, }], }), }); ``` ## Resource Properties [#resource-properties] ### Required Arguments [#required-arguments] * `key` (string): Unique identifier for the feature flag ### Optional Arguments [#optional-arguments] * `name` (string): Display name for the feature flag * `active` (boolean): Whether the flag is active (default: true) * `filters` (string): JSON string containing targeting and rollout configuration with groups, properties, and conditions * `rolloutPercentage` (number): Simple overall rollout percentage (0-100) - use this for basic rollouts or `filters` for advanced targeting * `tags` (string\[]): Set of tags for the feature flag **Note**: For advanced targeting with user properties and multiple groups, use the `filters` property. For simple percentage-based rollouts, you can use either `rolloutPercentage` or configure it within `filters`. ### Attributes [#attributes] * `featureFlagId`: Numeric feature flag ID * `key`: Feature flag key * `active`: Current active status ## Filter Structure [#filter-structure] The `filters` property accepts a JSON string with the following structure: ```typescript { groups: [ { properties: [ { key: "property_name", // Person or event property value: "value" | ["values"], // Single value or array type: "person" | "event", // Property type operator: "exact" | "in" | "gte" | "lte" | "contains", // Comparison operator } ], rolloutPercentage: 0-100, // Percentage for this group } ], multivariate?: { // Optional for A/B tests variants: [ { key: "variant_key", name: "Variant Name", rolloutPercentage: 0-100, } ] } } ``` ## Best Practices [#best-practices] ### 1. Use Clear Naming [#1-use-clear-naming] ```typescript // ✅ Good const flag = new posthog.FeatureFlag("payment-redesign", { key: "payment-redesign-2024", name: "Payment Flow Redesign (2024)", active: true, }); // ❌ Avoid const flag = new posthog.FeatureFlag("flag1", { key: "flag1", name: "Flag 1", active: true, }); ``` ### 2. Start with Small Rollouts [#2-start-with-small-rollouts] ```typescript const rollout = new posthog.FeatureFlag("major-feature", { key: "major-feature", name: "Major Feature", active: true, filters: JSON.stringify({ groups: [{ properties: [], rolloutPercentage: 5, // Start with 5% }], }), }); ``` ### 3. Use Tags for Organization [#3-use-tags-for-organization] ```typescript const organizedFlag = new posthog.FeatureFlag("feature", { key: "q1-2024-mobile-redesign", name: "Mobile Redesign", active: true, tags: ["mobile", "ui", "q1-2024", "high-priority"], }); ``` ## Common Use Cases [#common-use-cases] ### Gradual Feature Rollout [#gradual-feature-rollout] Start with a small percentage and gradually increase: ```typescript const gradualFeature = new posthog.FeatureFlag("gradual", { key: "new-ui", name: "New UI", active: true, filters: JSON.stringify({ groups: [{ properties: [], rolloutPercentage: 10, // Start at 10%, increase over time }], }), }); ``` ### Beta Testing Program [#beta-testing-program] Target specific users for beta testing: ```typescript const betaFlag = new posthog.FeatureFlag("beta", { key: "beta-feature", name: "Beta Feature", active: true, filters: JSON.stringify({ groups: [{ properties: [{ key: "beta_tester", value: true, type: "person", operator: "exact", }], rolloutPercentage: 100, }], }), }); ``` ### Premium Feature Gating [#premium-feature-gating] Restrict features to premium users: ```typescript const premiumFlag = new posthog.FeatureFlag("premium", { key: "premium-analytics", name: "Premium Analytics", active: true, filters: JSON.stringify({ groups: [{ properties: [{ key: "subscription_tier", value: ["premium", "enterprise"], type: "person", operator: "in", }], rolloutPercentage: 100, }], }), }); ``` ## Troubleshooting [#troubleshooting] ### Flag Not Updating [#flag-not-updating] **Issue**: Changes to feature flags don't take effect **Solution**: * Feature flags are cached client-side * Wait for cache expiration or force refresh * Verify the flag key is unique ### Targeting Not Working [#targeting-not-working] **Issue**: Users aren't being targeted correctly **Solution**: * Check property names match exactly (case-sensitive) * Verify property values are set on user profiles * Use the PostHog debugger to test targeting ### Invalid JSON in Filters [#invalid-json-in-filters] **Issue**: Error when creating feature flag **Solution**: ```typescript // ✅ Always use JSON.stringify() for filters filters: JSON.stringify({ groups: [{ properties: [], rolloutPercentage: 50 }] }) // ❌ Don't use object literals filters: { groups: [...] } // This will fail ``` ## Additional Resources [#additional-resources] * [PostHog Feature Flags Documentation](https://posthog.com/docs/feature-flags) * [Configuration](/docs/providers/posthog/configuration) * [PostHog Provider](/docs/providers/posthog) # Hog Function The HogFunction resource allows you to create and manage custom Hog functions for transforming events, routing data, and executing custom logic in PostHog. ## Example Usage [#example-usage] ### Basic Transformation Function [#basic-transformation-function] ```typescript import * as posthog from "pulumi-posthog"; const transform = new posthog.HogFunction("event-transform", { name: "Add Processed Flag", enabled: true, type: "transformation", hog: ` fun transform(event) { event.properties.processed = true event.properties.processed_at = now() return event } `, }); ``` ### Function with Filters [#function-with-filters] ```typescript const filteredTransform = new posthog.HogFunction("pageview-transform", { name: "Enhance Pageviews", enabled: true, type: "transformation", hog: ` fun transform(event) { event.properties.enhanced = true return event } `, filtersJson: JSON.stringify({ events: [{ id: "$pageview" }], }), }); ``` ### Destination Function [#destination-function] ```typescript const webhook = new posthog.HogFunction("webhook-destination", { name: "Send to Webhook", enabled: true, type: "destination", hog: ` fun send(event) { fetch('https://api.example.com/webhook', { method: 'POST', body: event }) } `, filtersJson: JSON.stringify({ events: [{ id: "purchase_completed" }], }), }); ``` ### Function with Inputs [#function-with-inputs] ```typescript const configurable = new posthog.HogFunction("configurable-transform", { name: "Configurable Transform", enabled: true, type: "transformation", hog: ` fun transform(event, inputs) { event.properties.custom_field = inputs.fieldValue return event } `, inputsJson: JSON.stringify({ fieldValue: { value: "default_value", }, }), }); ``` ### Function with Execution Order [#function-with-execution-order] ```typescript const orderedFunction = new posthog.HogFunction("first-transform", { name: "First Transformation", enabled: true, type: "transformation", executionOrder: 1, // Runs first hog: ` fun transform(event) { event.properties.step = 1 return event } `, }); const secondFunction = new posthog.HogFunction("second-transform", { name: "Second Transformation", enabled: true, type: "transformation", executionOrder: 2, // Runs second hog: ` fun transform(event) { event.properties.step = 2 return event } `, }); ``` ## Resource Properties [#resource-properties] ### Optional Arguments (all optional) [#optional-arguments-all-optional] * `name` (string): Name of the Hog function * `description` (string): Description of the Hog function * `enabled` (boolean): Whether the Hog function is enabled (default: true) * `hog` (string): The Hog code to execute (not required when using a template) * `type` (string): Type of Hog function - `"destination"`, `"siteDestination"`, `"internalDestination"`, `"sourceWebhook"`, `"siteApp"`, or `"transformation"` * `filtersJson` (string): JSON string defining filters for when the Hog function should execute * `inputsJson` (string): JSON string containing the input values for the Hog function * `mappingsJson` (string): JSON array of mapping configurations * `maskingJson` (string): JSON object configuring PII masking * `templateId` (string): ID of a template to use as the basis for this Hog function * `executionOrder` (number): Order in which this Hog function executes (0-32767) * `iconUrl` (string): URL of the icon for this Hog function ### Attributes [#attributes] * All configured properties ## Function Types [#function-types] ### Transformation [#transformation] Transform events before they're stored: ```typescript type: "transformation" ``` ### Destination [#destination] Send events to external systems: ```typescript type: "destination" ``` ### Site Destination [#site-destination] Browser-side destinations: ```typescript type: "siteDestination" ``` ### Site App [#site-app] Browser-side applications: ```typescript type: "siteApp" ``` ## Hog Language Basics [#hog-language-basics] ### Event Transformation [#event-transformation] ```typescript hog: ` fun transform(event) { // Add properties event.properties.new_field = "value" // Modify properties event.properties.user_id = upper(event.properties.user_id) // Return modified event return event } ` ``` ### Conditional Logic [#conditional-logic] ```typescript hog: ` fun transform(event) { if event.event == '$pageview' { event.properties.is_pageview = true } else { event.properties.is_pageview = false } return event } ` ``` ### Using Inputs [#using-inputs] ```typescript hog: ` fun transform(event, inputs) { event.properties.custom = inputs.customValue.value return event } ` ``` ## Best Practices [#best-practices] ### 1. Use Descriptive Names [#1-use-descriptive-names] ```typescript // ✅ Good const fn = new posthog.HogFunction("add-user-segment", { name: "Add User Segment Classification", description: "Classify users into segments based on behavior", }); // ❌ Avoid const fn = new posthog.HogFunction("fn1", { name: "Function 1", }); ``` ### 2. Filter Appropriately [#2-filter-appropriately] Only process events that need transformation: ```typescript const fn = new posthog.HogFunction("purchase-enrichment", { name: "Enrich Purchase Events", filtersJson: JSON.stringify({ events: [{ id: "purchase_completed" }], // Only purchases }), hog: ` fun transform(event) { // Expensive enrichment only for purchases return event } `, }); ``` ### 3. Set Execution Order [#3-set-execution-order] Control the order of transformations: ```typescript // First: Clean data const cleaner = new posthog.HogFunction("clean", { name: "Clean Event Data", executionOrder: 1, }); // Second: Enrich data const enricher = new posthog.HogFunction("enrich", { name: "Enrich Event Data", executionOrder: 2, }); // Third: Format data const formatter = new posthog.HogFunction("format", { name: "Format Event Data", executionOrder: 3, }); ``` ## Common Use Cases [#common-use-cases] ### Add Timestamp [#add-timestamp] ```typescript const timestamp = new posthog.HogFunction("add-timestamp", { name: "Add Processing Timestamp", type: "transformation", hog: ` fun transform(event) { event.properties.processed_timestamp = now() return event } `, }); ``` ### User Classification [#user-classification] ```typescript const classify = new posthog.HogFunction("user-classification", { name: "Classify User Type", type: "transformation", hog: ` fun transform(event) { let revenue = event.properties.total_revenue || 0 if revenue > 1000 { event.properties.user_segment = 'premium' } else if revenue > 100 { event.properties.user_segment = 'standard' } else { event.properties.user_segment = 'free' } return event } `, }); ``` ### Webhook Integration [#webhook-integration] ```typescript const webhookFn = new posthog.HogFunction("slack-notification", { name: "Notify Slack on High-Value Purchase", type: "destination", filtersJson: JSON.stringify({ events: [{ id: "purchase_completed" }], }), hog: ` fun send(event) { if event.properties.amount > 1000 { fetch('https://hooks.slack.com/your-webhook', { method: 'POST', body: { text: 'High-value purchase: $' + event.properties.amount } }) } } `, }); ``` ### Data Sanitization [#data-sanitization] ```typescript const sanitize = new posthog.HogFunction("pii-removal", { name: "Remove PII from Events", type: "transformation", executionOrder: 1, // Run first hog: ` fun transform(event) { // Remove sensitive fields (check if they exist first) if has(event.properties, 'email') { delete event.properties.email } if has(event.properties, 'phone') { delete event.properties.phone } if has(event.properties, 'ssn') { delete event.properties.ssn } // Hash user identifiers if present if has(event.properties, 'user_id') { event.properties.user_id = sha256(event.properties.user_id) } return event } `, }); ``` ## Troubleshooting [#troubleshooting] ### Function Not Executing [#function-not-executing] **Issue**: Hog function doesn't run **Solution**: * Verify `enabled` is true * Check `filtersJson` matches your events * Ensure `executionOrder` doesn't conflict * Review Hog syntax for errors ### Syntax Errors [#syntax-errors] **Issue**: Hog code has syntax errors **Solution**: * Test Hog code in PostHog's Hog debugger * Check for missing brackets, quotes, or semicolons * Verify function signature matches type (e.g., `transform(event)`) ### Performance Issues [#performance-issues] **Issue**: Functions slow down event processing **Solution**: * Use filters to process only necessary events * Avoid expensive operations in hot paths * Consider using `executionOrder` to optimize ## Additional Resources [#additional-resources] * [PostHog Hog Functions Documentation](https://posthog.com/docs/cdp) * [Hog Language Reference](https://posthog.com/docs/hogql) * [Configuration](/docs/providers/posthog/configuration) # PostHog Provider The PostHog provider enables you to manage product analytics, feature flags, and event tracking resources in PostHog using Pulumi. This provider is dynamically bridged from the [Terraform PostHog Provider](https://registry.terraform.io/providers/PostHog/posthog). ## Installation [#installation] Install the PostHog provider package using your preferred package manager: ```bash bun add pulumi-posthog ``` ```bash pnpm add pulumi-posthog ``` ```bash yarn add pulumi-posthog ``` ```bash npm install pulumi-posthog ``` ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as posthog from "pulumi-posthog"; // Create a feature flag const betaFeature = new posthog.FeatureFlag("beta-feature", { key: "new-dashboard", name: "New Dashboard Beta", active: true, // filters must be a JSON string, not an object filters: JSON.stringify({ groups: [{ properties: [], rolloutPercentage: 25, }], }), }); // Export the feature flag key export const featureFlagKey = betaFeature.key; ``` ## Key Features [#key-features] The PostHog provider supports the following resources for managing your product analytics infrastructure: ### [Feature Flags](/docs/providers/posthog/feature-flag) [#feature-flags] Create and manage feature flags with advanced targeting and rollout strategies. Perfect for gradual rollouts, A/B testing, and targeted feature releases. ```typescript const flag = new posthog.FeatureFlag("premium-feature", { key: "premium-features", name: "Premium Features", active: true, filters: JSON.stringify({ groups: [{ properties: [{ key: "plan", value: "premium", type: "person", operator: "exact", }], rolloutPercentage: 100, }], }), }); ``` ### [Dashboards](/docs/providers/posthog/dashboard) [#dashboards] Build custom dashboards for visualizing your analytics data. ```typescript const dashboard = new posthog.Dashboard("analytics", { name: "Product Analytics", description: "Key metrics for product performance", pinned: true, }); ``` ### [Insights](/docs/providers/posthog/insight) [#insights] Configure analytics insights for tracking events, funnels, retention, and other metrics. ```typescript const pageviews = new posthog.Insight("pageviews", { name: "Pageview Trends", queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "$pageview", name: "Pageviews", }], dateRange: { date_from: "-30d", }, }), }); ``` ### [Alerts](/docs/providers/posthog/alert) [#alerts] Set up alerts to monitor metrics and get notified when thresholds are exceeded. ```typescript const errorAlert = new posthog.Alert("high-errors", { name: "High Error Rate", insight: 12345, // Numeric insight ID from PostHog enabled: true, thresholdType: "absolute", thresholdUpper: 100, subscribedUsers: [67890], // User IDs to notify }); ``` ### [Hog Functions](/docs/providers/posthog/hog-function) [#hog-functions] Create custom Hog functions for data transformation and routing. ```typescript const transform = new posthog.HogFunction("transform", { name: "Custom Event Transform", enabled: true, type: "transformation", hog: ` fun transform(event) { event.properties.processed = true return event } `, filtersJson: JSON.stringify({ events: [{ id: "$pageview" }], }), }); ``` ### Surveys [#surveys] Create project-scoped surveys with display conditions, targeting, and iteration scheduling. ```typescript const npsSurvey = new posthog.Survey("nps-survey", { name: "NPS Q2", type: "popover", projectId: "12345", questionsJson: JSON.stringify([ { type: "rating", question: "How likely are you to recommend us?", scale: 10, }, ]), }); ``` ### External Data Sources [#external-data-sources] Sync data from external systems (Postgres, Stripe, etc.) into PostHog as warehouse tables. ```typescript const stripeSource = new posthog.ExternalDataSource("stripe", { sourceType: "Stripe", projectId: "12345", schemas: ["Customer", "Invoice", "Subscription"], syncFrequency: "day", // Secrets are redacted on read by PostHog; the planned value is preserved in state. jobInputsJson: JSON.stringify({ stripe_account_id: "acct_123", stripe_secret_key: "sk_live_...", }), }); ``` ### Proxy Records [#proxy-records] Provision organization-scoped PostHog reverse-proxy records on custom domains. ```typescript const proxy = new posthog.ProxyRecord("marketing-proxy", { domain: "analytics.example.com", }); // After apply, create a CNAME pointing to this target. export const cnameTarget = proxy.targetCname; ``` ## Configuration [#configuration] See the [Configuration](/docs/providers/posthog/configuration) guide for detailed setup instructions, including: * Getting your PostHog API key * Setting up Pulumi configuration * Configuring self-hosted PostHog instances * Multi-environment and multi-project setups ## Common Use Cases [#common-use-cases] ### Feature Rollout Strategy [#feature-rollout-strategy] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as posthog from "pulumi-posthog"; // Beta feature with gradual rollout const betaFeature = new posthog.FeatureFlag("beta-checkout", { key: "new-checkout", name: "New Checkout Experience", active: true, filters: JSON.stringify({ groups: [ { // 100% for beta testers properties: [{ key: "is_beta_tester", value: true, type: "person", operator: "exact", }], rolloutPercentage: 100, }, { // 5% for everyone else properties: [], rolloutPercentage: 5, }, ], }), }); export const betaFlagKey = betaFeature.key; ``` ### Analytics Dashboard [#analytics-dashboard] ```typescript // Create insights const signupInsight = new posthog.Insight("signups", { name: "User Signups", queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "user_signed_up", name: "User Signed Up", }], dateRange: { date_from: "-30d", }, }), }); // Create dashboard const dashboard = new posthog.Dashboard("product-dashboard", { name: "Product Metrics", description: "Key product performance metrics", pinned: true, }); // Set up alert const signupAlert = new posthog.Alert("signup-alert", { name: "Low Signup Alert", insight: 12345, // Get insight ID from PostHog enabled: true, thresholdType: "absolute", thresholdLower: 10, subscribedUsers: [67890], // Get user IDs from PostHog }); export const dashboardId = dashboard.dashboardId; ``` ## Resource Types [#resource-types] | Resource | Description | | --------------------------------------------------- | ------------------------------------------------------------------ | | [FeatureFlag](/docs/providers/posthog/feature-flag) | Feature flag management for gradual rollouts and A/B testing | | [Dashboard](/docs/providers/posthog/dashboard) | Custom dashboards for data visualization | | [Insight](/docs/providers/posthog/insight) | Analytics queries and insights | | [Alert](/docs/providers/posthog/alert) | Metric alerts and notifications | | [HogFunction](/docs/providers/posthog/hog-function) | Custom data transformation functions | | Survey | Project-scoped surveys with targeting and iteration scheduling | | ExternalDataSource | Sync warehouse data from external systems (Postgres, Stripe, etc.) | | ProxyRecord | Organization-scoped reverse-proxy records on custom domains | ## Getting Help [#getting-help] * [PostHog Documentation](https://posthog.com/docs) * [PostHog API Reference](https://posthog.com/docs/api) * [Terraform PostHog Provider](https://registry.terraform.io/providers/PostHog/posthog/latest/docs) * [Configuration Guide](/docs/providers/posthog/configuration) # Insight The Insight resource allows you to create and manage analytics insights for tracking events, funnels, retention, and other metrics in PostHog. ## Example Usage [#example-usage] ### Basic Trends Insight [#basic-trends-insight] ```typescript import * as posthog from "pulumi-posthog"; const pageviews = new posthog.Insight("pageviews", { name: "Pageview Trends", queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "$pageview", name: "Pageviews", }], dateRange: { date_from: "-30d", }, }), }); ``` ### Signup Tracking [#signup-tracking] ```typescript const signups = new posthog.Insight("signups", { name: "User Signups", description: "Track new user registrations", queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "user_signed_up", name: "User Signed Up", }], dateRange: { date_from: "-30d", }, }), }); ``` ### Retention Analysis [#retention-analysis] ```typescript const retention = new posthog.Insight("retention", { name: "User Retention", description: "Track how many users return after signup", queryJson: JSON.stringify({ kind: "RetentionQuery", retentionFilter: { targetEntity: { id: "user_signed_up", type: "events", }, returningEntity: { id: "$pageview", type: "events", }, }, dateRange: { date_from: "-90d", }, }), }); ``` ### Insight on Dashboard [#insight-on-dashboard] ```typescript const dashboardInsight = new posthog.Insight("dashboard-metric", { name: "Key Metric", dashboardIds: [12345], // Add to specific dashboard queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "purchase_completed", }], dateRange: { date_from: "-7d" }, }), }); ``` ### Insight with Tags [#insight-with-tags] ```typescript const taggedInsight = new posthog.Insight("tagged-insight", { name: "Mobile App Sessions", tags: ["mobile", "engagement", "daily"], queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "app_session_start", }], dateRange: { date_from: "-30d" }, }), }); ``` ### Funnel Analysis [#funnel-analysis] ```typescript const funnel = new posthog.Insight("conversion-funnel", { name: "Signup to Purchase Funnel", description: "Track user journey from signup to first purchase", queryJson: JSON.stringify({ kind: "FunnelsQuery", series: [ { kind: "EventsNode", event: "user_signed_up", name: "Signed Up", }, { kind: "EventsNode", event: "added_to_cart", name: "Added to Cart", }, { kind: "EventsNode", event: "purchase_completed", name: "Purchased", }, ], dateRange: { date_from: "-30d", }, }), }); ``` ## Resource Properties [#resource-properties] ### Required Arguments [#required-arguments] * `queryJson` (string): Raw JSON serialized query payload accepted by PostHog (e.g., `InsightVizNode` with a `TrendsQuery`) ### Optional Arguments [#optional-arguments] * `name` (string): Insight name * `description` (string): Insight description * `dashboardIds` (number\[]): List of dashboard IDs to add the insight to * `createInFolder` (string): Folder where the insight is created * `tags` (string\[]): List of tags to apply to the insight * `deleted` (boolean): Whether the insight is deleted (soft delete) * `derivedName` (string): Insight derived name (auto-generated by PostHog when name is not set) ### Attributes [#attributes] * `insightId`: Numeric insight ID * `name`: Insight name * `derivedName`: Auto-generated insight name ## Query Types [#query-types] PostHog supports several query types: ### TrendsQuery [#trendsquery] Track event volumes over time: ```typescript queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "event_name", }], dateRange: { date_from: "-30d" }, }) ``` ### FunnelsQuery [#funnelsquery] Analyze conversion funnels: ```typescript queryJson: JSON.stringify({ kind: "FunnelsQuery", series: [ { kind: "EventsNode", event: "step_1" }, { kind: "EventsNode", event: "step_2" }, { kind: "EventsNode", event: "step_3" }, ], dateRange: { date_from: "-30d" }, }) ``` ### RetentionQuery [#retentionquery] Measure user retention: ```typescript queryJson: JSON.stringify({ kind: "RetentionQuery", retentionFilter: { targetEntity: { id: "signup", type: "events" }, returningEntity: { id: "$pageview", type: "events" }, }, dateRange: { date_from: "-90d" }, }) ``` ## Best Practices [#best-practices] ### 1. Use Descriptive Names [#1-use-descriptive-names] ```typescript // ✅ Good const insight = new posthog.Insight("user-engagement", { name: "Daily Active Users - Last 30 Days", description: "Number of unique users who performed any action", }); // ❌ Avoid const insight = new posthog.Insight("insight1", { name: "Insight 1", }); ``` ### 2. Add to Relevant Dashboards [#2-add-to-relevant-dashboards] ```typescript const insight = new posthog.Insight("key-metric", { name: "Revenue Trend", dashboardIds: [123, 456], // Add to multiple dashboards queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "purchase" }], dateRange: { date_from: "-30d" }, }), }); ``` ### 3. Use Tags for Organization [#3-use-tags-for-organization] ```typescript const insight = new posthog.Insight("mobile-metric", { name: "Mobile App Launches", tags: ["mobile", "engagement", "ios", "android"], queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "app_launched" }], dateRange: { date_from: "-7d" }, }), }); ``` ## Common Use Cases [#common-use-cases] ### User Engagement Tracking [#user-engagement-tracking] ```typescript const engagement = new posthog.Insight("engagement", { name: "Daily Active Users", description: "Unique users per day", tags: ["engagement", "daily"], queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "$pageview", math: "dau", // Daily active users }], dateRange: { date_from: "-30d" }, }), }); ``` ### Feature Adoption [#feature-adoption] ```typescript const adoption = new posthog.Insight("feature-adoption", { name: "New Feature Usage", description: "Track adoption of newly released feature", tags: ["product", "feature"], queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "new_feature_used", }], dateRange: { date_from: "-90d" }, }), }); ``` ### Conversion Tracking [#conversion-tracking] ```typescript const conversion = new posthog.Insight("conversion", { name: "Signup to Trial Conversion", description: "Users who start trial after signing up", queryJson: JSON.stringify({ kind: "FunnelsQuery", series: [ { kind: "EventsNode", event: "user_signed_up" }, { kind: "EventsNode", event: "trial_started" }, ], dateRange: { date_from: "-30d" }, }), }); ``` ## Troubleshooting [#troubleshooting] ### Invalid Query JSON [#invalid-query-json] **Issue**: Error creating insight with invalid query **Solution**: * Ensure `queryJson` is a valid JSON string * Use `JSON.stringify()` to convert objects to JSON strings * Validate query structure matches PostHog's query schema ```typescript // ✅ Correct - use JSON.stringify() queryJson: JSON.stringify({ kind: "TrendsQuery", series: [{ kind: "EventsNode", event: "$pageview" }], }) // ❌ Incorrect - queryJson must be a string, not an object queryJson: { kind: "TrendsQuery" } // This will fail ``` ### Insight Not Showing Data [#insight-not-showing-data] **Issue**: Insight created but shows no data **Solution**: * Verify the event name exists in PostHog * Check the date range includes data * Confirm events are being captured ### Dashboard Assignment Fails [#dashboard-assignment-fails] **Issue**: Cannot add insight to dashboard **Solution**: * Use numeric dashboard IDs (from PostHog) * Ensure dashboard exists before creating insight * Verify you have access to the dashboard ## Additional Resources [#additional-resources] * [Dashboard Resource](/docs/providers/posthog/dashboard) * [PostHog Insights Documentation](https://posthog.com/docs/user-guides/insights) * [PostHog Query Types](https://posthog.com/docs/product-analytics/insights) * [Configuration](/docs/providers/posthog/configuration) # TeamCity Provider The TeamCity provider enables you to manage projects, build configurations, VCS roots, and team resources in TeamCity using Pulumi. This provider is dynamically bridged from the [Terraform TeamCity Provider](https://github.com/JetBrains/terraform-provider-teamcity). ## Installation [#installation] Install the TeamCity provider package using your preferred package manager: ```bash bun add pulumi-teamcity ``` ```bash pnpm add pulumi-teamcity ``` ```bash yarn add pulumi-teamcity ``` ```bash npm install pulumi-teamcity ``` ## Configuration [#configuration] ### Provider Setup [#provider-setup] ```bash pulumi config set teamcity:url https://teamcity.example.com pulumi config set teamcity:username admin pulumi config set teamcity:password YOUR_PASSWORD --secret ``` Or using environment variables: ```bash export TEAMCITY_URL="https://teamcity.example.com" export TEAMCITY_USERNAME="admin" export TEAMCITY_PASSWORD="your-password" ``` ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as teamcity from "pulumi-teamcity"; // Create a project const project = new teamcity.Project("web-project", { name: "Web Application", projectId: "WebApp", }); // Create a VCS root const vcsRoot = new teamcity.Vcsroot("git-repo", { name: "Main Repository", projectId: project.projectId, git: { url: "https://github.com/example/repo.git", branch: "refs/heads/main", }, }); // Create a build configuration const buildConfig = new teamcity.BuildConfiguration("build", { name: "Build", projectId: project.projectId, }); // Attach the VCS root to the build configuration new teamcity.BuildConfigurationVcsRoot("build-vcs", { buildConfigurationId: buildConfig.buildConfigurationId, vcsRootId: vcsRoot.vcsrootId, checkoutRules: "+:. => source", }); export const projectId = project.projectId; ``` ## Key Features [#key-features] ### Project Management [#project-management] ```typescript const project = new teamcity.Project("project", { name: "Web Application", projectId: "WebApp", }); ``` ### Build Configurations [#build-configurations] A TeamCity build configuration is composed of several resources that all reference the parent `BuildConfiguration` by `buildConfigurationId`: ```typescript const build = new teamcity.BuildConfiguration("build", { name: "CI Build", projectId: project.projectId, buildType: "regular", }); // Build configuration settings (artifact rules, build number format, etc.) new teamcity.BuildConfigurationSettings("build-settings", { buildConfigurationId: build.buildConfigurationId, buildNumberPattern: "%build.counter%", }); // Add a build step new teamcity.BuildConfigurationStep("build-step", { buildConfigurationId: build.buildConfigurationId, name: "Compile", type: "simpleRunner", properties: { "script.content": "npm install && npm run build", "use.custom.script": "true", }, }); // Add a VCS trigger new teamcity.BuildConfigurationTrigger("vcs-trigger", { buildConfigurationId: build.buildConfigurationId, type: "vcsTrigger", properties: { branchFilter: "+:*", }, }); ``` ### VCS Roots [#vcs-roots] ```typescript const vcs = new teamcity.Vcsroot("repo", { name: "Repository", projectId: project.projectId, git: { url: "https://github.com/example/repo.git", branch: "refs/heads/main", }, }); ``` # Configuration The Time provider allows you to manage time-based resources in your infrastructure, useful for scheduled operations, delays, and time calculations. ## Provider Configuration [#provider-configuration] The Time provider doesn't require authentication or API credentials. It's a utility provider that operates locally. ### Basic Setup [#basic-setup] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; // No provider configuration needed // Resources can be used directly const offset = new time.Offset("example", { offsetDays: 7, }); ``` ### Provider Instance (Optional) [#provider-instance-optional] While not required, you can explicitly create a provider instance: ```typescript const provider = new time.Provider("time-provider", { // No configuration options required }); const resource = new time.Offset("with-provider", { offsetDays: 7, }, { provider }); ``` ## Configuration Reference [#configuration-reference] The Time provider has no required configuration options. All functionality is available without setup. ### Optional Settings [#optional-settings] While the provider itself needs no configuration, individual resources have their own options: * **Triggers**: Force resource recreation on specific changes * **Base time**: Reference time for calculations * **Offsets**: Time adjustments (days, hours, minutes, etc.) * **Rotation**: Automatic rotation intervals ## Use Cases [#use-cases] The Time provider is commonly used for: 1. **Scheduled Rotations**: Rotate secrets, passwords, or certificates on a schedule 2. **Deployment Delays**: Add delays between resource creations 3. **Time Calculations**: Calculate future or past timestamps 4. **Expiration Management**: Track and manage resource expiration ## Timezone Handling [#timezone-handling] All timestamps are in UTC. If you need local time: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; const offset = new time.Offset("local-time", { offsetHours: -5, // EST is UTC-5 }); export const utcTime = offset.rfc3339; export const note = "Adjust offsetHours based on your timezone"; ``` ## Integration Examples [#integration-examples] ### With Secret Rotation [#with-secret-rotation] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; import * as random from "@pulumi/random"; // Rotate every 30 days const rotation = new time.Rotating("password-rotation", { rotationDays: 30, }); // Password changes when rotation triggers const password = new random.RandomPassword("db-password", { length: 32, special: true, }, { // Rotation ID triggers new password generation ignoreChanges: [], replaceOnChanges: [rotation.id], }); export const nextRotation = rotation.rfc3339; ``` ### With Deployment Delays [#with-deployment-delays] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; import * as aws from "@pulumi/aws"; const database = new aws.rds.Instance("db", { // ... configuration }); // Wait 30 seconds after database creation const delay = new time.Sleep("db-ready-delay", { createDuration: "30s", }, { dependsOn: [database] }); // Application deployment waits for delay const app = new aws.ecs.Service("app", { // ... configuration }, { dependsOn: [delay] }); ``` ## Best Practices [#best-practices] ### Use Descriptive Names [#use-descriptive-names] ```typescript // ✅ Good: Clear purpose const certificateRotation = new time.Rotating("cert-rotation-90d", { rotationDays: 90, }); // ❌ Bad: Unclear purpose const time1 = new time.Rotating("time", { rotationDays: 90, }); ``` ### Document Rotation Schedules [#document-rotation-schedules] ```typescript const apiKeyRotation = new time.Rotating("api-key-rotation", { rotationDays: 30, }); // Export for visibility export const apiKeyRotationSchedule = { interval: "30 days", nextRotation: apiKeyRotation.rfc3339, purpose: "Rotate API keys monthly for security", }; ``` ### Combine with Other Providers [#combine-with-other-providers] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; import * as namecheap from "@pulumi/namecheap"; // Calculate certificate expiration (90 days from now) const certExpiration = new time.Offset("cert-expiration", { offsetDays: 90, }); // Use in DNS TXT record for tracking const dns = new namecheap.DomainRecords("dns", { domain: "example.com", records: [ { hostname: "_cert-expires", type: "TXT", address: certExpiration.rfc3339.apply(t => `expires=${t}`), }, ], }); ``` ## Testing [#testing] ### Local Development [#local-development] Time resources work immediately: ```typescript const testTime = new time.Static("test", {}); export const currentTime = testTime.rfc3339; // Run to see current time // pulumi up ``` ### Validation [#validation] ```typescript const offset = new time.Offset("validation", { offsetDays: 7, }); // Verify calculation export const oneWeekFromNow = offset.rfc3339; export const verification = offset.rfc3339.apply(t => `One week from now: ${t}` ); ``` ## Troubleshooting [#troubleshooting] ### Common Issues [#common-issues] **Issue: Time not updating** ``` Time resources are static after creation ``` Solution: Use Rotating resource for automatic updates **Issue: Timezone confusion** ``` All times are in UTC ``` Solution: Use offsetHours to adjust for your timezone **Issue: Rotation not triggering** ``` Resources not recreating on schedule ``` Solution: Ensure rotation triggers are properly configured ## Next Steps [#next-steps] * [Offset Resource](/docs/providers/time/offset) - Calculate time offsets * [Rotating Resource](/docs/providers/time/rotating) - Automatic time-based rotations * [Sleep Resource](/docs/providers/time/sleep) - Add delays to deployments * [Static Resource](/docs/providers/time/static) - Capture current timestamp * [Time Management Guide](/docs/providers/time/time-guide) - Patterns and best practices # Time Provider The Time provider enables you to manage time-based resources like delays, offsets, and rotations in your infrastructure code. This provider is dynamically bridged from the [Terraform Time Provider](https://registry.terraform.io/providers/hashicorp/time). ## Installation [#installation] Install the Time provider package using your preferred package manager: ```bash bun add pulumi-time ``` ```bash pnpm add pulumi-time ``` ```bash yarn add pulumi-time ``` ```bash npm install pulumi-time ``` ## Configuration [#configuration] No configuration required - the Time provider works out of the box. ## Quick Start [#quick-start] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "pulumi-time"; // Create a delay const delay = new time.Sleep("wait-for-deployment", { createDuration: "30s", // Wait 30 seconds }); // Use the delay in resource dependencies const resource = new SomeResource("dependent", { // Resource properties }, { dependsOn: [delay] }); ``` ## Key Features [#key-features] ### Delays [#delays] Add delays between resource operations: ```typescript // Wait 60 seconds before creating next resource const delay = new time.Sleep("deployment-delay", { createDuration: "60s", }); // Wait 30 seconds before destroying const deleteDelay = new time.Sleep("cleanup-delay", { destroyDuration: "30s", }); ``` ### Time Offsets [#time-offsets] Calculate time offsets: ```typescript const offset = new time.Offset("future-time", { offsetDays: 30, offsetHours: 12, offsetMinutes: 30, }); export const expirationTime = offset.rfc3339; ``` ### Time Rotation [#time-rotation] Rotate resources on a schedule: ```typescript const rotation = new time.Rotating("monthly-rotation", { rotationDays: 30, }); // Use rotation trigger const secret = new SomeSecret("rotated-secret", { value: pulumi.interpolate`secret-${rotation.id}`, }); ``` ### Static Times [#static-times] Create static time values: ```typescript const staticTime = new time.Static("timestamp", {}); export const createdAt = staticTime.rfc3339; ``` ## Common Use Cases [#common-use-cases] ### Staged Deployment [#staged-deployment] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "pulumi-time"; // Deploy database const database = new Database("db", { /* ... */ }); // Wait for database to stabilize const dbDelay = new time.Sleep("db-stabilize", { createDuration: "60s", }, { dependsOn: [database] }); // Deploy application after delay const app = new Application("app", { databaseUrl: database.connectionString, }, { dependsOn: [dbDelay] }); ``` ### Certificate Rotation [#certificate-rotation] ```typescript const rotation = new time.Rotating("cert-rotation", { rotationDays: 90, // Rotate every 90 days }); const certificate = new Certificate("cert", { // Certificate properties rotationTrigger: rotation.id, }); ``` ### Scheduled Resource Updates [#scheduled-resource-updates] ```typescript const monthlyRotation = new time.Rotating("monthly", { rotationDays: 30, }); const apiKey = new ApiKey("key", { name: pulumi.interpolate`key-${monthlyRotation.id}`, }); ``` ## Duration Format [#duration-format] Durations use Go's time format: * `s` = seconds * `m` = minutes * `h` = hours * Examples: `30s`, `5m`, `2h`, `1h30m` ## Best Practices [#best-practices] ### 1. Use Delays for External Dependencies [#1-use-delays-for-external-dependencies] ```typescript // Wait for external service const externalDelay = new time.Sleep("external-ready", { createDuration: "120s", }); ``` ### 2. Implement Graceful Shutdowns [#2-implement-graceful-shutdowns] ```typescript const shutdownDelay = new time.Sleep("graceful-shutdown", { destroyDuration: "30s", }); ``` ### 3. Rotate Credentials Regularly [#3-rotate-credentials-regularly] ```typescript const credentialRotation = new time.Rotating("creds", { rotationDays: 90, }); ``` # Offset Resource The `time.Offset` resource calculates a future or past timestamp by applying offsets to a base time. ## Example Usage [#example-usage] ### Basic Offset [#basic-offset] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; // Calculate 7 days from now const futureDate = new time.Offset("future-date", { offsetDays: 7, }); export const oneWeekFromNow = futureDate.rfc3339; ``` ### Multiple Offsets [#multiple-offsets] ```typescript const complexOffset = new time.Offset("complex-offset", { offsetDays: 30, offsetHours: 12, offsetMinutes: 30, }); export const calculatedTime = complexOffset.rfc3339; ``` ### Custom Base Time [#custom-base-time] ```typescript const customOffset = new time.Offset("custom-base", { baseRfc3339: "2024-01-01T00:00:00Z", offsetDays: 90, }); export const futureDate = customOffset.rfc3339; ``` ### Past Timestamps [#past-timestamps] ```typescript const pastDate = new time.Offset("past-date", { offsetDays: -7, // 7 days ago }); export const lastWeek = pastDate.rfc3339; ``` ## Argument Reference [#argument-reference] ### Optional Arguments [#optional-arguments] * **`baseRfc3339`** (string): Base timestamp in RFC3339 format. Defaults to current time. * **`offsetDays`** (number): Number of days to offset (positive for future, negative for past). * **`offsetHours`** (number): Number of hours to offset. * **`offsetMinutes`** (number): Number of minutes to offset. * **`offsetMonths`** (number): Number of months to offset. * **`offsetSeconds`** (number): Number of seconds to offset. * **`offsetYears`** (number): Number of years to offset. * **`triggers`** (map): Arbitrary map of values that triggers recreation. ## Attribute Reference [#attribute-reference] * **`day`** (number): Day of the month (1-31). * **`hour`** (number): Hour of the day (0-23). * **`minute`** (number): Minute of the hour (0-59). * **`month`** (number): Month of the year (1-12). * **`rfc3339`** (string): Full timestamp in RFC3339 format. * **`second`** (number): Second of the minute (0-59). * **`unix`** (number): Unix timestamp (seconds since epoch). * **`year`** (number): Year. ## Use Cases [#use-cases] ### Certificate Expiration [#certificate-expiration] ```typescript import * as time from "@pulumi/time"; const certExpiration = new time.Offset("cert-expires", { offsetDays: 90, // 90-day certificate }); // Use with certificate resource export const certificateValidUntil = certExpiration.rfc3339; export const expirationWarning = certExpiration.rfc3339.apply(t => `Certificate expires on ${t}` ); ``` ### License Management [#license-management] ```typescript const licenseExpiration = new time.Offset("license-expiry", { baseRfc3339: "2024-01-01T00:00:00Z", offsetYears: 1, // 1-year license }); export const licenseValidUntil = licenseExpiration.rfc3339; export const daysRemaining = licenseExpiration.unix.apply(unix => { const now = Math.floor(Date.now() / 1000); const days = Math.floor((unix - now) / 86400); return `${days} days remaining`; }); ``` ### Scheduled Events [#scheduled-events] ```typescript const eventDate = new time.Offset("event-date", { offsetDays: 14, // Event in 2 weeks offsetHours: 9, // 9 AM }); export const eventSchedule = { dateTime: eventDate.rfc3339, unix: eventDate.unix, humanReadable: eventDate.rfc3339.apply(t => new Date(t).toLocaleString()), }; ``` ### Maintenance Windows [#maintenance-windows] ```typescript const maintenanceStart = new time.Offset("maintenance-start", { offsetDays: 7, offsetHours: 2, // 2 AM next week }); const maintenanceEnd = new time.Offset("maintenance-end", { baseRfc3339: maintenanceStart.rfc3339, offsetHours: 4, // 4-hour window }); export const maintenanceWindow = { start: maintenanceStart.rfc3339, end: maintenanceEnd.rfc3339, duration: "4 hours", }; ``` ## Import [#import] Time offset resources cannot be imported as they represent calculations rather than existing infrastructure. ## Notes [#notes] * All times are in UTC * Offsets are cumulative (days + hours + minutes, etc.) * Base time defaults to the time of resource creation * Resource is recreated if triggers change # Rotating Resource The `time.Rotating` resource creates a timestamp that automatically changes based on a rotation schedule, useful for triggering resource recreation. ## Example Usage [#example-usage] ### Basic Rotation [#basic-rotation] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; // Rotate every 30 days const rotation = new time.Rotating("monthly-rotation", { rotationDays: 30, }); export const currentRotation = rotation.rfc3339; export const nextRotation = rotation.rfc3339.apply(t => { const date = new Date(t); date.setDate(date.getDate() + 30); return date.toISOString(); }); ``` ### With Secret Rotation [#with-secret-rotation] ```typescript import * as time from "@pulumi/time"; import * as random from "@pulumi/random"; const passwordRotation = new time.Rotating("password-rotation", { rotationDays: 90, // Rotate every 90 days }); // Password automatically regenerates when rotation occurs const dbPassword = new random.RandomPassword("db-password", { length: 32, special: true, }, { replaceOnChanges: [passwordRotation.id], }); export const rotationId = passwordRotation.id; export const passwordLastRotated = passwordRotation.rfc3339; ``` ### Monthly Rotation [#monthly-rotation] ```typescript const monthlyRotation = new time.Rotating("monthly", { rotationMonths: 1, }); export const currentMonth = monthlyRotation.month; export const rotationTimestamp = monthlyRotation.rfc3339; ``` ### Custom Rotation Period [#custom-rotation-period] ```typescript const customRotation = new time.Rotating("custom-rotation", { rotationHours: 168, // Weekly (7 days × 24 hours) }); export const weeklyRotation = customRotation.rfc3339; ``` ## Argument Reference [#argument-reference] ### Optional Arguments [#optional-arguments] * **`rotationDays`** (number): Number of days between rotations. * **`rotationHours`** (number): Number of hours between rotations. * **`rotationMinutes`** (number): Number of minutes between rotations. * **`rotationMonths`** (number): Number of months between rotations. * **`rotationRfc3339`** (string): Base timestamp for rotation calculation in RFC3339 format. * **`rotationYears`** (number): Number of years between rotations. * **`triggers`** (map): Arbitrary map of values that forces new rotation. ## Attribute Reference [#attribute-reference] * **`day`** (number): Day of the current rotation (1-31). * **`hour`** (number): Hour of the current rotation (0-23). * **`id`** (string): Unique identifier that changes on each rotation. * **`minute`** (number): Minute of the current rotation (0-59). * **`month`** (number): Month of the current rotation (1-12). * **`rfc3339`** (string): Current rotation timestamp in RFC3339 format. * **`second`** (number): Second of the current rotation (0-59). * **`unix`** (number): Current rotation Unix timestamp. * **`year`** (number): Year of the current rotation. ## Use Cases [#use-cases] ### API Key Rotation [#api-key-rotation] ```typescript import * as time from "@pulumi/time"; import * as random from "@pulumi/random"; const apiKeyRotation = new time.Rotating("api-key-rotation", { rotationDays: 30, // Monthly rotation }); const apiKey = new random.RandomString("api-key", { length: 32, special: false, }, { replaceOnChanges: [apiKeyRotation.id], }); export const apiKeyRotationSchedule = { current: apiKeyRotation.rfc3339, nextRotation: apiKeyRotation.rfc3339.apply(t => { const next = new Date(t); next.setDate(next.getDate() + 30); return next.toISOString(); }), rotationId: apiKeyRotation.id, }; ``` ### Certificate Rotation [#certificate-rotation] ```typescript const certRotation = new time.Rotating("cert-rotation", { rotationDays: 90, // Quarterly rotation }); // Certificate resource that recreates on rotation // export const certificateRotation = certRotation.id; ``` ### Database Password Rotation [#database-password-rotation] ```typescript import * as time from "@pulumi/time"; import * as random from "@pulumi/random"; import * as aws from "@pulumi/aws"; const dbPasswordRotation = new time.Rotating("db-password-rotation", { rotationDays: 90, }); const dbPassword = new random.RandomPassword("db-password", { length: 32, special: true, }, { replaceOnChanges: [dbPasswordRotation.id], }); const dbInstance = new aws.rds.Instance("database", { password: dbPassword.result, // ... other configuration }); export const passwordLastRotated = dbPasswordRotation.rfc3339; export const passwordRotationId = dbPasswordRotation.id; ``` ### Token Rotation [#token-rotation] ```typescript const tokenRotation = new time.Rotating("token-rotation", { rotationHours: 24, // Daily rotation }); const token = new random.RandomString("access-token", { length: 64, special: false, }, { replaceOnChanges: [tokenRotation.id], }); export const tokenInfo = { lastRotated: tokenRotation.rfc3339, rotatesIn: "24 hours", rotationId: tokenRotation.id, }; ``` ## Rotation Behavior [#rotation-behavior] * The rotation occurs when `pulumi up` is run **after** the rotation period has elapsed * Multiple rotation periods (days, hours, minutes) are cumulative * The resource ID changes on each rotation, triggering dependent resources * First rotation happens when the resource is created ## Example: Multi-Environment Rotation [#example-multi-environment-rotation] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; const config = new pulumi.Config(); const environment = config.require("environment"); // Different rotation schedules per environment const rotationDays: Record = { dev: 7, // Weekly for dev staging: 30, // Monthly for staging prod: 90, // Quarterly for prod }; const rotation = new time.Rotating(`${environment}-rotation`, { rotationDays: rotationDays[environment], }); export const rotationSchedule = { environment, rotationDays: rotationDays[environment], lastRotation: rotation.rfc3339, rotationId: rotation.id, }; ``` ## Import [#import] Time rotating resources cannot be imported as they represent time-based triggers rather than existing infrastructure. ## Notes [#notes] * Rotations only occur during `pulumi up` after the period has elapsed * The `id` attribute changes on each rotation * Use `replaceOnChanges` to trigger dependent resource recreation * Rotation periods are cumulative (e.g., rotationDays + rotationHours) * Consider time zone implications (all times are UTC) # Sleep Resource The `time.Sleep` resource adds delays before or after resource creation/destruction, useful for waiting on external systems. ## Example Usage [#example-usage] ### Basic Delay [#basic-delay] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; // Wait 30 seconds after creation const delay = new time.Sleep("wait", { createDuration: "30s", }); ``` ### Wait for Database [#wait-for-database] ```typescript import * as aws from "@pulumi/aws"; import * as time from "@pulumi/time"; const database = new aws.rds.Instance("db", { // ... configuration }); // Wait for database to be fully ready const dbReadyDelay = new time.Sleep("db-ready", { createDuration: "60s", }, { dependsOn: [database] }); // Application starts after delay const app = new aws.ecs.Service("app", { // ... configuration }, { dependsOn: [dbReadyDelay] }); ``` ### Destruction Delay [#destruction-delay] ```typescript const cleanupDelay = new time.Sleep("cleanup-delay", { destroyDuration: "30s", }); // Resources that depend on this will wait 30s before being destroyed ``` ### Both Create and Destroy Delays [#both-create-and-destroy-delays] ```typescript const bothDelays = new time.Sleep("both-delays", { createDuration: "60s", destroyDuration: "30s", }); ``` ## Argument Reference [#argument-reference] ### Optional Arguments [#optional-arguments] * **`createDuration`** (string): Duration to wait after creation (e.g., "30s", "5m", "1h"). * **`destroyDuration`** (string): Duration to wait before destruction. * **`triggers`** (map): Arbitrary map that causes recreation when values change. ## Duration Format [#duration-format] Durations use Go duration format: * `s` - seconds * `m` - minutes * `h` - hours Examples: * `"30s"` - 30 seconds * `"5m"` - 5 minutes * `"1h30m"` - 1 hour 30 minutes * `"90s"` - 90 seconds (1.5 minutes) ## Use Cases [#use-cases] ### Database Connection Delay [#database-connection-delay] ```typescript import * as aws from "@pulumi/aws"; import * as time from "@pulumi/time"; const db = new aws.rds.Instance("database", { engine: "postgres", // ... configuration }); // Wait for database to accept connections const dbWait = new time.Sleep("db-connection-wait", { createDuration: "120s", // 2 minutes }, { dependsOn: [db] }); const dbSetup = new aws.lambda.Invocation("db-setup", { functionName: "setup-database", input: JSON.stringify({ dbEndpoint: db.endpoint }), }, { dependsOn: [dbWait] }); ``` ### Service Stabilization [#service-stabilization] ```typescript import * as kubernetes from "@pulumi/kubernetes"; import * as time from "@pulumi/time"; const deployment = new kubernetes.apps.v1.Deployment("app", { // ... configuration }); // Wait for pods to be ready const stabilizationWait = new time.Sleep("stabilization", { createDuration: "60s", }, { dependsOn: [deployment] }); const healthCheck = new kubernetes.batch.v1.Job("health-check", { // ... configuration }, { dependsOn: [stabilizationWait] }); ``` ### DNS Propagation Delay [#dns-propagation-delay] ```typescript import * as namecheap from "@pulumi/namecheap"; import * as time from "@pulumi/time"; const dns = new namecheap.DomainRecords("dns", { domain: "example.com", records: [ { hostname: "@", type: "A", address: "192.0.2.1" }, ], }); // Wait for DNS propagation const dnsPropagation = new time.Sleep("dns-propagation", { createDuration: "5m", // 5 minutes }, { dependsOn: [dns] }); // SSL certificate request after DNS is propagated // const sslCert = new ... { dependsOn: [dnsPropagation] }); ``` ### Graceful Shutdown [#graceful-shutdown] ```typescript const gracefulShutdown = new time.Sleep("graceful-shutdown", { destroyDuration: "30s", // Allow 30s for connections to drain }); // When this resource is destroyed, wait 30s before continuing ``` ### API Rate Limiting [#api-rate-limiting] ```typescript import * as time from "@pulumi/time"; // Stagger API calls to avoid rate limits const delays = [0, 10, 20, 30, 40].map(seconds => new time.Sleep(`delay-${seconds}`, { createDuration: `${seconds}s`, }) ); // Create resources with delays between them const resources = delays.map((delay, i) => new SomeResource(`resource-${i}`, { // ... configuration }, { dependsOn: [delay] }) ); ``` ### Multi-Stage Deployment [#multi-stage-deployment] ```typescript import * as aws from "@pulumi/aws"; import * as time from "@pulumi/time"; // Stage 1: Infrastructure const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16", }); const stage1Wait = new time.Sleep("stage-1-wait", { createDuration: "30s", }, { dependsOn: [vpc] }); // Stage 2: Database const db = new aws.rds.Instance("db", { // ... configuration }, { dependsOn: [stage1Wait] }); const stage2Wait = new time.Sleep("stage-2-wait", { createDuration: "60s", }, { dependsOn: [db] }); // Stage 3: Application const app = new aws.ecs.Service("app", { // ... configuration }, { dependsOn: [stage2Wait] }); export const deploymentStages = { stage1: "Infrastructure", stage2: "Database", stage3: "Application", totalWaitTime: "90 seconds", }; ``` ## Best Practices [#best-practices] ### Use Appropriate Durations [#use-appropriate-durations] ```typescript // ✅ Good: Appropriate for the use case const dbWait = new time.Sleep("db-ready", { createDuration: "60s", // Reasonable for database startup }); // ❌ Bad: Unnecessarily long const shortWait = new time.Sleep("quick", { createDuration: "10m", // 10 minutes is too long for most cases }); ``` ### Document Why Delays Are Needed [#document-why-delays-are-needed] ```typescript // ✅ Good: Documented reason const apiWait = new time.Sleep("api-propagation", { createDuration: "30s", }); export const apiWaitReason = "API gateway needs 30s to propagate configuration changes"; // ❌ Bad: No context const wait = new time.Sleep("wait", { createDuration: "30s", }); ``` ### Consider Alternatives [#consider-alternatives] Before using Sleep, consider if there's a better way: ```typescript // ❌ Using Sleep to wait for resource const wait = new time.Sleep("wait", { createDuration: "60s", }, { dependsOn: [resource] }); // ✅ Better: Use resource's ready status if available const dependent = new SomeResource("dependent", { // ... configuration }, { dependsOn: [resource], // Some resources have built-in readiness checks }); ``` ## Troubleshooting [#troubleshooting] ### Delay Not Working [#delay-not-working] **Problem**: Resources still fail despite delay ``` Resource creation failed: Connection refused ``` **Solution**: Increase duration or check if resource has readiness checks ```typescript // Increase duration const longerWait = new time.Sleep("wait", { createDuration: "120s", // Was 60s }); ``` ### Deployments Too Slow [#deployments-too-slow] **Problem**: Delays making deployments unnecessarily slow **Solution**: Use minimum necessary delays ```typescript // Start with longer delay, then optimize const optimizedWait = new time.Sleep("wait", { createDuration: "30s", // Reduced from 60s after testing }); ``` ## Import [#import] Time sleep resources cannot be imported as they represent delays rather than existing infrastructure. ## Notes [#notes] * Sleep occurs during `pulumi up` or `pulumi destroy` * Does not consume cloud resources (local operation) * Useful for eventual consistency delays * Consider using resource-specific readiness checks when available * All times are wall-clock time, not CPU time # Static Resource The `time.Static` resource captures a timestamp when the resource is created and keeps it unchanged until the resource is recreated. ## Example Usage [#example-usage] ### Basic Timestamp [#basic-timestamp] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; // Capture creation timestamp const timestamp = new time.Static("creation-time", {}); export const createdAt = timestamp.rfc3339; ``` ### With Triggers [#with-triggers] ```typescript const config = new pulumi.Config(); const version = config.require("version"); const deploymentTime = new time.Static("deployment-time", { triggers: { version: version, // New timestamp when version changes }, }); export const deploymentInfo = { version: version, deployedAt: deploymentTime.rfc3339, }; ``` ### Resource Tagging [#resource-tagging] ```typescript import * as aws from "@pulumi/aws"; import * as time from "@pulumi/time"; const createdAt = new time.Static("creation-time", {}); const instance = new aws.ec2.Instance("server", { // ... configuration tags: { Name: "web-server", CreatedAt: createdAt.rfc3339, CreatedUnix: createdAt.unix.apply(u => u.toString()), }, }); ``` ## Argument Reference [#argument-reference] ### Optional Arguments [#optional-arguments] * **`rfc3339`** (string): Base timestamp in RFC3339 format. Defaults to current time. * **`triggers`** (map): Arbitrary map that causes new timestamp when values change. ## Attribute Reference [#attribute-reference] * **`day`** (number): Day of the timestamp (1-31). * **`hour`** (number): Hour of the timestamp (0-23). * **`id`** (string): Unique identifier (same as unix timestamp). * **`minute`** (number): Minute of the timestamp (0-59). * **`month`** (number): Month of the timestamp (1-12). * **`rfc3339`** (string): Timestamp in RFC3339 format. * **`second`** (number): Second of the timestamp (0-59). * **`unix`** (number): Unix timestamp (seconds since epoch). * **`year`** (number): Year of the timestamp. ## Use Cases [#use-cases] ### Deployment Tracking [#deployment-tracking] ```typescript import * as time from "@pulumi/time"; const deploymentTimestamp = new time.Static("deployment-timestamp", {}); export const deploymentMetadata = { timestamp: deploymentTimestamp.rfc3339, unix: deploymentTimestamp.unix, readable: deploymentTimestamp.rfc3339.apply(t => new Date(t).toLocaleString() ), }; ``` ### Resource Lifecycle Tracking [#resource-lifecycle-tracking] ```typescript import * as aws from "@pulumi/aws"; import * as time from "@pulumi/time"; const resourceCreated = new time.Static("resource-created", {}); const bucket = new aws.s3.Bucket("data", { bucket: "my-bucket", tags: { CreatedAt: resourceCreated.rfc3339, CreatedBy: "pulumi", Environment: "production", }, }); export const bucketMetadata = { name: bucket.bucket, createdAt: resourceCreated.rfc3339, }; ``` ### Version Tracking [#version-tracking] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; const config = new pulumi.Config(); const appVersion = config.require("appVersion"); const versionTimestamp = new time.Static("version-timestamp", { triggers: { version: appVersion, }, }); export const versionInfo = { version: appVersion, deployedAt: versionTimestamp.rfc3339, deployedUnix: versionTimestamp.unix, }; ``` ### Audit Trail [#audit-trail] ```typescript import * as time from "@pulumi/time"; const stackCreated = new time.Static("stack-created", {}); const auditInfo = { stackName: pulumi.getStack(), projectName: pulumi.getProject(), createdAt: stackCreated.rfc3339, createdUnix: stackCreated.unix, }; export const auditTrail = auditInfo; ``` ### Configuration Snapshot [#configuration-snapshot] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; const config = new pulumi.Config(); const configHash = config.require("configHash"); const configTimestamp = new time.Static("config-timestamp", { triggers: { configHash: configHash, // New timestamp when config changes }, }); export const configSnapshot = { hash: configHash, updatedAt: configTimestamp.rfc3339, updatedUnix: configTimestamp.unix, }; ``` ## Comparison with Other Time Resources [#comparison-with-other-time-resources] | Resource | Behavior | Use Case | | ------------ | ----------------------------------- | --------------------------- | | **Static** | Captures timestamp once, unchanging | Creation time tracking | | **Offset** | Calculates future/past time | Expiration dates | | **Rotating** | Changes on schedule | Automatic rotation triggers | | **Sleep** | Adds delays | Waiting for resources | ## Example: Complete Metadata System [#example-complete-metadata-system] ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; import * as aws from "@pulumi/aws"; const config = new pulumi.Config(); const appVersion = config.require("version"); const environment = config.require("environment"); // Capture deployment time const deployedAt = new time.Static("deployed-at", { triggers: { version: appVersion, environment: environment, }, }); // Calculate expiration (90 days for dev, never for prod) const expiresAt = new time.Offset("expires-at", { baseRfc3339: deployedAt.rfc3339, offsetDays: environment === "dev" ? 90 : 36500, // 100 years = "never" }); // Create resources with metadata const instance = new aws.ec2.Instance("app-server", { instanceType: "t3.micro", tags: { Name: `app-${environment}`, Version: appVersion, Environment: environment, DeployedAt: deployedAt.rfc3339, ExpiresAt: expiresAt.rfc3339, ManagedBy: "pulumi", }, }); export const metadata = { version: appVersion, environment: environment, deployedAt: deployedAt.rfc3339, expiresAt: expiresAt.rfc3339, instanceId: instance.id, }; ``` ## Best Practices [#best-practices] ### Use for Immutable Timestamps [#use-for-immutable-timestamps] ```typescript // ✅ Good: Capture creation time const created = new time.Static("created", {}); // ❌ Bad: Use Rotating for changing timestamps // const changing = new time.Static("changing", {}); ``` ### Combine with Triggers [#combine-with-triggers] ```typescript // ✅ Good: Update timestamp on meaningful changes const deployed = new time.Static("deployed", { triggers: { version: appVersion, config: configHash, }, }); // ❌ Bad: Random triggers const timestamp = new time.Static("timestamp", { triggers: { random: Math.random().toString(), }, }); ``` ### Export for Visibility [#export-for-visibility] ```typescript const timestamp = new time.Static("timestamp", {}); // ✅ Good: Export for tracking export const creationTime = timestamp.rfc3339; export const creationUnix = timestamp.unix; ``` ## Import [#import] Time static resources cannot be imported as they represent point-in-time captures rather than existing infrastructure. ## Notes [#notes] * Timestamp is captured at resource creation * Unchanging unless resource is recreated * Use triggers to force new timestamp * All times are in UTC * Useful for audit trails and metadata # Time Management Guide This guide covers common patterns and best practices for using the Time provider in your infrastructure. ## Understanding Time Resources [#understanding-time-resources] The Time provider offers four resource types: | Resource | Purpose | Updates | | ------------ | -------------------------- | ------------------------ | | **Static** | Capture timestamp | Never (unless recreated) | | **Offset** | Calculate future/past time | On recreation or trigger | | **Rotating** | Automatic rotation | On schedule | | **Sleep** | Add delays | On every operation | ## Common Patterns [#common-patterns] ### Secret Rotation Pattern [#secret-rotation-pattern] Automatically rotate secrets on a schedule: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; import * as random from "@pulumi/random"; import * as aws from "@pulumi/aws"; // Define rotation schedule const rotation = new time.Rotating("secret-rotation", { rotationDays: 90, // Quarterly rotation }); // Generate new password on rotation const password = new random.RandomPassword("db-password", { length: 32, special: true, }, { replaceOnChanges: [rotation.id], }); // Store in Secrets Manager const secret = new aws.secretsmanager.Secret("db-credentials", { name: "database-password", }); const secretVersion = new aws.secretsmanager.SecretVersion("db-credentials-version", { secretId: secret.id, secretString: password.result, }); export const rotationInfo = { lastRotated: rotation.rfc3339, rotationId: rotation.id, nextRotation: rotation.rfc3339.apply(t => { const next = new Date(t); next.setDate(next.getDate() + 90); return next.toISOString(); }), }; ``` ### Certificate Lifecycle Pattern [#certificate-lifecycle-pattern] Manage certificate creation and expiration: ```typescript import * as time from "@pulumi/time"; import * as tls from "@pulumi/tls"; // Capture certificate creation time const certCreated = new time.Static("cert-created", {}); // Calculate expiration (1 year from creation) const certExpires = new time.Offset("cert-expires", { baseRfc3339: certCreated.rfc3339, offsetDays: 365, }); // Set up rotation 30 days before expiration const certRotation = new time.Rotating("cert-rotation", { rotationDays: 335, // 365 - 30 }); // Generate certificate const cert = new tls.PrivateKey("cert-key", { algorithm: "RSA", rsaBits: 4096, }, { replaceOnChanges: [certRotation.id], }); export const certificateLifecycle = { createdAt: certCreated.rfc3339, expiresAt: certExpires.rfc3339, rotatesAt: certRotation.rfc3339, }; ``` ### Staged Deployment Pattern [#staged-deployment-pattern] Deploy resources in stages with delays: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; import * as aws from "@pulumi/aws"; // Stage 1: Database const database = new aws.rds.Instance("db", { engine: "postgres", instanceClass: "db.t3.micro", allocatedStorage: 20, }); const dbReadyWait = new time.Sleep("db-ready", { createDuration: "120s", }, { dependsOn: [database] }); // Stage 2: Cache const cache = new aws.elasticache.Cluster("cache", { engine: "redis", nodeType: "cache.t3.micro", numCacheNodes: 1, }, { dependsOn: [dbReadyWait] }); const cacheReadyWait = new time.Sleep("cache-ready", { createDuration: "60s", }, { dependsOn: [cache] }); // Stage 3: Application const app = new aws.ecs.Service("app", { // ... configuration }, { dependsOn: [cacheReadyWait] }); export const deploymentSequence = { stage1: "Database (wait 120s)", stage2: "Cache (wait 60s)", stage3: "Application", totalWaitTime: "180 seconds", }; ``` ### Expiration Management Pattern [#expiration-management-pattern] Track and manage resource expiration: ```typescript import * as pulumi from "@pulumi/pulumi"; import * as time from "@pulumi/time"; const licenseStart = new time.Static("license-start", {}); const licenseExpiry = new time.Offset("license-expiry", { baseRfc3339: licenseStart.rfc3339, offsetYears: 1, }); // Calculate days remaining const daysRemaining = pulumi.all([licenseStart.unix, licenseExpiry.unix]) .apply(([start, expiry]) => { const now = Math.floor(Date.now() / 1000); return Math.floor((expiry - now) / 86400); }); export const licenseInfo = { issuedAt: licenseStart.rfc3339, expiresAt: licenseExpiry.rfc3339, daysRemaining: daysRemaining, status: daysRemaining.apply(days => days > 30 ? "active" : days > 0 ? "expiring-soon" : "expired" ), }; ``` ## Best Practices [#best-practices] ### Choose the Right Resource [#choose-the-right-resource] **Use Static when:** * Tracking resource creation time * Need unchanging timestamp * Building audit trails **Use Offset when:** * Calculating future dates * Setting expiration times * Scheduling one-time events **Use Rotating when:** * Implementing automatic rotation * Periodic updates needed * Triggering dependent resources **Use Sleep when:** * Waiting for external systems * Rate limiting API calls * Allowing resources to stabilize ### Rotation Schedule Guidelines [#rotation-schedule-guidelines] | Asset Type | Recommended Rotation | Reason | | --------------- | --------------------- | ------------------------- | | Passwords | 90 days | Security compliance | | API Keys | 30-90 days | Limit exposure window | | Certificates | 90 days (renew at 60) | Prevent expiration | | Access Tokens | 24 hours | Minimize breach impact | | Encryption Keys | 1 year | Balance security/overhead | ```typescript // Example: Environment-based rotation const config = new pulumi.Config(); const env = config.require("environment"); const rotationDays: Record = { dev: 7, // Weekly staging: 30, // Monthly prod: 90, // Quarterly }; const rotation = new time.Rotating(`${env}-rotation`, { rotationDays: rotationDays[env], }); ``` ### Delay Guidelines [#delay-guidelines] **Use minimum necessary delays:** ```typescript // ✅ Good: Specific, tested delays const dbWait = new time.Sleep("db-ready", { createDuration: "60s", // Database typically ready in 45s }); // ❌ Bad: Arbitrary, excessive delays const longWait = new time.Sleep("wait", { createDuration: "5m", // Too long without justification }); ``` **Document why delays exist:** ```typescript const apiWait = new time.Sleep("api-propagation", { createDuration: "30s", // API Gateway needs 20-30s to propagate changes }); export const deploymentNotes = { apiPropagationDelay: "30 seconds", reason: "API Gateway configuration propagation time", }; ``` ### Time Zone Considerations [#time-zone-considerations] All time resources use UTC: ```typescript // ✅ Good: Document timezone needs const maintenanceTime = new time.Offset("maintenance", { offsetHours: 2, // 2 AM UTC = 9 PM EST }); export const maintenanceSchedule = { time: maintenanceTime.rfc3339, timezone: "UTC", note: "2 AM UTC = 9 PM EST", }; // Use offsets for local time const localTime = new time.Offset("local-9am", { offsetHours: 9 - (new Date().getTimezoneOffset() / 60), }); ``` ## Advanced Patterns [#advanced-patterns] ### Multi-Stage Rotation [#multi-stage-rotation] Different rotation schedules for different stages: ```typescript import * as time from "@pulumi/time"; const stages = { development: new time.Rotating("dev-rotation", { rotationDays: 7, }), staging: new time.Rotating("staging-rotation", { rotationDays: 30, }), production: new time.Rotating("prod-rotation", { rotationDays: 90, }), }; export const rotationSchedules = { development: "Weekly (7 days)", staging: "Monthly (30 days)", production: "Quarterly (90 days)", }; ``` ### Coordinated Rotation [#coordinated-rotation] Rotate multiple resources together: ```typescript import * as time from "@pulumi/time"; import * as random from "@pulumi/random"; const coordinatedRotation = new time.Rotating("coordinated", { rotationDays: 90, }); // All these rotate together const dbPassword = new random.RandomPassword("db-pass", { length: 32, }, { replaceOnChanges: [coordinatedRotation.id] }); const apiKey = new random.RandomString("api-key", { length: 64, }, { replaceOnChanges: [coordinatedRotation.id] }); const encryptionKey = new random.RandomBytes("encryption", { length: 32, }, { replaceOnChanges: [coordinatedRotation.id] }); export const rotationGroup = { rotationId: coordinatedRotation.id, lastRotated: coordinatedRotation.rfc3339, resources: ["database-password", "api-key", "encryption-key"], }; ``` ### Graceful Updates [#graceful-updates] Handle rotation without downtime: ```typescript import * as time from "@pulumi/time"; import * as aws from "@pulumi/aws"; const newRotation = new time.Rotating("new-credentials", { rotationDays: 90, }); // Create new credentials before old ones expire const newPassword = new random.RandomPassword("new-password", { length: 32, }, { replaceOnChanges: [newRotation.id] }); // Update gradually (blue-green pattern) // 1. Create new credentials // 2. Update applications to use new credentials // 3. Remove old credentials after grace period const gracePeriod = new time.Sleep("grace-period", { createDuration: "300s", // 5 minute overlap }, { dependsOn: [newPassword] }); ``` ## Troubleshooting [#troubleshooting] ### Rotation Not Triggering [#rotation-not-triggering] **Problem**: Resources not recreating on rotation ```typescript // ❌ Missing replaceOnChanges const password = new random.RandomPassword("pass", { length: 32, }); ``` **Solution**: Use replaceOnChanges ```typescript // ✅ Correct const password = new random.RandomPassword("pass", { length: 32, }, { replaceOnChanges: [rotation.id], }); ``` ### Unexpected Delays [#unexpected-delays] **Problem**: Deployments taking too long **Solution**: Review and optimize sleep durations ```bash # Check current delays pulumi preview --show-replacement-steps ``` ### Time Calculation Errors [#time-calculation-errors] **Problem**: Incorrect time calculations ```typescript // ❌ Wrong: Mixing base time and offsets const wrong = new time.Offset("wrong", { baseRfc3339: "2024-01-01T00:00:00Z", offsetDays: 30, // Don't mix with rotating or static }); ``` **Solution**: Use consistent time sources ```typescript // ✅ Correct: Use one base time const base = new time.Static("base", {}); const future = new time.Offset("future", { baseRfc3339: base.rfc3339, offsetDays: 30, }); ``` ## Next Steps [#next-steps] * [Offset Resource](/docs/providers/time/offset) - Time calculations * [Rotating Resource](/docs/providers/time/rotating) - Automatic rotation * [Sleep Resource](/docs/providers/time/sleep) - Deployment delays * [Static Resource](/docs/providers/time/static) - Timestamp capture