Bootstrap Pulumi self-managed backend

Pulumi supports using self-managed backends for storing infrastructure state. Let's see how we can solve the Chicken and Egg problem: create infrastructure for an AWS self-managed backend for Pulumi (and with Pulumi).

The goal is to create the following AWS resources via Pulumi:

Prerequisites #

Bootstrap and deploy backend #

Step 1: Create a new Pulumi project #

Create a new directory pulumi-backend-bootstrap, and open it in your terminal. Add the following files:

Pulumi.yaml
name: pulumi-backend-bootstrap
runtime:
name: python
options:
virtualenv: venv
description: Create resources for setting up a self-managed pulumi backend

The above file defines the Pulumi project metadata.

requirements.txt
pulumi==3.40.2
pulumi-aws==5.16.0
__main__.py
import json
import pulumi
import pulumi_aws as aws

# create S3 bucket for storing pulumi state
pulumi_backend_state_bucket = aws.s3.Bucket(
"pulumi-backend-state-bucket",
acl="private",
versioning=aws.s3.BucketVersioningArgs(enabled=True),
server_side_encryption_configuration=aws.s3.BucketServerSideEncryptionConfigurationArgs(
rule=aws.s3.BucketServerSideEncryptionConfigurationRuleArgs(
apply_server_side_encryption_by_default=aws.s3.BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultArgs(
sse_algorithm="AES256"
)
)
),
)

# block all public access for the bucket
aws.s3.BucketPublicAccessBlock(
"pulumi-backend-state-bucket-public-access-block",
bucket=pulumi_backend_state_bucket.id,
block_public_acls=True,
block_public_policy=True,
ignore_public_acls=True,
restrict_public_buckets=True,
)


aws_account_id = aws.get_caller_identity().account_id
pulumi_secrets_provider_encryption_key = aws.kms.Key(
"pulumi-secrets-provider-encryption-key",
deletion_window_in_days=10,
policy=json.dumps(
{
"Version": "2012-10-17",
"Statement": [
# policy which gives the AWS account that owns the KMS key full access to the KMS key
{
"Sid": "Enable IAM policies",
"Effect": "Allow",
"Action": "kms:*",
"Principal": {"AWS": [f"arn:aws:iam::{aws_account_id}:root"]},
"Resource": "*",
},
],
}
),
)

pulumi.export(
"PULUMI_BACKEND_URL", pulumi_backend_state_bucket.id.apply(lambda v: f"s3://{v}")
)
pulumi.export(
"Pulumi Backend Login Command",
pulumi_backend_state_bucket.id.apply(lambda v: f"pulumi login s3://{v}"),
)

pulumi.export(
"PULUMI_SECRETS_PROVIDER",
pulumi_secrets_provider_encryption_key.key_id.apply(lambda v: f"awskms:///{v}"),
)
pulumi.export(
"Pulumi Stack Init Command",
pulumi_secrets_provider_encryption_key.key_id.apply(
lambda v: f"pulumi stack init --secrets-provider='awskms:///{v}' <project-name>.<stack-name>"
),
)

The above code will create an S3 bucket and a KMS key. The S3 bucket has versioning enabled and public access blocked. The KMS key has a default key policy allowing the AWS account that owns the KMS key full access to the KMS key.

Step 2: Configure Pulumi backend #

All Pulumi programs need a backend for storing infrastructure state. This is the Chicken and Egg problem alluded to earlier, where we need to use some backend to provision our self-managed backend. Luckily for us, Pulumi supports using the local filesystem as a backend.

$> pulumi login --local
Logged in to **** as **** (file://~)

Step 3: Initialize Pulumi stack #

Run the command below in your terminal for initializing the stack. You will be prompted to enter a passphrase to encrypt any config/secrets set in the stack config file as we haven't set any secrets provider.

$> pulumi stack init dev
Created stack 'dev'
Enter your passphrase to protect config/secrets:
Re-enter your passphrase to confirm:
$> pulumi stack ls
NAME LAST UPDATE RESOURCE COUNT
dev* n/a n/a

Your file structure should now look something like this:

$> ls
__main__.py Pulumi.dev.yaml Pulumi.yaml requirements.txt

Step 4: Set your AWS region #

You should configure the AWS region you would like to use. Running the Pulumi program will create all resources in this AWS region.

$> pulumi config set aws:region eu-central-1
$> cat Pulumi.dev.yaml
encryptionsalt: v1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
config:
aws:region: eu-central-1

Step 5: Deploy the stack #

Run the command below for deploying the stack. Pulumi will create a virtualenv venv in the current directory, install all the packages as specified in requirements.txt. A preview will then be shown and deployment will proceed once confirmed.

$> pulumi up
Enter your passphrase to unlock config/secrets
(set PULUMI_CONFIG_PASSPHRASE or PULUMI_CONFIG_PASSPHRASE_FILE to remember):
Previewing update (dev):
Type Name Plan
+ pulumi:pulumi:Stack pulumi-backend-bootstrap-dev create
+ ├─ aws:s3:Bucket pulumi-backend-state-bucket create
+ ├─ aws:s3:BucketPublicAccessBlock pulumi-backend-state-bucket-public-access-block create
+ └─ aws:kms:Key pulumi-secrets-provider-encryption-key create

Outputs:
PULUMI_BACKEND_URL : output<string>
PULUMI_SECRETS_PROVIDER : output<string>
Pulumi Backend Login Command: output<string>
Pulumi Stack Init Command : output<string>

Resources:
+ 4 to create

Do you want to perform this update? yes
Updating (dev):
Type Name Status
+ pulumi:pulumi:Stack pulumi-backend-bootstrap-dev created
+ ├─ aws:s3:Bucket pulumi-backend-state-bucket created
+ ├─ aws:kms:Key pulumi-secrets-provider-encryption-key created
+ └─ aws:s3:BucketPublicAccessBlock pulumi-backend-state-bucket-public-access-block created

Outputs:
PULUMI_BACKEND_URL : "s3://pulumi-backend-state-bucket-xxxx"
PULUMI_SECRETS_PROVIDER : "awskms:///xxxx-xxxx-xxxx-xxxx-xxxx"
Pulumi Backend Login Command: "pulumi login s3://pulumi-backend-state-bucket-xxxx"
Pulumi Stack Init Command : "pulumi stack init --secrets-provider='awskms:///xxxx-xxxx-xxxx-xxxx-xxxx' <project-name>.<stack-name>"

Resources:
+ 4 created

Duration: 6s

Configure Pulumi to use self-managed backend #

Note: when using a self-managed backend with multiple Pulumi projects / stacks, it's a good practice to ensure that the stack names are unique and always namespaced with the project name: pulumi stack init <project-name>.<stack-name>. See this issue for more details.

# Specify the outputs from the previous command
export PULUMI_BACKEND_URL="<PULUMI_BACKEND_URL>"
export PULUMI_SECRETS_PROVIDER="<PULUMI_SECRETS_PROVIDER>"
pulumi stack init --secrets-provider="<PULUMI_SECRETS_PROVIDER>" <project-name>.<stack-name>

Viola! You can now use the S3 bucket as Pulumi's backend, and the KMS key as Pulumi's Secrets Provider.

The full source code for this post can be found on Github.


Since you have made it this far, sharing or retweeting this article would be highly appreciated! For feedback, please join the conversation on Twitter or ping me directly.

Published