Setup basic SaaS infrastructure using Terraform
Tags: terraform, devops, sre, infra
Published: 2022-12-04 15:48:52

## Introduction

In this tutorial, we will be using Terraform to setup the infrastructure for a SaaS company from scratch. We will be using various AWS resources including S3 buckets, ACM Certificate, Route53 entries, VPC, EC2 instances, CloudWatch, Lambdas, RDS, and API Gateways.

## Prerequisites
- AWS Account
- Terraform installed on your system
- AWS CLI configured on your system


## Getting Started
Before we start, let's create a new Terraform project and initialize it. Create a new directory and change your current directory to it.

```shell
mkdir my-saas-company
cd my-saas-company
```

Now, run the following command to initialize the Terraform project.

```shell
terraform init
This will download the required plugins for the AWS provider.
```

### Setup S3 Buckets
We will start by setting up four S3 buckets for hosting our SaaS company's website and other data. Add the following code to a file named s3.tf.

```tf
resource "aws_s3_bucket" "hosting" {
  count          = 4
  bucket         = "my-saas-company-${count.index+1}"
  acl            = "public-read"
  force_destroy  = true
}
```

This code will create four S3 buckets with names my-saas-company-1, my-saas-company-2, my-saas-company-3, and my-saas-company-4. The count and count.index+1 variables are used to create multiple resources with different names. The force_destroy attribute is used to ensure that the S3 bucket is deleted along with all its contents when the Terraform project is destroyed.

### Setup ACM Certificate
Next, we will create an ACM (AWS Certificate Manager) certificate for our domain. Add the following code to the same s3.tf file.

```tf
resource "aws_acm_certificate" "certificate" {
  domain_name       = "my-saas-company.com"
  validation_method = "DNS"

  tags = {
    Name = "my-saas-company-certificate"
  }
}
```

This code will create an ACM certificate for the domain my-saas-company.com using DNS validation. You can also add tags to the certificate for easier identification and management.

### Setup Route53 Entries
Next, we will setup Route53 entries for our domain and S3 buckets. Add the following code to the s3.tf file.

```tf
resource "aws_route53_record" "website" {
  count       = 4
  zone_id     = aws_route53_zone.primary.zone_id
  name        = "my-saas-company-${count.index+1}.my-saas-company.com"
  type        = "A"
  alias {
    name                   = aws_s3_bucket.hosting.*.website_endpoint[count.index]
    zone_id                = aws_s3_bucket.hosting.*.hosted_zone_id[count.index]
    evaluate_target_health = true
   }
}

resource "aws_route53_zone" "primary" {
   name = "my-saas-company.com"
}
```

This code will create four Route53 entries, one for each of our S3 buckets. The `aws_route53_zone` resource is used to create a new zone for our domain. The `aws_route53_record` resource is then used to create four A-type records for the subdomains of our S3 buckets. The `alias` attribute is used to link these records to our S3 buckets.

## Setup VPC
Next, we will setup a VPC (Virtual Private Cloud) for our EC2 instances. Add the following code to a new file named `vpc.tf`.

```tf
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "my-saas-company-vpc"
  }
}
```

This code will create a VPC with a CIDR block of `10.0.0.0/16`. You can also add tags to the VPC for easier identification and management.

## Setup EC2 Instances
Next, we will create EC2 instances for our workers and development environment. Add the following code to the `vpc.tf` file.

```tf
resource "aws_instance" "worker" {
   count = 2
   ami = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
   vpc_security_group_ids = [aws_security_group.instance.id]
   tags = {
      Name = "my-saas-company-worker-${count.index+1}"
   }
}

resource "aws_instance" "dev" {
   ami = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
   vpc_security_group_ids = [aws_security_group.instance.id]

   tags = {
      Name = "my-saas-company-dev"
   }
}

resource "aws_security_group" "instance" {
   name = "my-saas-company-instance"
   description = "Security group for EC2 instances"
   vpc_id = aws_vpc.main.id

   ingress {
      from_port = 0
      to_port = 0
      protocol = "-1"
      cidr_blocks = ["0.0.0.0/0"]
   }

   egress {
      from_port = 0
      to_port = 0
      protocol = "-1"
      cidr_blocks = ["0.0.0.0/0"]
   }

   tags = {
      Name = "my-saas-company-instance"
   }
}
```

This code will create two worker instances and one development instance. The AMI used in this example is the latest Amazon Linux AMI. You can choose any AMI that suits your needs. The `aws_security_group` resource is used to create a security group for our instances that allows all traffic.

## Setup CloudWatch Logging
CloudWatch is a useful tool for monitoring and logging the performance and health of your AWS resources. We will setup CloudWatch logging for our EC2 instances. Add the following code to the vpc.tf file.

```tf
resource "aws_cloudwatch_log_group" "instance" {
  name              = "my-saas-company-instance"
  retention_in_days = 14

  tags = {
    Name = "my-saas-company-instance"
  }
}

resource "aws_cloudwatch_log_stream" "instance" {
  count             = 2
  name              = "my-saas-company-worker-${count.index+1}"
  log_group_name    = aws_cloudwatch_log_group.instance.name

  tags = {
    Name = "my-saas-company-worker-${count.index+1}"
  }
}

resource "aws_cloudwatch_log_stream" "dev" {
  name              = "my-saas-company-dev"
  log_group_name    = aws_cloudwatch_log_group.instance.name

  tags = {
    Name = "my-saas-company-dev"
  }
}

resource "aws_cloudwatch_log_metric_filter" "instance" {
  count             = 2
  name              = "my-saas-company-worker-${count.index+1}"
  pattern           = "[...]"
  log_group_name    = aws_cloudwatch_log_group.instance.name
  metric_transformation {
    name              = "my-saas-company-worker-${count.index+1}"
    namespace         = "my-saas-company"
    value             = "1"
    default_value     = "0"
  }

  tags = {
    Name = "my-saas-company-worker-${count.index+1}"
  }
}

resource "aws_cloudwatch_log_metric_filter" "dev" {
  name              = "my-saas-company-dev"
  pattern           = "[...]"
  log_group_name    = aws_cloudwatch_log_group.instance.name
  metric_transformation {
    name              = "my-saas-company-dev"
    namespace         = "my-saas-company"
    value             = "1"
    default_value     = "0"
  }

  tags = {
    Name = "my-saas-company-dev"
  }
}
```

This code will create a CloudWatch log group and streams for our EC2 instances. It will also create metric filters that can be used to monitor specific log events. You can specify a custom pattern for the metric filters to filter out the logs that you are interested in.

## Setup Lambdas
Lambdas are a useful tool for running code without the need to provision or manage servers. We will create two Lambdas for our SaaS company. Add the following code to a new file named lambda.tf.

```tf
resource "aws_lambda_function" "function" {
  count             = 2
  filename          = "lambda_function.zip"
  function_name     = "my-saas-company-lambda-${count.index+1}"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "nodejs8.10"

   tags = {
      Name = "my-saas-company-lambda-${count.index+1}"
   }
}

resource "aws_iam_role" "lambda" {
   name = "my-saas-company-lambda"

   assume_role_policy = <<EOF
      {
         "Version": "2012-10-17",
         "Statement": [
            {
               "Action": "sts:AssumeRole",
               "Principal": {
                  "Service": "lambda.amazonaws.com"
               },
               "Effect": "Allow",
               "Sid": ""
            }
         ]
      }
   EOF
}

resource "aws_iam_policy" "lambda" {
   name = "my-saas-company-lambda"

   policy = <<EOF
   {
      "Version": "2012-10-17",
      "Statement": [
         {
            "Action": [
               "logs:CreateLogGroup",
               "logs:CreateLogStream",
               "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:::*",
            "Effect": "Allow"
         }
      ]
   }
   EOF
}

resource "aws_iam_policy_attachment" "lambda" {
   name = "my-saas-company-lambda"
   roles = [aws_iam_role.lambda.name]
   policy_arn = aws_iam_policy.lambda.arn
}

```

This code will create two Lambda functions and the required IAM role and policy for them. The Lambda functions can be triggered by different events such as an S3 bucket event, an API Gateway event, or a custom event.

## Setup RDS
Next, we will create three RDS (Relational Database Service) instances for our SaaS company. Add the following code to a new file named `rds.tf`.

```tf
resource "aws_db_instance" "database" {
   count = 3
   allocated_storage = 10
   storage_type = "gp2"
   engine = "mysql"
   engine_version = "5.7"
   instance_class = "db.t2.micro"
   name = "my-saas-company-database-${count.index+1}"
   username = "my-saas-company"
   password = "my-saas-company-password"
   vpc_security_group_ids = [aws_security_group.rds.id]

   tags = {
      Name = "my-saas-company-database-${count.index+1}"
   }
}

resource "aws_security_group" "rds" {
   name = "my-saas-company-rds"
   description = "Security group for RDS instances"
   vpc_id = aws_vpc.main.id

   ingress {
      from_port = 3306
      to_port = 3306
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
   }

   egress {
      from_port = 0
      to_port = 0
      protocol = "-1"
      cidr_blocks = ["0.0.0.0/0"]
   }

   tags = {
      Name = "my-saas-company-rds"
   }
}
```

This code will create three RDS instances for the MySQL database engine. The RDS instances are secured using a security group that only allows traffic on the default MySQL port (3306) from any IP address. You can customize this security group to only allow traffic from specific IP addresses or VPCs.

## Setup API Gateways
API Gateways are useful for creating and managing APIs for web and mobile applications. We will create three API Gateways for our SaaS company, one for production, one for staging, and one for development. Add the following code to a new file named `api-gateway.tf`.

```tf
resource "aws_api_gateway_rest_api" "prod" {
   name = "my-saas-company-prod"
   description = "API Gateway for production environment"

   tags = {
       Name = "my-saas-company-prod"
   }
}

resource "aws_api_gateway_rest_api" "staging" {
   name = "my--saas-company-staging"
   description = "API Gateway for staging environment"

   tags = {
      Name = "my-saas-company-staging"
   }
}

resource "aws_api_gateway_rest_api" "dev" {
   name = "my-saas-company-dev"
   description = "API Gateway for development environment"

   tags = {
      Name = "my-saas-company-dev"
   }
}
```

This code will create three API Gateway REST APIs, one for each environment. You can then create and manage different API resources, methods, and integrations for these APIs-.

## Conclusion
In this tutorial, we have seen how to use Terraform to setup the infrastructure for a SaaS company from scratch. We have created various AWS resources including S3 buckets, ACM Certificate, Route53 entries, VPC, EC2 instances, CloudWatch logging, Lambdas, RDS, and API Gateways. You can further customize this infrastructure to suit your specific needs and requirements.