Pulumi Verbose Diagnostic Architecture and Troubleshooting

The ability to gain visibility into the internal mechanics of Infrastructure as Code (IaC) execution is critical for maintaining stable cloud environments. Pulumi provides a sophisticated, multi-tiered logging and debugging architecture designed to expose the internal state of the Pulumi engine, the behavior of resource providers, and the execution flow of the user-defined program. This system is partitioned into two primary domains: CLI logging, which focuses on the operational health and communications of the engine, and program logging, which allows developers to emit custom diagnostic telemetry from within their application logic. When these two systems are leveraged in tandem, they provide a comprehensive audit trail that can be used to resolve everything from simple configuration errors to complex cloud provider API failures.

CLI Verbose Logging and Engine Internals

CLI verbose logging is the primary mechanism for diagnosing how the Pulumi engine is interacting with resource providers and the state backend. By utilizing specific flags during command execution, operators can peel back layers of abstraction to see the exact sequence of events occurring during a deployment or a refresh operation.

The fundamental trigger for this level of visibility is the -v flag. This flag accepts an integer value that determines the granularity of the logs emitted. Pulumi utilizes a scale from 1 to 11.

  • Log Level 1 through 9: These levels provide increasing amounts of detail regarding engine operations. Level 1 is the least verbose, while level 9 is typically sufficient for most advanced troubleshooting needs.
  • Log Level 10: This is a high-verbosity threshold. At this level and below, Pulumi maintains a security posture that intentionally prevents the exposure of known credentials in the log output.
  • Log Level 11: This is the maximum verbosity level. At level 11, Pulumi will intentionally expose known credentials. This behavior is designed exclusively for emergency debugging scenarios where credential transmission issues must be isolated. Because of the security risk associated with plain-text credentials, level 11 should be used only in isolated environments and never in production pipelines without extreme caution.

By default, the Pulumi CLI writes these logs to the top-level temporary directory of the operating system. On most Linux and macOS systems, this corresponds to /tmp, or it will follow the path defined by the $TMPDIR environment variable. To change how these logs are handled, several flags are available:

  • --logtostderr: This flag redirects the log output from the temporary files directly to the standard error (stderr) stream. This is essential for developers who prefer to see logs in real-time within their terminal or for those capturing output via shell redirection.
  • --logflow: This is a critical flag for provider-level debugging. Normally, specifying -v only increases the verbosity of the Pulumi engine. The --logflow flag instructs the engine to pass that same verbosity level down to the underlying resource providers.

The impact of combining -v=9 and --logflow is significant. At this level of detail, resource providers will log the actual HTTP requests and responses sent to cloud APIs. For a DevOps engineer, this means the ability to see the raw JSON payloads sent to AWS, Azure, or GCP, as well as the exact response codes and error messages returned by those services. This removes the guesswork when dealing with 404 Not Found errors, 403 Forbidden permissions issues, or incorrect API parameter definitions.

Program Logging and Custom Diagnostics

While CLI logging focuses on the "plumbing" of the IaC process, program logging allows the developer to insert "instrumentation" into the actual infrastructure code. This is handled via the Pulumi SDK and enables the emission of messages that are displayed in the CLI output and persisted in Pulumi Cloud for historical auditing.

The SDK provides several logging functions to categorize the importance of the message. These functions are available across multiple supported languages, including Python, TypeScript, Go, and Java.

  • pulumi.log.info("message"): Used for general operational information. These messages confirm that a certain block of code was reached or a specific logic path was taken.
  • pulumi.log.debug("message"): Used for high-volume diagnostic data. These messages are hidden by default unless the -d or --debug flag is passed during command execution.
  • pulumi.log.warn("warning"): Used to alert the user to potential issues that do not stop the execution but may require attention.
  • pulumi.log.error("fatal error"): Used to signal a critical failure within the program logic.

In languages like Go, more granular control is provided through the pulumi.LogArgs structure. This allows developers to associate a log message with a specific resource or a StreamID, and to mark the log as Ephemeral, meaning it is not persisted for the long term.

The Java implementation follows a similar pattern, providing ctx.log().info(), ctx.log().debug(), ctx.log().warn(), and ctx.log().error() methods within the stack context.

Synergistic Debugging Strategies

For the most complex issues, relying on a single logging stream is often insufficient. The most effective troubleshooting approach involves the simultaneous activation of CLI engine logs and program-level debug logs.

A comprehensive debug command would look as follows:

bash pulumi up --logtostderr --logflow -v=9 -d 2> out.txt

The components of this command provide a layered defense against uncertainty:
- pulumi up: Initiates the update process.
- --logtostderr: Ensures logs are captured in the output stream rather than hidden in /tmp.
- --logflow: Ensures the cloud provider's internal API calls are exposed.
- -v=9: Sets the engine and provider verbosity to a high level.
- -d: Activates the pulumi.log.debug messages emitted from the code.
- 2> out.txt: Redirects the stderr stream to a file for later analysis.

Integration with CI/CD Pipelines

In automated environments, such as GitHub Actions, GitLab CI, or Jenkins, it is not always possible to modify the CLI command arguments directly within the pipeline configuration. In these instances, Pulumi provides PULUMI_OPTION_* environment variables that act as direct equivalents to the CLI flags.

The following mapping is used to enable verbose logging in a pipeline:

  • PULUMI_OPTION_LOGFLOW=true: Equivalent to --logflow.
  • PULUMI_OPTION_LOGTOSTDERR=true: Equivalent to --logtostderr.
  • PULUMI_OPTION_VERBOSE=9: Equivalent to -v=9.

By setting these environment variables in the pipeline's secret or variable store, an engineer can trigger detailed logging for a failing build without needing to commit code changes to the pipeline definition.

Provider-Specific Diagnostic Overrides

Some Pulumi resource providers are bridged from Terraform providers. These providers may have their own independent logging mechanisms that operate outside the standard Pulumi verbose flags. The most common example is the TF_LOG environment variable.

TF_LOG can be set to several levels: TRACE, DEBUG, INFO, WARN, or ERROR. When set to TRACE, it provides the most granular look at the provider's internal state.

Example usage for a bridged provider:

bash TF_LOG=TRACE pulumi up --logtostderr --logflow -v=10 2> out.txt

This combination forces both the Pulumi engine (via -v=10) and the underlying Terraform-based provider (via TF_LOG=TRACE) to emit maximum diagnostic data.

Advanced Python Debugging and Runtime Analysis

When logging is insufficient to identify a bug in a Python-based Pulumi program, developers can attach a professional debugger to the running process. This is necessary because Pulumi programs are executed as a separate process by the Pulumi engine.

To enable this, the debugpy library is utilized. The following logic must be added to the __main__.py file:

python import debugpy debugpy.listen(5678) debugpy.wait_for_client()

This code instructs the Pulumi program to open a network socket on port 5678 and pause execution until a debugger attaches to the process. In an IDE like VS Code, the user creates a "Python: Remote Attach" configuration pointing to port 5678. Once the pulumi up command is run, the program pauses, allowing the developer to set breakpoints, inspect the call stack, and evaluate variables in real-time.

Common Error Patterns and Resolution

Throughout the lifecycle of Pulumi development, certain error patterns emerge frequently. Understanding these patterns allows for faster resolution without always needing to resort to verbose logging.

Output Handling Errors

A common mistake for beginners is attempting to use a pulumi.Output object as if it were a standard Python string. This occurs because Pulumi outputs are asynchronous wrappers around values that may not yet exist (such as an IP address of a VM being created).

If a user attempts a operation like bucket_id + " - suffix", they will encounter:
TypeError: unsupported operand type(s) for +: 'Output' and 'str'

There are two primary fixes for this:
1. Use pulumi.concat(): This function is designed to handle a mix of strings and Output objects.
2. Use the .apply() method: This allows the user to define a function that executes only once the value is available.

Example of correct output printing for debugging:

```python
bucket = aws.s3.Bucket("data")

Use apply to print the actual value during the preview/update phase

bucket.id.apply(lambda id: print(f"Bucket ID: {id}"))
```

Configuration and State Failures

Missing configuration is another frequent source of failure. If a program expects a variable that has not been set, Pulumi will emit:
error: Missing required configuration variable 'my-infra:instance_type'

The resolution is to set the variable via the CLI:

bash pulumi config set instance_type t3.micro

Similarly, when a resource already exists in the cloud but is not tracked in the Pulumi state file, the engine will report:
error: importing arn:aws:s3:::my-bucket - already exists

The resolution for this is to either change the physical name of the resource in the code or use the pulumi import command to bring the existing resource under Pulumi's management.

Exception Handling and Clean Exits

Pulumi provides a specific exception class, pulumi.RunError, which is intended to terminate a program abruptly but cleanly. Unlike a standard Python exception, pulumi.RunError prevents the CLI from emitting a verbose, unhandled error log that includes the entire source program text and a complete system stack trace.

However, there is a known edge case involving pulumi.Output.apply(). If a pulumi.RunError is raised inside an .apply() block—particularly when dealing with a stack reference output—the "clean exit" behavior fails. Instead, the engine reverts to printing the full verbose stack trace.

Consider this problematic implementation:

typescript import * as pulumi from "@pulumi/pulumi"; new pulumi.StackReference('organization/test/dev') .getOutput('foo') .apply(output => { throw new pulumi.RunError("fail") })

In this scenario, the actual output will include a deep trace through node_modules/@pulumi/output.ts and processTicksAndRejections, rather than the expected clean error message: error: Error: fail. This behavior is important for developers to recognize so they do not mistake a known RunError for a systemic failure of the Pulumi engine itself.

Summary of Diagnostic Commands and Tooling

For quick reference, the following table summarizes the most impactful commands for visibility, secrets management, and organization control.

Category Command Purpose
Logging pulumi logs --follow Stream logs in real-time
Logging pulumi logs --resource my-function Filter logs by specific resource
Logging pulumi logs --since 2h View logs from the last 2 hours
Logging pulumi up --logtostderr -v=9 Deploy with high-level engine verbosity
Security pulumi up --suppress-outputs Hide sensitive values from the CLI output
Secrets pulumi config set --secret apiKey sk-123 Encrypt a secret in the config file
Secrets pulumi config get --show-secrets View a decrypted secret value
Secrets pulumi stack export --show-secrets Export state including decrypted secrets
Secrets pulumi config refresh Update secrets with a new encryption key
Org Mgmt pulumi org ls List all associated organizations
Org Mgmt pulumi org get-default Identify current default organization
Org Mgmt pulumi org set-default my-org Change the active organization
Org Mgmt pulumi org create my-new-org Provision a new organization
Plugins pulumi plugin ls List all installed resource plugins
Plugins pulumi plugin install resource aws v5.0.0 Force install a specific plugin version
Plugins pulumi plugin rm resource aws v4.0.0 Remove an outdated plugin version

Configuration File Structures

To understand how verbose logging interacts with project settings, it is necessary to understand the structure of the Pulumi configuration files.

The Pulumi.yaml file defines the project-level metadata:

yaml name: my-infrastructure runtime: python description: Production AWS infrastructure backend: url: s3://my-pulumi-state-bucket

The Pulumi.[stack].yaml file (e.g., Pulumi.dev.yaml) contains the stack-specific configurations. Note how secrets are stored in an encrypted format:

yaml config: aws:region: us-west-2 myproject:instanceType: t3.micro myproject:dbPassword: secure: AAABAHVzLXdlc3QtMg== # Encrypted value myproject:environment: dev

When running verbose logs (-v=11), the engine may attempt to decrypt these secure values to diagnose authentication failures between the provider and the cloud API.

Conclusion

The Pulumi verbose logging ecosystem is a sophisticated framework that transforms the "black box" of cloud deployment into a transparent, auditable process. By distinguishing between CLI engine logs and program-level diagnostics, Pulumi allows engineers to isolate whether a failure is originating from the infrastructure logic, the resource provider's implementation, or the cloud vendor's API.

The strategic use of flags like --logflow and -v=9 provides the raw HTTP data necessary to debug API handshake failures, while pulumi.log functions allow for the creation of custom breadcrumbs within the code. For the most extreme cases, the integration of debugpy allows for a full stop-the-world analysis of the program state. When combined with the PULUMI_OPTION_* environment variables for CI/CD integration and TF_LOG for bridged providers, the developer possesses a complete suite of tools to ensure infrastructure stability. The critical takeaway for any practitioner is the balance between verbosity and security: while level 11 logging is an invaluable tool for resolving credential issues, it must be handled as a high-risk operation to prevent the leakage of sensitive secrets into log files.

Related Posts