Serverless Architecture

In the rapidly evolving realm of cloud computing, an anonymous client embarked on a strategic shift towards serverless architecture on AWS, envisioning a more streamlined and cost-effective operational model. However, the journey was met with an unexpected challenge - a notable surge in their monthly AWS bill. This prompted a thorough reassessment of their serverless deployment strategy, emphasizing the need for both operational optimization and cost-effectiveness.

Despite the inherent promises of serverless computing in terms of maintenance ease and cost reduction, the transition paradoxically led to escalating operational expenses for the client. This unforeseen financial upturn necessitated a critical evaluation of their deployment strategy, with a keen focus on identifying and mitigating the underlying factors contributing to the rise in costs.

Pump’s Proposed Solution

To address this challenge comprehensively, Pump proposed a multifaceted strategy:

  • Infrastructure Refinement for Cost-Optimization: Pump meticulously refined the client's serverless infrastructure. This involved strategic utilization of AWS Step Functions, fine-tuning memory requirements, and enhancing overall infrastructure efficiency.

  • Rightsizing and Autoscaling: Recognizing the impact of overprovisioned resources on costs, Pump prioritized rightsizing the existing infrastructure to match actual usage needs. Additionally, the implementation of autoscaling policies ensured resource allocation dynamically adapted to traffic patterns, curbing unnecessary expenditure.

  • Strategic Investment in Savings Plans and Reserved Instances: Pump provided expert guidance on selecting AWS Savings Plans and AWS EC2 Reserved Instances (RI), aligning purchasing decisions with the client's usage patterns and financial objectives.

Here is a sample Terraform configuration that reflects the described infrastructure setup, including serverless architecture optimization, autoscaling, and a multi-account structure with tagging and cost management practice

# Specify the provider
provider "aws" {
  region = var.aws_region
}

# Create a VPC
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "my-vpc"
    Environment = var.environment
  }
}

# Public Subnet
resource "aws_subnet" "public" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone = "us-east-1a"

  tags = {
    Name = "public-subnet"
    Environment = var.environment
  }
}

# Private Subnet
resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "private-subnet"
    Environment = var.environment
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw"
    Environment = var.environment
  }
}

# Public Route Table
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "public-rt"
    Environment = var.environment
  }
}

# Associate Public Route Table with Public Subnet
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# Elastic IP for NAT Gateway
resource "aws_eip" "nat" {
  vpc = true

  tags = {
    Name = "nat-eip"
    Environment = var.environment
  }
}

# NAT Gateway
resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public.id

  tags = {
    Name = "main-nat"
    Environment = var.environment
  }
}

# Private Route Table
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }

  tags = {
    Name = "private-rt"
    Environment = var.environment
  }
}

# Associate Private Route Table with Private Subnet
resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}

# DynamoDB Table
resource "aws_dynamodb_table" "main" {
  name           = "my-table"
  hash_key       = "ID"
  range_key      = "Timestamp"
  billing_mode   = "PAY_PER_REQUEST"

  attribute {
    name = "ID"
    type = "S"
  }

  attribute {
    name = "Timestamp"
    type = "N"
  }

  global_secondary_index {
    name            = "GSI1"
    hash_key        = "GSI1PK"
    range_key       = "GSI1SK"
    projection_type = "ALL"

    attribute {
      name = "GSI1PK"
      type = "S"
    }

    attribute {
      name = "GSI1SK"
      type = "S"
    }
  }

  tags = {
    Name = "my-dynamodb-table"
    Environment = var.environment
  }
}

# Lambda Function
resource "aws_lambda_function" "example" {
  function_name = "example_lambda"
  role          = aws_iam_role.lambda_exec.arn
  handler       = "index.handler"
  runtime       = "nodejs14.x"
  memory_size   = 128
  timeout       = 10

  environment {
    variables = {
      TABLE_NAME = aws_dynamodb_table.main.name
    }
  }

  tags = {
    Name = "example_lambda"
    Environment = var.environment
  }
}

# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "main" {
  name = "/aws/lambda/example_lambda"

  retention_in_days = 14

  tags = {
    Name = "lambda-log-group"
    Environment = var.environment
  }
}

# IAM Role for Lambda Execution
resource "aws_iam_role" "lambda_exec" {
  name = "lambda_exec_role"

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

  tags = {
    Name = "lambda_exec_role"
    Environment = var.environment
  }
}

# Attach IAM Policy to Role
resource "aws_iam_role_policy_attachment" "lambda_policy" {
  role       = aws_iam_role.lambda_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# Autoscaling Configuration for Lambda (through provisioned concurrency)
resource "aws_lambda_provisioned_concurrency_config" "example" {
  function_name                       = aws_lambda_function.example.function_name
  provisioned_concurrent_executions   = 10

  qualifier = "$LATEST"
}

# Define variables for environment and region
variable "aws_region" {
  description = "The AWS region to deploy to"
  type        = string
  default     = "us-east-1"
}

variable "environment" {
  description = "The environment for the resources (e.g., dev, prod)"
  type        = string
  default     = "dev"
}

# Output the VPC ID
output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.main.id
}

# Output the Public Subnet ID
output "public_subnet_id" {
  description = "The ID of the Public Subnet"
  value       = aws_subnet.public.id
}

# Output the Private Subnet ID
output "private_subnet_id" {
  description = "The ID of the Private Subnet"
  value       = aws_subnet.private.id
}

# Output the DynamoDB Table Name
output "dynamodb_table_name" {
  description = "The name of the DynamoDB table"
  value       = aws_dynamodb_table.main.name
}

# Output the Lambda Function Name
output "lambda_function_name" {
  description = "The name of the Lambda function"
  value       = aws_lambda_function.example.function_name
}

Account Segregation

A pivotal aspect of Pump's strategy involved segregating the client's workloads into distinct dev and prod AWS accounts. This segregation yielded several key benefits:

  • Enhanced Security and Stability: By maintaining separate accounts, prod environments benefited from stricter security policies and monitoring, ensuring elevated reliability and performance.

  • Cost-Effective Resource Utilization: Dev accounts were optimized for cost savings, leveraging lower-cost resources and deactivating unnecessary services when idle, without impacting prod stability.

  • Agile Development and Testing: Isolating dev workloads in their dedicated account facilitated faster iterations, risk-free testing, and innovation, accelerating the development cycle without compromising prod environment integrity.

Outcomes for the Client

Pump's strategic intervention yielded remarkable results:

  • Efficient Resource Utilization: Through optimization of Amazonnb Lambda and Step Functions, and right-sizing of Amazon Lambda memory limits and Amazon ElastiCache clusters, Pump eradicated overprovisioning, aligning resource utilization with actual needs.

  • Cost Reduction via Autoscaling: Implementing autoscaling for Amazon ElastiCache clusters addressed inefficiencies linked with peak traffic provisioning, ensuring resources scaled dynamically in response to real-time demand.

  • Strategic Financial Planning: Adopting a staggered savings plan approach facilitated substantial cost savings, adhering to AWS best practices for maintaining high coverage in a financially prudent manner.

Multi-Account Structure

Pump implemented a meticulously crafted multi-account structure aligning with AWS's best practices. This choice bolstered operational excellence and security within the cloud environment. By segregating resources across multiple AWS accounts, Pump enhanced overall security posture, simplified billing and cost management, and enabled granular control over access and resources.

Recognizing the customer's need for environment tagging in accounts, Pump leveraged AWS Organizations' tagging features to establish account-level tags with metadata on environment information. This facilitated informed decision-making by aggregating cost and usage data across different environments. Pump also utilized SCPs and Tag Policies to enforce tagging policies, ensuring compliance and consistency.

Conclusion

Pump was able to provide a comprehensive breakdown of the total cost of ownership aggregated across multiple accounts, facilitating precise cost allocation. This granularity informed reservation recommendations, considering the dynamic nature of environments like dev environments where long-term commitments are generally discouraged.

In conclusion, Pump's strategic interventions optimized costs, enhanced operational efficiency, and fortified security, empowering the client to navigate the complexities of serverless architecture with confidence and agility.

Last updated