How I Did It: Step-by-Step Tutorial

Here’s how I built this site and other AWS resources with Pulumi. Let’s walk through it together!

1. Setting Up Pulumi and AWS CLI on Ubuntu

sudo apt update
sudo apt install unzip curl -y
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
aws --version

curl -fsSL https://get.pulumi.com | sh
echo 'export PATH=$PATH:$HOME/.pulumi/bin' >> ~/.bashrc
source ~/.bashrc
pulumi version

I installed AWS CLI and Pulumi on Ubuntu. The --version commands confirmed everything worked.

2. Creating an IAM User in AWS

In the AWS console, I set up a user for Pulumi:

  1. Go to IAM > User Groups > Create Group.
  2. Name it PulumiUsers.
  3. Attach policies: AmazonEC2FullAccess, AmazonS3FullAccess, CloudFrontFullAccess, IAMFullAccess.
  4. Create the group.
  5. Go to Users > Add users.
  6. Name: pulumi-user, access: Programmatic access, group: PulumiUsers.
  7. Save the Access Key ID and Secret Access Key from the .csv.
IAM User Creation

3. Configuring AWS Credentials

aws configure

I entered my Access Key ID, Secret Access Key, region (us-east-1), and output format (json).

AWS CLI Config

4. Creating an EC2 Instance

mkdir my-ec2 && cd my-ec2
pulumi new vm-aws-python

I used Pulumi’s vm-aws-python template to spin up a simple EC2 instance. Then I ran:

pulumi up

Confirmed with "yes," and my first IaC resource was live!

EC2 Architecture

5. Building the Static S3 Website

mkdir my-site && cd my-site
pulumi new static-website-aws-python

I created a new project for the static site you’re seeing now.

6. Fixing an S3 Error

When I ran pulumi up, I got this error:

TypeError: BucketV2._internal_init() got an unexpected keyword argument 'website'
Pulumi Error

The fix? I updated __main__.py to separate the bucket and website config:

bucket = aws.s3.BucketV2("bucket")
aws.s3.BucketWebsiteConfigurationV2(
    "bucket-website",
    bucket=bucket.bucket,
    index_document={"suffix": "index.html"},
    error_document={"key": "error.html"}
)
aws.s3.BucketPublicAccessBlock("public-access-block", bucket=bucket.bucket, block_public_acls=False)
aws.s3.BucketOwnershipControls("ownership-controls", bucket=bucket.bucket, rule={"object_ownership": "ObjectWriter"})

7. Adding CloudFront and Deploying

I edited __main__.py to include CloudFront (full code below), then deployed:

pulumi up --refresh

The --refresh synced everything, and I got the URLs for my site!

S3 Architecture

8. Final Code

Here’s the complete __main__.py for this site:

import pulumi
import pulumi_aws as aws
import pulumi_synced_folder as synced_folder

config = pulumi.Config()
path = config.get("path") or "./www"
index_document = config.get("indexDocument") or "index.html"
error_document = config.get("errorDocument") or "error.html"

bucket = aws.s3.BucketV2("bucket")
bucket_website = aws.s3.BucketWebsiteConfigurationV2(
    "bucket-website", bucket=bucket.bucket,
    index_document={"suffix": index_document}, error_document={"key": error_document}
)
aws.s3.BucketOwnershipControls("ownership-controls", bucket=bucket.bucket, rule={"object_ownership": "ObjectWriter"})
aws.s3.BucketPublicAccessBlock("public-access-block", bucket=bucket.bucket, block_public_acls=False)
bucket_folder = synced_folder.S3BucketFolder(
    "bucket-folder", acl="public-read", bucket_name=bucket.bucket, path=path,
    opts=pulumi.ResourceOptions(depends_on=[ownership_controls, public_access_block])
)

cdn = aws.cloudfront.Distribution(
    "cdn", enabled=True,
    origins=[{
        "origin_id": bucket.arn, "domain_name": bucket_website.website_endpoint,
        "custom_origin_config": {"origin_protocol_policy": "http-only", "http_port": 80, "https_port": 443, "origin_ssl_protocols": ["TLSv1.2"]}
    }],
    default_cache_behavior={
        "target_origin_id": bucket.arn, "viewer_protocol_policy": "redirect-to-https",
        "allowed_methods": ["GET", "HEAD", "OPTIONS"], "cached_methods": ["GET", "HEAD", "OPTIONS"],
        "default_ttl": 600, "max_ttl": 600, "min_ttl": 600,
        "forwarded_values": {"query_string": True, "cookies": {"forward": "all"}}
    },
    price_class="PriceClass_100",
    custom_error_responses=[{"error_code": 404, "response_code": 404, "response_page_path": f"/{error_document}"}],
    restrictions={"geo_restriction": {"restriction_type": "none"}},
    viewer_certificate={"cloudfront_default_certificate": True}
)

pulumi.export("originURL", pulumi.Output.concat("http://", bucket_website.website_endpoint))
pulumi.export("cdnURL", pulumi.Output.concat("https://", cdn.domain_name))