Pulumi Component Resource - IaC Reusable Block

Like Terraform, Pulumi [1] is also an IaC (infrastructure-as-code)tool. It takes a programming-first approach to infrastructure management and configuration. That way, one can leverage the complete power of the programming language for infra needs. It has providers for all major platforms like AWS, GCP, Azure, K8s It supports multiple popular languages like Go, Python, Typescript, Java. In this article, we will focus on Pulumi's reusable abstraction: Component Resource [2]

Prerequisite

Install Pulumi, Golang, AWS Cli on your local system.

Component Resource

Resource are building blocks in Pulumi. Component Resource is a logical grouping of one or more than one resource. It can help with enforcing standardizations, guard-rails & proper-tagging of resources

Here we will go with a simple exercise of creating a component-resource in Pulumi using Golang. This creates a simple hello-world lambda in aws. Once defined, we can create as many instances of custom-resource (lambda in this case) as we want. For this example, we shall be using Golang.

We will go with the following steps.

  1. Lambda Handler Definition
  2. Component Resource Definition: Lambda + IAM (Role + Policies)
  3. Component Resource Instantiation: How To Use Component Resource

1. Lambda Handler Definition

package main

import (
	"context"
	"encoding/json"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
	body := map[string]any{
		"message": "hello world 👋",
		"path":    req.RawPath,
	}
	b, _ := json.Marshal(body)

	return events.APIGatewayV2HTTPResponse{
		StatusCode: 200,
		Headers: map[string]string{
			"content-type": "application/json",
		},
		Body: string(b),
	}, nil
}

func main() {
	lambda.Start(handler)
}

2. Component Resource Definition: Lambda + IAM (Role + Policies)

package components

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/lambda"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

type SimpleLambdaArgs struct {
	PathToBinary string
}

type SimpleLambdaComponent struct {
	pulumi.ResourceState  // important for pulumi's state tracking

	Function *lambda.Function `pulumi:"function"`
}

func NewSimpleLambdaComponent(ctx *pulumi.Context, name string, args *SimpleLambdaArgs, opts ...pulumi.ResourceOption) (*SimpleLambdaComponent, error) {
	component := &SimpleLambdaComponent{}

	// register the component with pulumi engine
	err := ctx.RegisterComponentResource("pkg:component:simple:lambda", name, component, opts...)
	if err != nil {
		return nil, err
	}

	// 3. Create an IAM Role for the Lambda
	role, err := iam.NewRole(ctx, "iam-role-"+name, &iam.RoleArgs{AssumeRolePolicy: pulumi.String(`
                    {  
				  "Version": "2012-10-17",
				  "Statement": [{
					"Action": "sts:AssumeRole",
					"Principal": {"Service": "lambda.amazonaws.com"},
					"Effect": "Allow"
				  }]
				}
       `)}, pulumi.Parent(component))

	if err != nil {
		return nil, err
	}

	// 4. Attach the Basic Execution Policy (for logging to CloudWatch)
	_, err = iam.NewRolePolicyAttachment(ctx, name+"-policy", &iam.RolePolicyAttachmentArgs{
		Role:      role.Name,
		PolicyArn: pulumi.String("arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"),
	}, pulumi.Parent(component))
	if err != nil {
		return nil, err
	}

	component.Function, err = lambda.NewFunction(ctx, name+"-lambda", &lambda.FunctionArgs{
		Role:    role.Arn,
		Runtime: pulumi.String("provided.al2023"), // Modern Go runtime
		Handler: pulumi.String("bootstrap"),       // AWS standard handler name
		Code: pulumi.NewAssetArchive(map[string]interface{}{
			"bootstrap": pulumi.NewFileAsset(args.PathToBinary),
		}),
	}, pulumi.Parent(component))
	if err != nil {
		return nil, err
	}

	// This ensures outputs are resolved and visible.
	if err := ctx.RegisterResourceOutputs(component, pulumi.Map{
		"functionName": component.Function.Name,
		"functionArn":  component.Function.Arn,
	}); err != nil {
		return nil, err
	}

	return component, nil
}

3. Component Resource Instantiation: How To Use Component Resource

package main

import (
	pkg "lambda-hello/components"

	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		// Instantiate our custom Component
		myLambda, err := pkg.NewSimpleLambdaComponent(ctx, "simple-lambda", &pkg.SimpleLambdaArgs{
			PathToBinary: "./lambda/bin/bootstrap",
		})
		if err != nil {
			return err
		}

		// Export the function name so we can test it later
		ctx.Export("lambdaName", myLambda.Function.Name)

		return nil
	})
}

Running pulumi up on local system, we get pulumi_success