The two previous lessons have brought MercadoFresco's infrastructure under control: declarative templates, reusable constructs, tests and a pipeline that deploys the infrastructure and updates itself. It is the result of eight modules of work. And now comes an uncomfortable question: was all of this necessary?

There is an alternative nobody on the team evaluated at the time, because back in module 1 nobody knew it existed. Elastic Beanstalk deploys an application when you hand it a compressed file and a string with the language version; in exchange, it provisions and maintains EC2, auto scaling, a load balancer, security groups and metrics. This lesson explains what it does exactly, what it controls and what it does not, and ends with the honest decision for MercadoFresco — which is not the one you would expect after two lessons of infrastructure as code.

Cost warning. Beanstalk is free: you pay exactly for the resources it creates. A test environment with a single t3.small and a load balancer costs about 0.04 USD/hour, that is, around 1 USD a day; and if it is a web server environment with an ALB, the load balancer alone is another 0.025 USD/hour plus traffic. The cleanup section terminates the environment with one command. Fictitious data.

Contents

  1. What a platform as a service is
  2. What Beanstalk creates underneath
  3. Concepts: application, version, environment and platform
  4. Web server environments and worker environments
  5. Supported platforms and deploying the shop step by step
  6. Customisation with .ebextensions and .platform
  7. Deployment policies, saved configurations and IaC
  8. Scaling, enhanced health and the health dashboard
  9. Beanstalk with a database
  10. The honest decision for MercadoFresco
  11. Comparison with CDK, Fargate, App Runner and Amplify
  12. The exit route
  13. Cost and cleanup
  14. Common mistakes and tips
  15. Exercises
  16. Conclusion

What a platform as a service is

A platform as a service (PaaS) is a model in which the provider manages everything beneath your code: the operating system, the language runtime, the application server, load balancing, scaling and basic monitoring. You hand over the code and a few configuration decisions.

Model You manage AWS manages Example in this course
IaaS OS, runtime, application, scaling Hardware, network, virtualisation EC2 with the module 2 ASG
PaaS Application and configuration Everything else Elastic Beanstalk
CaaS Container image Nodes, orchestration Fargate (10-02)
FaaS Function Everything else Lambda (02-05)

Beanstalk sits at a specific point on that scale: more control than Lambda, far less work than EC2. And unlike a closed PaaS, it does not hide the infrastructure from you: the resources it creates are ordinary EC2, ASG, ALB and security groups, visible in the console and reachable from the CLI. You can SSH into the instances, look at the logs and modify the ASG. That transparency is its greatest virtue, and also the source of its problems when somebody edits by hand what Beanstalk believes it governs.

What Beanstalk creates underneath

When you create an environment, Beanstalk generates a CloudFormation template and deploys a stack. You have been using it without knowing since 09-01: if you list the stacks of an account with Beanstalk, you will see one called awseb-e-abc123xyz-stack.

flowchart TB
    A[Code: mercadofresco-tienda.zip] --> B[Beanstalk: application version]
    B --> C[CloudFormation template<br/>generated by Beanstalk]
    C --> D[Stack awseb-e-xxxx-stack]
    D --> E[Auto scaling group]
    D --> F[ALB load balancer]
    D --> G[Security groups]
    D --> H[CloudWatch alarms]
    D --> I[IAM instance profile]
    E --> J[EC2 instances with<br/>the Beanstalk agent]
    J --> K[Web server + your application]

This has three consequences worth being clear about from the start:

  • It is not magic, it is CloudFormation. The same states, the same timings and the same limits as 09-01. A deployment that stops halfway leaves the stack in UPDATE_ROLLBACK_COMPLETE.
  • The stack is Beanstalk's, not yours. Modifying it directly breaks the model: Beanstalk will overwrite it on the next operation. Every change goes through Beanstalk's configuration options.
  • There is an agent on every instance. It is the one that receives the order to deploy, downloads the version from S3, runs the hooks and reports health. When something fails oddly, its logs in /var/log/eb-engine.log are the first place to look.

Concepts: application, version, environment and platform

Beanstalk has five concepts, and confusion between the first three causes 80 % of early mistakes.

Concept What it is Important detail
Application A logical container. E.g.: tienda-mercadofresco Costs nothing, deploys nothing
Application version A specific artefact in S3 with a label. E.g.: v1.6.0 Immutable: it is the artefact of 08-02
Environment A version deployed onto resources. E.g.: mercadofresco-tienda-produccion This is where all the cost is
Saved configuration A snapshot of an environment's options Lets you create identical environments
Platform OS + runtime + server. E.g.: Python 3.12 on AL2023 It gets updated; that has to be managed

The relationship is one-to-many at each level: an application has N versions and M environments, and each environment has one specific version deployed. That fits perfectly with the discipline of 08-05: the artefact is built once and that same object is promoted through the environments. In Beanstalk it translates into eb deploy --version v1.6.0 against each environment, rebuilding nothing.

Platform updates deserve attention because they are the work Beanstalk does not eliminate, only reduces. Each platform branch has versions (Python 3.12 running on 64bit Amazon Linux 2023/4.2.1) and AWS publishes a new one every few weeks with security patches. There are two ways to apply them:

# Managed update: AWS applies it on its own within the given window
eb config          # and in the editor: ManagedActions: true, PreferredStartTime: Tue:03:00
# Or manually, with full control over the timing
aws elasticbeanstalk update-environment --environment-name mercadofresco-tienda-produccion \
  --platform-arn "arn:aws:elasticbeanstalk:eu-west-1::platform/Python 3.12 .../4.2.1"

Managed platform updates are one of the strongest reasons to choose Beanstalk: they apply operating system patches within a defined window, with an immutable deployment and automatic rollback if it fails. It is exactly the work that in module 2 meant rebuilding the AMI by hand.

Web server environments and worker environments

Beanstalk has two environment types, and the second one fits MercadoFresco's architecture almost literally.

A web server environment receives HTTP traffic through an ALB and has a public DNS name of the form mercadofresco-tienda-produccion.eu-west-1.elasticbeanstalk.com.

A worker environment has no load balancer and no inbound traffic. Instead, Beanstalk creates an SQS queue and deploys on each instance a daemon, sqsd, that reads messages from the queue and hands them to your application as HTTP POST requests to localhost on a configurable path.

flowchart LR
    A[Shop: web environment] -->|publishes| Q[cola-mercadofresco-pedidos]
    Q --> S[sqsd on every instance<br/>of the worker environment]
    S -->|POST localhost/tareas| W[Your processing code]
    W -->|200 OK| S
    S -->|deletes the message| Q
    W -->|error or timeout| D[DLQ: mercadofresco-pedidos-fallidos]

The important detail is how the result is translated: if your code answers 200, sqsd deletes the message from the queue; if it answers an error or times out, the message goes back to the queue and, after MaxRetries attempts, on to the dead letter queue. It is exactly the pattern of 07-01 and 07-05 — visibility, retries and DLQ — but with the consumption loop written by AWS.

That brings one advantage and one trap. The advantage: your worker code becomes an ordinary HTTP handler, with no SQS library, no receive_message, no visibility management. The trap: idempotency is still your responsibility, because a message can be delivered more than once exactly as in 07-05. The mercadofresco-idempotencia table is still needed.

Beanstalk also lets you use an existing queue rather than creating one: in the environment options, aws:elasticbeanstalk:sqsd/WorkerQueueURL points at cola-mercadofresco-pedidos. With that, module 7's asg-mercadofresco-trabajadores could be replaced by a worker environment without touching either the queue or the producer. It is the use case where Beanstalk is still competitive at MercadoFresco today.

A useful aside: sqsd also supports periodic tasks through a cron.yaml file in the package, which makes it a replacement for a scheduled EventBridge rule for simple recurring jobs.

Supported platforms and deploying the shop step by step

Platform Server Beanstalk sets up What it expects in your package
Python nginx + Gunicorn application.py with a WSGI object and requirements.txt
Node.js nginx + your process package.json with start, or app.js
Java SE / Tomcat The JAR itself / Tomcat application.jar / a .war
.NET Core / Windows Kestrel behind nginx / IIS The project's publish output
PHP / Ruby / Go nginx + PHP-FPM / Puma / binary Conventions specific to each
Docker The container engine Dockerfile or Dockerrun.aws.json

The last two rows deserve a clarification: Beanstalk can deploy containers, both a single image and several with ECS underneath. It is a perfectly valid route, but if the architecture is going to be container-based, the sensible thing is to use ECS or Fargate directly — module 10 — rather than a layer that wraps them. MercadoFresco's shop is Python, so it fits the first row.

The Python platform's requirement is simple: a WSGI object called application in application.py, or the path given in the configuration, plus a requirements.txt.

pip install awsebcli --upgrade

cd mercadofresco-tienda
eb init tienda-mercadofresco \
  --platform "Python 3.12" --region eu-west-1 --profile mercadofresco-dev

eb create mercadofresco-tienda-pruebas \
  --instance-types t3.small \
  --elb-type application \
  --min-instances 2 --max-instances 4 \
  --envvars ENTORNO=pruebas,NIVEL_LOG=info \
  --tags Proyecto=mercadofresco,Entorno=pruebas,Componente=tienda,Propietario=luis,CentroCoste=plataforma

That single eb create command takes between five and seven minutes and creates the ALB, the target group, the ASG, the launch template, two security groups, the IAM instance profile, two CloudWatch alarms and the log group. It is the content of aplicacion-mercadofresco.yaml from 09-01, without writing a single line. That sentence is the whole argument in favour of Beanstalk.

The daily working cycle is just as short:

eb status                    # state, health, deployed version and CNAME
eb deploy                    # packages the directory, uploads to S3 and deploys
eb deploy --version v1.6.0   # promotes an ALREADY built version: the right way (08-05)
eb logs --all                # downloads the logs from every instance
eb ssh                       # opens a shell on an instance (needs a key and SG with port 22 open)
eb open                      # opens the URL in the browser
eb health --refresh          # health dashboard in the terminal, instance by instance
eb events -f                 # live environment events: the first thing to check when it fails
eb terminate mercadofresco-tienda-pruebas

Two warnings about eb deploy that catch people out on day one. The first: it packages the contents of the directory, not of the last commit — unless you use eb deploy --staged or configure artefacts — so an unsaved file or a local .env can end up deployed. The second, a consequence of the first: eb deploy is exactly the "deploy from the laptop" that module 8 got rid of. In a serious project, eb deploy is run by the pipeline, not by a person.

Customisation with .ebextensions and .platform

Beanstalk is opinionated, but not closed. There are two customisation mechanisms and they serve different purposes.

.ebextensions/*.config are YAML files inside the package that configure the environment and its resources: Beanstalk options, system packages, files, commands and additional CloudFormation resources.

# .ebextensions/01-opciones.config
option_settings:
  aws:elasticbeanstalk:application:environment:
    ENTORNO: produccion
    REGION_AWS: eu-west-1
  aws:autoscaling:asg:
    MinSize: 2
    MaxSize: 4
  aws:elasticbeanstalk:environment:
    LoadBalancerType: application
  aws:elasticbeanstalk:healthreporting:system:
    SystemType: enhanced          # enhanced health: essential, see below
  aws:elasticbeanstalk:command:
    DeploymentPolicy: Immutable   # deployment policy

packages:
  yum:
    postgresql15: []              # psql client for diagnostics

files:
  "/etc/nginx/conf.d/limites.conf":
    mode: "000644"
    owner: root
    content: |
      client_max_body_size 20M;   # product photos are heavy

container_commands:
  01_migraciones:
    command: "python manage.py migrate --noinput"
    leader_only: true             # ONLY on one instance: avoids concurrent migrations
# .ebextensions/02-recursos.config — your own resources in the Beanstalk stack
Resources:
  ColaCorreo:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub 'cola-mercadofresco-correo-${AWSEBEnvironmentName}'

.platform/ is the modern mechanism, available on Amazon Linux 2 platforms and later, and it replaces much of the above with ordinary files instead of YAML:

.platform/
├── nginx/conf.d/limites.conf        # copied as-is into the nginx configuration
├── hooks/prebuild/01-dependencias.sh   # before building
├── hooks/predeploy/01-comprobar.sh     # before activating the new version
└── hooks/postdeploy/01-avisar.sh       # afterwards, with the application already running
Mechanism Format When it runs Use it for
option_settings YAML When configuring the environment Beanstalk and ASG options
packages, files, commands YAML Before deploying the application System packages, files
container_commands YAML With the application deployed, before activating it Migrations, collecting static files
.platform/hooks/* Scripts In the prebuild, predeploy and postdeploy phases Everything else, with more control

Options are organised into namespaces, and knowing the main ones saves a lot of time searching the documentation:

Namespace What it controls
aws:elasticbeanstalk:application:environment Your application's environment variables
aws:elasticbeanstalk:command Deployment policy, batch size, timeout
aws:elasticbeanstalk:environment Environment type, load balancer type, service role
aws:elasticbeanstalk:healthreporting:system Basic or enhanced health
aws:elasticbeanstalk:managedactions Managed platform updates and their window
aws:elasticbeanstalk:sqsd The worker environment daemon
aws:autoscaling:asg / aws:autoscaling:trigger ASG size and scaling triggers
aws:ec2:vpc VPC, subnets and whether the load balancer is public
aws:elbv2:listener:443 HTTPS listener, certificate and security policy

An example that brings several together and which is, in practice, the minimum serious configuration for a MercadoFresco production environment:

# .ebextensions/05-produccion.config
option_settings:
  aws:ec2:vpc:
    VPCId: vpc-0a1b2c3d4e5f6a7b8
    Subnets: subnet-app-a,subnet-app-b            # instances in the application subnets
    ELBSubnets: subnet-publica-a,subnet-publica-b # load balancer in the public ones
    ELBScheme: public
  aws:elbv2:listener:443:
    Protocol: HTTPS
    SSLCertificateArns: arn:aws:acm:eu-west-1:111122223333:certificate/abc-123
    SSLPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
  aws:elasticbeanstalk:managedactions:
    ManagedActionsEnabled: true
    PreferredStartTime: "Tue:03:00"               # Tuesday small hours, never Friday
  aws:elasticbeanstalk:managedactions:platformupdate:
    UpdateLevel: minor                            # patches and minor versions
    InstanceRefreshEnabled: true

Notice PreferredStartTime: the platform update window is a business decision, just like the deployment window of 08-05. A Tuesday in the small hours is acceptable; a Friday afternoon, with five times as many orders at stake, is not.

The limits of this model are real and worth knowing before you marry Beanstalk. leader_only applies only during deployments, not when the ASG launches a new instance, so a migration in container_commands does not run when scaling out — which is fine — but neither can you rely on it for initialisation. Hooks run on every instance, so everything they do must be idempotent and fast: a slow hook multiplies deployment time by the number of instances. And there are things you simply cannot change: the structure of the stack, the one-process-per-instance model or the fact that the environment is the unit of everything.

Deployment policies

This is where Beanstalk can be compared head to head with CodeDeploy (08-03), and the comparison is instructive.

Policy How it works Time Extra cost Risk Capacity during
All at once Updates every instance simultaneously The fastest None High: service outage 0 % during the change
Rolling In batches; each batch leaves the ALB, updates and returns Medium None Medium Reduced
Rolling with additional batch Adds instances before starting Medium-high One extra instance Low 100 %
Immutable Creates a new ASG with new instances and joins them to the ALB High Double during the change Very low 100 %
Blue/green (CNAME swap) A whole new environment and a DNS switch The highest Double environment Very low 100 %

Two notes on the last two, which are the only ones acceptable in production:

Immutable is the closest equivalent to the blue/green of 08-03 within a single environment: brand new instances from scratch, with no configuration leftovers, and if the health checks fail the new ones are destroyed and nothing has happened. Its advantage over rolling is that it catches problems that only appear on a freshly booted instance — a dependency that no longer downloads, a permission that was missing — which a rolling deployment masks.

Blue/green by CNAME swap is the 08-03 model taken to the extreme: a complete environment is created with the new version, tested with its own URL, and then eb swap exchanges the CNAMEs of the two environments. It is the only option that allows a rollback in seconds, because the blue environment stays alive.

eb create mercadofresco-tienda-verde --cname mercadofresco-tienda-verde
eb deploy mercadofresco-tienda-verde --version v1.6.0
./pruebas/humo.sh https://mercadofresco-tienda-verde.eu-west-1.elasticbeanstalk.com
eb swap mercadofresco-tienda-produccion --destination_name mercadofresco-tienda-verde

And now comes the honest comparison with 08-03, which has two parts.

What Beanstalk does just as well: the policies cover the full spectrum of trade-offs between speed, cost and risk, and the immutable one offers real guarantees with nothing to configure.

What it does not do: there is no canary — you cannot send 10 % of traffic to the new version and watch — there is no automatic rollback based on business metrics, there are no lifecycle hooks equivalent to BeforeAllowTraffic with a validation Lambda, and the CNAME swap depends on the DNS TTL: clients with the entry cached keep going to the old environment for minutes. The quality gate of 08-04, with its fifteen minutes of metrics and its ninety-second rollback, has no equivalent here. That difference is what stops a shop with 900 orders an hour on Fridays from settling for Beanstalk.

flowchart TB
    subgraph R[Rolling in batches]
      R1[Batch 1 leaves the ALB] --> R2[It is updated] --> R3[It returns to the ALB]
      R3 --> R4[Batch 2: same cycle]
    end
    subgraph I[Immutable]
      I1[New temporary ASG] --> I2[Clean instances<br/>with the new version]
      I2 --> I3{Checks OK?}
      I3 -->|Yes| I4[They join the ALB and<br/>the old ones are retired]
      I3 -->|No| I5[The temporary ASG is destroyed<br/>nothing has happened]
    end

Saved configurations: keeping pre-production from drifting away from production

The problem that opened this module — pre-production not being the same as production — also has an answer inside Beanstalk, although a more limited one than 09-02's. A saved configuration is a snapshot of all of an environment's options, stored in S3 and applicable to another:

# Save the production configuration under a name
eb config save mercadofresco-tienda-produccion --cfg base-produccion

# It lands in .elasticbeanstalk/saved_configs/base-produccion.cfg.yml: it IS versioned in Git
git add .elasticbeanstalk/saved_configs/base-produccion.cfg.yml

# Create pre-production from exactly the same configuration
eb create mercadofresco-tienda-preproduccion --cfg base-produccion \
  --envvars ENTORNO=preproduccion

The resulting file is readable and comparable with diff, which is precisely what was needed:

EnvironmentConfigurationMetadata:
  Description: Common baseline for the shop environments
OptionSettings:
  aws:autoscaling:asg:
    MinSize: '2'
    MaxSize: '4'
  aws:elasticbeanstalk:command:
    DeploymentPolicy: Immutable
  aws:elasticbeanstalk:healthreporting:system:
    SystemType: enhanced
Platform:
  PlatformArn: arn:aws:elasticbeanstalk:eu-west-1::platform/Python 3.12 .../4.2.1

The difference from 09-02's config/entornos.ts is one of degree: here the configuration is saved, versioned and compared, but it is not generated from a single source, so nothing stops someone changing an option in one environment and not in the other. It is a snapshot, not a contract.

Defining a Beanstalk environment from CloudFormation or CDK

Beanstalk and infrastructure as code are not mutually exclusive: there are native resource types, so an environment can live inside the unified model of 09-01 and 09-02.

Resources:
  AppTienda:
    Type: AWS::ElasticBeanstalk::Application
    Properties: { ApplicationName: tienda-mercadofresco }
  EntornoPruebas:
    Type: AWS::ElasticBeanstalk::Environment
    Properties:
      ApplicationName: !Ref AppTienda
      EnvironmentName: mercadofresco-tienda-pruebas
      SolutionStackName: '64bit Amazon Linux 2023 v4.2.1 running Python 3.12'
      OptionSettings:
        - { Namespace: 'aws:autoscaling:asg', OptionName: MinSize, Value: '2' }
        - { Namespace: 'aws:elasticbeanstalk:command',
            OptionName: DeploymentPolicy, Value: Immutable }

This partly answers the objection that "the configuration lives in the application repository": the structural options move to the template or to the CDK, and .ebextensions is reserved for what travels with the code. It is the right way to use Beanstalk in a team that already practises infrastructure as code, and it is worth knowing before discarding it for that reason.

Scaling, enhanced health and the health dashboard

Scaling is the module 2 kind, because it is an ASG: you configure a minimum, a maximum and a trigger based on a metric.

eb scale 4        # sets the desired number of instances
# .ebextensions/03-escalado.config — scaling on requests, not on CPU
option_settings:
  aws:autoscaling:trigger:
    MeasureName: RequestCount
    Statistic: Sum
    Unit: Count
    Period: 1
    BreachDuration: 2
    UpperThreshold: 6000        # aggregate requests per minute
    UpperBreachScaleIncrement: 1
    LowerThreshold: 2000
    LowerBreachScaleIncrement: -1

The piece that is Beanstalk's own and does add real value is enhanced health reporting. An agent on each instance combines system, web server and load balancer metrics and produces a per-instance status and an overall environment status, with concrete causes.

Colour Meaning Typical example
Green (Ok) All normal
Grey (Info/Pending) Operation in progress Deployment under way
Yellow (Warning) Something is wrong but traffic is served 5 % of responses are 5xx
Orange (Degraded) Clear impact Several instances failing the check
Red (Severe) Serious No healthy instances

eb health --refresh shows that dashboard in the terminal with the causes — "the application process is not responding on port 8080" — which is considerably more useful than a CPU graph. Enhanced health also publishes its own metrics to CloudWatch (ApplicationRequests5xx, ApplicationLatencyP99, InstancesSevere), on which you can build the alarms of 05-01 and wire them to alertas-mercadofresco.

One warning that causes grief: enhanced health is not switched on in every creation path, and without it Beanstalk falls back to the basic ALB check. Always enable it; it is free apart from the CloudWatch metrics.

Beanstalk with a database

Beanstalk lets you create an RDS instance inside the environment, and it is the best known trap in the service.

If you do, the database becomes part of the environment's stack. That means eb terminate deletes it, that recreating the environment recreates it empty, and that the database inherits the application's lifecycle, which is precisely the opposite of what should happen: the application changes four times a week and the data must survive everything. It is the same lifecycle partitioning principle as 09-01, and here it is violated flagrantly.

It is convenient for a demo or for a development environment destroyed every night. For anything else, the database lives outside and the environment connects to it:

# .ebextensions/04-basedatos.config
option_settings:
  aws:elasticbeanstalk:application:environment:
    DB_HOST: aurora-mercadofresco-pedidos.cluster-abc123.eu-west-1.rds.amazonaws.com
    DB_NOMBRE: pedidos
    DB_SECRETO: mercadofresco/produccion/rds/mfadmin

The password does not go in an environment variable: it goes in Secrets Manager (04-03) and the application resolves it at start-up using the instance role, exactly as in module 4. And connectivity is solved with security groups as in 03-02: the SG that Beanstalk creates for the instances is authorised as a source in sg-mercadofresco-basedatos.

# Find out the security group of the environment's instances
aws elasticbeanstalk describe-configuration-settings \
  --application-name tienda-mercadofresco --environment-name mercadofresco-tienda-pruebas \
  --query "ConfigurationSettings[0].OptionSettings[?OptionName=='SecurityGroups']"

If you already made the mistake and have the database inside, there is a way out but it is awkward: snapshot, restore outside the environment, change the connection variable, verify and deploy. With a maintenance window, because the endpoint changes.

The honest decision for MercadoFresco

Here is the heart of the lesson, and the answer has two parts that look contradictory.

Beanstalk would have been an excellent decision in module 2. At that point, MercadoFresco had a Python monolith on one server, a three-person team with no AWS experience and four urgent problems. Beanstalk would have solved two of them — the Friday outages, with auto scaling and a load balancer; and much of the deployment risk, with the immutable policy — in an afternoon instead of across three modules. The cost would have been identical, because Beanstalk charges nothing, and the team would have gained months.

And today it no longer fits. Not because Beanstalk is worse than it was, but because the architecture has changed underneath:

What MercadoFresco has today Why it does not fit in Beanstalk
Five Lambda functions with their triggers Beanstalk does not manage Lambda
Queues, topics, an event bus and a state machine Outside its scope; they would have to be managed separately
Aurora, DynamoDB, Redshift and ElastiCache Outside the environment, with their own infrastructure
Blue/green with a canary and metric-based rollback Beanstalk has neither canary nor metric-based rollback
A VPC designed with six subnets and endpoints Beanstalk can use it, but it does not define it
CloudFront, WAF, Route 53 Outside the environment
Infrastructure as code reviewed in a PR Beanstalk configuration lives in .ebextensions, in the application repo

The web shop is today one piece of an architecture of twenty, and Beanstalk is designed to be the architecture. Putting the shop into a Beanstalk environment would leave 80 % of the system outside, managed with CDK, and would create two infrastructure models coexisting: the worst of both options.

The general criterion, which holds well beyond this case:

Choose Beanstalk if… Avoid it if…
Small team with no infrastructure specialist You already practise IaC and have a mature pipeline
Standard monolithic application in a supported language Distributed architecture with many managed services
In a hurry: you need something in production this week Deployment needs a canary or metric-based rollback
Little infrastructure customisation You need fine control of network, IAM or deployment
Workers consuming from a queue The infrastructure must all be in one model

And there is one concrete case where it does still make sense for MercadoFresco today: the workers. A worker environment pointed at cola-mercadofresco-pedidos would remove the consumption code, the visibility management and the scaling from asg-mercadofresco-trabajadores, in exchange for accepting a second model for one contained piece. Marta notes it down as an option to evaluate, not as a decision taken.

Comparison with CDK, Fargate, App Runner and Amplify

Option Unit Control Up-front work Fit with MercadoFresco
CloudFormation / CDK Resources Total High The chosen one: the whole architecture, one model
Elastic Beanstalk Application Medium Very low A good option in module 2; today, workers only
AWS Fargate (10-02) Container High Medium A serious candidate: no instances to patch
AWS App Runner Container or repository Low Very low Convenient, but no fine network control
AWS Amplify Front-end web application Low Very low Only for the static part and simple APIs

App Runner is, in a way, "the Beanstalk of containers": you give it an image or a repository and it takes care of everything, including scaling from zero requests. It is simpler than Beanstalk and also more limited: reduced network control and fewer deployment options.

Amplify solves a different problem — front-end web applications with hosting, CI/CD and a managed backend — and for MercadoFresco it would be relevant if the catalogue were served as a single-page application, not as a replacement for the shop.

Fargate is the important comparison, and it is developed in 10-02. The essential difference from Beanstalk is what disappears: Beanstalk manages the instances for you, but they still exist, they still need patching and they still take two minutes to boot; Fargate eliminates the instances, and the unit becomes the container. It is exactly the problem this module closes on.

The exit route

A reasonable fear before adopting any PaaS is getting locked in. With Beanstalk the lock-in is moderate, and it is worth knowing why before deciding.

What is not specific to Beanstalk: your code, which is an ordinary WSGI application; the resources it creates, which are standard EC2, ASG and ALB; and the database, if you put it outside. What is specific: the .ebextensions files, the workers' sqsd daemon, the environment variables defined in the environment and the .platform hooks.

The exit route, in five steps and with no service downtime:

  1. Get the state out. If the database was inside, it comes out first. Nothing else moves until that is done.
  2. Translate the configuration. Every option_setting has its equivalent in CDK or CloudFormation, and every hook its place in the CodeDeploy AppSpec or in the launch template's user data.
  3. Stand up the new infrastructure in parallel, with its own ALB and its own target group, with no traffic.
  4. Move traffic gradually with Route 53 (03-05), with weighted routing: 10 %, 50 %, 100 %, watching the metrics at each step. This is what Beanstalk did not offer and what here you can actually do.
  5. Terminate the Beanstalk environment once it has gone a week without traffic.

The general lesson applies to any decision of this kind: lock-in is not measured by the service, but by where the state lives and how much configuration is specific. If the state is outside and the configuration is translatable, starting simple and migrating later is a perfectly reasonable strategy, and almost always better than building the definitive architecture before you have customers.

Cost and cleanup

Beanstalk does not charge: you pay for EC2, the ALB, EBS, traffic and CloudWatch metrics. A typical MercadoFresco production environment — two m6i.large, an ALB, enhanced health — would come to around 165 USD a month, exactly what it would cost built by hand, because they are the same resources.

Component Approximate monthly cost Note
2 × m6i.large on demand ~140 USD The same ones the CDK would create
ALB ~18 USD + traffic One ALB per web server environment
EBS (2 × 20 GB gp3) ~3.2 USD Root volume of each instance
Enhanced health (metrics) ~3 USD Custom metrics in CloudWatch
Beanstalk 0 USD The service does not charge

Two sources of invisible spending: application versions pile up in S3 without being deleted, and the default limit is 1,000 versions, after which deployments fail with an error that does not mention the cause; and forgotten development environments, which cost the same as production ones if they have an ALB. An environment with --single (one instance with an elastic IP and no load balancer) brings a test environment's cost down to under 10 USD a month, and it is the right choice for development.

# Lifecycle policy: keep 50 versions and delete the artefact from S3
aws elasticbeanstalk update-application-resource-lifecycle \
  --application-name tienda-mercadofresco \
  --resource-lifecycle-config 'ServiceRole=arn:aws:iam::111122223333:role/aws-elasticbeanstalk-service-role,VersionLifecycleConfig={MaxCountRule={Enabled=true,MaxCount=50,DeleteSourceFromS3=true}}'

# Cleaning up this lesson's test environment
eb terminate mercadofresco-tienda-pruebas --force
aws elasticbeanstalk describe-environments --application-name tienda-mercadofresco \
  --query 'Environments[?Status!=`Terminated`].[EnvironmentName,Status]' --output table

eb terminate deletes the underlying CloudFormation stack and with it every resource in the environment. If you created the database inside, there it goes.

Common Mistakes and Tips

Mistake: creating the database inside the environment. It is the classic mistake and the most expensive. Tip: RDS and Aurora always outside, connected through environment variables and security groups. The only exception is a throwaway development environment.

Mistake: modifying by hand the resources Beanstalk manages. Changing the ASG from the console seems to work until the environment's next operation overwrites it. Tip: every change through option_settings or eb config.

Mistake: confusing application, version and environment. Many a "my change is not deploying" is really an eb deploy against the wrong environment. Tip: eb status before deploying and eb use to pin the default environment.

Mistake: using "all at once" in production. It is the default on some creation paths and produces a service outage on every deployment. Tip: Immutable or blue/green by CNAME in production; "all at once" only in development.

Mistake: deploying from the laptop with eb deploy. It packages whatever is in the directory and skips the whole pipeline. Tip: have CodeBuild run it with --version against an already built artefact.

Mistake: not enabling enhanced health. Without it, diagnosis stops at "unhealthy" with no cause. Tip: always enable it and build alarms on ApplicationRequests5xx and InstancesSevere.

Tip: save configurations with eb config save. They produce a reusable template with which to create identical environments: it is Beanstalk's way of keeping pre-production and production from diverging.

Tip: if you use worker environments, point them at the existing queue. WorkerQueueURL avoids duplicating queues and lets it coexist with the rest of the 07-01 architecture.

Tip: check /var/log/eb-engine.log before any other log. When a deployment fails for no apparent reason, the cause is usually there, not in the application logs.

Tip: put a version lifecycle policy in place on day one. The 1,000-version limit is reached sooner than you think with an active pipeline, and the error it produces does not say the versions are the problem.

Tip: use --single in development environments. With no load balancer, the cost drops to less than a fifth and you lose nothing relevant for development.

Tip: enable managed platform updates with an explicit window. It is the Beanstalk advantage that saves the most work in the long run, and the one most people leave unconfigured. Tuesday in the small hours, never Friday.

Exercises

Exercise 1: MercadoFresco's workers on Beanstalk

Marta wants to seriously evaluate replacing asg-mercadofresco-trabajadores with a Beanstalk worker environment consuming from cola-mercadofresco-pedidos. Describe how you would set it up: what environment type, how it connects to the existing queue, what changes in the worker code compared with the current receive_message consumption, what happens to the mercadofresco-pedidos-fallidos DLQ and to the idempotency of 07-05, how scaling is configured and what deployment policy you would use. Finish with three arguments for and three against making the change.

Exercise 2: choosing a deployment policy

For each of these four cases, choose a Beanstalk deployment policy and justify it in terms of time, cost, risk and available capacity: (a) a development environment where Luis deploys fifteen times a day; (b) pre-production, where every version is validated before production; (c) production on a Tuesday morning with a version that only changes text; (d) production on a Thursday with a version that changes the database access library and the start-up process. State in which of them rollback is fast and exactly how it is done.

Exercise 3: the decision that was not taken

Imagine MercadoFresco had adopted Beanstalk in module 2, with the monolithic shop in a web server environment and Aurora outside the environment. The eight modules of the course have gone by and the same needs have appeared: queues, Lambdas, CloudFront, WAF, DynamoDB, a pipeline with a canary. Answer: (a) at exactly what point in the course Beanstalk would have started getting in the way, and why; (b) which three concrete needs it could not have covered; (c) whether getting out would have been more expensive than never having gone in; (d) what general rule you draw for choosing a platform's level of abstraction.

Solutions

Solution 1

The set-up. A worker environment (eb create mercadofresco-trabajadores --tier worker) on the same Python platform, in the snet-mercadofresco-app-a/-b subnets of the existing VPC and with the sg-mercadofresco-tienda SG or one of its own able to reach Aurora and ElastiCache. The connection to the existing queue is made through the sqsd options, not by creating a new queue:

# .ebextensions/10-trabajador.config
option_settings:
  aws:elasticbeanstalk:sqsd:
    WorkerQueueURL: https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-pedidos
    HttpPath: /tareas/pedido
    MaxRetries: 5
    VisibilityTimeout: 180
    InactivityTimeout: 120
    HttpConnections: 10
  aws:elasticbeanstalk:environment:
    EnvironmentType: LoadBalanced

What changes in the code. The whole consumption loop disappears: receive_message, delete_message, visibility management, batch control and retry handling. The worker becomes an HTTP handler: it receives a POST on /tareas/pedido with the message body, processes it and returns 200. An error or a timeout translates into a non-2xx code and the message goes back to the queue. It is less code and less error surface, and the instance role only needs read and delete permissions on the queue.

The DLQ and idempotency. The DLQ does not change at all: it is still the retry policy of the SQS queue itself, defined in its RedrivePolicy with maxReceiveCount, and mercadofresco-pedidos-fallidos still receives whatever exhausts the attempts. There is one important nuance: sqsd's MaxRetries and the queue's maxReceiveCount are two different counters, and the lower of the two wins; setting them to inconsistent values produces the classic "messages never reach the DLQ". Idempotency is still the code's responsibility: sqsd provides no exactly-once delivery guarantee, so the mercadofresco-idempotencia table from 07-05 stays exactly as it is.

Scaling and deployment. Scaling on queue depth, not on CPU: an alarm on ApproximateNumberOfMessagesVisible for cola-mercadofresco-pedidos wired to the ASG trigger. That is the right choice for a worker, because CPU can be low while thousands of messages pile up waiting on the database. Deployment policy: immutable: a worker serves no users, so there is no hurry, and in exchange you get clean instances with full validation before they accept load.

Three in favour: infrastructure code disappears from the worker — the consumption loop is where two of the module 7 bugs lived; operating system patches become managed updates; and scaling and health checks come solved out of the box.

Three against: it introduces a second infrastructure model coexisting with the CDK, which is exactly what the previous lesson avoided; the worker configuration gets split between .ebextensions in the application repository and the CDK in mercadofresco-infra, so the source of truth is no longer single; and the HTTP model hides fine-grained batch control, which may matter at the Friday peak.

A reasonable verdict: technically it works well and it is the best fit for Beanstalk at MercadoFresco today, but the argument for a single model weighs more. If the evaluation happens, it should be done against Fargate (10-02), which solves the same thing without opening a second model.

Solution 2

(a) Development, fifteen deployments a day: all at once. It is the fastest — a couple of minutes — it costs no extra instances and the outage does not matter because only Luis suffers it. With fifteen deployments a day, any slower policy adds up to hours lost every week. Rollback: deploy the previous version again; it takes the same time and nothing happens.

(b) Pre-production: immutable. Even though the business risk is nil, pre-production exists to rehearse the production deployment, and rehearsing it with a different policy rehearses nothing. Besides, the immutable policy is the one that catches cold-start problems, which is precisely what you want to find out before production. The extra cost is acceptable because there are few deployments.

(c) Production, Tuesday, text only: rolling with an additional batch. The change is low risk and touches neither dependencies nor start-up, so the extra guarantee of the immutable policy does not justify its time. The additional batch keeps 100 % capacity throughout the process — critical in a shop — in exchange for a single extra instance for a few minutes. Rollback: redeploy the previous version with the same policy; a few minutes.

(d) Production, Thursday, database library and start-up: blue/green by CNAME swap. It is the highest-risk case in the exercise: a change in the start-up process can fail only on new instances, and a change of database library can degrade performance without failing any health check. The green environment lets you run the smoke tests with controlled real traffic and, above all, the blue environment stays alive: the rollback is an eb swap back, in seconds.

And the cross-cutting reading, which links to 08-05: rollback is fast only in (d). In (a), (b) and (c), rolling back means deploying again, that is, minutes with the system in the bad state. That is the real difference between blue/green and everything else, and the reason the policy is chosen by the risk of the change, not by habit. One final detail: eb swap depends on the CNAME TTL, so it is worth lowering it to 60 seconds days before a deployment like that.

Solution 3

(a) Beanstalk would have started getting in the way in module 7, with application integration. Up to that point, the architecture was a web application with a database, and that is exactly what Beanstalk does well; module 3 (VPC, ALB, CloudFront) and module 4 (IAM, WAF) would have coexisted without trouble, because Beanstalk can be deployed into an existing VPC and CloudFront sits in front of any ALB. The turning point is the moment the application stops being a process and becomes a system of coordinated pieces: queues, topics, a bus and a state machine that Beanstalk does not manage. From then on, the environment goes from being "the architecture" to being "one more piece", which is exactly what it is not.

(b) Three uncovered needs: the canary deployment with automatic rollback on business metrics of 08-03 and 08-04, which has no equivalent; the orchestration of the five Lambdas and the state machine of 07-04, completely outside its scope; and the unified, reviewable infrastructure as code of this module, because Beanstalk configuration lives in .ebextensions inside the application repository, mixing two lifecycles that 09-01 recommends keeping apart.

(c) No, getting out would not have been more expensive than never going in. With the database outside — which was the premise of the question — the state is not trapped, and what has to be translated are option_settings and hooks, a job of days, not months. The migration would have been done with Route 53 weighted routing and no service downtime. Against that, the alternative was to have built in module 2 an infrastructure the team did not yet know how to design, for an architecture that did not yet exist: it would probably have been done badly and would have had to be redone anyway, but without having had anything in production in the meantime. The cost of going in and coming out is real, but it is smaller than the cost of the initial paralysis.

(d) The general rule. Choose the highest level of abstraction that covers your current requirements, provided the state stays outside and the configuration is translatable. All three conditions matter: "current requirements" and not imagined ones, because building for a future architecture that may never arrive is the most expensive way to be wrong; "the state outside", because it is the only thing that does not migrate painlessly; and "translatable configuration", because that is what turns a change of platform into a project of days. The rule applies both upwards and downwards: that is why MercadoFresco, which today needs fine control, uses CDK, and for the very same reason in 10-02 it will consider raising the level of abstraction again with Fargate.

Conclusion

Elastic Beanstalk turns a compressed file and a string with the language version into an application in production with a load balancer, auto scaling, security groups, alarms and an instance profile, and it does so by generating a CloudFormation template and deploying a stack — awseb-e-xxxx-stack — under the same rules as 09-01. It is a platform as a service that does not hide the infrastructure: you can SSH in, look at the logs and see the resources, with the flip side that modifying them by hand breaks the model, because Beanstalk overwrites them on the next operation.

You have its five concepts, and in particular the separation between application version — the immutable artefact of 08-02, promoted with eb deploy --version — and environment, which is where all the cost sits. You have worker environments, with sqsd translating SQS messages into HTTP requests to localhost: the consumption loop written by AWS, with the warning that idempotency is still yours and that MaxRetries and maxReceiveCount are two different counters where the lower one wins. And you have customisation with .ebextensions and .platform, with leader_only for migrations and the warning that a slow hook multiplies deployment time by the number of instances.

The five deployment policies cover the full spectrum between speed, cost and risk, and the comparison with 08-03 is the most useful part of the lesson: the immutable one gives real guarantees with nothing to configure and catches the failures that only appear on freshly booted instances; the blue/green CNAME swap is the only thing that allows a rollback in seconds, with the DNS TTL caveat. But there is no canary, no automatic rollback on business metrics and no validation hooks, and that absence is what rules Beanstalk out for a shop with 900 orders an hour on Fridays. Plus the best known trap in the service: the database never inside the environment, because eb terminate takes it with it.

And the honest decision, which has two faces. Beanstalk would have been excellent in module 2 — a monolith, three people with no AWS experience, two of the four problems solved in an afternoon and identical cost, because the service is free. And today it no longer fits, not because it has got worse, but because the web shop went from being the architecture to being one piece of twenty: five Lambdas, four queues, a bus, a state machine, four databases and a pipeline with a canary that would all sit outside the environment, forcing two infrastructure models to be maintained at once. The rule that remains is more valuable than the service: choose the highest level of abstraction that covers your current requirements, provided the state stays outside and the configuration is translatable, because then the exit route is a project of days rather than a sentence.

One problem remains that Beanstalk did not solve either and that none of this module's three lessons has touched yet. All of MercadoFresco's infrastructure — the nine CDK stacks, the three environments, the pipeline that deploys itself — lives in a single account, 111122223333. A misdirected cdk destroy reaches production, the elastic IP and instance quotas are shared between development and production, no IAM policy fully isolates someone who already has broad permissions, and the bill does not really separate what each environment costs. The infrastructure is reproducible, but the blast radius is not contained.

In 09-04, "AWS Organizations", the module closes by attacking exactly that: separate accounts per environment, organisational units, service control policies that limit what can be done even as an administrator, consolidated billing, access with IAM Identity Center so that Marta, Luis and Sara can work across four accounts without multiplying users, and a baseline that deploys itself into every new account with the StackSets of 09-01.

© Copyright 2026. All rights reserved