Skip to content

Latest commit

 

History

History
194 lines (122 loc) · 7.36 KB

README.md

File metadata and controls

194 lines (122 loc) · 7.36 KB

fenna

fenna is a thin wrapper for Terraform built around a set of conventions extracted from my experience introducing Terraform into teams. The overall goal is to support collaboration through a consistent developer experience.

With fenna, developers can develop against their own instances of a service while ensuring the same Terraform is deployed across all environments. This is especially handy if you are programming with infrastructure.

For a longer discussion of the motivations behind fenna, check out my post “Terraform for Teams”.

For an in-depth example of bootstrapping a multi-account setup with AWS, check out “How to Bootstrap Multiple Environments on AWS with Terraform & Fenna”.

This documentation assumes familiarity with the basics of Terraform, so if you’re new to Terraform, you’ll want to start here.

Currently, fenna only supports AWS and Git.

Conventions

fenna introduces some high-level conventions that make working with Terraform and communicating with your team simpler.

Service

A service is a collection of AWS resources and code that fulfills a desired function. It has a well-defined interface that other services and users can interact with. And it is something we want to deploy and test as a unit.

Environment

An environment is where we want to deploy a service. Each environment has a single backend, and each environment is isolated in its own AWS account. An environment’s backend is not typically stored in the environment—instead, we create an environment dedicated to storing things like Terraform state.

Sandbox

A sandbox is an environment where a developer can safely create their own instance of a service for development and testing.

Suffix

To prevent name collisions in a sandbox, a developer can configure a suffix that is appended to the service’s state key on init and passed into the service’s top-level Terraform module as a variable on every plan.

Should developers need to collaborate on the same sandbox infrastructure, they need only set the same suffix.

To simplify usage of the suffix within your HCL (and automatically handle a blank suffix), use local.suffix instead of var.suffix.

Root

The root environment is where access control to the other environments is managed. Similarly, a developer’s root profile holds the credentials for accessing the root environment.

Each developer can name their root profile anything they like (which comes in handy when, like me, you’re regularly working with more than one team).

Profiles

fenna uses named profiles to standardize and simplify access to each environment.

Dependence on AWS profiles is probably the single biggest blocker to supporting other backends and providers. Pull requests to resolve this are most welcome.

fenna assumes profile names will follow this convention: [root profile]-[env].

For example, here is a sample AWS configuration with ops as the root profile:

~/.aws/credentials

[ops]
aws_access_key_id=[insert-access-key-id-here]
aws_secret_access_key=[insert-secret-access-key-here]

~/.aws/config

[profile ops]

[profile ops-tools]
source_profile = ops
role_arn = arn:aws:iam::[insert-account-id-here]:role/Ops

[profile ops-dev]
source_profile = ops
role_arn = arn:aws:iam::[insert-account-id-here]:role/Ops

[profile ops-stage]
source_profile = ops
role_arn = arn:aws:iam::[insert-account-id-here]:role/Ops

[profile ops-prod]
source_profile = ops
role_arn = arn:aws:iam::[insert-account-id-here]:role/Ops

Backends

fenna keeps backend configuration DRY across multiple services via Git submodules and symlinks. You can manually create backend configuration files at .fenna/backends for each service, but you’re better off extracting all backend configuration into its own repository.

fenna assumes each backend configuration will be stored at the root of the backends repository in a file named for its corresponding environment—for example, root.tfvars, tools.tfvars, dev.tfvars, etc.

Each file should contain the necessary partial configuration for each backend:

bucket         = ""
region         = ""
dynamodb_table = ""
encrypt        = true
kms_key_id     = ""

Now you only need a minimal terraform block:

terraform {
  required_version = "~> 0.12"
  backend "s3" {}
}

Installation

If you're using Homebrew:

brew tap jdhollis/fenna
brew install fenna

Alternatively, you can download the fenna script and drop it somewhere in your PATH.

Usage

bootstrap

When creating a new service that uses fenna, run the following from the service’s root module:

fenna bootstrap

fenna will ask a series of questions to configure the service for everyone. (It will then follow on with the onboard process for you.)

Commit any changed files to the repo—this will serve as the base configuration for any developers collaborating on the service in the future.

onboard

When a developer starts working on a service for the first time, they need to run the following from the service’s root module:

fenna onboard

This will configure the developer’s root profile and, if necessary, suffix.

The files generated by fenna onboard are ignored by Git by default—they are developer-specific and should not be committed to the repository.

init, plan, apply

fenna init
fenna plan
fenna apply

These commands are wrappers around terraform. You can pass additional arguments to terraform through them.

For example:

fenna init -upgrade

Or:

fenna plan -destroy
fenna apply

init injects the necessary backend details into terraform including profile and key (with an appended suffix if targeting a sandbox).

plan injects profile, root_profile, service_name, and suffix into terraform. When targeting a sandbox, it also injects a tfvars file for that environment (e.g., dev.tfvars) and a user.tfvars for developer-specific overrides.

apply just applies the plan.

CI/CD

fenna is intended for local usage, but we always want to use identical HCL across all environments whenever possible.

There is an assume_role_arn variable that can be added to your provider blocks for injecting the proper role ARN during an automated build:

provider "aws" {
  version = "~> 2.68"
  region  = var.region
  profile = var.profile

  assume_role {
    role_arn = var.assume_role_arn
  }
}

In CI/CD, you'll also need to handle injecting the backend details.