Besom Scala SDK for Pulumi

repository·main·Indexed 41 days ago

https://github.com/virtuslab/besom

Besom is a Scala SDK for Pulumi that enables developers to define cloud infrastructure using the Scala programming language. It provides a native Scala implementation of the Pulumi engine, bypassing the Pulumi Java SDK.

Tokens
43.9K
Snippets
177
Records
212
Agent score
51%

What's inside Besom

  1. What is Besom?

    main

    Besom is a Pulumi SDK for Scala 3. It allows you to define cloud resources using Scala, bringing rich type safety to infrastructure-as-code. It is designed to catch mistakes at compile-time using Scala's compiler support.

    Key characteristics:

    • Type Safety: Leverages Scala's type system for mission-critical infrastructure management.
    • Ecosystem Compatibility: Supports various Scala technological ecosystems including Future, Akka/Apache Pekko, cats/cats-effect, and ZIO.
    • Functional Semantics: The API uses pure, lazy, functional evaluation semantics to remain compatible with different concurrency models.
    • Stability: The 0.5.x series is a release-candidate for version 1.0 and offers near-complete parity with upstream Pulumi SDKs.
  2. Explore Besom cloud infrastructure examples

    main

    The examples/ directory in the Besom repository contains various implementations for building and deploying cloud applications and infrastructure. Examples are organized using a <cloud>-<topic> naming convention to identify the target provider and the specific infrastructure topic.

    Common cloud prefixes include:

    • aws: Amazon Web Services
    • azure: Microsoft Azure
    • gcp: Google Cloud Platform
    • kubernetes: Kubernetes

    For comprehensive guidance on using Besom, refer to the official Besom documentation.

  3. Use dependsOn to manage resource ordering

    main

    When creating cloud resources, some must exist before others can be configured. For example, an S3 Bucket Policy cannot be applied until the Bucket's Public Access Block settings have been updated to allow public policies.

    Use the opts(dependsOn = ...) parameter in resource constructors to inform Pulumi of these dependencies. This prevents errors like 403 Forbidden caused by Pulumi attempting to create resources in parallel before their prerequisites are ready.

    ```scala
    val feedBucketPolicy = bucketName.flatMap {\
  4. Understand Besom's lazy semantics and how to avoid dangling resources

    main

    Besom uses lazy semantics, meaning that declaring a resource in code does not automatically execute it. If a resource is not composed into the main program flow, it will not appear in your deployment plan (e.g., when running pulumi up).

    A resource is considered "dangling" (and thus ignored) if it is:

    • Never maped or flatMaped to derive another value.
    • Not used by Stack() or exported via Stack.export.
    • Never passed as an argument to another Resource.

    To ensure a resource is evaluated, you can:

    1. Pass it to the Stack constructor: Use this if the resource just needs to exist and you don't need its properties.
    2. Compose it into exports or other resources: If you use its properties (via map) or pass its Output to another resource, Besom will implicitly pull it into the main flow.

    Recommendation: To catch unused resources, enable the Scala compiler flag -Wunused:all.

    import besom.*
    import besom.api.aws
    
    // This resource is dangling and will NOT be deployed
    @main def main = Pulumi.run {
      val s3Bucket: Output[aws.s3.Bucket] = aws.s3.Bucket("my-bucket")
      
      Stack.exports()
    }
    
    // FIX 1: Pass to Stack to ensure evaluation
    @main def main = Pulumi.run {
      val s3Bucket: Output[aws.s3.Bucket] = aws.s3.Bucket("my-bucket")
      Stack(s3Bucket) 
    }
    
    // FIX 2: Compose via mapping/exports
    @main def main = Pulumi.run {
      val s3Bucket: Output[aws.s3.Bucket] = aws.s3.Bucket("my-bucket")
      Stack.exports(s3Url = s3Bucket.map(_.websiteEndpoint))
    }
  5. Understand Besom Inputs

    main

    Inputs are specialized types used in resource constructors to facilitate configuration. They provide an elastic model that allows users to provide values in multiple formats, making it easier to define infrastructure resources.

    An Input[A] can accept:

    • Raw values of type A.
    • Values wrapped in an Output[A].
    • Nothing (when using Input.Optional[A]).

    Input.Optional[A] can accept:

    • Raw Option[A] (e.g., Some(value) or None).
    • Output[Option[A]].
    • Output[A] (which is automatically treated as Some in the context of the input).
    val int1: Input[Int] = 23
    val int2: Input[Int] = Output(23)
    
    // Using Input.Optional
    val maybeInt1: Input.Optional[Int] = 23
    val maybeInt2: Input.Optional[Int] = None
    val maybeInt3: Input.Optional[Int] = Some(23)
    val maybeInt4: Input.Optional[Int] = Output(23)
    val maybeInt5: Input.Optional[Int] = Output(Option(23))
    val maybeInt6: Input.Optional[Int] = Output(None)
  6. Difference between Resource constructors and plain Outputs regarding memoization

    main

    It is critical to distinguish between Resource constructors and plain Outputs when managing side effects:

    • Resource Constructors are memoized: Pulumi requires that any given resource be declared only once. Besom handles this by caching calls to resource constructors with the same arguments. If you call a resource constructor multiple times with the same arguments, it returns the same resource instance and only executes the effect once.
    • Plain Outputs are NOT memoized: If you wrap a side effect in a plain Output { ... } block, that effect will be executed every time the Output is referenced or evaluated in the program flow.

    Best Practice: Always compose resource constructors into the main flow and avoid using plain Output blocks for side effects to prevent unexpected multiple executions.

    import besom.*
    import besom.api.aws
    
    @main def main = Pulumi.run {
      val s3Bucket: Output[aws.s3.Bucket] = aws.s3.Bucket("my-bucket")
      // This side effect will run TWICE because it is a plain Output, not a Resource constructor
      val launchMissiles: Output[_] = Output { launch() }
      
      Stack(s3Bucket, launchMissiles)
        .export(
          url     = s3Bucket.map(_.websiteEndpoint),    
          fallout = launchMissiles
        )
    }
  7. How Context and imports work in Besom

    main

    Besom uses Scala's Context Functions to pass the Context implicitly. The Pulumi.run block provides this context automatically to any code inside it.

    If you define functions outside of the Pulumi.run block that need to create resources or access stack configuration, you must explicitly declare the context using a using clause: (using Context) or (using besom.Context).

    To ensure all user-facing types and functions are available, every Besom program file should include an import besom.* clause. If you are using a functional effect system, use the specific variant imports:

    • Cats-Effect: import besom.cats.*
    • ZIO: import besom.zio.*

    Important Dependency Note: Always use org.virtuslab::besom-* dependencies. Besom is a separate implementation and does not depend on the com.pulumi:* Java SDK.

    import besom.*
    
    // Functions defined outside `Pulumi.run` must use the `(using Context)` clause
    def deployPostgres(dbName: String)(using Context): Output[Postgres] =
      ...
    
    @main def run = Pulumi.run {
      // Inside `Pulumi.run`, Context is provided implicitly
      val component = deployPostgres("my-db")
    
      Stack.exports(
        aComponentUrn = component.map(_.urn)
      )
    }
  8. Understand Besom project structure and dependencies

    main

    A Besom project consists of several key files:

    • Pulumi.yaml: The main Pulumi project configuration.
    • project.scala: Contains Scala-CLI dependencies.
    • Main.scala: The entry point for your Infrastructure as Code.

    Critical Dependency Rule: When adding dependencies, only use org.virtuslab::besom-*. Do NOT use com.pulumi:*. Besom is a completely separate implementation and does not depend on the Pulumi Java SDK.

    Example Scala-CLI configuration in Main.scala:

    //> using scala "3.3.1"
    //> using plugin "org.virtuslab::besom-compiler-plugin:0.5.0"
    //> using dep "org.virtuslab::besom-core:0.5.0"
    //> using dep "org.virtuslab::besom-aws:6.31.0-core.0.5"
    
    import besom.*
    import besom.api.aws
    
    @main def run = Pulumi.run { 
       val bucket = aws.s3.Bucket("my-bucket")
    
       Stack.exports(
          bucketName = bucket.bucket
       )
    }
  9. Understand Besom Outputs

    main

    Outputs are the primary asynchronous data structure in Besom programs. They represent values that will be resolved by the Besom runtime during the execution of the Pulumi.run function.

    Key characteristics:

    • Pure and Lazy: Evaluation of code is suspended until the Besom runtime performs interpretation at the 'end-of-the-world'.
    • Monadic: Outputs support map and flatMap operators, allowing them to be used within Scala for-comprehensions.
    • Effect Integration: Outputs can consume other effects via the ToFuture typeclass. Besom provides built-in support for:
      • scala.concurrent.Future (via besom-core)
      • cats.effect.IO (via besom-cats)
      • zio.Task (via besom-zio)
  10. Manage resource dependencies in Besom

    main

    While Pulumi automatically handles dependencies when one resource's output is passed as another's input, you can manually specify dependencies using opts(dependsOn = ...) when a resource must wait for a specific action (like enabling a cloud service) that doesn't involve direct data passing.

    val service = gcp.cloudrun.Service(
      "service",
      gcp.cloudrun.ServiceArgs(
        // ... args
      ),
      opts(dependsOn = enableServices(GoogleApis.CloudRun))
    )
  11. How Besom selects build executors

    main

    Besom automatically detects the appropriate build tool (executor) based on the files present in your project directory. If use-executor is specified in Pulumi.yaml, it replaces the command used by the chosen executor but does not change the executor itself.

    ExecutorSelection CriteriaCommand Example
    jarbinary points to a .jarjava from PATH
    sbtbuild.sbt or project/build.properties exists, or use-executor: sbt./sbt or sbt
    Gradlesettings.gradle[.kts] or build.gradle exists, or use-executor: gradle./gradlew or gradle
    Mavenpom.xml exists, or use-executor: mvn./mvnw or mvn
    Scala CLINone of the above matchscala-cli or scala

    Important Notes:

    • Scala CLI: Besom supports scala-cli or scala (for Scala 3.5+). If both are installed, you must disambiguate using use-executor in Pulumi.yaml or the BESOM_LANGHOST_SCALA_CLI_COMMAND environment variable.
    • Wrappers: Besom prefers local wrappers (like ./gradlew) if they follow standard conventions. On Windows, it looks for .bat versions.
    • Version 0.5.2+ Change: A local ./scala-cli script is no longer automatically picked up. Use BESOM_LANGHOST_SCALA_CLI_COMMAND=./scala-cli to point to it explicitly.
  12. Use lifting to access properties of an Output

    main

    Besom provides 'lifting' support via Scala 3 extension methods. This allows you to access properties of a data structure held within an Output[T] directly, without manually calling .map every time.

    When you access a property on an Output, Besom returns a new Output containing the value of that property. This lifting works recursively, meaning you can chain property access (e.g., output.propertyA.propertyB) to traverse nested data structures.

    // Standard way using .map
    val s3Url: Output[String] = s3Bucket.map(_.websiteEndpoint)
    
    // Lifted way using extension methods
    val s3Url: Output[String] = s3Bucket.websiteEndpoint