From 655ef46f50d95209c39da1aa26a2d1c520b77193 Mon Sep 17 00:00:00 2001 From: Kenny Ballou Date: Mon, 9 Apr 2018 21:50:35 -0600 Subject: Add cloudformation templates and tooling Blag will be managed using cloudfront/S3/lambdaedge, specified via cloudformation. Signed-off-by: Kenny Ballou --- .gitignore | 1 + Makefile | 43 +++++++++ autogen/stack.def | 1 + autogen/stack.def.in | 1 + params/blog.tpl | 1 + stacks/blog.tpl | 233 ++++++++++++++++++++++++++++++++++++++++++++++ stacks/uri-rewrite.in | 13 +++ tools/bin/deploy-stack.sh | 44 +++++++++ 8 files changed, 337 insertions(+) create mode 100644 Makefile create mode 100644 autogen/stack.def create mode 100644 autogen/stack.def.in create mode 100644 params/blog.tpl create mode 100644 stacks/blog.tpl create mode 100644 stacks/uri-rewrite.in create mode 100755 tools/bin/deploy-stack.sh diff --git a/.gitignore b/.gitignore index a201305..7a20842 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ ssl www +_build diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2acd93f --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +PROJROOT:=$(git rev-parse --show-toplevel) + +AG:=autogen +JQ:=jq +JQ_OPTIONS:= +REGION?=us-east-1 +BUILD_DIR:=_build/$(REGION) +VERBOSE?=0 + +ifeq ($(VERBOSE),1) + VFILTER := +else + VFILTER :=>/dev/null +endif + +SOURCE_TEMPLATES:=$(wildcard stacks/*.tpl) +SOURCE_PARAMS:=$(wildcard params/*.tpl) +TEMPLATES:=$(patsubst %.tpl, $(BUILD_DIR)/%.template, $(SOURCE_TEMPLATES)) +STACK_PARAMS:=$(patsubst %.tpl, $(BUILD_DIR)/%.params, $(SOURCE_PARAMS)) + +SOURCES:=$(shell find stacks/ -type f -name '*.in') + +.PHONY: all +all: autogen/stack.def \ + $(TEMPLATES) \ + $(STACK_PARAMS) + +autogen/stack.def: autogen/stack.def.in + cat $^ > $@ + +$(BUILD_DIR)/%.json: %.tpl $(SOURCES) + mkdir -p $(dir $@) + $(AG) --override-tpl=$< --definitions=autogen/stack.def > $@ + +$(BUILD_DIR)/%.template: $(BUILD_DIR)/%.json + $(JQ) . $< > $@ + +$(BUILD_DIR)/%.params: $(BUILD_DIR)/%.json + $(JQ) '.' $< > $@ + +.PHONY: clean +clean: + -rm -r $(BUILD_DIR) diff --git a/autogen/stack.def b/autogen/stack.def new file mode 100644 index 0000000..327cb03 --- /dev/null +++ b/autogen/stack.def @@ -0,0 +1 @@ +autogen definitions blog_kennyballou_com; diff --git a/autogen/stack.def.in b/autogen/stack.def.in new file mode 100644 index 0000000..327cb03 --- /dev/null +++ b/autogen/stack.def.in @@ -0,0 +1 @@ +autogen definitions blog_kennyballou_com; diff --git a/params/blog.tpl b/params/blog.tpl new file mode 100644 index 0000000..a261a8b --- /dev/null +++ b/params/blog.tpl @@ -0,0 +1 @@ +[+ autogen5 template -*- mode: json -*- +] diff --git a/stacks/blog.tpl b/stacks/blog.tpl new file mode 100644 index 0000000..15bc7b4 --- /dev/null +++ b/stacks/blog.tpl @@ -0,0 +1,233 @@ +[+ autogen5 template -*- mode: json -*- +] +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Blog of kennyballou.com", + "Parameters": { + "DomainName": { + "Description": "Domain name of site", + "Type": "String", + "Default": "kennyballou.com" + }, + "BlogBucketName": { + "Description": "Name of S3 Bucket", + "Type": "String", + "Default": "blog.kennyballou.com" + }, + "CloudFrontHostedZone": { + "Description": "CloudFront Hosted Zone ID", + "Type": "String", + "Default": "Z2FDTNDATAQYW2" + } + }, + "Resources": { + "HostedZone": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": {"Ref": "DomainName"} + } + }, + "BlogContentBucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "Private", + "BucketName": {"Ref": "BlogBucketName"}, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionExpirationInDays": 90, + "Status": "Enabled" + } + ] + }, + "VersioningConfiguration": { + "Status": "Enabled" + }, + "WebsiteConfiguration": { + "IndexDocument": "index.html", + "ErrorDocument": "404.html" + } + } + }, + "BlogContentBucketPolicy": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": {"Ref": "BlogContentBucket"}, + "PolicyDocument": { + "Statement": [ + { + "Action": ["s3:GetObject"], + "Effect": "Allow", + "Resource": [ + {"Fn::Join": ["/", [ + {"Fn::GetAtt": [ + "BlogContentBucket", "Arn"]}, + "*" + ]]} + ], + "Principal": { + "CanonicalUser": {"Fn::GetAtt": [ + "OriginAccessId", + "S3CanonicalUserId"]} + } + } + ] + } + } + }, + "SSLCertificate": { + "Type": "AWS::CertificateManager::Certificate", + "Properties": { + "DomainName": {"Ref": "DomainName"} + } + }, + "OriginAccessId": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "S3 Bucket Access" + } + } + }, + "CFDistribution": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "Aliases": [ + {"Ref": "DomainName"} + ], + "DefaultRootObject": "index.html", + "Enabled": true, + "IPV6Enabled": true, + "HttpVersion": "http2", + "DefaultCacheBehavior": { + "TargetOriginId": {"Fn::Join": [".", [ + "s3", + {"Ref": "BlogBucketName"}]]}, + "ViewerProtocolPolicy": "redirect-to-https", + "MinTTL": 0, + "AllowedMethods": ["HEAD", "GET"], + "CachedMethods": ["HEAD", "GET"], + "ForwardedValues": { + "QueryString": true, + "Cookies": { + "Forward": "none" + } + }, + "LambdaFunctionAssociations": [ + { + "EventType": "origin-request", + "LambdaFunctionARN": { + "Ref": "URIRewriteLambdaVersion" + } + } + ] + }, + "Origins": [ + { + "S3OriginConfig": { + "OriginAccessIdentity": {"Fn::Join": ["/", [ + "origin-access-identity/cloudfront", + {"Ref": "OriginAccessId"} + ]]} + }, + "DomainName": {"Fn::Join": [".", [ + {"Ref": "BlogBucketName"}, + "s3.amazonaws.com"]]}, + "Id": {"Fn::Join": [".", [ + "s3", + {"Ref": "BlogBucketName"}]]} + } + ], + "PriceClass": "PriceClass_100", + "Restrictions": { + "GeoRestriction": { + "RestrictionType": "none", + "Locations": [] + } + }, + "ViewerCertificate": { + "SslSupportMethod": "sni-only", + "MinimumProtocolVersion": "TLSv1.1_2016", + "AcmCertificateArn": {"Ref": "SSLCertificate"} + } + } + } + }, + "BlogAliasRecord": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "AliasTarget": { + "DNSName": {"Fn::GetAtt": ["CFDistribution", "DomainName"]}, + "HostedZoneId": {"Ref": "CloudFrontHostedZone"} + }, + "HostedZoneId": {"Ref": "HostedZone"}, + "Name": {"Ref": "DomainName"}, + "Type": "A" + } + }, + "URIRewriteLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "Lambda Function performing URI rewriting", + "Code": { + "ZipFile": [+ INCLUDE "uri-rewrite.in" +] + }, + "Handler": "index.handler", + "MemorySize": 128, + "Role": {"Fn::GetAtt": ["URIRewriteLambdaRole", "Arn"]}, + "Runtime": "nodejs6.10", + "Tags": [ + {"Key": "Domain", "Value": {"Ref": "DomainName"}} + ] + } + }, + "URIRewriteLambdaRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Principal": { + "Service": [ + "edgelambda.amazonaws.com", + "lambda.amazonaws.com" + ] + } + } + ] + }, + "Policies": [ + { + "PolicyName": "GrantCloudwatchLogAccess", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "arn:aws:logs:*:*:*" + ] + } + ] + } + } + ] + } + }, + "URIRewriteLambdaVersion": { + "Type": "AWS::Lambda::Version", + "Properties": { + "FunctionName": {"Ref": "URIRewriteLambdaFunction"}, + "Description": "Lambda Function performing URI rewriting" + } + } + } +} diff --git a/stacks/uri-rewrite.in b/stacks/uri-rewrite.in new file mode 100644 index 0000000..6df707c --- /dev/null +++ b/stacks/uri-rewrite.in @@ -0,0 +1,13 @@ +[+ autogen5 template -*- mode: json -*- +] +{"Fn::Join": ["\n", [ + "'use strict';", + "exports.handler = (event, context, callback) => {", + " var whitelist = ['css', 'html', 'jpg', 'svg', 'png', 'txt', 'xml', 'pdf'];", + " var request = event.Records[0].cf.request;", + " var extension = request.uri.split('.').pop();", + " if (typeof extension == 'undefined' || !whitelist.includes(extension)) {", + " request.uri = request.uri.replace(/\\\/?$/, '\/index.html');", + " }", + " return callback(null, request);", + "};" +]]} diff --git a/tools/bin/deploy-stack.sh b/tools/bin/deploy-stack.sh new file mode 100755 index 0000000..06faf80 --- /dev/null +++ b/tools/bin/deploy-stack.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh + +set -ex + +STACK_NAME=blog-kennyballou +REGION=us-east-1 + +function deploy() { + aws cloudformation \ + --region ${REGION} \ + create-stack \ + --stack-name ${STACK_NAME} \ + --capabilities CAPABILITY_NAMED_IAM \ + --template-body file://$(pwd)/_build/${REGION}/stacks/blog.template +} + +function undeploy() { + aws cloudformation \ + --region ${REGION} \ + delete-stack \ + --stack-name ${STACK_NAME} +} + +function changeset() { + aws cloudformation \ + --region ${REGION} \ + create-change-set \ + --stack-name ${STACK_NAME} \ + --change-set-name ${STACK_NAME}-$(uuidgen) \ + --capabilities CAPABILITY_NAMED_IAM \ + --template-body file://$(pwd)/_build/${REGION}/stacks/blog.template +} + +case $1 in + deploy) + deploy + ;; + changeset) + changeset + ;; + undeploy) + undeploy + ;; +esac -- cgit v1.2.1