Building Your Serverless Sandbox: A Detailed Guide to Multi-Environment Deployments (or How I Learned to Stop Worrying and Love the Cloud)

Written by:

Introduction

Welcome, intrepid serverless adventurers! In the wild world of cloud computing, creating a robust, multi-environment deployment pipeline is crucial for maintaining code quality and ensuring smooth transitions from development to production.
Here is part 1 and part 2 of this series. Feel free to read them before continuing on.

This guide will walk you through the process of setting up a serverless sandbox using GitLab CI/CD, Terraform, and AWS. By the end, you’ll have a flexible, scalable system that allows developers to work independently while maintaining a clear path to production.

Here is a diagram of what we are building. Feel free to read the first 2 blog posts to get a refresher of what we are building.

Prerequisites

Before we dive in, make sure you have:

  • An AWS account with appropriate permissions
  • A GitLab account (self-hosted or GitLab.com)
  • Terraform (version 0.14 or later) installed locally
  • Git installed locally (because time travel is essential in software development)
  • Basic familiarity with YAML, Terraform, and AWS services (or a high tolerance for learning curves)
  • A sense of humor (trust me, you’ll need it)

Setting up OIDC Integration between GitLab and AWS IAM

Before we set up our GitLab repository, we need to configure the OIDC integration between GitLab and AWS IAM. If you feel like reading the details, here is the documentation.

Create an Identity Provider in AWS IAM

  1. Log in to your AWS Management Console and navigate to the IAM service.
  2. In the left sidebar, click on “Identity providers” under “Access management”.
  3. Click the “Add provider” button.
  4. Select “OpenID Connect” as the provider type.
  5. For the provider URL, enter: https://gitlab.com f. For the Audience, enter: https://gitlab.com
  6. Click “Get thumbprint” to retrieve the server certificate thumbprint.
  7. Click “Add provider” to create the Identity Provider

Create an IAM Role for GitLab 

  1. Log in to your AWS Management Console.
  2. Navigate to the IAM service:
    • Click on “Services” at the top of the page.
    • Under “Security, Identity, & Compliance”, click on “IAM”.
  3. In the left sidebar of the IAM dashboard, click on “Roles”.
  4. Click the “Create role” button.
  5. Under “Trusted entity type”, select “Web identity”.
  6. In the “Web identity” section:
    • For “Identity provider”, select the GitLab provider you created earlier (it should be listed as “gitlab.com”).
    • For “Audience”, select “https://gitlab.com” from the dropdown.
  7. Click “Next” to proceed to the permissions page.
  8. In the “Add permissions” page:
    • Use the search bar to find and select the policies needed for your Terraform operations. Common policies might include:
      • AmazonS3FullAccess
      • AWSLambda_FullAccess
      • AmazonDynamoDBFullAccess
      • AmazonAPIGatewayAdministrator
      • CloudWatchLogsFullAccess
    • Select any other policies required for your specific infrastructure needs.
  9. After selecting all necessary policies, click “Next”.
  10. On the “Name, review, and create” page:
    • Enter a role name (e.g., “GitLabOIDCRole”).
    • (Optional) Enter a description for the role.
    • Review the trusted entities and permissions to ensure they’re correct.
  11. Click “Create role” at the bottom of the page.
  12. You’ll be redirected to the Roles page. Find and click on the role you just created.
  13. On the role’s summary page, note the “Role ARN” at the top. You’ll need this ARN when configuring your GitLab CI/CD variables.

Remember, the exact permissions you attach to this role should align with the principle of least privilege. Only grant the permissions necessary for your specific Terraform operations and AWS resource management needs.

Setting Up Your GitLab Repository

Feel free to fork my repo. You will get better training value if you do everything from scratch though.

  1. Log into your GitLab account and create a new project. Try to resist the urge to name it “yet-another-project-that-will-change-the-world”. Clone the repository to your local machine:
    git clone https://gitlab.com/your-username/your-project.git
    cd your-project
  2. In the project settings, navigate to Settings > CI/CD > Variables.
  3. Add the following variable:
    AWS_GITLAB_ROLE_ARN: The ARN of the IAM role that GitLab will assume. Make the variable masked. Do not protect this variable. Protection implies that this variable will be unavailable to any branch that is not protected. Therefore if you attempt to deploy your infrastructure from an unprotected branch, your pipeline will fail as it will not have access to this variable.

Crafting Your Terraform Code

Create the following directory structure in your project. Or feel free to fork my GitLab repo if you want to make it quick.

In main.tf, define your AWS provider. For testing, we will simply create an S3 bucket in resources.tf file. Feel free to create any additional terraform resources but any resources you declare in the terraform code must be creatable by the IAM role you created above or else, creation will fail.

Configuring the GitLab CI/CD Pipeline

Understanding Pipeline Variables

At the top of your .gitlab-ci.yml file, you’ll find several variables defined.

These variables serve various purposes:

  • TF_ROOT: Defines the root directory for Terraform operations.
  • TF_ADDRESS and TF_HTTP_ADDRESS: Specify the address for the Terraform HTTP backend, which stores the state files. In our case, it is the GitLab HTTP backend.
  • TF_IN_AUTOMATION: Tells Terraform it’s running in an automated environment.
  • TF_VAR_*: These variables are passed to Terraform as input variables, allowing you to use GitLab CI/CD information in your Terraform configurations.

TF_HTTP_USERNAME and TF_HTTP_PASSWORD: Provide authentication for the Terraform HTTP backend.

Cache and Before Script Blocks

The cache block helps speed up subsequent pipeline runs by caching the .terraform directory, which contains downloaded providers and modules. The before_script block runs at the start of every job. It changes to the Terraform root directory and installs curl and jq, which are often useful for script operations.

Pipeline Stages

These stages are like the circle of life for your infrastructure. From nothing, to something, to something better, and then back to nothing. It’s poetic, really. These stages define the order of operations in your pipeline:

  • prepare: Initial setup and validation
  • build: Creating the Terraform plan
  • deploy: Applying the Terraform plan
  • promote: Moving changes between environments
  • destroy: Tearing down the infrastructure (usually manual)

Reusable Templates

The pipeline uses several reusable templates:

  • setup_aws_config: Configures AWS credentials using OIDC.
  • validate_script: Runs terraform validate to check for configuration errors.
  • plan_script: Creates a Terraform plan.
  • apply_script: Applies the Terraform plan.
  • destroy_script: Destroys the Terraform-managed infrastructure.
  • promotion_script: Handles promoting changes between environments.

These templates encapsulate common operations, making the pipeline more maintainable and reducing duplication. Read the optional section at the end for details of these scripts.

YAML Anchors and Aliases

YAML anchors and aliases are used to avoid repetition in the pipeline file. For example:

The &setup_aws_config defines an anchor, and *setup_aws_config references it, allowing you to reuse the configuration without duplication.

Using ‘extends’ for Job Templates

The extends keyword is used to inherit configuration from job templates:

This allows you to define common job configurations once and reuse them across multiple jobs.

ID Token Usage for IAM Role Credentials

This configuration requests an OIDC token from GitLab, which is then used to assume an AWS IAM role. This provides secure, temporary AWS credentials without needing to store long-lived access keys.

Rules for Conditional Job Execution

Rules are used to control when jobs run:

This example rule ensures the job only runs for branches other than “main”.

Artifacts Passing from Plan to Apply Jobs

This configuration saves the Terraform plan as an artifact, which can then be used by the apply job. This ensures that what’s applied is exactly what was planned.
Environments, Deployment Tiers, and Actions

In our GitLab CI/CD pipeline, we use environments, deployment tiers, and actions to organize and manage our deployments effectively. These elements work together to provide a clear structure for our multi-environment deployment strategy.

Environments

Environments in GitLab represent different stages in your software development lifecycle. In our pipeline, we have several environments:

  • sandbox-a, sandbox-b, sandbox-c, sandbox-d: Individual developer environments
  • sandbox-hotfix, sandbox-minor-release: Special sandboxes for hotfixes and minor releases
  • pre-prod, pre-prod-hotfix, pre-prod-minor-release: Pre-production environments
  • production: The live production environment

Each environment is isolated, allowing developers to work independently without affecting others or the production system.

Deployment Tiers

Deployment tiers categorize your environments based on their purpose in the development lifecycle. In our pipeline, we use three tiers:

  • development: For sandbox environments where initial development and testing occur
  • testing: For pre-production environments where integration testing and final checks are performed
  • production: For the live production environment

Using deployment tiers helps in visualizing the progression of changes through your pipeline and can be used to apply different rules or approvals based on the tier.

Actions

Actions define what operation is being performed on the environment. Common actions include:

  • start: Indicates that the job is deploying to or starting up the environment
  • stop: Used when tearing down or stopping an environment
  • prepare: For jobs that set up an environment but don’t deploy to it
  • verify: For jobs that run tests or checks on an environment

In our pipeline, we primarily use the start action as we’re deploying to our environments:





How They Work Together in the Pipeline

  1. Sandbox Deployments: When a developer pushes code to a branch, it triggers a deployment to their sandbox environment:
  2. Promotion to Pre-production: When code is ready for further testing, it’s promoted to a pre-production environment
  3. Production Deployment: Finally, when code is fully tested and ready for release, it’s deployed to production

By using these elements, our pipeline provides:

  • Clear visualization of where code is deployed
  • Isolated environments for development and testing
  • A structured promotion process from development to production
  • Ability to apply different rules and approvals based on environment and tier
  • Easy tracking of deployment history for each environment

Promotion Paths and Script

The promotion script is used to promote changes from a lower environment to a higher one, ensuring that exactly what was deployed in the lower environment is replicated in the higher one.

There are three main promotion paths:

  1. sandbox-a -> pre-prod -> prod
  2. sandbox-hotfix -> pre-prod-hotfix -> prod
  3. sandbox-minor-release -> pre-prod-minor-release -> prod

Each path serves a different purpose:

  • The first is for regular feature development and releases.
  • The second is for urgent fixes that need to bypass the regular release cycle.
  • The third is for planned minor releases that may include multiple features or fixes.

Managing Multiple Terraform States

GitLab’s HTTP backend is used to manage multiple Terraform states. The state file name is based on the environment name:

Testing Your Setup

To test your setup:

  1. Make changes to your Terraform configuration.
  2. Commit and push your changes to GitLab.
  3. Observe the pipeline execution in GitLab CI/CD interface.
  4. Verify that resources are created in AWS as expected.
  5. Test the promotion process by manually triggering the promotion jobs.

Best Practices and Tips

  • Use clear, consistent naming conventions for all resources and interpolate the environment name into resource name so that if there are multiple resource being created in the same AWS account, they do not come into naming conflicts.
  • Implement proper IAM policies to restrict access between environments.
  • Use GitLab’s environment-scoped variables for sensitive information.
  • Regularly clean up unused resources to avoid unnecessary costs.

Conclusion

This guide has walked you through setting up a sophisticated multi-environment deployment pipeline for serverless applications using GitLab CI/CD, Terraform, and AWS. By leveraging GitLab environments, Terraform state management, and AWS OIDC integration, we’ve created a system that allows for isolated development while maintaining a clear path to production.

This setup provides great flexibility for development teams, supporting various workflows including feature development, hotfixes, and minor releases. However, it’s important to maintain discipline in your development and deployment processes to fully benefit from this setup.

Remember, while this guide provides a solid foundation, you may need to adjust and expand upon it to fit your specific needs and workflows. Happy serverless coding!

Optional Deep Dive into Reusable Templates

For those who want to understand the inner workings of our pipeline, let’s break down each of our reusable templates and explain how their commands function.

setup_aws_config: Configuring AWS Credentials Using OIDC

  1. mkdir -p ~/.aws: Creates the AWS configuration directory if it doesn’t exist.
  2. echo "${GITLAB_OIDC_TOKEN}" > /tmp/gitlab-oidc-token: Saves the GitLab-provided OIDC token to a temporary file.
  3. The next four echo commands create an AWS config file with a web-identity profile:
    • Specifies the IAM role to assume
    • Points to the OIDC token file
    • Sets a unique session name
  4. export AWS_PROFILE="web-identity": Sets the AWS CLI to use this profile

This setup allows the pipeline to authenticate with AWS using the OIDC token, enhancing security by avoiding long-lived access keys.

validate_script: Running Terraform Validate

  1. Defines a terraform_validate function.
  2. If TF_ROOT is set, it prepares a -chdir option to run Terraform in a specific directory.
  3. Runs terraform init with -backend=false to initialize Terraform without configuring a backend.
  4. Runs terraform validate to check the configuration for errors.

This script ensures that your Terraform configurations are syntactically correct and internally consistent.

plan_script: Creating a Terraform Plan

  1. Calls setup_aws_config to set up AWS credentials.
  2. Defines a terraform_plan function.
  3. Sets up the -chdir option if TF_ROOT is defined.
  4. If TF_IMPLICIT_INIT is true, runs terraform init.
  5. Runs terraform plan, outputting the plan to a file named after the current environment.
  6. Uses environment-specific and common variable files.

This script creates a Terraform plan, showing what changes would be made to your infrastructure.

apply_script: Applying the Terraform Plan

  1. Calls setup_aws_config to set up AWS credentials.
  2. Defines a terraform_apply function.
  3. Sets up the -chdir option if TF_ROOT is defined.
  4. If TF_IMPLICIT_INIT is true, runs terraform init.
  5. Runs terraform apply using the plan file created in the plan stage.

This script applies the Terraform plan, making the specified changes to your infrastructure.

destroy_script: Destroying Terraform-Managed Infrastructure

  1. Calls setup_aws_config to set up AWS credentials.
  2. Defines a terraform_destroy function.
  3. Sets up the -chdir option if TF_ROOT is defined.
  4. If TF_IMPLICIT_INIT is true, runs terraform init.
  5. Runs terraform destroy, using environment-specific and common variable files.

This script destroys all Terraform-managed infrastructure in the specified environment.

promotion_script: Promoting Changes Between Environments

  1. Defines an apply_to_target function that takes source and target environments as parameters.
  2. Creates a temporary directory and extracts the source environment’s artifact into it.
  3. Changes to the extracted directory and initializes Terraform.
  4. Sets the Terraform state address for the target environment.
  5. Applies the Terraform configuration to the target environment, using the target’s variable files.
  6. Checks the exit status and fails the job if the promotion was unsuccessful.

This script handles the promotion of changes from one environment to another, ensuring that the exact configuration from the source environment is applied to the target environment.

These reusable templates form the backbone of our CI/CD pipeline, handling everything from AWS authentication to infrastructure deployment and promotion between environments. By understanding these scripts, you can better customize and troubleshoot your pipeline as needed.

Leave a comment