DEV Community

Marco Aguzzi
Marco Aguzzi

Posted on • Originally published at marcoaguzzi.it on

Please stop publishing AWS S3 buckets as static websites! Read here for a secure, fast, and free-ish approach [1st episode]

I promise this is not yet another tutorial on how to publish a static website using AWS S3, or at least not solely smashing the S3 content onto the web. I’d like to show you a GitHub project that uses Java to orchestrate Cloudformation when deploying the architecture of a static website.

The main purpose of this tool is going beyond the S3 out of the box website functionality, that is:

  • Make the S3 bucket private (so, secure )
  • Provide HTTPS certificates ( secure, again)
  • Serve the content via cloudfront cache (so, fast )
  • Hide the complexities of working with Cloudformation

I’m in for fast and secure, but free…ish?

Not all the resources that need to be fired up for this architecture are within the AWS free tier, expecially the domain. Nevertheless, all the costs that I’ve seen after this website was published were only live costs. Let’s review them from the most to the least expensive:

  • The domain: 20€ / year if hosted on Route53 (as marcoaguzzi.it) but you can host it elsewere (on cloudns, and it’s free)
  • Route53 and Codepipeline: 1€ / month each. It’s one for the hosted zone and one for the pipeline. The pipeline comes with a good amount of free build / minutes
  • Secret manager: less than 0.5€ / month (there’s a grace period when started)
  • Cloudfront and S3: 0.01€ / month each

Of course these are starting costs, they can go a lot higher as the usage increase, but it should be a welcomed issue, I suppose

Hide the complexities of Cloudformation

Cloudformation migth be a burden to use, especially within the web UI. These are the main issue I addressed in the project:

  • Have a self - contained architecture
  • Be repeatable. Could it deploy the same architecture on another domain?
  • Ease the deploy process, especially when the domain is not hosted on Route53
  • The nested stacks are not automatically resolved by Cloudformation

The Java tool to the rescue

While experimenting with Java and Gradle, I wondered if I could use Java to mitigate the problems listed above by orchestrating the instructions that Cloudformation needs in order to deploy the website. This turned out as a Github project: https://github.com/maguzzi/s3_static_website_gradle. The Gradle build creates a distributable archive with all the needed jars.

How to use the project

After the packaged app has been downloaded, what’s needed to run?

  • Java 8+
  • An AWS account, with authentication in place. As of now, I’ve tested it with having ACCCESS_KEY and ACCESS_SECRET as enviornment variables locally. If those are not found, the tool stops.
  • A dns domain. Let’s use a free service: s3staticwebsitetest.cloudns.ch
  • A config file named website.properties containing the website information, like this:
name = Fast, secure, free-ish S3 static website
environment = dev
domain = dev.s3staticwebsitetest.cloudns.ch
Enter fullscreen mode Exit fullscreen mode

Tool commands

Let’s set the LOG_LEVEL to INFO in order not to clog the shell, and check the existing stack in our AWS account:

DISTRIBUTION

Expecting the aws account still without this infrastructure, this is the first command to run:

java -jar s3\_static\_website\_gradle-all.jar DISTRIBUTION

Enter fullscreen mode Exit fullscreen mode

Here’s the output:

2024-05-01T14:14:16 [main] INFO - AWS\_REGION: us-east-1
2024-05-01T14:14:16 [main] INFO - AWS\_ACCESS\_KEY\_ID: AKIA\*\*\*\*
2024-05-01T14:14:16 [main] INFO - AWS\_SECRET: \*\*\*\*\*
2024-05-01T14:14:16 [main] INFO - AWS setup done.
2024-05-01T14:14:17 [main] INFO - reading content from file: /home/maguzzi/demo/./website.properties
2024-05-01T14:14:17 [main] INFO - Command: DISTRIBUTION environment: dev
2024-05-01T14:14:17 [main] WARN - .websitesetup file does not exists. Creating
2024-05-01T14:14:17 [main] INFO - reading content from file: /home/maguzzi/demo/./.websitesetup
2024-05-01T14:14:17 [main] INFO - Setup new pseudoRandomTimestampString to 20240501141417491
2024-05-01T14:14:17 [main] INFO - Setup new zipDate to 20240501
2024-05-01T14:14:17 [main] INFO - 
2024-05-01T14:14:17 [main] INFO - -- s3-static-website-bootstrap-stack - dev CREATION START --
2024-05-01T14:14:17 [main] INFO - 
2024-05-01T14:14:17 [main] INFO - reading content from jar: jar:file:/home/maguzzi/demo/s3\_static\_website\_gradle-all.jar!/bootstrap/bootstrap.json
2024-05-01T14:14:18 [main] INFO - Stack s3-static-website-bootstrap-stack-dev not yet completed, wait
2024-05-01T14:14:23 [main] INFO - Stack s3-static-website-bootstrap-stack-dev not yet completed, wait
...

Enter fullscreen mode Exit fullscreen mode

It states:

  • The environment variable for the AWS configuration are in place
  • The website.properties file is in place
  • The .websitesetup file does not exist, and is created at run time.

pseudoRandomTimestampString is used to have unique names for the s3 buckets, while zipDate to identify the artifact for the lambda.

Now the bootstrap stack is being created, so that the S3 buckets will be in place when needed for:

  • packaging the templates
  • upload the lambda artifact
  • host the website content

The last two lines state that the tool is waiting for the completion of the bootstrap stack.

Once that the first stack has been successfully created, the tool states it and outputs the export key for the s3 buckets that will be needed in the distribution stacks:

Stack creation for stack id arn:aws:cloudformation:us-east-1:\*\*\*\*:stack/s3-static-website-bootstrap-stack-dev/\*\*\*\*\* terminated.
2024-05-01T14:14:54 [main] INFO - 
2024-05-01T14:14:54 [main] INFO - -- s3-static-website-bootstrap-stack - dev CREATION END --
2024-05-01T14:14:54 [main] INFO - 
2024-05-01T14:14:54 [main] INFO - ArtifactS3Bucket -> s3-static-website-lambda-artifact-dev-20240501141417491 (s3-static-website-bootstrap-stack-dev-LambdaArtifactBucket-Export-dev)
2024-05-01T14:14:54 [main] INFO - CompiledTemplateBucket -> s3-static-website-compiled-template-dev-20240501141417491 (s3-static-website-bootstrap-stack-dev-CompiledTemplateBucket-Export-dev)

Enter fullscreen mode Exit fullscreen mode

It then continues to prepare the files that will be needed for the distribution stack (let’s view a trimmed version of the log):

2024-05-01T14:14:54 [main] INFO - -- ZIP ARTIFACT START --
...
2024-05-01T14:14:54 [main] INFO - -- ZIP ARTIFACT END --
2024-05-01T14:14:54 [main] INFO - 
2024-05-01T14:14:54 [main] INFO - ARTIFACT\_COMPRESSED\_PATH -> /tmp/cloudformation\_tmp10381003167251864437/lambda-edge-dev-20240501.zip (-)
...
2024-05-01T14:14:54 [main] INFO - -- UPLOAD FILE TO BUCKET START --
...
2024-05-01T14:14:55 [main] INFO - URL: https://s3-static-website-lambda-artifact-dev-20240501141417491.s3.amazonaws.com/lambda-edge-dev-20240501.zip
2024-05-01T14:14:55 [main] INFO - -- UPLOAD FILE TO BUCKET END --
...
2024-05-01T14:14:56 [main] INFO - -- PACKAGE TEMPLATE START --
2024-05-01T14:14:56 [main] INFO - reading content from jar: jar:file:/home/maguzzi/demo/s3\_static\_website\_gradle-all.jar!/distribution/website-distribution.json
2024-05-01T14:14:56 [main] INFO - -- PACKAGE TEMPLATE END --
2024-05-01T14:14:56 [main] INFO - 
...
2024-05-01T14:14:56 [main] INFO - -- s3-static-website-distribution-stack - dev CREATION START --
2024-05-01T14:14:56 [main] INFO - 
2024-05-01T14:14:56 [main] INFO - reading content from file: /tmp/202405011414567687239912532179902\_compiled\_template.json
2024-05-01T14:14:56 [main] INFO - 
2024-05-01T14:14:56 [main] INFO - -- s3-static-website-distribution-stack - dev CREATION END --
2024-05-01T14:14:56 [main] INFO - 

Enter fullscreen mode Exit fullscreen mode

In the log it can be seen that zip files for artifacts are loaded onto S3, and then the template with the sub-stack reference is packaged and put into a temporary folder before the create-stack command is issued.

DNS_INFO

Since we’re using a free domain, the tool stops here because cloudformation can’t complete its creation without configuring the domain provider (cloudns in this case). So let’s check the DNS information that has to be provided to the cloudns:

java -jar s3\_static\_website\_gradle-all.jar DNS\_INFO dns-info.txt

Enter fullscreen mode Exit fullscreen mode

and the output:

2024-05-01T14:15:39 [main] INFO - AWS\_REGION: us-east-1
2024-05-01T14:15:39 [main] INFO - AWS\_ACCESS\_KEY\_ID: AK\*\*\*\*\*\*\*\*
2024-05-01T14:15:39 [main] INFO - AWS\_SECRET: \*\*\*\*\*
2024-05-01T14:15:39 [main] INFO - AWS setup done.
2024-05-01T14:15:40 [main] INFO - reading content from file: /home/maguzzi/demo/./website.properties
2024-05-01T14:15:40 [main] INFO - Command: DNS\_INFO environment: dev
2024-05-01T14:15:40 [main] INFO - reading content from file: /home/maguzzi/demo/./.websitesetup
2024-05-01T14:15:40 [main] INFO - PseudoRandomTimestampString already set to 20240501141417491
2024-05-01T14:15:40 [main] INFO - ZipDate already set to 20240501
2024-05-01T14:15:40 [main] INFO - 
2024-05-01T14:15:40 [main] INFO - -- ROUTE 53 INFO START --
2024-05-01T14:15:40 [main] INFO - 
2024-05-01T14:15:41 [main] INFO - Got hosted zone Id Optional[Z\*\*\*\*\*\*] for stack s3-static-website-distribution-stack-dev
2024-05-01T14:15:41 [main] INFO - hostedZoneId: Optional[Z\*\*\*\*\*]
2024-05-01T14:15:41 [main] INFO - Name: dev.s3staticwebsitetest.cloudns.ch. TTL: 172800
2024-05-01T14:15:41 [main] INFO - ns-1333.awsdns-38.org.
2024-05-01T14:15:41 [main] INFO - ns-1572.awsdns-04.co.uk.
2024-05-01T14:15:41 [main] INFO - ns-844.awsdns-41.net.
2024-05-01T14:15:41 [main] INFO - ns-458.awsdns-57.com.
2024-05-01T14:15:41 [main] INFO - 
2024-05-01T14:15:41 [main] INFO - -- ROUTE 53 INFO END --
2024-05-01T14:15:41 [main] INFO - 

Enter fullscreen mode Exit fullscreen mode

The tool conventiently outputs the DNS information (last 4 lines) in the file dns-info.txt (as specified on the command line).

It can be uploaded as-is on the cloudns web ui.

CHECK

Once that the DNS provider has propagated the DNS records provided by AWS, the tool can be started again to check if the distribution stack has been completed.

java -jar s3\_static\_website\_gradle-all.jar CHECK

Enter fullscreen mode Exit fullscreen mode

The output is pretty straightforward:

2024-05-01T14:24:11 [main] INFO - AWS\_REGION: us-east-1
2024-05-01T14:24:11 [main] INFO - AWS\_ACCESS\_KEY\_ID: AKIA\*\*\*\*\*\*\*\*
2024-05-01T14:24:11 [main] INFO - AWS\_SECRET: \*\*\*\*\*
2024-05-01T14:24:11 [main] INFO - AWS setup done.
2024-05-01T14:24:12 [main] INFO - reading content from file: /home/maguzzi/demo/./website.properties
2024-05-01T14:24:12 [main] INFO - Command: CHECK environment: dev
2024-05-01T14:24:13 [main] INFO - Stack s3-static-website-distribution-stack-dev not yet completed, wait
...
2024-05-01T14:26:32 [main] INFO - Stack s3-static-website-distribution-stack-dev not yet completed, wait
2024-05-01T14:26:42 [main] INFO - Stack creation for stack id arn:aws:cloudformation:us-east-1:\*\*\*\*:stack/s3-static-website-distribution-stack-dev/\*\*\*\*\*\* terminated.

Enter fullscreen mode Exit fullscreen mode

LIST

Now we can list the stacks that have been created, along with their tags. As you can see, the random string that has been setup in the beginning has been propagated onto all the stacks, along with the S3 buckets and all the taggable resources.

java -jar s3\_static\_website\_gradle-all.jar LIST

Enter fullscreen mode Exit fullscreen mode

Here’s the output:

2024-05-01T14:29:59 [main] INFO - AWS\_REGION: us-east-1
2024-05-01T14:29:59 [main] INFO - AWS\_ACCESS\_KEY\_ID: AKIA\*\*\*\*\*
2024-05-01T14:29:59 [main] INFO - AWS\_SECRET: \*\*\*\*\*
2024-05-01T14:29:59 [main] INFO - AWS setup done.
2024-05-01T14:30:00 [main] INFO - reading content from file: /home/maguzzi/demo/./website.properties
2024-05-01T14:30:00 [main] INFO - Command: LIST environment: dev
2024-05-01T14:30:00 [main] INFO - reading content from file: /home/maguzzi/demo/./.websitesetup
2024-05-01T14:30:00 [main] INFO - PseudoRandomTimestampString already set to 20240501141417491
2024-05-01T14:30:00 [main] INFO - ZipDate already set to 20240501
2024-05-01T14:30:00 [main] INFO - 
2024-05-01T14:30:00 [main] INFO - -- LIST STACK START --
2024-05-01T14:30:00 [main] INFO - 
2024-05-01T14:30:01 [main] INFO - s3-static-website-distribution-stack-dev-LambdaEdgeCloudFrontStack-FZ\*\*\*\*\*\* (CREATE\_COMPLETE) - [Tag(Key=s3\_static\_website\_environment, Value=dev), Tag(Key=s3\_static\_website, Value=S3 static website test), Tag(Key=s3\_static\_website\_timestamp\_tag, Value=20240501141417491)]
2024-05-01T14:30:01 [main] INFO - s3-static-website-distribution-stack-dev (CREATE\_COMPLETE) - [Tag(Key=s3\_static\_website\_environment, Value=dev), Tag(Key=s3\_static\_website, Value=S3 static website test), Tag(Key=s3\_static\_website\_timestamp\_tag, Value=20240501141417491)]
2024-05-01T14:30:01 [main] INFO - s3-static-website-bootstrap-stack-dev (CREATE\_COMPLETE) - [Tag(Key=s3\_static\_website\_environment, Value=dev), Tag(Key=s3\_static\_website, Value=S3 static website test), Tag(Key=s3\_static\_website\_timestamp\_tag, Value=20240501141417491)]
2024-05-01T14:30:02 [main] INFO - 
2024-05-01T14:30:02 [main] INFO - -- LIST STACK END --
2024-05-01T14:30:02 [main] INFO - 

Enter fullscreen mode Exit fullscreen mode

At this point, the website has no content. The only thing left to do is to upload a random index.html on the S3 bucket to see if all the process worked fine. I’ve let the step out of the tool because the website content is intented to be created and uploaded by a pipeline.

You can catch the random string of the s3 bucket in the log:

aws s3 cp index.html s3 bucket

Enter fullscreen mode Exit fullscreen mode

And then you can point the browser to http://dev.s3staticwebsitetest.cloudns.ch and see that it worked!

What’s next?

In the next post, we’ll integrate the CICD pipeline that uploads the website content, along with some consideration about how to delete the stack without using the UI. Cloudformation and Cloudfront force some contraints on how the resources should be deleted, so it’s worth spending some time on it.

Top comments (0)