Pulumi lets you write infrastructure code in familiar programming languages like Go, and it manages the lifecycle of your cloud resources. The most surprising thing about using Pulumi with Go is that you’re not just defining resources; you’re writing imperative programs that happen to declare desired state in the cloud.
Let’s spin up a simple S3 bucket in AWS. First, you need to set up your Pulumi project. If you don’t have it already, install the Pulumi CLI. Then, create a new Go project:
mkdir pulumi-go-s3-example
cd pulumi-go-s3-example
pulumi new go --yes
This creates a Pulumi.yaml file for project configuration and a main.go file. Open main.go. It will look something like this:
package main
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Create an AWS resource (S3 Bucket)
bucket, err := aws.NewBucket(ctx, "my-bucket", &aws.BucketArgs{
Website: aws.BucketWebsiteArgs{
IndexDocument: pulumi.String("index.html"),
},
})
if err != nil {
return err
}
// Export the bucket name
ctx.Export("bucketName", bucket.ID())
return nil
})
}
You’ll need to add the AWS provider import. Edit main.go to include it:
package main
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
// Import the AWS provider
"github.com/pulumi/pulumi-aws/sdk/v6/go/aws"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Create an AWS resource (S3 Bucket)
bucket, err := aws.NewBucket(ctx, "my-bucket", &aws.BucketArgs{
Website: aws.BucketWebsiteArgs{
IndexDocument: pulumi.String("index.html"),
},
})
if err != nil {
return err
}
// Export the bucket name
ctx.Export("bucketName", bucket.ID())
return nil
})
}
Now, to deploy this, you’ll need AWS credentials configured. Pulumi uses the standard AWS SDK credential chain. Run:
pulumi up
Pulumi will show you a preview of the resources to be created. In this case, it will be a single S3 bucket named my-bucket (with a Pulumi-generated suffix for uniqueness) configured for static website hosting. After you confirm, Pulumi will provision the bucket in your AWS account.
The pulumi up command orchestrates a desired state. When you run it, Pulumi compares your Go code’s declared resources with the actual state in your cloud provider. It then generates a plan to make the actual state match the desired state. The key here is that the Go code isn’t directly calling AWS APIs. Instead, it’s building up a logical graph of resources and their dependencies. Pulumi’s engine then translates this graph into the necessary API calls.
Consider how you might conditionally create a resource. In Go, this is a simple if statement:
package main
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi-aws/sdk/v6/go/aws"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Create an AWS resource (S3 Bucket)
bucket, err := aws.NewBucket(ctx, "my-bucket", &aws.BucketArgs{
Website: aws.BucketWebsiteArgs{
IndexDocument: pulumi.String("index.html"),
},
})
if err != nil {
return err
}
// Conditionally create a bucket policy only if the bucket name is "my-bucket"
// In a real scenario, you'd likely use a config value or a dynamic condition.
if ctx.Stack() == "dev" { // Example: condition based on stack name
_, err = aws.NewBucketPolicy(ctx, "my-bucket-policy", &aws.BucketPolicyArgs{
Bucket: bucket.ID(), // Reference the bucket created above
Policy: pulumi.String(`{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::${bucket.id}/*"
}]
}`),
})
if err != nil {
return err
}
}
// Export the bucket name
ctx.Export("bucketName", bucket.ID())
return nil
})
}
Here, the aws.NewBucketPolicy call is wrapped in a conditional. When pulumi up runs, Pulumi evaluates this Go code. If the condition ctx.Stack() == "dev" is true, it includes the BucketPolicy in the desired state. If false, it doesn’t. Pulumi’s engine then determines that the policy should be created or not. This is powerful because it means standard Go control flow (if, for, functions, etc.) directly influences your infrastructure deployment.
The ctx.Export calls are how you expose outputs from your infrastructure. These values are displayed after pulumi up and can be referenced by other Pulumi stacks or programs. For instance, if you wanted to create a load balancer in one stack and point it to the S3 bucket in another, you could export the bucket’s endpoint from the S3 stack and then import it into the load balancer stack.
When you define a resource like aws.NewBucket, you’re not just passing arguments. You’re building a directed acyclic graph (DAG) of resources. The bucket.ID() in aws.NewBucketPolicyArgs signifies a dependency. Pulumi understands that the bucket policy depends on the S3 bucket existing. This dependency information is crucial for Pulumi to order operations correctly during pulumi up and pulumi destroy. It ensures that resources are created or deleted in the right sequence to avoid errors.
The ctx.Export("bucketName", bucket.ID()) line is where the magic of Pulumi’s state management becomes apparent. bucket.ID() is not a simple string variable at this point. It’s a Pulumi Resource Reference. When Pulumi executes this code, it knows that bucket.ID() will eventually resolve to the actual ID of the S3 bucket after it’s created. This reference is stored in Pulumi’s state file. This allows Pulumi to track the resource’s lifecycle and know its attributes even before it’s fully provisioned or after it’s been destroyed and recreated.
The next concept you’ll likely explore is managing different environments using Pulumi Stacks.