App-FargateStack

 view release on metacpan or  search on metacpan

lib/App/FargateStack/Pod.pm  view on Meta::CPAN


=item Create a Docker image that implements our worker, web app or API

=item Create a minimal configuration file that describes our application

=item Execute the framework's script and create the necessary AWS infrastructure

=item Launch the http server, daemon, scheduled job, or adhoc worker

=back

Of course, this is only a "good idea" if creating the initial
configuration file is truly minimal, otherwise it becomes an exercise
similar to using Terraform or CloudFormation. So what is the minimum
amount of configuration to inform our framework so it can create our
Fargate worker? How's this for minimal?

 ---
 app:
   name: my-stack
 tasks:
   my-worker:
     type: task
     image: my-worker:latest
     schedule: cron(50 12 * * * *)

I<TIP: You can use the L</create-stack> command to create minimal
configuration files for various Fargate application scenarios.>

Using this minimal configuration and running C<app-FargateStack> like this:

 app-FargateStack plan

...the framework would create the following resources in your VPC:

=over 8

=item * a cluster named C<my-stack-cluster>

=item * a security group for the cluster

=item * an IAM role for the the cluster

=item * an IAM  policy that has permissions enabling your worker

=item * an ECS task definition that describes your task

=item * a CloudWatch log group

=item * an EventBridge target event

=item * an IAM role for EventBridge

=item * an IAM policy for EventBridge

=item * an EventBridge rule that schedules the worker

=back

...so as you can see, rolling all of this by hand could be a daunting
task and one made even more difficult when you decide to use other AWS
resources inside your task like buckets, queues or an EFS file
systems!

=head2 Web Applications

Creating a web application using a minimal configuration works too. To
build a web application you can start with this minimal configuration:

 ---
 app:
   name: my-web-app
 domain: my-web-app.example.com
 tasks:
   apache:
     type: https
     image: my-web-app:latest

This will create an externally facing web application for you with
these resources:

=over 4

=item *  A certificate for your domain

=item * A Fargate cluster

=item * IAM roles and policies

=item * A listener and listener rules

=item * A CloudWatch log group

=item * Security groups

=item * A target group

=item * A task definition

=item * An ALB if one is not detected

=back

Once again, launching a Fargate service requires a
lot of fiddling with AWS resources! Getting all of the plumbing
installed and working requires a lot of what and how knowledge.

=head2 Adding or Changing Resources

Adding or updating resources for an existing application should also
be easy. Updating the infrastructure should just be a matter of
updating the configuration and re-running the framework's script. When
you update the configuration the C<App::FargateStack> will detect the
changes and update the necessary resources.

Currently the framework supports adding a single SQS queue, a single
S3 bucket, volumes using EFS mount points, environment variables and
secrets from AWS Secrets Manager.

 my-worker:
   image: my-worker:latest

lib/App/FargateStack/Pod.pm  view on Meta::CPAN

It generates a default F<web-acl.json> file in your project
directory. This file contains the complete definition of your Web ACL,
including the rules generated from your C<managed_rules> keywords.

=item 2.

It calls C<aws wafv2 create-web-acl> to create a new Web ACL.

=item 3.

It calls C<aws wafv2 associate-web-acl> to link the new Web ACL to
your Application Load Balancer.

=item 4.

It updates your configuration file with the state of the new
WAF resources, including its Name, ID, ARN, LockToken, and a checksum
of the F<web-acl.json> file.

=item 5.

The C<waf> block in your F<fargate-stack.yml> is updated to reflect
the bootstrapped state. If the C<managed_rules> key was not present,
it will be added with the default value of C<[default]>.

=back

=head3 Ongoing Management (Subsequent Runs)

After the initial creation, you take full control of the rules. To
add, remove, or modify rules, you simply edit the F<web-acl.json> file
directly.

On subsequent runs of C<apply>, C<App::FargateStack> will:

=over 4

=item *

Calculate a checksum of your F<web-acl.json> file.

=item *

If the checksum has changed, it will safely update the remote Web ACL
with your new rule set.

=item *

If the checksum has not changed, it will skip the update to avoid
unnecessary API calls.

=back

This model gives you the best of both worlds: the "minimal
configuration, maximum results" of a secure default, and the full
"transparent box" control to customize your security posture as your
application's needs evolve.

=head3 Conflict and Drift Management

The framework includes robust safety checks to prevent accidental data
loss. If it detects that the Web ACL has been modified in the AWS
Console I<and> you have also modified your local F<web-acl.json> file,
it will detect the state conflict, refuse to make any changes, and
provide a clear error message with instructions on how to resolve it.

=head3 Estimated Cost

The default WAF configuration is designed to provide a strong security
baseline while remaining cost-effective. When you enable WAF without
specifying any C<managed_rules>, the framework applies the C<default>
bundle, which includes the C<base> and C<sql> rule sets.

The approximate monthly cost for this default configuration is
B<~$9.00 per month>, plus per-request charges.

The cost is broken down as follows:

=over 4

=item * B<$5.00 / month> for the Web ACL itself.

=item * B<$4.00 / month> for the four AWS Managed Rule Groups included
in the C<default> bundle (3 in 'base', 1 in 'sql').

=item * B<$0.60 / per 1 million requests> processed by the Web ACL.

=back

B<Warning:> Enabling the C<premium> rule set will incur significant
additional monthly and per-request fees for services like Bot Control
and Account Takeover Prevention. Always review the L<AWS WAF
pricing|https://aws.amazon.com/waf/pricing/> page before enabling
premium features.

=head2 Roadmap for HTTP Services

=over 4

=item * path based routing on ALB listeners

=back

=head1 AUTOSCALING

=head2 Overview

For services that experience variable load, such as HTTP applications or
background job processors, C<App::FargateStack> can automate the process of
scaling the number of running tasks up or down to meet demand. This ensures
high availability during traffic spikes and saves costs during quiet periods.

The framework integrates with AWS Application Auto Scaling to provide target
tracking scaling policies. This allows you to define a target metric - such as
average CPU utilization or the number of requests per minute - and the framework
will automatically manage the number of Fargate tasks to keep that metric at
your desired level.

=head2 Enabling Autoscaling

To enable autoscaling for a service, add an C<autoscaling> block to its task
configuration in your .yml configuration file.

tasks:
  my-service:
    # ... other task settings ...
    autoscaling:
      min_capacity: 1
      max_capacity: 10
      cpu: 60

=head2 Configuration Parameters

The C<autoscaling> block accepts the following keys:

=over

=item * B<min_capacity> (Required)

The minimum number of tasks to keep running at all times. The service will
never scale in below this number.

=item * B<max_capacity> (Required)

The maximum number of tasks that the service can scale out to. This acts as
a safeguard to control costs.

=item * B<cpu> OR B<requests> (Required, mutually exclusive)

You must specify exactly one scaling metric.

=over

=item * C<cpu>: The target average CPU utilization percentage across all tasks in
the service. Valid values are between 1 and 100.

=item * C<requests>: The target number of requests per minute for each task. This
is only valid for tasks of type C<http> or C<https> that are behind an
Application Load Balancer.

=back

=item * B<scale_in_cooldown> (Optional)

The amount of time, in seconds, to wait after a scale-in activity before
another scale-in activity can start. This prevents the service from scaling
in too aggressively.

Default: C<300>

=item * B<scale_out_cooldown> (Optional)

The amount of time, in seconds, to wait after a scale-out activity before
another scale-out activity can start. This allows new tasks time to warm up
and start accepting traffic before the service decides to scale out again.

Default: C<60>

=item * B<policy_name> (Managed by CApp::FargateStack)

This is a unique name for the scaling policy generated by the framework. It
is written to your configuration file and used to detect drift between your
configuration and the live environment in AWS. You should not modify this
value.

=back

=head2 Example: Scaling on CPU Utilization

This configuration will maintain at least 1 task, scale up to a maximum of 5
tasks, and will add or remove tasks to keep the average CPU utilization at or
near 60%.

 tasks:
   my-cpu-intensive-worker:
     type: daemon
     image: my-worker:latest
     autoscaling:
       min_capacity: 1
       max_capacity: 5
       cpu: 60

=head2 Example: Scaling on ALB Requests

This configuration will maintain at least 2 tasks, scale up to a maximum of 20
tasks, and will add or remove tasks to keep the number of requests per minute
for each task at or near 1000. It also specifies custom cooldown periods.

 tasks:
   my-website:
     type: https
     image: my-website:latest
     autoscaling:
       min_capacity: 2
       max_capacity: 20
       requests: 1000
       scale_in_cooldown: 600
       scale_out_cooldown: 120

=head2 Scheduled Scaling Configuration

To configure predictive, time-based scaling, add a C<scheduled> block
inside the main C<autoscaling> configuration. This allows you to
define named time windows for scaling.

Example:

 autoscaling:
   ...
   scheduled:
     business_hours:
       start_time: 00:18
       end_time: 00:02
       min_capacity: 2/1
       max_capacity: 3/2

lib/App/FargateStack/Pod.pm  view on Meta::CPAN

=item * B<start_time> (Required): The time to scale up, in HH:MM
format (24-hour clock, UTC).

=item * B<end_time> (Required): The time to scale down, in HH:MM
format (24-hour clock, UTC).

=item * B<days> (Required): The days of the week for the schedule. Can
be a range (e.g., C<MON-FRI>) or comma-separated values.

=item * B<min_capacity> (Optional): The minimum capacity during and
outside the window. The two values should be separated by a slash,
comma, colon, hyphen, or space (e.g., C<2/1> or C<2,1>).

=item * B<max_capacity> (Optional): The maximum capacity during and
outside the window, using the same C<in/out> format as
C<min_capacity>.

=back

=back

The parser will generate two scheduled actions from this block: one to
apply the "in" capacity at the C<start_time> and one to apply the
"out" capacity at the C<end_time>.

=head2 Example: Combined Metric and Scheduled Scaling

This configuration creates a robust scaling strategy. The service will
reactively scale based on CPU load at all times, but the capacity
"guardrails" will be adjusted automatically for business hours.

 tasks:
   my-website:
     type: https
     image: my-website:latest
     autoscaling:
       # Default metric-based scaling policy
       min_capacity: 1
       max_capacity: 10
       cpu: 75
 
       # Scheduled scaling actions to adjust the guardrails
       schedule:
         business_hours:
           start_time: "09:00"
           end_time: "18:00"
           days: MON-FRI
           min_capacity: 2/1
           max_capacity: 10/2

=head2 Drift Detection and Management

CApp::FargateStack treats your YAML configuration as the single source of
truth. On every C<plan> or C<apply> run, it will compare the C<autoscaling>
configuration in your file with the live scaling policy in AWS.

If it detects any differences (e.g., someone manually changed the max capacity
in the AWS Console), it will report the drift and will not apply any changes.
To overwrite the live settings and enforce the configuration from your file,
you must re-run the C<apply> command with the C<--force> flag. This provides a
critical safety check against accidental configuration changes.

=head3 The C<autoscaling> keyword

For any service type (C<https>, C<http>, C<daemon>, or C<task>), you can enable
and configure autoscaling directly from the command line. This provides a
quick-start method to make your service elastic from the moment it's created.

The Cautoscaling: keyword accepts a metric and an optional target value:

=over

=item * B<Enable with a specific target value:>

autoscaling:requests=500
autoscaling:cpu=60

This will enable autoscaling and set the target for either ALB requests per
task or average CPU utilization.

=item * B<Enable with default target value:>

autoscaling:requests
autoscaling:cpu

If you omit the target value, a sensible default will be used (e.g.,
C<500> for requests, C<60> for CPU).

=back

When the C<create-stack> command sees the Cautoscaling: keyword, it
will generate a complete C<autoscaling> block in your F<fargate-stack.yml>
file. This block will be populated with safe defaults (C<min_capacity: 1>,
C<max_capacity: 2>), the specified metric, and all other necessary fields,
making it easy to review and customize later. See L<"AUTOSCALING"> for
a full list of configuration options.

=head1 CURRENT LIMITATIONS

=over 4

=item * Stacks may contain multiple daemon services, but only one task
may be exposed as an HTTP/HTTPS service via an ALB.

=item * Limited configuration options for some resources such as
advanced load balancer listener rules, custom CloudWatch metrics, or
task health check tuning.

=item * Some out of band infrastructure changes may break the ability
to re-run C<app-FargateStack> without manually updating the
configuration

=item * Support for only 1 EFS filesystem per task

=item * This framework assumes that the
L<operatingSystemFamily|https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters_ec2.html#runtime-platform_ec2>
is "LINUX" and the C<cpuArchitecture> is "X86_64" LINUX. This is
unlikely to change.

=back

=head1 TROUBLESHOOTING

=head2 Warning: task placed in a public subnet

When running a task you may see:

  [2025/08/05 03:40:58] run-task: subnet-id: [subnet-7c160c37] is in a public subnet...consider running your jobs in a private subnet

This means the task is being scheduled in a subnet that has a
0.0.0.0/0 route to an Internet Gateway (a public subnet).

While not fatal, placing tasks in public subnets is discouraged unless
you have a specific need.

=head3 Why this matters

Running tasks in public subnets can introduce risk and operational
surprises:

=over 4

=item * Accidental exposure

If the task is assigned a public IP and the security group allows
inbound access, it may be reachable from the internet.

=item * Unintended dependency

Public-subnet egress typically relies on a public IP and the Internet
Gateway. That can bypass intended egress controls, logging, or central
inspection.

=item * Narrow security margin

Safety depends entirely on security groups and NACLs. A small
misconfiguration can expose services or data.

=back

=head3 Recommended pattern

Use private subnets for most Fargate workloads. Private subnets do not
route directly to the internet.

If the task needs outbound access (for example, to pull images from
ECR or call external APIs), use one of:

=over 4

=item * A NAT Gateway (private subnet egress to the internet)

=item * VPC interface endpoints for ECR (ecr.api and ecr.dkr) and a
gateway endpoint for S3, so image pulls stay inside the VPC with no
public IPs

=back

For public-facing applications, the common pattern is: tasks in
private subnets, fronted by a public Application Load Balancer in
public subnets.

=head3 When is a public subnet acceptable?

Use a public subnet only when the task itself must have a public IP
and terminate client connections directly (uncommon). If you do:

=over 4

=item * Set assignPublicIp=ENABLED so the task can reach the internet
via the Internet Gateway

=item * Keep security groups locked down and monitor egress on TCP 443

=back

=head3 Note on image pulls

To pull from ECR, the task needs a path to ECR API, ECR DKR, and S3:

=over 4

=item * Public subnet: requires a public IP (assignPublicIp=ENABLED),



( run in 0.506 second using v1.01-cache-2.11-cpan-df04353d9ac )