The ability to observe the internal state and operational flow of Infrastructure as Code (IaC) is critical for maintaining stable cloud environments. Pulumi implements a dual-layered logging architecture that separates the concerns of the Pulumi engine—which manages the state and resource graph—from the Pulumi program, which contains the user-defined logic. This separation ensures that operators can distinguish between a failure in the cloud provider's API, a bug within the Pulumi engine's orchestration, and a logical error within the custom application code. Understanding how to navigate these layers, from CLI flags to SDK-level logging methods, is essential for any engineer moving beyond simple deployments into complex, production-grade infrastructure management.
The Pulumi Engine Logging Framework
Pulumi provides a sophisticated mechanism for diagnosing the internals of the Pulumi engine and its associated resource providers. This is primarily handled through CLI-level configuration, allowing users to increase the granularity of the information emitted during a deployment or refresh cycle without modifying the underlying source code.
The primary tool for this is the -v flag, which controls the verbosity level of the engine logs. These levels range from 1 to 11.
The impact of these levels is significant regarding security and visibility. For the vast majority of troubleshooting scenarios, levels 1 through 10 are sufficient. However, there is a critical security boundary at level 10. When the verbosity is set to 10 or below, Pulumi is designed to avoid intentionally exposing known credentials in the log output. This protects sensitive data such as API keys or passwords from being leaked into log files or CI/CD console outputs. When a user elevates the verbosity to level 11, Pulumi intentionally exposes some known credentials. This is a high-risk setting intended solely for extreme debugging scenarios where the developer needs to verify the exact credentials being passed to a provider.
By default, the Pulumi CLI writes these logs to the top-level temporary directory of the operating system. On most Linux systems, this is /tmp, or it will follow the path defined by the $TMPDIR environment variable. While writing to files prevents the main CLI output from becoming cluttered, it can make real-time monitoring difficult. To solve this, Pulumi provides the --logtostderr flag, which redirects these internal logs to the standard error stream instead of temporary files, allowing for immediate visibility in the terminal.
Another critical component of engine logging is the --logflow flag. In a standard Pulumi operation, the verbosity level set by the -v flag applies only to the Pulumi engine. Resource providers, which are separate processes that communicate with cloud APIs, continue to operate at their own default log levels. By applying --logflow, the user instructs the Pulumi engine to pass the specified log level down to the resource providers. This is particularly vital when debugging cloud provider API interactions, as it enables the providers to emit detailed telemetry about the requests they are sending and the responses they are receiving.
At high verbosity levels, specifically -v=9 and above, the combination of the engine and the resource provider (via --logflow) will log the actual HTTP requests and responses sent to cloud APIs. This level of detail allows an engineer to diagnose specific failures such as:
- Incorrect resource references that lead to 404 Not Found errors.
- API parameter problems where a value is rejected by the cloud provider.
- Permission issues where the identity being used lacks the required IAM roles for a specific call.
Programmatic Logging via the Pulumi SDK
While CLI logging focuses on the engine, program logging allows developers to emit custom diagnostics from within their infrastructure code. This is essential for tracking the logic of the program, such as conditional resource creation or the results of a dynamic lookup.
Program logs are integrated into the Pulumi lifecycle and are displayed alongside standard output in the CLI and within the Pulumi Cloud console. These logs are persisted for historical purposes, providing an audit trail of what the program "thought" was happening during a specific execution.
The Pulumi SDK provides several severity levels to categorize these messages:
- Info: Used for general operational messages that describe the progress of the program.
- Debug: Used for detailed information that is hidden by default and only visible when the
-dor--debugflag is passed. - Warn: Used to alert the user to potential issues that do not stop the deployment.
- Error: Used for fatal errors. It is recommended to raise an exception immediately after calling an error log to halt the program and prevent a partial or corrupted deployment.
The implementation of these logs varies across languages, but the underlying engine call remains consistent. In Python, the pulumi.log module handles these requests. The internal logic checks for the existence of the Pulumi engine via get_engine(). If the engine is present, it uses the _log function to send a protobuf message (engine_pb2) containing the severity, message, and optional metadata. If the engine is not present (for example, during a unit test run), the SDK falls back to printing the message to sys.stderr.
The Python SDK supports several optional arguments for every log call:
- msg: The actual string message to be emitted.
- resource: An optional reference to a Pulumi resource. Associating a log with a resource allows the Pulumi CLI and Cloud console to link the log directly to the affected infrastructure component.
- stream_id: An optional integer used to associate a message with a stream of other messages, which is useful for grouping related log entries.
- ephemeral: A boolean that determines if the log should be treated as temporary.
Across different languages, the syntax varies but the functionality is identical:
- In TypeScript/JavaScript, methods like
pulumi.log.info("message")orpulumi.log.debug("hidden by default")are used. - In Go, the
ctx.Logobject provides methods such asctx.Log.Info("message", nil)orctx.Log.Error("fatal error", nil). Thepulumi.LogArgsstruct can be used to pass the resource, stream ID, and ephemeral status. - In Java, the
ctx.log()method returns a logger providing.info(),.debug(),.warn(), and.error().
Integration with Besom and Scala
Besom provides a way to use Scala for Pulumi programs. In this environment, logging requires a specific approach because of how Besom interacts with the Pulumi engine.
In any scope where the Pulumi Context is available and the global Besom import is included, the user can summon the log object. For example, using @main def run = Pulumi.run { Stack(log.warn("Nothing to do.")) }.
A critical technical detail of Besom is that logging is an asynchronous, effectful operation. Consequently, calling a logging method returns an Output. This means that logging statements cannot exist as isolated side effects; they must be composed into other values that are eventually passed as Stack arguments or exports. This design mirrors the behavior of functional logging frameworks like log4cats or ZIO.
Users are explicitly warned against using println in Besom. Because Besom's Scala code is executed in a different process than the Pulumi engine, the engine drives the process by calling Besom. Therefore, any output sent to the standard output of the Scala process via println will have no visible effect in the Pulumi CLI. All logging must be routed through the provided Besom functions to be correctly captured and relayed by the Pulumi engine.
Advanced Troubleshooting and Pipeline Configuration
For complex debugging, engineers often combine CLI verbose logging and program logging. This provides a holistic view of the operation, combining the high-level logic of the program with the low-level API calls of the engine.
An example of a comprehensive debugging command is:
pulumi up --logtostderr --logflow -v=9 -d 2> out.txt
In this command:
- --logtostderr ensures internal logs are visible in the stream.
- --logflow pushes the verbosity to the resource providers.
- -v=9 enables detailed HTTP request/response logging.
- -d enables the pulumi.log.debug messages from the program code.
- 2> out.txt redirects the combined error and log stream to a file for later analysis.
When operating within a CI/CD pipeline, it is not always possible to modify the execution command. In such cases, Pulumi supports environment variables to achieve the same result. The following environment variables are equivalent to the flags mentioned above:
PULUMI_OPTION_LOGFLOW=truePULUMI_OPTION_LOGTOSTDERR=truePULUMI_OPTION_VERBOSE=9
Furthermore, certain resource providers—specifically those that are bridged from Terraform—can be tuned using provider-specific environment variables. The TF_LOG variable can be set to TRACE, DEBUG, INFO, WARN, or ERROR to extract even more granular diagnostic information from the underlying Terraform provider. An example of a maximum-verbosity command including the Terraform bridge would be:
TF_LOG=TRACE pulumi up --logtostderr --logflow -v=10 2> out.txt
Project Initialization and Environment Setup
To utilize these logging features, a Pulumi project must first be correctly initialized. The process begins with the creation of a backend for state storage. For AWS users, this often involves creating an S3 bucket:
aws s3 mb s3://pulumi-hands-on-awsf
Once the bucket is created, the user logs into the Pulumi service using that bucket as the state store:
pulumi login s3://pulumi-hands-on-awsf
The project structure is then created using a template. For a TypeScript project on AWS, the steps are:
mkdir pulumi-hands-on-awsf
cd pulumi-hands-on-awsf
pulumi new aws-typescript
During this initialization, the user is prompted for several critical pieces of information:
- Project Name: The unique identifier for the project.
- Project Description: A summary of the infrastructure's purpose.
- Stack Name: The specific instance of the environment (e.g., dev, staging, prod).
- Phrase to protect secrets: A passphrase used to encrypt sensitive data in the state file.
- Package Manager: The desired tool for managing dependencies (e.g., npm, yarn).
- AWS Region: The physical location where the resources will be deployed.
Once these steps are complete, the project is ready for deployment using pulumi up. At this stage, any of the previously discussed logging flags or environment variables can be applied to monitor the creation of the resources.
Logging Summary and Specification Table
The following table summarizes the interaction between CLI flags, environment variables, and their resulting impact on the Pulumi ecosystem.
| Feature | CLI Flag | Environment Variable | Impact/Behavior |
|---|---|---|---|
| Engine Verbosity | -v [1-11] |
PULUMI_OPTION_VERBOSE |
Controls internal engine log detail. Level 11 exposes credentials. |
| Log Destination | --logtostderr |
PULUMI_OPTION_LOGTOSTDERR |
Switches output from temp files to the stderr stream. |
| Provider Flow | --logflow |
PULUMI_OPTION_LOGFLOW |
Passes the engine's verbosity level to resource providers. |
| Program Debugging | -d |
N/A | Unhides pulumi.log.debug statements in the program. |
| TF Bridge Logs | N/A | TF_LOG |
Controls logging for bridged Terraform providers (TRACE, DEBUG, etc). |
Conclusion: Strategic Analysis of the Pulumi Logging Ecosystem
The architecture of Pulumi's logging system reveals a calculated approach to the challenges of distributed infrastructure management. By decoupling the engine logs from the program logs, Pulumi acknowledges that failures in IaC usually fall into two distinct categories: orchestration failures and logic failures.
The inclusion of a credential-exposure boundary at log level 11 is a sophisticated security feature. Most tools either redact everything or nothing; Pulumi's tiered approach allows an engineer to stay in a "safe" zone for 99% of troubleshooting while providing an "emergency" escape hatch for the rarest and most difficult bugs.
The transition of logging in Besom to an asynchronous, effectful Output system is a necessary concession to the functional paradigms of Scala. This ensures that the order of operations is preserved in an environment where resources are being created in parallel. It prevents the "race condition" of logging, where a log message might appear before the resource it refers to has actually been initiated.
Ultimately, the synergy between the PULUMI_OPTION_* variables and the CLI flags makes the system highly portable. Whether a developer is debugging locally in a terminal or an SRE is analyzing a failed deployment in a locked-down GitLab or GitHub Actions runner, the ability to toggle visibility through environment variables ensures that diagnostics are never more than a configuration change away. The combination of TF_LOG for the provider layer and -v for the engine layer provides a full-stack observability window into the entire lifecycle of a cloud resource, from the initial code execution to the final HTTP 200 OK from the cloud API.