Skip to content

Commit

Permalink
~aws-ts-static-website: Create certificate if certificateArn is omitt…
Browse files Browse the repository at this point in the history
…ed. (pulumi#199)

If certificateArn is omitted from the configuration, create an ACM certificate and associated DNS records for certificate verification.
  • Loading branch information
clstokes committed Jan 7, 2019
1 parent 51d8b74 commit 21a913e
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 9 deletions.
4 changes: 2 additions & 2 deletions aws-ts-static-website/Pulumi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ template:
aws:region:
description: The AWS region to deploy into
default: us-east-1
static-website:certificateArn:
description: ACM certificate ARN for the target domain; must be in the us-east-1 region
static-website:targetDomain:
description: The domain to serve the website at (e.g. www.example.com)
static-website:pathToWebsiteContents:
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.
63 changes: 56 additions & 7 deletions aws-ts-static-website/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
import { Output } from "@pulumi/pulumi";

import * as fs from "fs";
import * as mime from "mime";
Expand All @@ -15,8 +14,8 @@ const config = {
pathToWebsiteContents: stackConfig.require("pathToWebsiteContents"),
// targetDomain is the domain/host to serve content at.
targetDomain: stackConfig.require("targetDomain"),
// ACM certificate for the target domain. Must be in the us-east-1 region.
certificateArn: stackConfig.require("certificateArn"),
// (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"),
};

// contentBucket is the S3 bucket that the website's contents will be stored in.
Expand Down Expand Up @@ -79,11 +78,61 @@ const logsBucket = new aws.s3.Bucket("requestLogs",

const tenMinutes = 60 * 10;

let certificateArn: pulumi.Input<string> = config.certificateArn!;

/**
* Only provision a certificate (and related resources) if a certificateArn is _not_ provided via configuration.
*/
if (config.certificateArn === undefined) {

const eastRegion = new aws.Provider("east", {
region: "us-east-1", // Per AWS, ACM certificate must be in the us-east-1 region.
});

const certificate = new aws.acm.Certificate("certificate", {
domainName: config.targetDomain,
validationMethod: "DNS",
}, { provider: eastRegion });

const domainParts = getDomainAndSubdomain(config.targetDomain);
const hostedZoneId = aws.route53.getZone({ name: domainParts.parentDomain }).then(zone => zone.id);

/**
* Create a DNS record to prove that we _own_ the domain we're requesting a certificate for.
* See https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html for more info.
*/
const certificateValidationDomain = new aws.route53.Record(`${config.targetDomain}-validation`, {
name: certificate.domainValidationOptions.apply(d => d[0].resourceRecordName),
zoneId: hostedZoneId,
type: certificate.domainValidationOptions.apply(d => d[0].resourceRecordType),
records: [certificate.domainValidationOptions.apply(d => d[0].resourceRecordValue)],
ttl: tenMinutes,
});

/**
* 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
* created (or updated or deleted).
*
* See https://www.terraform.io/docs/providers/aws/r/acm_certificate_validation.html for slightly more detail
* and https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/resource_aws_acm_certificate_validation.go
* for the actual implementation.
*/
const certificateValidation = new aws.acm.CertificateValidation("certificateValidation", {
certificateArn: certificate.arn,
validationRecordFqdns: [certificateValidationDomain.fqdn],
}, { provider: eastRegion });

certificateArn = certificateValidation.certificateArn;
}

// 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 ],

// We only specify one origin for this distribution, the S3 content bucket.
Expand Down Expand Up @@ -139,9 +188,8 @@ const distributionArgs: aws.cloudfront.DistributionArgs = {
},
},

// CloudFront certs must be in us-east-1, just like API Gateway.
viewerCertificate: {
acmCertificateArn: config.certificateArn,
acmCertificateArn: certificateArn, // Per AWS, ACM certificate must be in the us-east-1 region.
sslSupportMethod: "sni-only",
},

Expand All @@ -156,7 +204,7 @@ const cdn = new aws.cloudfront.Distribution("cdn", distributionArgs);

// Split a domain name into its subdomain and parent domain names.
// e.g. "www.example.com" => "www", "example.com".
function getDomainAndSubdomain(domain: string): { subdomain: string, parentDomain: string} {
function getDomainAndSubdomain(domain: string): { subdomain: string, parentDomain: string } {
const parts = domain.split(".");
if (parts.length < 2) {
throw new Error(`No TLD found on ${domain}`);
Expand All @@ -177,7 +225,7 @@ function getDomainAndSubdomain(domain: string): { subdomain: string, parentDomai

// Creates a new Route53 DNS record pointing the domain to the CloudFront distribution.
async function createAliasRecord(
targetDomain: string, distribution: aws.cloudfront.Distribution): Promise<aws.route53.Record> {
targetDomain: string, distribution: aws.cloudfront.Distribution): Promise<aws.route53.Record> {
const domainParts = getDomainAndSubdomain(targetDomain);
const hostedZone = await aws.route53.getZone({ name: domainParts.parentDomain });
return new aws.route53.Record(
Expand All @@ -203,3 +251,4 @@ const aRecord = createAliasRecord(config.targetDomain, cdn);
export const contentBucketUri = contentBucket.bucket.apply(b => `s3:https://${b}`);
export const contentBucketWebsiteEndpoint = contentBucket.websiteEndpoint;
export const cloudFrontDomain = cdn.domainName;
export const targetDomainEndpoint = `https://${config.targetDomain}/`;

0 comments on commit 21a913e

Please sign in to comment.