Skip to content

Commit

Permalink
Merge pull request pulumi#962 from beeekind/aws-ts-static-website-www…
Browse files Browse the repository at this point in the history
…-support

Update the aws-ts-static-website example to support a www subdomain
  • Loading branch information
jaxxstorm committed Apr 14, 2021
2 parents 572fb82 + 662836b commit df12a6c
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 8 deletions.
1 change: 1 addition & 0 deletions aws-ts-static-website/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pulumi.ts3.yaml
3 changes: 3 additions & 0 deletions aws-ts-static-website/Pulumi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ template:
description: Relative path to the website's contents (e.g. the `./www` folder)
static-website:certificateArn:
description: (Optional) ACM certificate ARN for the target domain; must be in the us-east-1 region. If omitted, a certificate will be created.
static-website:includeWWW:
description: If true create an A record for the www subdomain of targetDomain pointing to the generated cloudfront distribution. If a certificate was generated it will support this subdomain.
default: true
24 changes: 21 additions & 3 deletions aws-ts-static-website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Install prerequisites with:
npm install
```

Configure the Pulumi program. There are several configuration settings that need to be
Configure the Pulumi program using ```pulumi config set KEY VALUE```. There are several configuration settings that need to be
set:

- `certificateArn` - ACM certificate to serve content from. ACM certificate creation needs to be
Expand All @@ -29,6 +29,7 @@ set:
the parent domain (example.com) is a Route53 Hosted Zone in the AWS account you are running the
Pulumi program in.
- `pathToWebsiteContents` - Directory of the website's contents. e.g. the `./www` folder.
- `includeWWW` - If true this will create an additional alias record for the www subdomain to your cloudfront distribution.

## How it works

Expand All @@ -54,9 +55,11 @@ const contentFile = new aws.s3.BucketObject(

The Pulumi program then creates an `aws.cloudfront.Distribution` resource, which will serve
the contents of the S3 bucket. The CloudFront distribution can be configured to handle
things like custom error pages, cache TTLs, and so on.
things like custom error pages, cache TTLs, and so on. If includeWWW is set to true both the
cloudfront distribution and any generated certificate will contain an alias for accessing the site
from the www subdomain.

Finally, an `aws.route53.Record` is created to associate the domain name (www.example.com)
Finally, an `aws.route53.Record(s)` is created to associate the domain name (example.com)
with the CloudFront distribution (which would be something like d3naiyyld9222b.cloudfront.net).

```typescript
Expand Down Expand Up @@ -120,3 +123,18 @@ using the AWS CLI.
```bash
aws s3 sync ./www/ s3:https://example-bucket/
```

## Access Denied while creating S3 bucket

This error can occur when a bucket with the same name as targetDomain already exists. Remove all items from the pre-existing bucket
and delete the bucket to continue.

## Fail to delete S3 bucket while running pulumi destroy, this bucket is not empty.

The contents of the S3 bucket are not automatically deleted. You can manually delete these contents in the AWS Console or with
the AWS CLI.

## pulumi up fails when the targetDomain includes a www subdomain and includeWWW is set to true

This will fail because the program will attempt to create an alias record and certificate for both the targetDomain
and `www.${targetDomain}` when includeWWW is set to true.
62 changes: 57 additions & 5 deletions aws-ts-static-website/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import * as path from "path";
// so that different Pulumi Stacks can be brought up using the same code.

const stackConfig = new pulumi.Config("static-website");

const config = {
// pathToWebsiteContents is a relativepath to the website's contents.
pathToWebsiteContents: stackConfig.require("pathToWebsiteContents"),
// targetDomain is the domain/host to serve content at.
targetDomain: stackConfig.require("targetDomain"),
// (Optional) ACM certificate ARN for the target domain; must be in the us-east-1 region. If omitted, an ACM certificate will be created.
certificateArn: stackConfig.get("certificateArn"),
// If true create an A record for the www subdomain of targetDomain pointing to the generated cloudfront distribution.
// If a certificate was generated it will support this subdomain.
// default: true
includeWWW: stackConfig.getBoolean("includeWWW") || true
};

// contentBucket is the S3 bucket that the website's contents will be stored in.
Expand Down Expand Up @@ -92,10 +97,14 @@ if (config.certificateArn === undefined) {
region: "us-east-1", // Per AWS, ACM certificate must be in the us-east-1 region.
});

const certificate = new aws.acm.Certificate("certificate", {
// if config.includeWWW include required subjectAlternativeNames to support the www subdomain
let certificateConfig:aws.acm.CertificateArgs = {
domainName: config.targetDomain,
validationMethod: "DNS",
}, { provider: eastRegion });
validationMethod: "DNS",
subjectAlternativeNames: config.includeWWW ? [`www.${config.targetDomain}`]: []
}

const certificate = new aws.acm.Certificate("certificate", certificateConfig, { provider: eastRegion });

const domainParts = getDomainAndSubdomain(config.targetDomain);
const hostedZoneId = aws.route53.getZone({ name: domainParts.parentDomain }, { async: true }).then(zone => zone.zoneId);
Expand All @@ -112,6 +121,22 @@ if (config.certificateArn === undefined) {
ttl: tenMinutes,
});

// if config.includeWWW ensure we validate the www subdomain as well
var subdomainCertificateValidationDomain;
if (config.includeWWW) {
subdomainCertificateValidationDomain = new aws.route53.Record(`${config.targetDomain}-validation2`, {
name: certificate.domainValidationOptions[1].resourceRecordName,
zoneId: hostedZoneId,
type: certificate.domainValidationOptions[1].resourceRecordType,
records: [certificate.domainValidationOptions[1].resourceRecordValue],
ttl: tenMinutes,
});
}

// if config.includeWWW include the validation record for the www subdomain
const validationRecordFqdns = subdomainCertificateValidationDomain === undefined ?
[certificateValidationDomain.fqdn] : [certificateValidationDomain.fqdn, subdomainCertificateValidationDomain.fqdn]

/**
* This is a _special_ resource that waits for ACM to complete validation via the DNS record
* checking for a status of "ISSUED" on the certificate itself. No actual resources are
Expand All @@ -123,20 +148,23 @@ if (config.certificateArn === undefined) {
*/
const certificateValidation = new aws.acm.CertificateValidation("certificateValidation", {
certificateArn: certificate.arn,
validationRecordFqdns: [certificateValidationDomain.fqdn],
validationRecordFqdns: validationRecordFqdns,
}, { provider: eastRegion });

certificateArn = certificateValidation.certificateArn;
}

// if config.includeWWW include an alias for the www subdomain
const distributionAliases = config.includeWWW ? [config.targetDomain, `www.${config.targetDomain}`]:[config.targetDomain]

// distributionArgs configures the CloudFront distribution. Relevant documentation:
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html
// https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html
const distributionArgs: aws.cloudfront.DistributionArgs = {
enabled: true,
// Alternate aliases the CloudFront distribution can be reached at, in addition to https://xxxx.cloudfront.net.
// Required if you want to access the distribution via config.targetDomain as well.
aliases: [ config.targetDomain ],
aliases: distributionAliases,

// We only specify one origin for this distribution, the S3 content bucket.
origins: [
Expand Down Expand Up @@ -247,7 +275,31 @@ function createAliasRecord(
});
}

function createWWWAliasRecord(targetDomain: string, distribution: aws.cloudfront.Distribution): aws.route53.Record {
const domainParts = getDomainAndSubdomain(targetDomain);
const hostedZoneId = aws.route53.getZone({ name: domainParts.parentDomain }, { async: true }).then(zone => zone.zoneId);

return new aws.route53.Record(
`${targetDomain}-www-alias`,
{
name: `www.${targetDomain}`,
zoneId: hostedZoneId,
type: "A",
aliases: [
{
name: distribution.domainName,
zoneId: distribution.hostedZoneId,
evaluateTargetHealth: true,
},
],
}
)
}

const aRecord = createAliasRecord(config.targetDomain, cdn);
if (config.includeWWW){
const cnameRecord = createWWWAliasRecord(config.targetDomain, cdn);
}

// Export properties from this stack. This prints them at the end of `pulumi up` and
// makes them easier to access from the pulumi.com.
Expand Down

0 comments on commit df12a6c

Please sign in to comment.