Onwards is my attempt at building a re-usable URL shortener for links that I'll put in my book, Rust for Rustaceans. I didn't want to pay a monthly fee to a URL shortener with support for custom domains, not because the additional data analytics they give you and such wouldn't be nice, but because $8+/month seemed excessive to agree to pay in perpetuity.
So, onwards was born. It is hosted using an AWS Lambda, meaning there is no always-on server cost. It keeps no access statistics and hard-codes the shortlinks in the binary, so there is no storage cost. It uses AWS CloudFront for caching, so even if there are floods of traffic, the incurred cost is minimal. And, because it's all stateless and serverless, it should scale to basically any user load.
I haven't published the version of the book that has these links yet, but will update this with my final bill once I do. My expectation is about $1/month for the traffic, with half of that going to the fixed cost of Route 53, AWS' DNS provider. Meaning in total I pay $1/month, with full control over the shortening (and no limits!).
If anyone has ideas for reducing this cost further without affecting the stable-state workflow, I'd love to hear them.
It's a bit of a process to get the infrastructure set up, but once it's set up, changing the short links is one GitHub MR that you hit merge on. In other words, only really headache up front, and then you don't have to touch it. If anyone has ideas for removing steps from this without affecting the stable-state workflow, I'd love to hear them.
Here's what you do:
- Sign up for an AWS account if you haven't already, then go to AWS Organizations and hit "Add an AWS account". Create a new one, and give it whatever name + email you want. I recommend keeping the IAM role name the default.
- Once created, copy the account number of the newly created AWS
organization account, hit the user dropdown top left of the AWS
console, and select "Switch role". Input the account ID for the newly
created account,
OrganizationAccountAccessRole
as the IAM role name, and hit the "Switch Role" button. - Fork this repo
- Go to your onwards fork on GitHub -> Settings -> Environments
- Add (or edit) the environment called "prod". Set it to target the
main
branch. - Next, go to Secrets and variables -> Actions -> Variables. Use "New
repository variable" to add the following variables:
AWS_REGION
: the AWS region you'd like to host the service inDOMAIN
: the domain you want to host the service underAWS_PLAN_ROLE
:arn:aws:iam::$THE_AWS_ACCOUNT_NUMBER_FROM_ABOVE:role/tf-plan-role
AWS_APPLY_ROLE
:arn:aws:iam::$THE_AWS_ACCOUNT_NUMBER_FROM_ABOVE:role/tf-apply-role
- Now, we need to make it possible to run Terraform locally for the
first apply, which will also set up the permissions needed for GitHub
Actions to run plan and apply. You'll want to set up the AWS CLI
locally, and then in your
~/.aws/config
, add a stanza likeTo test it, see that you can run:[profile onwards] role_arn = arn:aws:iam::$THE_AWS_ACCOUNT_NUMBER_FROM_ABOVE:role/OrganizationAccountAccessRole source_profile = default
env AWS_PROFILE=onwards aws account get-account-information
- We also need to manually set up the S3 bucket that Terraform's state
will be kept in. Luckily, we only have to do so once. You can do that
by running the following commands, substituting in
$DOMAIN
and$AWS_REGION
:Once that's done, openenv AWS_PROFILE=onwards aws s3api create-bucket --bucket onwards.$DOMAIN.terraform --region $AWS_REGION --create-bucket-configuration LocationConstraint=$AWS_REGION env AWS_PROFILE=onwards aws s3api put-bucket-versioning --bucket onwards.$DOMAIN.terraform --versioning-configuration Status=Enabled
infra/main.tf
and look for theCHANGEME NOTE
. Update the bucket name and region there to match what you gave in the command above. - We're almost done now. Unfortunately, there's one complication: the
GitHub actions apply job tries to publish the Docker image for the
Lambda function to AWS ECR (the Docker registry). This will fail
because we haven't run Terraform yet to create it. But, we can't run
Terraform until we have an image tag for the Lambda function, because
otherwise Terraform's creation of the Lambda will fail. So, we have
to tell terraform locally to initiate only ECR for now.
cd infra/
and run:It will prompt you for three values:terraform init terraform apply -target=aws_ecr_repository.onwards -var "lambda_image_tag=latest"
aws_region
anddomain
, which you should provide the same value as you did for the GitHub variables.github_repo
, which you should set to the GitHub repository of your fork (e.g.,jonhoo/onwards
). After the "Plan" step finishes, you'll have to confirm that you want to apply the changes (type "yes" and hit enter). This step should the complete successfully!
- Now we just need to ensure that GitHub can actually modify all our
various AWS state. We do that by another targeted apply that just
instantiates the required IAM policy:
It will ask you again for the same variables as above.
terraform apply -target=aws_iam_role_policies_exclusive.tf_apply_role_policies -var "lambda_image_tag=latest"
- Commit your change to
infra/main.tf
and push! You should be able to go to GitHub and see the terraform/apply step run. The first time it runs, it will make a lot of changes -- that's fine. However, you'll see the TLS certificate creation hang. This is expected until we finish domain setup, so leave the hanging apply open while we set up the name servers: - In the AWS console, go to Route 53 -> Hosted zones, open your domain, expand the "Hosted zone details" box. You'll want to take all the domains listed under "Name servers" and make them be the name servers set for your domain with your domain registrar. Do that now. Eventually, the terraform/apply GitHub action step should finally finish successfully. If it timed out, just restart it.
- Open
$yourdomain/about
and see that it redirects to the onwards GitHub project. Congratulations -- setup is now done! Let's check that adding some links works. - Open an MR against your fork of the repo where you edit
src/lib.rs
to add additional short-links. Once CI passes, merge the MR. - Open the CI for the
main
branch; there should be a job running named "Terraform Cloud Apply Run / Terraform Apply". It should succeed. When it does: - Test your new short-link! The process for adding more links is the
same: push a commit that changes
src/lib.rs
— that's it. Even the MR is optional. - (optional) If you want email through your domain, it's already set up
to use https://improvmx.com/ out of the box, which is free for a
single domain! All you should need to do is make an account and input
your domain, and all should be green. If you want to do email through
another service, you'll have to modify
infra/domain.tf
.
Now, if you do end up using this "for real", please let me know, because it makes me happy!
Also, you may want to merge from this repo occasionally in case I've made improvements to the system. I don't anticipate adding any features really, though may improve the infrastructure setup (mainly to make it cheaper).