This repo contains the Terraform, Ansible, and Capistrano configurations to deploy a static Jekyll site to multiple instances behind a LoadBalancer.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

5.9 KiB

Linode Static Site Infrastructure

This repo provides the Terraform and Ansible configuration for generating and hosting static-sites using Linode.

The infrastructure inside Linode will consist of two application instances served behind a NodeBalancer. These instances will have external IPs used to configure and deploy to them. Linode Firewalls in combination with UFW are used to limit access to them.

On the instances themselves, an administrative user and deploy user are created (as well as a user for Caddy). Caddy is used as the web-server in order to accommodate automatic-HTTPS of each site.

The jekyll site provided in this repo is an example and includes the necessary Capistrano configuration to perform deploys.

Hint: If configured properly using the provided setup, a different set of static sites can be served to multiple domains from the same set of app instances.

Using the repo

Initialize Linode Infrastructure - Terraform

First, clone the repo. Then cd terraform/. From here, create a .envrc file to load a few custom variables into your shell using direnv:

    export TF_VAR_token=TF_VAR_token=<<linode_account_api_token>>
    export TF_VAR_root_pass=<<randomly_generated_root_pass_for_all_instances>>
    export AWS_ACCESS_KEY_ID=<<linode_object_storage_access_key>>
    export AWS_SECRET_ACCESS_KEY=<<linode_object_storage_secret_access_key>>

Replace the name and region names in terraform/backend.tf to match the bucket created to store the .tfstate file.

Use direnv to load the vars and Terraform to the Linode account:

    echo "export TF_VAR_token=<<linode_account_api_token>>" >> .envrc  ## Add this manually, don't put a token in a CLI history.
    
    direnv allow
    terraform init

Once Terraform has been initialized, set the appropriate variables for the desired infrastructure in site.auto.tfvars:

    site = "example.com"
    region = "us-southeast"
    environment = "production"
    app_servers = [
    {
            type = "g6-nanode-1"
            image = "linode/ubuntu20.04"
    },
    {
            type = "g6-nanode-1"
            image = "linode/ubuntu20.04"
    }
    ]
    bastion_server = {
            type = "g6-nanode-1"
            image = "linode/ubuntu20.04"
    }
    ssh_key = "~/.ssh/id_rsa.pub"

After filling in the variables, use terraform plan to ensure the proper infrastructure will be generated. terraform apply will generate the infrastructure.

Upon completion, terraform apply will supply the IPv4 addresses of the NodeBalancer, as well as the site instances:

    Outputs:

    linode_instance_ip_address = [
    toset([
    "192.168.232.187",
    "45.79.216.88",
    ]),
    toset([
    "192.168.144.144",
    "45.79.216.70",
    ]),
    ]
    nodebalancer_ip_address = "45.79.245.251"

Use the NodeBalancer IP address to set the DNS A record for the website(s).


Configure Application VMs - Ansible

Once the infrastructure has been created, cd to the ansible/ directory. From here, we'll set a few inventory variables, and the IP addresses of the hosts.

In inventories/production/hosts set the IP address of each instance created. Use the external IP addresses provided by the terraform output.

Using inventories/production/group_vars/all, set the variables for the site in /common-main.yml and /main.yml:

    ## main.yml
    # Website/Blog settings
    domain: "example.com"
    staging_domain: "staging.example.com"
    site_name: "site"


    ## common-main.yml
    ruby_version: '2.7'
    bundler_version: '2.1.4'

    ssh__keys:
    - key: ssh-key-of-the-deployer

After setting these variables appropriately for your project, use the site.yml playbook to install the configuration.

    ansible-playbook site.yml -i inventories/production/hosts --diff

Since this is the first time running the playbook on the instances, we won't be using --check as we need to install python first

Using the defaulted configuration will result in a few convenient settings:

  • A staging site is served at /srv/{{site_name}}-staging/
  • A production site is served at /srv/{{site-name}}/
  • A jekyll user can be used for deploys.
  • An ops user exists to perform administrative actions as needed.

Deploy a site with Jekyll

A default Jekyll site exists in the /jekyll-site repo. This site will use Capistrano for deploys to the instances.

Navigate to the jekyll-site directory.

First, look in config/deploy.rb and set the ssh url of the repository storing the site.

Set the app instance IP addresses in config/production.rb and config/staging.rb. Additionally, ensure the deploy_to directories match the deploy directories set in the Ansible configuration.

From here, use Capistrano to deploy the site to the instances:

    bundle exec cap production deploy --trace

--dry-run can be used to test the deploy

Wrapping up

If all the steps have been completed, the instances should be serving their static site content at the specified domains. To make the infrastructure even more secure, we can take a few additional steps to secure different aspects from the initial build.

First, navigate to terraform/firewall.tf. In this file, remove the inbound rules for port 22. Since we have changed the default port to 8822 we can now close the traffic to this port completely. apply this change to the firewall.

Second, navigate to ansible/inventories/production/hosts. Notice that the ansible_user is set as root and the ansible_port is 22. Change the port to 8822 and the user to the specified admin user (default: ops). Root is used for initial ansible run as Linode only provides root access to start. After initial run is complete, port 22 is closed.