AWS Cloud Static Website Hosting
Many years ago when AWS began 'giving away' free servers (EC2 instances) I signed up.
For 12 months you get a free t2.micro (about as powerful as a mid range laptop) instance to do with what you want.
Naturally I thought I would use the server to host my personal website.
This website went though many generations, eventually I installed a LAMP stack and wordpress and just left it running.
I have a love/hate relationship with Wordpress. When it works as you want it to it's so very easy.
But then when you go to view your website after a few weeks absence and you get a MySQL error, you begin to realise you may have made a mistake.
It is not quite as fire and forget as they'd like you to believe.. And it's an absolute honey pot for hackers.
After many years the EC2 hosting costs for this t2.micro instance began to creep up from $8 a month to over $20.
At that point I decided that I didn't really need an actual server of my own as I never really got around to using it's full potential.
I knew I could reduce my costs by just flattening the HTML out and uploading it to S3 as a static site, but then I'd forever lose any ability to
render server side code.
So I began a quest to create an AWS hosted S3 based static website fronted by CloudFront, and with a lambda element allowing the rendering of server side code if necessary.
Being me I didn't just do this through the AWS console. I wanted to be able to recreate it if something went wrong, or delete it if it became too expensive.
So I created some IAC to create the entire thing. This bash script is the meat of that code.
A quick guide.
-
This file belongs in the following directory structure:
root_folder/ |-- Legacy Stuff/ # Ignore. /S3 isn't the root of my git repo |-- S3/ | |-- iac | | `-- iac.sh # Infrastructure and CICD script | |-- lambda | | `-- lambda_function.py # The actual lambda function | |-- output # directory in which rendered html is placed | |-- site # jinja2 templates | | |-- fragments # page sections used by jinja | | |-- blog | | |-- css | | |-- images | | |-- etc, . | | `-- index.html | |-- build.py # runs jinja to process /site to /output | `-- build.sh # just calls the python script |-- .gitignore `-- README.md
- You will need an AWS profile configured in ~/.aws/credentials etc. Make sure that this is configured correctly in the PROFILE variable
- begin by modifying the rest of the global variables at the top of the file to match your specific needs (ignore the github variables they're not used)
- Source this file (The one below, callit iac.sh, in bash type 'source ./iac.sh') and then just use the functions (ie, after sourcing just type 'site_create' etc.)
- You'll need aws cli, python3, the Jinja2 python module, zip and some other things installing
- If you don't know anything about Jinja don't worry, html files are fine in the /site directory
You'll have to pick the bones out of it as I can't be bothered explaining what it is and does.
You can see that I used generative AI to intially create the script using the conventions in the top of the file.
But right now (2024) the AI produced running code, that just didn't work. There's many hours of work in this.
#!/bin/bash
######################################
## Script creates a static S3 website
## Static content is pulled from GitHub
######################################
# AI prompts
# When writing a bash script which makes use of the AWS CLI I always use the following conventions:
# * All calls to aws cli functions should have the '--profile' and '--region' parameters set from global variables, and those parameters should come immediately after the opening 'aws' keyword of the cli command
# * If an aws resource (a role, lambda, ec2 instance etc) is being created, it should be done so in a bash function
# * All bash functions should be prefixed with the 'function' keyword
# * Any local variables inside bash functions should be declared 'local' using the 'local' keyword
# * If a resource has a create function, a matching 'isExist' function should be created which checks for the existinance of that resource
# * Any bash function which checks for the existance of a resource should be named 'isResourceExists' where 'resource' is replaced with the type of resource (ie isEc2Exists)
# * Any bash function which returns a boolean should use 'return 0' and 'return 1' such that it can be used like 'if functionname; then'
# * All bash functions and variables should be named in camel case (ie isEc2Exists, createEc2Instance, instanceId etc.)
# * if a resource has a create function and an isExists function it should also have a 'destroy' function
# * In the create and destroy functions for any given resource, a check should first be made to the isExists function before the create or destroy is attempted
# * Any resource required to create any other resource (such as a role required for a lambda function) must also have create, exists and destroy functions such that when a resource is destroyed all it's dependent resources can also be destroyed
# * where possible resources should be created with a tag or other field that allows these resources to be found using that tag or comment etc.
# * exists methods should then use the tags or comment fields to find the requested resource
# * If a resource requires JSON or YAML config to be sent, the config should be created inline inside the script using multiline comments rather than references to external files
# * If during the generation of the script you reach a limit on the number of output lines, you should continue with a second response until the script is complete.
# * All create and delete functions should be called from global 'create' and 'destroy' functions
# * Presently I will ask you to create a bash script using these conventions, no response is required right now
# Using these conventions please create a bash script which sets up an S3 and cloudfront website
# The cloudfront distribution should return HTTP protocol, and the defaultRootObject should be index.html
# The S3 bucket should be only accessible by cloudfront
# There should also be a second origin in the cloudfront configuration that sends any request to the /lambda context root to a lambda function instead of a file got from S3
# That lambda function should be created in the script and the function should be created to respond to any HTTP get or post with a response containing the body 'Hello World'
#####
# OLD EC2 IP : 54.93.65.166
#####
# AWS shenannigans
export AWS_PAGER=""
# Variables
DOMAIN_NAME="richardsenior.net"
BUCKET_NAME="richardsenior"
REGION="eu-central-1"
DISTRIBUTION_COMMENT="S3 bucket exposed via CloudFront"
PROFILE="rms"
TAG_NAME="richardsenior"
GITHUB_REPO="richardsenior.net"
GITHUB_SECRET_NAME="CLOUDFRONT_DISTRIBUTION_ID"
LAMBDA_FUNCTION_NAME="richardsenior-lambda"
LAMBDA_ROLE_NAME="rms-lambda-role"
function isPythonModuleInstalled {
if [ -z "$1" ]; then
echo "must supply the name of the module in the first parameter"
return 1
fi
python3 -c "import sys, pkgutil; sys.exit(0 if pkgutil.find_loader(sys.argv[1]) else 1)" $1
}
function isApplicationInstalled() {
if [ -z "$1" ]; then
echo "must supply the name of the command in the first parameter"
return 1
fi
if [ -z "$(command -v $1)" ]; then
return 1
else
return 0
fi
}
function deleteRole {
if [ -z "$1" ]; then
echo "must supply role name in second parameter"
exit 1
fi
local INSTANCE_PROFILES=$(aws --profile $PROFILE --region $REGION iam list-instance-profiles-for-role --role-name $1 --query "InstanceProfiles[].InstanceProfileId" --output text)
if [ ! -z "$INSTANCE_PROFILES" ]; then
echo "found instance profiles for this role"
for IPR in $INSTANCE_PROFILES; do
echo "$IPR"
done
#TODO something like aws ec2 describe-iam-instance-profile-associations
#is instance still existing?
echo "Role may still be in use.. ignoring it"
return
fi
#attached policies
local POLICIES=$(aws --profile $PROFILE --region $REGION iam list-attached-role-policies --role-name $1 --query "AttachedPolicies[].PolicyArn" --output text)
if [ ! -z "$POLICIES" ]; then
for POLICY in $POLICIES; do
local ATTACHMENT_COUNT=$(aws --profile $PROFILE --region $REGION iam get-policy --policy-arn $POLICY --query "Policy.AttachmentCount" --output text)
echo "detaching $POLICY from $1"
aws --profile $PROFILE --region $REGION iam detach-role-policy --role-name $1 --policy-arn $POLICY
if [ 1 == $ATTACHMENT_COUNT ]; then
echo "leaving policies alone as they are service managed"
#echo "Policy is only attached to $ATTACHMENT_COUNT roles, deleting it."
#aws --profile $PROFILE --region $REGION iam delete-policy --policy-arn $POLICY
else
echo "Detatch but don't delete policy because it's attached to $ATTACHMENT_COUNT roles"
fi
done
fi
#Inline policies
local POLICIES=$(aws --profile $PROFILE --region $REGION iam list-role-policies --role-name $1 --query "PolicyNames[]" --output text)
if [ ! -z "$POLICIES" ]; then
for POLICY in $POLICIES; do
aws --profile $PROFILE --region $REGION iam delete-role-policy --role-name $1 --policy-name $POLICY
done
fi
echo "Deleting role $1"
aws --profile $PROFILE --region $REGION iam delete-role --role-name $1
}
function getAccountId {
local ret=$(aws --profile rms --region $REGION sts get-caller-identity --query "Account" --output text)
if [ -z "$ret" ]; then
echo "couldn't get account number"
exit 1
fi
echo "$ret"
}
# Function to check if S3 bucket exists
function isBucketExists {
local bucket_name=$(aws --profile $PROFILE --region $REGION s3api list-buckets --query "Buckets[?contains(Name, '$BUCKET_NAME')].Name" --output text)
if [ -z "$bucket_name" ]; then
return 1
else
return 0
fi
}
function waitForDistributionDeployment {
local did=$(getDistributionId)
echo 'Waiting for distribution to be deployed...'
aws --profile "$PROFILE" --region "$REGION" cloudfront wait distribution-deployed --id $did
}
function getDistributionStatus {
local did=$(getDistributionId)
local status=$(aws --profile "$PROFILE" --region "$REGION" cloudfront get-distribution --id $did --query "Distribution.Status" --output text)
echo "$status"
}
function getDistributionArn {
local did=$(getDistributionId)
local arn=$(aws --profile "$PROFILE" --region "$REGION" cloudfront get-distribution --id $did --query "Distribution.ARN" --output text)
echo "$arn"
}
function getDistributionUrl {
local did=$(getDistributionId)
local dn=$(aws --profile "$PROFILE" --region "$REGION" cloudfront get-distribution --id $did --query "Distribution.DomainName" --output text)
local dn="https://$dn"
echo "$dn"
}
# Notes: fix jq errors related to quoting by passing in a parameter in bash
function getDistributionId {
local ret=$(aws --profile "$PROFILE" --region "$REGION" cloudfront list-distributions | jq -r --arg origin_id "$TAG_NAME" '.DistributionList.Items[] | select(.Origins.Items[].Id == $origin_id) | .Id')
echo "$ret"
}
function isDistributionExists {
local did=$(getDistributionId)
if [ -z "$did" ]; then
return 1
else
return 0
fi
}
# Function to create S3 bucket
function create_s3_bucket {
if isBucketExists; then
echo "bucket exists"
return;
fi
echo "creating bucket"
if ! isOaiExists; then
create_oai
fi
local oaiid=$(getOaiId)
if [ -z "$oaiid" ]; then
echo "failed to get oai id"
exit 1
else
echo "OAI id is $oaiid"
fi
aws --profile $PROFILE --region $REGION s3api create-bucket --bucket $BUCKET_NAME --create-bucket-configuration LocationConstraint=$REGION
local p=$(cat <<-END
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity $oaiid"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::$BUCKET_NAME/*"
}
]
}
END
)
echo "policy is \n $p"
aws --profile $PROFILE --region $REGION s3api put-bucket-policy --bucket $BUCKET_NAME --policy "$p"
aws --profile $PROFILE --region $REGION s3api put-bucket-tagging --bucket $BUCKET_NAME --tagging "TagSet=[{Key=Name,Value=$TAG_NAME}]"
#echo "bucket created, now enabling static website hosting"
#aws --profile $PROFILE --region $REGION s3 website s3://$BUCKET_NAME/ --index-document index.html --error-document error.html
echo "Uploading a couple of files just for testing purposes"
uploadInitalFiles
}
function getOaiId {
local ol=$(aws --profile $PROFILE --region $REGION cloudfront list-cloud-front-origin-access-identities --query 'CloudFrontOriginAccessIdentityList.Items[*].Id' --output text)
if [ 0 -ne $? ]; then return; fi
if [ -z "$ol" ]; then return 1; fi
for id in $ol; do
local comment=$(aws --profile $PROFILE --region $REGION cloudfront get-cloud-front-origin-access-identity --id $id --query "CloudFrontOriginAccessIdentity.CloudFrontOriginAccessIdentityConfig.Comment" --output text)
if [ -z "$comment" ]; then continue; fi
if [ "$TAG_NAME" != "$comment" ]; then continue; fi
echo "$id"
return
done
}
function isOaiExists {
local ol=$(aws --profile $PROFILE --region $REGION cloudfront list-cloud-front-origin-access-identities --query 'CloudFrontOriginAccessIdentityList.Items[*].Id' --output text)
if [ -z "$ol" ]; then return 1; fi
for id in $ol; do
local comment=$(aws --profile $PROFILE --region $REGION cloudfront get-cloud-front-origin-access-identity --id $id --query "CloudFrontOriginAccessIdentity.CloudFrontOriginAccessIdentityConfig.Comment" --output text)
if [ -z "$comment" ]; then
echo "oai with ID $id has no comment"
continue;
fi
if [ "$TAG_NAME" != "$comment" ]; then
echo "oai $comment does not equal $TAG_NAME"
continue;
fi
return 0
done
return 1
}
function isOacExists {
local id=$(getOacId)
if [ -z "$id" ]; then return 1; else return 0; fi
}
function getOacId {
#aws --profile rms --region eu-central-1 cloudfront list-origin-access-controls --query 'OriginAccessControlList.Items[*].Id' --output text
local acs=$(aws --profile $PROFILE --region $REGION cloudfront list-origin-access-controls --query 'OriginAccessControlList.Items[*].Id' --output text)
if [ 0 -ne $? ]; then return; fi
if [ -z "$acs" ]; then return; fi
for id in $acs; do
# aws --profile rms --region eu-central-1 cloudfront get-origin-access-control --id E5ZOMM9UC8YWY --query "OriginAccessControl.OriginAccessControlConfig.Name" --output text
local name=$(aws --profile $PROFILE --region $REGION cloudfront get-origin-access-control --id $id --query "OriginAccessControl.OriginAccessControlConfig.Name" --output text)
if [ -z "$name" ]; then continue; fi
if [ "$TAG_NAME" != "$name" ]; then continue; fi
echo "$id"
return
done
}
function deleteOac {
local id=$(getOacId)
if [ -z "$id" ]; then return;fi
local oac=$(aws --profile $PROFILE --region $REGION cloudfront get-origin-access-control --id $id --output json)
local etag=$(echo "$oac" | jq -r '.ETag')
aws --profile $PROFILE --region $REGION cloudfront delete-origin-access-control --id $id --if-match $etag
}
function createOac {
local id=$(getOacId)
if [ ! -z "$id" ]; then return; fi
aws --profile $PROFILE --region $REGION cloudfront create-origin-access-control --origin-access-control-config "Name=$TAG_NAME,SigningProtocol=sigv4,SigningBehavior=always,OriginAccessControlOriginType=lambda"
#aws --profile rms --region eu-central-2 cloudfront create-origin-access-control --origin-access-control-config "Name=richardsenior,SigningProtocol=sigv4,SigningBehavior=always,OriginAccessControlOriginType=lambda" --query "Id" --output text
}
function create_oai {
if isOaiExists; then return; fi
echo "creating OAI with id:"
#aws --profile $PROFILE --region $REGION cloudfront create-cloud-front-origin-access-identity --cloud-front-origin-access-identity-config CallerReference=$(date +%s),Comment="$TAG_NAME" --query 'CloudFrontOriginAccessIdentity.Id' --output text
aws --profile $PROFILE --region $REGION cloudfront create-cloud-front-origin-access-identity --cloud-front-origin-access-identity-config CallerReference=$TAG_NAME,Comment="$TAG_NAME" --query 'CloudFrontOriginAccessIdentity.Id' --output text
}
function delete_oai {
local ol=$(aws --profile $PROFILE --region $REGION cloudfront list-cloud-front-origin-access-identities --query 'CloudFrontOriginAccessIdentityList.Items[*].Id' --output text)
if [ -z "$ol" ]; then return 1; fi
for id in $ol; do
if [ -z "$id" ]; then continue; fi
if [ "None" == "$id" ]; then continue; fi
local comment=$(aws --profile $PROFILE --region $REGION cloudfront get-cloud-front-origin-access-identity --id $id --query "CloudFrontOriginAccessIdentity.CloudFrontOriginAccessIdentityConfig.Comment" --output text)
if [ -z "$comment" ]; then continue; fi
# TODO put this back in when it's all working
#if [ "$TAG_NAME" != "$comment" ]; then
# echo "$TAG_NAME is not equal to $comment"
# continue;
#fi
local etag=$(aws --profile $PROFILE --region $REGION cloudfront get-cloud-front-origin-access-identity-config --id "$id" --query 'ETag' --output text)
aws --profile $PROFILE --region $REGION cloudfront delete-cloud-front-origin-access-identity --id "$id" --if-match "$etag"
echo "deleted $id"
done
}
function getDistributionConfig {
local enabled="true"
if [ ! -z "$1" ]; then enabled="false";fi
local oaid=$(getOaiId)
local oaiconf="origin-access-identity/cloudfront/$oaid"
if [ ! -z "$1" ]; then oaiconf="";fi
#ambda.$REGION.amazonaws.com
local fdom=$(getFunctionDomain)
local oacid=$(getOacId)
# website url would be : $BUCKET_NAME.s3-website.$REGION.amazonaws.com
# "CallerReference": "$(date +%s)",
local distribution_config=$(cat <<-END
{
"CallerReference": "$TAG_NAME",
"Comment": "$DISTRIBUTION_COMMENT",
"Enabled": $enabled,
"PriceClass": "PriceClass_100",
"IsIPV6Enabled": false,
"HttpVersion": "http2",
"ViewerCertificate": {
"CloudFrontDefaultCertificate": true
},
"Origins": {
"Quantity": 2,
"Items": [
{
"Id": "$BUCKET_NAME",
"DomainName": "$BUCKET_NAME.s3.$REGION.amazonaws.com",
"S3OriginConfig": {
"OriginAccessIdentity": "$oaiconf"
}
},
{
"Id": "$LAMBDA_FUNCTION_NAME",
"OriginAccessControlId": "$oacid",
"DomainName": "$fdom",
"CustomOriginConfig": {
"HTTPPort": 80,
"HTTPSPort": 443,
"OriginProtocolPolicy": "https-only"
}
}
]
},
"DefaultRootObject": "index.html",
"DefaultCacheBehavior": {
"TargetOriginId": "$BUCKET_NAME",
"ViewerProtocolPolicy": "allow-all",
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"],
"CachedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
}
},
"ForwardedValues": {
"QueryString": false,
"Cookies": {
"Forward": "none"
}
},
"MinTTL": 0,
"MaxTTL": 0
},
"CacheBehaviors": {
"Quantity": 1,
"Items": [
{
"PathPattern": "/lambda*",
"TargetOriginId": "$LAMBDA_FUNCTION_NAME",
"ViewerProtocolPolicy": "allow-all",
"AllowedMethods": {
"Quantity": 7,
"Items": ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"],
"CachedMethods": {
"Quantity": 2,
"Items": ["HEAD", "GET"]
}
},
"ForwardedValues": {
"QueryString": false,
"Cookies": {
"Forward": "none"
}
},
"MinTTL": 0,
"MaxTTL": 0
}
]
}
}
END
)
echo "$distribution_config"
}
# Function to create CloudFront distribution
function create_cloudfront_distribution {
if isDistributionExists; then
echo "distribution already exists"
return
fi
local conf=$(getDistributionConfig)
echo "creating distribution with config : $conf"
aws --profile $PROFILE --region $REGION cloudfront create-distribution --distribution-config "$conf" --output json
# now update lambda permissions etc.
local arn=$(getDistributionArn)
aws --profile $PROFILE --region $REGION lambda add-permission \
--statement-id "AllowCloudFrontServicePrincipal" \
--action "lambda:InvokeFunctionUrl" \
--principal "cloudfront.amazonaws.com" \
--source-arn "$arn" \
--function-name $LAMBDA_FUNCTION_NAME
}
# Function to update GitHub secret
function update_github_secret {
if [ -z "$1" ]; then
echo "must supply distribution id in first parameter"
return 1
fi
local secret_value=$1
local public_key=$(curl -s -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/$GITHUB_REPO/actions/secrets/public-key)
local key_id=$(echo $public_key | jq -r '.key_id')
local key=$(echo $public_key | jq -r '.key')
local encrypted_value=$(echo -n $secret_value | openssl rsautl -encrypt -pubin -inkey <(echo $key | base64 -d) | base64)
curl -X PUT -H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"encrypted_value\":\"$encrypted_value\",\"key_id\":\"$key_id\"}" \
https://api.github.com/repos/$GITHUB_REPO/actions/secrets/$GITHUB_SECRET_NAME
}
function getLambdaRoleArn {
local foo=$(aws --profile $PROFILE --region $REGION iam get-role --role-name $LAMBDA_ROLE_NAME --query 'Role.Arn' --output text 2>/dev/null | xargs)
if [ -z "$foo" ]; then
echo "failed to get role arn.. Fatal"
exit 1
fi
echo "$foo"
}
function isLambdaRoleExists {
local foo=$(aws --profile $PROFILE --region $REGION iam get-role --role-name $LAMBDA_ROLE_NAME --query 'Role.RoleName' --output text 2>/dev/null | xargs)
if [ -z "$foo" ]; then
return 1
else
return 0
fi
}
# Function to create IAM role for Lambda
function create_lambda_role {
if ! isLambdaRoleExists; then
echo "Creating Lambda Role"
local p=$(cat <<-END
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com",
"edgelambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
END
)
aws --profile $PROFILE --region $REGION iam create-role --role-name $LAMBDA_ROLE_NAME --assume-role-policy-document "$p"
aws --profile $PROFILE --region $REGION iam tag-role --role-name $LAMBDA_ROLE_NAME --tags Key=Name,Value=$TAG_NAME
aws --profile $PROFILE --region $REGION iam attach-role-policy --role-name $LAMBDA_ROLE_NAME --policy-arn arn:aws:iam::aws:policy/service-role/AmazonS3ObjectLambdaExecutionRolePolicy
fi
}
# Function to delete IAM role for Lambda
function delete_lambda_role {
if ! isLambdaRoleExists; then
echo "no lambda role exists"
return
fi
deleteRole "$LAMBDA_ROLE_NAME"
}
function getLambdafunctionArn {
local ret=$(aws --profile $PROFILE --region $REGION lambda get-function --function-name $LAMBDA_FUNCTION_NAME --query "Configuration.FunctionArn" --output text)
echo "$ret"
}
function isLambdaFunctionExists {
aws --profile $PROFILE --region $REGION lambda get-function --function-name $LAMBDA_FUNCTION_NAME > /dev/null 2>&1
if [ 0 -eq $? ]; then
return 0
else
return 1
fi
}
function createLambdaZip {
echo "deleting current lambda zip if it exists.."
if [ -f "../lambda/lambda.zip" ]; then
echo "lambda zip exists, deleting.."
rm ../lambda/lambda.zip 2> /dev/null
else
echo "no lambda zip exists"
fi
echo "zipping lambda function.."
zip -r ../lambda/lambda.zip ../lambda/ -x '**/.DS_Store' -j
if [ ! -f "../lambda/lambda.zip" ]; then
echo "FAILED TO BUILD THE LAMBDA ZIP"
return
else
echo "zip created"
fi
}
function syncS3 {
buildHtml
echo "running : aws --region $REGION S3 sync ../output s3://$BUCKET_NAME --size-only --delete"
aws --profile $PROFILE --region $REGION s3 sync ../output s3://$BUCKET_NAME --size-only --delete
}
function doGithub {
local did=$(aws --region "$REGION" cloudfront list-distributions | jq -r --arg origin_id "$TAG_NAME" '.DistributionList.Items[] | select(.Origins.Items[].Id == $origin_id) | .Id')
if [ 0 -nq $? ]; then
echo "Failed to find the distribution id (bad response from aws call) [$?]"
exit 1
fi
if [ -z "$did" ]; then
echo "Failed to find the distribution id (null response from aws call) [$did]"
exit 1
fi
echo "--- BUILDING HTML FROM JINJA TEMPLATES AND DEPLOYING TO S3 ---"
buildHtml
echo "running : aws --region $REGION S3 sync ../output s3://$BUCKET_NAME --size-only --delete"
aws --region $REGION s3 sync ../output s3://$BUCKET_NAME --size-only --delete
echo "--- CREATING LAMBDA ZIP, AND UPDATING LAMBDA SOURCE CODE ---"
createLambdaZip
echo "updating lambda in github pipeline"
aws --region $REGION lambda update-function-code --function-name $LAMBDA_FUNCTION_NAME --zip-file fileb://../lambda/lambda.zip
echo "--- CREATING CLOUDFRONT CACHE INVALIDATION TO UPDATE THE SITE ---"
aws --region $REGION cloudfront create-invalidation --distribution-id "$did" --paths "/*"
local dn=$(aws --region "$REGION" cloudfront get-distribution --id $did --query "Distribution.DomainName" --output text)
local dn="https://$dn"
echo "THE CLOUDFRONT DISTRIBUTION URL IS $dn"
}
function isLambdaHasUrlConfig {
aws --profile $PROFILE --region $REGION lambda get-function-url-config --function-name $LAMBDA_FUNCTION_NAME > /dev/null 2>&1
if [ 0 -eq $? ]; then
return 0
else
return 1
fi
}
function getFunctionDomain {
local u=$(getFunctionUrl)
local d=$(echo "$u" | cut -d '/' -f 3)
echo "$d"
}
function getFunctionUrl {
if ! isLambdaHasUrlConfig; then
echo "Cannot get function url if there is no config for it"
exit 1
fi
local ret=$(aws --profile $PROFILE --region $REGION lambda get-function-url-config --function-name $LAMBDA_FUNCTION_NAME --query "FunctionUrl" --output text)
echo "$ret"
}
function deleteLambdaUrlConfig {
if ! isLambdaHasUrlConfig; then return; fi
aws --profile $PROFILE --region $REGION lambda delete-function-url-config --function-name $LAMBDA_FUNCTION_NAME
}
function createLambdaUrlConfig {
if isLambdaHasUrlConfig; then return; fi
echo "setting lambda auth-type"
# now set the auth type to AWS_IAM so that only cloudfront can invoke it
aws --profile $PROFILE --region $REGION lambda create-function-url-config \
--function-name $LAMBDA_FUNCTION_NAME \
--auth-type "AWS_IAM"
}
# Function to create Lambda function
function create_lambda_function {
if ! isLambdaRoleExists; then
echo "Lambda role not created? Creating.."
create_lambda_role;
fi
if isLambdaFunctionExists; then return; fi
echo "creating lambda function"
if [ ! -f "../lambda/lambda.zip" ]; then
createLambdaZip
fi
local zippath=$(pathname=../lambda/lambda.zip)
echo "Zip is at $zippath"
createLambdaZip
# problem with aws api's, mitigated by sleeping
sleep 10
local rolearn=$(getLambdaRoleArn)
echo "$rolearn"
aws --profile $PROFILE --region $REGION lambda create-function \
--function-name $LAMBDA_FUNCTION_NAME \
--runtime python3.12 \
--timeout 300 \
--role $rolearn \
--handler lambda_function.lambda_handler \
--zip-file fileb://../lambda/lambda.zip \
--tags "Name=$TAG_NAME"
}
# Function to delete Lambda function
function delete_lambda_function {
if ! isLambdaFunctionExists; then
echo "no lambda function to delete"
return
fi
echo "deleting lambda function"
aws --profile $PROFILE --region $REGION lambda delete-function --function-name $LAMBDA_FUNCTION_NAME
}
# Function to delete S3 bucket
function delete_s3_bucket {
if ! isBucketExists; then
echo "No bucket to delete"
return
else
echo "bucket found"
fi
echo "deleting s3 bucket"
aws --profile $PROFILE --region $REGION s3 rb s3://$BUCKET_NAME --force
echo "bucket deleted"
}
# Function to delete CloudFront distribution
function delete_cloudfront_distribution {
if ! isDistributionExists; then
echo "distribution does not exist"
return
fi
echo "Disabling cloudfront distribution"
local did=$(getDistributionId)
local conf=$(aws --profile $PROFILE --region $REGION cloudfront get-distribution-config --id $did --output json)
local ifmatch=$(echo "$conf" | jq -r '.ETag')
local conf=$(echo "$conf" | jq -r '.DistributionConfig')
local conf=$(echo "$conf" | jq '.Enabled=false')
local conf=$(echo "$conf" | jq '.Origins.Items[0].S3OriginConfig.OriginAccessIdentity=""')
aws --profile $PROFILE --region $REGION cloudfront update-distribution --id $did --if-match "$ifmatch" --distribution-config "$conf"
# here we should poll until the distribution is deletable
echo "waiting for distribution to be deployed or undeployed"
waitForDistributionDeployment
echo "deleting distribution $did"
# Get the etag again.. Nasty
local conf=$(aws --profile $PROFILE --region $REGION cloudfront get-distribution-config --id $did --output json)
local ifmatch=$(echo "$conf" | jq -r '.ETag')
aws --profile $PROFILE --region $REGION cloudfront delete-distribution --id $did --if-match "$ifmatch" --output text
echo "CloudFront Distribution ID $did deleted"
}
function buildHtml {
if ! isApplicationInstalled "python3"; then
echo "you must install python3 to run this script"
exit 1
fi
if ! isPythonModuleInstalled "jinja2"; then
echo "you must pip install the python Jinja2 module"
exit 1
fi
python3 ../build.py
}
function uploadInitalFiles {
# just upload the index.html file for now
aws --profile $PROFILE --region $REGION s3api put-object --bucket $BUCKET_NAME --key index.html --body ../output/index.html --content-type="text/html"
aws --profile $PROFILE --region $REGION s3api put-object --bucket $BUCKET_NAME --key error.html --body ../output/error.html --content-type="text/html"
}
# Main function to create resources
function site_create {
# slurp in some values we don't want ending up in github
# the setenv file will be in the gitignore
# rm -R ../output 2> /dev/null
if [ -f ./setenv.sh ]; then
source ./setenv.sh
else
echo "You must create a ./setenv.sh file and it should export GITHUB_TOKEN"
exit 0
fi
createLambdaZip
create_oai
createOac
create_s3_bucket
syncS3
create_lambda_role
create_lambda_function
if ! isLambdaFunctionExists; then
echo "failed to create lambda function, retrying"
sleep 10
create_lambda_function
fi
if ! isLambdaFunctionExists; then
echo "Failed to create lambda after retry, exiting"
exit 1
fi
createLambdaUrlConfig
create_cloudfront_distribution
local url=$(getDistributionUrl)
echo "CLOUDFRONT DISTRIBUTION URL: $url"
}
# Main function to delete resources
function site_destroy {
delete_cloudfront_distribution
delete_oai
deleteOac
delete_s3_bucket
delete_lambda_function
delete_lambda_role
deleteLambdaUrlConfig
}
# NEXT:
# * Go to aws certificate manager console and request a PUBLIC certificate
# * If your domain name is richardsenior.net (for example) then make sure to add *.richardsenior.net, www.richardsenior.net and richardsenior.net etc.
# * Go to your dns host (in my case astutium) and add a CNAME record that matches the information certificate manager gave you.
# For example CNAME NAME will be something like : _e46fddcad9665e4c663df910ed58df08.richardsenior.net.
# So that is what you enter for the name in your CNAME entry in your dns host dashboard (not in aws dashboard).
# If your host automatically adds the domainname (ie richard.net) then just chop off the raw domain (ie enter _e46fddcad9665e4c663df910ed58df08)
# The value of the CNAME entry will be something like _c1f4a4838f242504d86b235be793c2f1.kirrbxfjtw.acm-validations.aws.
# * This will cause AWS to send dns requests to _e46fddcad9665e4c663df910ed58df08.richardsenior.net etc, expecting a resolution of the value know (ie, confirmation we own the domain)
# * Wait for the certificate to be validated (hours)
# * Go into aws console into your cloudfront distribution, add your hostnames as 'alias' (*.richardsenior.net, www.richardsenior.net etc.), and select the newly created certificate.
# * Make sure that your DNS host also has an entry for www.YOURHOSTNAME pointing to the cloudfront distrubition URL (something like d1izdntgees9tap.cloudfront.net etc.)
# * You should now be able to enter https://www.YOURDOMAIN and get a response!