HumanGov: Streamlining AWS Infrastructure with Terraform Modules: A Secure Guide to Multi-Tenant SaaS Deployment on AWS CodeCommit

Karishe Reid
9 min readJan 24, 2024

--

In the Humangov project, my role as a Cloud Architect involved designing and implementing a multi-tenant SaaS infrastructure on AWS, utilizing the power of Terraform modules. HumanGov, the web application at the core of this project, operates on an AWS EC2 instance. It’s designed to collect user data, which is stored in AWS DynamoDB, and to manage driver’s license information securely in an AWS S3 bucket. The use of Terraform modules was a strategic choice, greatly enhancing the efficiency of configuring services across all 50 states. For the purposes of secure and efficient management of our infrastructure code, AWS CodeCommit was employed, a decision that is exemplified in Figure-1. This approach not only streamlined our operations but also ensured a high standard of security and scalability for the HumanGov application.

(Architectural Diagram)

The successful execution of this project was marked by a continuous progression of stages, each laying the groundwork for the next. This step-by-step development led to the seamless completion of ‘HumanGov,’ our SaaS application, showcasing a well-orchestrated blend of planning, execution, and refinement throughout the project lifecycle.For completing these phases, one of key provision I utilized is an AWS Cloud9 Integrated Development Environment (IDE), which contains already established folders for both the application and infrastructure components. Additionally remote code repositories within AWS CodeCommit has already been set up.

The first task needed and completed by myself is craft a dedicated project folder called ‘human-gov-infrastructure,’ a central repository for our Terraform files residing in the ‘terraform’ root folder. Next, create a root module folder named ‘modules’ and nest another folder ‘aws_humangov_infrastructure,’ to house our essential module files.” The file structure is illustrated below in Figure 1.

Figure 1- human-gov-infrastructure folder directory

Within the aws_humangov_infrastructure folder, I create 3 files. The first one is the ‘main.tf’ file, which plays a pivotal role in specifying the system resources to be deployed in Amazon Web Services, including EC2 instances, DynamoDB databases, and S3 buckets. In Terraform, the ‘main.tf’ file serves as the primary configuration file where infrastructure resources and their settings are defined and declared. In the ‘HumanGov’ project I shared, it’s the ‘main.tf’ file that outlines the core infrastructure components and their configurations. In this project, I am going to deploy an EC2 instance, a DynamoDB table, a S3 bucket just for the states of California, Florida and Texas as illustrated in the main.tf illustrated below in Figure 2.

Figure 2- HumanGov Infrastructure main.tf file

The second file created is the ‘output.tf’ file in Terraform for defining and declaring the outputs or values that should be made accessible after provisioning HumanGov infrastructure, allowing users to retrieve important information or results from the configuration. This file is illustrated in Figure-3.

Figure 3- HumanGov Infrastructure output.tf file

The third one is the ‘variables.tf’ file which in Terraform is where you list the things you might want to change about your infrastructure, like the size of a server or the name of a resource which is reflected in Figure-4 below.

Figure 4- HumanGov Infrastructure variables.tf file

Having finalized the configuration files in the ‘aws_humangov_infrastructure’ directory, my next step involves setting up the ‘main.tf’, ‘variables.tf’, and ‘outputs.tf’ files within this same folder. While these files share their names with those in a previous directory, their contents will be distinct, tailored to the specific needs of this phase of the project. These files are integral to invoking the reusable modules we’ve previously defined, enabling an efficient deployment and management of our AWS infrastructure. The nuances of this setup and how these files interlink within our architecture are clearly depicted in Figure 5, illustrating the strategic organization and planning behind our infrastructure configuration.

Figure 5 — main.tf file

Next, we have the ‘variables.tf’ file, which defines the variable ‘states’ without assigning actual values. The flexibility of this approach lies in ‘terraform.tfvars’ or variable overrides, where the specific values for the ‘states’ variable can be assigned. This structure is advantageous because it allows for easy addition or modification of US state values as required, without altering the core module configuration. This is illustrated in Figure 6 below.

Figure 6- variables.tf

Finally, I have crafted an ‘outputs.tf’ file in the parent directory, as illustrated in Figure 7. This file is designed to capture and display the outputs generated by the reusable modules, providing a clear view of the results and data returned from these modules upon execution.

Figure 7- outputs.tf file

Once the configuration files are prepared, I execute a series of Terraform commands to manage and apply the infrastructure changes:

  • terraform fmt: This command reformats the configuration files into a standardized format and style, ensuring consistency across the codebase.
  • terraform init: This initializes the working directory that contains the Terraform configuration files. It also downloads and installs any necessary plugins for the providers specified in the configuration.
  • terraform validate: This command checks the configuration files in the directory for any errors or inconsistencies, ensuring that the configuration is syntactically valid and internally consistent before creating an execution plan.
  • terraform plan: This generates an execution plan, providing a preview of the changes Terraform intends to make to the infrastructure based on the current configuration.
  • terraform apply: Finally, this command applies the proposed changes detailed in the Terraform plan, creating, updating, or destroying infrastructure resources as defined in the configuration files. Look at Figure 8 below.
Figure 8- AWS resources provisioned

The next step is validating that these resources were created, provisioned and deployed on Amazon Management Console as reflected in Figures 9–11 below.

Figure 9- EC2 Instances
Figure 10- Amazon S3 Buckets
Figure 11- Dynamo DB Tables

Now that all the infrastructure resources have been successfully provisioned, the next critical step was to establish a remote backend for managing the Terraform state. To ensure resilience and avoid the risk of accidental deletion through a Terraform destroy operation, the remote state storage components were set up manually, outside of Terraform’s management. For this purpose, Amazon S3 was chosen as the storage solution for the terraform.tfstate file, providing a secure and scalable environment. Additionally, Amazon DynamoDB was utilized for state locking and consistency checking, safeguarding against concurrent state modifications and potential conflicts. Figure 12 illustrate the contents of the terraform.tfstate file created applicable to this project.

Figure 12- terraform.tfstate file

Then i went onto the Amazon Web Services management console to validate creation of this resource in the allocated s3 bucket reflected in Figure 13 below.

Figure 13- terraform-state file in AWS Management console via S3 bucket

I also make it a point to inspect the DynamoDB table to verify the presence of the state lock record. This step is crucial as it ensures that the state locking mechanism, integral for maintaining consistency and preventing simultaneous state modifications in Terraform, is functioning correctly.

Figure 14- state-file locking file present in Dynamo DB

The concluding step in my workflow involved integrating the Terraform Infrastructure as Code (IaC) into a pre-configured AWS CodeCommit repository. To begin this process, I first created a .gitignore file. This was a crucial step to ensure that only relevant files were included in the repository, excluding any unnecessary or sensitive files (like state files or override configurations) from being tracked and uploaded. Look at Figure 15.

Figure 15- gitignore

Following the creation of the .gitignore file, I executed the command git add . to stage all the relevant files for the next commit. To double-check which files were staged, I ran git status.This step provided a clear overview of the changes queued for the next commit, ensuring that only the intended files were included for upload to the CodeCommit repository, in line with my configuration and security practices. The illustration in Figure 16 reflects this action being taken.

Figure 16- Git Add and Git Status commands output

After completing these steps, the modified Terraform code scripts are ready to be committed. As depicted in Figure 17, these updates are then securely pushed to the AWS CodeCommit repository. This action ensures that all changes are tracked and stored in the central version-controlled environment provided by CodeCommit, facilitating collaboration and version management of the Terraform scripts.

Figure 17- Changes pushed AWS CodeCommit repository

Aligned with the process outlined in Figure 18, to confirm the successful commitment of these Terraform files, I navigated to the AWS Management Console and accessed CodeCommit. This step allowed me to directly verify that the files had been accurately and securely committed to the repository, ensuring that our infrastructure code is up-to-date and consistently managed within AWS’s robust version control system.

Figure 18- Terraform files were committed and validated

The Humangov infrastructure project’s adoption of Terraform introduced several key benefits, both from a business efficiency and IT security perspective:

  1. Efficient Configuration Management: One significant advantage was the use of Terraform’s modular approach. This allowed for the streamlined addition of new US states to the infrastructure with minimal configuration changes. Instead of laboriously coding resources for each state individually, a single line modification sufficed, offering immense time savings, especially when making widespread changes like updating EC2 instance types.
  2. Enhanced Collaboration and Security with Remote State Management: The decision to avoid local .tfstate files, opting instead for Terraform’s remote state capabilities with S3 and DynamoDB, proved invaluable. This approach not only secured state files but also facilitated a collaborative environment. It prevented concurrent operations by different DevOps engineers, as Terraform’s state locking mechanism ensured that only one set of operations could be performed at a time, thus avoiding conflicts and potential disruptions.
  3. Robust Version Control and Accessibility: Lastly, the integration of Infrastructure as Code (IaC) into a version-controlled code repository aligned with best practices in DevOps. It ensured that the infrastructure code was as manageable and trackable as application code. This setup made the code readily accessible to team members and safeguarded it with robust version control, contributing to both operational efficiency and enhanced security.

Key takeaways for me on this project included using the modules in Terraform so that I could easily add more US states with just one configuration line change, rather than hard-coding individually all of the resources for each and every state that needed to be created (imagine the time savings if anything needed to globally change like EC2 instance type, etc.)

I also learned the value of not having a local .tfstate file, instead using Terraform’s ability to use remote state and S3 and DynamoDB as a remote store to handle this for me. This is beneficial because if another DevOps engineer were to work with the same Terraform files while I was running Terraform commands, they would not be able to do operations because Terraform would be in a locked state.

Finally, I saw the wisdom in having IaC stored in a code repository where it is accessible and version-controlled like any application should be, which is definitely a DevOps best practice.

--

--

Karishe Reid

Senior IT Security Professional with focus on Cloud, AI & DevSecOps | AWS | Microsoft Azure | Google Cloud