App-FargateStack
view release on metacpan or search on metacpan
lib/App/FargateStack/Checker.pm view on Meta::CPAN
package App::FargateStack::Checker;
use strict;
use warnings;
use App::FargateStack::Builder::Utils qw(choose);
use Carp;
use Carp::Always;
use CLI::Simple::Constants qw(:booleans %LOG_LEVELS);
use Data::Dumper;
use English qw(-no_match_vars);
use Getopt::Long qw(GetOptions);
use List::Util qw(any none uniq);
use Text::ASCIITable::EasyTable;
use Sub::Util qw(subname);
use parent qw(CLI::Simple);
__PACKAGE__->use_log4perl( log_level => 'info' );
caller or __PACKAGE__->main();
########################################################################
sub check_fargate_env {
########################################################################
my ($self) = @_;
my @rows;
push @rows, check( $self, \&check_credentials );
push @rows, check( $self, \&check_service_linked_roles );
push @rows, check( $self, \&check_vpc_and_subnets );
push @rows, check( $self, \&check_egress );
push @rows, check( $self, \&check_ecs_permissions );
push @rows, check( $self, \&check_elbv2_permissions );
push @rows, check( $self, \&check_logs_permissions );
push @rows, check( $self, \&check_ecr_access );
push @rows, check( $self, \&check_events_permissions );
push @rows, check( $self, \&check_passrole );
if ( $self->get_dns ) { push @rows, check_route53($self); }
if ( $self->get_https ) { push @rows, check_acm($self); }
if ( $self->get_secrets ) { push @rows, check_secrets_hint($self); }
my $exit = render_table( $self, \@rows );
my ( $caps, $m ) = summarize_capabilities( \@rows );
# Temporary debug to stderr
my %sets = (
'HTTP services' =>
[ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'ELBv2 perms', 'CloudWatch Logs perms', 'iam:PassRole' ],
'HTTPS service' =>
[ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'ELBv2 perms', 'CloudWatch Logs perms', 'iam:PassRole', 'ACM' ],
'Scheduled tasks' =>
[ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'Events perms', 'CloudWatch Logs perms', 'iam:PassRole' ],
'One-shot tasks' => [ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'CloudWatch Logs perms', 'iam:PassRole' ],
'Daemon services' => [ 'Credentials', 'VPC/Subnets', 'Egress', 'ECS perms', 'CloudWatch Logs perms', 'iam:PassRole' ],
);
foreach my $cap ( sort keys %sets ) {
my @missing = grep { !exists $m->{$_} } @{ $sets{$cap} };
if (@missing) {
print {*STDERR} sprintf "capability[%s] missing checks: %s\n", $cap, join q{, }, @missing;
}
}
render_capabilities( $caps, $self->get_account, $self->get_region );
return $exit;
}
########################################################################
sub check {
########################################################################
my ( $self, $sub ) = @_;
my ( undef, $name ) = split /_/xsm, subname($sub);
$self->get_logger->info( sprintf 'checking %s...', $name );
return $sub->($self);
}
my %clients;
########################################################################
sub new_client {
########################################################################
my ( $class, @args ) = @_;
lib/App/FargateStack/Checker.pm view on Meta::CPAN
$err = $EVAL_ERROR || 'unknown error';
};
return ( !!$out, $out, $err );
}
########################################################################
sub render_table {
########################################################################
my ( $self, $rows ) = @_;
my $title = sprintf 'Fargate environment preflight (profile: [%s] region: [%s], Route53 profile: [%s])',
$self->get_profile, $self->get_region, $self->get_route53_profile;
my $exit_code = 0;
foreach my $r ( @{$rows} ) {
if ( $r->{Status} eq 'FAIL' ) {
$exit_code = 2;
}
if ( $r->{Status} eq 'WARN' && $exit_code == 0 ) {
$exit_code = 1;
}
}
print {*STDOUT} easy_table(
table_options => { headingText => $title },
columns => [qw(Check Status Detail)],
data => $rows,
);
return $exit_code;
}
########################################################################
sub row_ok { return { Check => $_[0], Status => 'PASS', Detail => $_[1] // q{} }; }
sub row_warn { return { Check => $_[0], Status => 'WARN', Detail => $_[1] // q{} }; }
sub row_fail { return { Check => $_[0], Status => 'FAIL', Detail => $_[1] // q{} }; }
########################################################################
########################################################################
sub check_events_permissions {
########################################################################
my ($opt) = @_;
my $events = new_client( 'App::Events', $opt );
# Read-only probe; no mutation. Either of these is fine:
# list-event-buses => broad, minimal perms
# list-rules => stricter, proves rule visibility
my ( $ok, $buses, $err ) = try_aws( sub { $events->command( 'list-event-buses' => [ '--query' => 'EventBuses[].Name' ] ) } );
if ( !$ok ) {
return row_fail( 'Events perms', 'Cannot list event buses' );
}
return row_ok( 'Events perms', 'Describe OK' );
}
########################################################################
sub check_credentials {
########################################################################
my ($opt) = @_;
my $sts = new_client( 'App::STS', $opt );
my ( $ok, $out, $err ) = try_aws( sub { $sts->get_caller_identity() } );
if ( !$ok || !$out ) {
return row_fail( 'Credentials', 'Cannot call STS GetCallerIdentity' );
}
my $acct = $out->{Account} || q{};
my $arn = $out->{Arn} || q{};
$opt->set_account($acct);
if ( !$acct ) {
return row_fail( 'Credentials', 'Missing account in STS response' );
}
return row_ok( 'Credentials', "Account $acct, Principal $arn" );
}
########################################################################
sub check_service_linked_roles {
########################################################################
my ($opt) = @_;
my $iam = new_client( 'App::IAM', $opt );
my @required = ('AWSServiceRoleForECS'); # core ECS control plane
my @advisory = ('AWSServiceRoleForElasticLoadBalancing'); # only if you use ALB/NLB
# If you intend to use service autoscaling, include this:
if ( $opt->{check_scaling} ) {
push @advisory, 'AWSServiceRoleForApplicationAutoScaling_ECSService';
}
my @missing_req;
my @missing_adv;
foreach my $r (@required) {
my ($ok) = try_aws( sub { $iam->command( 'get-role' => [ '--role-name' => $r ] ) } );
if ( !$ok ) {
push @missing_req, $r;
}
}
foreach my $r (@advisory) {
my ($ok) = try_aws( sub { $iam->command( 'get-role' => [ '--role-name' => $r ] ) } );
if ( !$ok ) {
push @missing_adv, $r;
}
}
if ( @missing_req && @missing_adv ) {
return row_warn( 'Service-linked roles',
'Missing required: ' . join( ', ', @missing_req ) . ' ; advisory missing: ' . join( ', ', @missing_adv ) );
}
lib/App/FargateStack/Checker.pm view on Meta::CPAN
my $cli = App::FargateStack::Checker->new(
commands => \%commands,
default_options => \%default_options,
option_specs => \@option_specs,
extra_options => [qw(account global_options role_names route53_profile)],
);
my @expected_roles = (
# Execution role and task role your builder will create
'FargateStack/my-svc/TaskExecutionRole',
'FargateStack/my-svc/TaskRole',
'FargateStack/my-svc/EventsInvokeRole',
);
$cli->set_role_names( \@expected_roles );
$cli->set_global_options(
{ profile => $cli->get_profile,
region => $cli->get_region
}
);
return $cli->run();
}
1;
__END__
=pod
=head1 NAME
app-FargateStack-env.pl - Preflight checker for ECS Fargate environments
=head1 USAGE
app-FargateStack-env.pl [options]
=head1 DESCRIPTION
Runs read-only checks against the target AWS account and region to verify
that common ECS Fargate deployment scenarios are feasible. Produces an
ASCII table with PASS/WARN/FAIL rows and a capabilities summary for:
- HTTP services
- HTTPS service
- Scheduled tasks
- One-shot tasks
- Daemon services
No resources are created or modified. Intended as a fast âcan I deploy here?â
probe for humans and CI.
=head2 Options
=over 4
=item B<--profile> I<STR>
AWS config/credentials profile to use. Defaults to C<$ENV{AWS_PROFILE}> or the
SDKâs default behavior if unset.
=item B<--region> I<STR>
AWS region to target (e.g. C<us-east-1>). Defaults to C<$ENV{AWS_REGION}> if set.
=item B<--dns> | B<--no-dns>
Enable or disable Route 53 checks. Default: B<enabled>.
Use B<--no-dns> (or B<--nodns>) to skip DNS checks.
=item B<--dns-profile> I<STR>
Alternate AWS profile for Route 53 lookups. Useful when DNS is managed in a
separate account. Falls back to C<--profile> if not provided.
=item B<--https> | B<--no-https>
Enable or disable ACM certificate checks (same region as the load balancer).
Default: B<disabled>. Turn on if you plan to deploy HTTPS.
=item B<--secrets> | B<--no-secrets>
Enable or disable Secrets Manager reachability checks. Default: B<disabled>.
When enabled, the checker verifies control-plane reachability (e.g., VPC
endpoint present or NAT available). It does not validate individual
C<GetSecretValue> permissions for task roles.
=back
=head1 OUTPUT
The main table includes rows like:
Credentials
Service-linked roles
VPC/Subnets
Egress
ECS perms
ELBv2 perms
CloudWatch Logs perms
ECR
Events perms
iam:PassRole
Route 53
ACM
Secrets Manager
Each row has a Status of B<PASS>, B<WARN>, or B<FAIL> and a Detail string.
Typical examples:
- Egress: PASS with NAT present; missing VPC endpoints are called out as optional.
- Events perms: PASS when EventBridge APIs are readable (e.g., list-event-buses).
- iam:PassRole: PASS when simulation allows passing target roles to
C<ecs-tasks.amazonaws.com> and C<events.amazonaws.com>.
=head2 CAPABILITIES SUMMARY
After the table, a summary lists readiness for common Fargate scenarios:
( run in 0.834 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )