Setting Up AWS Lambda for Serverless Functions with Terraform

Serverless functions break down due to cold starts, manual changes in the console, and drifting environments. We describe infrastructure as code and configure AWS Lambda end to end: predictable invocations, reproducible environments, transparent access permissions. We reduce the time required to release changes and remove manual operations from the process.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1359
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

AWS Lambda configuration: where serverless functions break

A serverless function rarely breaks because of code. More often, it responds with a delay under peak load, and after a few minutes returns an error — and the cause lies in how it is described in the infrastructure, not in the handler.

Function timeout is usually set "with a margin" without checking against actual request processing time.

When execution exceeds the limit, the platform interrupts it, and the message is returned to the source and invoked again — without idempotency, this produces duplicate records, double charges, and emails sent twice.

This is closed by an explicit idempotency key, a cap on retries, and a dead-letter queue where everything that could not be processed goes.

Configuration errors accumulate unnoticed: one role with permissions for the entire account instead of a narrow policy for a specific function, secrets in environment variables, memory size not tied to the load profile, no concurrency limit set.

The latter hits external systems hardest — the database receives hundreds of connections from parallel copies of the function and starts failing repeatedly.

The fix is not editing in the console but describing infrastructure: AWS Lambda configuration in Terraform locks permissions, limits, retries, and tracing into a single state, and any change passes validation before rollout. Below, we break down how this is closed and what remains with you after the work.

Measurable effect of serverless configuration

After the environment is reworked, the serverless layer stops being a source of surprises. Function response time stays within a narrow corridor both under steady load and during spikes: warmed instances pick up requests, while cold starts remain only on rare new routes.

The on-call engineer no longer needs to manually warm up the service before a peak — predictable load is ensured by settings, not by team heroics.

This works through boundaries defined declaratively.

Memory and execution time limits are selected for the profile of a specific function, connections to the database and external services are reused instead of recreated on every invocation, and heavy initialization is moved outside the handler.

The environment is fully described by configuration, so the test environment does not diverge from production and the "someone tweaked something in the console" scenario is excluded. It is deployed on AWS Lambda without manual steps.

What becomes visible in metrics:

  • resource consumption stops jumping — peak values are aligned with the function profile;
  • the share of cold starts drops to isolated cases on warmed routes;
  • the class of failures related to exhausted concurrent executions and exceeded external call timeouts disappears.

Failure behavior is checked separately: retries are limited, and unprocessed events go to the dead-letter queue instead of blocking the flow. The failure of one function stops cascading into adjacent services — this is the predictability for which the serverless model is chosen.

Lambda modes: comparing launch options

The same function's launch can be described in different ways, and this determines how predictably it behaves in different environments. Below are the options that are actually chosen between when configuring AWS Lambda, along with their limitations.

The key difference is where the description of the execution environment lives: in the console, in the repository, or partly in both. As long as the configuration exists only in the web interface, it cannot be validated before applying and cannot be rolled back.

Option When it fits Limitations
Manual configuration in the console one-off function, hypothesis testing, handler debugging no change history, environments diverge, repeat launch is not reproducible
Description in the repository (Terraform) multiple environments, teamwork, frequent releases state control is needed, roles and access policies are described explicitly
Mixed approach inherited functions, phased transition manual edits are overwritten on the next configuration apply
Templates and configuration generators similar functions, uniform default requirements limit non-standard scenarios, some parameters are still set outside the template

Limitations matter more than capabilities here: they determine what maintenance will cost.

We migrate existing functions to infrastructure as code gradually — first importing the current state, then locking it into the description so that manual edits are not lost on the next apply.

The execution environment becomes identical across all environments, and a change is validated before rollout.

Implementation order: from audit to handover

We fix the order of work in advance: each stage ends with a verifiable artifact, otherwise the infrastructure sprawls into manual console edits that cannot be reproduced on the next deployment. Below are the six steps that make up implementation.

  1. Environment audit. We collect an inventory: which functions are already running, who created them manually, which roles and policies are attached to them, where Terraform state is stored, and whether there are discrepancies between the description and reality.
  2. Interaction diagram. We capture the connections of functions to queues, object storage, API gateway, and external services, including timeouts and retries — this is the basis for future modules.
  3. Configuration stages. We move functions into code: Terraform modules, roles with the minimum required permissions, environment variables, memory, time limit, and build versioning strategy.
  4. Load validation. We run the load profile on real scenarios: cold starts, concurrency, behavior during external service failures, dead-letter queue triggering.
  5. Observability. We enable logs, metrics, and tracing, and configure alerts by error rate and duration — before handover, not after the first failure in production.
  6. Environment handover. We hand over documentation, access, change procedures, and incident analysis; the client's team edits the infrastructure on its own, without us.

Composition of the transferred configuration and documentation

AWS Lambda configuration ends not with a working environment but with a package of artifacts your team can use to change the infrastructure without us: add a function, tighten permissions, deploy a copy in another account. Below is what remains with you after acceptance.

  • Resource descriptions. Declarative descriptions of functions, roles, queues, triggers, and schedules. The environment is brought up from scratch with a single command — this is verified at acceptance in a clean account, not just stated.
  • Access policies. Execution roles with the minimum necessary permission set, separation of build and function runtime permissions, description of external trust relationships and the zones each role covers.
  • Relationship diagrams. A diagram of event flows: source, handler, queue, storage, external services. It marks timeouts, retries, and failure points — showing where the chain breaks.
  • Update procedure. The order of change rollout, the set of checks before release, rules for working with environment variables and secrets, and the conditions under which a release is considered successful.
  • Rollback procedure. How to return to the previous version, what happens to events in the queue and already accumulated data, and how quickly the environment returns to a working state.
  • Environment documentation. Environment variables, limits, schedules, owners, on-call instructions, and escalation procedure — so a nighttime failure does not wait for the code author.

Case breakdown: cold start and retries

When configuring AWS Lambda for an order processing service, we encountered a familiar picture: during quiet hours the functions responded instantly, but at peaks customers received timeouts and retried sending.

The external system also retried the event if it did not wait for confirmation — a single order was processed twice: two charges, two emails, discrepancies in reporting.

We described the infrastructure as Terraform code: function configuration, permissions, environment variables, and triggers are reproduced in a new environment without manual steps.

Heavy dependencies were moved to layers, and database connection initialization was moved to the launch phase so it is not repeated on every invocation.

For hot routes we enabled reserved concurrency; for the rest we limited concurrency at the source: a spike no longer eats the limits of neighboring services.

Duplication was closed with idempotency: retrying an event with the same key does not start the work again but returns the result of the previous processing.

Errors after several attempts with increasing delay go to the dead-letter queue — instead of silent data loss, there is analysis and re-invocation.

Result in numbers

The share of cold starts on critical routes fell from 15–20% to single-digit percent, and response time stopped depending on when the request arrived. Reprocessing of a single event dropped to zero — the idempotent rejection counter shows only real attempts.

Under load, the service sustains traffic growth of several times without manual intervention: throttles dampen the spike, and the queue picks up lagging events.

Rolling back to the previous configuration is one command, because the infrastructure is described as code rather than assembled by hand.

Configure functions manually or describe infrastructure as code?

Manual configuration remains workable while there are one or two functions and they change rarely: adjust memory, timeout, environment variables in the console, attach a trigger — and forget it.

As long as the service fits in one engineer's head, this scheme does not create problems and requires less preparation than infrastructure as code.

The threshold comes with change frequency. When the configuration is edited several times a week and there are already three environments, the manual process stops being reproducible: the test environment diverges from production, and no one can list exactly how.

The main cost is divergence. A setting made in the web interface is not recorded anywhere: when recreating an environment or moving to another region, it is restored from memory and logs, with inevitable gaps.

Terraform keeps the same parameters in the description and state file, and shows drift as a difference during planning — before apply, not after a failure.

So the question is not "manual or code" but "at what scale." Signs that manual mode has stopped being manageable: more than two or three functions, several environments, several people with console access, changes more often than once a week.

Beyond that, the same set of resources is deployed from the description in a new account with a single command — this is environment reproducibility.

The reverse transition is also normal. A small service on one function with rare edits is cheaper to keep in the console.

The hybrid — when infrastructure is described as code and changes go only through it: then a manual edit will not be "lost" but will surface on the next apply.

Technical concerns before starting work

Engineering concerns before starting are usually the same: how to update functions on live traffic, what to do with those already deployed manually, what permissions to grant, and how to see a failure before the user does. Below are short answers so the decision is made on facts.

How to update functions without downtime?

We publish an immutable version and route part of the invocations to it through a version alias, gradually increasing the share. The old version remains available, so rollback is returning the weight back, without rebuilding and redeploying. Configuration is stored as code, so the state always matches what is described.

We already have functions deployed manually. Will they have to be recreated?

No. First we describe the current state as it is and import the existing functions into the state file without deleting or recreating them. Then the parameters are brought to a common form — environment variables, time and memory limits, trigger bindings. After each step, behavior is compared with the previous one.

What permissions should be granted to a function?

A separate role for each function and access only to the resources it works with: a specific table, a specific storage, a specific queue. No broad permissions or shared roles for the entire account.

Secrets and connection strings live outside the code, in a secure vault, and are injected during deployment.

How to monitor errors and load growth?

We collect error rate, execution time, and number of concurrent invocations, and store logs for a limited period. We set alerts on deviation from the norm, not on every single failure — otherwise the on-call engineer drowns in noise. We raise concurrency limits in advance, before peak season, not after unavailability occurs.

Let's discuss the task: what is needed for an estimate and plan

A quick estimate comes when the input contains facts about the current state, not a general "we need to move AWS Lambda functions to managed infrastructure." Below is what to send: half the items are usually covered by one call with the engineer who currently runs deployment.

  • Diagram of today's deployment: where Terraform state is stored, who applies changes, whether manual console edits remain.
  • Inventory of functions and triggers: what invokes each one, which external services are involved, which execution time limits are critical.
  • Access model: separate accounts for environments, roles, who grants permissions and how, whether there is separation between read and change.
  • Composition of environments: how many there are, how variables differ, whether separate build artifacts are needed for each.
  • Observability: where logs are written, whether tracing and alerts exist, what the team considers a failure.
  • Constraints: change windows, security requirements and procedures, which services must not be touched.
  • Incident history: what broke during rollouts and how rollbacks were done — this determines the scope of protective checks.

In response, you receive a stage-by-stage work plan with checkpoints, responsibility boundaries — what we cover and what remains on your side — and timelines for each stage. Describe the task and attach what you already have: we will come back with an estimate and plan.