Reading time ~10 minutes
My Blogging Stack
- A Brief History of marcolancini.it
- The AWS Setup
- Website Generation via Jekyll
- Automatic Deployments via GitHub Actions
- How Much Does this Cost?
- Conclusion
A few people lately have been asking me about my blogging setup, so Iβve decided to write a post to explain whatβs behind the scenes of this website. In a further post I will also tackle the setup for cloudseclist.com.
A Brief History of marcolancini.it
Iβve had a personal website since 2003, and, as you can see from the screenshots below, web design has always been my strongest skill (π€¦ββοΈ).






Over the years, I went through a few iterations, and for a period I even ran on Wordpress (π±).
The only constant was that Iβve always used a niche hosting provider from Italy, which only offered support for static websites and Wordpress (hence why I was once crazy enough to move everything to it, only to run back to static websites shortly after). Additionally, the provider only offered TLS certificates as a βpremiumβ feature, costing double the price just for being able to serve a website via HTTPS. Even better, the only way to publish a new version of the website (new post, etc.), was to manually upload the content via FTP. Yep, youβve read it right: clear, plain text FTP.
This was probably fine in 2003 but not nowadays. To me it looked a lot like:

If you donβt recognize this image, it is from the cover of The Phoenix Project (and if you havenβt read it, you really should!).
So, roughly two years ago (summer 2018) I got fed up with this and decided to move everything to the cloudβ¦
The AWS Setup
β¦and it was incredible. Nowadays, marcolancini.it is a static website hosted in an S3 bucket and deployed automatically via GitHub actions. The setup looks more or less like the diagram below.

Letβs analyze the different components from this picture.
Domain and DNS
Although the domain names for both marcolancini.it
and marcolancini.com
are registered with Route53, Iβve found CloudFlare to be more customizable (and cheaper) for DNS management, so from Route53 I pointed the authoritative nameservers to the CloudFlare ones.
Static Hosting
Two S3 buckets host the content of this website:
www.marcolancini.it
: an S3 bucket configured for static web hosting, and with a bucket policy that allows everyone to read its content.marcolancini.it
: another bucket configured for static hosting, but which redirects all requests to the βmainβ bucketwww.marcolancini.it
.
With the 2 buckets setup, DNS entries in CloudFlare are configured to point two CNAMEs (marcolancini.it
, and www
) to the URL of the main bucket:
CNAME marcolancini.it www.marcolancini.it.s3-website.eu-west-2.amazonaws.com
CNAME www www.marcolancini.it.s3-website.eu-west-2.amazonaws.com
I also use CloudFlare for its ability to automatically provide free TLS certificates, and, most importantly, for its CDN network to speed up delivery across the globe. Though, I have to admit that I could make some further improvements on the caching configuration.

Sending/Receiving Emails
Since I wanted to manage everything from within AWS, I needed a way to be able to both receive and send emails from my @marcolancini.it
domain.
For this, Iβve found the setup proposed by aws-lambda-ses-forwarder quite effective (with only a couple of tweakings needed). The README in that repo is quite explicative, but, in short, the process to set up email reception with SES is the following:
- First of all, I had to verify both the
marcolancini.it
domain and a forwarding email address (<redacted>@gmail.com
) within SES. - Then, I had to create an S3 bucket to store incoming emails (letβs call it
mailbox-bucket
). This bucket has a bucket policy that allows SES to put objects in it, and a lifecycle configured to delete objects after 90 days from creation. - The next step involved setting up a lambda (letβs call it
SesForwarder
) that forwards every incoming email to a destination address (Gmail in my case). This can be obtained by modifying the constants in theindex.js
file (provided in the aws-lambda-ses-forwarder repo) to fetch emails frommailbox-bucket
and forward them to<redacted>@gmail.com
. - Finally, I had to setup a Reception Rule on SES, with 2 actions performed for every email incoming into SES:
- S3 action: choose
mailbox-bucket
. This will allow SES to store the incoming email as an object in the specified S3 bucket. - Lambda action: choose the
SesForwarder
Lambda function. This will trigger the lambda, which, in the end, will forward the email to the destination address.
- S3 action: choose
Setting up outgoing emails was then just a matter of creating an SMTP user in SES, and configuring Gmail to send emails as that SMTP user.
Monitoring
Although I do not collect personally identifiable information, Google Analytics and CloudFlare Statistics allow me to obtain a high-level picture of the readers of this blog. For example, the picture below shows the geographical distribution of readers by country, which for the past month saw the following as the most frequent sources of traffic:
- USA
- France
- UK
- Germany
- China

Another aspect I wanted to monitor, given its criticality,
was the SesForwarder
lambda, since its failure will result in a loss of emails
destined to @marcolancini.it
.
In addition to CloudWatch Alarms, triggered to fire to any invocation failure,
Iβve also decided to run periodic health-checks to ensure emails are still getting forwarded to my personal Gmail address.
To do so, Iβve subscribed a couple of email addresses within the @marcolancini.it
domain
to an SNS topic. Then, I leverage CloudWatch Events to trigger another lambda (SNSTrigger
in the diagram above) which simply puts a new message to that SNS topic.
If I donβt receive any email at the predefined intervals, I can then be sure something is wrong.

Website Generation via Jekyll
Having discussed the overall setup, and how the content is stored and delivered through AWS, it is now time to explain how I generate the website and write new posts.
First of all, Iβm a fan of monorepos, so the code for all my websites (well, I mainly have 2 at the moment) is in a single private repository on Github:
β― tree -L 1 websites/
websites
βββ README.md
βββ cloudseclist.com
βββ marcolancini.com
βββ marcolancini.it
3 directories, 1 file
As mentioned previously, Iβm going to blog about the setup for cloudseclist.com in another post, whereas here we will be focusing on marcolancini.it:
β― tree marcolancini.it -L 3
marcolancini.it
βββ README.md
βββ docker-compose.yml
βββ resources
βΒ Β βββ setup
βΒ Β βββ aws.md
βΒ Β βββ diagrams
βΒ Β βββ lambda
βββ web
βββ Dockerfile
βββ Dockerfile_grunt
βββ site
βββ 404.html
βββ Gemfile
βββ Gemfile.lock
βββ Gruntfile.js
βββ _config.yml
βββ _config_dev.yml
βββ _drafts
βββ _includes
βββ _layouts
βββ _posts
βββ _sass
βββ _site
βββ _templates
βββ assets
βββ images
βββ index.html
βββ node_modules
βββ package-lock.json
βββ package.json
βββ pages
βββ pages_tech
βββ search.json
Since historically my hosting provider only supported static sites, I ended up using Jekyll, a pretty straightforward static site generator written in Ruby by one of GitHubβs co-founders. There are plenty of websites explaining how to setup your blog with Jekyll (in case you were interested in replicating my setup), but definitely a good place to start is the official documentation.
From the directory listing above, you can see the web/site/
folder which contains the Jekyll website (pretty standard format).
The custom modifications Iβve made (apart from a custom theme) are related to the way I run Jekyll, which is via custom Docker images coordinated via docker-compose
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
version: '2'
services:
# ------------------------------------------------------------------------------------
# GRUNT
# ------------------------------------------------------------------------------------
grunt:
container_name: ml_grunt
build:
context: ./web/
dockerfile: Dockerfile_grunt
volumes:
- $PWD/web/site/:/src/website/
entrypoint: grunt
# ------------------------------------------------------------------------------------
# WEBSITE
# ------------------------------------------------------------------------------------
web:
container_name: ml_web
restart: always
build:
context: ./web/
dockerfile: Dockerfile
volumes:
- $PWD/web/site/:/src/website/
ports:
- 127.0.0.1:4000:4000
environment:
- VIRTUAL_HOST=127.0.0.1
- VIRTUAL_PORT=4000
command: jekyll serve --config _config.yml,_config_dev.yml --host 0.0.0.0 --port 4000
- Lines
6
-13
: define a container for Grunt, a JavaScript task runner I mainly use to minify images before publishing them. - Lines
21
-23
: the image for the container running Jekyll comes from a custom Dockerfile (shown below):
FROM jekyll/jekyll:latest
#Β Create workdir
RUN mkdir -p /src/website/
WORKDIR /src/website/
# Cache bundle install
COPY ./site/Gemfile* /src/website/
RUN chmod a+w Gemfile.lock
RUN bundle install
- Line
25
: theweb/site/
folder (containing the Jekyll installation) is shared with the container, so I can make changes from VisualStudio Code on my host and have Jekyll automatically pick up the changes and render them. - Line
27
: port4000
of the container is exposed to the same port on localhost, so that I can access the preview onhttp://127.0.0.1:4000
. - Line
31
: the command to run Jekyll, with the_config_dev.yml
file used to override the base setup just for local usage.

With this setup, I can add a new markdown file in the web/site/posts/
folder and write the content locally.
But how does this ends up published?
Automatic Deployments via GitHub Actions
The final loop in the chain is provided by GitHub actions, which allow me to automatically deploy to S3 every time I push to the master
branch.
Here is the content of the workflow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
name: Marcolancini
on:
push:
branches:
- master
paths:
- 'marcolancini.it/web/site/*'
- 'marcolancini.it/web/site/*/*'
- 'marcolancini.it/web/site/*/*/*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/[email protected]
with:
ref: master
fetch-depth: 1
- name: Build the site in the jekyll/builder container
run: |
docker run \
-v ${FOLDER}:/srv/jekyll -v ${FOLDER}/_site:/srv/jekyll/_site \
jekyll/builder:latest /bin/bash -c "chmod 777 /srv/jekyll && jekyll build --future"
env:
FOLDER: ${github.workspace}/marcolancini.it/web/site
- name: Deploy
run: aws s3 sync ${FOLDER}/_site/ s3://${BUCKET} --delete
env:
FOLDER: ${github.workspace}/marcolancini.it/web/site
BUCKET: www.marcolancini.it
AWS_ACCESS_KEY_ID: ${secrets.AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${secrets.AWS_SECRET_ACCESS_KEY}
- Lines
5-10
: the workflow only runs when a file gets modified in themarcolancini.it/web/site/*
folder on themaster
branch. This avoids triggering the pipeline from commits on developing branches. - Lines
17-21
: the first step simply checks out the repository. - Lines
22-28
: the second step builds the site in thejekyll/builder
container. - Line
30
: finally, the third step syncs the generated website with the main S3 bucket, using the AWS API keys specified in lines34-35
.

How Much Does this Cost?
After I first released this post, thereβs been interest around how much this setup costs me every month, since most of the services I use are paid ones. So, letβs do some math!
Letβs start by the monthly cost of AWS services: the table and graph below show my expenditure for the AWS account hosting marcolancini.it for the past 5 months (1st November 2019 - 31st March 2020):
Month | Tax($) | S3($) | SES($) | Lambda($) | SNS($) | CloudWatch($) | Total cost($) |
---|---|---|---|---|---|---|---|
Nov 2019 | 0.13 | 0.67 | 0.00 | 0.00 | 0.00 | 0.00 | 0.80 |
Dec 2019 | 0.17 | 0.86 | 0.00 | 0.00 | 0.00 | 0.00 | 1.03 |
Jan 2020 | 0.16 | 0.78 | 0.00 | 0.00 | 0.00 | 0.00 | 0.94 |
Feb 2020 | 0.18 | 0.74 | 0.13 | 0.00 | 0.00 | 0.00 | 1.05 |
Mar 2020 | 0.2 | 1.00 | 0.00 | 0.00 | 0.00 | 0.00 | 1.20 |
Service Total | 0.84 | 4.05 | 0.13 | 0.00 | 0.00 | 0.00 | 5.02 |

Notice: Data transfer costs are included in the services that theyβre associated with, such as Amazon EC2 or Amazon S3. They arenβt represented as either a separate line item in the data table or a bar in the chart.
On average this adds up to ~$1 per month (~Β£0.80 at the current exchange rate). On top of this, we have to add:
- Domain names registration (
marcolancini.it
,marcolancini.com
): $13.5 each, for a total of $27 per year ($2.25 per month). - GitHub actions: $0, since Iβm way below the 2,000 minutes per month of the free tier.
- CloudFlare: $0, since Iβm on the free tier.
So, in total, Iβm spending $3.25 per month (~Β£2.59), mostly coming from the domain name fees:
Monthly Cost | Total($) |
---|---|
AWS Services | 1.00 |
Domain Names | 2.25 |
GitHub Actions | 0.00 |
CloudFlare | 0.00 |
Total | 3.25 |
Conclusion
Thank you for making it this far! I hope you found this post interesting, as it described my workflow for publishing new posts on this website.
In a following post I will tackle the setup for cloudseclist.com, which is a bit more convoluted since it also has a serverless mailer solution based on top off SES!
If something is unclear, or if Iβve overlooked some aspects, please do let me know on Twitter @lancinimarco.